Skip to content

Commit 2ccac95

Browse files
committed
add custom validation mechanism.
1 parent 5d4fa6f commit 2ccac95

File tree

4 files changed

+244
-57
lines changed

4 files changed

+244
-57
lines changed

lib/scenic/themes.ex

Lines changed: 123 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ defmodule Scenic.Themes do
44
By registering themes in this way you can safely pull in themes from external libraries,
55
without theme names colliding, as well as get all the validation.
66
7+
You can add additional keys to be validated on your custom themes by returning a tuple with your map of themes and a list of keys to be validated
8+
from your load function.
9+
10+
All themes will validate against the default schema. If you provide additional keys, the list will get merged with the list of default keys.
11+
712
### Required Configuration
813
Setting up themes requires some initial setup.
914
@@ -23,13 +28,15 @@ defmodule Scenic.Themes do
2328
text: @text
2429
}
2530
31+
schema [:surface] # add additional required keys to your theme
32+
2633
use Scenic.Themes,
2734
sources: [
2835
{:scenic, Scenic.Themes"},
2936
{:my_app, load()}
3037
]
3138
32-
def load(), do: @themes
39+
def load(), do: {@themes, @schema}
3340
end
3441
```
3542
@@ -42,67 +49,41 @@ defmodule Scenic.Themes do
4249
4350
Now themes are passed around scenic in the form of `{:library_name, :theme_name}` as opposed to just :theme_name.
4451
"""
45-
46-
@theme_light %{
47-
text: :black,
48-
background: :white,
49-
border: :dark_grey,
50-
active: {215, 215, 215},
51-
thumb: :cornflower_blue,
52-
focus: :blue,
53-
highlight: :saddle_brown
54-
}
55-
56-
@theme_dark %{
57-
text: :white,
58-
background: :black,
59-
border: :light_grey,
60-
active: {40, 40, 40},
61-
thumb: :cornflower_blue,
62-
focus: :cornflower_blue,
63-
highlight: :sandy_brown
64-
}
65-
66-
@primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}})
67-
@secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}})
68-
@success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}})
69-
@danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}})
70-
@warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}})
71-
@info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}})
72-
@text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear})
73-
@themes %{
74-
light: @theme_light,
75-
dark: @theme_dark,
76-
primary: @primary,
77-
secondary: @secondary,
78-
success: @success,
79-
danger: @danger,
80-
warning: @warning,
81-
info: @info,
82-
text: @text
83-
}
84-
85-
@callback load() :: list
52+
@callback load() :: {map, list} | map
53+
@optional_callbacks load: 0
8654

8755
defmacro __using__(using_opts \\ []) do
8856
quote do
8957
alias Scenic.Primitive.Style.Paint.Color
9058
@behaviour Scenic.Themes
9159
@sources Keyword.get(unquote(using_opts), :sources, [])
60+
@default_schema [:text, :background, :border, :active, :thumb, :focus]
61+
9262
@library_themes Enum.reduce(@sources, %{}, fn
9363
{lib, module}, acc when is_atom(module) ->
94-
themes = module.load()
95-
Map.put_new(acc, lib, themes)
64+
case module.load() do
65+
{themes, schema} ->
66+
Map.put_new(acc, lib, {themes, List.flatten([@default_schema | schema])})
67+
themes ->
68+
Map.put_new(acc, lib, {themes, @default_schema})
69+
end
70+
{lib, {themes, schema}}, acc ->
71+
Map.put_new(acc, lib, {themes, List.flatten([@default_schema | schema])})
9672
{lib, themes}, acc ->
97-
Map.put_new(acc, lib, themes)
73+
Map.put_new(acc, lib, {themes, @default_schema})
9874
_, acc -> acc
9975
end)
10076

10177
def validate(theme)
102-
def validate({lib, theme} = lib_theme) when is_atom(theme) do
78+
def validate({lib, theme_name} = lib_theme) when is_atom(theme_name) do
79+
{_, schema} = Map.get(@library_themes, lib)
10380
case normalize(lib_theme) do
104-
map when is_map(map) ->
105-
{:ok, lib_theme}
81+
theme ->
82+
# validate against the schema
83+
case validate(theme, schema) do
84+
{:ok, _} -> {:ok, lib_theme}
85+
error -> error
86+
end
10687
nil ->
10788
{
10889
:error,
@@ -118,6 +99,23 @@ defmodule Scenic.Themes do
11899
end
119100
end
120101

102+
def validate(
103+
theme,
104+
schema
105+
) do
106+
# we have the schema so we can validate against it.
107+
schema
108+
|> Enum.reduce({:ok, theme}, fn
109+
_, {:error, msg} ->
110+
{:error, msg}
111+
key, {:ok, _} = acc ->
112+
case Map.has_key?(theme, key) do
113+
true -> acc
114+
false -> err_key(key, theme)
115+
end
116+
end)
117+
end
118+
121119
def validate(
122120
%{
123121
text: _,
@@ -128,6 +126,8 @@ defmodule Scenic.Themes do
128126
focus: _
129127
} = theme
130128
) do
129+
# we dont have the schema so validate against the default,
130+
# this is not ideal, but should be fine for now.
131131
# we know all the required colors are there.
132132
# now make sure they are all valid colors, including any custom added ones.
133133
theme
@@ -153,6 +153,7 @@ defmodule Scenic.Themes do
153153
You passed in a map, but it didn't include all the required color specifications.
154154
It must contain a valid color for each of the following entries.
155155
:text, :background, :border, :active, :thumb, :focus
156+
If you're using a custom theme please check the documentation for that specific theme.
156157
#{IO.ANSI.default_color()}
157158
"""
158159
}
@@ -176,6 +177,37 @@ defmodule Scenic.Themes do
176177
}
177178
end
178179

180+
@doc false
181+
def normalize({lib, theme_name}) when is_atom(theme_name) do
182+
case Map.get(@library_themes, lib) do
183+
{themes, schema} -> Map.get(themes, theme_name)
184+
nil -> nil
185+
end
186+
end
187+
188+
def normalize(theme) when is_map(theme), do: theme
189+
190+
@doc false
191+
def preset({lib, theme_name}) do
192+
case Map.get(@library_themes, lib) do
193+
{themes, schema} -> Map.get(themes, theme_name)
194+
nil -> nil
195+
end
196+
end
197+
198+
defp err_key(key, map) do
199+
{
200+
:error,
201+
"""
202+
#{IO.ANSI.red()}Invalid theme specification
203+
Received: #{inspect(map)}
204+
#{IO.ANSI.yellow()}
205+
Map entry: #{inspect(key)}
206+
#{IO.ANSI.default_color()}
207+
"""
208+
}
209+
end
210+
179211
defp err_color(key, msg) do
180212
{
181213
:error,
@@ -186,17 +218,10 @@ defmodule Scenic.Themes do
186218
"""
187219
}
188220
end
189-
190-
@doc false
191-
def normalize({lib, theme}) when is_atom(theme), do: Map.get(Map.get(@library_themes, lib), theme)
192-
def normalize(theme) when is_map(theme), do: theme
193-
194-
@doc false
195-
def preset({lib, theme}), do: Map.get(Map.get(@library_themes, lib), theme)
196221
end
197222
end
198223

199-
@moduledoc false
224+
@doc false
200225
def module() do
201226
with {:ok, config} <- Application.fetch_env(:scenic, :themes),
202227
{:ok, module} <- Keyword.fetch(config, :module) do
@@ -226,12 +251,54 @@ defmodule Scenic.Themes do
226251
end
227252
end
228253

254+
@doc false
229255
def validate(theme), do: module().validate(theme)
230256

257+
@doc false
231258
def normalize(theme), do: module().normalize(theme)
232259

260+
@doc false
233261
def preset(theme), do: module().preset(theme)
234262

263+
@theme_light %{
264+
text: :black,
265+
background: :white,
266+
border: :dark_grey,
267+
active: {215, 215, 215},
268+
thumb: :cornflower_blue,
269+
focus: :blue,
270+
highlight: :saddle_brown
271+
}
272+
273+
@theme_dark %{
274+
text: :white,
275+
background: :black,
276+
border: :light_grey,
277+
active: {40, 40, 40},
278+
thumb: :cornflower_blue,
279+
focus: :cornflower_blue,
280+
highlight: :sandy_brown
281+
}
282+
283+
@primary Map.merge(@theme_dark, %{background: {72, 122, 252}, active: {58, 94, 201}})
284+
@secondary Map.merge(@theme_dark, %{background: {111, 117, 125}, active: {86, 90, 95}})
285+
@success Map.merge(@theme_dark, %{background: {99, 163, 74}, active: {74, 123, 56}})
286+
@danger Map.merge(@theme_dark, %{background: {191, 72, 71}, active: {164, 54, 51}})
287+
@warning Map.merge(@theme_light, %{background: {239, 196, 42}, active: {197, 160, 31}})
288+
@info Map.merge(@theme_dark, %{background: {94, 159, 183}, active: {70, 119, 138}})
289+
@text Map.merge(@theme_dark, %{text: {72, 122, 252}, background: :clear, active: :clear})
290+
@themes %{
291+
light: @theme_light,
292+
dark: @theme_dark,
293+
primary: @primary,
294+
secondary: @secondary,
295+
success: @success,
296+
danger: @danger,
297+
warning: @warning,
298+
info: @info,
299+
text: @text
300+
}
301+
235302
@doc false
236303
def load(), do: @themes
237304
end

test/scenic/themes_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,24 @@ defmodule Scenic.ThemesTest do
6363
assert Themes.normalize({:scenic, :dark}) == @theme_dark
6464
end
6565

66+
test "custom validate method accepts names themes" do
67+
assert Themes.validate({:custom_scenic, :custom_dark}) == {:ok, {:custom_scenic, :custom_dark}}
68+
assert Themes.validate({:custom_scenic, :custom_light}) == {:ok, {:custom_scenic, :custom_light}}
69+
assert Themes.validate({:custom_scenic, :custom_primary}) == {:ok, {:custom_scenic, :custom_primary}}
70+
assert Themes.validate({:custom_scenic, :custom_secondary}) == {:ok, {:custom_scenic, :custom_secondary}}
71+
assert Themes.validate({:custom_scenic, :custom_success}) == {:ok, {:custom_scenic, :custom_success}}
72+
assert Themes.validate({:custom_scenic, :custom_danger}) == {:ok, {:custom_scenic, :custom_danger}}
73+
assert Themes.validate({:custom_scenic, :custom_warning}) == {:ok, {:custom_scenic, :custom_warning}}
74+
assert Themes.validate({:custom_scenic, :custom_info}) == {:ok, {:custom_scenic, :custom_info}}
75+
assert Themes.validate({:custom_scenic, :custom_text}) == {:ok, {:custom_scenic, :custom_text}}
76+
end
77+
78+
test "custom validate method rejects map without custom standard color" do
79+
{:error, msg} = Themes.validate({:custom_scenic, :custom_invalid})
80+
assert msg =~ "Invalid theme specification"
81+
assert msg =~ "Map entry: :surface"
82+
end
83+
6684
test "validate accepts the named themes" do
6785
assert Themes.validate({:scenic, :dark}) == {:ok, {:scenic, :dark}}
6886
assert Themes.validate({:scenic, :light}) == {:ok, {:scenic, :light}}

test/support/custom_themes.ex

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
defmodule Scenic.Test.CustomThemes do
2+
@theme_light %{
3+
text: :black,
4+
background: :white,
5+
surface: :gainsboro,
6+
border: :dark_grey,
7+
active: {215, 215, 215},
8+
thumb: :cornflower_blue,
9+
focus: :blue,
10+
highlight: :saddle_brown
11+
}
12+
13+
@theme_dark %{
14+
text: :white,
15+
background: :black,
16+
surface: :gainsboro,
17+
border: :light_grey,
18+
active: {40, 40, 40},
19+
thumb: :cornflower_blue,
20+
focus: :cornflower_blue,
21+
highlight: :sandy_brown
22+
}
23+
24+
@theme_dark_invalid %{
25+
text: :white,
26+
background: :black,
27+
border: :light_grey,
28+
active: {40, 40, 40},
29+
thumb: :cornflower_blue,
30+
focus: :cornflower_blue
31+
}
32+
33+
@primary Map.merge(@theme_dark, %{surface: :gainsboro, background: {72, 122, 252}, active: {58, 94, 201}})
34+
@secondary Map.merge(@theme_dark, %{surface: :gainsboro, background: {111, 117, 125}, active: {86, 90, 95}})
35+
@success Map.merge(@theme_dark, %{surface: :gainsboro, background: {99, 163, 74}, active: {74, 123, 56}})
36+
@danger Map.merge(@theme_dark, %{surface: :gainsboro, background: {191, 72, 71}, active: {164, 54, 51}})
37+
@warning Map.merge(@theme_light, %{surface: :gainsboro, background: {239, 196, 42}, active: {197, 160, 31}})
38+
@info Map.merge(@theme_dark, %{surface: :gainsboro, background: {94, 159, 183}, active: {70, 119, 138}})
39+
@text Map.merge(@theme_dark, %{text: {72, 122, 252}, surface: :gainsboro, background: :clear, active: :clear})
40+
41+
@themes %{
42+
custom_light: @theme_light,
43+
custom_dark: @theme_dark,
44+
custom_primary: @primary,
45+
custom_secondary: @secondary,
46+
custom_success: @success,
47+
custom_danger: @danger,
48+
custom_warning: @warning,
49+
custom_info: @info,
50+
custom_text: @text,
51+
custom_invalid: @theme_dark_invalid
52+
}
53+
54+
@schema [:surface]
55+
56+
use Scenic.Themes,
57+
sources: [
58+
{:custom_scenic, {@themes, @schema}}
59+
]
60+
61+
def load(), do: {@themes, @schema}
62+
end

0 commit comments

Comments
 (0)