Protein v0.16.0 Protein.Client View Source

Calls services in remote systems.

Usage

Here's how your RPC client module may look like:

defmodule MyProject.RemoteRPC do
  use Protein.Client

  # then, declare services with a convention driven config
  proto :create_user

  # ...or with custom proto file name (equivalent of previous call above)
  proto Path.expand("./proto/create_user.proto", __DIR__)

  # ...or with a completely custom config (equivalent of previous calls above)
  service proto: [from: Path.expand("./proto/create_user.proto", __DIR__)],
          service_name: "create_user",
          proto_mod: __MODULE__.CreateUser
          request_mod: __MODULE__.CreateUser.Request,
          response_mod: __MODULE__.CreateUser.Response,
          mock_mod: __MODULE__.CreateUserMock
end

Make sure to add it to the supervision tree in application.ex as follows:

defmodule MyProject.Application do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec

    children = [
      supervisor(MyProject.Repo, []),
      supervisor(MyProject.Web.Endpoint, []),
      # ...
      supervisor(MyProject.RemoteRPC, []),
    ]

    opts = [strategy: :one_for_one, name: MyProject.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Having that, you can call your RPC as follows:

alias MyProject.RemoteRPC
alias MyProject.RemoteRPC.CreateUser.{Request, Response}

request = %Request{}

case RemoteRPC.call(request) do
  {:ok, response = %Response{}} ->
    # do stuff with response
  {:error, errors}
    # do stuff with errors
end

# ...or assume that a failure is out of the question
response = RemoteRPC.call!(request)

# ...or issue a push to non-responding service (recognized by lack of Response structure)
RemoteRPC.push(request)

Macros and functions

By invoking use Protein.Client, you include the following in your client module:

Mocking for tests

Client call mocking is enabled by default for Mix.env == :test. You can configure it explicitly via the mocking_enabled config flag as follows:

config :protein, mocking_enabled: true

You can add a mock module for your specific service to test/support. The module should be the mock_mod on sample above (which by default is a service_mod with the Mock suffix). For example, to mock the service sourced from create_user.proto on example above, you may implement the following module:

# test/support/my_project/remote_rpc/create_user_mock.ex

alias MyProject.RemoteRPC.CreateUser.{Request, Response}

defmodule MyProject.RemoteRPC.CreateUserMock do
  # with default response
  def call(request = %Request{) do
    :ok
  end

  # ...or with specific response
  def call(request = %Request{}) do
    {:ok, %Response{}}
  end

  # ...or with default error
  def call(request = %Request{}) do
    :error
  end

  # ...or with specific error code
  def call(request = %Request{}) do
    {:error, :something_happened}
  end

  # ...or with specific error message
  def call(request = %Request{}) do
    {:error, "Something went wrong"}
  end

  # ...or with error related to specific part of the request
  def call(request = %Request{}) do
    {:error, {:specific_arg_error, struct: "user", struct: "images", repeated: 0}}
  end

  # ...or with multiple errors (all above syntaxes are supported)
  def call(request = %Request{}) do
    {:error, [
      :something_happened,
      "Something went wrong",
      {:specific_arg_error, struct: "user", struct: "images", repeated: 0}
    ]}
  end
end

You can define multiple call clauses in your mock and use pattern matching to create different output based on varying input.

Mock bypasses the transport layer (obviously), but it still encodes/decodes your request protobuf just as regular client does and it still encodes/decodes the response from your mock. This ensures that your test structures are compilant with specific proto in use.

For non-responding services, mock modules are optional and will be executed only if defined. Otherwise, the client with mocking mode enabled will still encode the request, but then it will silently drop it without throwing an error.