Skip to content

Commit 8c136ca

Browse files
committed
Conditionally compile Visualizer and Standalone on optional deps
1 parent 5bba420 commit 8c136ca

2 files changed

Lines changed: 143 additions & 144 deletions

File tree

lib/color/palette/visualizer.ex

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,67 @@
1-
defmodule Color.Palette.Visualizer do
2-
@moduledoc """
3-
A web-based visualizer for the palettes produced by
4-
`Color.Palette`.
1+
# `Color.Palette.Visualizer` is a `Plug.Router`. It is only
2+
# defined when both `:plug` and `:plug_router` are available
3+
# (i.e., when the consumer has added `{:plug, "~> 1.15"}` to
4+
# their deps). When `:plug` isn't installed, the module simply
5+
# doesn't exist — calling any function on it raises the
6+
# standard `UndefinedFunctionError: module not available`,
7+
# which is the conventional Elixir signal for "you're missing
8+
# a dependency."
9+
if Code.ensure_loaded?(Plug.Router) do
10+
defmodule Color.Palette.Visualizer do
11+
@moduledoc """
12+
A web-based visualizer for the palettes produced by
13+
`Color.Palette`.
514
6-
This module is a `Plug.Router` that can be mounted inside a
7-
Phoenix or Plug application, or run standalone during
8-
development via `Color.Palette.Visualizer.Standalone`.
15+
This module is a `Plug.Router` that can be mounted inside a
16+
Phoenix or Plug application, or run standalone during
17+
development via `Color.Palette.Visualizer.Standalone`.
918
10-
## Three views
19+
## Three views
1120
12-
* `/tonal` — [UI Colors](https://uicolors.app/) style. One seed
13-
becomes a row of swatches with hex, OKLCH, and contrast
14-
values, plus exportable CSS custom properties and Tailwind
15-
config.
21+
* `/tonal` — [UI Colors](https://uicolors.app/) style. One seed
22+
becomes a row of swatches with hex, OKLCH, and contrast
23+
values, plus exportable CSS custom properties and Tailwind
24+
config.
1625
17-
* `/theme` — [Material Theme Builder](https://material-foundation.github.io/material-theme-builder/)
18-
style. Five tonal scales and a grid of Material Design 3
19-
role tokens (primary / on-primary / surface / outline / …)
20-
for light and dark schemes.
26+
* `/theme` — [Material Theme Builder](https://material-foundation.github.io/material-theme-builder/)
27+
style. Five tonal scales and a grid of Material Design 3
28+
role tokens (primary / on-primary / surface / outline / …)
29+
for light and dark schemes.
2130
22-
* `/contrast` — [Adobe Leonardo](https://leonardocolor.io/)
23-
style. Contrast-targeted swatches against a chosen background
24-
and a pass/fail matrix for common text sizes.
31+
* `/contrast` — [Adobe Leonardo](https://leonardocolor.io/)
32+
style. Contrast-targeted swatches against a chosen background
33+
and a pass/fail matrix for common text sizes.
2534
26-
All state lives in the URL — copy a URL and you've shared the
27-
palette.
35+
All state lives in the URL — copy a URL and you've shared the
36+
palette.
2837
29-
## Mounting in Phoenix
38+
## Mounting in Phoenix
3039
31-
In your `router.ex`:
40+
In your `router.ex`:
3241
33-
forward "/palette", Color.Palette.Visualizer
42+
forward "/palette", Color.Palette.Visualizer
3443
35-
## Running standalone
44+
## Running standalone
3645
37-
Color.Palette.Visualizer.Standalone.start(port: 4001)
46+
Color.Palette.Visualizer.Standalone.start(port: 4001)
3847
39-
## Optional dependencies
48+
## Optional dependencies
4049
41-
The visualizer pulls in `:plug` (required for the router) and
42-
`:bandit` (only used by the standalone helper). Both are
43-
declared `optional: true` in this library's `mix.exs`, so you
44-
must add them to your own project's deps to use the visualizer:
50+
The visualizer pulls in `:plug` (required for the router) and
51+
`:bandit` (only used by the standalone helper). Both are
52+
declared `optional: true` in this library's `mix.exs`, so you
53+
must add them to your own project's deps to use the visualizer:
4554
46-
{:plug, "~> 1.15"},
47-
{:bandit, "~> 1.5"}
55+
{:plug, "~> 1.15"},
56+
{:bandit, "~> 1.5"}
4857
49-
The core palette algorithms have no such dependency and will
50-
compile without either of these in place.
58+
The core palette algorithms have no such dependency and will
59+
compile without either of these in place. This module itself
60+
is only compiled when `:plug` is present — without it,
61+
`Color.Palette.Visualizer` doesn't exist as a module.
5162
52-
"""
63+
"""
5364

54-
# Deferred compile-time check: raise at use time, not at
55-
# compile time of this library, so users who never touch the
56-
# visualizer don't need plug installed.
57-
if Code.ensure_loaded?(Plug.Router) do
5865
use Plug.Router
5966

6067
plug(Plug.Logger, log: :debug)
@@ -341,14 +348,5 @@ defmodule Color.Palette.Visualizer do
341348
end
342349

343350
defp atom_default(_, _, default), do: default
344-
else
345-
@compile_error "Color.Palette.Visualizer requires :plug. " <>
346-
"Add `{:plug, \"~> 1.15\"}` to your project's deps."
347-
348-
@doc false
349-
def init(_), do: raise(@compile_error)
350-
351-
@doc false
352-
def call(_, _), do: raise(@compile_error)
353351
end
354352
end
Lines changed: 96 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,113 @@
1-
defmodule Color.Palette.Visualizer.Standalone do
2-
@moduledoc """
3-
A tiny helper that runs `Color.Palette.Visualizer` as a
4-
standalone web server for local development.
5-
6-
Requires `:bandit` in your project's deps.
7-
8-
Color.Palette.Visualizer.Standalone.start(port: 4001)
9-
# Visit http://localhost:4001
10-
11-
To stop the server, call
12-
`Color.Palette.Visualizer.Standalone.stop/1` with the PID
13-
returned from `start/1`.
14-
15-
"""
16-
17-
@doc """
18-
Starts the visualizer on the given port.
19-
20-
### Options
21-
22-
* `:port` — TCP port to listen on. Default `4001`.
23-
24-
* `:ip` — IP address to bind to. Default `:loopback` (only
25-
accessible from localhost). Pass `:any` to bind on all
26-
interfaces.
27-
28-
### Returns
29-
30-
* `{:ok, pid}` on success.
31-
32-
* `{:error, reason}` on failure — most commonly a port-in-use
33-
error.
34-
35-
"""
36-
@spec start(keyword()) :: {:ok, pid()} | {:error, term()}
37-
def start(options \\ []) do
38-
ensure_bandit!()
39-
40-
port = Keyword.get(options, :port, 4001)
41-
ip = Keyword.get(options, :ip, :loopback)
42-
43-
bandit_options = [
44-
plug: Color.Palette.Visualizer,
45-
port: port,
46-
ip: ip_tuple(ip)
47-
]
48-
49-
Bandit.start_link(bandit_options)
50-
end
51-
52-
@doc """
53-
Returns a child spec suitable for embedding under a
54-
supervision tree.
1+
# `Color.Palette.Visualizer.Standalone` runs the visualizer as
2+
# a self-contained Bandit web server. It is only defined when
3+
# `:bandit` is available (which transitively pulls in `:plug`),
4+
# so consumer apps that don't need a standalone dev server
5+
# don't get compiler warnings about missing modules. When
6+
# `:bandit` isn't installed, this module simply doesn't exist
7+
# and any call to it raises the standard
8+
# `UndefinedFunctionError`.
9+
if Code.ensure_loaded?(Bandit) do
10+
defmodule Color.Palette.Visualizer.Standalone do
11+
@moduledoc """
12+
A tiny helper that runs `Color.Palette.Visualizer` as a
13+
standalone web server for local development.
14+
15+
Requires `:bandit` in your project's deps. This module is
16+
only compiled when `:bandit` is present — without it,
17+
`Color.Palette.Visualizer.Standalone` doesn't exist as a
18+
module and calling its functions raises
19+
`UndefinedFunctionError`.
20+
21+
Color.Palette.Visualizer.Standalone.start(port: 4001)
22+
# Visit http://localhost:4001
23+
24+
To stop the server, call
25+
`Color.Palette.Visualizer.Standalone.stop/1` with the PID
26+
returned from `start/1`.
27+
28+
"""
29+
30+
@doc """
31+
Starts the visualizer on the given port.
32+
33+
### Options
34+
35+
* `:port` — TCP port to listen on. Default `4001`.
36+
37+
* `:ip` — IP address to bind to. Default `:loopback` (only
38+
accessible from localhost). Pass `:any` to bind on all
39+
interfaces.
40+
41+
### Returns
42+
43+
* `{:ok, pid}` on success.
44+
45+
* `{:error, reason}` on failure — most commonly a port-in-use
46+
error.
47+
48+
"""
49+
@spec start(keyword()) :: {:ok, pid()} | {:error, term()}
50+
def start(options \\ []) do
51+
port = Keyword.get(options, :port, 4001)
52+
ip = Keyword.get(options, :ip, :loopback)
53+
54+
bandit_options = [
55+
plug: Color.Palette.Visualizer,
56+
port: port,
57+
ip: ip_tuple(ip)
58+
]
59+
60+
Bandit.start_link(bandit_options)
61+
end
5562

56-
### Options
63+
@doc """
64+
Returns a child spec suitable for embedding under a
65+
supervision tree.
5766
58-
See `start/1`.
67+
### Options
5968
60-
### Returns
69+
See `start/1`.
6170
62-
* A child specification map.
71+
### Returns
6372
64-
"""
65-
@spec child_spec(keyword()) :: Supervisor.child_spec()
66-
def child_spec(options \\ []) do
67-
ensure_bandit!()
73+
* A child specification map.
6874
69-
port = Keyword.get(options, :port, 4001)
70-
ip = Keyword.get(options, :ip, :loopback)
75+
"""
76+
@spec child_spec(keyword()) :: Supervisor.child_spec()
77+
def child_spec(options \\ []) do
78+
port = Keyword.get(options, :port, 4001)
79+
ip = Keyword.get(options, :ip, :loopback)
7180

72-
%{
73-
id: __MODULE__,
74-
start:
75-
{Bandit, :start_link, [[plug: Color.Palette.Visualizer, port: port, ip: ip_tuple(ip)]]},
76-
type: :supervisor
77-
}
78-
end
81+
%{
82+
id: __MODULE__,
83+
start:
84+
{Bandit, :start_link, [[plug: Color.Palette.Visualizer, port: port, ip: ip_tuple(ip)]]},
85+
type: :supervisor
86+
}
87+
end
7988

80-
@doc """
81-
Stops a standalone server started by `start/1`.
89+
@doc """
90+
Stops a standalone server started by `start/1`.
8291
83-
### Arguments
92+
### Arguments
8493
85-
* `pid` — the process identifier returned by `start/1`.
94+
* `pid` — the process identifier returned by `start/1`.
8695
87-
### Returns
96+
### Returns
8897
89-
* `:ok`.
98+
* `:ok`.
9099
91-
"""
92-
@spec stop(pid()) :: :ok
93-
def stop(pid) when is_pid(pid) do
94-
_ = Supervisor.stop(pid)
95-
:ok
96-
end
100+
"""
101+
@spec stop(pid()) :: :ok
102+
def stop(pid) when is_pid(pid) do
103+
_ = Supervisor.stop(pid)
104+
:ok
105+
end
97106

98-
# ---- helpers ------------------------------------------------------------
107+
# ---- helpers ------------------------------------------------------------
99108

100-
defp ensure_bandit! do
101-
unless Code.ensure_loaded?(Bandit) do
102-
raise """
103-
Color.Palette.Visualizer.Standalone requires :bandit.
104-
Add `{:bandit, "~> 1.5"}` to your project's deps and run mix deps.get.
105-
"""
106-
end
109+
defp ip_tuple(:loopback), do: {127, 0, 0, 1}
110+
defp ip_tuple(:any), do: {0, 0, 0, 0}
111+
defp ip_tuple({_, _, _, _} = tuple), do: tuple
107112
end
108-
109-
defp ip_tuple(:loopback), do: {127, 0, 0, 1}
110-
defp ip_tuple(:any), do: {0, 0, 0, 0}
111-
defp ip_tuple({_, _, _, _} = tuple), do: tuple
112113
end

0 commit comments

Comments
 (0)