• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

code-shoily / choreo / 79946cc69ce25f666f182f96decf62fd4a0e0dbb

20 Jul 2026 03:40AM UTC coverage: 82.838% (-0.04%) from 82.88%
79946cc69ce25f666f182f96decf62fd4a0e0dbb

push

github

code-shoily
Add Choreo render mix task

57 of 78 new or added lines in 3 files covered. (73.08%)

7525 of 9084 relevant lines covered (82.84%)

19.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.0
/lib/choreo/render_script.ex
1
defmodule Choreo.RenderScript do
2
  @moduledoc false
3

4
  @type target :: :mermaid | :dot
5
  @type artifact :: %{name: String.t(), model: struct(), opts: keyword()}
6

7
  @doc false
8
  @spec eval_file(Path.t()) :: any()
9
  def eval_file(path) when is_binary(path) do
10
    path
11
    |> Code.eval_file()
12
    |> elem(0)
6✔
13
  end
14

15
  @doc false
16
  @spec normalize(any(), keyword()) :: [artifact()]
17
  def normalize(value, opts \\ []) do
18
    default_name = opts |> Keyword.fetch!(:default_name) |> artifact_name!()
13✔
19

20
    value
21
    |> do_normalize(default_name)
22
    |> maybe_filter(Keyword.get(opts, :only))
13✔
23
  end
24

25
  @doc false
26
  @spec render_artifact(artifact(), target()) :: String.t()
27
  def render_artifact(%{model: model, opts: opts}, :mermaid), do: Choreo.to_mermaid(model, opts)
14✔
28
  def render_artifact(%{model: model, opts: opts}, :dot), do: Choreo.to_dot(model, opts)
2✔
29

30
  @doc false
31
  @spec extension(target()) :: String.t()
32
  def extension(:mermaid), do: ".mmd"
4✔
NEW
33
  def extension(:dot), do: ".dot"
×
34

35
  @doc false
36
  @spec parse_target(String.t() | nil) :: target()
37
  def parse_target(nil), do: :mermaid
5✔
NEW
38
  def parse_target("mermaid"), do: :mermaid
×
NEW
39
  def parse_target("mmd"), do: :mermaid
×
40
  def parse_target("dot"), do: :dot
1✔
41

42
  def parse_target(other) do
NEW
43
    raise ArgumentError, "unsupported render target #{inspect(other)}; expected mermaid or dot"
×
44
  end
45

46
  @doc false
47
  @spec output_path(artifact(), target(), Path.t(), single?: boolean()) :: Path.t()
48
  def output_path(%{name: name}, target, out, single?: single?) do
49
    cond do
7✔
50
      output_directory?(out) ->
51
        Path.join(out, name <> extension(target))
4✔
52

53
      single? ->
3✔
54
        out
3✔
55

NEW
56
      true ->
×
NEW
57
        raise ArgumentError,
×
58
              "multiple artifacts require --out to be a directory, got #{inspect(out)}"
59
    end
60
  end
61

62
  @doc false
63
  @spec default_name(Path.t()) :: String.t()
64
  def default_name(path) do
65
    path
66
    |> Path.basename()
67
    |> String.replace_suffix(".choreo.exs", "")
68
    |> String.replace_suffix(".exs", "")
69
    |> String.replace_suffix(".ex", "")
70
    |> artifact_name!()
6✔
71
  end
72

73
  defp do_normalize(%_struct{} = model, default_name),
4✔
74
    do: [%{name: default_name, model: model, opts: []}]
75

76
  defp do_normalize(%{} = map, _default_name) do
77
    map
78
    |> Enum.map(fn {name, value} -> artifact_from_named_value(name, value) end)
11✔
79
    |> Enum.sort_by(& &1.name)
7✔
80
  end
81

82
  defp do_normalize(value, default_name) when is_list(value) do
83
    if Keyword.keyword?(value) do
4✔
84
      value
85
      |> Enum.reduce(%{}, fn {name, artifact_value}, acc ->
86
        Map.put(acc, artifact_name!(name), artifact_value)
6✔
87
      end)
88
      |> do_normalize(default_name)
3✔
89
    else
90
      invalid_return!(value)
1✔
91
    end
92
  end
93

94
  defp do_normalize({model, opts}, default_name) when is_list(opts) do
95
    if Keyword.keyword?(opts) do
1✔
96
      [%{name: default_name, model: model, opts: opts}]
97
    else
NEW
98
      invalid_artifact_value!(default_name, {model, opts})
×
99
    end
100
  end
101

NEW
102
  defp do_normalize(model, default_name), do: [%{name: default_name, model: model, opts: []}]
×
103

104
  defp artifact_from_named_value(name, {model, opts}) when is_list(opts) do
105
    name = artifact_name!(name)
2✔
106

107
    if Keyword.keyword?(opts) do
2✔
108
      %{name: name, model: model, opts: opts}
2✔
109
    else
NEW
110
      invalid_artifact_value!(name, {model, opts})
×
111
    end
112
  end
113

114
  defp artifact_from_named_value(name, model),
115
    do: %{name: artifact_name!(name), model: model, opts: []}
9✔
116

117
  defp maybe_filter(artifacts, nil), do: artifacts
9✔
118

119
  defp maybe_filter(artifacts, only) do
120
    requested = artifact_name!(only)
2✔
121
    selected = Enum.filter(artifacts, &(&1.name == requested))
2✔
122

123
    case selected do
2✔
124
      [] ->
NEW
125
        available = Enum.map_join(artifacts, ", ", & &1.name)
×
126

NEW
127
        raise ArgumentError,
×
NEW
128
              "artifact #{inspect(requested)} was not found; available artifacts: #{available}"
×
129

130
      _ ->
131
        selected
2✔
132
    end
133
  end
134

135
  defp artifact_name!(name) when is_atom(name), do: name |> Atom.to_string() |> artifact_name!()
9✔
136

137
  defp artifact_name!(name) when is_binary(name) do
138
    name
139
    |> String.downcase()
140
    |> String.replace(~r/[^a-z0-9_.-]+/u, "_")
141
    |> String.trim("_")
142
    |> case do
37✔
NEW
143
      "" -> raise ArgumentError, "artifact names cannot be empty"
×
144
      safe -> safe
37✔
145
    end
146
  end
147

148
  defp artifact_name!(name) do
149
    raise ArgumentError, "artifact names must be atoms or strings, got #{inspect(name)}"
1✔
150
  end
151

152
  defp output_directory?(path), do: String.ends_with?(path, ["/", "\\"]) or File.dir?(path)
7✔
153

154
  defp invalid_return!(value) do
155
    raise ArgumentError,
1✔
156
          "expected render script to return a Choreo renderable struct, " <>
157
            "{struct, opts}, a map of name => struct | {struct, opts}, or a keyword list of name => struct | {struct, opts}; " <>
158
            "got #{inspect(value)}"
159
  end
160

161
  defp invalid_artifact_value!(name, value) do
NEW
162
    raise ArgumentError,
×
163
          "artifact #{inspect(name)} must be a Choreo renderable struct or {struct, keyword_opts}; " <>
164
            "got #{inspect(value)}"
165
  end
166
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc