neiam
/diogramos
diogramos
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
Credo: style validation as a table of checks; connector style split by part
c93a87c · Gabe Morell · 2026-11-04 20:32
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