Skip to content

Commit 94a95c5

Browse files
committed
feat(matchers): exclude bundled matchers by default
commit 3188526 Author: Niklas Lindgren <niklas@musicglue.com> Date: Tue Jun 11 15:24:24 2024 +0300 docs(readme): adds new section on including Solid in your project This documents the altered approach to including Solid a project, the changes to importing the default matchers and how to bring your own matchers for types with bundled matchers. commit ddcb0fd Author: Niklas Lindgren <niklas@musicglue.com> Date: Tue Jun 11 15:24:05 2024 +0300 test(test helpers): add `use Solid` to test helpers commit 67c60c7 Author: Niklas Lindgren <niklas@musicglue.com> Date: Tue Jun 11 15:21:59 2024 +0300 feat: export a `use Solid` macro for default config usage This is a convenience method, that makes including Solid in your project using the default config easier. This will be the simplest and likeliest upgrade path for most Solid users. commit 2dcd980 Author: Niklas Lindgren <niklas@musicglue.com> Date: Tue Jun 11 15:20:28 2024 +0300 test(matchers): add cases for built-in matchers commit ac54b6b Author: Niklas Lindgren <niklas@musicglue.com> Date: Tue Jun 11 14:57:33 2024 +0300 feat!(matchers): exclude bundled matchers by default Packages the bundled matchers in a new module `Solid.Matcher.Builtins`, adding a `__using__` macro for including them in your own project. This is a breaking change, but the only way for supporting custom implementations in place of the bundled matchers. commit 0a86872 Author: Matt Sutkowski <msutkowski@gmail.com> Date: Mon Apr 29 21:14:23 2024 -0700 chore: update docs around render/3 and README
1 parent 8ad3fa3 commit 94a95c5

5 files changed

Lines changed: 286 additions & 82 deletions

