Skip to content

Commit 004a5d3

Browse files
fahchenclaude
andauthored
refactor: migrate Ecto schemas to ecto_typed_schema (#71)
## Summary - Add `ecto_typed_schema ~> 0.2.0` and bump `typed_structor ~> 0.6` (requires `override: true` — `dprint_markdown_formatter` still pins `~> 0.5.0`) - Migrate 7 Ecto schemas from the `typed_structor + schema` dual-block pattern to `ecto_typed_schema`'s unified `typed_schema` macro; `t()` is derived directly from Ecto field definitions, with `typed: [type: ...]` overrides for custom codec fields - Drop dead `Types.{association,maybe_association,maybe_id}` helpers and the unused `alias Types` in the base schema macro - Incidental upgrades: credo 1.7.7→1.7.18 (fixes Elixir 1.19.5 regex compile error), dialyxir 1.4.3→1.4.7 (fixes OTP 27+ formatter crash) - Add `.dialyzer_ignore` to filter Ecto.Multi/MapSet opaque false positives (plain-text format rather than `.exs` — `:opaque_compare` isn't in dialyxir's warning registry, so only the legacy substring-match path filters it) ## Migration pattern Before: ```elixir typed_structor define_struct: false, enforce: true do field :id, Types.id() field :flow, Types.association(Flow.t()) field :state, state(), default: :running end schema "enactments" do belongs_to :flow, Flow field :state, Ecto.Enum, values: [...], default: :running end ``` After: ```elixir typed_schema "enactments", null: false do belongs_to :flow, Flow field :state, Ecto.Enum, values: [...], default: :running, typed: [type: state()] end ``` ## Test plan - [x] `mix compile` (only pre-existing Elixir 1.19 struct-update warnings remain) - [x] `mix format --check-formatted` - [x] `mix credo --strict` - [x] `mix test test/coloured_flow/runner/` (138 tests, 0 failures) - [x] `mix dialyzer` (7 warnings all matched by `.dialyzer_ignore`, passed successfully) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 72895e5 commit 004a5d3

20 files changed

Lines changed: 143 additions & 197 deletions

File tree

.dialyzer_ignore.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# OTP 28 Dialyzer is stricter with opaque types. Ecto.Multi internally uses
2+
# MapSet which wraps the opaque :sets.set(), triggering false positives.
3+
# Tracked in: https://github.com/elixir-ecto/ecto/issues/4707
4+
# Remove after upgrading to an Elixir version that fully fixes this.
5+
[
6+
~r/call_without_opaque.*opaque term/
7+
]

lib/coloured_flow/definition/arc.ex

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,28 @@ defmodule ColouredFlow.Definition.Arc do
8888

8989
def build_expression(:p_to_t, code) do
9090
with {:ok, expression} <- Expression.build(code) do
91-
case validate_bind_exprs(expression.expr) do
92-
[] ->
93-
{:error, {[], "missing `bind` in expression", code}}
94-
95-
validations ->
96-
case Enum.find(validations, &match?({:error, _reason}, &1)) do
97-
nil -> {:ok, expression}
98-
{:error, reason} -> {:error, reason}
99-
end
100-
end
91+
validate_p_to_t_bindings(expression, code)
10192
end
10293
end
10394

10495
def build_expression(:t_to_p, code) do
10596
Expression.build(code)
10697
end
10798

99+
defp validate_p_to_t_bindings(expression, code) do
100+
case validate_bind_exprs(expression.expr) do
101+
[] -> {:error, {[], "missing `bind` in expression", code}}
102+
validations -> first_binding_error(expression, validations)
103+
end
104+
end
105+
106+
defp first_binding_error(expression, validations) do
107+
case Enum.find(validations, &match?({:error, _reason}, &1)) do
108+
nil -> {:ok, expression}
109+
{:error, reason} -> {:error, reason}
110+
end
111+
end
112+
108113
@spec build_expression!(orientation(), code :: binary() | nil) :: Expression.t()
109114
def build_expression!(orientation, code) do
110115
case build_expression(orientation, code) do

lib/coloured_flow/enabled_binding_elements/binding.ex

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,20 @@ defmodule ColouredFlow.EnabledBindingElements.Binding do
7171
"""
7272
@spec combine(bindings_list :: [[[binding()]]]) :: [[binding()]]
7373
def combine(bindings_list) do
74-
Enum.reduce(bindings_list, [[]], fn bindings, prevs ->
75-
for prev <- prevs, binding <- bindings, reduce: [] do
76-
acc ->
77-
case get_conflicts(prev, binding) do
78-
[] -> [Keyword.merge(prev, binding) | acc]
79-
_other -> acc
80-
end
81-
end
82-
end)
74+
Enum.reduce(bindings_list, [[]], &combine_step/2)
75+
end
76+
77+
defp combine_step(bindings, prevs) do
78+
for prev <- prevs, binding <- bindings, reduce: [] do
79+
acc -> merge_if_compatible(acc, prev, binding)
80+
end
81+
end
82+
83+
defp merge_if_compatible(acc, prev, binding) do
84+
case get_conflicts(prev, binding) do
85+
[] -> [Keyword.merge(prev, binding) | acc]
86+
_other -> acc
87+
end
8388
end
8489

8590
@doc """

lib/coloured_flow/enabled_binding_elements/computation.ex

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -51,37 +51,43 @@ defmodule ColouredFlow.EnabledBindingElements.Computation do
5151
binding_combinations = Binding.combine(arc_bindings)
5252

5353
Enum.flat_map(binding_combinations, fn binding ->
54-
inputs
55-
|> Enum.reduce_while([], fn {arc, place}, acc ->
56-
arc_binding = merge_constants(binding, arc, constants)
57-
58-
with(
59-
{:ok, {coefficient, value}} <- eval_arc(arc, arc_binding),
60-
colour_set = fetch_colour_set!(place.colour_set, cpnet),
61-
of_type_context = build_of_type_context(cpnet),
62-
{:ok, ^value} <- ColourSet.Of.of_type(value, colour_set.type, of_type_context),
63-
guard_binding = merge_constants(binding, transition, constants),
64-
{:ok, true} <- eval_transition_guard(transition, guard_binding),
65-
marking = get_marking(place, markings),
66-
tokens = MultiSet.duplicate(value, coefficient),
67-
true <- MultiSet.include?(marking.tokens, tokens)
68-
) do
69-
{:cont, [%Marking{place: place.name, tokens: tokens} | acc]}
70-
else
71-
_other -> {:halt, :error}
72-
end
73-
end)
74-
|> case do
75-
:error ->
76-
[]
54+
build_binding_element(inputs, binding, transition, constants, markings, cpnet)
55+
end)
56+
end
7757

78-
to_consume ->
79-
# binding here should not contain constants
80-
[BindingElement.new(transition.name, binding, to_consume)]
81-
end
58+
defp build_binding_element(inputs, binding, transition, constants, markings, cpnet) do
59+
case collect_consumption(inputs, binding, transition, constants, markings, cpnet) do
60+
:error -> []
61+
to_consume -> [BindingElement.new(transition.name, binding, to_consume)]
62+
end
63+
end
64+
65+
defp collect_consumption(inputs, binding, transition, constants, markings, cpnet) do
66+
Enum.reduce_while(inputs, [], fn {arc, place}, acc ->
67+
try_consume(arc, place, binding, transition, constants, markings, cpnet, acc)
8268
end)
8369
end
8470

71+
defp try_consume(arc, place, binding, transition, constants, markings, cpnet, acc) do
72+
arc_binding = merge_constants(binding, arc, constants)
73+
74+
with(
75+
{:ok, {coefficient, value}} <- eval_arc(arc, arc_binding),
76+
colour_set = fetch_colour_set!(place.colour_set, cpnet),
77+
of_type_context = build_of_type_context(cpnet),
78+
{:ok, ^value} <- ColourSet.Of.of_type(value, colour_set.type, of_type_context),
79+
guard_binding = merge_constants(binding, transition, constants),
80+
{:ok, true} <- eval_transition_guard(transition, guard_binding),
81+
marking = get_marking(place, markings),
82+
tokens = MultiSet.duplicate(value, coefficient),
83+
true <- MultiSet.include?(marking.tokens, tokens)
84+
) do
85+
{:cont, [%Marking{place: place.name, tokens: tokens} | acc]}
86+
else
87+
_other -> {:halt, :error}
88+
end
89+
end
90+
8591
@spec reject_invalid_bandings([[[BindingElement.binding()]]], ColouredPetriNet.t()) ::
8692
[[[BindingElement.binding()]]]
8793
defp reject_invalid_bandings(bindings_list, cpnet) do

lib/coloured_flow/notation/colset.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ defmodule ColouredFlow.Notation.Colset do
173173
defp decompose_union([], acc) do
174174
duplicate_tags = find_duplicate_tags(acc)
175175

176-
if length(duplicate_tags) > 0 do
176+
if duplicate_tags != [] do
177177
raise """
178178
Invalid union tags, duplicate tags found: `#{type_to_string(duplicate_tags)}`
179179
"""

lib/coloured_flow/runner/enactment/workitem_calibration.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ defmodule ColouredFlow.Runner.Enactment.WorkitemCalibration do
134134
workitem_occurrences = Keyword.fetch!(options, :workitem_occurrences)
135135

136136
to_consume_markings =
137-
Enum.flat_map(workitem_occurrences, fn {workitem, _} ->
137+
Enum.flat_map(workitem_occurrences, fn {workitem, _occurrence} ->
138138
workitem.binding_element.to_consume
139139
end)
140140

@@ -170,7 +170,9 @@ defmodule ColouredFlow.Runner.Enactment.WorkitemCalibration do
170170
ColouredPetriNet.t()
171171
) :: {enactment_state(), MultiSet.t(BindingElement.t())}
172172
defp produce_workitems(%Enactment{} = state, workitem_occurrences, %ColouredPetriNet{} = cpnet) do
173-
completed_workitem_ids = Enum.map(workitem_occurrences, fn {workitem, _} -> workitem.id end)
173+
completed_workitem_ids =
174+
Enum.map(workitem_occurrences, fn {workitem, _occurrence} -> workitem.id end)
175+
174176
occurrences = Enum.map(workitem_occurrences, &elem(&1, 1))
175177

176178
{steps, markings} =

lib/coloured_flow/runner/storage/schemas/enactment.ex

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,20 @@ defmodule ColouredFlow.Runner.Storage.Schemas.Enactment do
1313

1414
@type state() :: unquote(ColouredFlow.Types.make_sum_type(states))
1515

16-
typed_structor define_struct: false, enforce: true do
17-
field :id, Types.id()
18-
field :flow_id, Types.id()
19-
field :flow, Types.association(Flow.t())
20-
21-
field :state, state(), default: :running
22-
field :label, String.t(), enforce: false
23-
field :initial_markings, [Marking.t()]
24-
field :final_markings, [Marking.t()], enforce: false
16+
typed_schema "enactments", null: false do
17+
belongs_to :flow, Flow
2518

26-
field :steps, Types.association([Occurrence.t()])
19+
field :state, Ecto.Enum,
20+
values: [:running, :exception, :terminated],
21+
default: :running,
22+
typed: [type: state()]
2723

28-
field :inserted_at, DateTime.t()
29-
field :updated_at, DateTime.t()
30-
end
31-
32-
schema "enactments" do
33-
belongs_to :flow, Flow
24+
field :label, :string, typed: [null: true]
25+
field :initial_markings, {:array, Object}, codec: Codec.Marking, typed: [type: [Marking.t()]]
3426

35-
field :state, Ecto.Enum, values: [:running, :exception, :terminated], default: :running
36-
field :label, :string
37-
field :initial_markings, {:array, Object}, codec: Codec.Marking
38-
field :final_markings, {:array, Object}, codec: Codec.Marking
27+
field :final_markings, {:array, Object},
28+
codec: Codec.Marking,
29+
typed: [type: [Marking.t()], null: true]
3930

4031
has_many :steps, Occurrence
4132

lib/coloured_flow/runner/storage/schemas/enactment_log.ex

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,34 @@ defmodule ColouredFlow.Runner.Storage.Schemas.EnactmentLog do
77

88
alias ColouredFlow.Runner.Storage.Schemas.Enactment
99

10-
typed_structor define_struct: false, enforce: true do
11-
field :id, Types.id()
12-
field :enactment_id, Types.id()
13-
field :enactment, Types.association(Enactment.t())
14-
15-
field :state, Enactment.state()
16-
17-
field :termination,
18-
%{type: ColouredFlow.Runner.Termination.type(), message: String.t() | nil},
19-
enforce: false
20-
21-
field :exception, %{type: String.t(), message: String.t(), original: String.t()},
22-
enforce: false
23-
24-
field :inserted_at, DateTime.t()
25-
end
26-
27-
schema "enactment_logs" do
10+
typed_schema "enactment_logs", null: false do
2811
belongs_to :enactment, Enactment
2912

30-
field :state, Ecto.Enum, values: Enactment.__states__()
13+
field :state, Ecto.Enum, values: Enactment.__states__(), typed: [type: Enactment.state()]
3114

32-
embeds_one :termination, Termination, primary_key: false, on_replace: :delete do
15+
embeds_one :termination, Termination,
16+
primary_key: false,
17+
on_replace: :delete,
18+
typed: [null: true] do
3319
@moduledoc false
3420

35-
field :type, Ecto.Enum, values: ColouredFlow.Runner.Termination.__types__()
36-
field :message, :string
21+
field :type, Ecto.Enum,
22+
values: ColouredFlow.Runner.Termination.__types__(),
23+
typed: [type: ColouredFlow.Runner.Termination.type()]
24+
25+
field :message, :string, typed: [null: true]
3726
end
3827

39-
embeds_one :exception, Exception, primary_key: false, on_replace: :delete do
28+
embeds_one :exception, Exception,
29+
primary_key: false,
30+
on_replace: :delete,
31+
typed: [null: true] do
4032
@moduledoc false
4133

42-
field :reason, Ecto.Enum, values: ColouredFlow.Runner.Exception.__reasons__()
34+
field :reason, Ecto.Enum,
35+
values: ColouredFlow.Runner.Exception.__reasons__(),
36+
typed: [type: ColouredFlow.Runner.Exception.reason()]
37+
4338
field :type, :string
4439
field :message, :string
4540
field :original, :string

lib/coloured_flow/runner/storage/schemas/flow.ex

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,9 @@ defmodule ColouredFlow.Runner.Storage.Schemas.Flow do
77

88
alias ColouredFlow.Definition.ColouredPetriNet
99

10-
typed_structor define_struct: false, enforce: true do
11-
field :id, Types.id()
12-
field :name, String.t()
13-
field :definition, ColouredPetriNet.t()
14-
15-
field :inserted_at, DateTime.t()
16-
end
17-
18-
schema "flows" do
10+
typed_schema "flows", null: false do
1911
field :name, :string
20-
field :definition, Object, codec: Codec.ColouredPetriNet
12+
field :definition, Object, codec: Codec.ColouredPetriNet, typed: [type: ColouredPetriNet.t()]
2113

2214
timestamps(updated_at: false)
2315
end

lib/coloured_flow/runner/storage/schemas/json_instance/codec/colour_set.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ defmodule ColouredFlow.Runner.Storage.Schemas.JsonInstance.Codec.ColourSet do
151151
Converts the `descr` to a JSON representation.
152152
"""
153153
@spec encode_descr(ColourSet.descr()) :: map()
154-
def encode_descr({primitive, _}) when primitive in @primitive_types do
154+
def encode_descr({primitive, _args}) when primitive in @primitive_types do
155155
%{
156156
"type" => Atom.to_string(primitive),
157157
"args" => []

0 commit comments

Comments
 (0)