Credo: style validation as a table of checks; connector style split by part

c93a87c · Gabe Morell · 2026-11-04 20:32

4 files +69 -108

Files changed

modified lib/diogramos/diagrams/document.ex
+69 −108
@@ -155,78 +155,42 @@ defmodule Diogramos.Diagrams.Document do
155 155 @doc "Whitelist of shape label size presets."
156 156 def label_sizes, do: @label_sizes
157 157
158 − defp validate_style(%{} = style) do
159 − fill = Map.get(style, "fill")
160 − stroke = Map.get(style, "stroke")
161 − shadow = Map.get(style, "shadow")
162 − font_family = Map.get(style, "font_family")
163 − font_size = Map.get(style, "font_size")
164 − font_bold = Map.get(style, "font_bold")
165 − font_italic = Map.get(style, "font_italic")
166 −
167 − dash = Map.get(style, "dash")
168 − stroke_width = Map.get(style, "stroke_width")
169 −
170 − label_position = Map.get(style, "label_position")
171 − label_size = Map.get(style, "label_size")
172 − label_color = Map.get(style, "label_color")
173 − label_font_family = Map.get(style, "label_font_family")
174 − label_bold = Map.get(style, "label_bold")
175 − label_italic = Map.get(style, "label_italic")
176 −
177 − cond do
178 − not is_nil(fill) and not Diogramos.Themes.valid_color?(fill) ->
179 − {:error, :invalid_color}
180 −
181 − not is_nil(stroke) and not Diogramos.Themes.valid_color?(stroke) ->
182 − {:error, :invalid_color}
183 −
184 − not is_nil(shadow) and not is_boolean(shadow) ->
185 − {:error, :invalid_shadow}
186 −
187 − not is_nil(dash) and dash not in @dash_presets ->
188 − {:error, :invalid_dash}
189 −
190 − not is_nil(stroke_width) and (not is_number(stroke_width) or stroke_width <= 0) ->
191 − {:error, :invalid_stroke_width}
192 −
193 − not is_nil(label_position) and label_position not in @label_positions ->
194 − {:error, :invalid_label_position}
195 −
196 − not is_nil(label_size) and label_size not in @label_sizes ->
197 − {:error, :invalid_label_size}
198 −
199 − not is_nil(label_color) and not Diogramos.Themes.valid_color?(label_color) ->
200 − {:error, :invalid_label_color}
201 −
202 − not is_nil(label_font_family) and label_font_family not in @font_families ->
203 − {:error, :invalid_label_font_family}
204 −
205 − not is_nil(label_bold) and not is_boolean(label_bold) ->
206 − {:error, :invalid_label_style}
158 + # Each style key is optional; when present it must pass its check. Checked
159 + # in order, so the first failing key decides the error.
160 + @style_checks [
161 + {"fill", :color, :invalid_color},
162 + {"stroke", :color, :invalid_color},
163 + {"shadow", :boolean, :invalid_shadow},
164 + {"dash", {:one_of, @dash_presets}, :invalid_dash},
165 + {"stroke_width", :positive_number, :invalid_stroke_width},
166 + {"label_position", {:one_of, @label_positions}, :invalid_label_position},
167 + {"label_size", {:one_of, @label_sizes}, :invalid_label_size},
168 + {"label_color", :color, :invalid_label_color},
169 + {"label_font_family", {:one_of, @font_families}, :invalid_label_font_family},
170 + {"label_bold", :boolean, :invalid_label_style},
171 + {"label_italic", :boolean, :invalid_label_style},
172 + {"font_family", {:one_of, @font_families}, :invalid_font_family},
173 + {"font_size", :font_size, :invalid_font_size},
174 + {"font_bold", :boolean, :invalid_font_style},
175 + {"font_italic", :boolean, :invalid_font_style}
176 + ]
207 177
208 − not is_nil(label_italic) and not is_boolean(label_italic) ->
209 − {:error, :invalid_label_style}
210 −
211 − not is_nil(font_family) and font_family not in @font_families ->
212 − {:error, :invalid_font_family}
213 −
214 − not is_nil(font_size) and (not is_number(font_size) or font_size < 4 or font_size > 256) ->
215 − {:error, :invalid_font_size}
216 −
217 − not is_nil(font_bold) and not is_boolean(font_bold) ->
218 − {:error, :invalid_font_style}
219 −
220 − not is_nil(font_italic) and not is_boolean(font_italic) ->
221 − {:error, :invalid_font_style}
178 + defp validate_style(%{} = style) do
179 + Enum.find_value(@style_checks, :ok, fn {key, check, error} ->
180 + value = Map.get(style, key)
222 181
223 − true ->
224 − :ok
225 − end
182 + if is_nil(value) or valid_style_value?(check, value), do: nil, else: {:error, error}
183 + end)
226 184 end
227 185
228 186 defp validate_style(_), do: :ok
229 187
188 + defp valid_style_value?(:color, value), do: Diogramos.Themes.valid_color?(value)
189 + defp valid_style_value?(:boolean, value), do: is_boolean(value)
190 + defp valid_style_value?({:one_of, allowed}, value), do: value in allowed
191 + defp valid_style_value?(:positive_number, value), do: is_number(value) and value > 0
192 + defp valid_style_value?(:font_size, value), do: is_number(value) and value >= 4 and value <= 256
193 +
230 194 @doc "Whitelist of font-family tokens accepted on text elements."
231 195 def font_families, do: @font_families
232 196
@@ -377,54 +341,51 @@ defmodule Diogramos.Diagrams.Document do
377 341 stroke_width = Map.get(connector, "stroke_width", 2)
378 342 color = Map.get(connector, "color", "base-content")
379 343
380 − cond do
381 − routing not in @connector_routings ->
382 − {:error, :invalid_routing}
383 −
384 − dash not in @dash_presets ->
385 − {:error, :invalid_dash}
386 −
387 − not Diogramos.Themes.valid_color?(color) ->
388 − {:error, :invalid_color}
389 −
390 − marker_start not in @end_markers ->
391 − {:error, :invalid_marker}
392 −
393 − marker_end not in @end_markers ->
394 − {:error, :invalid_marker}
395 −
396 − not is_number(start_gap) or start_gap < 0 ->
397 − {:error, :invalid_gap}
398 −
399 − not is_number(end_gap) or end_gap < 0 ->
400 − {:error, :invalid_gap}
401 −
402 − not is_number(stroke_width) or stroke_width <= 0 ->
403 − {:error, :invalid_stroke_width}
344 + with :ok <- validate_connector_path(routing, dash, color),
345 + :ok <- validate_connector_ends(marker_start, marker_end, start_gap, end_gap),
346 + :ok <- validate_connector_stroke_width(stroke_width) do
347 + {:ok,
348 + %{
349 + "routing" => routing,
350 + "dash" => dash,
351 + "marker_start" => marker_start,
352 + "marker_end" => marker_end,
353 + "start_gap" => start_gap,
354 + "end_gap" => end_gap,
355 + "stroke_width" => stroke_width,
356 + "color" => color
357 + }}
358 + end
359 + end
404 360
405 − true ->
406 − {:ok,
407 − %{
408 − "routing" => routing,
409 − "dash" => dash,
410 − "marker_start" => marker_start,
411 − "marker_end" => marker_end,
412 − "start_gap" => start_gap,
413 − "end_gap" => end_gap,
414 − "stroke_width" => stroke_width,
415 − "color" => color
416 − }}
361 + defp validate_connector_path(routing, dash, color) do
362 + cond do
363 + routing not in @connector_routings -> {:error, :invalid_routing}
364 + dash not in @dash_presets -> {:error, :invalid_dash}
365 + not Diogramos.Themes.valid_color?(color) -> {:error, :invalid_color}
366 + true -> :ok
417 367 end
418 368 end
419 369
420 − defp merge_connector(connector, patch, doc) when is_map(patch) do
370 + defp validate_connector_ends(marker_start, marker_end, start_gap, end_gap) do
421 371 cond do
422 − Map.has_key?(patch, "id") and patch["id"] != connector["id"] ->
423 − {:error, :id_immutable}
372 + marker_start not in @end_markers -> {:error, :invalid_marker}
373 + marker_end not in @end_markers -> {:error, :invalid_marker}
374 + not is_number(start_gap) or start_gap < 0 -> {:error, :invalid_gap}
375 + not is_number(end_gap) or end_gap < 0 -> {:error, :invalid_gap}
376 + true -> :ok
377 + end
378 + end
424 379
425 − true ->
426 − merged = deep_merge(connector, patch)
427 − validate_connector(merged, doc)
380 + defp validate_connector_stroke_width(width) when is_number(width) and width > 0, do: :ok
381 + defp validate_connector_stroke_width(_), do: {:error, :invalid_stroke_width}
382 +
383 + defp merge_connector(connector, patch, doc) when is_map(patch) do
384 + if Map.has_key?(patch, "id") and patch["id"] != connector["id"] do
385 + {:error, :id_immutable}
386 + else
387 + merged = deep_merge(connector, patch)
388 + validate_connector(merged, doc)
428 389 end
429 390 end
430 391

Parents: 5ea0ebf