diff --git a/doc/src/content/docs/reference/languages/elixir.mdx b/doc/src/content/docs/reference/languages/elixir.mdx new file mode 100644 index 0000000..5e3cd2b --- /dev/null +++ b/doc/src/content/docs/reference/languages/elixir.mdx @@ -0,0 +1,226 @@ +--- +title: Elixir Language Support +description: Protocol Buffer and gRPC support for Elixir applications, enabling distributed systems and real-time services. +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; +import { Code } from "astro:components"; +import basicConfig from "./elixir.x-basic-configuration.nix?raw"; + +# Elixir Language Support + +**Status**: ✅ Full Support +**Examples**: +- [`examples/elixir-basic/`](https://github.com/conneroisu/bufr.nix/tree/main/examples/elixir-basic) - Basic protobuf messages +- [`examples/elixir-grpc/`](https://github.com/conneroisu/bufr.nix/tree/main/examples/elixir-grpc) - gRPC services + +Elixir support provides Protocol Buffer and gRPC integration for building distributed systems, real-time applications, and microservices. + +## Available Plugins + +| Plugin | Description | Generated Files | +| ----------------------- | --------------------- | ------------------------ | +| **`protoc-gen-elixir`** | Base messages & gRPC | `*.pb.ex` | + +## Configuration + +### Basic Configuration + +```nix +languages.elixir = { + enable = true; + outputPath = "lib/proto"; +}; +``` + +### Full Configuration + + + +## Features + +### Message Generation + +Generates Elixir modules for all protobuf messages with: +- Full type safety using structs +- Binary encoding/decoding +- JSON serialization support +- Default values and field presence tracking + +### gRPC Support + +When enabled, generates: +- Service modules with server behavior +- Client stubs for service calls +- Support for all RPC types (unary, streaming, bidirectional) +- Error handling with proper gRPC status codes + +### Validation Support + +Provides hooks for integrating with validation libraries: +- Field validation rules +- Custom validation functions +- Integration with Ecto changesets + +## Usage Example + +### Basic Message Usage + +```elixir +# Create a message +message = %MyApp.Proto.User{ + id: 1, + name: "Alice", + email: "alice@example.com", + roles: ["admin", "user"] +} + +# Encode to binary +binary = MyApp.Proto.User.encode(message) + +# Decode from binary +{:ok, decoded} = MyApp.Proto.User.decode(binary) +``` + +### gRPC Server Implementation + +```elixir +defmodule MyApp.UserService.Server do + use GRPC.Server, service: MyApp.Proto.UserService.Service + + def get_user(request, _stream) do + user = MyApp.Users.find(request.id) + %MyApp.Proto.GetUserResponse{user: user} + end + + def list_users(request, stream) do + MyApp.Users.list() + |> Stream.map(&%MyApp.Proto.User{&1}) + |> Enum.each(&GRPC.Server.send_reply(stream, &1)) + end +end +``` + +### gRPC Client Usage + +```elixir +# Connect to server +{:ok, channel} = GRPC.Stub.connect("localhost:50051") + +# Make RPC call +request = %MyApp.Proto.GetUserRequest{id: 123} +{:ok, response} = MyApp.Proto.UserService.Stub.get_user(channel, request) +``` + +## Integration with Phoenix + +Elixir protobuf integrates well with Phoenix applications: + +```elixir +defmodule MyAppWeb.ProtoController do + use MyAppWeb, :controller + + def show(conn, %{"id" => id}) do + user = MyApp.Users.get(id) + proto = %MyApp.Proto.User{user} + + conn + |> put_resp_content_type("application/x-protobuf") + |> send_resp(200, MyApp.Proto.User.encode(proto)) + end +end +``` + +## Mix.exs Dependencies + +Add these dependencies to your `mix.exs`: + +```elixir +defp deps do + [ + {:protobuf, "~> 0.12.0"}, + {:grpc, "~> 0.7.0"}, # If using gRPC + {:jason, "~> 1.4"} # For JSON support + ] +end +``` + +## Configuration Options + +### `languages.elixir` + +| Option | Type | Default | Description | +| ---------------- | ------------------- | ------------- | ---------------------------------------------- | +| `enable` | `bool` | `false` | Enable Elixir code generation | +| `package` | `package` | `protoc-gen-elixir` | The protoc plugin package | +| `outputPath` | `string \| [string]`| `"lib"` | Output directory for generated code | +| `options` | `[string]` | `[]` | Options to pass to protoc-gen-elixir | +| `namespace` | `string` | `""` | Module namespace prefix (e.g., "MyApp.Proto") | +| `files` | `[string] \| null` | `null` | Specific proto files for this language | +| `additionalFiles`| `[string]` | `[]` | Additional proto files to compile | + +### `languages.elixir.grpc` + +| Option | Type | Default | Description | +| --------- | ---------- | -------------------- | -------------------------------- | +| `enable` | `bool` | `false` | Enable gRPC code generation | +| `package` | `package` | `protoc-gen-elixir` | The protoc plugin package | +| `options` | `[string]` | `[]` | Options for gRPC generation | + +### `languages.elixir.validate` + +| Option | Type | Default | Description | +| --------- | ---------- | ------- | -------------------------------------- | +| `enable` | `bool` | `false` | Enable validation support | +| `package` | `package` | `null` | Validation package (if any) | +| `options` | `[string]` | `[]` | Options for validation generation | + +## Tips and Best Practices + +1. **Module Organization**: Use the `namespace` option to organize generated modules under your application's namespace. + +2. **OTP Integration**: Generated gRPC servers integrate with OTP supervision trees: + ```elixir + children = [ + {GRPC.Server.Supervisor, endpoint: MyApp.Endpoint, port: 50051} + ] + ``` + +3. **Error Handling**: Use proper gRPC status codes: + ```elixir + raise GRPC.RPCError, status: :not_found, message: "User not found" + ``` + +4. **Testing**: Use the generated modules in tests: + ```elixir + test "encodes and decodes messages" do + original = %MyApp.Proto.User{id: 1, name: "Test"} + binary = MyApp.Proto.User.encode(original) + {:ok, decoded} = MyApp.Proto.User.decode(binary) + assert decoded == original + end + ``` + +5. **Performance**: For high-performance scenarios, consider using binary pattern matching directly on encoded messages. + +## Common Issues + +### Module Not Found + +If you get "module not found" errors after generation: +1. Ensure the `outputPath` is in your Elixir project's lib path +2. Run `mix compile` to compile the generated modules +3. Check that the namespace matches your project structure + +### gRPC Connection Issues + +For gRPC connection problems: +1. Verify the server is running on the correct port +2. Check firewall settings +3. Ensure both client and server use the same proto definitions + +## See Also + +- [Elixir Protobuf Library](https://github.com/elixir-protobuf/protobuf) +- [Elixir gRPC](https://github.com/elixir-grpc/grpc) +- [Phoenix Framework Integration](https://hexdocs.pm/phoenix) \ No newline at end of file diff --git a/doc/src/content/docs/reference/languages/elixir.x-basic-configuration.nix b/doc/src/content/docs/reference/languages/elixir.x-basic-configuration.nix new file mode 100644 index 0000000..229b3d9 --- /dev/null +++ b/doc/src/content/docs/reference/languages/elixir.x-basic-configuration.nix @@ -0,0 +1,29 @@ +languages.elixir = { + enable = true; + outputPath = "lib/proto"; + namespace = "MyApp.Proto"; + options = []; + + # Enable gRPC service generation + grpc = { + enable = true; + options = []; + }; + + # Enable validation support + validate = { + enable = true; + options = []; + }; + + # Compile specific proto files for Elixir + files = [ + "./proto/services/v1/user_service.proto" + "./proto/messages/v1/common.proto" + ]; + + # Additional proto files beyond the global list + additionalFiles = [ + "./proto/internal/v1/admin.proto" + ]; +}; \ No newline at end of file diff --git a/doc/src/content/docs/reference/languages/index.mdx b/doc/src/content/docs/reference/languages/index.mdx index ae2760d..2d94f0f 100644 --- a/doc/src/content/docs/reference/languages/index.mdx +++ b/doc/src/content/docs/reference/languages/index.mdx @@ -32,6 +32,11 @@ All examples shown are fully functional and can be found in the [`examples/`](ht Documentation →](./dart) + + Protocol Buffer and gRPC support for distributed systems and real-time applications. [View Elixir + Documentation →](./elixir) + + Multiple generation options including betterproto, mypy stubs, and gRPC support. [View Python Documentation →](./python) diff --git a/examples/elixir-basic/README.md b/examples/elixir-basic/README.md new file mode 100644 index 0000000..20eac3c --- /dev/null +++ b/examples/elixir-basic/README.md @@ -0,0 +1,91 @@ +# Elixir Basic Example + +This example demonstrates basic Protocol Buffer code generation for Elixir using Bufrnix. + +## Features + +- Basic protobuf message generation +- Nested messages +- Enums +- Maps +- Oneof fields +- Repeated fields + +## Prerequisites + +- Nix with flakes enabled +- Elixir development environment (provided by the flake) + +## Usage + +### Generate Protobuf Code + +```bash +# Generate the protobuf code +nix run + +# Or enter the development shell and generate +nix develop +nix run +``` + +### Use in Elixir Project + +After generation, the protobuf modules will be available in `lib/proto/`. You can use them in your Elixir code: + +```elixir +# Create a message +message = %Proto.Example.V1.ExampleMessage{ + id: 1, + name: "Alice", + email: "alice@example.com", + tags: ["elixir", "protobuf"], + created_at: %Proto.Example.V1.TimestampMessage{ + seconds: System.system_time(:second), + nanos: 0 + } +} + +# Encode to binary +binary = Proto.Example.V1.ExampleMessage.encode(message) + +# Decode from binary +decoded = Proto.Example.V1.ExampleMessage.decode(binary) +``` + +### Run the Example + +```bash +# Enter the development shell +nix develop + +# Install dependencies +mix deps.get + +# Generate protobuf code +nix run + +# Run the example +iex -S mix +iex> ExampleUsage.demo() +``` + +## Generated Files + +After running `nix run`, you'll find the generated Elixir code in: + +- `lib/proto/example/v1/example.pb.ex` - Generated protobuf modules + +## Project Structure + +``` +. +├── flake.nix # Nix flake configuration +├── mix.exs # Elixir project file +├── proto/ # Protocol buffer definitions +│ └── example/v1/ +│ └── example.proto +└── lib/ # Elixir source code + ├── proto/ # Generated protobuf code (after running nix run) + └── example_usage.ex # Example usage code +``` \ No newline at end of file diff --git a/examples/elixir-basic/flake.lock b/examples/elixir-basic/flake.lock new file mode 100644 index 0000000..34adfb8 --- /dev/null +++ b/examples/elixir-basic/flake.lock @@ -0,0 +1,113 @@ +{ + "nodes": { + "bufrnix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "path": "../..", + "type": "path" + }, + "original": { + "path": "../..", + "type": "path" + }, + "parent": [] + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1747958103, + "narHash": "sha256-qmmFCrfBwSHoWw7cVK4Aj+fns+c54EBP8cGqp/yK410=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "fe51d34885f7b5e3e7b59572796e1bcb427eccb1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1757487488, + "narHash": "sha256-zwE/e7CuPJUWKdvvTCB7iunV4E/+G0lKfv4kk/5Izdg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ab0f3607a6c7486ea22229b92ed2d355f1482ee0", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "bufrnix": "bufrnix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1749194973, + "narHash": "sha256-eEy8cuS0mZ2j/r/FE0/LYBSBcIs/MKOIVakwHVuqTfk=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "a05be418a1af1198ca0f63facb13c985db4cb3c5", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/examples/elixir-basic/flake.nix b/examples/elixir-basic/flake.nix new file mode 100644 index 0000000..8768b2f --- /dev/null +++ b/examples/elixir-basic/flake.nix @@ -0,0 +1,49 @@ +{ + description = "Basic Elixir example for bufrnix"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + # bufrnix.url = "github:conneroisu/bufrnix"; + bufrnix.url = "path:../.."; + bufrnix.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + bufrnix, + }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + in { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + elixir + erlang + protobuf + ]; + }; + packages = { + default = bufrnix.lib.mkBufrnixPackage { + inherit pkgs; + config = { + root = ./.; + protoc = { + sourceDirectories = ["./proto"]; + includeDirectories = ["./proto"]; + files = ["./proto/example/v1/example.proto"]; + }; + languages.elixir = { + enable = true; + package = pkgs.protoc-gen-elixir; + outputPath = "lib/proto"; + namespace = "Proto"; + options = []; + }; + }; + }; + }; + }); +} \ No newline at end of file diff --git a/examples/elixir-basic/lib/example_usage.ex b/examples/elixir-basic/lib/example_usage.ex new file mode 100644 index 0000000..1f6e66e --- /dev/null +++ b/examples/elixir-basic/lib/example_usage.ex @@ -0,0 +1,65 @@ +defmodule ExampleUsage do + @moduledoc """ + Example usage of generated Protobuf modules. + + This module demonstrates how to use the Protobuf code generated by Bufrnix. + """ + + # Import the generated modules (these will exist after running bufrnix) + # alias Proto.Example.V1.ExampleMessage + # alias Proto.Example.V1.TimestampMessage + # alias Proto.Example.V1.StatusMessage + # alias Proto.Example.V1.ConfigMessage + # alias Proto.Example.V1.NotificationMessage + + def demo do + IO.puts("Bufrnix Elixir Example") + IO.puts("======================") + IO.puts("") + IO.puts("This example demonstrates Protocol Buffer code generation for Elixir.") + IO.puts("") + IO.puts("To generate the protobuf code:") + IO.puts(" 1. Run: nix run") + IO.puts(" 2. The generated code will be in lib/proto/") + IO.puts("") + IO.puts("Once generated, you can use the modules like:") + IO.puts(" message = %Proto.Example.V1.ExampleMessage{") + IO.puts(" id: 1,") + IO.puts(" name: \"Test User\",") + IO.puts(" email: \"test@example.com\",") + IO.puts(" tags: [\"elixir\", \"protobuf\"]") + IO.puts(" }") + IO.puts("") + IO.puts(" # Encode to binary") + IO.puts(" binary = Proto.Example.V1.ExampleMessage.encode(message)") + IO.puts("") + IO.puts(" # Decode from binary") + IO.puts(" decoded = Proto.Example.V1.ExampleMessage.decode(binary)") + end + + # Example functions that will work after code generation + def create_example_message do + # Uncomment after generation: + # %Proto.Example.V1.ExampleMessage{ + # id: 1, + # name: "Alice", + # email: "alice@example.com", + # tags: ["developer", "elixir"], + # description: "An example user", + # created_at: %Proto.Example.V1.TimestampMessage{ + # seconds: System.system_time(:second), + # nanos: 0 + # } + # } + {:ok, "Example message will be created after protobuf generation"} + end + + def encode_decode_example do + # Uncomment after generation: + # message = create_example_message() + # binary = Proto.Example.V1.ExampleMessage.encode(message) + # decoded = Proto.Example.V1.ExampleMessage.decode(binary) + # IO.inspect(decoded, label: "Decoded message") + {:ok, "Encode/decode will work after protobuf generation"} + end +end \ No newline at end of file diff --git a/examples/elixir-basic/lib/proto/.formatter.exs b/examples/elixir-basic/lib/proto/.formatter.exs new file mode 100644 index 0000000..3a43859 --- /dev/null +++ b/examples/elixir-basic/lib/proto/.formatter.exs @@ -0,0 +1,4 @@ +[ + inputs: ["*.{ex,exs}", "{lib,test}/**/*.{ex,exs}"], + line_length: 120 +] diff --git a/examples/elixir-basic/lib/proto/example/v1/example.pb.ex b/examples/elixir-basic/lib/proto/example/v1/example.pb.ex new file mode 100644 index 0000000..e105bb6 --- /dev/null +++ b/examples/elixir-basic/lib/proto/example/v1/example.pb.ex @@ -0,0 +1,89 @@ +defmodule Example.V1.Status do + @moduledoc false + + use Protobuf, enum: true, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :STATUS_UNSPECIFIED, 0 + field :STATUS_ACTIVE, 1 + field :STATUS_INACTIVE, 2 + field :STATUS_PENDING, 3 + field :STATUS_ARCHIVED, 4 +end + +defmodule Example.V1.ExampleMessage do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :id, 1, type: :int32 + field :name, 2, type: :string + field :email, 3, type: :string + field :tags, 4, repeated: true, type: :string + field :description, 5, proto3_optional: true, type: :string + field :created_at, 6, type: Example.V1.TimestampMessage, json_name: "createdAt" +end + +defmodule Example.V1.TimestampMessage do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :seconds, 1, type: :int64 + field :nanos, 2, type: :int32 +end + +defmodule Example.V1.StatusMessage do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :id, 1, type: :int32 + field :status, 2, type: Example.V1.Status, enum: true + field :message, 3, type: :string +end + +defmodule Example.V1.ConfigMessage.SettingsEntry do + @moduledoc false + + use Protobuf, map: true, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :string +end + +defmodule Example.V1.ConfigMessage.ExamplesByIdEntry do + @moduledoc false + + use Protobuf, map: true, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :key, 1, type: :int32 + field :value, 2, type: Example.V1.ExampleMessage +end + +defmodule Example.V1.ConfigMessage do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :settings, 1, repeated: true, type: Example.V1.ConfigMessage.SettingsEntry, map: true + + field :examples_by_id, 2, + repeated: true, + type: Example.V1.ConfigMessage.ExamplesByIdEntry, + json_name: "examplesById", + map: true +end + +defmodule Example.V1.NotificationMessage do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + oneof :notification, 0 + + field :email, 1, type: :string, oneof: 0 + field :sms, 2, type: :string, oneof: 0 + field :push, 3, type: :string, oneof: 0 + field :content, 4, type: :string + field :sent_at, 5, type: Example.V1.TimestampMessage, json_name: "sentAt" +end diff --git a/examples/elixir-basic/mix.exs b/examples/elixir-basic/mix.exs new file mode 100644 index 0000000..a83ab91 --- /dev/null +++ b/examples/elixir-basic/mix.exs @@ -0,0 +1,28 @@ +defmodule ElixirBasicExample.MixProject do + use Mix.Project + + def project do + [ + app: :elixir_basic_example, + version: "0.1.0", + elixir: "~> 1.14", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:protobuf, "~> 0.12.0"}, + {:jason, "~> 1.4"} + ] + end +end \ No newline at end of file diff --git a/examples/elixir-basic/proto/example/v1/example.proto b/examples/elixir-basic/proto/example/v1/example.proto new file mode 100644 index 0000000..ff2b224 --- /dev/null +++ b/examples/elixir-basic/proto/example/v1/example.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +package example.v1; + +// Example message for demonstrating Elixir protobuf generation +message ExampleMessage { + int32 id = 1; + string name = 2; + string email = 3; + repeated string tags = 4; + optional string description = 5; + TimestampMessage created_at = 6; +} + +// Nested message to demonstrate complex types +message TimestampMessage { + int64 seconds = 1; + int32 nanos = 2; +} + +// Enum to demonstrate enum generation +enum Status { + STATUS_UNSPECIFIED = 0; + STATUS_ACTIVE = 1; + STATUS_INACTIVE = 2; + STATUS_PENDING = 3; + STATUS_ARCHIVED = 4; +} + +// Message with enum field +message StatusMessage { + int32 id = 1; + Status status = 2; + string message = 3; +} + +// Message with map field +message ConfigMessage { + map settings = 1; + map examples_by_id = 2; +} + +// Message with oneof field +message NotificationMessage { + oneof notification { + string email = 1; + string sms = 2; + string push = 3; + } + string content = 4; + TimestampMessage sent_at = 5; +} \ No newline at end of file diff --git a/examples/elixir-grpc/README.md b/examples/elixir-grpc/README.md new file mode 100644 index 0000000..f239c8a --- /dev/null +++ b/examples/elixir-grpc/README.md @@ -0,0 +1,135 @@ +# Elixir gRPC Example + +This example demonstrates gRPC service generation for Elixir using Bufrnix. + +## Features + +- gRPC service definition and implementation +- Server and client code examples +- Unary RPC methods +- Server streaming RPC +- Bidirectional streaming RPC +- Error handling with gRPC status codes + +## Prerequisites + +- Nix with flakes enabled +- Elixir development environment (provided by the flake) + +## Usage + +### Generate gRPC Code + +```bash +# Generate the protobuf and gRPC code +nix run + +# Or enter the development shell and generate +nix develop +nix run +``` + +### Run the gRPC Server + +```bash +# Enter the development shell +nix develop + +# Install dependencies +mix deps.get + +# Generate protobuf code +nix run + +# Start the gRPC server (runs on port 50051) +iex -S mix +``` + +### Test with Client + +In another terminal: + +```bash +# Enter the development shell +nix develop + +# Start an IEx session +iex -S mix + +# Connect to the server +iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051") + +# Create a user (after uncommenting the generated code) +iex> request = %Proto.Example.V1.CreateUserRequest{ +...> username: "john", +...> email: "john@example.com", +...> full_name: "John Doe" +...> } +iex> {:ok, response} = Proto.Example.V1.UserService.Stub.create_user(channel, request) + +# List users +iex> request = %Proto.Example.V1.ListUsersRequest{page_size: 10} +iex> {:ok, response} = Proto.Example.V1.UserService.Stub.list_users(channel, request) +``` + +## Generated Files + +After running `nix run`, you'll find the generated code in: + +- `lib/proto/example/v1/service.pb.ex` - Protobuf message definitions +- `lib/proto/example/v1/service_grpc.pb.ex` - gRPC service and stub definitions + +## Project Structure + +``` +. +├── flake.nix # Nix flake configuration +├── mix.exs # Elixir project file +├── proto/ # Protocol buffer definitions +│ └── example/v1/ +│ └── service.proto # gRPC service definition +└── lib/ + ├── proto/ # Generated code (after nix run) + └── elixir_grpc_example/ + ├── application.ex # OTP application + ├── endpoint.ex # gRPC endpoint configuration + ├── user_service_server.ex # Server implementation + └── client.ex # Client example code +``` + +## Implementation Notes + +1. After running `nix run`, you'll need to uncomment the code in: + - `user_service_server.ex` - Server implementation + - `client.ex` - Client code + - `endpoint.ex` - Add the service to the endpoint + +2. The example includes a simple in-memory storage for demonstration purposes. + +3. Error handling is demonstrated using `GRPC.RPCError` for proper gRPC status codes. + +## Advanced Features + +### Streaming + +The example includes streaming RPCs: +- `WatchUsers` - Server streaming for real-time updates +- `BatchProcess` - Bidirectional streaming for batch operations + +### Authentication + +For production use, you would typically add authentication interceptors: + +```elixir +defmodule MyApp.AuthInterceptor do + use GRPC.Server.Interceptor + + def call(req, stream, next, _opts) do + with {:ok, _claims} <- verify_token(stream) do + next.(req, stream) + else + _ -> raise GRPC.RPCError, status: :unauthenticated + end + end +end +``` \ No newline at end of file diff --git a/examples/elixir-grpc/flake.lock b/examples/elixir-grpc/flake.lock new file mode 100644 index 0000000..34adfb8 --- /dev/null +++ b/examples/elixir-grpc/flake.lock @@ -0,0 +1,113 @@ +{ + "nodes": { + "bufrnix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "treefmt-nix": "treefmt-nix" + }, + "locked": { + "path": "../..", + "type": "path" + }, + "original": { + "path": "../..", + "type": "path" + }, + "parent": [] + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1747958103, + "narHash": "sha256-qmmFCrfBwSHoWw7cVK4Aj+fns+c54EBP8cGqp/yK410=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "fe51d34885f7b5e3e7b59572796e1bcb427eccb1", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1757487488, + "narHash": "sha256-zwE/e7CuPJUWKdvvTCB7iunV4E/+G0lKfv4kk/5Izdg=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ab0f3607a6c7486ea22229b92ed2d355f1482ee0", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "bufrnix": "bufrnix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1749194973, + "narHash": "sha256-eEy8cuS0mZ2j/r/FE0/LYBSBcIs/MKOIVakwHVuqTfk=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "a05be418a1af1198ca0f63facb13c985db4cb3c5", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/examples/elixir-grpc/flake.nix b/examples/elixir-grpc/flake.nix new file mode 100644 index 0000000..ad38cba --- /dev/null +++ b/examples/elixir-grpc/flake.nix @@ -0,0 +1,53 @@ +{ + description = "Elixir gRPC example for bufrnix"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + # bufrnix.url = "github:conneroisu/bufrnix"; + bufrnix.url = "path:../.."; + bufrnix.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + bufrnix, + }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + in { + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + elixir + erlang + protobuf + ]; + }; + packages = { + default = bufrnix.lib.mkBufrnixPackage { + inherit pkgs; + config = { + root = ./.; + protoc = { + sourceDirectories = ["./proto"]; + includeDirectories = ["./proto"]; + files = ["./proto/example/v1/service.proto"]; + }; + languages.elixir = { + enable = true; + package = pkgs.protoc-gen-elixir; + outputPath = "lib/proto"; + namespace = "Proto"; + options = []; + grpc = { + enable = true; + options = []; + }; + }; + }; + }; + }; + }); +} \ No newline at end of file diff --git a/examples/elixir-grpc/lib/elixir_grpc_example/application.ex b/examples/elixir-grpc/lib/elixir_grpc_example/application.ex new file mode 100644 index 0000000..9057048 --- /dev/null +++ b/examples/elixir-grpc/lib/elixir_grpc_example/application.ex @@ -0,0 +1,16 @@ +defmodule ElixirGrpcExample.Application do + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + # Start the gRPC server + {GRPC.Server.Supervisor, endpoint: ElixirGrpcExample.Endpoint, port: 50051, start_server: true} + ] + + opts = [strategy: :one_for_one, name: ElixirGrpcExample.Supervisor] + Supervisor.start_link(children, opts) + end +end \ No newline at end of file diff --git a/examples/elixir-grpc/lib/elixir_grpc_example/client.ex b/examples/elixir-grpc/lib/elixir_grpc_example/client.ex new file mode 100644 index 0000000..7dece45 --- /dev/null +++ b/examples/elixir-grpc/lib/elixir_grpc_example/client.ex @@ -0,0 +1,57 @@ +defmodule ElixirGrpcExample.Client do + @moduledoc """ + gRPC client for testing the UserService. + + This module demonstrates how to use the generated gRPC client code. + """ + + def connect(host \\ "localhost", port \\ 50051) do + # After generation, uncomment: + # {:ok, channel} = GRPC.Stub.connect("#{host}:#{port}") + # channel + {:ok, "Client will connect after protobuf generation"} + end + + def create_user(channel, username, email, full_name) do + # After generation, uncomment: + # request = %Proto.Example.V1.CreateUserRequest{ + # username: username, + # email: email, + # full_name: full_name, + # password: "temporary" + # } + # Proto.Example.V1.UserService.Stub.create_user(channel, request) + {:ok, "create_user will work after protobuf generation"} + end + + def get_user(channel, id) do + # After generation, uncomment: + # request = %Proto.Example.V1.GetUserRequest{id: id} + # Proto.Example.V1.UserService.Stub.get_user(channel, request) + {:ok, "get_user will work after protobuf generation"} + end + + def list_users(channel, page_size \\ 10) do + # After generation, uncomment: + # request = %Proto.Example.V1.ListUsersRequest{ + # page_size: page_size, + # page: 1 + # } + # Proto.Example.V1.UserService.Stub.list_users(channel, request) + {:ok, "list_users will work after protobuf generation"} + end + + def demo do + IO.puts("Elixir gRPC Client Demo") + IO.puts("========================") + IO.puts("") + IO.puts("After running 'nix run' to generate the protobuf code:") + IO.puts(" 1. Uncomment the code in this module") + IO.puts(" 2. Start the server: iex -S mix") + IO.puts(" 3. In another terminal, connect a client:") + IO.puts("") + IO.puts(" iex> {:ok, channel} = ElixirGrpcExample.Client.connect()") + IO.puts(" iex> ElixirGrpcExample.Client.list_users(channel)") + IO.puts("") + end +end \ No newline at end of file diff --git a/examples/elixir-grpc/lib/elixir_grpc_example/endpoint.ex b/examples/elixir-grpc/lib/elixir_grpc_example/endpoint.ex new file mode 100644 index 0000000..f694027 --- /dev/null +++ b/examples/elixir-grpc/lib/elixir_grpc_example/endpoint.ex @@ -0,0 +1,7 @@ +defmodule ElixirGrpcExample.Endpoint do + use GRPC.Endpoint + + # This will be updated after protobuf generation to include the actual service + # intercept GRPC.Server.Interceptors.Logger + # run ElixirGrpcExample.UserService.Server +end \ No newline at end of file diff --git a/examples/elixir-grpc/lib/elixir_grpc_example/user_service_server.ex b/examples/elixir-grpc/lib/elixir_grpc_example/user_service_server.ex new file mode 100644 index 0000000..5b60d28 --- /dev/null +++ b/examples/elixir-grpc/lib/elixir_grpc_example/user_service_server.ex @@ -0,0 +1,90 @@ +defmodule ElixirGrpcExample.UserServiceServer do + @moduledoc """ + gRPC server implementation for UserService. + + This module will use the generated protobuf modules after running `nix run`. + """ + + # After generation, uncomment: + # use GRPC.Server, service: Proto.Example.V1.UserService.Service + + # Sample in-memory storage for demonstration + @initial_users %{ + 1 => %{ + id: 1, + username: "alice", + email: "alice@example.com", + full_name: "Alice Smith", + roles: ["user", "admin"], + created_at: 1234567890, + updated_at: 1234567890 + }, + 2 => %{ + id: 2, + username: "bob", + email: "bob@example.com", + full_name: "Bob Johnson", + roles: ["user"], + created_at: 1234567891, + updated_at: 1234567891 + } + } + + def init(_args) do + {:ok, %{users: @initial_users, next_id: 3}} + end + + # Uncomment after protobuf generation: + + # @spec create_user(Proto.Example.V1.CreateUserRequest.t(), GRPC.Server.Stream.t()) :: + # Proto.Example.V1.CreateUserResponse.t() + # def create_user(request, _stream) do + # # Implementation would go here + # user = %Proto.Example.V1.User{ + # id: 3, + # username: request.username, + # email: request.email, + # full_name: request.full_name, + # roles: ["user"], + # created_at: System.system_time(:second), + # updated_at: System.system_time(:second) + # } + # + # %Proto.Example.V1.CreateUserResponse{ + # user: user, + # message: "User created successfully" + # } + # end + + # @spec get_user(Proto.Example.V1.GetUserRequest.t(), GRPC.Server.Stream.t()) :: + # Proto.Example.V1.GetUserResponse.t() + # def get_user(request, _stream) do + # case @initial_users[request.id] do + # nil -> + # raise GRPC.RPCError, status: :not_found, message: "User not found" + # + # user_data -> + # user = struct(Proto.Example.V1.User, user_data) + # %Proto.Example.V1.GetUserResponse{user: user} + # end + # end + + # @spec list_users(Proto.Example.V1.ListUsersRequest.t(), GRPC.Server.Stream.t()) :: + # Proto.Example.V1.ListUsersResponse.t() + # def list_users(request, _stream) do + # users = + # @initial_users + # |> Map.values() + # |> Enum.map(&struct(Proto.Example.V1.User, &1)) + # |> Enum.take(request.page_size || 10) + # + # %Proto.Example.V1.ListUsersResponse{ + # users: users, + # total: length(users), + # page: request.page || 1, + # page_size: request.page_size || 10 + # } + # end + + # Additional method implementations would go here... +end \ No newline at end of file diff --git a/examples/elixir-grpc/lib/proto/.formatter.exs b/examples/elixir-grpc/lib/proto/.formatter.exs new file mode 100644 index 0000000..3a43859 --- /dev/null +++ b/examples/elixir-grpc/lib/proto/.formatter.exs @@ -0,0 +1,4 @@ +[ + inputs: ["*.{ex,exs}", "{lib,test}/**/*.{ex,exs}"], + line_length: 120 +] diff --git a/examples/elixir-grpc/lib/proto/example/v1/service.pb.ex b/examples/elixir-grpc/lib/proto/example/v1/service.pb.ex new file mode 100644 index 0000000..e357511 --- /dev/null +++ b/examples/elixir-grpc/lib/proto/example/v1/service.pb.ex @@ -0,0 +1,162 @@ +defmodule Example.V1.UserEvent.EventType do + @moduledoc false + + use Protobuf, enum: true, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :EVENT_TYPE_UNSPECIFIED, 0 + field :EVENT_TYPE_CREATED, 1 + field :EVENT_TYPE_UPDATED, 2 + field :EVENT_TYPE_DELETED, 3 +end + +defmodule Example.V1.User do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :id, 1, type: :int32 + field :username, 2, type: :string + field :email, 3, type: :string + field :full_name, 4, type: :string, json_name: "fullName" + field :roles, 5, repeated: true, type: :string + field :created_at, 6, type: :int64, json_name: "createdAt" + field :updated_at, 7, type: :int64, json_name: "updatedAt" +end + +defmodule Example.V1.CreateUserRequest do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :username, 1, type: :string + field :email, 2, type: :string + field :full_name, 3, type: :string, json_name: "fullName" + field :password, 4, type: :string +end + +defmodule Example.V1.CreateUserResponse do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :user, 1, type: Example.V1.User + field :message, 2, type: :string +end + +defmodule Example.V1.GetUserRequest do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :id, 1, type: :int32 +end + +defmodule Example.V1.GetUserResponse do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :user, 1, type: Example.V1.User +end + +defmodule Example.V1.ListUsersRequest do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :page_size, 1, type: :int32, json_name: "pageSize" + field :page, 2, type: :int32 + field :search, 3, type: :string +end + +defmodule Example.V1.ListUsersResponse do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :users, 1, repeated: true, type: Example.V1.User + field :total, 2, type: :int32 + field :page, 3, type: :int32 + field :page_size, 4, type: :int32, json_name: "pageSize" +end + +defmodule Example.V1.UpdateUserRequest do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :id, 1, type: :int32 + field :email, 2, type: :string + field :full_name, 3, type: :string, json_name: "fullName" + field :roles, 4, repeated: true, type: :string +end + +defmodule Example.V1.UpdateUserResponse do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :user, 1, type: Example.V1.User + field :message, 2, type: :string +end + +defmodule Example.V1.DeleteUserRequest do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :id, 1, type: :int32 +end + +defmodule Example.V1.DeleteUserResponse do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :success, 1, type: :bool + field :message, 2, type: :string +end + +defmodule Example.V1.UserEvent do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :event_type, 1, type: Example.V1.UserEvent.EventType, json_name: "eventType", enum: true + field :user, 2, type: Example.V1.User + field :timestamp, 3, type: :int64 +end + +defmodule Example.V1.WatchUsersRequest do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.15.0", syntax: :proto3 + + field :user_ids, 1, repeated: true, type: :int32, json_name: "userIds" +end + +defmodule Example.V1.UserService.Service do + @moduledoc false + + use GRPC.Service, name: "example.v1.UserService", protoc_gen_elixir_version: "0.15.0" + + rpc :CreateUser, Example.V1.CreateUserRequest, Example.V1.CreateUserResponse + + rpc :GetUser, Example.V1.GetUserRequest, Example.V1.GetUserResponse + + rpc :ListUsers, Example.V1.ListUsersRequest, Example.V1.ListUsersResponse + + rpc :UpdateUser, Example.V1.UpdateUserRequest, Example.V1.UpdateUserResponse + + rpc :DeleteUser, Example.V1.DeleteUserRequest, Example.V1.DeleteUserResponse + + rpc :WatchUsers, Example.V1.WatchUsersRequest, stream(Example.V1.UserEvent) + + rpc :BatchProcess, stream(Example.V1.UpdateUserRequest), stream(Example.V1.UpdateUserResponse) +end + +defmodule Example.V1.UserService.Stub do + @moduledoc false + + use GRPC.Stub, service: Example.V1.UserService.Service +end diff --git a/examples/elixir-grpc/mix.exs b/examples/elixir-grpc/mix.exs new file mode 100644 index 0000000..77ad286 --- /dev/null +++ b/examples/elixir-grpc/mix.exs @@ -0,0 +1,31 @@ +defmodule ElixirGrpcExample.MixProject do + use Mix.Project + + def project do + [ + app: :elixir_grpc_example, + version: "0.1.0", + elixir: "~> 1.14", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {ElixirGrpcExample.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:grpc, "~> 0.7.0"}, + {:protobuf, "~> 0.12.0"}, + {:jason, "~> 1.4"}, + {:gun, "~> 2.0"} + ] + end +end \ No newline at end of file diff --git a/examples/elixir-grpc/proto/example/v1/service.proto b/examples/elixir-grpc/proto/example/v1/service.proto new file mode 100644 index 0000000..a64ba16 --- /dev/null +++ b/examples/elixir-grpc/proto/example/v1/service.proto @@ -0,0 +1,111 @@ +syntax = "proto3"; + +package example.v1; + +// User message +message User { + int32 id = 1; + string username = 2; + string email = 3; + string full_name = 4; + repeated string roles = 5; + int64 created_at = 6; + int64 updated_at = 7; +} + +// Request/Response messages for User service +message CreateUserRequest { + string username = 1; + string email = 2; + string full_name = 3; + string password = 4; +} + +message CreateUserResponse { + User user = 1; + string message = 2; +} + +message GetUserRequest { + int32 id = 1; +} + +message GetUserResponse { + User user = 1; +} + +message ListUsersRequest { + int32 page_size = 1; + int32 page = 2; + string search = 3; +} + +message ListUsersResponse { + repeated User users = 1; + int32 total = 2; + int32 page = 3; + int32 page_size = 4; +} + +message UpdateUserRequest { + int32 id = 1; + string email = 2; + string full_name = 3; + repeated string roles = 4; +} + +message UpdateUserResponse { + User user = 1; + string message = 2; +} + +message DeleteUserRequest { + int32 id = 1; +} + +message DeleteUserResponse { + bool success = 1; + string message = 2; +} + +// Streaming messages +message UserEvent { + enum EventType { + EVENT_TYPE_UNSPECIFIED = 0; + EVENT_TYPE_CREATED = 1; + EVENT_TYPE_UPDATED = 2; + EVENT_TYPE_DELETED = 3; + } + + EventType event_type = 1; + User user = 2; + int64 timestamp = 3; +} + +message WatchUsersRequest { + repeated int32 user_ids = 1; +} + +// User management service +service UserService { + // Create a new user + rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); + + // Get a user by ID + rpc GetUser(GetUserRequest) returns (GetUserResponse); + + // List users with pagination + rpc ListUsers(ListUsersRequest) returns (ListUsersResponse); + + // Update user information + rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse); + + // Delete a user + rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse); + + // Stream user events + rpc WatchUsers(WatchUsersRequest) returns (stream UserEvent); + + // Bidirectional streaming for batch operations + rpc BatchProcess(stream UpdateUserRequest) returns (stream UpdateUserResponse); +} \ No newline at end of file diff --git a/src/languages/elixir/default.nix b/src/languages/elixir/default.nix new file mode 100644 index 0000000..09236e9 --- /dev/null +++ b/src/languages/elixir/default.nix @@ -0,0 +1,92 @@ +{ + pkgs, + config, + lib, + cfg ? config.languages.elixir, + ... +}: +with lib; let + # Define output path and options + outputPath = cfg.outputPath; + elixirOptions = cfg.options; + + # Import Elixir-specific sub-modules + grpcModule = import ./grpc.nix { + inherit pkgs lib; + cfg = + (cfg.grpc or {enable = false;}) + // { + outputPath = outputPath; + }; + }; + + validateModule = import ./validate.nix { + inherit pkgs lib; + cfg = + (cfg.validate or {enable = false;}) + // { + outputPath = outputPath; + }; + }; + + # Combine all sub-modules + combineModuleAttrs = attr: + concatLists (catAttrs attr [ + grpcModule + validateModule + ]); +in { + # Runtime dependencies for Elixir code generation + runtimeInputs = + [ + cfg.package + ] + ++ (combineModuleAttrs "runtimeInputs"); + + # Protoc plugin configuration for Elixir + protocPlugins = + # Only add the base plugin if gRPC is not handling it + (if (cfg.grpc.enable or false) then [] else [ + "--elixir_out=${outputPath}" + ]) + ++ (optionals (elixirOptions != []) [ + "--elixir_opt=${concatStringsSep " --elixir_opt=" elixirOptions}" + ]) + ++ (combineModuleAttrs "protocPlugins"); + + # Initialization hook for Elixir + initHooks = + '' + # Create elixir-specific directories + mkdir -p "${outputPath}" + ${optionalString (cfg.namespace != "") '' + echo "Creating Elixir modules with namespace: ${cfg.namespace}" + ''} + '' + + concatStrings (catAttrs "initHooks" [ + grpcModule + validateModule + ]); + + # Code generation hook for Elixir + generateHooks = + '' + # Elixir-specific code generation steps + echo "Generating Elixir code..." + mkdir -p ${outputPath} + + # Add .formatter.exs if it doesn't exist + if [ ! -f "${outputPath}/.formatter.exs" ]; then + cat > "${outputPath}/.formatter.exs" << 'EOF' +[ + inputs: ["*.{ex,exs}", "{lib,test}/**/*.{ex,exs}"], + line_length: 120 +] +EOF + fi + '' + + concatStrings (catAttrs "generateHooks" [ + grpcModule + validateModule + ]); +} \ No newline at end of file diff --git a/src/languages/elixir/grpc.nix b/src/languages/elixir/grpc.nix new file mode 100644 index 0000000..dbd6e0c --- /dev/null +++ b/src/languages/elixir/grpc.nix @@ -0,0 +1,40 @@ +{ + pkgs, + lib, + cfg, + ... +}: +with lib; let + outputPath = cfg.outputPath; + grpcOptions = cfg.options or []; +in + if (cfg.enable or false) then { + # Runtime dependencies for Elixir gRPC + runtimeInputs = [ + cfg.package or pkgs.protoc-gen-elixir + ]; + + # Protoc plugin configuration for Elixir gRPC + protocPlugins = + [ + "--elixir_out=plugins=grpc:${outputPath}" + ] + ++ (optionals (grpcOptions != []) [ + "--elixir_opt=${concatStringsSep " --elixir_opt=" grpcOptions}" + ]); + + # Initialization hook for Elixir gRPC + initHooks = '' + echo "Enabling Elixir gRPC generation..." + ''; + + # Code generation hook for Elixir gRPC + generateHooks = '' + echo "Generated Elixir gRPC services in ${outputPath}" + ''; + } else { + runtimeInputs = []; + protocPlugins = []; + initHooks = ""; + generateHooks = ""; + } \ No newline at end of file diff --git a/src/languages/elixir/validate.nix b/src/languages/elixir/validate.nix new file mode 100644 index 0000000..47f0e9a --- /dev/null +++ b/src/languages/elixir/validate.nix @@ -0,0 +1,32 @@ +{ + pkgs, + lib, + cfg, + ... +}: +with lib; let + outputPath = cfg.outputPath; +in + if (cfg.enable or false) then { + # Runtime dependencies for Elixir validation + runtimeInputs = []; + + # Protoc plugin configuration for Elixir validation + protocPlugins = []; + + # Initialization hook for Elixir validation + initHooks = '' + echo "Preparing Elixir validation support..." + ''; + + # Code generation hook for Elixir validation + generateHooks = '' + # Add validation support hints + echo "Note: To use validation in Elixir, add protoc_validate to your mix.exs dependencies" + ''; + } else { + runtimeInputs = []; + protocPlugins = []; + initHooks = ""; + generateHooks = ""; + } \ No newline at end of file diff --git a/src/lib/bufrnix-options.nix b/src/lib/bufrnix-options.nix index ee90482..c68c190 100644 --- a/src/lib/bufrnix-options.nix +++ b/src/lib/bufrnix-options.nix @@ -1256,6 +1256,106 @@ with lib; { }; }; + # Elixir language options + elixir = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable Elixir code generation"; + }; + + package = mkOption { + type = types.package; + defaultText = literalExpression "pkgs.protoc-gen-elixir"; + description = "The protoc-gen-elixir package to use"; + }; + + outputPath = mkOption { + type = types.either types.str (types.listOf types.str); + default = "lib"; + description = "Output directory(ies) for generated Elixir code"; + example = literalExpression '' + [ + "lib/proto" + "lib/generated" + ] + ''; + }; + + options = mkOption { + type = types.listOf types.str; + default = []; + description = "Options to pass to protoc-gen-elixir"; + }; + + files = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + description = "Proto files to compile for Elixir only. Overrides global protoc.files. If null, uses global protoc.files."; + example = [ + "./proto/services/v1/user_service.proto" + "./proto/common/v1/types.proto" + ]; + }; + + additionalFiles = mkOption { + type = types.listOf types.str; + default = []; + description = "Additional proto files to compile for Elixir. Extends global protoc.files."; + example = [ + "./proto/elixir/v1/elixir_extensions.proto" + "./proto/services/v1/notification_service.proto" + ]; + }; + + namespace = mkOption { + type = types.str; + default = ""; + description = "Elixir module namespace prefix for generated code"; + example = "MyApp.Proto"; + }; + + grpc = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable gRPC code generation for Elixir"; + }; + + package = mkOption { + type = types.package; + defaultText = literalExpression "pkgs.protoc-gen-elixir"; + description = "The protoc-gen-elixir package to use (same as parent for Elixir)"; + }; + + options = mkOption { + type = types.listOf types.str; + default = []; + description = "Options to pass to protoc-gen-elixir for gRPC"; + }; + }; + + validate = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enable validation support for Elixir protobuf messages"; + }; + + package = mkOption { + type = types.package; + defaultText = literalExpression "null"; + description = "The validation package to use (if any)"; + }; + + options = mkOption { + type = types.listOf types.str; + default = []; + description = "Options for Elixir validation"; + }; + }; + }; + # Documentation language options doc = { enable = mkOption { diff --git a/src/lib/mkBufrnix.nix b/src/lib/mkBufrnix.nix index 535ad7e..fc05fdf 100644 --- a/src/lib/mkBufrnix.nix +++ b/src/lib/mkBufrnix.nix @@ -97,6 +97,11 @@ with pkgs.lib; let package = pkgs.protoc-gen-dart; grpc.package = pkgs.protoc-gen-dart; }; + elixir = { + package = pkgs.protoc-gen-elixir; + grpc.package = pkgs.protoc-gen-elixir; + validate.package = null; # Not yet in nixpkgs + }; doc = { package = pkgs.protoc-gen-doc; }; diff --git a/test-examples.sh b/test-examples.sh index 0fe59ee..ebafda6 100755 --- a/test-examples.sh +++ b/test-examples.sh @@ -427,6 +427,15 @@ test_example "java-protovalidate" \ "gen/java/build.gradle" \ "gen/java/pom.xml" +# Test Elixir examples +test_example "elixir-basic" \ + "lib/proto/example/v1/example.pb.ex" \ + "lib/proto/.formatter.exs" + +test_example "elixir-grpc" \ + "lib/proto/example/v1/service.pb.ex" \ + "lib/proto/.formatter.exs" + # Summary echo -e "\n${YELLOW}Test Summary:${NC}" echo -e "${GREEN}Passed: ${#PASSED_TESTS[@]}${NC}"