Published my first Hex library Injecto. Looking for feedback and comments!

I just published my first Hex library Injecto (link to repo), which roughly means Into JSON schema and Ecto. Declaring:

defmodule Post do
  @properties %{
    title: {:string, required: true},
    description: {:string, []},
    likes: {:integer, required: true, minimum: 0}
  }
  use Injecto
end

defines Post as an Ecto schema, and has an accessible JSON schema along with the options:

%ExJsonSchema.Schema.Root{
  schema: %{
    "properties" => %{
      "description" => %{
        "anyOf" => [%{"type" => "string"}, %{"type" => "null"}]
      },
      "likes" => %{"minimum" => 0, "type" => "integer"},
      "title" => %{"type" => "string"}
    },
    "required" => ["likes", "title"],
    "title" => "Elixir.Post",
    "type" => "object",
    "x-struct" => "Elixir.Post"
  },
  refs: %{},
  definitions: %{},
  location: :root,
  version: 7,
  custom_format_validator: nil
}

For a bit of background, I was looking for a tool to achieve a couple of things:

  1. validate data coming in from external sources and data going out; and
  2. validate requests and responses using JSON schema and expose the specs using Swagger.

For point 1, I found Ecto changesets and Elixir structs to be really nice to work with, but I couldn’t find anything to automatically translate Ecto schemas into JSON schemas (CMIIW). As for point 2, ex_json_schema works well, but defining JSON schemas by hand is quite clunky, and I didn’t find a way to do automatic struct definition.

I set out to write my own solution for the two options above - that seems to be the recommendation out of this discussion. However, I found that the code I wrote was quite verbose, and packing it into a use macro seems to cut down a lot of the boilerplate code.

I’m still very new with Elixir. Any comments or feedback or suggestions to do things a better way would be very much appreciated!

5 Likes

This is very neat and certainly useful! I’m working on a project that does a lot of mapping between my Elixir code and API resources, and so far I’ve been rather lax/unstructured about it, but I’ve been thinking of doing something like this.

The first thing that comes to mind – have you considered using Ecto.Schema’s reflection API instead of a custom data language, so that users can define their schemas using Ecto’s own DSL?

Here’s how it might theoretically look:

defmodule Post do
  use Ecto.Schema
  use Injecto

  embedded_schema do
    field :description, :string

    @injecto title: [required: true]
    field :title, :string

    @injecto likes: [required: true, minimum: 0]
    field :likes, :integer

    belongs_to :user, User
  end
end

defmodule User do
  use Ecto.Schema
  use Injecto

  embedded_schema do
    @injecto display_name: [required: true]
    field :display_name, :string, source: :displayName
  end
end

A few “tricks” that would make something like the above possible:

  • The Injecto injected function definitions could expect to find data in :persistent_term storage (or similar).

  • Module.register_attribute(__MODULE__, :injecto, accumulate: true, persist: true) could make the @injecto declarations available at runtime through module.__info__(:attributes) (docs).

  • An @after_compile hook could use the Ecto __schema__(...) reflection API along with the persisted @injecto attribute to pre-compute whatever state Injecto needs to do its stuff and save the result in :persistent_term, or error/warn/etc. if something is wrong (e.g. two @injecto title: [...] declarations are found).

I can think of a number of benefits to this approach, but the biggest would be that it would be really easy to adopt. No learning a new thing – if you’re okay with everything being optional, you could stick use Injecto in an existing schema and you’re all set.

If this is a direction you’re interested in, I’d be happy to help where necessary!

Regarding the current API, I also have a couple suggestions:

  • (Perhaps optionally) @properties as a keyword option to use Injecto:
defmodule Post do
  use Injecto,
    properties: [
      title: {:string, required: true},
      ...
    ]
end
  • Remove the requirement that @properties be defined before use Injecto by using a @before_compile callback to inject your code. Combined with the above suggestion, the rough pattern would be something like:
defmacro __using__(opts) do
  if props = Keyword.get(opts, :properties) do
    Module.put_attribute(__CALLER__.module, :properties, props)
  end

  quote do
    @before_compile Injecto
  end
end

def __before_compile__(env) do
  props = Module.get_attribute(env.module, :properties, nil)

  unless props do
    # raise or warn that properties weren't set
  end

  quote do
    # injected schema / functions
  end
end
  • Would be great to provide some way to map between JSON keys and Ecto keys, e.g. snake_case to camelCase. This would probably mean a custom Jason.Encoder definition. In the example I gave with the theoretical API using Ecto’s schema DSL, I thought of using the schema field :source to map to the API key, but it might make sense to separate it in case you’re persisting these and don’t want your database field changed.
# using :source
field :display_name, :string, source: :displayName

# using custom attribute
@injecto display_name: [key: :displayName]
field :display_name, :string
1 Like