File tree

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,69 @@ def deps do
4747
end
4848
```
4949

50+
## Including Solid in your project
51+
52+
Solid comes with bundled matchers for the basic Elixir data types. To enable you to use your own
53+
implementations instead, the bundled matchers aren't included by default. This is necessary,
54+
because matchers implement a protocol, which means they can only be defined once for each type.
55+
56+
If you're happy to use the bundled matchers (the defaults), all you need to to do is to include a
57+
call to `use Solid` or `use Solid.Matcher.Builtins` in your application code. Which one you choose
58+
is up to you and effectually makes no difference. If you, on the other hand, want to replace some
59+
of the defaults in Solid, there is some nuance.
60+
61+
By default, calling `use Solid` includes the bundled matchers and creates local methods for
62+
`render/3`, `render!/3`, `parse/2` and `parse!/2` in your wrapper module. To select which
63+
delegates are created, use the `:delegates` argument. The empty list or `false` omits all
64+
delegates.
65+
66+
If you want to wrap Solid &ndash; it can be a convenient way to wrap calls to public methods for
67+
e.g. always including specific options &ndash; and _also_ want to bring your own matchers, pass
68+
the `:nomatchers` argument with any value.
69+
70+
```elixir
71+
defmodule MyProject.Solid do
72+
# `use Solid` by default creates delegates to `render/3`, `render!/3`, `parse/2` and
73+
# `parse!/2` in your wrapper module. That means you can call `MyProject.Solid.render/3` etc,
74+
# instead of using Solid methods directly. Using wrapper methods in your module can be a
75+
# convenient way to e.g. including custom options. Always calling the public methods on
76+
# your wrapper module makes it convenient to change from delegate to wrapped method later on.
77+
#
78+
# To pick which local methods are created, use the `delegates` argument.
79+
# To not import the bundled matchers, use the `nomatchers` argument.
80+
81+
# wrap Solid, using all defaults
82+
use Solid
83+
84+
# wrap Solid, but exclude the bundled matchers
85+
use Solid, nomatchers: true
86+
87+
# wrap Solid, but exclude the delegated render methods
88+
use Solid, delegates: [:parse, :parse!]
89+
end
90+
```
91+
92+
The `use Solid.Matcher.Builtins` macro has options for cherry-picking the bundled matchers,
93+
refer to the module documentation for more details. Call `use Solid.Matcher.Builtins` in your
94+
module alongside your custom matchers and you're good to go.
95+
96+
```elixir
97+
defmodule MyProject.Solid.Matchers do
98+
# `use Solid.Matcher.Builtins` includes the bundled matchers for basic Elixir data types.
99+
# If you want to bring your own custom implementation, use the `except` and `only`
100+
# arguments for cherry-picking and add your own implementations to this module.
101+
102+
# use all the bundled matchers
103+
use Solid.Matcher.Builtins
104+
105+
# exclude the matcher for the Map type
106+
use Solid.Matcher.Builtins, except: [:map]
107+
108+
# only include the matchers for the Any and Atom types
109+
use Solid.Matcher.Builtins, only: [:any, :atom]
110+
end
111+
```
112+
50113
## Custom tags
51114

52115
To implement a new tag you need to create a new module that implements the `Tag` behaviour:

lib/solid.ex

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,56 @@
11
defmodule Solid do
2+
@doc """
3+
It sets up Solid with built-in matchers for basic types and creates local delegates to `render`,
4+
`render!`, `parse`, and `parse!` in your wrapper module.
5+
6+
Use this macro to include Solid in your project with a default configuration.
7+
8+
If you're not using the default configuration and wish to customise one or several of the basic
9+
matchers, refer to the documentation in the `Solid.Matcher.Builtins` module.
10+
11+
Use the `:delegates` option to include only a subset of the delegates in your wrapper module. To
12+
exclude all delegates, pass `false` or the empty list.
13+
14+
Use the `:nomatchers` option to exclude the bundled matchers.
15+
"""
16+
@all_delegates [:render, :render!, :parse, :parse!]
17+
@type delegates :: :render | :render! | :parse | :parse!
18+
@type option :: {:delegates, list(delegates()) | boolean()} | {:nomatchers, any()}
19+
@type options :: list(option())
20+
@spec __using__(options()) :: Macro.t()
21+
defmacro __using__(options) do
22+
delegates =
23+
case Keyword.get(options, :delegates, @all_delegates) do
24+
true -> @all_delegates
25+
[_ | _] = v -> v
26+
_ -> []
27+
end
28+
29+
matchers = not Keyword.has_key?(options, :nomatchers)
30+
31+
quote do
32+
if unquote(matchers) do
33+
use Solid.Matcher.Builtins
34+
end
35+
36+
if :render in unquote(delegates) do
37+
defdelegate render, to: Solid
38+
end
39+
40+
if :render! in unquote(delegates) do
41+
defdelegate render!, to: Solid
42+
end
43+
44+
if :parse in unquote(delegates) do
45+
defdelegate parse, to: Solid
46+
end
47+
48+
if :parse! in unquote(delegates) do
49+
defdelegate parse!, to: Solid
50+
end
51+
end
52+
end
53+
254
@moduledoc """
355
Main module to interact with Solid
456
"""

lib/solid/matcher.ex

Lines changed: 106 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,78 +4,123 @@ defprotocol Solid.Matcher do
44
def match(_, _)
55
end
66

7-
defimpl Solid.Matcher, for: Any do
8-
def match(data, []), do: {:ok, data}
9-
10-
def match(_, _), do: {:error, :not_found}
11-
end
7+
defmodule Solid.Matcher.Builtins do
8+
@doc """
9+
Solid comes with built-in matchers for the Atom, Map, List, and String/BitString types, as well
10+
as a fallback matcher for the Any type.
1211
13-
defimpl Solid.Matcher, for: List do
14-
def match(data, []), do: {:ok, data}
12+
The using macro supports options to selectively include (`:only`) and exclude (`:except`)
13+
individual matchers, should you wish to replace all or a subset with a custom matcher.
1514
16-
def match(data, ["size"]) do
17-
{:ok, Enum.count(data)}
18-
end
15+
The full list of available matchers is `:any`, `:atom`, `:list`, `:map`, `:string`
1916
20-
def match(data, [key | keys]) when is_integer(key) do
21-
case Enum.fetch(data, key) do
22-
{:ok, value} -> @protocol.match(value, keys)
23-
_ -> {:error, :not_found}
24-
end
25-
end
26-
end
17+
Examples:
2718
28-
defimpl Solid.Matcher, for: Map do
29-
def match(data, []) do
30-
{:ok, data}
31-
end
19+
# include all built-in matchers
20+
use Solid.Matcher.Builtins
3221
33-
def match(data, ["size"]) do
34-
{:ok, Map.get(data, "size", Enum.count(data))}
35-
end
22+
# selectively include only a subset
23+
use Solid.Matcher.Builtins, only: [:any, :list]
3624
37-
def match(data, [key | []]) do
38-
case Map.fetch(data, key) do
39-
{:ok, value} -> {:ok, value}
40-
_ -> {:error, :not_found}
41-
end
42-
end
25+
# selectively exclude a subset
26+
use Solid.Matcher.Builtins, except: [:map, :atom]
27+
"""
4328

44-
def match(data, [key | keys]) do
45-
case Map.fetch(data, key) do
46-
{:ok, value} -> @protocol.match(value, keys)
47-
_ -> {:error, :not_found}
29+
@all_matchers [:any, :atom, :string, :list, :map]
30+
31+
@type matcher :: :any | :atom | :list | :map | :string
32+
@type option :: {:only, list(matcher())} | {:except, list(matcher())}
33+
@type options :: list(option())
34+
@spec __using__(options()) :: Macro.t()
35+
defmacro __using__(opts) do
36+
excluded = Keyword.get(opts, :except, [])
37+
38+
included =
39+
opts
40+
|> Keyword.get(:only, @all_matchers)
41+
|> Enum.reject(fn m -> m in excluded end)
42+
43+
quote do
44+
if :list in unquote(included) do
45+
defimpl Solid.Matcher, for: List do
46+
def match(data, []), do: {:ok, data}
47+
48+
def match(data, ["size" | tail]), do: data |> Enum.count() |> @protocol.match(tail)
49+
50+
def match(data, [key | keys]) when is_integer(key) do
51+
case Enum.fetch(data, key) do
52+
{:ok, value} -> @protocol.match(value, keys)
53+
_ -> {:error, :not_found}
54+
end
55+
end
56+
end
57+
end
58+
59+
if :map in unquote(included) do
60+
defimpl Solid.Matcher, for: Map do
61+
def match(data, []) do
62+
{:ok, data}
63+
end
64+
65+
def match(data, ["size" | tail]),
66+
do: data |> Map.get("size", Enum.count(data)) |> @protocol.match(tail)
67+
68+
def match(data, [head | []]) do
69+
case Map.fetch(data, head) do
70+
{:ok, value} -> {:ok, value}
71+
_ -> {:error, :not_found}
72+
end
73+
end
74+
75+
def match(data, [head | tail]) do
76+
case Map.fetch(data, head) do
77+
{:ok, value} -> @protocol.match(value, tail)
78+
_ -> {:error, :not_found}
79+
end
80+
end
81+
end
82+
end
83+
84+
if :string in unquote(included) do
85+
defimpl Solid.Matcher, for: [BitString, String] do
86+
def match(current, []), do: {:ok, current}
87+
88+
def match(data, ["size" | tail]), do: data |> String.length() |> @protocol.match(tail)
89+
90+
def match(_data, [i | _]) when is_integer(i) do
91+
{:error, :not_found}
92+
end
93+
94+
def match(_data, [i | _]) when is_binary(i) do
95+
{:error, :not_found}
96+
end
97+
end
98+
end
99+
100+
if :atom in unquote(included) do
101+
defimpl Solid.Matcher, for: Atom do
102+
def match(current, []) when is_nil(current), do: {:ok, nil}
103+
def match(data, []), do: {:ok, data}
104+
def match(nil, _), do: {:error, :not_found}
105+
106+
@doc """
107+
Matches all remaining cases
108+
"""
109+
def match(_current, [key]) when is_binary(key), do: {:error, :not_found}
110+
end
111+
end
112+
113+
if :any in unquote(included) do
114+
defimpl Solid.Matcher, for: Any do
115+
def match(data, []), do: {:ok, data}
116+
117+
def match(d, s), do: {:error, :not_found}
118+
end
119+
end
48120
end
49121
end
50122
end
51123

52-
defimpl Solid.Matcher, for: BitString do
53-
def match(current, []), do: {:ok, current}
54-
55-
def match(data, ["size"]) do
56-
{:ok, String.length(data)}
57-
end
58-
59-
def match(_data, [i | _]) when is_integer(i) do
60-
{:error, :not_found}
61-
end
62-
63-
def match(_data, [i | _]) when is_binary(i) do
64-
{:error, :not_found}
65-
end
66-
end
67-
68-
defimpl Solid.Matcher, for: Atom do
69-
def match(current, []) when is_nil(current), do: {:ok, nil}
70-
def match(data, []), do: {:ok, data}
71-
def match(nil, _), do: {:error, :not_found}
72-
73-
@doc """
74-
Matches all remaining cases
75-
"""
76-
def match(_current, [key]) when is_binary(key), do: {:error, :not_found}
77-
end
78-
79124
defimpl Solid.Matcher, for: Tuple do
80125
def match(data, []), do: {:ok, data}
81126

0 commit comments

Comments
 (0)