Packages

Declarative Ecto embedded schemas for data validation, coercion, and manipulation.

Current section

12 Versions

Jump to

Compare versions

6 files changed
+163 additions
-6 deletions
  @@ -1,6 +1,6 @@
1 1 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/acalejos/flint">>}]}.
2 2 {<<"name">>,<<"flint">>}.
3 - {<<"version">>,<<"0.4.2">>}.
3 + {<<"version">>,<<"0.4.3">>}.
4 4 {<<"description">>,
5 5 <<"Declarative Ecto embedded schemas for data validation, coercion, and manipulation.">>}.
6 6 {<<"elixir">>,<<"~> 1.14">>}.
  @@ -32,7 +32,8 @@
32 32 <<"lib/flint.ex">>,<<"lib/flint">>,<<"lib/flint/types">>,
33 33 <<"lib/flint/types/union.ex">>,<<"lib/flint/extension">>,
34 34 <<"lib/flint/extension/field.ex">>,<<"lib/flint/extension/dsl.ex">>,
35 - <<"lib/flint/extensions">>,<<"lib/flint/extensions/pre_transforms.ex">>,
35 + <<"lib/flint/extension/entity.ex">>,<<"lib/flint/extensions">>,
36 + <<"lib/flint/extensions/pre_transforms.ex">>,
36 37 <<"lib/flint/extensions/post_transforms.ex">>,
37 38 <<"lib/flint/extensions/ecto_validations.ex">>,
38 39 <<"lib/flint/extensions/when.ex">>,<<"lib/flint/extensions/json.ex">>,
  @@ -7,7 +7,8 @@ defmodule Flint.Extension do
7 7
8 8 1. Schema-level attributes
9 9 2. Field-level additional options
10 - 3. Injected Code
10 + 3. Default `field` and `field!` definitions
11 + 4. Injected Code
11 12
12 13 Extension authors define what fields / options / attributes `Flint` should look for in the module / schema definition and
13 14 strip out and store in a schema reflection function, but it is still the resposibility of either the extension author or
  @@ -98,6 +99,53 @@ defmodule Flint.Extension do
