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:
- validate data coming in from external sources and data going out; and
- 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
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_termstorage (or similar). -
Module.register_attribute(__MODULE__, :injecto, accumulate: true, persist: true)could make the@injectodeclarations available at runtime throughmodule.__info__(:attributes)(docs). -
An
@after_compilehook could use the Ecto__schema__(...)reflection API along with the persisted@injectoattribute 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)
@propertiesas a keyword option touse Injecto:
defmodule Post do
use Injecto,
properties: [
title: {:string, required: true},
...
]
end
- Remove the requirement that
@propertiesbe defined beforeuse Injectoby using a@before_compilecallback 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_casetocamelCase. This would probably mean a customJason.Encoderdefinition. In the example I gave with the theoretical API using Ecto’s schema DSL, I thought of using the schema field:sourceto 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
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








