anthony-khong

anthony-khong

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!

Showing Posts 1 to 1

zachallaun

zachallaun

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
— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews