Current section
12 Versions
Jump to
Current section
12 Versions
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…