Current section
Files
Jump to
Current section
Files
lib/rolodex/schema.ex
defmodule Rolodex.Schema do
@moduledoc """
Exposes functions and macros for defining reusable parameter schemas.
It includes two macros. Used together, you can setup a reusable schema:
- `schema/3` - for declaring a schema
- `field/3` - for declaring schema fields
It also exposes the following functions:
- `is_schema_module?/1` - determines if the provided item is a module that has
defined a reuseable schema
- `to_map/1` - serializes a schema module into a map for use by a `Rolodex.Processor`
behaviour
- `get_refs/1` - traverses a schema and searches for any nested schemas within
"""
alias Rolodex.{ContentUtils, Field}
defmacro __using__(_opts) do
quote do
import Rolodex.Schema
end
end
@doc """
Opens up the schema definition for the current module. Will name the schema
and generate metadata for the schema based on subsequent calls to `field/3`
**Accepts**
- `name` - the schema name
- `opts` - a keyword list of options (currently, only looks for a `desc` key)
- `block` - the inner schema definition with one or more calls to `field/3`
## Example
defmodule MySchema do
use Rolodex.Schema
schema "MySchema", desc: "Example schema" do
# Atomic field with no description
field :id, :uuid
# Atomic field with a description
field :name, :string, desc: "The object's name"
# A field that refers to another, nested object
field :other, OtherSchema
# A field that is an array of items of one-or-more types
field :multi, :list, of: [:string, OtherSchema]
# A field that is one of the possible provided types
field :any, :one_of, of: [:string, OtherSchema]
end
end
"""
defmacro schema(name, opts \\ [], do: block) do
quote do
Module.register_attribute(__MODULE__, :fields, accumulate: true)
unquote(block)
def __schema__(:name), do: unquote(name)
def __schema__(:desc), do: unquote(Keyword.get(opts, :desc, nil))
def __schema__(:fields) do
Map.new(@fields, fn {id, opts} -> {id, Field.new(opts)} end)
end
end
end
@doc """
Adds a new field to the schema. Will generate a method `__field__/1` where the
one argument is the field `identifier`. This can be used to fetch the field
metadata later.
See `Rolodex.Field` for more information about valid field metadata.
Accepts
- `identifier` - field name
- `type` - either an atom or another Rolodex.Schema module
- `opts` - a keyword list of options, looks for `desc` and `of` (for array types)
## Example
defmodule MySchema do
use Rolodex.Schema
schema "MySchema", desc: "Example schema" do
# Atomic field with no description
field :id, :uuid
# Atomic field with a description
field :name, :string, desc: "The object's name"
# A field that refers to another, nested object
field :other, OtherSchema
# A field that is an array of items of one-or-more types
field :multi, :list, of: [:string, OtherSchema]
# You can use a shorthand to define a list field, the below is identical
# to the above
field :multi, [:string, OtherSchema]
# A field that is one of the possible provided types
field :any, :one_of, of: [:string, OtherSchema]
end
end
"""
defmacro field(identifier, type, opts \\ []) do
ContentUtils.set_field(:fields, identifier, type, opts)
end
@doc """
Determines if an arbitrary item is a module that has defined a reusable schema
via `Rolodex.Schema` macros
## Example
iex> defmodule SimpleSchema do
...> use Rolodex.Schema
...> schema "SimpleSchema", desc: "Demo schema" do
...> field :id, :uuid
...> end
...> end
iex>
iex> # Validating a schema module
iex> Rolodex.Schema.is_schema_module?(SimpleSchema)
true
iex> # Validating some other module
iex> Rolodex.Schema.is_schema_module?(OtherModule)
false
"""
@spec is_schema_module?(any()) :: boolean()
def is_schema_module?(item)
def is_schema_module?(module) when is_atom(module) do
try do
module.__info__(:functions)
|> Keyword.has_key?(:__schema__)
rescue
# Any error means that `module` isn't a module and so we can just say `false`
_ -> false
end
end
def is_schema_module?(_), do: false
@doc """
Serializes the `Rolodex.Schema` metadata into a formatted map.
## Example
iex> defmodule OtherSchema do
...> use Rolodex.Schema
...>
...> schema "OtherSchema" do
...> field :id, :uuid
...> end
...> end
iex>
iex> defmodule MySchema do
...> use Rolodex.Schema
...>
...> schema "MySchema", desc: "An example" do
...> # Atomic field with no description
...> field :id, :uuid
...>
...> # Atomic field with a description
...> field :name, :string, desc: "The schema's name"
...>
...> # A field that refers to another, nested object
...> field :other, OtherSchema
...>
...> # A field that is an array of items of one-or-more types
...> field :multi, :list, of: [:string, OtherSchema]
...>
...> # A field that is one of the possible provided types
...> field :any, :one_of, of: [:string, OtherSchema]
...> end
...> end
iex>
iex> Rolodex.Schema.to_map(MySchema)
%{
type: :object,
desc: "An example",
properties: %{
id: %{type: :uuid},
name: %{desc: "The schema's name", type: :string},
other: %{type: :ref, ref: Rolodex.SchemaTest.OtherSchema},
multi: %{
type: :list,
of: [
%{type: :string},
%{type: :ref, ref: Rolodex.SchemaTest.OtherSchema}
]
},
any: %{
type: :one_of,
of: [
%{type: :string},
%{type: :ref, ref: Rolodex.SchemaTest.OtherSchema}
]
}
}
}
"""
@spec to_map(module()) :: map()
def to_map(schema) do
desc = schema.__schema__(:desc)
fields = schema.__schema__(:fields)
Field.new(type: :object, properties: fields, desc: desc)
end
@doc """
Traverses a serialized Schema and collects any nested references to other
Schemas within. See `Rolodex.Field.get_refs/1` for more info.
"""
@spec get_refs(module()) :: [module()]
def get_refs(schema) do
schema
|> to_map()
|> Field.get_refs()
end
end