About

Library for creating and managing apis and their implementations.

Library features:

  1. Calling feature asynchronously on api
  2. Calling feature synchronously on implementation
  3. Checking api and implementations.
  4. Defining apis and their implementations.
  5. Defining default implementation.
  6. Defining fallback implementation.
  7. Grouping implementations.
  8. Listing all apis, their features, implementations, specs and more.
  9. Marking implementation features as supported, not supported and not implemented.
  10. Registering and unregistering apis and implementations.

Installation

Simply add :ex_api to your deps in mix.exs file and run command: mix deps.get.

defp deps do
  [
    # ...
      {:ex_api, "~> 1.0.0-rc.2"},
    # ...
  ]
end

Why someone should use ExApi rather than Behaviour or Protocol?

We should use ExApi when we want to get list of implementations dynamically and when we do not have a target data type.

Example use case

Imagine that you are implementing unique api for similar services. First service could use JSON based api, second could use XML based api and so on …

Comparing to Enum module you do not need to have any useful data that you need to pass and work on them. Of course you could use empty struct or struct with data that you do not want to modify and/or use, but … why?

As a developer you are going to make your code clean and short and here is solution for you. :-)

Usage

Call ExApi.Api.def_api/2 macro to define api:

import ExApi.Api

def_api MyApi do
  # docs, specs and features goes here ...
end

Call ExApi.Implementation.def_api_impl/5 macro to define api implementation:

import ExApi.Implementation

def_api_impl MyApi, :impl_id, ImplCustomModuleName do
  # set group, implement and/or mark as not supported `MyApi` features
end

Use functions in ExApi module to manage apis and implementations:

{:ok, server_pid} = ExApi.register_api(MyApi)
{:ok, ^server_pid} = ExApi.register_impl(ImplCustomModuleName)
{:ok, ^server_pid} = ExApi.set_default_impl(ImplCustomModuleName)
{:ok, api} = ExApi.get(MyApi)
{:ok, impl} = ExApi.get_impl(ImplCustomModuleName)
# and more ...

Api Features

Implementing feature

You can implement (or not) feature in 3 ways:

  1. :normal - just implement it, see: ExApi.Kernel.def_feature_impl/2
  2. :not_supported - mark feature as not supported (for example 3rd party api), see: ExApi.Kernel.def_no_support/2
  3. :not_implemented - just don’t implement feature and it will be automatically marked as not implemeted!

Calling feature

You can call feature in four ways:

  1. On each implementation, see: ExApi.get_impls/1 or YourApi.get_impls/0
  2. On default implementation, see: ExApi.get_default_impl/1 or YourApi.get_default_impl/0
  3. On specified implementation, see: ExApi.get_impl/2 or YourApi.get_impl/1
  4. On specified implementation module, see: YourImplementationModule api.

Preview features

You can see each feature status by calling ExApi.get_feature_support/2.

More information about each features could be find on related api pages.

Configuration

You can specify which apis and implementations by default will be loaded into state.

use Mix.Config

config :ex_api, apis: %{
  MyApi => [MyApi.FirstImpl, MyApi.SecondImpl, DifferentModuleName]},
},
default_groups: %{MyApi => :group_name},
default_impls: %{MyApi => {:group_name, :impl_id}}

Grouping features

Default group

If some functions you can pass {group, id} or simpler id. ExApi will try to automatically find implementation with default group and id. Default group affects also implementations that does not have specified group. Grouping implementations is useful when two teams are making library/project containing implementations for same api, so here group name will be :team_name. Then you can easily test work of two teams simply by providing group_name argument in test function call.

{:ok, impl} = ExApi.get_impl(MyApi, {:team_name, :impl_id})
# here you can benchmark each feature for this implementation or just check if it works as expected
{:ok, result} = impl.module.feature_name(arg1, arg2, arg3)
# assert ....

Defining implementation

You can set group when defining implementation:

def_api_impl MyApi, :my_impl do
  @impl_group :group_name

  # ...
end

Override group on registration

You can also override default or defined group, by calling ExApi.register_impl/2. This is helpful in case you are implementing 3rd-party services that are deployed in multiple domains. So you have not only same api, but also same implementation for 2 domains. Using this library you can register two copies of same implementation.

def_api_impl MyApiImpl do
  # ...
end
ExApi.register_impl(MyApiImpl, :local)
ExApi.register_impl(MyApiImpl, :example)
# ...
ExApi.get_impls(MyApi)[:local][:my_api_impl]#.feature_name(arg1, ...)
ExApi.get_impls(MyApi)[:example][:my_api_impl]#.feature_name(arg1, ...)
# ...
config :my_app, hosts: %{example: {"example.com", 80}, local: {"localhost", 4000}}

From here you should see that you can easily store informations about implementations and then dynamically register same api implementation for multiple sites specified in for example user input.

Conventions

There is only one convention. ExApi follows Elixir-way for return values. ExApi will help you define specs for features, for example this code:

import ExApi.Api

def_api MyApi do
  @spec feature_name() :: String.t
  def_feature feature_name()
end

will be readed as same as:

import ExApi.Api

def_api MyApi do
  @spec feature_name() :: {:ok, String.t}
  def_feature feature_name()
end

Return specs are really easily parsed. First case is moved into {:ok, result} tuple and every other case is moved into {:error, result} tuple.

@spec feature_name() :: String.t | String.t | String.t
# is readed as:
@spec feature_name() :: {:ok, String.t} | {:error, String.t} | {:error, String.t}

Of course you could expect more than one {:ok, result} tuples. In this case you should change your spec like:

@spec feature_name() :: {:ok, your_first_result | your_second_result}
# or
@spec feature_name() :: {:ok, your_first_result} | {:ok, your_second_result}

This convention works also when you are implementing feature, so you returning {:ok, tuple} is optional. Every implementation result that is not a {:error, error} or {:ok, result} will be changed from term to {:ok, term}.

def_api MyApi do
  def_feature feature_name(string)

  def_impl :a_impl do
    def_feature_impl feature_name(string) when is_bitstring(string) do
      {:error, "Oh no! Why you always pass strings?"}
    end

    def_feature_impl feature_name(term) do
      "Input: " <> inspect(term)
    end
  end
end

MyApi.AImpl.feature_name(123) # -> {:ok, "Input: 123"}
MyApi.AImpl.feature_name("123") # -> {:error, "Oh no! Why you always pass strings?"}

This convention is important, because we expected that API is stable and never raises any exceptions, so you only need to check return value.

{:ok, my_api} = ExApi.get(MyApi)
{:ok, impl} = ExApi.get_default_impl(my_api)
case impl.module.feature_name() do
  {:error, error} -> IO.inspect error # not implemented, not supported or your custom error here ...
  {:ok, result} -> continue_your_work_if_feature_is_supported(result)
end

From now your API will never be limited by any of its implementations!

Contributing

Feel free to share ideas. Describe a situation when your idea could be helpful. Examples and links to resources are also welcome.