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

gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
hpopp
To simplify some tasks at work, I wrote and published this package yesterday. It’s a simple macro that enables Access behaviour on struct...
New
Mooodi
Given a string, how can I get access to its character by index? Enum.at("my_string", 2) doesn't work. Or rather, not char, but a substr...
New

Other popular topics Top

pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
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
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement