Current section
12 Versions
Jump to
Current section
12 Versions
Compare versions
8
files changed
+366
additions
-275
deletions
| @@ -3,8 +3,8 @@ | |
| 3 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], |
| 4 4 | import_deps: [:ecto], |
| 5 5 | locals_without_parens: [ |
| 6 | - # Partial |
| 7 | - defpartial: 2, |
| 6 | + # Type |
| 7 | + deftype: 2, |
| 8 8 | # Schema |
| 9 9 | field!: 1, |
| 10 10 | field!: 2, |
| @@ -23,6 +23,7 @@ Declarative [`Ecto`](https://github.com/elixir-ecto/ecto) `embedded_schema`s for | |
| 23 23 | * `__schema__(:post_transforms` - `Keyword` mapping of fields to post-transformations (currently only `:map` option) |
| 24 24 | * Convenient generated function (`changeset`,`new`,`new!`,...) ([Generated Functions](#generated-functions)) |
| 25 25 | * Configurable `Application`-wide defaults for `Ecto.Schema` API ([Config](#config)) |
| 26 | + * Conveniently create new `Ecto` types using the `Flint.Type` module and its `deftype/2` macro ([`Flint.Type`](#flinttype)) |
| 26 27 | |
| 27 28 | ## Installation |
| 28 29 | |
| @@ -101,6 +102,45 @@ Since a call to `Flint`'s `embedded_schema` or `use Flint, schema: []` just cre | |
| 101 102 | |
| 102 103 | Union type for Ecto. Allows the field to be any of the specified types. |
| 103 104 | |
| 105 | + ## `Flint.Type` |
| 106 | + |
| 107 | + `Flint.Type` is meant to make writing new `Ecto` types require much less boilerplate, because you can base your |
| 108 | + type off of an existing type and only modify the callbacks that have different behavior. |
| 109 | + |
| 110 | + Simply `use Flint.Type` and pass the `:extends` option which says which type module to inherit callbacks |
| 111 | + from. This will delegate all required callbacks and any implemented optional callbacks and make them |
| 112 | + overridable. |
| 113 | + |
| 114 | + It also lets you make a type from an `Ecto.ParameterizedType` with default parameter values. |
| 115 | + You may supply any number of default parameters. This essentially provides a new |
| 116 | + `init/1` implementation for the type, supplying the default values, while not affecting any of the |
| 117 | + other `Ecto.ParameterizedType` callbacks. You may still override the newly set defaults at the local level. |
| 118 | + |
| 119 | + Just supply all options that you wish to be defaults as extra options when using `Flint.Type`. |
| 120 | + |
| 121 | + You may override any of the inherited callbacks inherity from the extended module |
| 122 | + in the case that you wish to customize the module further. |
| 123 | + |
| 124 | + ### Examples |
| 125 | + |
| 126 | + ``` elixir |
| 127 | + defmodule Category do |
| 128 | + use Flint.Type, extends: Ecto.Enum, values: [:folder, :file] |
| 129 | + end |
| 130 | + ``` |
| 131 | + |
| 132 | + This will apply default `values` to `Ecto.Enum` when you supply a `Category` type |
| 133 | + to an Ecto schema. You may still override the values if you supply the `:values` |
| 134 | + option for the field. |
| 135 | + |
| 136 | + ```elixir |
| 137 | + import Flint.Type |
| 138 | + deftype NewUID, extends: Ecto.UUID, dump: &String.length/1 |
| 139 | + ``` |
| 140 | + |
| 141 | + This will create a new `NewUID` type that behaves exactly like an `Ecto.UUID` except it dumps |
| 142 | + its string length. |
| 143 | + |
| 104 144 | ## Generated Functions |
| 105 145 | |
| 106 146 | `Flint` provides default implementations for the following functions for any schema declaration. Each of these is overridable. |
| @@ -473,7 +513,7 @@ Flint.Schema.dump(book) | |
| 473 513 | |
| 474 514 | In this example, you can see how you can share multiple representations of the same data using this distinction. |
| 475 515 | |
| 476 | - You can also implement your own `Ecto.Type` and further customize this: |
| 516 | + You can also implement your own `Ecto.Type` and further customize this (see [`Flint.Type`](#flinttype)): |
| 477 517 | |
| 478 518 | ```elixir |
| 479 519 | defmodule ContentType do |
| @@ -1,6 +1,6 @@ | |
| 1 1 | {<<"links">>,[{<<"GitHub">>,<<"https://github.com/acalejos/flint">>}]}. |
| 2 2 | {<<"name">>,<<"flint">>}. |
| 3 | - {<<"version">>,<<"0.2.1">>}. |
| 3 | + {<<"version">>,<<"0.3.0">>}. |
| 4 4 | {<<"description">>, |
| 5 5 | <<"Declarative Ecto embedded schemas for data validation, coercion, and manipulation.">>}. |
| 6 6 | {<<"elixir">>,<<"~> 1.14">>}. |
| @@ -18,8 +18,8 @@ | |
| 18 18 | {<<"requirement">>,<<"~> 1.4">>}, |
| 19 19 | {<<"repository">>,<<"hexpm">>}]]}. |
| 20 20 | {<<"files">>, |
| 21 | - [<<"lib">>,<<"lib/schema.ex">>,<<"lib/flint.ex">>,<<"lib/flint">>, |
| 22 | - <<"lib/flint/types">>,<<"lib/flint/types/union.ex">>, |
| 23 | - <<"lib/flint/partial.ex">>,<<"lib/pipeline.ex">>,<<".formatter.exs">>, |
| 24 | - <<"mix.exs">>,<<"README.md">>,<<"LICENSE.md">>]}. |
| 21 | + [<<"lib">>,<<"lib/type.ex">>,<<"lib/schema.ex">>,<<"lib/flint.ex">>, |
| 22 | + <<"lib/flint">>,<<"lib/flint/types">>,<<"lib/flint/types/union.ex">>, |
| 23 | + <<"lib/pipeline.ex">>,<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>, |
| 24 | + <<"LICENSE.md">>]}. |
| 25 25 | {<<"build_tools">>,[<<"mix">>]}. |
| @@ -2,91 +2,4 @@ defmodule Flint do | |
| 2 2 | @moduledoc """ |
| 3 3 | #{File.cwd!() |> Path.join("README.md") |> File.read!() |> then(&Regex.run(~r/.*<!-- BEGIN MODULEDOC -->(?P<body>.*)<!-- END MODULEDOC -->.*/s, &1, capture: :all_but_first)) |> hd()} |
| 4 4 | """ |
| 5 | - |
| 6 | - defmacro __using__(opts) do |
| 7 | - {schema, opts} = Keyword.pop(opts, :schema) |
| 8 | - |
| 9 | - opts = |
| 10 | - Keyword.validate!( |
| 11 | - opts, |
| 12 | - primary_key: false, |
| 13 | - schema_prefix: nil, |
| 14 | - schema_context: nil, |
| 15 | - timestamp_opts: [type: :naive_datetime] |
| 16 | - ) |
| 17 | - |
| 18 | - Module.register_attribute(__CALLER__.module, :required, accumulate: true) |
| 19 | - Module.register_attribute(__CALLER__.module, :validations, accumulate: true) |
| 20 | - Module.register_attribute(__CALLER__.module, :pre_transforms, accumulate: true) |
| 21 | - Module.register_attribute(__CALLER__.module, :post_transforms, accumulate: true) |
| 22 | - |
| 23 | - prelude = |
| 24 | - quote do |
| 25 | - alias Flint.Types.Union |
| 26 | - import Flint.Partial |
| 27 | - |
| 28 | - @after_compile Flint.Schema |
| 29 | - |
| 30 | - @behaviour Access |
| 31 | - |
| 32 | - @impl true |
| 33 | - defdelegate fetch(term, key), to: Map |
| 34 | - @impl true |
| 35 | - defdelegate get_and_update(term, key, fun), to: Map |
| 36 | - @impl true |
| 37 | - defdelegate pop(data, key), to: Map |
| 38 | - |
| 39 | - def __schema__(:required), do: @required |> Enum.reverse() |
| 40 | - def __schema__(:validations), do: @validations |> Enum.reverse() |
| 41 | - def __schema__(:pre_transforms), do: @pre_transforms |> Enum.reverse() |
| 42 | - def __schema__(:post_transforms), do: @post_transforms |> Enum.reverse() |
| 43 | - |
| 44 | - defdelegate changeset(schema, params \\ %{}, bindings \\ []), to: Flint.Pipeline |
| 45 | - def new(params \\ %{}, bindings \\ []), do: Flint.Schema.new(__MODULE__, params, bindings) |
| 46 | - |
| 47 | - def new!(params \\ %{}, bindings \\ []), |
| 48 | - do: Flint.Schema.new!(__MODULE__, params, bindings) |
| 49 | - |
| 50 | - defoverridable new: 0, |
| 51 | - new: 1, |
| 52 | - new: 2, |
| 53 | - new!: 0, |
| 54 | - new!: 1, |
| 55 | - new!: 2, |
| 56 | - changeset: 1, |
| 57 | - changeset: 2, |
| 58 | - changeset: 3 |
| 59 | - |
| 60 | - if Code.ensure_loaded?(Jason) do |
| 61 | - defimpl Jason.Encoder do |
| 62 | - def encode(value, opts) do |
| 63 | - value |> Ecto.embedded_dump(:json) |> Jason.Encode.map(opts) |
| 64 | - end |
| 65 | - end |
| 66 | - end |
| 67 | - |
| 68 | - use Ecto.Schema |
| 69 | - import Ecto.Schema, except: [embedded_schema: 1] |
| 70 | - import Flint.Schema, only: [embedded_schema: 1] |
| 71 | - |
| 72 | - @schema_prefix unquote(opts[:schema_prefix]) |
| 73 | - @schema_context unquote(opts[:schema_context]) |
| 74 | - @timestamp_opts unquote(opts[:timestamp_opts]) |
| 75 | - @primary_key unquote(opts[:primary_key]) |
| 76 | - end |
| 77 | - |
| 78 | - if schema do |
| 79 | - quote do |
| 80 | - unquote(prelude) |
| 81 | - |
| 82 | - embedded_schema do |
| 83 | - unquote(schema) |
| 84 | - end |
| 85 | - end |
| 86 | - else |
| 87 | - quote do |
| 88 | - unquote(prelude) |
| 89 | - end |
| 90 | - end |
| 91 | - end |
| 92 5 | end |
| @@ -1,147 +0,0 @@ | |
| 1 | - defmodule Flint.Partial do |
| 2 | - @moduledoc """ |
| 3 | - `Flint.Partial` is meant to make writing new `Ecto` types require much less boilerplate, because you can base your |
| 4 | - type off of an existing type and only modify the callbacks that have different behavior. |
| 5 | - |
| 6 | - Simply `use Flint.Partial` and pass the `:extends` option which says which type module to inherit callbacks |
| 7 | - from. This will delegate all required callbacks and any implemented optional callbacks and make them |
| 8 | - overridable. |
| 9 | - |
| 10 | - It also lets you make a type from an `Ecto.ParameterizedType` with default parameter values. |
| 11 | - You may supply any number of default parameters. This essentially provides a new |
| 12 | - `init/1` implementation for the type, supplying the default values, while not affecting any of the |
| 13 | - other `Ecto.ParameterizedType` callbacks. You may still override the newly set defaults at the local level. |
| 14 | - |
| 15 | - Just supply all options that you wish to be defaults as extra options when using `Flint.Partial`. |
| 16 | - |
| 17 | - You may override any of the inherited callbacks inherity from the extended module |
| 18 | - in the case that you wish to customize the module further. |
| 19 | - |
| 20 | - ## Examples |
| 21 | - |
| 22 | - ``` elixir |
| 23 | - defmodule Category do |
| 24 | - use Flint.Partial, extends: Ecto.Enum, values: [:folder, :file] |
| 25 | - end |
| 26 | - ``` |
| 27 | - |
| 28 | - This will apply default `values` to `Ecto.Enum` when you supply a `Category` type |
| 29 | - to an Ecto schema. You may still override the values if you supply the `:values` |
| 30 | - option for the field. |
| 31 | - """ |
| 32 | - require Logger |
| 33 | - |
| 34 | - defmacro __using__(opts) do |
| 35 | - {extends, opts} = Keyword.pop!(opts, :extends) |
| 36 | - extends = Macro.expand_literals(extends, __CALLER__) |
| 37 | - |
| 38 | - unless Code.ensure_loaded?(extends) do |
| 39 | - raise ArgumentError, "Cannot load module #{inspect(extends)}!" |
| 40 | - end |
| 41 | - |
| 42 | - type = |
| 43 | - cond do |
| 44 | - implements_behaviour?(extends, Ecto.Type) -> |
| 45 | - Ecto.Type |
| 46 | - |
| 47 | - implements_behaviour?(extends, Ecto.ParameterizedType) -> |
| 48 | - Ecto.ParameterizedType |
| 49 | - |
| 50 | - true -> |
| 51 | - raise ArgumentError, |
| 52 | - "You must extend from either a `Ecto.Type` or `Ecto.ParameterizedType`!" |
| 53 | - end |
| 54 | - |
| 55 | - if type == Ecto.Type && opts != [] do |
| 56 | - Logger.warning(""" |
| 57 | - You are passing options #{inspect(opts)} to `Flint.Partial` |
| 58 | - when extending a module of type `Ecto.Type`. `Ecto.Type` does not accept paremters, so these options will be ignored. |
| 59 | - """) |
| 60 | - end |
| 61 | - |
| 62 | - callbacks = |
| 63 | - type |
| 64 | - |> required_callbacks() |
| 65 | - |> Keyword.drop([:init]) |
| 66 | - |> Enum.map(fn {name, arity} -> |
| 67 | - args = Macro.generate_arguments(arity, nil) |
| 68 | - |
| 69 | - quote do |
| 70 | - defdelegate unquote(name)(unquote_splicing(args)), to: unquote(extends) |
| 71 | - defoverridable [{unquote(name), unquote(arity)}] |
| 72 | - end |
| 73 | - end) |
| 74 | - |
| 75 | - callbacks = |
| 76 | - callbacks ++ |
| 77 | - Enum.map(apply(type, :behaviour_info, [:optional_callbacks]), fn {name, arity} -> |
| 78 | - if function_exported?(extends, name, arity) do |
| 79 | - args = Macro.generate_arguments(arity, nil) |
| 80 | - |
| 81 | - quote do |
| 82 | - defdelegate unquote(name)(unquote_splicing(args)), to: unquote(extends) |
| 83 | - defoverridable [{unquote(name), unquote(arity)}] |
| 84 | - end |
| 85 | - end |
| 86 | - end) |
| 87 | - |
| 88 | - using = quote(do: use(unquote(type))) |
| 89 | - |
| 90 | - init = |
| 91 | - if type == Ecto.ParameterizedType do |
| 92 | - quote do |
| 93 | - def init(opts) do |
| 94 | - opts = opts ++ unquote(opts) |
| 95 | - unquote(extends).init(opts) |
| 96 | - end |
| 97 | - end |
| 98 | - end |
| 99 | - |
| 100 | - quoted = |
| 101 | - ([ |
| 102 | - using, |
| 103 | - init |
| 104 | - ] ++ |
| 105 | - callbacks) |
| 106 | - |> Enum.filter(& &1) |
| 107 | - |
| 108 | - quote do |
| 109 | - (unquote_splicing(quoted)) |
| 110 | - end |
| 111 | - end |
| 112 | - |
| 113 | - @doc """ |
| 114 | - A shorthand for creating a new module to represent a partial `Ecto.ParameterizedType`. This is equivalent to |
| 115 | - creating a new module with name `module` and calling `use Flint.Partial` and passing `opts`. |
| 116 | - |
| 117 | - Practially speaking, this is only really useful for defining default values for an `Ecto.ParameterizedType` with |
| 118 | - no changes to any of the other callbacks. |
| 119 | - |
| 120 | - ## Example |
| 121 | - |
| 122 | - ```elixir |
| 123 | - defpartial Vehicle, extends: Ecto.Enum, values: [:car, :motorcycle, :truck] |
| 124 | - ``` |
| 125 | - """ |
| 126 | - def defpartial(module, opts) do |
| 127 | - quoted = |
| 128 | - quote do |
| 129 | - require Flint.Partial |
| 130 | - Flint.Partial.__using__(unquote(opts)) |
| 131 | - end |
| 132 | - |
| 133 | - Module.create(module, quoted, Macro.Env.location(__ENV__)) |
| 134 | - end |
| 135 | - |
| 136 | - defp required_callbacks(module) do |
| 137 | - all_callbacks = apply(module, :behaviour_info, [:callbacks]) |
| 138 | - optional_callbacks = apply(module, :behaviour_info, [:optional_callbacks]) |
| 139 | - Keyword.drop(all_callbacks, Keyword.keys(optional_callbacks)) |
| 140 | - end |
| 141 | - |
| 142 | - defp implements_behaviour?(module, behaviour) do |
| 143 | - Enum.all?(required_callbacks(behaviour), fn {k, v} -> |
| 144 | - function_exported?(module, k, v) |
| 145 | - end) |
| 146 | - end |
| 147 | - end |
Loading more files…