98 99 ]
99 100 ```
100 101
102 + ## Default `field` and `field!` definitions
103 +
104 + Sometimes you might want to always have one of more fields defined for a given schema type without having
105 + to write it each time. Much in the same way that, by default, the `@prmimary_key` attribute will set an `:id` field
106 + for the schema, you might wish to template out some fields and values.
107 +
108 + If you want to define fields that are defined by default in a schema that uses your extension, just use the
109 + `field` and `field!` macros. These accept the same arguments as their counterparts in `Flint.Schema`, and will be
110 + added at the end of the `embedded_schema` definition.
111 +
112 + > #### Duplicate Fields {: .info}
113 + >
114 + > Note that if you define a template for a field in your extension then anyone who uses your extension will be
115 + > unable to override that field name with their own definition.
116 +
117 + ### Example
118 +
119 + Given the following extension:
120 +
121 + ```elixir
122 + defmodule Event do
123 + use Flint.Extension
124 +
125 + field! :timestamp, :utc_datetime_usec
126 + field! :id, :binary_id
127 + end
128 + ```
129 +
130 + and this schema:
131 +
132 + ```elixir
133 + defmodule Webhook do
134 + use Flint.Schema, extensions: [Event, Embedded]
135 +
136 + embedded_schema do
137 + field :route, :string
138 + end
139 + end
140 + ```
141 +
142 + Then any schema that uses this extension will have these fields by default:
143 +
144 + ```elixir
145 + Webhook.__schema__(:fields)
146 + # [:route, :timestamp, :id]
147 + ```
148 +
101 149 ## Injected Code
102 150
103 151 Lastly, extensions allow you to define custom `__using__/1` macros which will be passed through
  @@ -124,8 +172,36 @@ defmodule Flint.Extension do
124 172 many_extension_kinds: [:extensions],
125 173 default_extensions: [extensions: Flint.Extension.Dsl]
126 174
175 + @doc """
176 + Registers a default field to be added to any `embedded_schema` using this extension
177 + """
178 + defmacro field(name, type \\ :string, opts \\ []) do
179 + quote do
180 + entity do
181 + name(unquote(name))
182 + type(unquote(type))
183 + opts(unquote(opts))
184 + end
185 + end
186 + end
187 +
188 + @doc """
189 + Same as `field` but marks the field as required
190 + """
191 + defmacro field!(name, type \\ :string, opts \\ []) do
192 + quote do
193 + entity do
194 + name(unquote(name))
195 + type(unquote(type))
196 + opts(unquote(opts))
197 + required(true)
198 + end
199 + end
200 + end
201 +
127 202 defmacro __using__(opts) do
128 203 quote do
204 + import Flint.Extension
129 205 unquote_splicing(super(opts))
130 206
131 207 defmacro __using__(opts) do
  @@ -142,6 +218,9 @@ defmodule Flint.Extension do
142 218 @doc false
143 219 def attribute_names(),
144 220 do: Spark.Dsl.Extension.get_entities(__MODULE__, :attributes) |> Enum.map(& &1.name)
221 +
222 + def field_names(),
223 + do: Spark.Dsl.Extension.get_entities(__MODULE__, :fields) |> Enum.map(& &1.name)
145 224 end
146 225 end
147 226 end
  @@ -41,10 +41,43 @@ defmodule Flint.Extension.Dsl do
41 41 args: [:name],
42 42 schema: @common_schema
43 43 }
44 + @field %Spark.Dsl.Entity{
45 + name: :entity,
46 + target: Flint.Extension.Entity,
47 + schema: [
48 + name: [
49 + type: :atom,
50 + required: true,
51 + doc: "The name as an atom."
52 + ],
53 + type: [
54 + required: false,
55 + default: :string,
56 + # Let the call to `field` inside the schema handle validating
57 + type: :any
58 + ],
59 + opts: [
60 + type: :keyword_list,
61 + default: [],
62 + required: false
63 + ],
64 + required: [
65 + type: :boolean,
66 + default: false,
67 + doc:
68 + "Whether the field is required. If it is required and not provided there will be a compile time error thrown."
69 + ]
70 + ]
71 + }
44 72 @attributes %Spark.Dsl.Section{
45 73 name: :attributes,
46 74 entities: [@attribute],
47 75 top_level?: true
48 76 }
49 - use Spark.Dsl.Extension, sections: [@attributes, @options]
77 + @fields %Spark.Dsl.Section{
78 + name: :fields,
79 + entities: [@field],
80 + top_level?: true
81 + }
82 + use Spark.Dsl.Extension, sections: [@attributes, @options, @fields]
50 83 end
  @@ -0,0 +1,9 @@
1 + defmodule Flint.Extension.Entity do
2 + @moduledoc false
3 + defstruct [
4 + :name,
5 + type: :string,
6 + required: false,
7 + opts: []
8 + ]
9 + end
  @@ -438,7 +438,7 @@ defmodule Flint.Schema do
438 438 def __embeds_module__(env, module, opts, block) do
439 439 {pk, opts} = Keyword.pop(opts, :primary_key, false)
440 440
441 - extensions = Module.get_attribute(env.module, :extensions, [])
441 + extensions = Module.get_attribute(env.module, :extensions, [])
442 442
443 443 block =
444 444 quote do
  @@ -458,6 +458,22 @@ defmodule Flint.Schema do
458 458 Wraps `Ecto`'s `embedded_schema` macro, injecting `Flint`'s custom macro implementation into the module space.
459 459 """
460 460 defmacro embedded_schema(do: block) do
461 + # TODO: Do we want to allow an `overridable` field?
462 + field_calls =
463 + for {_extension, field} <- Module.get_attribute(__CALLER__.module, :extension_fields) do
464 + case field do
465 + %{name: name, opts: opts, type: type, required: true} ->
466 + quote do
467 + field! unquote(name), unquote(type), unquote_splicing(opts)
468 + end
469 +
470 + %{name: name, opts: opts, type: type} ->
471 + quote do
472 + field unquote(name), unquote(type), unquote_splicing(opts)
473 + end
474 + end
475 + end
476 +
461 477 quote do
462 478 Ecto.Schema.embedded_schema do
463 479 import Ecto.Schema,
  @@ -477,6 +493,7 @@ defmodule Flint.Schema do
477 493 @after_compile Flint.Schema
478 494
479 495 unquote(block)
496 + unquote_splicing(field_calls)
480 497 end
481 498 end
482 499 end
  @@ -579,6 +596,23 @@ defmodule Flint.Schema do
579 596 end
580 597 end)
581 598
599 + fields =
600 + extensions
601 + |> Enum.flat_map(fn extension ->
602 + extension =
603 + case extension do
604 + {ext, _opts} when is_atom(ext) ->
605 + ext
606 +
607 + ext when is_atom(ext) ->
608 + ext
609 + end
610 +
611 + for attr <- Spark.Dsl.Extension.get_entities(extension, :fields) do
612 + {extension, attr}
613 + end
614 + end)
615 +
582 616 Module.register_attribute(__CALLER__.module, :required, accumulate: true)
583 617 Module.register_attribute(__CALLER__.module, :blocks, accumulate: true)
584 618 # Extension-Related Attributes
  @@ -586,6 +620,7 @@ defmodule Flint.Schema do
586 620 Module.register_attribute(__CALLER__.module, :extension_options, accumulate: true)
587 621 Module.register_attribute(__CALLER__.module, :extra_options, accumulate: true)
588 622 Module.put_attribute(__CALLER__.module, :extensions, extensions)
623 + Module.put_attribute(__CALLER__.module, :extension_fields, fields)
589 624
590 625 attrs =
591 626 Enum.map(attributes, fn {_extension, field} = attr ->
Loading more files…