Packages

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

Current section

12 Versions

Jump to

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…