anthony-khong

anthony-khong

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!

Most Liked

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

Where Next?

Popular in Questions Top

myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a > b) do {:ok, "a"} end if (a < b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
jaysoifer
Is there a way to rollback a specific migration and only that one (“skipping” all the other ones)? Would mix ecto.rollback -v 200809061...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New

Other popular topics Top

9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30877 112
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
grych
Hi folks, Few months ago I have announced the proof-of-concept of the library to manipulate the browsers DOM objects directly from Elixi...
639 52341 488
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New

We're in Beta

About us Mission Statement