diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..2bf2b0307
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,27 @@
+# Changelog
+
+## v0.11.5 (2025-11-14)
+
+### Enhancements
+
+ * Feat add `exception_log_filter` option to server
+
+### Bug fixes
+
+ * Fix ensure thers is only one `GRPC.Client.Supervisor`.
+ * Fix report `GRPC.Errors` as normal shutdowns
+
+## v0.11.4 (2025-11-07)
+
+### Enhancements
+
+ * Feat added new function to handle side-effects.
+ * Feat added error handler for unary and stream pipelines.
+ * Docs adds a better explanation of the different types of input.
+ * Docs improvements to module documentation.
+ * Docs livebooks added directly to the documentation.
+
+### Bug fixes
+
+ * Fix refresh error spam on direct_state (no lb).
+ * Fix correct return type in doc.
diff --git a/README.md b/README.md
index 6f266dc71..67ce8648d 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,10 @@
- [Unary RPC using Stream API](#unary-rpc-using-stream-api)
- [Server-Side Streaming](#server-side-streaming)
- [Bidirectional Streaming](#bidirectional-streaming)
+ - [Effects and Error Handling](#effects-and-error-handling)
+ - [Side Effects](#side-effects-with-effect2)
+ - [Recovery from errors](#recovery-from-errors)
+ - [Unified Error Matching and Propagation](#unified-error-matching-and-propagation)
- [Application Startup](#application-startup)
- [Client Usage](#client-usage)
- [Basic Connection and RPC](#basic-connection-and-rpc)
@@ -101,8 +105,9 @@ defmodule HelloworldStreams.Server do
alias Helloworld.HelloReply
@spec say_unary_hello(HelloRequest.t(), GRPC.Server.Stream.t()) :: any()
- def say_unary_hello(request, _materializer) do
- GRPC.Stream.unary(request)
+ def say_unary_hello(request, materializer) do
+ request
+ |> GRPC.Stream.unary(materializer: materializer)
|> GRPC.Stream.map(fn %HelloReply{} = reply ->
%HelloReply{message: "[Reply] #{reply.message}"}
end)
@@ -144,28 +149,104 @@ def say_bid_stream_hello(request, materializer) do
|> GRPC.Stream.run_with(materializer)
end
```
-The Stream API supports composable stream transformations via `ask`, `map`, `run` and others functions, enabling clean and declarative stream pipelines. See the table below:
-
-| Function | Description | Parameters / Options |
-|:---------------------------------|:-------------|:----------------------|
-| **`from(input, opts \\\\ [])`** | Converts a gRPC stream (or list) into a `Flow` with backpressure support. Allows joining with external `GenStage` producers. | **Parameters:**
• `input` — stream, list, or gRPC struct.
**Options:**
• `:join_with` — PID or name of an external `GenStage` producer.
• `:dispatcher` — dispatcher module (default: `GenStage.DemandDispatcher`).
• `:propagate_context` — if `true`, propagates the materializer context.
• `:materializer` — the current `%GRPC.Server.Stream{}`.
• Other options supported by `Flow`. |
-| **`unary(input, opts \\\\ [])`** | Creates a `Flow` from a single gRPC request (unary). Useful for non-streaming calls that still leverage the Flow API. | **Parameters:**
• `input` — single gRPC message.
**Options:** same as `from/2`. |
-| **`to_flow(stream)`** | Returns the underlying `Flow` from a `GRPC.Stream`. If uninitialized, returns `Flow.from_enumerable([])`. | **Parameters:**
• `stream` — `%GRPC.Stream{}` struct. |
-| **`run(stream)`** | Executes the `Flow` for a unary stream and returns the first materialized result. | **Parameters:**
• `stream` — `%GRPC.Stream{}` with `unary: true` option. |
-| **`run_with(stream, materializer, opts \\\\ [])`** | Executes the `Flow` and sends responses into the gRPC server stream. Supports `:dry_run` for test mode without sending messages. | **Parameters:**
• `stream` — `%GRPC.Stream{}`.
• `materializer` — `%GRPC.Server.Stream{}`.
**Options:**
• `:dry_run` — if `true`, responses are not sent. |
-| **`ask(stream, target, timeout \\\\ 5000)`** | Sends a request to an external process (`PID` or named process) and waits for a response (`{:response, msg}`). Returns an updated stream or an error. | **Parameters:**
• `stream` — `%GRPC.Stream{}`.
• `target` — PID or atom.
• `timeout` — in milliseconds. |
-| **`ask!(stream, target, timeout \\\\ 5000)`** | Same as `ask/3`, but raises an exception on failure (aborts the Flow). | Same parameters as `ask/3`. |
-| **`filter(stream, fun)`** | Filters items in the stream by applying a concurrent predicate function. | **Parameters:**
• `stream` — `%GRPC.Stream{}`.
• `fun` — function `(item -> boolean)`. |
-| **`flat_map(stream, fun)`** | Applies a function returning a list or enumerable, flattening the results. | **Parameters:**
• `stream` — `%GRPC.Stream{}`.
• `fun` — `(item -> Enumerable.t())`. |
-| **`map(stream, fun)`** | Applies a transformation function to each item in the stream. | **Parameters:**
• `stream` — `%GRPC.Stream{}`.
• `fun` — `(item -> term)`. |
-| **`map_with_context(stream, fun)`** | Applies a function to each item, passing the stream context (e.g., headers) as an additional argument. | **Parameters:**
• `stream` — `%GRPC.Stream{}`.
• `fun` — `(context, item -> term)`. |
-| **`partition(stream, opts \\\\ [])`** | Partitions the stream to group items by key or condition before stateful operations like `reduce/3`. | **Parameters:**
• `stream` — `%GRPC.Stream{}`.
• `opts` — partitioning options (`Flow.partition/2`). |
-| **`reduce(stream, acc_fun, reducer_fun)`** | Reduces the stream using an accumulator, useful for aggregations. | **Parameters:**
• `stream` — `%GRPC.Stream{}`.
• `acc_fun` — initializer function `() -> acc`.
• `reducer_fun` — `(item, acc -> acc)`. |
-| **`uniq(stream)`** | Emits only distinct items from the stream (no custom uniqueness criteria). | **Parameters:**
• `stream` — `%GRPC.Stream{}`. |
-| **`uniq_by(stream, fun)`** | Emits only unique items based on the return value of the provided function. | **Parameters:**
• `stream` — `%GRPC.Stream{}`.
• `fun` — `(item -> term)` for uniqueness determination. |
-| **`get_headers(stream)`** | Retrieves HTTP/2 headers from a `%GRPC.Server.Stream{}`. | **Parameters:**
• `stream` — `%GRPC.Server.Stream{}`.
**Returns:** `map` containing decoded headers. |
-
-For a complete list of available operators see [here](lib/grpc/stream.ex).
+The Stream API supports composable stream transformations via `ask`, `map`, `run` and others functions, enabling clean and declarative stream pipelines. For a complete list of available operators see [here](lib/grpc/stream.ex).
+
+---
+
+### Effects and Error Handling
+
+#### Side Effects
+
+The `effect/2` operator executes user-defined functions for each element in the stream, allowing the integration of non-transformative actions such as logging, metrics, or external notifications.
+
+Unlike transformation operators (e.g., `map/2`), `effect/2` does not modify or filter values — it preserves the original stream while executing the provided callback safely for each emitted element.
+
+```elixir
+iex> parent = self()
+iex> stream =
+...> GRPC.Stream.from([1, 2, 3])
+...> |> GRPC.Stream.effect(fn x -> send(parent, {:seen, x * 2}) end)
+...> |> GRPC.Stream.to_flow()
+...> |> Enum.to_list()
+iex> assert_receive {:seen, 2}
+iex> assert_receive {:seen, 4}
+iex> assert_receive {:seen, 6}
+iex> stream
+[1, 2, 3]
+```
+
+Key characteristics:
+
+* The callback function (`effect_fun`) is invoked for each item emitted downstream.
+* The result of the callback is ignored, ensuring that the stream’s structure and values remain unchanged.
+* Execution is lazy and occurs only when the stream is materialized using run/1, run_with/3, or to_flow/1.
+* Exceptions raised inside the callback are captured internally, preventing interruption of the dataflow.
+
+This operator is designed for observability, telemetry, auditing, and integration with external systems that must react to events flowing through the gRPC stream.
+
+---
+
+#### Recovery from errors
+
+The `map_error/2` operator intercepts and transforms errors or exceptions emitted by previous stages in a stream pipeline.
+
+It provides a unified mechanism for handling:
+
+* Expected errors, such as validation or domain failures (`{:error, reason}`)
+* Unexpected runtime errors, including raised or thrown exceptions inside other operators.
+
+```elixir
+iex> GRPC.Stream.from([1, 2])
+...> |> GRPC.Stream.map(fn
+...> 2 -> raise "boom"
+...> x -> x
+...> end)
+...> |> GRPC.Stream.map_error(fn
+...> {:error, {:exception, _reason}} ->
+...> {:error, GRPC.RPCError.exception(message: "Booomm")}
+...> end)
+```
+
+In this example:
+
+* The function inside `map/2` raises an exception for the value `2`.
+* `map_error/2` captures and transforms that error into a structured `GRPC.RPCError` response.
+* The stream continues processing without being interrupted.
+
+This makes map_error/2 suitable for input validation, runtime fault recovery, and user-facing error translation within gRPC pipelines.
+
+---
+
+#### Unified Error Matching and Propagation
+
+All stream operators share a unified error propagation model that guarantees consistent handling of exceptions and failures across the pipeline.
+
+This ensures that user-defined functions within the stream — whether pure transformations, side effects, or external calls — always produce a predictable and recoverable result, maintaining the integrity of the dataflow even in the presence of unexpected errors.
+
+```elixir
+def say_unary_hello(request, _materializer) do
+ GRPCStream.unary(request)
+ |> GRPCStream.ask(Transformer)
+ |> GRPCStream.map(fn
+ %HelloReply{} = reply ->
+ %HelloReply{message: "[Reply] #{reply.message}"}
+
+ {:error, reason} ->
+ {:error, GRPC.RPCError.exception(message: "error calling external process: #{inspect(reason)}")}
+
+ error ->
+ Logger.error("Unknown error")
+ error
+ end)
+ |> GRPCStream.run()
+end
+```
+
+By normalizing all possible outcomes, `GRPC.Stream` ensures fault-tolerant, exception-safe pipelines where operators can freely raise, throw, or return tuples without breaking the flow execution.
+
+This unified model allows developers to build composable and reliable streaming pipelines that gracefully recover from both domain and runtime errors.
+
+>_NOTE_: In the example above, we could use `map_error/2` instead of `map/2` to handle error cases explicitly. However, since the function also performs a transformation on successful values, `map/2` remains appropriate and useful in this context.
---
@@ -175,7 +256,7 @@ Add the server supervisor to your application's supervision tree:
```elixir
defmodule Helloworld.Application do
- @ false
+ @moduledoc false
use Application
@impl true
diff --git a/examples/helloworld/.gitignore b/examples/helloworld/.gitignore
deleted file mode 100644
index 06dbcb6f6..000000000
--- a/examples/helloworld/.gitignore
+++ /dev/null
@@ -1,23 +0,0 @@
-# The directory Mix will write compiled artifacts to.
-/_build
-
-# If you run "mix test --cover", coverage assets end up here.
-/cover
-
-# The directory Mix downloads your dependencies sources to.
-/deps
-
-# Where 3rd-party dependencies like ExDoc output generated docs.
-/doc
-
-# If the VM crashes, it generates a dump, let's ignore it too.
-erl_crash.dump
-
-# Also ignore archive artifacts (built via "mix archive.build").
-*.ez
-
-/priv/grpc_c.so*
-/src/grpc_c
-/tmp
-
-/log
\ No newline at end of file
diff --git a/examples/helloworld/README.md b/examples/helloworld/README.md
deleted file mode 100644
index e3c2b3454..000000000
--- a/examples/helloworld/README.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Helloworld in grpc-elixir
-
-## Usage
-
-1. Install deps and compile
-```shell
-$ mix do deps.get, compile
-```
-
-2. Run the server
-```shell
-$ mix run --no-halt
-```
-
-3. Run the client script
-```shell
-$ mix run priv/client.exs
-```
-
-## HTTP Transcoding
-
-``` shell
-# Say hello
-curl -H 'Content-type: application/json' http://localhost:50051/v1/greeter/test
-
-# Say hello from
-curl -XPOST -H 'Content-type: application/json' -d '{"name": "test", "from": "anon"}' http://localhost:50051/v1/greeter
-```
-
-## Regenerate Elixir code from proto
-
-1. Modify the proto `priv/protos/helloworld.proto`
-2. Install `protoc` [here](https://developers.google.com/protocol-buffers/docs/downloads)
-3. Install `protoc-gen-elixir`
-```
-mix escript.install hex protobuf
-```
-4. Generate the code:
-
-```shell
-$ (cd ../../; mix build_protobuf_escript && mix escript.build)
-$ protoc -I priv/protos --elixir_out=:./lib/ --grpc_elixir_out=./lib --plugin="../../deps/protobuf/protoc-gen-elixir" --plugin="../../protoc-gen-grpc_elixir" priv/protos/helloworld.proto
-```
-
-Refer to [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) for more information.
-
-## How to start server when starting your application?
-
-Pass `start_server: true` as an option for the `GRPC.Server.Supervisor` in your supervision tree.
-
-## Benchmark
-
-Using [ghz](https://ghz.sh/)
-
-```
-$ MIX_ENV=prod iex -S mix
-# Now cowboy doesn't work well with concurrency in a connection, like --concurrency 6 --connections 1
-$ ghz --insecure --proto priv/protos/helloworld.proto --call helloworld.Greeter.SayHello -d '{"name":"Joe"}' -z 10s --concurrency 6 --connections 6 127.0.0.1:50051
-# The result is for branch improve-perf
-Summary:
- Count: 124239
- Total: 10.00 s
- Slowest: 18.85 ms
- Fastest: 0.18 ms
- Average: 0.44 ms
- Requests/sec: 12423.71
-
-# Go
-Summary:
- Count: 258727
- Total: 10.00 s
- Slowest: 5.39 ms
- Fastest: 0.09 ms
- Average: 0.19 ms
- Requests/sec: 25861.68
-```
diff --git a/examples/helloworld/config/config.exs b/examples/helloworld/config/config.exs
deleted file mode 100644
index 9def7c2c5..000000000
--- a/examples/helloworld/config/config.exs
+++ /dev/null
@@ -1,3 +0,0 @@
-import Config
-
-import_config "#{Mix.env}.exs"
diff --git a/examples/helloworld/config/dev.exs b/examples/helloworld/config/dev.exs
deleted file mode 100644
index becde7693..000000000
--- a/examples/helloworld/config/dev.exs
+++ /dev/null
@@ -1 +0,0 @@
-import Config
diff --git a/examples/helloworld/config/prod.exs b/examples/helloworld/config/prod.exs
deleted file mode 100644
index 2c946e805..000000000
--- a/examples/helloworld/config/prod.exs
+++ /dev/null
@@ -1,4 +0,0 @@
-import Config
-
-config :logger,
- level: :warning
diff --git a/examples/helloworld/config/test.exs b/examples/helloworld/config/test.exs
deleted file mode 100644
index becde7693..000000000
--- a/examples/helloworld/config/test.exs
+++ /dev/null
@@ -1 +0,0 @@
-import Config
diff --git a/examples/helloworld/lib/endpoint.ex b/examples/helloworld/lib/endpoint.ex
deleted file mode 100644
index c8bc64f69..000000000
--- a/examples/helloworld/lib/endpoint.ex
+++ /dev/null
@@ -1,6 +0,0 @@
-defmodule Helloworld.Endpoint do
- use GRPC.Endpoint
-
- intercept GRPC.Server.Interceptors.Logger
- run Helloworld.Greeter.Server
-end
diff --git a/examples/helloworld/lib/helloworld.pb.ex b/examples/helloworld/lib/helloworld.pb.ex
deleted file mode 100644
index 9a10f57ba..000000000
--- a/examples/helloworld/lib/helloworld.pb.ex
+++ /dev/null
@@ -1,41 +0,0 @@
-defmodule Helloworld.HelloRequest do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
-
- field :name, 1, type: :string
-end
-
-defmodule Helloworld.HelloRequestFrom do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
-
- field :name, 1, type: :string
- field :from, 2, type: :string
-end
-
-defmodule Helloworld.HelloReply do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
-
- field :message, 1, type: :string
- field :today, 2, type: Google.Protobuf.Timestamp
-end
-
-defmodule Helloworld.GetMessageRequest do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
-
- field :name, 1, type: :string
-end
-
-defmodule Helloworld.Message do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
-
- field :text, 1, type: :string
-end
diff --git a/examples/helloworld/lib/helloworld.svc.ex b/examples/helloworld/lib/helloworld.svc.ex
deleted file mode 100644
index dcf882ab1..000000000
--- a/examples/helloworld/lib/helloworld.svc.ex
+++ /dev/null
@@ -1,65 +0,0 @@
-defmodule Helloworld.Greeter.Service do
- @moduledoc false
-
- use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.11.0"
-
- rpc(:SayHello, Helloworld.HelloRequest, Helloworld.HelloReply, %{
- http: %{
- type: Google.Api.PbExtension,
- value: %Google.Api.HttpRule{
- __unknown_fields__: [],
- additional_bindings: [],
- body: "",
- pattern: {:get, "/v1/greeter/{name}"},
- response_body: "",
- selector: ""
- }
- }
- })
-
- rpc(:SayHelloFrom, Helloworld.HelloRequestFrom, Helloworld.HelloReply, %{
- http: %{
- type: Google.Api.PbExtension,
- value: %Google.Api.HttpRule{
- __unknown_fields__: [],
- additional_bindings: [],
- body: "*",
- pattern: {:post, "/v1/greeter"},
- response_body: "",
- selector: ""
- }
- }
- })
-end
-
-defmodule Helloworld.Greeter.Stub do
- @moduledoc false
-
- use GRPC.Stub, service: Helloworld.Greeter.Service
-end
-
-defmodule Helloworld.Messaging.Service do
- @moduledoc false
-
- use GRPC.Service, name: "helloworld.Messaging", protoc_gen_elixir_version: "0.11.0"
-
- rpc(:GetMessage, Helloworld.GetMessageRequest, Helloworld.Message, %{
- http: %{
- type: Google.Api.PbExtension,
- value: %Google.Api.HttpRule{
- __unknown_fields__: [],
- additional_bindings: [],
- body: "",
- pattern: {:get, "/v1/{name=messages/*}"},
- response_body: "",
- selector: ""
- }
- }
- })
-end
-
-defmodule Helloworld.Messaging.Stub do
- @moduledoc false
-
- use GRPC.Stub, service: Helloworld.Messaging.Service
-end
diff --git a/examples/helloworld/lib/helloworld_app.ex b/examples/helloworld/lib/helloworld_app.ex
deleted file mode 100644
index d84d62a58..000000000
--- a/examples/helloworld/lib/helloworld_app.ex
+++ /dev/null
@@ -1,12 +0,0 @@
-defmodule HelloworldApp do
- use Application
-
- def start(_type, _args) do
- children = [
- {GRPC.Server.Supervisor, endpoint: Helloworld.Endpoint, port: 50051, start_server: true}
- ]
-
- opts = [strategy: :one_for_one, name: HelloworldApp]
- Supervisor.start_link(children, opts)
- end
-end
diff --git a/examples/helloworld/lib/server.ex b/examples/helloworld/lib/server.ex
deleted file mode 100644
index 8786949fe..000000000
--- a/examples/helloworld/lib/server.ex
+++ /dev/null
@@ -1,31 +0,0 @@
-defmodule Helloworld.Greeter.Server do
- use GRPC.Server,
- service: Helloworld.Greeter.Service,
- http_transcode: true
-
- @spec say_hello(Helloworld.HelloRequest.t(), GRPC.Server.Stream.t()) ::
- Helloworld.HelloReply.t()
- def say_hello(request, _stream) do
- %Helloworld.HelloReply{
- message: "Hello #{request.name}",
- today: today()
- }
- end
-
- @spec say_hello_from(Helloworld.HelloFromRequest.t(), GRPC.Server.Stream.t()) ::
- Helloworld.HelloReply.t()
- def say_hello_from(request, _stream) do
- %Helloworld.HelloReply{
- message: "Hello #{request.name}. From #{request.from}",
- today: today()
- }
- end
-
- defp today do
- nanos_epoch = System.system_time() |> System.convert_time_unit(:native, :nanosecond)
- seconds = div(nanos_epoch, 1_000_000_000)
- nanos = nanos_epoch - seconds * 1_000_000_000
-
- %Google.Protobuf.Timestamp{seconds: seconds, nanos: nanos}
- end
-end
diff --git a/examples/helloworld/mix.exs b/examples/helloworld/mix.exs
deleted file mode 100644
index 78e2443c3..000000000
--- a/examples/helloworld/mix.exs
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Helloworld.Mixfile do
- use Mix.Project
-
- def project do
- [
- app: :helloworld,
- version: "0.1.0",
- elixir: "~> 1.4",
- build_embedded: Mix.env() == :prod,
- start_permanent: Mix.env() == :prod,
- deps: deps()
- ]
- end
-
- def application do
- [mod: {HelloworldApp, []}, applications: [:logger, :grpc]]
- end
-
- defp deps do
- [
- {:grpc, path: "../../"},
- {:jason, "~> 1.3.0"},
- {:protobuf, "~> 0.14"},
- {:google_protos, "~> 0.3.0"},
- ]
- end
-end
diff --git a/examples/helloworld/mix.lock b/examples/helloworld/mix.lock
deleted file mode 100644
index dbf35d41b..000000000
--- a/examples/helloworld/mix.lock
+++ /dev/null
@@ -1,12 +0,0 @@
-%{
- "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
- "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
- "google_protos": {:hex, :google_protos, "0.3.0", "15faf44dce678ac028c289668ff56548806e313e4959a3aaf4f6e1ebe8db83f4", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1f6b7fb20371f72f418b98e5e48dae3e022a9a6de1858d4b254ac5a5d0b4035f"},
- "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"},
- "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
- "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
- "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
- "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"},
- "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
- "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
-}
diff --git a/examples/helloworld/priv/client.exs b/examples/helloworld/priv/client.exs
deleted file mode 100644
index 0209556fc..000000000
--- a/examples/helloworld/priv/client.exs
+++ /dev/null
@@ -1,9 +0,0 @@
-{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Client.Interceptors.Logger])
-
-{:ok, reply} =
- channel
- |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir"))
-
-# pass tuple `timeout: :infinity` as a second arg to stay in IEx debugging
-
-IO.inspect(reply)
diff --git a/examples/helloworld/priv/protos/google/api/annotations.proto b/examples/helloworld/priv/protos/google/api/annotations.proto
deleted file mode 100644
index efdab3db6..000000000
--- a/examples/helloworld/priv/protos/google/api/annotations.proto
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2015 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-syntax = "proto3";
-
-package google.api;
-
-import "google/api/http.proto";
-import "google/protobuf/descriptor.proto";
-
-option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
-option java_multiple_files = true;
-option java_outer_classname = "AnnotationsProto";
-option java_package = "com.google.api";
-option objc_class_prefix = "GAPI";
-
-extend google.protobuf.MethodOptions {
- // See `HttpRule`.
- HttpRule http = 72295728;
-}
diff --git a/examples/helloworld/priv/protos/google/api/http.proto b/examples/helloworld/priv/protos/google/api/http.proto
deleted file mode 100644
index 113fa936a..000000000
--- a/examples/helloworld/priv/protos/google/api/http.proto
+++ /dev/null
@@ -1,375 +0,0 @@
-// Copyright 2015 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-syntax = "proto3";
-
-package google.api;
-
-option cc_enable_arenas = true;
-option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
-option java_multiple_files = true;
-option java_outer_classname = "HttpProto";
-option java_package = "com.google.api";
-option objc_class_prefix = "GAPI";
-
-// Defines the HTTP configuration for an API service. It contains a list of
-// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
-// to one or more HTTP REST API methods.
-message Http {
- // A list of HTTP configuration rules that apply to individual API methods.
- //
- // **NOTE:** All service configuration rules follow "last one wins" order.
- repeated HttpRule rules = 1;
-
- // When set to true, URL path parameters will be fully URI-decoded except in
- // cases of single segment matches in reserved expansion, where "%2F" will be
- // left encoded.
- //
- // The default behavior is to not decode RFC 6570 reserved characters in multi
- // segment matches.
- bool fully_decode_reserved_expansion = 2;
-}
-
-// # gRPC Transcoding
-//
-// gRPC Transcoding is a feature for mapping between a gRPC method and one or
-// more HTTP REST endpoints. It allows developers to build a single API service
-// that supports both gRPC APIs and REST APIs. Many systems, including [Google
-// APIs](https://github.com/googleapis/googleapis),
-// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC
-// Gateway](https://github.com/grpc-ecosystem/grpc-gateway),
-// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature
-// and use it for large scale production services.
-//
-// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies
-// how different portions of the gRPC request message are mapped to the URL
-// path, URL query parameters, and HTTP request body. It also controls how the
-// gRPC response message is mapped to the HTTP response body. `HttpRule` is
-// typically specified as an `google.api.http` annotation on the gRPC method.
-//
-// Each mapping specifies a URL path template and an HTTP method. The path
-// template may refer to one or more fields in the gRPC request message, as long
-// as each field is a non-repeated field with a primitive (non-message) type.
-// The path template controls how fields of the request message are mapped to
-// the URL path.
-//
-// Example:
-//
-// service Messaging {
-// rpc GetMessage(GetMessageRequest) returns (Message) {
-// option (google.api.http) = {
-// get: "/v1/{name=messages/*}"
-// };
-// }
-// }
-// message GetMessageRequest {
-// string name = 1; // Mapped to URL path.
-// }
-// message Message {
-// string text = 1; // The resource content.
-// }
-//
-// This enables an HTTP REST to gRPC mapping as below:
-//
-// HTTP | gRPC
-// -----|-----
-// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")`
-//
-// Any fields in the request message which are not bound by the path template
-// automatically become HTTP query parameters if there is no HTTP request body.
-// For example:
-//
-// service Messaging {
-// rpc GetMessage(GetMessageRequest) returns (Message) {
-// option (google.api.http) = {
-// get:"/v1/messages/{message_id}"
-// };
-// }
-// }
-// message GetMessageRequest {
-// message SubMessage {
-// string subfield = 1;
-// }
-// string message_id = 1; // Mapped to URL path.
-// int64 revision = 2; // Mapped to URL query parameter `revision`.
-// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`.
-// }
-//
-// This enables a HTTP JSON to RPC mapping as below:
-//
-// HTTP | gRPC
-// -----|-----
-// `GET /v1/messages/123456?revision=2&sub.subfield=foo` |
-// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield:
-// "foo"))`
-//
-// Note that fields which are mapped to URL query parameters must have a
-// primitive type or a repeated primitive type or a non-repeated message type.
-// In the case of a repeated type, the parameter can be repeated in the URL
-// as `...?param=A¶m=B`. In the case of a message type, each field of the
-// message is mapped to a separate parameter, such as
-// `...?foo.a=A&foo.b=B&foo.c=C`.
-//
-// For HTTP methods that allow a request body, the `body` field
-// specifies the mapping. Consider a REST update method on the
-// message resource collection:
-//
-// service Messaging {
-// rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
-// option (google.api.http) = {
-// patch: "/v1/messages/{message_id}"
-// body: "message"
-// };
-// }
-// }
-// message UpdateMessageRequest {
-// string message_id = 1; // mapped to the URL
-// Message message = 2; // mapped to the body
-// }
-//
-// The following HTTP JSON to RPC mapping is enabled, where the
-// representation of the JSON in the request body is determined by
-// protos JSON encoding:
-//
-// HTTP | gRPC
-// -----|-----
-// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id:
-// "123456" message { text: "Hi!" })`
-//
-// The special name `*` can be used in the body mapping to define that
-// every field not bound by the path template should be mapped to the
-// request body. This enables the following alternative definition of
-// the update method:
-//
-// service Messaging {
-// rpc UpdateMessage(Message) returns (Message) {
-// option (google.api.http) = {
-// patch: "/v1/messages/{message_id}"
-// body: "*"
-// };
-// }
-// }
-// message Message {
-// string message_id = 1;
-// string text = 2;
-// }
-//
-//
-// The following HTTP JSON to RPC mapping is enabled:
-//
-// HTTP | gRPC
-// -----|-----
-// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id:
-// "123456" text: "Hi!")`
-//
-// Note that when using `*` in the body mapping, it is not possible to
-// have HTTP parameters, as all fields not bound by the path end in
-// the body. This makes this option more rarely used in practice when
-// defining REST APIs. The common usage of `*` is in custom methods
-// which don't use the URL at all for transferring data.
-//
-// It is possible to define multiple HTTP methods for one RPC by using
-// the `additional_bindings` option. Example:
-//
-// service Messaging {
-// rpc GetMessage(GetMessageRequest) returns (Message) {
-// option (google.api.http) = {
-// get: "/v1/messages/{message_id}"
-// additional_bindings {
-// get: "/v1/users/{user_id}/messages/{message_id}"
-// }
-// };
-// }
-// }
-// message GetMessageRequest {
-// string message_id = 1;
-// string user_id = 2;
-// }
-//
-// This enables the following two alternative HTTP JSON to RPC mappings:
-//
-// HTTP | gRPC
-// -----|-----
-// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
-// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id:
-// "123456")`
-//
-// ## Rules for HTTP mapping
-//
-// 1. Leaf request fields (recursive expansion nested messages in the request
-// message) are classified into three categories:
-// - Fields referred by the path template. They are passed via the URL path.
-// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP
-// request body.
-// - All other fields are passed via the URL query parameters, and the
-// parameter name is the field path in the request message. A repeated
-// field can be represented as multiple query parameters under the same
-// name.
-// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields
-// are passed via URL path and HTTP request body.
-// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all
-// fields are passed via URL path and URL query parameters.
-//
-// ### Path template syntax
-//
-// Template = "/" Segments [ Verb ] ;
-// Segments = Segment { "/" Segment } ;
-// Segment = "*" | "**" | LITERAL | Variable ;
-// Variable = "{" FieldPath [ "=" Segments ] "}" ;
-// FieldPath = IDENT { "." IDENT } ;
-// Verb = ":" LITERAL ;
-//
-// The syntax `*` matches a single URL path segment. The syntax `**` matches
-// zero or more URL path segments, which must be the last part of the URL path
-// except the `Verb`.
-//
-// The syntax `Variable` matches part of the URL path as specified by its
-// template. A variable template must not contain other variables. If a variable
-// matches a single path segment, its template may be omitted, e.g. `{var}`
-// is equivalent to `{var=*}`.
-//
-// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL`
-// contains any reserved character, such characters should be percent-encoded
-// before the matching.
-//
-// If a variable contains exactly one path segment, such as `"{var}"` or
-// `"{var=*}"`, when such a variable is expanded into a URL path on the client
-// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The
-// server side does the reverse decoding. Such variables show up in the
-// [Discovery
-// Document](https://developers.google.com/discovery/v1/reference/apis) as
-// `{var}`.
-//
-// If a variable contains multiple path segments, such as `"{var=foo/*}"`
-// or `"{var=**}"`, when such a variable is expanded into a URL path on the
-// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded.
-// The server side does the reverse decoding, except "%2F" and "%2f" are left
-// unchanged. Such variables show up in the
-// [Discovery
-// Document](https://developers.google.com/discovery/v1/reference/apis) as
-// `{+var}`.
-//
-// ## Using gRPC API Service Configuration
-//
-// gRPC API Service Configuration (service config) is a configuration language
-// for configuring a gRPC service to become a user-facing product. The
-// service config is simply the YAML representation of the `google.api.Service`
-// proto message.
-//
-// As an alternative to annotating your proto file, you can configure gRPC
-// transcoding in your service config YAML files. You do this by specifying a
-// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same
-// effect as the proto annotation. This can be particularly useful if you
-// have a proto that is reused in multiple services. Note that any transcoding
-// specified in the service config will override any matching transcoding
-// configuration in the proto.
-//
-// Example:
-//
-// http:
-// rules:
-// # Selects a gRPC method and applies HttpRule to it.
-// - selector: example.v1.Messaging.GetMessage
-// get: /v1/messages/{message_id}/{sub.subfield}
-//
-// ## Special notes
-//
-// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the
-// proto to JSON conversion must follow the [proto3
-// specification](https://developers.google.com/protocol-buffers/docs/proto3#json).
-//
-// While the single segment variable follows the semantics of
-// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String
-// Expansion, the multi segment variable **does not** follow RFC 6570 Section
-// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion
-// does not expand special characters like `?` and `#`, which would lead
-// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding
-// for multi segment variables.
-//
-// The path variables **must not** refer to any repeated or mapped field,
-// because client libraries are not capable of handling such variable expansion.
-//
-// The path variables **must not** capture the leading "/" character. The reason
-// is that the most common use case "{var}" does not capture the leading "/"
-// character. For consistency, all path variables must share the same behavior.
-//
-// Repeated message fields must not be mapped to URL query parameters, because
-// no client library can support such complicated mapping.
-//
-// If an API needs to use a JSON array for request or response body, it can map
-// the request or response body to a repeated field. However, some gRPC
-// Transcoding implementations may not support this feature.
-message HttpRule {
- // Selects a method to which this rule applies.
- //
- // Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
- string selector = 1;
-
- // Determines the URL pattern is matched by this rules. This pattern can be
- // used with any of the {get|put|post|delete|patch} methods. A custom method
- // can be defined using the 'custom' field.
- oneof pattern {
- // Maps to HTTP GET. Used for listing and getting information about
- // resources.
- string get = 2;
-
- // Maps to HTTP PUT. Used for replacing a resource.
- string put = 3;
-
- // Maps to HTTP POST. Used for creating a resource or performing an action.
- string post = 4;
-
- // Maps to HTTP DELETE. Used for deleting a resource.
- string delete = 5;
-
- // Maps to HTTP PATCH. Used for updating a resource.
- string patch = 6;
-
- // The custom pattern is used for specifying an HTTP method that is not
- // included in the `pattern` field, such as HEAD, or "*" to leave the
- // HTTP method unspecified for this rule. The wild-card rule is useful
- // for services that provide content to Web (HTML) clients.
- CustomHttpPattern custom = 8;
- }
-
- // The name of the request field whose value is mapped to the HTTP request
- // body, or `*` for mapping all request fields not captured by the path
- // pattern to the HTTP body, or omitted for not having any HTTP request body.
- //
- // NOTE: the referred field must be present at the top-level of the request
- // message type.
- string body = 7;
-
- // Optional. The name of the response field whose value is mapped to the HTTP
- // response body. When omitted, the entire response message will be used
- // as the HTTP response body.
- //
- // NOTE: The referred field must be present at the top-level of the response
- // message type.
- string response_body = 12;
-
- // Additional HTTP bindings for the selector. Nested bindings must
- // not contain an `additional_bindings` field themselves (that is,
- // the nesting may only be one level deep).
- repeated HttpRule additional_bindings = 11;
-}
-
-// A custom pattern is used for defining custom HTTP verb.
-message CustomHttpPattern {
- // The name of this custom HTTP verb.
- string kind = 1;
-
- // The path matched by this custom verb.
- string path = 2;
-}
diff --git a/examples/helloworld/priv/protos/helloworld.proto b/examples/helloworld/priv/protos/helloworld.proto
deleted file mode 100644
index 632519a06..000000000
--- a/examples/helloworld/priv/protos/helloworld.proto
+++ /dev/null
@@ -1,62 +0,0 @@
-syntax = "proto3";
-
-option java_multiple_files = true;
-option java_package = "io.grpc.examples.helloworld";
-option java_outer_classname = "HelloWorldProto";
-option objc_class_prefix = "HLW";
-
-import "google/api/annotations.proto";
-import "google/protobuf/timestamp.proto";
-
-package helloworld;
-
-// The greeting service definition.
-service Greeter {
- // Sends a greeting
- rpc SayHello (HelloRequest) returns (HelloReply) {
- option (google.api.http) = {
- get: "/v1/greeter/{name}"
- };
- }
-
- rpc SayHelloFrom (HelloRequestFrom) returns (HelloReply) {
- option (google.api.http) = {
- post: "/v1/greeter"
- body: "*"
- };
- }
-}
-
-// The request message containing the user's name.
-message HelloRequest {
- string name = 1;
-}
-
-// HelloRequestFrom!
-message HelloRequestFrom {
- // Name!
- string name = 1;
- // From!
- string from = 2;
-}
-
-// The response message containing the greetings
-message HelloReply {
- string message = 1;
- google.protobuf.Timestamp today = 2;
-}
-
-service Messaging {
- rpc GetMessage(GetMessageRequest) returns (Message) {
- option (google.api.http) = {
- get: "/v1/{name=messages/*}"
- };
- }
-}
-
-message GetMessageRequest {
- string name = 1; // Mapped to URL path.
-}
-message Message {
- string text = 1; // The resource content.
-}
diff --git a/examples/helloworld/test/hello_world_test.exs b/examples/helloworld/test/hello_world_test.exs
deleted file mode 100644
index 55c51ed54..000000000
--- a/examples/helloworld/test/hello_world_test.exs
+++ /dev/null
@@ -1,16 +0,0 @@
-defmodule HelloworldTest do
- @moduledoc false
-
- use ExUnit.Case
-
- setup_all do
- {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Client.Interceptors.Logger])
- [channel: channel]
- end
-
- test "helloworld should be successful", %{channel: channel} do
- req = Helloworld.HelloRequest.new(name: "grpc-elixir")
- assert {:ok, %{message: msg, today: _}} = Helloworld.Greeter.Stub.say_hello(channel, req)
- assert msg == "Hello grpc-elixir"
- end
-end
diff --git a/examples/helloworld/test/test_helper.exs b/examples/helloworld/test/test_helper.exs
deleted file mode 100644
index 869559e70..000000000
--- a/examples/helloworld/test/test_helper.exs
+++ /dev/null
@@ -1 +0,0 @@
-ExUnit.start()
diff --git a/examples/helloworld_streams/.formatter.exs b/examples/helloworld_streams/.formatter.exs
deleted file mode 100644
index d2cda26ed..000000000
--- a/examples/helloworld_streams/.formatter.exs
+++ /dev/null
@@ -1,4 +0,0 @@
-# Used by "mix format"
-[
- inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
-]
diff --git a/examples/helloworld_streams/.gitignore b/examples/helloworld_streams/.gitignore
deleted file mode 100644
index 965abf9ca..000000000
--- a/examples/helloworld_streams/.gitignore
+++ /dev/null
@@ -1,26 +0,0 @@
-# The directory Mix will write compiled artifacts to.
-/_build/
-
-# If you run "mix test --cover", coverage assets end up here.
-/cover/
-
-# The directory Mix downloads your dependencies sources to.
-/deps/
-
-# Where third-party dependencies like ExDoc output generated docs.
-/doc/
-
-# Ignore .fetch files in case you like to edit your project deps locally.
-/.fetch
-
-# If the VM crashes, it generates a dump, let's ignore it too.
-erl_crash.dump
-
-# Also ignore archive artifacts (built via "mix archive.build").
-*.ez
-
-# Ignore package tarball (built via "mix hex.build").
-helloworld_streams-*.tar
-
-# Temporary files, for example, from tests.
-/tmp/
diff --git a/examples/helloworld_streams/README.md b/examples/helloworld_streams/README.md
deleted file mode 100644
index 79b5c6342..000000000
--- a/examples/helloworld_streams/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# HelloworldStreams
-
-**TODO: Add description**
-
-## Installation
-
-If [available in Hex](https://hex.pm/docs/publish), the package can be installed
-by adding `helloworld_streams` to your list of dependencies in `mix.exs`:
-
-```elixir
-def deps do
- [
- {:helloworld_streams, "~> 0.1.0"}
- ]
-end
-```
-
-Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
-and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
-be found at .
-
diff --git a/examples/helloworld_streams/lib/helloworld_streams.ex b/examples/helloworld_streams/lib/helloworld_streams.ex
deleted file mode 100644
index 8c1285a1c..000000000
--- a/examples/helloworld_streams/lib/helloworld_streams.ex
+++ /dev/null
@@ -1,5 +0,0 @@
-defmodule HelloworldStreams do
- @moduledoc """
- Documentation for `HelloworldStreams`.
- """
-end
diff --git a/examples/helloworld_streams/lib/helloworld_streams/application.ex b/examples/helloworld_streams/lib/helloworld_streams/application.ex
deleted file mode 100644
index 44cfaa165..000000000
--- a/examples/helloworld_streams/lib/helloworld_streams/application.ex
+++ /dev/null
@@ -1,19 +0,0 @@
-defmodule HelloworldStreams.Application do
- @moduledoc false
- use Application
-
- @impl true
- def start(_type, _args) do
- children = [
- HelloworldStreams.Utils.Transformer,
- GrpcReflection,
- {
- GRPC.Server.Supervisor,
- endpoint: HelloworldStreams.Endpoint, port: 50053, start_server: true
- }
- ]
-
- opts = [strategy: :one_for_one, name: HelloworldStreams.Supervisor]
- Supervisor.start_link(children, opts)
- end
-end
diff --git a/examples/helloworld_streams/lib/helloworld_streams/endpoint.ex b/examples/helloworld_streams/lib/helloworld_streams/endpoint.ex
deleted file mode 100644
index c02a0c9bc..000000000
--- a/examples/helloworld_streams/lib/helloworld_streams/endpoint.ex
+++ /dev/null
@@ -1,8 +0,0 @@
-defmodule HelloworldStreams.Endpoint do
- @moduledoc false
- use GRPC.Endpoint
-
- intercept(GRPC.Server.Interceptors.Logger)
- run(HelloworldStreams.Utils.Reflection)
- run(HelloworldStreams.Server)
-end
diff --git a/examples/helloworld_streams/lib/helloworld_streams/server.ex b/examples/helloworld_streams/lib/helloworld_streams/server.ex
deleted file mode 100644
index 76771939f..000000000
--- a/examples/helloworld_streams/lib/helloworld_streams/server.ex
+++ /dev/null
@@ -1,61 +0,0 @@
-defmodule HelloworldStreams.Server do
- @moduledoc """
- gRPC service for streaming data.
- """
- use GRPC.Server, service: Stream.EchoServer.Service
-
- alias HelloworldStreams.Utils.Transformer
- alias GRPC.Stream, as: GRPCStream
-
- alias Stream.HelloRequest
- alias Stream.HelloReply
-
- @spec say_unary_hello(HelloRequest.t(), GRPC.Server.Stream.t()) :: any()
- def say_unary_hello(request, _materializer) do
- GRPCStream.unary(request)
- |> GRPCStream.ask(Transformer)
- |> GRPCStream.map(fn %HelloReply{} = reply ->
- %HelloReply{message: "[Reply] #{reply.message}"}
- end)
- |> GRPCStream.run()
- end
-
- @spec say_server_hello(HelloRequest.t(), GRPC.Server.Stream.t()) :: any()
- def say_server_hello(request, materializer) do
- create_output_stream(request)
- |> GRPCStream.from()
- |> GRPCStream.run_with(materializer)
- end
-
- defp create_output_stream(msg) do
- Stream.repeatedly(fn ->
- index = :rand.uniform(10)
- %HelloReply{message: "[#{index}] I'm the Server for #{msg.name}"}
- end)
- |> Stream.take(10)
- |> Enum.to_list()
- end
-
- @spec say_bid_stream_hello(Enumerable.t(), GRPC.Server.Stream.t()) :: any()
- def say_bid_stream_hello(request, materializer) do
- # simulate a infinite stream of data
- # this is a simple example, in a real world application
- # you would probably use a GenStage or similar
- # to handle the stream of data
- output_stream =
- Stream.repeatedly(fn ->
- index = :rand.uniform(10)
- %HelloReply{message: "[#{index}] I'm the Server ;)"}
- end)
-
- GRPCStream.from(request, join_with: output_stream)
- |> GRPCStream.map(fn
- %HelloRequest{} = hello ->
- %HelloReply{message: "Welcome #{hello.name}"}
-
- output_item ->
- output_item
- end)
- |> GRPCStream.run_with(materializer)
- end
-end
diff --git a/examples/helloworld_streams/lib/helloworld_streams/stream.pb.ex b/examples/helloworld_streams/lib/helloworld_streams/stream.pb.ex
deleted file mode 100644
index b060b543f..000000000
--- a/examples/helloworld_streams/lib/helloworld_streams/stream.pb.ex
+++ /dev/null
@@ -1,156 +0,0 @@
-defmodule Stream.HelloRequest do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.0", syntax: :proto3
-
- def descriptor do
- # credo:disable-for-next-line
- %Google.Protobuf.DescriptorProto{
- name: "HelloRequest",
- field: [
- %Google.Protobuf.FieldDescriptorProto{
- name: "name",
- extendee: nil,
- number: 1,
- label: :LABEL_OPTIONAL,
- type: :TYPE_STRING,
- type_name: nil,
- default_value: nil,
- options: nil,
- oneof_index: nil,
- json_name: "name",
- proto3_optional: nil,
- __unknown_fields__: []
- }
- ],
- nested_type: [],
- enum_type: [],
- extension_range: [],
- extension: [],
- options: nil,
- oneof_decl: [],
- reserved_range: [],
- reserved_name: [],
- __unknown_fields__: []
- }
- end
-
- field(:name, 1, type: :string)
-end
-
-defmodule Stream.HelloReply do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.0", syntax: :proto3
-
- def descriptor do
- # credo:disable-for-next-line
- %Google.Protobuf.DescriptorProto{
- name: "HelloReply",
- field: [
- %Google.Protobuf.FieldDescriptorProto{
- name: "message",
- extendee: nil,
- number: 1,
- label: :LABEL_OPTIONAL,
- type: :TYPE_STRING,
- type_name: nil,
- default_value: nil,
- options: nil,
- oneof_index: nil,
- json_name: "message",
- proto3_optional: nil,
- __unknown_fields__: []
- }
- ],
- nested_type: [],
- enum_type: [],
- extension_range: [],
- extension: [],
- options: nil,
- oneof_decl: [],
- reserved_range: [],
- reserved_name: [],
- __unknown_fields__: []
- }
- end
-
- field(:message, 1, type: :string)
-end
-
-defmodule Stream.EchoServer.Service do
- @moduledoc false
-
- use GRPC.Service, name: "stream.EchoServer", protoc_gen_elixir_version: "0.14.0"
-
- def descriptor do
- # credo:disable-for-next-line
- %Google.Protobuf.ServiceDescriptorProto{
- name: "EchoServer",
- method: [
- %Google.Protobuf.MethodDescriptorProto{
- name: "SayUnaryHello",
- input_type: ".stream.HelloRequest",
- output_type: ".stream.HelloReply",
- options: %Google.Protobuf.MethodOptions{
- deprecated: false,
- idempotency_level: :IDEMPOTENCY_UNKNOWN,
- features: nil,
- uninterpreted_option: [],
- __pb_extensions__: %{},
- __unknown_fields__: []
- },
- client_streaming: false,
- server_streaming: false,
- __unknown_fields__: []
- },
- %Google.Protobuf.MethodDescriptorProto{
- name: "SayServerHello",
- input_type: ".stream.HelloRequest",
- output_type: ".stream.HelloReply",
- options: %Google.Protobuf.MethodOptions{
- deprecated: false,
- idempotency_level: :IDEMPOTENCY_UNKNOWN,
- features: nil,
- uninterpreted_option: [],
- __pb_extensions__: %{},
- __unknown_fields__: []
- },
- client_streaming: false,
- server_streaming: true,
- __unknown_fields__: []
- },
- %Google.Protobuf.MethodDescriptorProto{
- name: "SayBidStreamHello",
- input_type: ".stream.HelloRequest",
- output_type: ".stream.HelloReply",
- options: %Google.Protobuf.MethodOptions{
- deprecated: false,
- idempotency_level: :IDEMPOTENCY_UNKNOWN,
- features: nil,
- uninterpreted_option: [],
- __pb_extensions__: %{},
- __unknown_fields__: []
- },
- client_streaming: true,
- server_streaming: true,
- __unknown_fields__: []
- }
- ],
- options: nil,
- __unknown_fields__: []
- }
- end
-
- rpc(:SayUnaryHello, Stream.HelloRequest, Stream.HelloReply)
-
- rpc(:SayServerHello, Stream.HelloRequest, stream(Stream.HelloReply))
-
- rpc(:SayBidStreamHello, stream(Stream.HelloRequest), stream(Stream.HelloReply))
-end
-
-defmodule Stream.EchoServer.Stub do
- @moduledoc false
-
- use GRPC.Stub, service: Stream.EchoServer.Service
-end
diff --git a/examples/helloworld_streams/lib/helloworld_streams/utils/reflection.ex b/examples/helloworld_streams/lib/helloworld_streams/utils/reflection.ex
deleted file mode 100644
index a5e25e598..000000000
--- a/examples/helloworld_streams/lib/helloworld_streams/utils/reflection.ex
+++ /dev/null
@@ -1,8 +0,0 @@
-defmodule HelloworldStreams.Utils.Reflection do
- @moduledoc """
- gRPC reflection server.
- """
- use GrpcReflection.Server,
- version: :v1,
- services: [Stream.EchoServer.Service]
-end
diff --git a/examples/helloworld_streams/lib/helloworld_streams/utils/transformer.ex b/examples/helloworld_streams/lib/helloworld_streams/utils/transformer.ex
deleted file mode 100644
index ed9de0524..000000000
--- a/examples/helloworld_streams/lib/helloworld_streams/utils/transformer.ex
+++ /dev/null
@@ -1,20 +0,0 @@
-defmodule HelloworldStreams.Utils.Transformer do
- @moduledoc """
- `Transformer` GenServer for example purposes.
- """
- use GenServer
-
- alias Stream.HelloRequest
- alias Stream.HelloReply
-
- def start_link(_) do
- GenServer.start_link(__MODULE__, nil, name: __MODULE__)
- end
-
- def init(_), do: {:ok, %{}}
-
- def handle_info({:request, %HelloRequest{} = value, from}, state) do
- Process.send(from, {:response, %HelloReply{message: "Hello #{value.name}"}}, [])
- {:noreply, state}
- end
-end
diff --git a/examples/helloworld_streams/mix.exs b/examples/helloworld_streams/mix.exs
deleted file mode 100644
index 8107d0bbd..000000000
--- a/examples/helloworld_streams/mix.exs
+++ /dev/null
@@ -1,28 +0,0 @@
-defmodule HelloworldStreams.MixProject do
- use Mix.Project
-
- def project do
- [
- app: :helloworld_streams,
- version: "0.1.0",
- elixir: "~> 1.15",
- start_permanent: Mix.env() == :prod,
- deps: deps()
- ]
- end
-
- def application do
- [
- extra_applications: [:logger],
- mod: {HelloworldStreams.Application, []}
- ]
- end
-
- defp deps do
- [
- {:grpc, path: "../../", override: true},
- {:protobuf, "~> 0.14"},
- {:grpc_reflection, "~> 0.1"}
- ]
- end
-end
diff --git a/examples/helloworld_streams/mix.lock b/examples/helloworld_streams/mix.lock
deleted file mode 100644
index ee24db4b2..000000000
--- a/examples/helloworld_streams/mix.lock
+++ /dev/null
@@ -1,14 +0,0 @@
-%{
- "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
- "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
- "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"},
- "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
- "google_protos": {:hex, :google_protos, "0.3.0", "15faf44dce678ac028c289668ff56548806e313e4959a3aaf4f6e1ebe8db83f4", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1f6b7fb20371f72f418b98e5e48dae3e022a9a6de1858d4b254ac5a5d0b4035f"},
- "grpc_reflection": {:hex, :grpc_reflection, "0.1.5", "d00cdf8ef2638edb9578248eedc742e1b34eda9100e61be764c552c10f4b46cb", [:mix], [{:grpc, "~> 0.9", [hex: :grpc, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.14", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "848334d16029aee33728603be6171fc8bfcdfa3508cd6885ec1729e2e6ac60a5"},
- "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"},
- "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
- "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
- "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"},
- "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
- "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
-}
diff --git a/examples/helloworld_streams/priv/protos/stream.proto b/examples/helloworld_streams/priv/protos/stream.proto
deleted file mode 100644
index e0ad2ac0a..000000000
--- a/examples/helloworld_streams/priv/protos/stream.proto
+++ /dev/null
@@ -1,17 +0,0 @@
-syntax = "proto3";
-
-package stream;
-
-message HelloRequest {
- string name = 1;
-}
-
-message HelloReply {
- string message = 1;
-}
-
-service EchoServer {
- rpc SayUnaryHello (HelloRequest) returns (HelloReply) {}
- rpc SayServerHello (HelloRequest) returns (stream HelloReply) {}
- rpc SayBidStreamHello (stream HelloRequest) returns (stream HelloReply) {}
-}
diff --git a/examples/helloworld_streams/test/helloworld_streams_test.exs b/examples/helloworld_streams/test/helloworld_streams_test.exs
deleted file mode 100644
index 838751bbe..000000000
--- a/examples/helloworld_streams/test/helloworld_streams_test.exs
+++ /dev/null
@@ -1,4 +0,0 @@
-defmodule HelloworldStreamsTest do
- use ExUnit.Case
- doctest HelloworldStreams
-end
diff --git a/examples/helloworld_streams/test/test_helper.exs b/examples/helloworld_streams/test/test_helper.exs
deleted file mode 100644
index 869559e70..000000000
--- a/examples/helloworld_streams/test/test_helper.exs
+++ /dev/null
@@ -1 +0,0 @@
-ExUnit.start()
diff --git a/examples/helloworld_transcoding/.gitignore b/examples/helloworld_transcoding/.gitignore
deleted file mode 100644
index 06dbcb6f6..000000000
--- a/examples/helloworld_transcoding/.gitignore
+++ /dev/null
@@ -1,23 +0,0 @@
-# The directory Mix will write compiled artifacts to.
-/_build
-
-# If you run "mix test --cover", coverage assets end up here.
-/cover
-
-# The directory Mix downloads your dependencies sources to.
-/deps
-
-# Where 3rd-party dependencies like ExDoc output generated docs.
-/doc
-
-# If the VM crashes, it generates a dump, let's ignore it too.
-erl_crash.dump
-
-# Also ignore archive artifacts (built via "mix archive.build").
-*.ez
-
-/priv/grpc_c.so*
-/src/grpc_c
-/tmp
-
-/log
\ No newline at end of file
diff --git a/examples/helloworld_transcoding/README.md b/examples/helloworld_transcoding/README.md
deleted file mode 100644
index 66624ce0c..000000000
--- a/examples/helloworld_transcoding/README.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# Helloworld with HTTP/json transcoding in grpc-elixir
-
-## Usage
-
-1. Install deps and compile
-```shell
-$ mix do deps.get, compile
-```
-
-2. Run the server
-```shell
-$ mix run --no-halt
-```
-
-3. Run the client script
-```shell
-$ mix run priv/client.exs
-```
-
-## HTTP Transcoding
-
-``` shell
-# Say hello
-$ curl -H 'accept: application/json' http://localhost:50051/v1/greeter/test
-
-# Say hello from
-$ curl -XPOST -H 'Content-type: application/json' -d '{"name": "test", "from": "anon"}' http://localhost:50051/v1/greeter
-```
-
-## Regenerate Elixir code from proto
-
-1. Modify the proto `priv/protos/helloworld.proto`
-
-2. Install `protoc` [here](https://developers.google.com/protocol-buffers/docs/downloads)
-
-```
-mix deps.get
-```
-
-4. Generate `google.api.http` extensions:
-
-``` shell
-$ mix protobuf.generate --include-path=priv/protos --output-path=./lib priv/protos/google/api/annotations.proto priv/protos/google/api/http.proto
-```
-
-4. Generate the code:
-
-```shell
-$ mix protobuf.generate --include-path=priv/protos --plugins=ProtobufGenerate.Plugins.GRPCWithOptions --output-path=./lib priv/protos/helloworld.proto
-```
-
-Refer to [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) for more information.
-
-## How to start server when starting your application?
-
-Pass `start_server: true` as an option for the `GRPC.Server.Supervisor` in your supervision tree.
-
-## Benchmark
-
-Using [ghz](https://ghz.sh/)
-
-```
-$ MIX_ENV=prod iex -S mix
-# Now cowboy doesn't work well with concurrency in a connection, like --concurrency 6 --connections 1
-$ ghz --insecure --proto priv/protos/helloworld.proto --call helloworld.Greeter.SayHello -d '{"name":"Joe"}' -z 10s --concurrency 6 --connections 6 127.0.0.1:50051
-# The result is for branch improve-perf
-Summary:
- Count: 124239
- Total: 10.00 s
- Slowest: 18.85 ms
- Fastest: 0.18 ms
- Average: 0.44 ms
- Requests/sec: 12423.71
-
-# Go
-Summary:
- Count: 258727
- Total: 10.00 s
- Slowest: 5.39 ms
- Fastest: 0.09 ms
- Average: 0.19 ms
- Requests/sec: 25861.68
-```
diff --git a/examples/helloworld_transcoding/config/config.exs b/examples/helloworld_transcoding/config/config.exs
deleted file mode 100644
index 9def7c2c5..000000000
--- a/examples/helloworld_transcoding/config/config.exs
+++ /dev/null
@@ -1,3 +0,0 @@
-import Config
-
-import_config "#{Mix.env}.exs"
diff --git a/examples/helloworld_transcoding/config/dev.exs b/examples/helloworld_transcoding/config/dev.exs
deleted file mode 100644
index becde7693..000000000
--- a/examples/helloworld_transcoding/config/dev.exs
+++ /dev/null
@@ -1 +0,0 @@
-import Config
diff --git a/examples/helloworld_transcoding/config/prod.exs b/examples/helloworld_transcoding/config/prod.exs
deleted file mode 100644
index 2dd33c31d..000000000
--- a/examples/helloworld_transcoding/config/prod.exs
+++ /dev/null
@@ -1,4 +0,0 @@
-import Config
-
-config :logger,
- level: :warn
diff --git a/examples/helloworld_transcoding/config/test.exs b/examples/helloworld_transcoding/config/test.exs
deleted file mode 100644
index becde7693..000000000
--- a/examples/helloworld_transcoding/config/test.exs
+++ /dev/null
@@ -1 +0,0 @@
-import Config
diff --git a/examples/helloworld_transcoding/lib/endpoint.ex b/examples/helloworld_transcoding/lib/endpoint.ex
deleted file mode 100644
index c8bc64f69..000000000
--- a/examples/helloworld_transcoding/lib/endpoint.ex
+++ /dev/null
@@ -1,6 +0,0 @@
-defmodule Helloworld.Endpoint do
- use GRPC.Endpoint
-
- intercept GRPC.Server.Interceptors.Logger
- run Helloworld.Greeter.Server
-end
diff --git a/examples/helloworld_transcoding/lib/helloworld.pb.ex b/examples/helloworld_transcoding/lib/helloworld.pb.ex
deleted file mode 100644
index ef45cc5ca..000000000
--- a/examples/helloworld_transcoding/lib/helloworld.pb.ex
+++ /dev/null
@@ -1,55 +0,0 @@
-defmodule Helloworld.HelloRequest do
- @moduledoc false
- use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
-
- field :name, 1, type: :string
-end
-
-defmodule Helloworld.HelloRequestFrom do
- @moduledoc false
- use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
-
- field :name, 1, type: :string
- field :from, 2, type: :string
-end
-
-defmodule Helloworld.HelloReply do
- @moduledoc false
- use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
-
- field :message, 1, type: :string
- field :today, 2, type: Google.Protobuf.Timestamp
-end
-
-defmodule Helloworld.Greeter.Service do
- @moduledoc false
- use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.14.1"
-
- rpc(:SayHello, Helloworld.HelloRequest, Helloworld.HelloReply, %{
- http: %{
- type: Google.Api.PbExtension,
- value: %Google.Api.HttpRule{
- selector: "",
- body: "",
- additional_bindings: [],
- response_body: "",
- pattern: {:get, "/v1/greeter/{name}"},
- __unknown_fields__: []
- }
- }
- })
-
- rpc(:SayHelloFrom, Helloworld.HelloRequestFrom, Helloworld.HelloReply, %{
- http: %{
- type: Google.Api.PbExtension,
- value: %Google.Api.HttpRule{
- selector: "",
- body: "*",
- additional_bindings: [],
- response_body: "",
- pattern: {:post, "/v1/greeter"},
- __unknown_fields__: []
- }
- }
- })
-end
diff --git a/examples/helloworld_transcoding/lib/helloworld_app.ex b/examples/helloworld_transcoding/lib/helloworld_app.ex
deleted file mode 100644
index d84d62a58..000000000
--- a/examples/helloworld_transcoding/lib/helloworld_app.ex
+++ /dev/null
@@ -1,12 +0,0 @@
-defmodule HelloworldApp do
- use Application
-
- def start(_type, _args) do
- children = [
- {GRPC.Server.Supervisor, endpoint: Helloworld.Endpoint, port: 50051, start_server: true}
- ]
-
- opts = [strategy: :one_for_one, name: HelloworldApp]
- Supervisor.start_link(children, opts)
- end
-end
diff --git a/examples/helloworld_transcoding/lib/server.ex b/examples/helloworld_transcoding/lib/server.ex
deleted file mode 100644
index 8786949fe..000000000
--- a/examples/helloworld_transcoding/lib/server.ex
+++ /dev/null
@@ -1,31 +0,0 @@
-defmodule Helloworld.Greeter.Server do
- use GRPC.Server,
- service: Helloworld.Greeter.Service,
- http_transcode: true
-
- @spec say_hello(Helloworld.HelloRequest.t(), GRPC.Server.Stream.t()) ::
- Helloworld.HelloReply.t()
- def say_hello(request, _stream) do
- %Helloworld.HelloReply{
- message: "Hello #{request.name}",
- today: today()
- }
- end
-
- @spec say_hello_from(Helloworld.HelloFromRequest.t(), GRPC.Server.Stream.t()) ::
- Helloworld.HelloReply.t()
- def say_hello_from(request, _stream) do
- %Helloworld.HelloReply{
- message: "Hello #{request.name}. From #{request.from}",
- today: today()
- }
- end
-
- defp today do
- nanos_epoch = System.system_time() |> System.convert_time_unit(:native, :nanosecond)
- seconds = div(nanos_epoch, 1_000_000_000)
- nanos = nanos_epoch - seconds * 1_000_000_000
-
- %Google.Protobuf.Timestamp{seconds: seconds, nanos: nanos}
- end
-end
diff --git a/examples/helloworld_transcoding/mix.exs b/examples/helloworld_transcoding/mix.exs
deleted file mode 100644
index c67c06c53..000000000
--- a/examples/helloworld_transcoding/mix.exs
+++ /dev/null
@@ -1,28 +0,0 @@
-defmodule Helloworld.Mixfile do
- use Mix.Project
-
- def project do
- [
- app: :helloworld,
- version: "0.1.0",
- elixir: "~> 1.4",
- build_embedded: Mix.env() == :prod,
- start_permanent: Mix.env() == :prod,
- deps: deps()
- ]
- end
-
- def application do
- [mod: {HelloworldApp, []}, applications: [:logger, :grpc]]
- end
-
- defp deps do
- [
- {:grpc, path: "../../"},
- {:protobuf, "~> 0.14"},
- {:jason, "~> 1.3.0"},
- {:google_protos, "~> 0.3.0"},
- {:protobuf_generate, "~> 0.1", only: [:dev, :test]}
- ]
- end
-end
diff --git a/examples/helloworld_transcoding/mix.lock b/examples/helloworld_transcoding/mix.lock
deleted file mode 100644
index 439d97100..000000000
--- a/examples/helloworld_transcoding/mix.lock
+++ /dev/null
@@ -1,13 +0,0 @@
-%{
- "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
- "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
- "google_protos": {:hex, :google_protos, "0.3.0", "15faf44dce678ac028c289668ff56548806e313e4959a3aaf4f6e1ebe8db83f4", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1f6b7fb20371f72f418b98e5e48dae3e022a9a6de1858d4b254ac5a5d0b4035f"},
- "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"},
- "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
- "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
- "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
- "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"},
- "protobuf_generate": {:hex, :protobuf_generate, "0.1.1", "f6098b85161dcfd48a4f6f1abee4ee5e057981dfc50aafb1aa4bd5b0529aa89b", [:mix], [{:protobuf, "~> 0.11", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "93a38c8e2aba2a17e293e9ef1359122741f717103984aa6d1ebdca0efb17ab9d"},
- "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
- "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
-}
diff --git a/examples/helloworld_transcoding/priv/client.exs b/examples/helloworld_transcoding/priv/client.exs
deleted file mode 100644
index dc6bea5df..000000000
--- a/examples/helloworld_transcoding/priv/client.exs
+++ /dev/null
@@ -1,9 +0,0 @@
-{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client])
-
-{:ok, reply} =
- channel
- |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir"))
-
-# pass tuple `timeout: :infinity` as a second arg to stay in IEx debugging
-
-IO.inspect(reply)
diff --git a/examples/helloworld_transcoding/priv/protos/google/api/annotations.proto b/examples/helloworld_transcoding/priv/protos/google/api/annotations.proto
deleted file mode 100644
index efdab3db6..000000000
--- a/examples/helloworld_transcoding/priv/protos/google/api/annotations.proto
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2015 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-syntax = "proto3";
-
-package google.api;
-
-import "google/api/http.proto";
-import "google/protobuf/descriptor.proto";
-
-option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
-option java_multiple_files = true;
-option java_outer_classname = "AnnotationsProto";
-option java_package = "com.google.api";
-option objc_class_prefix = "GAPI";
-
-extend google.protobuf.MethodOptions {
- // See `HttpRule`.
- HttpRule http = 72295728;
-}
diff --git a/examples/helloworld_transcoding/priv/protos/google/api/http.proto b/examples/helloworld_transcoding/priv/protos/google/api/http.proto
deleted file mode 100644
index 113fa936a..000000000
--- a/examples/helloworld_transcoding/priv/protos/google/api/http.proto
+++ /dev/null
@@ -1,375 +0,0 @@
-// Copyright 2015 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-syntax = "proto3";
-
-package google.api;
-
-option cc_enable_arenas = true;
-option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
-option java_multiple_files = true;
-option java_outer_classname = "HttpProto";
-option java_package = "com.google.api";
-option objc_class_prefix = "GAPI";
-
-// Defines the HTTP configuration for an API service. It contains a list of
-// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
-// to one or more HTTP REST API methods.
-message Http {
- // A list of HTTP configuration rules that apply to individual API methods.
- //
- // **NOTE:** All service configuration rules follow "last one wins" order.
- repeated HttpRule rules = 1;
-
- // When set to true, URL path parameters will be fully URI-decoded except in
- // cases of single segment matches in reserved expansion, where "%2F" will be
- // left encoded.
- //
- // The default behavior is to not decode RFC 6570 reserved characters in multi
- // segment matches.
- bool fully_decode_reserved_expansion = 2;
-}
-
-// # gRPC Transcoding
-//
-// gRPC Transcoding is a feature for mapping between a gRPC method and one or
-// more HTTP REST endpoints. It allows developers to build a single API service
-// that supports both gRPC APIs and REST APIs. Many systems, including [Google
-// APIs](https://github.com/googleapis/googleapis),
-// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC
-// Gateway](https://github.com/grpc-ecosystem/grpc-gateway),
-// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature
-// and use it for large scale production services.
-//
-// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies
-// how different portions of the gRPC request message are mapped to the URL
-// path, URL query parameters, and HTTP request body. It also controls how the
-// gRPC response message is mapped to the HTTP response body. `HttpRule` is
-// typically specified as an `google.api.http` annotation on the gRPC method.
-//
-// Each mapping specifies a URL path template and an HTTP method. The path
-// template may refer to one or more fields in the gRPC request message, as long
-// as each field is a non-repeated field with a primitive (non-message) type.
-// The path template controls how fields of the request message are mapped to
-// the URL path.
-//
-// Example:
-//
-// service Messaging {
-// rpc GetMessage(GetMessageRequest) returns (Message) {
-// option (google.api.http) = {
-// get: "/v1/{name=messages/*}"
-// };
-// }
-// }
-// message GetMessageRequest {
-// string name = 1; // Mapped to URL path.
-// }
-// message Message {
-// string text = 1; // The resource content.
-// }
-//
-// This enables an HTTP REST to gRPC mapping as below:
-//
-// HTTP | gRPC
-// -----|-----
-// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")`
-//
-// Any fields in the request message which are not bound by the path template
-// automatically become HTTP query parameters if there is no HTTP request body.
-// For example:
-//
-// service Messaging {
-// rpc GetMessage(GetMessageRequest) returns (Message) {
-// option (google.api.http) = {
-// get:"/v1/messages/{message_id}"
-// };
-// }
-// }
-// message GetMessageRequest {
-// message SubMessage {
-// string subfield = 1;
-// }
-// string message_id = 1; // Mapped to URL path.
-// int64 revision = 2; // Mapped to URL query parameter `revision`.
-// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`.
-// }
-//
-// This enables a HTTP JSON to RPC mapping as below:
-//
-// HTTP | gRPC
-// -----|-----
-// `GET /v1/messages/123456?revision=2&sub.subfield=foo` |
-// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield:
-// "foo"))`
-//
-// Note that fields which are mapped to URL query parameters must have a
-// primitive type or a repeated primitive type or a non-repeated message type.
-// In the case of a repeated type, the parameter can be repeated in the URL
-// as `...?param=A¶m=B`. In the case of a message type, each field of the
-// message is mapped to a separate parameter, such as
-// `...?foo.a=A&foo.b=B&foo.c=C`.
-//
-// For HTTP methods that allow a request body, the `body` field
-// specifies the mapping. Consider a REST update method on the
-// message resource collection:
-//
-// service Messaging {
-// rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
-// option (google.api.http) = {
-// patch: "/v1/messages/{message_id}"
-// body: "message"
-// };
-// }
-// }
-// message UpdateMessageRequest {
-// string message_id = 1; // mapped to the URL
-// Message message = 2; // mapped to the body
-// }
-//
-// The following HTTP JSON to RPC mapping is enabled, where the
-// representation of the JSON in the request body is determined by
-// protos JSON encoding:
-//
-// HTTP | gRPC
-// -----|-----
-// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id:
-// "123456" message { text: "Hi!" })`
-//
-// The special name `*` can be used in the body mapping to define that
-// every field not bound by the path template should be mapped to the
-// request body. This enables the following alternative definition of
-// the update method:
-//
-// service Messaging {
-// rpc UpdateMessage(Message) returns (Message) {
-// option (google.api.http) = {
-// patch: "/v1/messages/{message_id}"
-// body: "*"
-// };
-// }
-// }
-// message Message {
-// string message_id = 1;
-// string text = 2;
-// }
-//
-//
-// The following HTTP JSON to RPC mapping is enabled:
-//
-// HTTP | gRPC
-// -----|-----
-// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id:
-// "123456" text: "Hi!")`
-//
-// Note that when using `*` in the body mapping, it is not possible to
-// have HTTP parameters, as all fields not bound by the path end in
-// the body. This makes this option more rarely used in practice when
-// defining REST APIs. The common usage of `*` is in custom methods
-// which don't use the URL at all for transferring data.
-//
-// It is possible to define multiple HTTP methods for one RPC by using
-// the `additional_bindings` option. Example:
-//
-// service Messaging {
-// rpc GetMessage(GetMessageRequest) returns (Message) {
-// option (google.api.http) = {
-// get: "/v1/messages/{message_id}"
-// additional_bindings {
-// get: "/v1/users/{user_id}/messages/{message_id}"
-// }
-// };
-// }
-// }
-// message GetMessageRequest {
-// string message_id = 1;
-// string user_id = 2;
-// }
-//
-// This enables the following two alternative HTTP JSON to RPC mappings:
-//
-// HTTP | gRPC
-// -----|-----
-// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
-// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id:
-// "123456")`
-//
-// ## Rules for HTTP mapping
-//
-// 1. Leaf request fields (recursive expansion nested messages in the request
-// message) are classified into three categories:
-// - Fields referred by the path template. They are passed via the URL path.
-// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP
-// request body.
-// - All other fields are passed via the URL query parameters, and the
-// parameter name is the field path in the request message. A repeated
-// field can be represented as multiple query parameters under the same
-// name.
-// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields
-// are passed via URL path and HTTP request body.
-// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all
-// fields are passed via URL path and URL query parameters.
-//
-// ### Path template syntax
-//
-// Template = "/" Segments [ Verb ] ;
-// Segments = Segment { "/" Segment } ;
-// Segment = "*" | "**" | LITERAL | Variable ;
-// Variable = "{" FieldPath [ "=" Segments ] "}" ;
-// FieldPath = IDENT { "." IDENT } ;
-// Verb = ":" LITERAL ;
-//
-// The syntax `*` matches a single URL path segment. The syntax `**` matches
-// zero or more URL path segments, which must be the last part of the URL path
-// except the `Verb`.
-//
-// The syntax `Variable` matches part of the URL path as specified by its
-// template. A variable template must not contain other variables. If a variable
-// matches a single path segment, its template may be omitted, e.g. `{var}`
-// is equivalent to `{var=*}`.
-//
-// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL`
-// contains any reserved character, such characters should be percent-encoded
-// before the matching.
-//
-// If a variable contains exactly one path segment, such as `"{var}"` or
-// `"{var=*}"`, when such a variable is expanded into a URL path on the client
-// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The
-// server side does the reverse decoding. Such variables show up in the
-// [Discovery
-// Document](https://developers.google.com/discovery/v1/reference/apis) as
-// `{var}`.
-//
-// If a variable contains multiple path segments, such as `"{var=foo/*}"`
-// or `"{var=**}"`, when such a variable is expanded into a URL path on the
-// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded.
-// The server side does the reverse decoding, except "%2F" and "%2f" are left
-// unchanged. Such variables show up in the
-// [Discovery
-// Document](https://developers.google.com/discovery/v1/reference/apis) as
-// `{+var}`.
-//
-// ## Using gRPC API Service Configuration
-//
-// gRPC API Service Configuration (service config) is a configuration language
-// for configuring a gRPC service to become a user-facing product. The
-// service config is simply the YAML representation of the `google.api.Service`
-// proto message.
-//
-// As an alternative to annotating your proto file, you can configure gRPC
-// transcoding in your service config YAML files. You do this by specifying a
-// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same
-// effect as the proto annotation. This can be particularly useful if you
-// have a proto that is reused in multiple services. Note that any transcoding
-// specified in the service config will override any matching transcoding
-// configuration in the proto.
-//
-// Example:
-//
-// http:
-// rules:
-// # Selects a gRPC method and applies HttpRule to it.
-// - selector: example.v1.Messaging.GetMessage
-// get: /v1/messages/{message_id}/{sub.subfield}
-//
-// ## Special notes
-//
-// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the
-// proto to JSON conversion must follow the [proto3
-// specification](https://developers.google.com/protocol-buffers/docs/proto3#json).
-//
-// While the single segment variable follows the semantics of
-// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String
-// Expansion, the multi segment variable **does not** follow RFC 6570 Section
-// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion
-// does not expand special characters like `?` and `#`, which would lead
-// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding
-// for multi segment variables.
-//
-// The path variables **must not** refer to any repeated or mapped field,
-// because client libraries are not capable of handling such variable expansion.
-//
-// The path variables **must not** capture the leading "/" character. The reason
-// is that the most common use case "{var}" does not capture the leading "/"
-// character. For consistency, all path variables must share the same behavior.
-//
-// Repeated message fields must not be mapped to URL query parameters, because
-// no client library can support such complicated mapping.
-//
-// If an API needs to use a JSON array for request or response body, it can map
-// the request or response body to a repeated field. However, some gRPC
-// Transcoding implementations may not support this feature.
-message HttpRule {
- // Selects a method to which this rule applies.
- //
- // Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
- string selector = 1;
-
- // Determines the URL pattern is matched by this rules. This pattern can be
- // used with any of the {get|put|post|delete|patch} methods. A custom method
- // can be defined using the 'custom' field.
- oneof pattern {
- // Maps to HTTP GET. Used for listing and getting information about
- // resources.
- string get = 2;
-
- // Maps to HTTP PUT. Used for replacing a resource.
- string put = 3;
-
- // Maps to HTTP POST. Used for creating a resource or performing an action.
- string post = 4;
-
- // Maps to HTTP DELETE. Used for deleting a resource.
- string delete = 5;
-
- // Maps to HTTP PATCH. Used for updating a resource.
- string patch = 6;
-
- // The custom pattern is used for specifying an HTTP method that is not
- // included in the `pattern` field, such as HEAD, or "*" to leave the
- // HTTP method unspecified for this rule. The wild-card rule is useful
- // for services that provide content to Web (HTML) clients.
- CustomHttpPattern custom = 8;
- }
-
- // The name of the request field whose value is mapped to the HTTP request
- // body, or `*` for mapping all request fields not captured by the path
- // pattern to the HTTP body, or omitted for not having any HTTP request body.
- //
- // NOTE: the referred field must be present at the top-level of the request
- // message type.
- string body = 7;
-
- // Optional. The name of the response field whose value is mapped to the HTTP
- // response body. When omitted, the entire response message will be used
- // as the HTTP response body.
- //
- // NOTE: The referred field must be present at the top-level of the response
- // message type.
- string response_body = 12;
-
- // Additional HTTP bindings for the selector. Nested bindings must
- // not contain an `additional_bindings` field themselves (that is,
- // the nesting may only be one level deep).
- repeated HttpRule additional_bindings = 11;
-}
-
-// A custom pattern is used for defining custom HTTP verb.
-message CustomHttpPattern {
- // The name of this custom HTTP verb.
- string kind = 1;
-
- // The path matched by this custom verb.
- string path = 2;
-}
diff --git a/examples/helloworld_transcoding/priv/protos/helloworld.proto b/examples/helloworld_transcoding/priv/protos/helloworld.proto
deleted file mode 100644
index 55c410057..000000000
--- a/examples/helloworld_transcoding/priv/protos/helloworld.proto
+++ /dev/null
@@ -1,47 +0,0 @@
-syntax = "proto3";
-
-option java_multiple_files = true;
-option java_package = "io.grpc.examples.helloworld";
-option java_outer_classname = "HelloWorldProto";
-option objc_class_prefix = "HLW";
-
-import "google/api/annotations.proto";
-import "google/protobuf/timestamp.proto";
-
-package helloworld;
-
-// The greeting service definition.
-service Greeter {
- // Sends a greeting
- rpc SayHello (HelloRequest) returns (HelloReply) {
- option (google.api.http) = {
- get: "/v1/greeter/{name}"
- };
- }
-
- rpc SayHelloFrom (HelloRequestFrom) returns (HelloReply) {
- option (google.api.http) = {
- post: "/v1/greeter"
- body: "*"
- };
- }
-}
-
-// The request message containing the user's name.
-message HelloRequest {
- string name = 1;
-}
-
-// HelloRequestFrom!
-message HelloRequestFrom {
- // Name!
- string name = 1;
- // From!
- string from = 2;
-}
-
-// The response message containing the greetings
-message HelloReply {
- string message = 1;
- google.protobuf.Timestamp today = 2;
-}
diff --git a/examples/helloworld_transcoding/test/hello_world_test.exs b/examples/helloworld_transcoding/test/hello_world_test.exs
deleted file mode 100644
index 962d07ac1..000000000
--- a/examples/helloworld_transcoding/test/hello_world_test.exs
+++ /dev/null
@@ -1,16 +0,0 @@
-defmodule HelloworldTest do
- @moduledoc false
-
- use ExUnit.Case
-
- setup_all do
- {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client])
- [channel: channel]
- end
-
- test "helloworld should be successful", %{channel: channel} do
- req = Helloworld.HelloRequest.new(name: "grpc-elixir")
- assert {:ok, %{message: msg, today: _}} = Helloworld.Greeter.Stub.say_hello(channel, req)
- assert msg == "Hello grpc-elixir"
- end
-end
diff --git a/examples/helloworld_transcoding/test/test_helper.exs b/examples/helloworld_transcoding/test/test_helper.exs
deleted file mode 100644
index 869559e70..000000000
--- a/examples/helloworld_transcoding/test/test_helper.exs
+++ /dev/null
@@ -1 +0,0 @@
-ExUnit.start()
diff --git a/examples/route_guide/.gitignore b/examples/route_guide/.gitignore
deleted file mode 100644
index 68f57605f..000000000
--- a/examples/route_guide/.gitignore
+++ /dev/null
@@ -1,19 +0,0 @@
-# The directory Mix will write compiled artifacts to.
-/_build
-
-# If you run "mix test --cover", coverage assets end up here.
-/cover
-
-# The directory Mix downloads your dependencies sources to.
-/deps
-
-# Where 3rd-party dependencies like ExDoc output generated docs.
-/doc
-
-# If the VM crashes, it generates a dump, let's ignore it too.
-erl_crash.dump
-
-# Also ignore archive artifacts (built via "mix archive.build").
-*.ez
-
-/log
diff --git a/examples/route_guide/README.md b/examples/route_guide/README.md
deleted file mode 100644
index 5e9fa0df4..000000000
--- a/examples/route_guide/README.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# RouteGuide in grpc-elixir
-
-## Usage
-
-1. Install deps and compile
-```
-$ mix do deps.get, compile
-```
-
-2. Run the server
-```
-$ mix run --no-halt
-```
-
-2. Run the client
-```
-$ mix run priv/client.exs
-```
-
-## Regenerate Elixir code from proto
-
-1. Modify the proto `priv/route_guide.proto`
-2. Install `protoc` [here](https://developers.google.com/protocol-buffers/docs/downloads)
-3. Install `protoc-gen-elixir`
-```
-mix escript.install hex protobuf
-```
-3. Generate the code:
-```shell
-$ protoc -I priv --elixir_out=plugins=grpc:./lib/ priv/route_guide.proto
-```
-
-Refer to [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) for more information.
-
-## Authentication
-
-```
-$ TLS=true mix run --no-halt
-$ TLS=true mix run priv/client.exs
-```
-
-## FAQ
-
-* How to change log level? Check out `config/config.exs`, default to warn
-* Use local grpc-elixir? Uncomment `{:grpc, path: "../../"}` in `mix.exs`
-* Why is output format of `Feature` & `Point` different from normal map? Check out `lib/inspect.ex`
diff --git a/examples/route_guide/lib/app.ex b/examples/route_guide/lib/app.ex
deleted file mode 100644
index 8cebbf319..000000000
--- a/examples/route_guide/lib/app.ex
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Routeguide.App do
- use Application
-
- @cert_path Path.expand("./tls/server1.pem", :code.priv_dir(:route_guide))
- @key_path Path.expand("./tls/server1.key", :code.priv_dir(:route_guide))
-
- def start(_type, _args) do
- children = [
- RouteGuide.Data,
- {GRPC.Server.Supervisor, start_args()}
- ]
-
- opts = [strategy: :one_for_one, name: Routeguide]
- Supervisor.start_link(children, opts)
- end
-
- defp start_args do
- opts = [endpoint: Routeguide.Endpoint, port: 10000, start_server: true]
-
- if System.get_env("TLS") do
- cred = GRPC.Credential.new(ssl: [certfile: @cert_path, keyfile: @key_path])
- Keyword.put(opts, :cred, cred)
- else
- opts
- end
- end
-end
diff --git a/examples/route_guide/lib/client.ex b/examples/route_guide/lib/client.ex
deleted file mode 100644
index 5d33f4ba4..000000000
--- a/examples/route_guide/lib/client.ex
+++ /dev/null
@@ -1,104 +0,0 @@
-defmodule RouteGuide.Client do
- def main(channel) do
- print_feature(channel, Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906))
- print_feature(channel, Routeguide.Point.new(latitude: 0, longitude: 0))
-
- # Looking for features between 40, -75 and 42, -73.
- print_features(
- channel,
- Routeguide.Rectangle.new(
- lo: Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000),
- hi: Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000)
- )
- )
-
- run_record_route(channel)
-
- run_route_chat(channel)
- end
-
- def print_feature(channel, point) do
- IO.puts("Getting feature for point (#{point.latitude}, #{point.longitude})")
- {:ok, reply} = channel |> Routeguide.RouteGuide.Stub.get_feature(point)
- IO.inspect(reply)
- end
-
- def print_features(channel, rect) do
- IO.puts("Looking for features within #{inspect(rect)}")
- {:ok, stream} = channel |> Routeguide.RouteGuide.Stub.list_features(rect)
-
- Enum.each(stream, fn {:ok, feature} ->
- IO.inspect(feature)
- end)
- end
-
- def run_record_route(channel) do
- ts = :os.timestamp()
- seed = :rand.seed(:exs64, ts)
- {count, seed} = :rand.uniform_s(seed)
- count = trunc(count * 100 + 2)
-
- {points, _seed} =
- Enum.reduce(1..count, {[], seed}, fn _, {acc, seed} ->
- {point, seed} = random_point(seed)
- {[point | acc], seed}
- end)
-
- IO.puts("Traversing #{length(points)} points.")
- stream = channel |> Routeguide.RouteGuide.Stub.record_route()
-
- Enum.reduce(points, points, fn _, [point | tail] ->
- opts = if length(tail) == 0, do: [end_stream: true], else: []
- GRPC.Stub.send_request(stream, point, opts)
- tail
- end)
-
- res = GRPC.Stub.recv(stream)
- IO.puts("Route summary: #{inspect(res)}")
- end
-
- def run_route_chat(channel) do
- data = [
- %{lat: 0, long: 1, msg: "First message"},
- %{lat: 0, long: 2, msg: "Second message"},
- %{lat: 0, long: 3, msg: "Third message"},
- %{lat: 0, long: 1, msg: "Fourth message"},
- %{lat: 0, long: 2, msg: "Fifth message"},
- %{lat: 0, long: 3, msg: "Sixth message"}
- ]
-
- stream = channel |> Routeguide.RouteGuide.Stub.route_chat()
-
- notes =
- Enum.map(data, fn %{lat: lat, long: long, msg: msg} ->
- point = Routeguide.Point.new(latitude: lat, longitude: long)
- Routeguide.RouteNote.new(location: point, message: msg)
- end)
-
- task =
- Task.async(fn ->
- Enum.reduce(notes, notes, fn _, [note | tail] ->
- opts = if length(tail) == 0, do: [end_stream: true], else: []
- GRPC.Stub.send_request(stream, note, opts)
- tail
- end)
- end)
-
- {:ok, result_enum} = GRPC.Stub.recv(stream)
- Task.await(task)
-
- Enum.each(result_enum, fn {:ok, note} ->
- IO.puts(
- "Got message #{note.message} at point(#{note.location.latitude}, #{note.location.longitude})"
- )
- end)
- end
-
- defp random_point(seed) do
- {lat, seed} = :rand.uniform_s(seed)
- {long, seed} = :rand.uniform_s(seed)
- lat = trunc((trunc(lat * 180) - 90) * 1.0e7)
- long = trunc((trunc(long * 360) - 180) * 1.0e7)
- {Routeguide.Point.new(latitude: lat, longitude: long), seed}
- end
-end
diff --git a/examples/route_guide/lib/data.ex b/examples/route_guide/lib/data.ex
deleted file mode 100644
index 7d1fe5284..000000000
--- a/examples/route_guide/lib/data.ex
+++ /dev/null
@@ -1,34 +0,0 @@
-defmodule RouteGuide.Data do
- use Agent
-
- @json_path Path.expand("../priv/route_guide_db.json", __DIR__)
-
- def start_link(_) do
- features = load_features()
- Agent.start_link(fn -> %{features: features, notes: %{}} end, name: __MODULE__)
- end
-
- def fetch_features do
- Agent.get(__MODULE__, &Map.get(&1, :features))
- end
-
- def fetch_notes do
- Agent.get(__MODULE__, &Map.get(&1, :notes))
- end
-
- def update_notes(notes) do
- Agent.update(__MODULE__, &Map.put(&1, :notes, notes))
- end
-
- defp load_features(path \\ @json_path) do
- data = File.read!(path)
- items = Jason.decode!(data)
-
- for %{"location" => location, "name" => name} <- items do
- point =
- Routeguide.Point.new(latitude: location["latitude"], longitude: location["longitude"])
-
- Routeguide.Feature.new(name: name, location: point)
- end
- end
-end
diff --git a/examples/route_guide/lib/endpoint.ex b/examples/route_guide/lib/endpoint.ex
deleted file mode 100644
index 5a25c582d..000000000
--- a/examples/route_guide/lib/endpoint.ex
+++ /dev/null
@@ -1,6 +0,0 @@
-defmodule Routeguide.Endpoint do
- use GRPC.Endpoint
-
- intercept GRPC.Server.Interceptors.Logger
- run Routeguide.RouteGuide.Server
-end
diff --git a/examples/route_guide/lib/inspect.ex b/examples/route_guide/lib/inspect.ex
deleted file mode 100644
index fb5cabac7..000000000
--- a/examples/route_guide/lib/inspect.ex
+++ /dev/null
@@ -1,36 +0,0 @@
-defimpl Inspect, for: Routeguide.Point do
- def inspect(%{__struct__: struct} = point, opts) do
- lat_str = Inspect.Integer.inspect(point.latitude || 0, opts)
- lng_str = Inspect.Integer.inspect(point.longitude || 0, opts)
- middle = "latitude: " <> lat_str <> ", longitude: " <> lng_str
-
- if Map.get(opts, :compact, true) do
- "<" <> middle <> ">"
- else
- name = Inspect.Atom.inspect(struct, opts)
- "%#{name}{" <> middle <> "}"
- end
- end
-end
-
-defimpl Inspect, for: Routeguide.Feature do
- def inspect(%{__struct__: struct} = feature, opts) do
- name = Inspect.Atom.inspect(struct, opts)
-
- name_str =
- if feature.name do
- Inspect.BitString.inspect(feature.name, opts)
- else
- Inspect.Atom.inspect(nil, opts)
- end
-
- loc_str = Inspect.Routeguide.Point.inspect(feature.location, opts)
- middle = "name: " <> name_str <> ", location: " <> loc_str
-
- if Map.get(opts, :compact, true) do
- "<" <> middle <> ">"
- else
- "%#{name}{" <> middle <> "}"
- end
- end
-end
diff --git a/examples/route_guide/lib/route_guide.pb.ex b/examples/route_guide/lib/route_guide.pb.ex
deleted file mode 100644
index 17cdb490f..000000000
--- a/examples/route_guide/lib/route_guide.pb.ex
+++ /dev/null
@@ -1,66 +0,0 @@
-defmodule Routeguide.Point do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.0", syntax: :proto3
-
- field :latitude, 1, type: :int32
- field :longitude, 2, type: :int32
-end
-
-defmodule Routeguide.Rectangle do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.0", syntax: :proto3
-
- field :lo, 1, type: Routeguide.Point
- field :hi, 2, type: Routeguide.Point
-end
-
-defmodule Routeguide.Feature do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.0", syntax: :proto3
-
- field :name, 1, type: :string
- field :location, 2, type: Routeguide.Point
-end
-
-defmodule Routeguide.RouteNote do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.0", syntax: :proto3
-
- field :location, 1, type: Routeguide.Point
- field :message, 2, type: :string
-end
-
-defmodule Routeguide.RouteSummary do
- @moduledoc false
-
- use Protobuf, protoc_gen_elixir_version: "0.14.0", syntax: :proto3
-
- field :point_count, 1, type: :int32, json_name: "pointCount"
- field :feature_count, 2, type: :int32, json_name: "featureCount"
- field :distance, 3, type: :int32
- field :elapsed_time, 4, type: :int32, json_name: "elapsedTime"
-end
-
-defmodule Routeguide.RouteGuide.Service do
- @moduledoc false
-
- use GRPC.Service, name: "routeguide.RouteGuide", protoc_gen_elixir_version: "0.14.0"
-
- rpc :GetFeature, Routeguide.Point, Routeguide.Feature
-
- rpc :ListFeatures, Routeguide.Rectangle, stream(Routeguide.Feature)
-
- rpc :RecordRoute, stream(Routeguide.Point), Routeguide.RouteSummary
-
- rpc :RouteChat, stream(Routeguide.RouteNote), stream(Routeguide.RouteNote)
-end
-
-defmodule Routeguide.RouteGuide.Stub do
- @moduledoc false
-
- use GRPC.Stub, service: Routeguide.RouteGuide.Service
-end
diff --git a/examples/route_guide/lib/server.ex b/examples/route_guide/lib/server.ex
deleted file mode 100644
index 3e90b8bc8..000000000
--- a/examples/route_guide/lib/server.ex
+++ /dev/null
@@ -1,112 +0,0 @@
-defmodule Routeguide.RouteGuide.Server do
- use GRPC.Server, service: Routeguide.RouteGuide.Service
- alias GRPC.Server
- alias RouteGuide.Data
-
- @spec get_feature(Routeguide.Point, GRPC.Server.Stream.t()) :: Routeguide.Feature.t()
- def get_feature(point, _stream) do
- features = Data.fetch_features()
- default_feature = Routeguide.Feature.new(location: point)
-
- Enum.find(features, default_feature, fn feature ->
- feature.location == point
- end)
- end
-
- @spec list_features(Routeguide.Rectangle.t(), GRPC.Server.Stream.t()) :: any()
- def list_features(rect, stream) do
- features = Data.fetch_features()
-
- features
- |> Enum.filter(fn %{location: loc} -> in_range?(loc, rect) end)
- |> Enum.each(fn feature -> Server.send_reply(stream, feature) end)
- end
-
- @spec record_route(Enumerable.t(), GRPC.Server.Stream.t()) :: Routeguide.RouteSummary.t()
- def record_route(req_enum, _stream) do
- features = Data.fetch_features()
- start_time = now_ts()
-
- {_, distance, point_count, feature_count} =
- Enum.reduce(req_enum, {nil, 0, 0, 0}, fn point,
- {last, distance, point_count, feature_count} ->
- point_count = point_count + 1
- found_feature = Enum.find(features, fn f -> f.location == point end)
- feature_count = if found_feature, do: feature_count + 1, else: feature_count
- distance = if last, do: distance + calc_distance(last, point), else: distance
- {point, distance, point_count, feature_count}
- end)
-
- Routeguide.RouteSummary.new(
- point_count: point_count,
- feature_count: feature_count,
- distance: distance,
- elapsed_time: now_ts() - start_time
- )
- end
-
- @spec record_route(Enumerable.t(), GRPC.Server.Stream.t()) :: any()
- def route_chat(req_enum, stream) do
- notes =
- Enum.reduce(req_enum, Data.fetch_notes(), fn note, notes ->
- key = serialize_location(note.location)
- new_notes = Map.update(notes, key, [note], &(&1 ++ [note]))
-
- Enum.each(new_notes[key], fn note ->
- IO.inspect(note)
- Server.send_reply(stream, note)
- end)
-
- new_notes
- end)
-
- Data.update_notes(notes)
- end
-
- defp in_range?(%{longitude: long, latitude: lat}, %{lo: low, hi: high}) do
- left = min(low.longitude, high.longitude)
- right = max(low.longitude, high.longitude)
- bottom = min(low.latitude, high.latitude)
- top = max(low.latitude, high.latitude)
-
- long >= left && long <= right && lat >= bottom && lat <= top
- end
-
- defp now_ts do
- DateTime.utc_now() |> DateTime.to_unix()
- end
-
- # calcDistance calculates the distance between two points using the "haversine" formula.
- # This code was taken from http://www.movable-type.co.uk/scripts/latlong.html.
- defp calc_distance(p1, p2) do
- cord_factor = 1.0e7
- r = 6_371_000.0
- lat1 = (p1.latitude || 0) / cord_factor
- lat2 = (p2.latitude || 0) / cord_factor
- lng1 = (p1.longitude || 0) / cord_factor
- lng2 = (p2.longitude || 0) / cord_factor
- phi1 = to_radians(lat1)
- phi2 = to_radians(lat2)
- delta_phi = to_radians(lat2 - lat1)
- delta_lambda = to_radians(lng2 - lng1)
-
- a =
- sqr(:math.sin(delta_phi / 2)) +
- :math.cos(phi1) * :math.cos(phi2) * sqr(:math.sin(delta_lambda / 2))
-
- c = 2 * :math.atan2(:math.sqrt(a), :math.sqrt(1 - a))
- round(r * c)
- end
-
- defp to_radians(num) do
- num * :math.pi() / 180
- end
-
- defp sqr(num) do
- num * num
- end
-
- def serialize_location(p) do
- "#{p.latitude} #{p.longitude}"
- end
-end
diff --git a/examples/route_guide/mix.exs b/examples/route_guide/mix.exs
deleted file mode 100644
index 504e82676..000000000
--- a/examples/route_guide/mix.exs
+++ /dev/null
@@ -1,38 +0,0 @@
-defmodule RouteGuide.Mixfile do
- use Mix.Project
-
- def project do
- [
- app: :route_guide,
- version: "0.1.0",
- elixir: "~> 1.11",
- build_embedded: Mix.env() == :prod,
- start_permanent: Mix.env() == :prod,
- deps: deps()
- ]
- end
-
- # Configuration for the OTP application
- #
- # Type "mix help compile.app" for more information
- def application do
- [mod: {Routeguide.App, []}, applications: [:logger, :grpc, :protobuf, :jason]]
- end
-
- # Dependencies can be Hex packages:
- #
- # {:mydep, "~> 0.3.0"}
- #
- # Or git/path repositories:
- #
- # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
- #
- # Type "mix help deps" for more examples and options
- defp deps do
- [
- {:grpc, path: "../../"},
- {:protobuf, "~> 0.14"},
- {:jason, "~> 1.2"}
- ]
- end
-end
diff --git a/examples/route_guide/mix.lock b/examples/route_guide/mix.lock
deleted file mode 100644
index f251ba2f8..000000000
--- a/examples/route_guide/mix.lock
+++ /dev/null
@@ -1,13 +0,0 @@
-%{
- "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
- "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
- "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"},
- "gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"},
- "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"},
- "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
- "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
- "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
- "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"},
- "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
- "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
-}
diff --git a/examples/route_guide/priv/client.exs b/examples/route_guide/priv/client.exs
deleted file mode 100644
index 5545ce9f7..000000000
--- a/examples/route_guide/priv/client.exs
+++ /dev/null
@@ -1,13 +0,0 @@
-opts = [interceptors: [GRPC.Client.Interceptors.Logger]]
-
-opts =
- if System.get_env("TLS") do
- ca_path = Path.expand("./tls/ca.pem", :code.priv_dir(:route_guide))
- cred = GRPC.Credential.new(ssl: [cacertfile: ca_path])
- [{:cred, cred} | opts]
- else
- opts
- end
-
-{:ok, channel} = GRPC.Stub.connect("localhost:10000", opts)
-RouteGuide.Client.main(channel)
diff --git a/examples/route_guide/priv/protos/route_guide.proto b/examples/route_guide/priv/protos/route_guide.proto
deleted file mode 100644
index 3cfe16040..000000000
--- a/examples/route_guide/priv/protos/route_guide.proto
+++ /dev/null
@@ -1,41 +0,0 @@
-syntax = "proto3";
-
-option java_multiple_files = true;
-option java_package = "io.grpc.examples.routeguide";
-option java_outer_classname = "RouteGuideProto";
-
-package routeguide;
-
-service RouteGuide {
- rpc GetFeature(Point) returns (Feature) {}
- rpc ListFeatures(Rectangle) returns (stream Feature) {}
- rpc RecordRoute(stream Point) returns (RouteSummary) {}
- rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
-}
-
-message Point {
- int32 latitude = 1;
- int32 longitude = 2;
-}
-
-message Rectangle {
- Point lo = 1;
- Point hi = 2;
-}
-
-message Feature {
- string name = 1;
- Point location = 2;
-}
-
-message RouteNote {
- Point location = 1;
- string message = 2;
-}
-
-message RouteSummary {
- int32 point_count = 1;
- int32 feature_count = 2;
- int32 distance = 3;
- int32 elapsed_time = 4;
-}
diff --git a/examples/route_guide/priv/route_guide_db.json b/examples/route_guide/priv/route_guide_db.json
deleted file mode 100644
index 9d6a980ab..000000000
--- a/examples/route_guide/priv/route_guide_db.json
+++ /dev/null
@@ -1,601 +0,0 @@
-[{
- "location": {
- "latitude": 407838351,
- "longitude": -746143763
- },
- "name": "Patriots Path, Mendham, NJ 07945, USA"
-}, {
- "location": {
- "latitude": 408122808,
- "longitude": -743999179
- },
- "name": "101 New Jersey 10, Whippany, NJ 07981, USA"
-}, {
- "location": {
- "latitude": 413628156,
- "longitude": -749015468
- },
- "name": "U.S. 6, Shohola, PA 18458, USA"
-}, {
- "location": {
- "latitude": 419999544,
- "longitude": -740371136
- },
- "name": "5 Conners Road, Kingston, NY 12401, USA"
-}, {
- "location": {
- "latitude": 414008389,
- "longitude": -743951297
- },
- "name": "Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA"
-}, {
- "location": {
- "latitude": 419611318,
- "longitude": -746524769
- },
- "name": "287 Flugertown Road, Livingston Manor, NY 12758, USA"
-}, {
- "location": {
- "latitude": 406109563,
- "longitude": -742186778
- },
- "name": "4001 Tremley Point Road, Linden, NJ 07036, USA"
-}, {
- "location": {
- "latitude": 416802456,
- "longitude": -742370183
- },
- "name": "352 South Mountain Road, Wallkill, NY 12589, USA"
-}, {
- "location": {
- "latitude": 412950425,
- "longitude": -741077389
- },
- "name": "Bailey Turn Road, Harriman, NY 10926, USA"
-}, {
- "location": {
- "latitude": 412144655,
- "longitude": -743949739
- },
- "name": "193-199 Wawayanda Road, Hewitt, NJ 07421, USA"
-}, {
- "location": {
- "latitude": 415736605,
- "longitude": -742847522
- },
- "name": "406-496 Ward Avenue, Pine Bush, NY 12566, USA"
-}, {
- "location": {
- "latitude": 413843930,
- "longitude": -740501726
- },
- "name": "162 Merrill Road, Highland Mills, NY 10930, USA"
-}, {
- "location": {
- "latitude": 410873075,
- "longitude": -744459023
- },
- "name": "Clinton Road, West Milford, NJ 07480, USA"
-}, {
- "location": {
- "latitude": 412346009,
- "longitude": -744026814
- },
- "name": "16 Old Brook Lane, Warwick, NY 10990, USA"
-}, {
- "location": {
- "latitude": 402948455,
- "longitude": -747903913
- },
- "name": "3 Drake Lane, Pennington, NJ 08534, USA"
-}, {
- "location": {
- "latitude": 406337092,
- "longitude": -740122226
- },
- "name": "6324 8th Avenue, Brooklyn, NY 11220, USA"
-}, {
- "location": {
- "latitude": 406421967,
- "longitude": -747727624
- },
- "name": "1 Merck Access Road, Whitehouse Station, NJ 08889, USA"
-}, {
- "location": {
- "latitude": 416318082,
- "longitude": -749677716
- },
- "name": "78-98 Schalck Road, Narrowsburg, NY 12764, USA"
-}, {
- "location": {
- "latitude": 415301720,
- "longitude": -748416257
- },
- "name": "282 Lakeview Drive Road, Highland Lake, NY 12743, USA"
-}, {
- "location": {
- "latitude": 402647019,
- "longitude": -747071791
- },
- "name": "330 Evelyn Avenue, Hamilton Township, NJ 08619, USA"
-}, {
- "location": {
- "latitude": 412567807,
- "longitude": -741058078
- },
- "name": "New York State Reference Route 987E, Southfields, NY 10975, USA"
-}, {
- "location": {
- "latitude": 416855156,
- "longitude": -744420597
- },
- "name": "103-271 Tempaloni Road, Ellenville, NY 12428, USA"
-}, {
- "location": {
- "latitude": 404663628,
- "longitude": -744820157
- },
- "name": "1300 Airport Road, North Brunswick Township, NJ 08902, USA"
-}, {
- "location": {
- "latitude": 407113723,
- "longitude": -749746483
- },
- "name": ""
-}, {
- "location": {
- "latitude": 402133926,
- "longitude": -743613249
- },
- "name": ""
-}, {
- "location": {
- "latitude": 400273442,
- "longitude": -741220915
- },
- "name": ""
-}, {
- "location": {
- "latitude": 411236786,
- "longitude": -744070769
- },
- "name": ""
-}, {
- "location": {
- "latitude": 411633782,
- "longitude": -746784970
- },
- "name": "211-225 Plains Road, Augusta, NJ 07822, USA"
-}, {
- "location": {
- "latitude": 415830701,
- "longitude": -742952812
- },
- "name": ""
-}, {
- "location": {
- "latitude": 413447164,
- "longitude": -748712898
- },
- "name": "165 Pedersen Ridge Road, Milford, PA 18337, USA"
-}, {
- "location": {
- "latitude": 405047245,
- "longitude": -749800722
- },
- "name": "100-122 Locktown Road, Frenchtown, NJ 08825, USA"
-}, {
- "location": {
- "latitude": 418858923,
- "longitude": -746156790
- },
- "name": ""
-}, {
- "location": {
- "latitude": 417951888,
- "longitude": -748484944
- },
- "name": "650-652 Willi Hill Road, Swan Lake, NY 12783, USA"
-}, {
- "location": {
- "latitude": 407033786,
- "longitude": -743977337
- },
- "name": "26 East 3rd Street, New Providence, NJ 07974, USA"
-}, {
- "location": {
- "latitude": 417548014,
- "longitude": -740075041
- },
- "name": ""
-}, {
- "location": {
- "latitude": 410395868,
- "longitude": -744972325
- },
- "name": ""
-}, {
- "location": {
- "latitude": 404615353,
- "longitude": -745129803
- },
- "name": ""
-}, {
- "location": {
- "latitude": 406589790,
- "longitude": -743560121
- },
- "name": "611 Lawrence Avenue, Westfield, NJ 07090, USA"
-}, {
- "location": {
- "latitude": 414653148,
- "longitude": -740477477
- },
- "name": "18 Lannis Avenue, New Windsor, NY 12553, USA"
-}, {
- "location": {
- "latitude": 405957808,
- "longitude": -743255336
- },
- "name": "82-104 Amherst Avenue, Colonia, NJ 07067, USA"
-}, {
- "location": {
- "latitude": 411733589,
- "longitude": -741648093
- },
- "name": "170 Seven Lakes Drive, Sloatsburg, NY 10974, USA"
-}, {
- "location": {
- "latitude": 412676291,
- "longitude": -742606606
- },
- "name": "1270 Lakes Road, Monroe, NY 10950, USA"
-}, {
- "location": {
- "latitude": 409224445,
- "longitude": -748286738
- },
- "name": "509-535 Alphano Road, Great Meadows, NJ 07838, USA"
-}, {
- "location": {
- "latitude": 406523420,
- "longitude": -742135517
- },
- "name": "652 Garden Street, Elizabeth, NJ 07202, USA"
-}, {
- "location": {
- "latitude": 401827388,
- "longitude": -740294537
- },
- "name": "349 Sea Spray Court, Neptune City, NJ 07753, USA"
-}, {
- "location": {
- "latitude": 410564152,
- "longitude": -743685054
- },
- "name": "13-17 Stanley Street, West Milford, NJ 07480, USA"
-}, {
- "location": {
- "latitude": 408472324,
- "longitude": -740726046
- },
- "name": "47 Industrial Avenue, Teterboro, NJ 07608, USA"
-}, {
- "location": {
- "latitude": 412452168,
- "longitude": -740214052
- },
- "name": "5 White Oak Lane, Stony Point, NY 10980, USA"
-}, {
- "location": {
- "latitude": 409146138,
- "longitude": -746188906
- },
- "name": "Berkshire Valley Management Area Trail, Jefferson, NJ, USA"
-}, {
- "location": {
- "latitude": 404701380,
- "longitude": -744781745
- },
- "name": "1007 Jersey Avenue, New Brunswick, NJ 08901, USA"
-}, {
- "location": {
- "latitude": 409642566,
- "longitude": -746017679
- },
- "name": "6 East Emerald Isle Drive, Lake Hopatcong, NJ 07849, USA"
-}, {
- "location": {
- "latitude": 408031728,
- "longitude": -748645385
- },
- "name": "1358-1474 New Jersey 57, Port Murray, NJ 07865, USA"
-}, {
- "location": {
- "latitude": 413700272,
- "longitude": -742135189
- },
- "name": "367 Prospect Road, Chester, NY 10918, USA"
-}, {
- "location": {
- "latitude": 404310607,
- "longitude": -740282632
- },
- "name": "10 Simon Lake Drive, Atlantic Highlands, NJ 07716, USA"
-}, {
- "location": {
- "latitude": 409319800,
- "longitude": -746201391
- },
- "name": "11 Ward Street, Mount Arlington, NJ 07856, USA"
-}, {
- "location": {
- "latitude": 406685311,
- "longitude": -742108603
- },
- "name": "300-398 Jefferson Avenue, Elizabeth, NJ 07201, USA"
-}, {
- "location": {
- "latitude": 419018117,
- "longitude": -749142781
- },
- "name": "43 Dreher Road, Roscoe, NY 12776, USA"
-}, {
- "location": {
- "latitude": 412856162,
- "longitude": -745148837
- },
- "name": "Swan Street, Pine Island, NY 10969, USA"
-}, {
- "location": {
- "latitude": 416560744,
- "longitude": -746721964
- },
- "name": "66 Pleasantview Avenue, Monticello, NY 12701, USA"
-}, {
- "location": {
- "latitude": 405314270,
- "longitude": -749836354
- },
- "name": ""
-}, {
- "location": {
- "latitude": 414219548,
- "longitude": -743327440
- },
- "name": ""
-}, {
- "location": {
- "latitude": 415534177,
- "longitude": -742900616
- },
- "name": "565 Winding Hills Road, Montgomery, NY 12549, USA"
-}, {
- "location": {
- "latitude": 406898530,
- "longitude": -749127080
- },
- "name": "231 Rocky Run Road, Glen Gardner, NJ 08826, USA"
-}, {
- "location": {
- "latitude": 407586880,
- "longitude": -741670168
- },
- "name": "100 Mount Pleasant Avenue, Newark, NJ 07104, USA"
-}, {
- "location": {
- "latitude": 400106455,
- "longitude": -742870190
- },
- "name": "517-521 Huntington Drive, Manchester Township, NJ 08759, USA"
-}, {
- "location": {
- "latitude": 400066188,
- "longitude": -746793294
- },
- "name": ""
-}, {
- "location": {
- "latitude": 418803880,
- "longitude": -744102673
- },
- "name": "40 Mountain Road, Napanoch, NY 12458, USA"
-}, {
- "location": {
- "latitude": 414204288,
- "longitude": -747895140
- },
- "name": ""
-}, {
- "location": {
- "latitude": 414777405,
- "longitude": -740615601
- },
- "name": ""
-}, {
- "location": {
- "latitude": 415464475,
- "longitude": -747175374
- },
- "name": "48 North Road, Forestburgh, NY 12777, USA"
-}, {
- "location": {
- "latitude": 404062378,
- "longitude": -746376177
- },
- "name": ""
-}, {
- "location": {
- "latitude": 405688272,
- "longitude": -749285130
- },
- "name": ""
-}, {
- "location": {
- "latitude": 400342070,
- "longitude": -748788996
- },
- "name": ""
-}, {
- "location": {
- "latitude": 401809022,
- "longitude": -744157964
- },
- "name": ""
-}, {
- "location": {
- "latitude": 404226644,
- "longitude": -740517141
- },
- "name": "9 Thompson Avenue, Leonardo, NJ 07737, USA"
-}, {
- "location": {
- "latitude": 410322033,
- "longitude": -747871659
- },
- "name": ""
-}, {
- "location": {
- "latitude": 407100674,
- "longitude": -747742727
- },
- "name": ""
-}, {
- "location": {
- "latitude": 418811433,
- "longitude": -741718005
- },
- "name": "213 Bush Road, Stone Ridge, NY 12484, USA"
-}, {
- "location": {
- "latitude": 415034302,
- "longitude": -743850945
- },
- "name": ""
-}, {
- "location": {
- "latitude": 411349992,
- "longitude": -743694161
- },
- "name": ""
-}, {
- "location": {
- "latitude": 404839914,
- "longitude": -744759616
- },
- "name": "1-17 Bergen Court, New Brunswick, NJ 08901, USA"
-}, {
- "location": {
- "latitude": 414638017,
- "longitude": -745957854
- },
- "name": "35 Oakland Valley Road, Cuddebackville, NY 12729, USA"
-}, {
- "location": {
- "latitude": 412127800,
- "longitude": -740173578
- },
- "name": ""
-}, {
- "location": {
- "latitude": 401263460,
- "longitude": -747964303
- },
- "name": ""
-}, {
- "location": {
- "latitude": 412843391,
- "longitude": -749086026
- },
- "name": ""
-}, {
- "location": {
- "latitude": 418512773,
- "longitude": -743067823
- },
- "name": ""
-}, {
- "location": {
- "latitude": 404318328,
- "longitude": -740835638
- },
- "name": "42-102 Main Street, Belford, NJ 07718, USA"
-}, {
- "location": {
- "latitude": 419020746,
- "longitude": -741172328
- },
- "name": ""
-}, {
- "location": {
- "latitude": 404080723,
- "longitude": -746119569
- },
- "name": ""
-}, {
- "location": {
- "latitude": 401012643,
- "longitude": -744035134
- },
- "name": ""
-}, {
- "location": {
- "latitude": 404306372,
- "longitude": -741079661
- },
- "name": ""
-}, {
- "location": {
- "latitude": 403966326,
- "longitude": -748519297
- },
- "name": ""
-}, {
- "location": {
- "latitude": 405002031,
- "longitude": -748407866
- },
- "name": ""
-}, {
- "location": {
- "latitude": 409532885,
- "longitude": -742200683
- },
- "name": ""
-}, {
- "location": {
- "latitude": 416851321,
- "longitude": -742674555
- },
- "name": ""
-}, {
- "location": {
- "latitude": 406411633,
- "longitude": -741722051
- },
- "name": "3387 Richmond Terrace, Staten Island, NY 10303, USA"
-}, {
- "location": {
- "latitude": 413069058,
- "longitude": -744597778
- },
- "name": "261 Van Sickle Road, Goshen, NY 10924, USA"
-}, {
- "location": {
- "latitude": 418465462,
- "longitude": -746859398
- },
- "name": ""
-}, {
- "location": {
- "latitude": 411733222,
- "longitude": -744228360
- },
- "name": ""
-}, {
- "location": {
- "latitude": 410248224,
- "longitude": -747127767
- },
- "name": "3 Hasta Way, Newton, NJ 07860, USA"
-}]
diff --git a/examples/route_guide/priv/tls/ca.pem b/examples/route_guide/priv/tls/ca.pem
deleted file mode 100644
index 6c8511a73..000000000
--- a/examples/route_guide/priv/tls/ca.pem
+++ /dev/null
@@ -1,15 +0,0 @@
------BEGIN CERTIFICATE-----
-MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
-BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
-aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
-Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
-YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
-BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
-+L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
-g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
-Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
-HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
-sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
-oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
-Dfcog5wrJytaQ6UA0wE=
------END CERTIFICATE-----
diff --git a/examples/route_guide/priv/tls/server1.key b/examples/route_guide/priv/tls/server1.key
deleted file mode 100644
index 143a5b876..000000000
--- a/examples/route_guide/priv/tls/server1.key
+++ /dev/null
@@ -1,16 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD
-M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf
-3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY
-AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm
-V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY
-tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p
-dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q
-K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR
-81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff
-DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd
-aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2
-ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3
-XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe
-F98XJ7tIFfJq
------END PRIVATE KEY-----
diff --git a/examples/route_guide/priv/tls/server1.pem b/examples/route_guide/priv/tls/server1.pem
deleted file mode 100644
index f3d43fcc5..000000000
--- a/examples/route_guide/priv/tls/server1.pem
+++ /dev/null
@@ -1,16 +0,0 @@
------BEGIN CERTIFICATE-----
-MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET
-MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
-dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx
-MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV
-BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50
-ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco
-LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg
-zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd
-9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw
-CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy
-em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G
-CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6
-hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh
-y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8
------END CERTIFICATE-----
diff --git a/guides/advanced/cors.md b/guides/advanced/cors.md
new file mode 100644
index 000000000..62baaeacf
--- /dev/null
+++ b/guides/advanced/cors.md
@@ -0,0 +1,16 @@
+# CORS
+
+When accessing gRPC from a browser via HTTP transcoding or gRPC-Web, CORS headers may be required for the browser to allow access to the gRPC endpoint. Adding CORS headers can be done by using `GRPC.Server.Interceptors.CORS` as an interceptor in your `GRPC.Endpoint` module, configuring it as described in the module documentation:
+
+Example:
+
+```elixir
+# Define your endpoint
+defmodule Helloworld.Endpoint do
+ use GRPC.Endpoint
+
+ intercept GRPC.Server.Interceptors.Logger
+ intercept GRPC.Server.Interceptors.CORS, allow_origin: "mydomain.io"
+ run Helloworld.Greeter.Server
+end
+```
\ No newline at end of file
diff --git a/guides/advanced/load_balancing.md b/guides/advanced/load_balancing.md
new file mode 100644
index 000000000..272470f66
--- /dev/null
+++ b/guides/advanced/load_balancing.md
@@ -0,0 +1,44 @@
+# Load Balancing
+
+Load balancing is a core capability of modern distributed gRPC systems. Instead of connecting directly to a single static address, the Elixir gRPC client can dynamically resolve multiple backend endpoints using pluggable target schemes (DNS, Unix sockets, xDS, and more). This allows clients to automatically distribute traffic across services and benefit from infrastructure-level routing — whether running on Kubernetes, service meshes like Istio, or traditional on-prem deployments.
+
+The implementation in this library follows the official gRPC Client Load Balancing specification, ensuring compatibility with ecosystem tooling such as Envoy, xDS control planes (see note below), and DNS-based service discovery.
+
+This guide explains how to define target URIs and how the built-in resolver discovers and continuously refreshes backend servers. Once configured, your load-balancing strategy becomes part of the connection string, no additional code required.
+
+## Target Schemes and Resolvers
+
+The `connect/2` function supports URI-like targets that are resolved via the internal **gRPC** [Resolver](lib/grpc/client/resolver.ex).
+You can connect using `DNS`, `Unix Domain sockets`, and `IPv4/IPv6` for now.
+
+### Supported formats:
+
+| Scheme | Example | Description |
+|:----------|:----------------------------|:---------------------------------------------|
+| `dns://` | `"dns://example.com:50051"` | Resolves via DNS `A/AAAA` records |
+| `ipv4:` | `"ipv4:10.0.0.5:50051"` | Connects directly to an IPv4 address |
+| `unix:` | `"unix:/tmp/service.sock"` | Connects via a Unix domain socket |
+| none | `"127.0.0.1:50051"` | Implicit DNS (default port `50051`) |
+
+---
+
+## Examples:
+
+### DNS
+
+```elixir
+iex> {:ok, _pid} = GRPC.Client.Supervisor.start_link()
+iex> {:ok, channel} = GRPC.Stub.connect("dns://orders.prod.svc.cluster.local:50051")
+iex> request = Orders.GetOrderRequest.new(id: "123")
+iex> {:ok, reply} = channel |> Orders.OrderService.Stub.get_order(request)
+```
+
+### Unix Domain Sockets
+
+```elixir
+iex> {:ok, channel} = GRPC.Stub.connect("unix:/tmp/my.sock")
+```
+
+>__Note__: When using `DNS` target, the connection layer periodically refreshes endpoints.
+
+---
\ No newline at end of file
diff --git a/guides/pooling.md b/guides/advanced/pooling.md
similarity index 94%
rename from guides/pooling.md
rename to guides/advanced/pooling.md
index 33a6e031a..93f5c9c08 100644
--- a/guides/pooling.md
+++ b/guides/advanced/pooling.md
@@ -1,4 +1,4 @@
-# Managing HTTP/2 Connections Efficiently
+# Pooling
When managing large numbers of gRPC HTTP/2 connections, you may benefit from pooling of some sort.
diff --git a/livebooks/telemetry.livemd b/guides/advanced/telemetry.livemd
similarity index 98%
rename from livebooks/telemetry.livemd
rename to guides/advanced/telemetry.livemd
index deaf0c04a..3d63f632b 100644
--- a/livebooks/telemetry.livemd
+++ b/guides/advanced/telemetry.livemd
@@ -1,17 +1,17 @@
# Telemetry
```elixir
-my_app_root = Path.join(__DIR__, "..")
+app_root = Path.join(__DIR__, "..")
Mix.install(
[
- {:grpc, path: my_app_root, env: :dev},
+ {:grpc, path: app_root, env: :dev},
{:telemetry_metrics, "~> 0.7"},
{:telemetry_metrics_prometheus, "~> 1.1"},
{:req, "~> 0.3"}
],
- config_path: Path.join(my_app_root, "config/config.exs"),
- lockfile: Path.join(my_app_root, "mix.lock")
+ config_path: Path.join(app_root, "config/config.exs"),
+ lockfile: Path.join(app_root, "mix.lock")
)
```
diff --git a/guides/advanced/transcoding.livemd b/guides/advanced/transcoding.livemd
new file mode 100644
index 000000000..c28d810f1
--- /dev/null
+++ b/guides/advanced/transcoding.livemd
@@ -0,0 +1,214 @@
+# Transcoding
+
+The goal of transcoding is to allow HTTP/JSON calls to be automatically converted into gRPC protobuf calls
+without external gateways.
+---
+
+## Setup
+
+```elixir
+app_root = Path.join(__DIR__, "..")
+
+Mix.install(
+ [
+ {:grpc, path: app_root, env: :dev}
+ ],
+ config_path: Path.join(app_root, "config/config.exs"),
+ lockfile: Path.join(app_root, "mix.lock")
+)
+```
+
+## Protobuf Service and Messages
+
+```elixir
+defmodule Helloworld.HelloRequest do
+ @moduledoc false
+ use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
+
+ field :name, 1, type: :string
+end
+
+defmodule Helloworld.HelloRequestFrom do
+ @moduledoc false
+ use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
+
+ field :name, 1, type: :string
+ field :from, 2, type: :string
+end
+
+defmodule Helloworld.HelloReply do
+ @moduledoc false
+ use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3
+
+ field :message, 1, type: :string
+ field :today, 2, type: Google.Protobuf.Timestamp
+end
+
+defmodule Helloworld.Greeter.Service do
+ @moduledoc false
+ use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.14.1"
+
+ rpc(:SayHello, Helloworld.HelloRequest, Helloworld.HelloReply, %{
+ http: %{
+ type: Google.Api.PbExtension,
+ value: %Google.Api.HttpRule{
+ selector: "",
+ body: "",
+ additional_bindings: [],
+ response_body: "",
+ pattern: {:get, "/v1/greeter/{name}"},
+ __unknown_fields__: []
+ }
+ }
+ })
+
+ rpc(:SayHelloFrom, Helloworld.HelloRequestFrom, Helloworld.HelloReply, %{
+ http: %{
+ type: Google.Api.PbExtension,
+ value: %Google.Api.HttpRule{
+ selector: "",
+ body: "*",
+ additional_bindings: [],
+ response_body: "",
+ pattern: {:post, "/v1/greeter"},
+ __unknown_fields__: []
+ }
+ }
+ })
+end
+```
+
+In a real-world application, this would be generated from your project's .proto files. You would have to annotate your protobuf in the following way:
+
+```protobuf
+import "google/api/annotations.proto";
+import "google/protobuf/timestamp.proto";
+
+package helloworld;
+
+service Greeter {
+ rpc SayHello (HelloRequest) returns (HelloReply) {
+ option (google.api.http) = {
+ get: "/v1/greeter/{name}"
+ };
+ }
+
+ rpc SayHelloFrom (HelloRequestFrom) returns (HelloReply) {
+ option (google.api.http) = {
+ post: "/v1/greeter"
+ body: "*"
+ };
+ }
+}
+```
+
+The compilation itself would be something like:
+
+```sh
+mix protobuf.generate --include-path=priv/proto --include-path=deps/googleapis --generate-descriptors=true --output-path=./lib --plugins=ProtobufGenerate.Plugins.GRPCWithOptions google/api/annotations.proto google/api/http.proto helloworld.proto
+```
+
+---
+
+## Enable transcoding on the Elixir side.
+
+```elixir
+defmodule Helloworld.Greeter.Server do
+ use GRPC.Server,
+ service: Helloworld.Greeter.Service,
+ http_transcode: true
+
+ alias GRPC.Stream, as: GRPCStream
+ alias Helloworld.HelloRequest
+ alias Helloworld.HelloReply
+
+ def say_hello(request, stream) do
+ GRPCStream.unary(request, materializer: stream)
+ |> GRPCStream.map(fn
+ %HelloRequest{} = reply ->
+ %HelloReply{
+ message: "Hello #{request.name}",
+ today: today()
+ }
+
+ {:error, reason} ->
+ {:error, GRPC.RPCError.exception(message: "[Error] #{inspect(reason)}")}
+ end)
+ |> GRPCStream.run()
+ end
+
+ def say_hello_from(request, _stream) do
+ GRPCStream.unary(request, materializer: stream)
+ |> GRPCStream.map(fn
+ %HelloFromRequest{} = reply ->
+ %HelloReply{
+ message: "Hello #{request.name}. From #{request.from}",
+ today: today()
+ }
+
+ _ ->
+ GRPC.RPCError.exception(message: "[Error] something bad happened")
+ end)
+ |> GRPCStream.run()
+ end
+
+ defp today() do
+ nanos_epoch = System.system_time() |> System.convert_time_unit(:native, :nanosecond)
+ seconds = div(nanos_epoch, 1_000_000_000)
+ nanos = nanos_epoch - seconds * 1_000_000_000
+
+ %Google.Protobuf.Timestamp{seconds: seconds, nanos: nanos}
+ end
+end
+```
+
+---
+
+## Endpoint + Supervisor
+
+```elixir
+defmodule TranscodeEndpoint do
+ use GRPC.Endpoint
+ intercept(GRPC.Server.Interceptors.Logger)
+ run(Helloworld.Greeter.Server)
+end
+
+{:ok, _pid} =
+ GRPC.Server.Supervisor.start_link(
+ endpoint: TranscodeEndpoint,
+ port: 50054,
+ start_server: true
+ )
+
+IO.puts("Transcoded gRPC Server running on :50054")
+```
+
+This automatically activates HTTP endpoints based on the annotations.
+
+---
+
+## Testing with `curl`
+
+```shell
+# Say hello
+$ curl -H 'accept: application/json' http://localhost:50054/v1/greeter/test
+```
+
+```shell
+# Say hello from
+$ curl -XPOST -H 'Content-type: application/json' -d '{"name": "test", "from": "anon"}' http://localhost:50054/v1/greeter
+```
+
+---
+
+## Important notes
+
+| Feature | Status |
+|-----------------------|-------------------------------------------|
+| CORS | Not enabled by default. See CORS section. |
+| Server Streaming | Supported. |
+| Query params → fields | Supported. See note below. |
+
+>__Note__: gRPC Transcode HttpRule https://docs.cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#google.api.HttpRule
+
+---
\ No newline at end of file
diff --git a/guides/cheatsheets/streams.cheatmd b/guides/cheatsheets/streams.cheatmd
new file mode 100644
index 000000000..943c3f6fa
--- /dev/null
+++ b/guides/cheatsheets/streams.cheatmd
@@ -0,0 +1,125 @@
+# Stream Operators
+
+`GRPC.Stream` provides a functional and composable API for building
+fault-tolerant pipelines for unary and streaming gRPC calls in Elixir.
+
+This cheatsheet summarizes the main operators available today.
+
+## Creating Streams
+
+### `unary/1`
+Wraps a single request into a stream (unary RPC type).
+
+```elixir
+iex> GRPC.Stream.unary(request)
+```
+
+### `from/1`
+Creates a stream from an stream RPC.
+
+```elixir
+iex> GRPC.Stream.from(request)
+```
+
+## Transforming Streams
+
+### `map/2`
+Transforms each element in the stream.
+
+```elixir
+iex> stream |> GRPC.Stream.map(&process/1)
+```
+
+### `flat_map/2`
+Transforms each element in the stream.
+
+```elixir
+iex> GRPC.Stream.from([1, 2])
+iex> |> GRPC.Stream.flat_map(&[&1, &1])
+iex> |> GRPC.Stream.to_flow()
+iex> |> Enum.to_list()
+```
+
+### `ask/3`
+Performs an external call using a Materializer.
+
+```elixir
+iex> pid =
+iex> spawn(fn ->
+iex> receive do
+iex> {:request, :hello, test_pid} ->
+iex> send(test_pid, {:response, :world})
+iex> end
+iex> end)
+iex>
+iex> GRPC.Stream.from([:hello])
+iex> |> GRPC.Stream.ask(pid)
+iex> |> GRPC.Stream.to_flow()
+iex> |> Enum.to_list()
+```
+
+## Filtering, grouping, and reduce Streams
+
+### `filter/2`
+Filters the stream using the given predicate function.
+
+```elixir
+iex> GRPC.Stream.from([1, 2, 3, 4])
+iex> |> GRPC.Stream.filter(&(rem(&1, 2) == 0))
+iex> |> GRPC.Stream.to_flow()
+iex> |> Enum.to_list()
+```
+
+## Effects
+
+### `effect/2`
+Applies a side-effect function to each element of the stream without altering its values.
+
+```elixir
+iex> parent = self()
+iex> stream =
+...> GRPC.Stream.from(request)
+...> |> GRPC.Stream.effect(fn x -> send(parent, {:seen, x*2}) end)
+...> |> GRPC.Stream.to_flow()
+...> |> Enum.to_list()
+iex> flush()
+iex> stream
+[1, 2, 3]
+```
+
+## Error Handling
+
+### `map_error/2`
+Intercepts and transforms errors & exceptions.
+
+```elixir
+iex> GRPC.Stream.from([1, 2])
+iex> |> GRPC.Stream.map(fn
+iex> 2 -> raise "boom"
+iex> x -> x
+iex> end)
+iex> |> GRPC.Stream.map_error(fn
+iex> {:error, %RuntimeError{message: "boom"}} ->
+iex> GRPC.RPCError.exception(message: "Validation or runtime error")
+iex>
+iex> msg ->
+iex> msg
+iex> end)
+```
+
+## Running Streams
+
+### `run/1`
+Executes the pipeline for unary RPC.
+
+```elixir
+iex> stream |> GRPC.Stream.run()
+```
+
+### `run_with/2`
+Executes the pipeline for stream RPC's.
+
+```elixir
+iex> stream |> GRPC.Stream.run_with(materializer)
+```
+
diff --git a/guides/getting_started/client.md b/guides/getting_started/client.md
new file mode 100644
index 000000000..1d748640b
--- /dev/null
+++ b/guides/getting_started/client.md
@@ -0,0 +1,86 @@
+# Client
+
+This section demonstrates how to establish client connections and perform RPC calls using the Elixir gRPC client.
+
+---
+
+## Basic Connection and RPC
+
+Typically, you start this client supervisor as part of your application's supervision tree:
+
+```elixir
+children = [
+ GRPC.Client.Supervisor
+]
+
+opts = [strategy: :one_for_one, name: MyApp.Supervisor]
+Supervisor.start_link(children, opts)
+```
+
+You can also start it manually in scripts or test environments:
+```elixir
+{:ok, _pid} = GRPC.Client.Supervisor.start_link()
+```
+
+Then connect with gRPC server:
+
+```elixir
+iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051")
+iex> request = Helloworld.HelloRequest.new(name: "grpc-elixir")
+iex> {:ok, reply} = channel |> Helloworld.GreetingServer.Stub.say_unary_hello(request)
+```
+
+---
+
+## Using Interceptors
+
+Client interceptors allow you to add logic to the request/response lifecycle, such as logging, tracing, or authentication.
+
+```elixir
+iex> {:ok, channel} =
+...> GRPC.Stub.connect("localhost:50051",
+...> interceptors: [GRPC.Client.Interceptors.Logger]
+...> )
+iex> request = Helloworld.HelloRequest.new(name: "Alice")
+iex> {:ok, reply} = channel |> Helloworld.GreetingServer.Stub.say_unary_hello(request)
+```
+
+---
+
+## Compression and Metadata
+
+You can specify message compression and attach default headers to all requests.
+
+```elixir
+iex> {:ok, channel} =
+...> GRPC.Stub.connect("localhost:50051",
+...> compressor: GRPC.Compressor.Gzip,
+...> headers: [{"authorization", "Bearer my-token"}]
+...> )
+```
+
+---
+
+## Client Adapters
+
+By default, `GRPC.Stub.connect/2` uses the **Gun** adapter.
+You can switch to **Mint** (pure Elixir HTTP/2) or other adapters as needed.
+
+### Using Mint Adapter
+
+```elixir
+iex> GRPC.Stub.connect("localhost:50051",
+...> adapter: GRPC.Client.Adapters.Mint
+...> )
+```
+
+You can configure adapter options globally via your application’s config:
+
+```elixir
+# File: config/config.exs
+config :grpc, GRPC.Client.Adapters.Mint,
+ timeout: 10_000,
+ transport_opts: [cacertfile: "/etc/ssl/certs/ca-certificates.crt"]
+```
+
+The accepted options are the same as [`Mint.HTTP.connect/4`](https://hexdocs.pm/mint/Mint.HTTP.html#connect/4-options).
\ No newline at end of file
diff --git a/guides/getting_started/codegen.md b/guides/getting_started/codegen.md
new file mode 100644
index 000000000..72c1dd1d0
--- /dev/null
+++ b/guides/getting_started/codegen.md
@@ -0,0 +1,54 @@
+# Codegen
+
+Use `protoc` with [protobuf elixir plugin](https://github.com/elixir-protobuf/protobuf) or using [protobuf_generate](https://hexdocs.pm/protobuf_generate/readme.html) hex package to generate the necessary files.
+
+---
+
+## Write your protobuf file
+
+```protobuf
+syntax = "proto3";
+
+package helloworld;
+
+// The request message containing the user's name.
+message HelloRequest {
+ string name = 1;
+}
+
+// The response message containing the greeting
+message HelloReply {
+ string message = 1;
+}
+
+// The greeting service definition.
+service GreetingServer {
+ rpc SayUnaryHello (HelloRequest) returns (HelloReply) {}
+ rpc SayServerHello (HelloRequest) returns (stream HelloReply) {}
+ rpc SayBidStreamHello (stream HelloRequest) returns (stream HelloReply) {}
+}
+```
+
+## Compile protos (protoc + elixir plugin)
+
+The most basic way to compile protobuf files is by using the elixir plugin for the protoc compiler:
+
+```bash
+protoc --elixir_out=plugins=grpc:./lib -I./priv/protos helloworld.proto
+```
+
+See more detailed explanation [here](https://hexdocs.pm/protobuf/readme.html#generate-elixir-code).
+
+But you can also benefit from more advanced options if you use the protobuf_generator. This is especially useful for use with HTTP Transcoding:
+
+```shell
+mix protobuf.generate \
+ --include-path=priv/proto \
+ --include-path=deps/googleapis \
+ --generate-descriptors=true \
+ --output-path=./lib \
+ --plugins=ProtobufGenerate.Plugins.GRPCWithOptions \
+ google/api/annotations.proto google/api/http.proto helloworld.proto
+```
+
+See more detailed explanation [here](https://hexdocs.pm/protobuf_generate/readme.html).
\ No newline at end of file
diff --git a/guides/getting_started/error_handling.md b/guides/getting_started/error_handling.md
new file mode 100644
index 000000000..63d3e79b4
--- /dev/null
+++ b/guides/getting_started/error_handling.md
@@ -0,0 +1,71 @@
+# Error Handling
+
+Effective error management is essential for maintaining reliability in gRPC streaming pipelines. In Elixir gRPC, all stream operators participate in a unified error propagation model, ensuring that failures — whether returned as {:error, reason} tuples or raised unexpectedly — are captured and translated consistently throughout the dataflow.
+
+Developers can intercept, transform, and recover from errors using dedicated operators such as `map_error/2`, enabling graceful degradation, domain-specific responses, and seamless conversion into `GRPC.RPCError` formats that propagate correctly to clients.
+
+This document explains how streaming error handling works, how exceptions interact with the pipeline, and how to design resilient services that continue processing even when individual elements fail.
+
+---
+
+## Recovery from errors
+
+The `map_error/2` operator intercepts and transforms errors or exceptions emitted by previous stages in a stream pipeline.
+
+It provides a unified mechanism for handling:
+
+* Expected errors, such as validation or domain failures (`{:error, reason}`)
+* Unexpected runtime errors, including raised or thrown exceptions inside other operators.
+
+```elixir
+iex> GRPC.Stream.from([1, 2])
+...> |> GRPC.Stream.map(fn
+...> 2 -> raise "boom"
+...> x -> x
+...> end)
+...> |> GRPC.Stream.map_error(fn
+...> {:error, {:exception, _reason}} ->
+...> {:error, GRPC.RPCError.exception(message: "Booomm")}
+...> end)
+```
+
+In this example:
+
+* The function inside `map/2` raises an exception for the value `2`.
+* `map_error/2` captures and transforms that error into a structured `GRPC.RPCError` response.
+* The stream continues processing without being interrupted.
+
+This makes `map_error/2` suitable for input validation, runtime fault recovery, and user-facing error translation within gRPC pipelines.
+
+---
+
+## Unified Error Matching and Propagation
+
+All stream operators share a unified error propagation model that guarantees consistent handling of exceptions and failures across the pipeline.
+
+This ensures that user-defined functions within the stream — whether pure transformations, side effects, or external calls — always produce a predictable and recoverable result, maintaining the integrity of the dataflow even in the presence of unexpected errors.
+
+```elixir
+def say_unary_hello(request, _materializer) do
+ GRPCStream.unary(request)
+ |> GRPCStream.ask(Transformer)
+ |> GRPCStream.map(fn
+ %HelloReply{} = reply ->
+ %HelloReply{message: "[Reply] #{reply.message}"}
+
+ {:error, reason} ->
+ {:error, GRPC.RPCError.exception(message: "error calling external process: #{inspect(reason)}")}
+
+ error ->
+ Logger.error("Unknown error")
+ error
+ end)
+ |> GRPCStream.run()
+end
+```
+
+By normalizing all possible outcomes, `GRPC.Stream` ensures fault-tolerant, exception-safe pipelines where operators can freely raise, throw, or return tuples without breaking the flow execution.
+
+This unified model allows developers to build composable and reliable streaming pipelines that gracefully recover from both domain and runtime errors.
+
+>_Note_: In the example above, we could use `map_error/2` instead of `map/2` to handle error cases explicitly. However, since the function also performs a transformation on successful values, `map/2` remains appropriate and useful in this context.
\ No newline at end of file
diff --git a/guides/getting_started/quickstart.livemd b/guides/getting_started/quickstart.livemd
new file mode 100644
index 000000000..7e3a55d87
--- /dev/null
+++ b/guides/getting_started/quickstart.livemd
@@ -0,0 +1,140 @@
+# Quickstart
+
+`GRPC` is a fully featured Elixir implementation of the gRPC protocol (grpc.io),
+enabling efficient communication between services through a unified and
+stream-oriented API. It supports all RPC types, friendly error handling, TLS,
+interceptors, reflection, and optional HTTP transcoding.
+
+Suitable for both server and client development in pure Elixir, enabling
+scalable, efficient and type-safe distributed systems.
+
+## Main features:
+
+ * Unary, Server Streaming, Client Streaming, Bi-directional Streaming RPCs;
+ * Streaming-first API for every call;
+ * Interceptors (auth, logging, rate limiting, tracing);
+ * Error handling with predictable propagation;
+ * TLS authentication and message compression;
+ * Connection load balancing strategies (Round Robin, Pick First);
+ * gRPC Reflection;
+ * HTTP Transcoding for REST ↔ gRPC compatibility;
+
+---
+
+## Setup
+
+```elixir
+app_root = Path.join(__DIR__, "..")
+
+Mix.install(
+ [
+ {:grpc, path: app_root, env: :dev},
+ {:protobuf, "~> 0.14"}, # optional for importing well-known Google gRPC types
+ {:grpc_reflection, "~> 0.2"}, # optional for enabling gRPC reflection
+ {:protobuf_generate, "~> 0.1", only: :dev} # optional for Protobuf code generation with plugins
+ ],
+ config_path: Path.join(app_root, "config/config.exs"),
+ lockfile: Path.join(app_root, "mix.lock")
+)
+```
+
+---
+
+## Protobuf Service and Messages
+
+```elixir
+defmodule Helloworld.HelloRequest do
+ use Protobuf, syntax: :proto3
+ field :name, 1, type: :string
+end
+
+defmodule Helloworld.HelloReply do
+ use Protobuf, syntax: :proto3
+ field :message, 1, type: :string
+end
+
+defmodule Helloworld.Greeter.Service do
+ use GRPC.Service, name: "helloworld.Greeter"
+ rpc :SayHello, Helloworld.HelloRequest, Helloworld.HelloReply
+end
+
+defmodule Helloworld.Greeter.Stub do
+ use GRPC.Stub, service: Helloworld.Greeter.Service
+end
+```
+
+---
+
+## Logging Interceptor
+
+We create a basic interceptor to log incoming RPC calls.
+
+```elixir
+defmodule LoggingInterceptor do
+ @behaviour GRPC.Server.Interceptor
+ require Logger
+
+ def init(options), do: options
+
+ def call(%GRPC.Server.Stream{} = stream, req, next, _opts) do
+ Logger.info("RPC: #{stream.service_name}/#{stream.method_name} received request")
+ next.(stream, req)
+ end
+end
+```
+
+---
+
+## gRPC Server Implementation
+
+```elixir
+defmodule HelloServer do
+ use GRPC.Server, service: Helloworld.Greeter.Service
+
+ def say_hello(%{name: name}, _stream) do
+ Helloworld.HelloReply.new(message: "Hello, #{name}!")
+ end
+end
+```
+
+---
+
+## Endpoint with Interceptor
+
+```elixir
+defmodule HelloEndpoint do
+ use GRPC.Endpoint
+
+ intercept(LoggingInterceptor)
+ run(HelloServer)
+end
+```
+
+---
+
+## Starting the Server
+
+Here we start the GRPC server under supervision at port `50051`.
+
+```elixir
+{:ok, _pid} =
+ GRPC.Server.Supervisor.start_link(endpoint: HelloEndpoint, port: 50051)
+
+IO.puts("gRPC Server running on port 50051")
+```
+
+---
+
+## Create a Client and Test the RPC
+
+```elixir
+{:ok, _} = GRPC.Client.Supervisor.start_link()
+{:ok, channel} = GRPC.Stub.connect("localhost:50051")
+
+request = Helloworld.HelloRequest.new(name: "Hello gRPC Livebook")
+{:ok, reply} = Helloworld.Greeter.Stub.say_hello(channel, request)
+
+IO.inspect(reply, label: "Received reply")
+```
+
+---
diff --git a/guides/getting_started/stream.livemd b/guides/getting_started/stream.livemd
new file mode 100644
index 000000000..378808f5a
--- /dev/null
+++ b/guides/getting_started/stream.livemd
@@ -0,0 +1,231 @@
+# Streaming
+
+gRPC streaming in Elixir introduces a fully composable way to process data as it flows
+between client and server. Instead of treating each request as an isolated transaction,
+streaming allows messages to be transformed incrementally, combined with other streams
+and enriched through side-effects while still in transit.
+
+The objective here is to demonstrate how `GRPC.Stream` enables functional composition
+applied over live data, allowing pipelines to evolve, react and continue processing
+even under constant input.
+
+By exploring unary, server streaming and bidirectional streaming examples, this
+document highlights how state, concurrency and data transformation can interact
+seamlessly in an event-driven communication model.
+
+---
+
+## Setup
+
+```elixir
+app_root = Path.join(__DIR__, "..")
+
+Mix.install(
+ [
+ {:grpc, path: app_root, env: :dev}
+ ],
+ config_path: Path.join(app_root, "config/config.exs"),
+ lockfile: Path.join(app_root, "mix.lock")
+)
+```
+
+---
+
+## Proto Messages & Service
+
+For simplicity, all proto definitions are inline.
+
+```elixir
+defmodule Stream.HelloRequest do
+ use Protobuf, syntax: :proto3
+ field :name, 1, type: :string
+end
+
+defmodule Stream.HelloReply do
+ use Protobuf, syntax: :proto3
+ field :message, 1, type: :string
+end
+
+defmodule Stream.EchoServer.Service do
+ use GRPC.Service, name: "stream.EchoServer"
+
+ rpc :SayUnaryHello, Stream.HelloRequest, Stream.HelloReply
+ rpc :SayServerHello, Stream.HelloRequest, stream(Stream.HelloReply)
+ rpc :SayBidStreamHello, stream(Stream.HelloRequest), stream(Stream.HelloReply)
+end
+
+defmodule Stream.EchoServer.Stub do
+ use GRPC.Stub, service: Stream.EchoServer.Service
+end
+```
+
+---
+
+## Transformer (Helper Process)
+
+Used for unary example composition.
+
+```elixir
+defmodule Transformer do
+ use GenServer
+
+ alias Stream.HelloRequest
+ alias Stream.HelloReply
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, nil, name: __MODULE__)
+ end
+
+ def init(_), do: {:ok, %{}}
+
+ def handle_info({:request, %HelloRequest{} = value, from}, state) do
+ Process.send(from, {:response, %HelloReply{message: "Hello #{value.name}"}}, [])
+ {:noreply, state}
+ end
+end
+
+{:ok, _} = Transformer.start_link(nil)
+```
+
+---
+
+## Server Implementation
+
+```elixir
+defmodule EchoStreamServer do
+ use GRPC.Server, service: Stream.EchoServer.Service
+
+ alias GRPC.Stream, as: GRPCStream
+ alias Stream.HelloRequest
+ alias Stream.HelloReply
+
+ # Unary example
+ def say_unary_hello(%HelloRequest{name: name}, _stream) do
+ GRPCStream.unary(request)
+ |> GRPCStream.ask(Transformer)
+ |> GRPCStream.map(fn
+ %HelloReply{} = reply ->
+ %HelloReply{message: "[Reply] #{reply.message}"}
+
+ {:error, reason} ->
+ {:error, GRPC.RPCError.exception(message: "[Error] #{inspect(reason)}")}
+ end)
+ |> GRPCStream.run()
+ end
+
+ # Server‑Side streaming
+ def say_server_hello(%HelloRequest{name: name} = _req, stream) do
+ Stream.repeatedly(fn ->
+ %HelloReply{message: "Hello from server → #{name}"}
+ end)
+ |> Stream.take(5)
+ |> GRPCStream.from()
+ |> GRPCStream.run_with(stream)
+ end
+
+ # Bidirectional streaming
+ def say_bid_stream_hello(request_stream, stream) do
+ GRPCStream.from(request_stream, join_with: output_join_stream())
+ |> GRPCStream.map(fn
+ %HelloRequest{name: name} ->
+ %HelloReply{message: "Welcome #{name}!"}
+ msg ->
+ msg
+ end)
+ |> GRPCStream.run_with(stream)
+ end
+
+ defp output_join_stream() do
+ Stream.repeatedly(fn ->
+ %Stream.HelloReply{message: "↔ Server heartbeat"}
+ end)
+ end
+end
+```
+
+---
+
+## Endpoint + Supervisor
+
+```elixir
+defmodule StreamingEndpoint do
+ use GRPC.Endpoint
+ intercept(GRPC.Server.Interceptors.Logger)
+ run(EchoStreamServer)
+end
+
+{:ok, _pid} =
+ GRPC.Server.Supervisor.start_link(
+ endpoint: StreamingEndpoint,
+ port: 50054,
+ start_server: true
+ )
+
+IO.puts("Streaming gRPC Server running on :50054")
+```
+
+---
+
+## Client Tests
+
+### Unary
+
+```elixir
+{:ok, _} = GRPC.Client.Supervisor.start_link()
+{:ok, channel} = GRPC.Stub.connect("localhost:50054")
+
+{:ok, reply} =
+ Stream.EchoServer.Stub.say_unary_hello(
+ channel,
+ %Stream.HelloRequest{name: "Unary Test"}
+ )
+
+IO.inspect(reply, label: "Unary reply")
+```
+
+---
+
+### Server Streaming
+
+```elixir
+{:ok, stream} =
+ Stream.EchoServer.Stub.say_server_hello(
+ channel,
+ %Stream.HelloRequest{name: "Server Stream"}
+ )
+
+Enum.each(stream, fn msg ->
+ IO.inspect(msg, label: "◀ Server message")
+end)
+```
+
+---
+
+### Bidirectional Streaming
+
+```elixir
+{:ok, bidi_stream} =
+ Stream.EchoServer.Stub.say_bid_stream_hello(channel)
+
+# Send 3 input messages
+Enum.each(~w(Alice Bob Carol)a, fn name ->
+ GRPC.Stub.send_request(
+ bidi_stream,
+ %Stream.HelloRequest{name: name}
+ )
+ Process.sleep(150)
+end)
+
+# Close input stream
+GRPC.Stub.end_stream(bidi_stream)
+
+# Receive responses
+{:ok, ex_stream} = GRPC.Stub.recv(bidi_stream)
+Enum.each(ex_stream, fn msg ->
+ IO.inspect(msg, label: "↔ Stream reply")
+end)
+```
+
+The Stream API supports composable stream transformations via `ask`, `map`, `run` and others functions, enabling clean and declarative stream pipelines. For a complete list of available operators see [here](lib/grpc/stream.ex).
+
+---
\ No newline at end of file
diff --git a/interop/dump-50051 copy.pcap b/interop/dump-50051 copy.pcap
deleted file mode 100644
index 11aefbaed..000000000
Binary files a/interop/dump-50051 copy.pcap and /dev/null differ
diff --git a/interop/dump-50051.pcap b/interop/dump-50051.pcap
deleted file mode 100644
index 11aefbaed..000000000
Binary files a/interop/dump-50051.pcap and /dev/null differ
diff --git a/lib/grpc.ex b/lib/grpc.ex
index b33f6c145..956881f07 100644
--- a/lib/grpc.ex
+++ b/lib/grpc.ex
@@ -1,4 +1,115 @@
defmodule GRPC do
+ @moduledoc """
+ GRPC is a fully featured Elixir implementation of the gRPC protocol (grpc.io),
+ enabling efficient communication between services through a unified and
+ stream-oriented API. It supports all RPC types, friendly error handling, TLS,
+ interceptors, reflection, and optional HTTP transcoding.
+
+ Suitable for both server and client development in pure Elixir, enabling
+ scalable, efficient and type-safe distributed systems.
+
+ ## Main features:
+
+ * Unary, Server Streaming, Client Streaming, Bi-directional Streaming RPCs;
+ * Streaming-first API for every call;
+ * Interceptors;
+ * Error handling with predictable propagation;
+ * TLS authentication and message compression;
+ * Connection load balancing strategies (Round Robin, Pick First);
+ * gRPC Reflection;
+ * HTTP Transcoding for REST ↔ gRPC compatibility;
+
+ ## Installation:
+
+ def deps do
+ [
+ {:grpc, "~> 0.11"},
+ {:protobuf, "~> 0.14"},
+ {:grpc_reflection, "~> 0.2"}
+ ]
+ end
+
+ ## Protobuf code generation:
+
+ protoc --elixir_out=plugins=grpc:./lib -I./priv/protos helloworld.proto
+
+ ## Basic Server Example
+
+ defmodule MyApp.Greeter.Server do
+ use GRPC.Server, service: MyApp.Greeter.Service
+ alias MyApp.{HelloRequest, HelloReply}
+
+ def say_hello(request, stream) do
+ request
+ |> GRPC.Stream.unary(materializer: stream)
+ |> GRPC.Stream.map(fn %HelloRequest{name: name} ->
+ %HelloReply{message: "Hello"}
+ end)
+ |> GRPC.Stream.run()
+ end
+ end
+
+ defmodule MyApp.Endpoint do
+ use GRPC.Endpoint
+ run MyApp.Greeter.Server
+ end
+
+ children = [
+ {GRPC.Server.Supervisor, endpoint: MyApp.Endpoint, port: 50051}
+ ]
+
+ ## Server-side streaming:
+
+ def say_hi_stream(request, stream) do
+ Stream.repeatedly(fn ->
+ %HelloReply{message: "Hi!"}
+ end)
+ |> Stream.take(5)
+ |> GRPC.Stream.from()
+ |> GRPC.Stream.run_with(stream)
+ end
+
+ ## Bidirectional streaming:
+
+ def chat(request_enum, stream) do
+ GRPC.Stream.from(request_enum)
+ |> GRPC.Stream.map(fn req ->
+ %HelloReply{message: "I'm the Server ;)"}
+ end)
+ |> GRPC.Stream.run_with(stream)
+ end
+
+ See `GRPC.Stream` for more Server examples.
+
+ ## Basic Client Example
+
+ {:ok, _} = GRPC.Client.Supervisor.start_link()
+ {:ok, channel} = GRPC.Stub.connect("localhost:50051")
+
+ req = MyApp.HelloRequest.new(name: "Elixir")
+ {:ok, reply} = MyApp.Greeter.Stub.say_hello(channel, req)
+
+ See `GRPC.Stub` for more Client examples.
+
+ ## HTTP Transcoding (optional)
+
+ Enable REST-to-gRPC mapping:
+
+ use GRPC.Server,
+ service: MyApp.Greeter.Service,
+ http_transcode: true
+
+ Useful when interacting with gRPC from browsers or REST clients.
+
+ ## CORS Support (optional)
+
+ defmodule MyApp.Endpoint do
+ use GRPC.Endpoint
+ intercept GRPC.Server.Interceptors.CORS, allow_origin: "*"
+ run MyApp.Greeter.Server
+ end
+
+ """
@version GRPC.Mixfile.project()[:version]
@doc """
diff --git a/lib/grpc/client/connection.ex b/lib/grpc/client/connection.ex
index f464fdfbb..4fb498f55 100644
--- a/lib/grpc/client/connection.ex
+++ b/lib/grpc/client/connection.ex
@@ -1,12 +1,12 @@
defmodule GRPC.Client.Connection do
@moduledoc """
- Connection manager for gRPC client channels, with optional **load balancing**
- and **name resolution** support.
+ Connection manager for gRPC client channels, with optional load balancing
+ and name resolution support.
A `Conn` process manages one or more underlying gRPC connections
- (`GRPC.Channel` structs) and exposes a **virtual channel** to be used by
+ (`GRPC.Channel` structs) and exposes a virtual channel to be used by
client stubs. The orchestration process runs as a `GenServer` registered
- globally (via `:global`), so only one orchestrator exists **per connection**
+ globally (via `:global`), so only one orchestrator exists per connection
in a BEAM node.
## Overview
@@ -255,8 +255,12 @@ defmodule GRPC.Client.Connection do
resp = {:ok, %Channel{channel | adapter_payload: %{conn_pid: nil}}}
if Map.has_key?(state, :real_channels) do
- Enum.map(state.real_channels, fn {_key, {:ok, ch}} ->
- adapter.disconnect(ch)
+ Enum.map(state.real_channels, fn
+ {_key, {:ok, ch}} ->
+ do_disconnect(adapter, ch)
+
+ _ ->
+ :ok
end)
keys_to_delete = [:real_channels, :virtual_channel]
@@ -273,9 +277,8 @@ defmodule GRPC.Client.Connection do
:refresh,
%{lb_mod: lb_mod, lb_state: lb_state, real_channels: channels, virtual_channel: vc} =
state
- ) do
- Logger.debug("refreshing LB pick, caller=#{inspect(self())}")
-
+ )
+ when not is_nil(lb_mod) do
{:ok, {prefer_host, prefer_port}, new_lb_state} = lb_mod.pick(lb_state)
channel_key = "#{prefer_host}:#{prefer_port}"
@@ -296,6 +299,8 @@ defmodule GRPC.Client.Connection do
end
end
+ def handle_info(:refresh, state), do: {:noreply, state}
+
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
Logger.warning(
"#{inspect(__MODULE__)} received :DOWN from #{inspect(pid)} with reason: #{inspect(reason)}"
@@ -323,6 +328,16 @@ defmodule GRPC.Client.Connection do
{:global, {__MODULE__, ref}}
end
+ defp do_disconnect(adapter, channel) do
+ adapter.disconnect(channel)
+ rescue
+ _ ->
+ :ok
+ catch
+ _type, _value ->
+ :ok
+ end
+
defp build_initial_state(target, opts) do
opts =
Keyword.validate!(opts,
diff --git a/lib/grpc/client/resolver.ex b/lib/grpc/client/resolver.ex
index de605c468..ec93a2de7 100644
--- a/lib/grpc/client/resolver.ex
+++ b/lib/grpc/client/resolver.ex
@@ -2,8 +2,8 @@ defmodule GRPC.Client.Resolver do
@moduledoc """
Behaviour for gRPC client resolvers.
- A gRPC resolver is responsible for translating a **target string** into
- a list of connection endpoints (addresses) and an optional `ServiceConfig`.
+ A gRPC resolver is responsible for translating a target string into
+ a list of connection endpoints (addresses) and an optional `GRPC.Client.ServiceConfig`.
gRPC supports multiple naming schemes, allowing clients to connect
to servers via DNS, fixed IPs, Unix domain sockets, or through
diff --git a/lib/grpc/client/resolver/ipv6.ex b/lib/grpc/client/resolver/ipv6.ex
index 14a5d7713..17cba904b 100644
--- a/lib/grpc/client/resolver/ipv6.ex
+++ b/lib/grpc/client/resolver/ipv6.ex
@@ -9,7 +9,7 @@ defmodule GRPC.Client.Resolver.IPv6 do
ipv6:[addr][:port][,[addr][:port],...]
- - IPv6 addresses **must** be enclosed in square brackets (`[...]`).
+ - IPv6 addresses must be enclosed in square brackets (`[...]`).
- The port is optional; if not provided, the default port is `443`.
- Multiple addresses can be comma-separated.
- `service_config` is always `nil` as IPv6 literals do not support DNS TXT or xDS.
diff --git a/lib/grpc/client/resolver/unix.ex b/lib/grpc/client/resolver/unix.ex
index 358e67a97..718e6a846 100644
--- a/lib/grpc/client/resolver/unix.ex
+++ b/lib/grpc/client/resolver/unix.ex
@@ -10,7 +10,7 @@ defmodule GRPC.Client.Resolver.Unix do
unix:///absolute/path/to/socket
- - The scheme **must** be `unix`.
+ - The scheme must be `unix`.
- The path must be absolute (`/var/run/my.sock`).
- The port is not used in Unix sockets; `:port` will be `nil`.
- The socket type is indicated via `:socket => :unix`.
diff --git a/lib/grpc/client/resolver/xds.ex b/lib/grpc/client/resolver/xds.ex
index b881b91ef..8b3345867 100644
--- a/lib/grpc/client/resolver/xds.ex
+++ b/lib/grpc/client/resolver/xds.ex
@@ -1,4 +1,5 @@
defmodule GRPC.Client.Resolver.XDS do
+ @moduledoc false
@behaviour GRPC.Client.Resolver
@impl GRPC.Client.Resolver
diff --git a/lib/grpc/client/service_config.ex b/lib/grpc/client/service_config.ex
index d81301af8..987142ebc 100644
--- a/lib/grpc/client/service_config.ex
+++ b/lib/grpc/client/service_config.ex
@@ -9,12 +9,12 @@ defmodule GRPC.Client.ServiceConfig do
According to the gRPC specification ([service_config.md](https://github.com/grpc/grpc/blob/master/doc/service_config.md)):
- - **loadBalancingConfig**: a list of load balancing policies.
+ - loadBalancingConfig: a list of load balancing policies.
The client should pick the first policy it supports. Common values are:
- `"pick_first"`: always pick the first server.
- `"round_robin"`: distribute calls across servers in round-robin.
- - **methodConfig**: a list of configurations applied to specific methods or services.
+ - methodConfig: a list of configurations applied to specific methods or services.
Each entry can include:
- `"name"`: a list of `{ "service": "", "method": "" }`
or `{ "service": "" }` to match all methods in the service.
diff --git a/lib/grpc/client/supervisor.ex b/lib/grpc/client/supervisor.ex
index 07832e56d..099cba730 100644
--- a/lib/grpc/client/supervisor.ex
+++ b/lib/grpc/client/supervisor.ex
@@ -44,7 +44,16 @@ defmodule GRPC.Client.Supervisor do
use DynamicSupervisor
def start_link(opts) do
- DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__)
+ case DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__) do
+ {:ok, _pid} = started ->
+ started
+
+ {:error, {:already_started, pid}} ->
+ {:ok, pid}
+
+ other ->
+ other
+ end
end
@impl true
diff --git a/lib/grpc/protoc/cli.ex b/lib/grpc/protoc/cli.ex
index c60afae55..0a468ac41 100644
--- a/lib/grpc/protoc/cli.ex
+++ b/lib/grpc/protoc/cli.ex
@@ -2,7 +2,7 @@ defmodule GRPC.Protoc.CLI do
@moduledoc """
`protoc` plugin for generating Elixir code.
- `protoc-gen-elixir` (this name is important) **must** be in `$PATH`. You are not supposed
+ `protoc-gen-elixir` (this name is important) must be in `$PATH`. You are not supposed
to call it directly, but only through `protoc`.
## Examples
diff --git a/lib/grpc/server/adapters/cowboy/handler.ex b/lib/grpc/server/adapters/cowboy/handler.ex
index d27fdedee..eb7056ddd 100644
--- a/lib/grpc/server/adapters/cowboy/handler.ex
+++ b/lib/grpc/server/adapters/cowboy/handler.ex
@@ -29,7 +29,8 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
pid: server_rpc_pid :: pid,
handling_timer: timeout_timer_ref :: reference,
pending_reader: nil | pending_reader,
- access_mode: GRPC.Server.Stream.access_mode()
+ access_mode: GRPC.Server.Stream.access_mode(),
+ exception_log_filter: exception_log_filter()
}
@type init_result ::
{:cowboy_loop, :cowboy_req.req(), stream_state} | {:ok, :cowboy_req.req(), init_state}
@@ -40,6 +41,8 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
@type headers :: %{binary() => binary()}
+ @type exception_log_filter :: {module(), atom()} | nil
+
@doc """
This function is meant to be called whenever a new request arrives to an existing connection.
This handler works mainly with two linked processes.
@@ -52,6 +55,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
@spec init(:cowboy_req.req(), state :: init_state) :: init_result
def init(req, {endpoint, {_name, server}, route, opts} = state) do
http_method = extract_http_method(req) |> String.to_existing_atom()
+ exception_log_filter = extract_exception_log_filter_opt(opts)
with {:ok, access_mode, sub_type, content_type} <- find_content_type_subtype(req),
{:ok, codec} <- find_codec(sub_type, content_type, server),
@@ -98,7 +102,8 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
pid: server_rpc_pid,
handling_timer: timer_ref,
pending_reader: nil,
- access_mode: access_mode
+ access_mode: access_mode,
+ exception_log_filter: exception_log_filter
}
}
else
@@ -110,6 +115,19 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
end
end
+ defp extract_exception_log_filter_opt(opts) do
+ case opts[:exception_log_filter] do
+ {module, func_name} when is_atom(module) and is_atom(func_name) ->
+ {module, func_name}
+
+ nil ->
+ nil
+
+ invalid ->
+ raise ArgumentError, "invalid exception log filter: #{inspect(invalid)}"
+ end
+ end
+
defp find_codec(subtype, content_type, server) do
if codec = Enum.find(server.__meta__(:codecs), nil, fn c -> c.name() == subtype end) do
{:ok, codec}
@@ -466,7 +484,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
[req: req]
|> ReportException.new(error)
- |> log_error()
+ |> maybe_log_error(state.exception_log_filter)
{:stop, req, state}
end
@@ -488,12 +506,12 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
end
# expected error raised from user to return error immediately
- def info({:EXIT, pid, {%RPCError{} = error, stacktrace}}, req, state = %{pid: pid}) do
+ def info({:EXIT, pid, {:shutdown, {%RPCError{} = error, stacktrace}}}, req, state = %{pid: pid}) do
req = send_error(req, error, state, :rpc_error)
[req: req]
|> ReportException.new(error, stacktrace)
- |> log_error(stacktrace)
+ |> maybe_log_error(state.exception_log_filter, stacktrace)
{:stop, req, state}
end
@@ -506,7 +524,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
[req: req]
|> ReportException.new(reason, stack, kind)
- |> log_error(stack)
+ |> maybe_log_error(state.exception_log_filter, stack)
{:stop, req, state}
end
@@ -517,7 +535,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
[req: req]
|> ReportException.new(reason, stacktrace)
- |> log_error(stacktrace)
+ |> maybe_log_error(state.exception_log_filter, stacktrace)
{:stop, req, state}
end
@@ -550,7 +568,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
case result do
{:error, %GRPC.RPCError{} = e} ->
- exit({e, _stacktrace = []})
+ exit({:shutdown, {e, _stacktrace = []}})
{:error, %{kind: _kind, reason: _reason, stack: _stack} = e} ->
exit({:handle_error, e})
@@ -561,9 +579,11 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
end
defp do_call_rpc(server, path, %{http_method: http_method} = stream) do
- result = server.__call_rpc__(path, http_method, stream)
+ case server.__call_rpc__(path, http_method, stream) do
+ {:ok, stream, :noreply} ->
+ GRPC.Server.send_trailers(stream, @default_trailers)
+ {:ok, stream}
- case result do
{:ok, stream, response} ->
stream
|> GRPC.Server.send_reply(response)
@@ -647,7 +667,12 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
defp extract_subtype("application/grpc-web-text+" <> rest), do: {:ok, :grpcweb, rest}
defp extract_subtype(type) do
- Logger.warning("Got unknown content-type #{type}, please create an issue. ")
+ Logger.warning("""
+ Received invalid content-type: "#{type}".
+ This usually means the request is missing a proper Content-Type header,
+ or it's using a non-standard value. This may cause request parsing or response
+ encoding errors.
+ """)
{:ok, :unknown, "unknown"}
end
@@ -698,11 +723,31 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
{:wait, ref}
end
- defp log_error(%ReportException{kind: kind} = exception, stacktrace \\ []) do
+ defp maybe_log_error(exception, filter, stacktrace \\ [])
+
+ defp maybe_log_error(
+ %ReportException{} = exception,
+ {module, func_name},
+ stacktrace
+ ) do
+ if apply(module, func_name, [exception]) do
+ log_error(exception, stacktrace)
+ else
+ :ok
+ end
+ end
+
+ defp maybe_log_error(exception, nil, stacktrace) do
+ log_error(exception, stacktrace)
+ end
+
+ defp log_error(%ReportException{kind: kind} = exception, stacktrace) do
crash_reason = GRPC.Logger.crash_reason(kind, exception, stacktrace)
kind
|> Exception.format(exception, stacktrace)
|> Logger.error(crash_reason: crash_reason)
+
+ :ok
end
end
diff --git a/lib/grpc/server/supervisor.ex b/lib/grpc/server/supervisor.ex
index ea486a1fd..946d92cc4 100644
--- a/lib/grpc/server/supervisor.ex
+++ b/lib/grpc/server/supervisor.ex
@@ -41,6 +41,9 @@ defmodule GRPC.Server.Supervisor do
* `:endpoint` - defines the endpoint module that will be started.
* `:port` - the HTTP port for the endpoint.
* `:servers` - the list of servers that will be be started.
+ * `:exception_log_filter` - a `{module, function :: atom}` tuple that refers to a filter function of arity 1.
+ This function will be called with a `GRPC.Server.Adapters.ReportException` struct and must return a boolean
+ indicating whether or not a given exception should be logged or dropped. Defaults to `nil`, which means all exceptions will be logged.
* `:adapter_opts` - options for the adapter.
Either `:endpoint` or `:servers` must be present, but not both.
@@ -62,13 +65,20 @@ defmodule GRPC.Server.Supervisor do
end
opts =
- case Keyword.validate(opts, [:endpoint, :servers, :start_server, :port, :adapter_opts]) do
+ case Keyword.validate(opts, [
+ :endpoint,
+ :servers,
+ :start_server,
+ :port,
+ :adapter_opts,
+ :exception_log_filter
+ ]) do
{:ok, _opts} ->
opts
{:error, _} ->
raise ArgumentError,
- "just [:endpoint, :servers, :start_server, :port, :adapter_opts] are accepted as arguments, and any other keys for adapters should be passed as adapter_opts!"
+ "just [:endpoint, :servers, :start_server, :port, :adapter_opts, :exception_log_filter] are accepted as arguments, and any other keys for adapters should be passed as adapter_opts!"
end
case validate_cred(opts) do
diff --git a/lib/grpc/stream.ex b/lib/grpc/stream.ex
index 9bc8f96b0..84b115ba1 100644
--- a/lib/grpc/stream.ex
+++ b/lib/grpc/stream.ex
@@ -71,7 +71,7 @@ defmodule GRPC.Stream do
- `:dispatcher` — Specifies the `Flow` dispatcher (defaults to `GenStage.DemandDispatcher`).
- `:propagate_context` - If `true`, the context from the `materializer` is propagated to the `Flow`.
- `:materializer` - The `%GRPC.Server.Stream{}` struct representing the current gRPC stream context.
-
+
And any other options supported by `Flow`.
## Returns
@@ -82,6 +82,7 @@ defmodule GRPC.Stream do
flow = GRPC.Stream.from(request, max_demand: 50)
"""
+ @doc type: :creation
@spec from(any(), Keyword.t()) :: t()
def from(input, opts \\ [])
@@ -109,12 +110,13 @@ defmodule GRPC.Stream do
And any other options supported by `Flow`.
## Returns
- - A `GRPCStream` that emits the single gRPC message under demand.
+ - A `GRPC.Stream` that emits the single gRPC message under demand.
## Example
- flow = GRPCStream.single(request, max_demand: 5)
+ flow = GRPC.Stream.unary(request, max_demand: 5)
"""
+ @doc type: :creation
@spec unary(any(), Keyword.t()) :: t()
def unary(input, opts \\ []) when is_struct(input),
do: build_grpc_stream([input], Keyword.merge(opts, unary: true))
@@ -128,36 +130,55 @@ defmodule GRPC.Stream do
A `Flow` pipeline.
"""
+ @doc type: :transforms
@spec to_flow(t()) :: Flow.t()
def to_flow(%__MODULE__{flow: flow}) when is_nil(flow), do: Flow.from_enumerable([])
def to_flow(%__MODULE__{flow: flow}), do: flow
@doc """
- Executes the underlying `Flow` for unary streams and emits responses into the provided gRPC server stream.
+ Executes the underlying `Flow` for a unary stream.
- ## Parameters
+ The response will be emitted automatically to the provided
+ `:materializer` (set to a `GRPC.Server.Stream`) for the single resulting
+ item in the materialized enumerable.
- - `flow`: A `GRPC.Stream` struct containing the flow to be executed.
- - `stream`: A `GRPC.Server.Stream` to which responses are sent.
- - `:dry_run` — If `true`, responses are not sent (used for testing or inspection).
+ The `stream` argument must be initialized as a `:unary` stream with
+ a `:materializer` set.
## Example
- GRPC.Stream.run(request)
+ def say_unary_hello(request, mat) do
+ GRPC.Stream.unary(request, materializer: mat)
+ |> GRPC.Stream.map(fn
+ %HelloReply{} = reply ->
+ %HelloReply{message: "[Reply] message"}
+
+ {:error, _reason} ->
+ GRPC.RPCError.exception(message: "[Error] Something bad happened")
+ end)
+ |> GRPC.Stream.run()
+ end
"""
- @spec run(t()) :: any()
+ @doc type: :materialization
+ @spec run(stream :: t()) :: :noreply
def run(%__MODULE__{flow: flow, options: opts}) do
- if !Keyword.get(opts, :unary, false) do
- raise ArgumentError, "run/2 is not supported for non-unary streams"
+ opts = Keyword.take(opts, [:unary, :materializer])
+
+ if opts[:unary] != true do
+ raise ArgumentError, "GRPC.Stream.run/1 only supports unary streams"
end
- # We have to call `Enum.to_list` because we want to actually run and materialize the full stream.
- # List.flatten and List.first are used so that we can obtain the first result of the materialized list.
- flow
- |> Enum.to_list()
- |> List.flatten()
- |> List.first()
+ materializer = opts[:materializer]
+
+ if is_nil(materializer) do
+ raise ArgumentError,
+ "GRPC.Stream.run/1 requires a materializer to be set in the GRPC.Stream"
+ end
+
+ send_response(materializer, Enum.at(flow, 0), opts)
+
+ :noreply
end
@doc """
@@ -178,8 +199,24 @@ defmodule GRPC.Stream do
## Example
- GRPC.Stream.run_with(request, mat)
+ def say_bid_stream_hello(request, materializer) do
+ output_stream =
+ Stream.repeatedly(fn ->
+ %HelloReply{message: "I'm the Server ;)"}
+ end)
+
+ GRPC.Stream.from(request, join_with: output_stream)
+ |> GRPC.Stream.map(fn
+ %HelloRequest{} = _hello ->
+ %HelloReply{message: "Welcome Sr!"}
+
+ {:error, _reason} ->
+ GRPC.RPCError.exception(message: "[Error] Something bad happened")
+ end)
+ |> GRPC.Stream.run_with(materializer)
+ end
"""
+ @doc type: :materialization
@spec run_with(t(), Stream.t(), Keyword.t()) :: :ok
def run_with(
%__MODULE__{flow: flow, options: flow_opts} = _stream,
@@ -190,19 +227,72 @@ defmodule GRPC.Stream do
raise ArgumentError, "run_with/3 is not supported for unary streams"
end
- dry_run? = Keyword.get(opts, :dry_run, false)
-
flow
- |> Flow.map(fn msg ->
- if not dry_run? do
- send_response(from, msg)
- end
-
- flow
+ |> Flow.map(fn
+ {:ok, msg} ->
+ send_response(from, msg, opts)
+ flow
+
+ {:error, %GRPC.RPCError{} = reason} ->
+ send_response(from, reason, opts)
+ flow
+
+ {:error, reason} ->
+ msg = GRPC.RPCError.exception(message: "#{inspect(reason)}")
+ send_response(from, msg, opts)
+ flow
+
+ msg ->
+ send_response(from, msg, opts)
+ flow
end)
|> Flow.run()
end
+ @doc """
+ Applies a side-effect function to each element of the stream without altering its values.
+
+ The `effect/2` function is useful for performing imperative or external actions
+ (such as logging, sending messages, collecting metrics, or debugging)
+ while preserving the original stream data.
+
+ It behaves like `Enum.each/2`, but returns the stream itself so it can continue in the pipeline.
+
+ ## Examples
+
+ ```elixir
+ iex> parent = self()
+ iex> stream =
+ ...> GRPC.Stream.from([1, 2, 3])
+ ...> |> GRPC.Stream.effect(fn x -> send(parent, {:seen, x*2}) end)
+ ...> |> GRPC.Stream.to_flow()
+ ...> |> Enum.to_list()
+ iex> assert_receive {:seen, 2}
+ iex> assert_receive {:seen, 4}
+ iex> assert_receive {:seen, 6}
+ iex> stream
+ [1, 2, 3]
+ ```
+ In this example, the effect/2 function sends a message to the current process
+ for each element in the stream, but the resulting stream values remain unchanged.
+
+ ## Parameters
+
+ - `stream` — The input `GRPC.Stream`.
+ - `effect_fun` — A function that receives each item and performs a side effect
+ (e.g. IO.inspect/1, Logger.info/1, send/2, etc.).
+
+ ### Notes
+
+ - This function is lazy — the `effect_fun` will only run once the stream is materialized
+ (e.g. via `GRPC.Stream.run/1` or `GRPC.Stream.run_with/3`).
+ - The use of `effect/2` ensures that the original item is returned unchanged,
+ enabling seamless continuation of the pipeline.
+ """
+ @doc type: :actions
+ @spec effect(t(), (term -> any)) :: t()
+ defdelegate effect(stream, effect_fun), to: Operators
+
@doc """
Sends a request to an external process and awaits a response.
@@ -217,12 +307,9 @@ defmodule GRPC.Stream do
- `target`: Target process PID or atom name.
- `timeout`: Timeout in milliseconds (defaults to `5000`).
- ## Returns
-
- - Updated stream if successful.
- - `{:error, item, reason}` if the request fails or times out.
"""
- @spec ask(t(), pid | atom, non_neg_integer) :: t() | {:error, item(), reason()}
+ @doc type: :actions
+ @spec ask(t(), pid | atom, non_neg_integer) :: t() | {:error, :timeout | :process_not_alive}
defdelegate ask(stream, target, timeout \\ 5000), to: Operators
@doc """
@@ -233,6 +320,7 @@ defmodule GRPC.Stream do
This version propagates errors via raised exceptions, which can crash the Flow worker and halt the pipeline.
Prefer `ask/3` for production usage unless failure should abort the stream.
"""
+ @doc type: :actions
@spec ask!(t(), pid | atom, non_neg_integer) :: t()
defdelegate ask!(stream, target, timeout \\ 5000), to: Operators
@@ -242,6 +330,7 @@ defmodule GRPC.Stream do
The filter function is applied concurrently to the stream entries, so it shouldn't rely on execution order.
"""
@spec filter(t(), (term -> term)) :: t
+ @doc type: :transforms
defdelegate filter(stream, filter), to: Operators
@doc """
@@ -249,19 +338,81 @@ defmodule GRPC.Stream do
Useful for emitting multiple messages for each input.
"""
+ @doc type: :transforms
@spec flat_map(t, (term -> Enumerable.t())) :: t()
defdelegate flat_map(stream, flat_mapper), to: Operators
@doc """
Applies a function to each stream item.
"""
+ @doc type: :transforms
@spec map(t(), (term -> term)) :: t()
defdelegate map(stream, mapper), to: Operators
+ @doc """
+ Intercepts and transforms error tuples or unexpected exceptions that occur
+ within a gRPC stream pipeline.
+
+ `map_error/3` allows graceful handling or recovery from errors produced by previous
+ operators (e.g. `map/2`, `flat_map/2`) or from validation logic applied to incoming data.
+
+ The provided `handler/1` function receives the error reason (or the exception struct) like:
+
+ {:error, reason} -> failure
+ {:error, {:exception, exception}} -> failure due to exception
+ {:error, {kind, reason}} -> failure due to throw or exit
+
+ And can either:
+
+ * Return a new error tuple — e.g. `{:error, new_reason}` — to re-emit a modified error.
+ * Return any other value to recover from the failure and continue the pipeline.
+
+ This makes it suitable for both input validation and capturing unexpected runtime errors
+ in stream transformations.
+
+ ## Parameters
+
+ - `stream` — The input stream or `Flow` pipeline.
+ - `func` — A function that takes an error reason or exception and returns either a new value or an error tuple.
+
+ ## Returns
+
+ - A new stream where all error tuples and raised exceptions are processed by `func/1`.
+
+ ## Examples
+
+ iex> GRPC.Stream.from([1, 2])
+ ...> |> GRPC.Stream.map(fn
+ ...> 2 -> raise "boom"
+ ...> x -> x
+ ...> end)
+ ...> |> GRPC.Stream.map_error(fn
+ ...> {:error, {:exception, _reason}} ->
+ ...> {:error, GRPC.RPCError.exception(message: "Validation or runtime error")}
+ ...> end)
+
+ In this example:
+
+ * The call to `GRPC.Stream.map/2` raises an exception for value `2`.
+ * `map_error/3` catches the error and wraps it in a `GRPC.RPCError` struct with a custom message.
+ * The pipeline continues execution, transforming errors into structured responses.
+
+ ## Notes
+
+ - `map_error/3` is lazy and only executes when the stream is materialized
+ (via `GRPC.Stream.run/1` or `GRPC.Stream.run_with/3`).
+
+ - Use this operator to implement robust error recovery, input validation, or
+ to normalize exceptions from downstream Flow stages into well-defined gRPC errors.
+ """
+ @doc type: :transforms
+ defdelegate map_error(stream, func), to: Operators
+
@doc """
Applies a transformation function to each stream item, passing the context as an additional argument.
This is useful for operations that require access to the stream's headers.
"""
+ @doc type: :transforms
@spec map_with_context(t(), (map(), term -> term)) :: t()
defdelegate map_with_context(stream, mapper), to: Operators
@@ -277,6 +428,7 @@ defmodule GRPC.Stream do
See https://hexdocs.pm/flow/Flow.html#module-partitioning for more details.
"""
+ @doc type: :transforms
@spec partition(t(), keyword()) :: t()
defdelegate partition(stream, options \\ []), to: Operators
@@ -292,6 +444,7 @@ defmodule GRPC.Stream do
See https://hexdocs.pm/flow/Flow.html#reduce/3 for more details.
"""
+ @doc type: :transforms
@spec reduce(t, (-> acc), (term(), acc -> acc)) :: t when acc: term()
defdelegate reduce(stream, acc_fun, reducer_fun), to: Operators
@@ -299,6 +452,7 @@ defmodule GRPC.Stream do
Emits only distinct items from the stream. See `uniq_by/2` for more information.
"""
+ @doc type: :transforms
@spec uniq(t) :: t
defdelegate uniq(stream), to: Operators
@@ -309,6 +463,7 @@ defmodule GRPC.Stream do
This function requires care when used for unbounded flows. For more information see https://hexdocs.pm/flow/Flow.html#uniq_by/2
"""
+ @doc type: :transforms
@spec uniq_by(t, (term -> term)) :: t
defdelegate uniq_by(stream, fun), to: Operators
@@ -335,6 +490,20 @@ defmodule GRPC.Stream do
opts = Keyword.merge(opts, metadata: metadata)
dispatcher = Keyword.get(opts, :default_dispatcher, GenStage.DemandDispatcher)
+ if opts[:unary] do
+ case opts[:materializer] do
+ %GRPC.Server.Stream{grpc_type: :unary} ->
+ :ok
+
+ %GRPC.Server.Stream{} ->
+ raise ArgumentError,
+ "materializer must be set to a unary GRPC.Server.Stream when unary: true is passed"
+
+ _ ->
+ raise ArgumentError, "materializer is required when unary: true is passed"
+ end
+ end
+
flow =
case Keyword.get(opts, :join_with) do
pid when is_pid(pid) ->
@@ -369,7 +538,11 @@ defmodule GRPC.Stream do
%__MODULE__{flow: flow, options: opts}
end
- defp send_response(from, msg) do
- GRPC.Server.send_reply(from, msg)
+ defp send_response(from, msg, opts) do
+ dry_run? = Keyword.get(opts, :dry_run, false)
+
+ if not dry_run? do
+ GRPC.Server.send_reply(from, msg)
+ end
end
end
diff --git a/lib/grpc/stream/operators.ex b/lib/grpc/stream/operators.ex
index 1f3e15bb7..3404abbf0 100644
--- a/lib/grpc/stream/operators.ex
+++ b/lib/grpc/stream/operators.ex
@@ -9,9 +9,9 @@ defmodule GRPC.Stream.Operators do
@type reason :: any()
@spec ask(GRPCStream.t(), pid | atom, non_neg_integer) ::
- GRPCStream.t() | {:error, any(), :timeout | :not_alive}
+ GRPCStream.t() | {:error, :timeout | :process_not_alive}
def ask(%GRPCStream{flow: flow} = stream, target, timeout \\ 5000) do
- mapper = fn item -> do_ask(item, target, timeout, raise_on_error: false) end
+ mapper = fn item -> safe_invoke(&do_ask(&1, target, timeout, raise_on_error: false), item) end
%GRPCStream{stream | flow: Flow.map(flow, mapper)}
end
@@ -33,7 +33,7 @@ defmodule GRPC.Stream.Operators do
raise "Target #{inspect(target)} is not alive. Cannot send request to it."
is_nil(resolved_target) ->
- {:error, item, :not_alive}
+ {:error, :process_not_alive}
true ->
send(resolved_target, {:request, item, self()})
@@ -45,25 +45,45 @@ defmodule GRPC.Stream.Operators do
if raise? do
raise "Timeout waiting for response from #{inspect(target)}"
else
- {:error, item, :timeout}
+ {:error, :timeout}
end
end
end
end
+ @spec effect(GRPCStream.t(), (term -> term())) :: GRPCStream.t()
+ def effect(%GRPCStream{flow: flow} = stream, effect_fun) when is_function(effect_fun, 1) do
+ flow =
+ Flow.map(flow, fn flow_item ->
+ tap(flow_item, fn item -> safe_invoke(effect_fun, item) end)
+ end)
+
+ %GRPCStream{stream | flow: flow}
+ end
+
@spec filter(GRPCStream.t(), (term -> term)) :: GRPCStream.t()
def filter(%GRPCStream{flow: flow} = stream, filter) do
- %GRPCStream{stream | flow: Flow.filter(flow, filter)}
+ flow_wrapper = Flow.filter(flow, fn item -> safe_invoke(filter, item) end)
+ %GRPCStream{stream | flow: flow_wrapper}
end
@spec flat_map(GRPCStream.t(), (term -> Enumerable.GRPCStream.t())) :: GRPCStream.t()
def flat_map(%GRPCStream{flow: flow} = stream, flat_mapper) do
- %GRPCStream{stream | flow: Flow.flat_map(flow, flat_mapper)}
+ flow_wrapper =
+ Flow.flat_map(flow, fn item ->
+ case safe_invoke(flat_mapper, item) do
+ {:error, reason} -> [{:error, reason}]
+ res -> res
+ end
+ end)
+
+ %GRPCStream{stream | flow: flow_wrapper}
end
@spec map(GRPCStream.t(), (term -> term)) :: GRPCStream.t()
def map(%GRPCStream{flow: flow} = stream, mapper) do
- %GRPCStream{stream | flow: Flow.map(flow, mapper)}
+ flow_wrapper = Flow.map(flow, fn item -> safe_invoke(mapper, item) end)
+ %GRPCStream{stream | flow: flow_wrapper}
end
@spec map_with_context(GRPCStream.t(), (map(), term -> term)) :: GRPCStream.t()
@@ -73,7 +93,37 @@ defmodule GRPC.Stream.Operators do
mapper.(meta, item)
end
- %GRPCStream{stream | flow: Flow.map(flow, wrapper)}
+ flow_wrapper = Flow.map(flow, fn item -> safe_invoke(wrapper, item) end)
+
+ %GRPCStream{stream | flow: flow_wrapper}
+ end
+
+ @spec map_error(GRPCStream.t(), (reason -> term)) :: GRPCStream.t()
+ def map_error(%GRPCStream{flow: flow} = stream, func) when is_function(func, 1) do
+ mapper =
+ Flow.map(flow, fn
+ {:error, reason} -> handle_error(func, reason)
+ {:ok, value} -> value
+ other -> other
+ end)
+
+ %GRPCStream{stream | flow: mapper}
+ end
+
+ defp handle_error(func, reason) do
+ case safe_invoke(func, {:error, reason}) do
+ {:error, %GRPC.RPCError{} = rpc_error} ->
+ {:error, rpc_error}
+
+ {:error, other_reason} ->
+ {:error, GRPC.RPCError.exception(message: inspect(other_reason))}
+
+ {:ok, value} ->
+ value
+
+ other ->
+ other
+ end
end
@spec partition(GRPCStream.t(), keyword()) :: GRPCStream.t()
@@ -88,7 +138,8 @@ defmodule GRPC.Stream.Operators do
@spec reject(GRPCStream.t(), (term -> term)) :: GRPCStream.t()
def reject(%GRPCStream{flow: flow} = stream, filter) do
- %GRPCStream{stream | flow: Flow.reject(flow, filter)}
+ flow_wrapper = Flow.reject(flow, fn item -> safe_invoke(filter, item) end)
+ %GRPCStream{stream | flow: flow_wrapper}
end
@spec uniq(GRPCStream.t()) :: GRPCStream.t()
@@ -100,4 +151,26 @@ defmodule GRPC.Stream.Operators do
def uniq_by(%GRPCStream{flow: flow} = stream, fun) do
%GRPCStream{stream | flow: Flow.uniq_by(flow, fun)}
end
+
+ # Normalizes and catches exceptions/throws.
+ # Returns:
+ # value -> successful value
+ # {:error, reason} -> failure
+ # {:error, {:exception, exception}} -> failure due to exception
+ # {:error, {kind, reason}} -> failure due to throw or exit
+ defp safe_invoke(fun, arg) do
+ res = fun.(arg)
+
+ case res do
+ {:ok, v} -> v
+ {:error, reason} -> {:error, reason}
+ other -> other
+ end
+ rescue
+ e ->
+ {:error, {:exception, e}}
+ catch
+ kind, reason ->
+ {:error, {kind, reason}}
+ end
end
diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex
index 715a14fd0..2ceabc60d 100644
--- a/lib/grpc/stub.ex
+++ b/lib/grpc/stub.ex
@@ -106,10 +106,10 @@ defmodule GRPC.Stub do
@doc """
Establishes a connection with a gRPC server and returns a `GRPC.Channel` required
- for sending requests. Supports advanced connection resolution via the gRPC `Resolver`
+ for sending requests. Supports advanced connection resolution via the gRPC `GRPC.Client.Resolver`
and various target schemes (`dns`, `unix`, `xds`, `host:port`, etc).
- This function is part of the **connection orchestration layer**, which manages
+ This function is part of the connection orchestration layer, which manages
connection setup, name resolution, and optional load balancing.
## Target Syntax
diff --git a/mix.exs b/mix.exs
index 6a8d4fe54..592a42f8e 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,7 +1,8 @@
defmodule GRPC.Mixfile do
use Mix.Project
- @version "0.11.2"
+ @source_url "https://github.com/elixir-grpc/grpc"
+ @version "0.11.5"
def project do
[
@@ -10,17 +11,13 @@ defmodule GRPC.Mixfile do
elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()),
build_embedded: Mix.env() == :prod,
- start_permanent: Mix.env() == :prod,
deps: deps(),
+ docs: docs(),
+ name: "gRPC",
+ start_permanent: Mix.env() == :prod,
package: package(),
aliases: aliases(),
- description: "The Elixir implementation of gRPC",
- docs: [
- extras: ["README.md"],
- main: "readme",
- source_ref: "v#{@version}",
- source_url: "https://github.com/elixir-grpc/grpc"
- ],
+ description: "The Elixir implementation of gRPC Protocol",
xref: [exclude: [IEx]]
]
end
@@ -43,12 +40,14 @@ defmodule GRPC.Mixfile do
{:cowlib, "~> 2.12"},
{:castore, "~> 0.1 or ~> 1.0", optional: true},
{:protobuf, "~> 0.14"},
- {:protobuf_generate, "~> 0.1.1", only: [:dev, :test]},
{:mint, "~> 1.5"},
- {:ex_doc, "~> 0.29", only: :dev},
+ {:telemetry, "~> 1.0"},
+ {:protobuf_generate, "~> 0.1.1", only: [:dev, :test]},
{:ex_parameterized, "~> 1.3.7", only: :test},
{:mox, "~> 1.2", only: :test},
- {:telemetry, "~> 1.0"}
+ {:ex_doc, "~> 0.39", only: [:dev, :docs]},
+ {:makeup, "~> 1.2.1", only: [:dev, :docs]},
+ {:makeup_syntect, "~> 0.1", only: [:dev, :docs]}
]
end
@@ -61,6 +60,110 @@ defmodule GRPC.Mixfile do
}
end
+ defp docs() do
+ [
+ main: "GRPC",
+ source_ref: "v#{@version}",
+ source_url_pattern: "#{@source_url}/blob/v#{@version}/grpc/%{path}#L%{line}",
+ extras: [
+ "CHANGELOG.md",
+ "guides/getting_started/quickstart.livemd",
+ "guides/getting_started/stream.livemd",
+ "guides/getting_started/error_handling.md",
+ "guides/getting_started/client.md",
+ "guides/getting_started/codegen.md",
+ "guides/cheatsheets/streams.cheatmd",
+ "guides/advanced/transcoding.livemd",
+ "guides/advanced/load_balancing.md",
+ "guides/advanced/cors.md",
+ "guides/advanced/telemetry.livemd",
+ "guides/advanced/pooling.md"
+ ],
+ skip_undefined_reference_warnings_on: ["CHANGELOG.md"],
+ groups_for_docs: [
+ "Functions: Creation": &(&1[:type] == :creation),
+ "Functions: Materializers": &(&1[:type] == :materialization),
+ "Functions: Transformers": &(&1[:type] == :transforms),
+ "Functions: Actions": &(&1[:type] == :actions)
+ ],
+ groups_for_modules: [
+ Server: [
+ GRPC.Server,
+ GRPC.Service,
+ GRPC.Endpoint,
+ GRPC.Server.Supervisor,
+ GRPC.Server.Adapter,
+ GRPC.Server.Adapters.Cowboy,
+ GRPC.Server.Adapters.Cowboy.Handler,
+ GRPC.Server.Adapters.Cowboy.Router,
+ GRPC.Status,
+ GRPC.Server.Stream,
+ GRPC.Server.Interceptors.Logger,
+ GRPC.Server.Interceptors.CORS,
+ GRPC.Server.Router
+ ],
+ Stream: [
+ GRPC.Stream,
+ GRPC.Stream.Operators
+ ],
+ Client: [
+ GRPC.Stub,
+ GRPC.Channel,
+ GRPC.Credential,
+ GRPC.Client.Supervisor,
+ GRPC.Client.Adapter,
+ GRPC.Client.Adapters.Gun,
+ GRPC.Client.Adapters.Mint,
+ GRPC.Client.Adapters.Mint.StreamResponseProcess,
+ GRPC.Client.Adapters.Mint.ConnectionProcess,
+ GRPC.Client.Adapters.Mint.ConnectionProcess.State,
+ GRPC.Client.Interceptor,
+ GRPC.Client.Interceptors.Logger,
+ GRPC.Client.Connection,
+ GRPC.Client.LoadBalancing,
+ GRPC.Client.LoadBalancing.RoundRobin,
+ GRPC.Client.LoadBalancing.PickFirst,
+ GRPC.Client.Resolver,
+ GRPC.Client.Resolver.DNS,
+ GRPC.Client.Resolver.DNS.Adapter,
+ GRPC.Client.Resolver.Unix,
+ GRPC.Client.Resolver.IPv4,
+ GRPC.Client.Resolver.IPv6,
+ GRPC.Client.ServiceConfig,
+ GRPC.Client.Stream
+ ],
+ Telemetry: [
+ GRPC.Telemetry
+ ],
+ Transport: [
+ GRPC.Transport.HTTP2,
+ GRPC.Transport.Utils
+ ],
+ Codecs: [
+ GRPC.Codec,
+ GRPC.Codec.Erlpack,
+ GRPC.Codec.JSON,
+ GRPC.Codec.Proto,
+ GRPC.Codec.WebText
+ ],
+ Compressors: [
+ GRPC.Compressor,
+ GRPC.Compressor.Gzip
+ ],
+ "Error Handling": [
+ GRPC.RPCError,
+ GRPC.Server.Adapters.ReportException,
+ GRPC.Logger
+ ]
+ ],
+ groups_for_extras: [
+ "Getting Started": ~r"^guides/getting_started/",
+ Advanced: ~r"^guides/advanced/",
+ Cheatsheets: ~r"^guides/cheatsheets/"
+ ]
+ ]
+ end
+
defp aliases do
[
gen_test_protos: &gen_test_protos/1
diff --git a/mix.lock b/mix.lock
index d56638390..00abe334a 100644
--- a/mix.lock
+++ b/mix.lock
@@ -2,8 +2,8 @@
"castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
"cowboy": {:hex, :cowboy, "2.11.0", "356bf784599cf6f2cdc6ad12fdcfb8413c2d35dab58404cf000e1feaed3f5645", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0fa395437f1b0e104e0e00999f39d2ac5f4082ac5049b67a5b6d56ecc31b1403"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
- "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
- "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"},
+ "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
+ "ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"},
"ex_parameterized": {:hex, :ex_parameterized, "1.3.7", "801f85fc4651cb51f11b9835864c6ed8c5e5d79b1253506b5bb5421e8ab2f050", [:mix], [], "hexpm", "1fb0dc4aa9e8c12ae23806d03bcd64a5a0fc9cd3f4c5602ba72561c9b54a625c"},
"flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"},
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
@@ -11,16 +11,18 @@
"gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
- "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
- "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
- "makeup_erlang": {:hex, :makeup_erlang, "0.1.4", "29563475afa9b8a2add1b7a9c8fb68d06ca7737648f28398e04461f008b69521", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f4ed47ecda66de70dd817698a703f8816daa91272e7e45812469498614ae8b29"},
+ "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
+ "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
+ "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
+ "makeup_syntect": {:hex, :makeup_syntect, "0.1.3", "ae2c3437f479ea50d08d794acaf02a2f3a8c338dd1f757f6b237c42eb27fcde1", [:mix], [{:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: false]}, {:rustler, "~> 0.36.1", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8.2", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "a27bd3bd8f7b87465d110295a33ed1022202bea78701bd2bbeadfb45d690cdbf"},
"meck": {:hex, :meck, "1.0.0", "24676cb6ee6951530093a93edcd410cfe4cb59fe89444b875d35c9d3909a15d0", [:rebar3], [], "hexpm", "680a9bcfe52764350beb9fb0335fb75fee8e7329821416cee0a19fec35433882"},
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
"nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"},
- "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
+ "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"},
"protobuf_generate": {:hex, :protobuf_generate, "0.1.3", "57841bc60e2135e190748119d83f78669ee7820c0ad6555ada3cd3cd7df93143", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "dae4139b00ba77a279251a0ceb5593b1bae745e333b4ce1ab7e81e8e4906016b"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
+ "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.3", "4e741024b0b097fe783add06e53ae9a6f23ddc78df1010f215df0c02915ef5a8", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "c23f5f33cb6608542de4d04faf0f0291458c352a4648e4d28d17ee1098cddcc4"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
}
diff --git a/test/grpc/client/supervisor_test.exs b/test/grpc/client/supervisor_test.exs
new file mode 100644
index 000000000..cd83c871f
--- /dev/null
+++ b/test/grpc/client/supervisor_test.exs
@@ -0,0 +1,14 @@
+defmodule GRPC.Client.SupervisorTest do
+ use ExUnit.Case, async: false
+
+ alias GRPC.Client
+
+ describe "start_link/1" do
+ test "allows multiple start_links" do
+ {:ok, second_pid} = Client.Supervisor.start_link([])
+ {:ok, third_pid} = Client.Supervisor.start_link([])
+
+ assert second_pid == third_pid
+ end
+ end
+end
diff --git a/test/grpc/integration/connection_test.exs b/test/grpc/integration/connection_test.exs
index 436e3ae5e..cede712c0 100644
--- a/test/grpc/integration/connection_test.exs
+++ b/test/grpc/integration/connection_test.exs
@@ -45,4 +45,18 @@ defmodule GRPC.Integration.ConnectionTest do
:ok = GRPC.Server.stop(server)
end
end
+
+ test "disconnect does not crash when some channels failed to connect" do
+ server = FeatureServer
+ {:ok, _, port} = GRPC.Server.start(server, 0)
+
+ # Connect with multiple addresses where one is unreachable.
+ # This produces {:error, _} entries in real_channels via build_real_channels.
+ # retry: 0 prevents Gun from retrying the unreachable address (~5s → <1ms).
+ {:ok, channel} =
+ GRPC.Stub.connect("ipv4:127.0.0.1:#{port},127.0.0.1:1", adapter_opts: [retry: 0])
+
+ assert {:ok, _} = GRPC.Stub.disconnect(channel)
+ :ok = GRPC.Server.stop(server)
+ end
end
diff --git a/test/grpc/integration/server_test.exs b/test/grpc/integration/server_test.exs
index c72d7d5b2..0e0684bd9 100644
--- a/test/grpc/integration/server_test.exs
+++ b/test/grpc/integration/server_test.exs
@@ -4,8 +4,12 @@ defmodule GRPC.Integration.ServerTest do
defmodule FeatureServer do
use GRPC.Server, service: Routeguide.RouteGuide.Service
- def get_feature(point, _stream) do
- %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"}
+ def get_feature(point, materializer) do
+ GRPC.Stream.unary(point, materializer: materializer)
+ |> GRPC.Stream.map(fn point ->
+ %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"}
+ end)
+ |> GRPC.Stream.run()
end
def route_chat(_ex_stream, stream) do
@@ -32,19 +36,25 @@ defmodule GRPC.Integration.ServerTest do
service: Transcode.Messaging.Service,
http_transcode: true
- def get_message(msg_request, _stream) do
- %Transcode.Message{name: msg_request.name, text: "get_message"}
+ def get_message(msg_request, stream) do
+ GRPC.Stream.unary(msg_request, materializer: stream)
+ |> GRPC.Stream.map(fn req ->
+ %Transcode.Message{name: req.name, text: "get_message"}
+ end)
+ |> GRPC.Stream.run()
end
- def stream_messages(msg_request, stream) do
- Enum.each(1..5, fn i ->
- msg = %Transcode.Message{
+ def stream_messages(msg_request, materializer) do
+ 1..5
+ |> Stream.take(5)
+ |> GRPC.Stream.from()
+ |> GRPC.Stream.map(fn i ->
+ %Transcode.Message{
name: msg_request.name,
text: "#{i}"
}
-
- GRPC.Server.send_reply(stream, msg)
end)
+ |> GRPC.Stream.run_with(materializer)
end
def create_message(msg, _stream) do
@@ -62,24 +72,33 @@ defmodule GRPC.Integration.ServerTest do
msg_request.message
end
- def get_message_with_response_body(msg_request, _) do
- %Transcode.MessageOut{
- response: %Transcode.Message{
- name: msg_request.name,
- text: "get_message_with_response_body"
+ def get_message_with_response_body(msg_request, materializer) do
+ GRPC.Stream.unary(msg_request, materializer: materializer)
+ |> GRPC.Stream.map(fn req ->
+ %Transcode.MessageOut{
+ response: %Transcode.Message{
+ name: req.name,
+ text: "get_message_with_response_body"
+ }
}
- }
+ end)
+ |> GRPC.Stream.run()
end
- def get_message_with_query(msg_request, _stream) do
- %Transcode.Message{name: msg_request.name, text: "get_message_with_query"}
+ def get_message_with_query(msg_request, materializer) do
+ GRPC.Stream.unary(msg_request, materializer: materializer)
+ |> GRPC.Stream.map(fn req ->
+ %Transcode.Message{name: req.name, text: "get_message_with_query"}
+ end)
+ |> GRPC.Stream.run()
end
- def get_message_with_subpath_query(msg_request, _stream) do
- %Transcode.Message{
- name: msg_request.message.name,
- text: "get_message_with_subpath_query"
- }
+ def get_message_with_subpath_query(msg_request, materializer) do
+ GRPC.Stream.unary(msg_request, materializer: materializer)
+ |> GRPC.Stream.map(fn req ->
+ %Transcode.Message{name: req.message.name, text: "get_message_with_subpath_query"}
+ end)
+ |> GRPC.Stream.run()
end
end
@@ -168,13 +187,14 @@ defmodule GRPC.Integration.ServerTest do
defmodule SlowServer do
use GRPC.Server, service: Routeguide.RouteGuide.Service
- def list_features(rectangle, stream) do
+ def list_features(rectangle, materializer) do
Process.sleep(400)
+ server_stream = Stream.each([rectangle.lo, rectangle.hi], fn point -> point end)
- Enum.each([rectangle.lo, rectangle.hi], fn point ->
- feature = simple_feature(point)
- GRPC.Server.send_reply(stream, feature)
- end)
+ server_stream
+ |> GRPC.Stream.from()
+ |> GRPC.Stream.map(&simple_feature/1)
+ |> GRPC.Stream.run_with(materializer)
end
defp simple_feature(point) do
@@ -188,6 +208,16 @@ defmodule GRPC.Integration.ServerTest do
end
end
+ defmodule ExceptionLogFilter do
+ def always_allow(_exception) do
+ true
+ end
+
+ def never_allow(_exception) do
+ false
+ end
+ end
+
test "multiple servers works" do
run_server([FeatureServer, HelloServer], fn port ->
{:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
@@ -257,6 +287,110 @@ defmodule GRPC.Integration.ServerTest do
assert logs =~ "Exception raised while handling /helloworld.Greeter/SayHello"
end
+ test "logs error if exception_log_filter returns true" do
+ logs =
+ ExUnit.CaptureLog.capture_log(fn ->
+ run_server(
+ [HelloErrorServer],
+ fn port ->
+ {:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
+ req = %Helloworld.HelloRequest{name: "unknown error"}
+ Helloworld.Greeter.Stub.say_hello(channel, req)
+ end,
+ 0,
+ exception_log_filter: {ExceptionLogFilter, :always_allow}
+ )
+ end)
+
+ assert logs =~ "Exception raised while handling /helloworld.Greeter/SayHello"
+ end
+
+ test "does not log error if exception_log_filter returns false" do
+ logs =
+ ExUnit.CaptureLog.capture_log(fn ->
+ run_server(
+ [HelloErrorServer],
+ fn port ->
+ {:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
+ req = %Helloworld.HelloRequest{name: "unknown error"}
+ Helloworld.Greeter.Stub.say_hello(channel, req)
+ end,
+ 0,
+ exception_log_filter: {TestFalseFilter, :never_allow}
+ )
+ end)
+
+ refute logs =~ "Exception raised while handling /helloworld.Greeter/SayHello"
+ end
+
+ defmodule ExceptionFilterMustBeRPCError do
+ def filter(exception) do
+ data = exception.adapter_extra[:req][:headers]["test-data"]
+
+ {pid, ref} = :erlang.binary_to_term(data)
+ send(pid, {:exception_log_filter, ref, exception})
+
+ true
+ end
+ end
+
+ test "passes RPCErrors to `exception_log_filter" do
+ test_pid = self()
+ ref = make_ref()
+
+ run_server(
+ [HelloErrorServer],
+ fn port ->
+ {:ok, channel} =
+ GRPC.Stub.connect("localhost:#{port}",
+ headers: [{"test-data", :erlang.term_to_binary({test_pid, ref})}]
+ )
+
+ req = %Helloworld.HelloRequest{name: "world"}
+ Helloworld.Greeter.Stub.say_hello(channel, req)
+ end,
+ 0,
+ exception_log_filter: {ExceptionFilterMustBeRPCError, :filter}
+ )
+
+ assert_receive {:exception_log_filter, ^ref,
+ %GRPC.Server.Adapters.ReportException{reason: %GRPC.RPCError{}}}
+ end
+
+ defmodule ExceptionFilterMustBeRaisedError do
+ def filter(exception) do
+ data = exception.adapter_extra[:req][:headers]["test-data"]
+
+ {pid, ref} = :erlang.binary_to_term(data)
+ send(pid, {:exception_log_filter, ref, exception})
+
+ true
+ end
+ end
+
+ test "passes thrown exceptions to `exception_log_filter" do
+ test_pid = self()
+ ref = make_ref()
+
+ run_server(
+ [HelloErrorServer],
+ fn port ->
+ {:ok, channel} =
+ GRPC.Stub.connect("localhost:#{port}",
+ headers: [{"test-data", :erlang.term_to_binary({test_pid, ref})}]
+ )
+
+ req = %Helloworld.HelloRequest{name: "unknown error", duration: 0}
+ Helloworld.Greeter.Stub.say_hello(channel, req)
+ end,
+ 0,
+ exception_log_filter: {ExceptionFilterMustBeRaisedError, :filter}
+ )
+
+ assert_receive {:exception_log_filter, ^ref,
+ %GRPC.Server.Adapters.ReportException{reason: %RuntimeError{}}}
+ end
+
test "returns appropriate error for stream requests" do
run_server([FeatureErrorServer], fn port ->
{:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
diff --git a/test/grpc/stream_test.exs b/test/grpc/stream_test.exs
index eebfa6a95..e21662335 100644
--- a/test/grpc/stream_test.exs
+++ b/test/grpc/stream_test.exs
@@ -1,5 +1,5 @@
defmodule GRPC.StreamTest do
- use ExUnit.Case
+ use GRPC.Integration.TestCase
doctest GRPC.Stream
describe "simple test" do
@@ -9,22 +9,42 @@ defmodule GRPC.StreamTest do
defmodule FakeAdapter do
def get_headers(_), do: %{"content-type" => "application/grpc"}
- end
- test "unary/2 creates a flow from a unary input" do
- input = %TestInput{message: 1}
+ def send_reply(%{test_pid: test_pid, ref: ref}, item, _opts) do
+ send(test_pid, {:send_reply, ref, item})
+ end
- result =
- GRPC.Stream.unary(input)
- |> GRPC.Stream.map(& &1)
- |> GRPC.Stream.run()
+ def send_trailers(%{test_pid: test_pid, ref: ref}, trailers) do
+ send(test_pid, {:send_trailers, ref, trailers})
+ end
+ end
- assert result == input
+ test "unary/2 creates a flow from a unary input" do
+ test_pid = self()
+ ref = make_ref()
+
+ input = %Routeguide.Point{latitude: 1, longitude: 2}
+
+ materializer = %GRPC.Server.Stream{
+ adapter: FakeAdapter,
+ payload: %{test_pid: test_pid, ref: ref},
+ grpc_type: :unary
+ }
+
+ assert :noreply =
+ GRPC.Stream.unary(input, materializer: materializer)
+ |> GRPC.Stream.map(fn item ->
+ item
+ end)
+ |> GRPC.Stream.run()
+
+ assert_receive {:send_reply, ^ref, response}
+ assert IO.iodata_to_binary(response) == Protobuf.encode(input)
end
test "unary/2 creates a flow with metadata" do
input = %TestInput{message: 1}
- materializer = %GRPC.Server.Stream{adapter: FakeAdapter}
+ materializer = %GRPC.Server.Stream{adapter: FakeAdapter, grpc_type: :unary}
flow =
GRPC.Stream.unary(input, materializer: materializer, propagate_context: true)
@@ -51,7 +71,7 @@ defmodule GRPC.StreamTest do
test "from_as_ctx/3 creates a flow from enumerable input" do
input = [%{message: "a"}, %{message: "b"}]
- materializer = %GRPC.Server.Stream{adapter: FakeAdapter}
+ materializer = %GRPC.Server.Stream{adapter: FakeAdapter, grpc_type: :unary}
flow =
GRPC.Stream.from(input, propagate_context: true, materializer: materializer)
@@ -107,7 +127,7 @@ defmodule GRPC.StreamTest do
|> GRPC.Stream.to_flow()
|> Enum.to_list()
- assert result == [{:error, "msg", :not_alive}]
+ assert result == [{:error, :process_not_alive}]
end
end
@@ -145,6 +165,138 @@ defmodule GRPC.StreamTest do
end
end
+ describe "ask/3 error handling" do
+ test "returns timeout error if response not received in time" do
+ pid =
+ spawn_link(fn ->
+ Process.sleep(:infinity)
+ end)
+
+ result =
+ GRPC.Stream.from([:hello])
+ # very short timeout
+ |> GRPC.Stream.ask(pid, 10)
+ |> GRPC.Stream.to_flow()
+ |> Enum.to_list()
+
+ assert result == [{:error, :timeout}]
+ end
+ end
+
+ describe "safe_invoke/2 handling {:ok, value} and direct value" do
+ test "maps {:ok, value} to value" do
+ stream =
+ GRPC.Stream.from([1, 2])
+ |> GRPC.Stream.map(fn x -> {:ok, x * 10} end)
+
+ result = stream |> GRPC.Stream.to_flow() |> Enum.to_list()
+ assert result == [10, 20]
+ end
+
+ test "keeps direct values as is" do
+ stream =
+ GRPC.Stream.from([1, 2])
+ |> GRPC.Stream.map(fn x -> x * 5 end)
+
+ result = stream |> GRPC.Stream.to_flow() |> Enum.to_list()
+ assert result == [5, 10]
+ end
+ end
+
+ describe "safe_invoke/2 catches errors" do
+ test "map/2 handles function returning {:error, reason}" do
+ stream =
+ GRPC.Stream.from([1, 2, 3])
+ |> GRPC.Stream.map(fn
+ 2 -> {:error, :fail}
+ x -> x
+ end)
+
+ results = stream |> GRPC.Stream.to_flow() |> Enum.to_list()
+ assert results == [1, {:error, :fail}, 3]
+ end
+
+ test "map/2 catches exceptions" do
+ stream =
+ GRPC.Stream.from([1, 2])
+ |> GRPC.Stream.map(fn
+ 2 -> raise "boom"
+ x -> x
+ end)
+
+ results = stream |> GRPC.Stream.to_flow() |> Enum.to_list()
+ assert match?([1, {:error, {:exception, %RuntimeError{message: "boom"}}}], results)
+ end
+
+ test "flat_map/2 catches thrown values" do
+ stream =
+ GRPC.Stream.from([1, 2])
+ |> GRPC.Stream.flat_map(fn
+ 2 -> throw(:fail)
+ x -> [x]
+ end)
+
+ results = stream |> GRPC.Stream.to_flow() |> Enum.to_list()
+ assert results == [1, {:error, {:throw, :fail}}]
+ end
+ end
+
+ describe "map_error/2" do
+ test "transforms {:error, reason} tuples" do
+ stream =
+ GRPC.Stream.from([{:error, :invalid_input}, {:ok, 42}, 100])
+ |> GRPC.Stream.map_error(fn
+ {:error, :invalid_input} -> {:error, :mapped_error}
+ msg -> msg
+ end)
+
+ result = stream |> GRPC.Stream.to_flow() |> Enum.to_list()
+
+ assert Enum.sort(result) == [
+ 42,
+ 100,
+ {:error,
+ %GRPC.RPCError{
+ __exception__: true,
+ details: nil,
+ message: ":mapped_error",
+ status: nil
+ }}
+ ]
+ end
+
+ test "transforms exceptions raised inside previous map" do
+ stream =
+ GRPC.Stream.from([1, 2])
+ |> GRPC.Stream.map(fn
+ 2 -> raise "boom"
+ x -> x
+ end)
+ |> GRPC.Stream.map_error(fn
+ {:error, %RuntimeError{message: "boom"}} ->
+ GRPC.RPCError.exception(message: "Validation or runtime error")
+
+ msg ->
+ msg
+ end)
+
+ result = stream |> GRPC.Stream.to_flow() |> Enum.to_list()
+
+ assert match?(
+ [
+ 1,
+ {:error,
+ %GRPC.RPCError{
+ status: nil,
+ message: "{:exception, %RuntimeError{message: \"boom\"}}",
+ details: nil
+ }}
+ ],
+ result
+ )
+ end
+ end
+
describe "map/2, flat_map/2, filter/2" do
test "maps values correctly" do
result =
@@ -177,6 +329,44 @@ defmodule GRPC.StreamTest do
end
end
+ describe "effect/2" do
+ test "applies side effects without altering values" do
+ parent = self()
+
+ result =
+ GRPC.Stream.from([1, 2, 3])
+ |> GRPC.Stream.effect(fn x -> send(parent, {:effect_called, x}) end)
+ |> GRPC.Stream.to_flow()
+ |> Enum.to_list()
+
+ assert Enum.sort(result) == [1, 2, 3]
+
+ assert_receive {:effect_called, 1}
+ assert_receive {:effect_called, 2}
+ assert_receive {:effect_called, 3}
+ end
+
+ test "continues pipeline even if effect function raises an error" do
+ parent = self()
+
+ result =
+ GRPC.Stream.from([1, 2, 3])
+ |> GRPC.Stream.effect(fn
+ 2 -> raise "boom"
+ x -> send(parent, {:effect_called, x})
+ end)
+ |> GRPC.Stream.to_flow()
+ |> Enum.to_list()
+
+ # Even with error 2, the pipeline should continue and return all elements
+ assert Enum.sort(result) == [1, 2, 3]
+
+ # The effect should have been called for 1 and 3
+ assert_receive {:effect_called, 1}
+ assert_receive {:effect_called, 3}
+ end
+ end
+
describe "test complex operations" do
test "pipeline with all GRPC.Stream operators" do
target =
@@ -257,6 +447,38 @@ defmodule GRPC.StreamTest do
end
end
+ describe "run/1" do
+ defmodule MyGRPCService do
+ use GRPC.Server, service: Routeguide.RouteGuide.Service
+
+ def get_feature(input, materializer) do
+ GRPC.Stream.unary(input, materializer: materializer)
+ |> GRPC.Stream.map(fn point ->
+ %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"}
+ end)
+ |> GRPC.Stream.run()
+ end
+ end
+
+ test "runs a unary stream" do
+ run_server([MyGRPCService], fn port ->
+ point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906}
+ {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", adapter_opts: [retry_timeout: 10])
+
+ expected_response = %Routeguide.Feature{
+ location: point,
+ name: "#{point.latitude},#{point.longitude}"
+ }
+
+ assert {:ok, response, %{trailers: trailers}} =
+ Routeguide.RouteGuide.Stub.get_feature(channel, point, return_headers: true)
+
+ assert response == expected_response
+ assert trailers == GRPC.Transport.HTTP2.server_trailers()
+ end)
+ end
+ end
+
defp receive_loop do
receive do
{:request, item, from} ->
diff --git a/test/support/integration_test_case.ex b/test/support/integration_test_case.ex
index 4a4dbe9a8..b51aecd19 100644
--- a/test/support/integration_test_case.ex
+++ b/test/support/integration_test_case.ex
@@ -10,7 +10,14 @@ defmodule GRPC.Integration.TestCase do
end
def run_server(servers, func, port \\ 0, opts \\ []) do
- {:ok, _pid, port} = GRPC.Server.start(servers, port, opts)
+ {:ok, _pid, port} =
+ start_supervised(%{
+ id: {GRPC.Server, System.unique_integer([:positive])},
+ start: {GRPC.Server, :start, [servers, port, opts]},
+ type: :worker,
+ restart: :permanent,
+ shutdown: 500
+ })
try do
func.(port)
@@ -20,7 +27,14 @@ defmodule GRPC.Integration.TestCase do
end
def run_endpoint(endpoint, func, port \\ 0) do
- {:ok, _pid, port} = GRPC.Server.start_endpoint(endpoint, port)
+ {:ok, _pid, port} =
+ start_supervised(%{
+ id: {GRPC.Server, System.unique_integer([:positive])},
+ start: {GRPC.Server, :start_endpoint, [endpoint, port]},
+ type: :worker,
+ restart: :permanent,
+ shutdown: 500
+ })
try do
func.(port)