DataSchema (data_schema v0.2.6) View Source
DataSchemas are declarative specifications of how to create structs from some kind of data source. For example you can define a schema that knows how to turn an elixir map into a struct, casting all of the values as it goes. Alternatively you can set up a schema to ingest XML data and create structs from the values inside the XML.
Below is an example of a simple schema:
defmodule Blog do
import DataSchema, only: [data_schema: 1]
data_schema([
field: {:name, "name", &{:ok, to_string(&1)}}
])
end
This says we will create a struct with a :name
key and will get the value for that key
from under the "name"
key in the source data. That value will be passed to to_string/1
and the result of that function will end up as the value under :name
in the resulting
struct.
In general this is the format for a field:
field {:content, "text", &cast_string/1}
# ^ ^ ^ ^
# field type | | |
# struct key name | |
# path to data in the source |
# casting function
Depending on your input data type the path pointing to a value in it may need to be
interpreted differently. For our example of a map input type, the "path" is really just
a key on that input map. But there is still flexibility in how we use that key to access
the value; we could use Map.get/2
or Map.fetch/2
for example. Additionally, for
different input data types what the path looks like and what it means for how you access
data can be different. Let's say your input data type was XML your path could be ".//MyNode",
ie could be an xpath. In which case what you do with that xpath is going to be different
from what you would do with a map key.
DataSchema allows for different schemas to handle different input types AND allows for the same input type to be handled differently in different schemas.
Finally when creating the struct we can choose to stop as soon as we find an error or to simply put whatever is returned from a casting function into the struct we are making. The latter approach encourages people to raise exceptions from their casting functions to halt the creation of the struct.
Field Types
There are 5 kinds of struct fields we can have:
field
- The value will be a casted value from the source data.list_of
- The value will be a list of casted values created from the source data.has_one
- The value will be created from a nested data schema (so will be a struct)has_many
- The value will be created by casting a list of values into a data schema. (You end up with a list of structs defined by the provided schema). Similar to has_many in ectoaggregate
- The value will a casted value formed from multiple bits of data in the source.
Examples
See the guides for more in depth examples but below you can see how we create a schema that will take a map of data and create a struct out of it. Given the following schema:
defmodule Sandwich do
require DataSchema
DataSchema.data_schema([
field: {:type, "the_type", &{:ok, String.upcase(&1)}},
list_of: {:fillings, "the_fillings", &({:ok, String.downcase(&1["name"])})}
])
end
input_data = %{
"the_type" => "fake steak",
"the_fillings" => [
%{"name" => "fake stake", "good?" => true},
%{"name" => "SAUCE"},
%{"name" => "sweetcorn"},
]
}
DataSchema.to_struct(input_data, Sandwich)
# outputs the following:
%Sandwich{
type: "FAKE STEAK",
fillings: ["fake stake", "sauce", "sweetcorn"],
}
Link to this section Summary
Functions
A macro that creates a data schema. By default all struct fields are required but you can specify that a field be optional by passing the correct option in. See the Options section below for more.
Accepts an data schema module and some source data and attempts to create the struct defined in the schema from the source data recursively.
Creates a struct or map from the provided arguments. This function can be used to define
runtime schemas for the most dynamic of cases. This means you don't have to define a schema
at compile time using the DataShema.data_schema/1
macro.
Link to this section Functions
A macro that creates a data schema. By default all struct fields are required but you can specify that a field be optional by passing the correct option in. See the Options section below for more.
Field Types
There are 5 kinds of struct fields we can have:
field
- The value will be a casted value from the source data.list_of
- The value will be a list of casted values created from the source data.has_one
- The value will be created from a nested data schema (so will be a struct)has_many
- The value will be created by casting a list of values into a data schema. (You end up with a list of structs defined by the provided schema). Similar to has_many in ectoaggregate
- The value will a casted value formed from multiple bits of data in the source.
Options
Available options are:
:optional?
- specifies whether or not the field in the struct should be included in the@enforce_keys
for the struct. By default all fields are required but you can mark them as optional by setting this totrue
. This will also be checked when creating a struct withDataSchema.to_struct/2
returning an error if the required field is null.
For example:
defmodule Sandwich do
require DataSchema
DataSchema.data_schema([
field: {:type, "the_type", &{:ok, String.upcase(&1)}, optional?: true},
])
end
Examples
See the guides for more in depth examples but below you can see how we create a schema that will take a map of data and create a struct out of it. Given the following schema:
defmodule Sandwich do
require DataSchema
DataSchema.data_schema([
field: {:type, "the_type", &{:ok, String.upcase.(&1)}},
list_of: {:fillings, "the_fillings", &({:ok, String.downcase(&1["name"])})}
])
end
input_data = %{
"the_type" => "fake steak",
"the_fillings" => [
%{"name" => "fake stake", "good?" => true},
%{"name" => "SAUCE"},
%{"name" => "sweetcorn"},
]
}
DataSchema.to_struct(input_data, Sandwich)
# outputs the following:
%Sandwich{
type: "FAKE STEAK",
fillings: ["fake stake", "sauce", "sweetcorn"],
}
Accepts an data schema module and some source data and attempts to create the struct defined in the schema from the source data recursively.
We essentially visit each field in the schema and extract the data the field points to from the sauce data, passing it to the field's casting function before setting the result of that as the value on the struct.
This function takes a simple approach to creating the struct - whatever you return from a casting function will be set as the value of the struct field. You should raise if you want casting to fail.
Examples
data = %{ "spice" => "enables space travel" }
defmodule Foo do
require DataSchema
DataSchema.data_schema(
field: {:a_rocket, "spice", &({:ok, &1})}
)
end
DataSchema.to_struct(data, Foo)
# => Outputs the following:
%Foo{a_rocket: "enables space travel"}
Creates a struct or map from the provided arguments. This function can be used to define
runtime schemas for the most dynamic of cases. This means you don't have to define a schema
at compile time using the DataShema.data_schema/1
macro.
Examples
Creating a struct:
defmodule Run do
defstruct [:time]
end
input = %{"time" => "10:00"}
fields = [
field: {:time, "time", &{:ok, to_string(&1)}}
]
DataSchema.to_struct(input, Run, fields, DataSchema.MapAccessor)
{:ok, %Run{time: "10:00"}}
Creating a map:
input = %{"time" => "10:00"}
fields = [
field: {:time, "time", &{:ok, to_string(&1)}}
]
DataSchema.to_struct(input, %{}, fields, DataSchema.MapAccessor)
{:ok, %{time: "10:00"}}