@@ -16,6 +16,7 @@ defmodule ColouredFlow.DSL.Builder do
1616 alias ColouredFlow.Validators.Exceptions.UniqueNameViolationError
1717
1818 @ doc false
19+ # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
1920 defmacro __before_compile__ ( env ) do
2021 cpnet = build_cpnet ( env . module )
2122 cpnet = resolve_function_results ( cpnet )
@@ -30,8 +31,20 @@ defmodule ColouredFlow.DSL.Builder do
3031 { :ok , validated } ->
3132 name = Module . get_attribute ( env . module , :cf_name )
3233 version = Module . get_attribute ( env . module , :cf_version )
34+ storage = Module . get_attribute ( env . module , :cf_storage )
35+ task_supervisor = Module . get_attribute ( env . module , :cf_task_supervisor )
36+ transition_actions = source_order_meta ( env . module , :cf_transition_actions )
37+ lifecycle_hooks = source_order_meta ( env . module , :cf_lifecycle_hooks )
38+
39+ action_clauses =
40+ Enum . map ( transition_actions , & compile_transition_action ( & 1 , task_supervisor ) )
41+
42+ lifecycle_clauses =
43+ Enum . map ( lifecycle_hooks , & compile_lifecycle_hook ( & 1 , task_supervisor ) )
3344
3445 quote do
46+ @ behaviour ColouredFlow.Runner.ActionHandler
47+
3548 @ doc """
3649 The `%ColouredFlow.Definition.ColouredPetriNet{}` materialised from the DSL
3750 declarations in this module.
@@ -54,6 +67,70 @@ defmodule ColouredFlow.DSL.Builder do
5467 def __cpn__ ( :name ) , do: unquote ( name )
5568 def __cpn__ ( :version ) , do: unquote ( version )
5669 def __cpn__ ( :initial_markings ) , do: unquote ( Macro . escape ( initial_markings ) )
70+
71+ @ doc """
72+ Idempotent flow setup. Inserts the materialised CPN under the workflow name (or
73+ raises if no `name` was declared) using the storage configured via
74+ `use ColouredFlow.DSL, storage: ...`. Re-invocations return the previously
75+ stored row.
76+ """
77+ # credo:disable-for-next-line JetCredo.Checks.ExplicitAnyType
78+ @ spec setup_flow! ( ) :: term ( )
79+ def setup_flow! do
80+ ColouredFlow.DSL.Builder . __setup_flow__! (
81+ __MODULE__ ,
82+ unquote ( storage ) ,
83+ unquote ( name )
84+ )
85+ end
86+
87+ @ doc """
88+ Insert an enactment for the given flow with the workflow's declared initial
89+ markings (overridable via the second argument).
90+ """
91+ # credo:disable-for-next-line JetCredo.Checks.ExplicitAnyType
92+ @ spec insert_enactment! ( term ( ) ) :: term ( )
93+ # credo:disable-for-next-line JetCredo.Checks.ExplicitAnyType
94+ @ spec insert_enactment! ( term ( ) , [ Marking . t ( ) ] ) :: term ( )
95+ def insert_enactment! ( flow , initial_markings \\ __cpn__ ( :initial_markings ) ) do
96+ ColouredFlow.DSL.Builder . __insert_enactment__! (
97+ unquote ( storage ) ,
98+ flow ,
99+ initial_markings
100+ )
101+ end
102+
103+ @ doc """
104+ Start the enactment under `ColouredFlow.Runner.Enactment.Supervisor`, binding
105+ this module as the per-instance `ColouredFlow.Runner.ActionHandler` unless the
106+ caller overrides it.
107+ """
108+ @ spec start_enactment ( binary ( ) , keyword ( ) ) ::
109+ DynamicSupervisor . on_start_child ( )
110+ def start_enactment ( enactment_id , opts \\ [ ] ) when is_binary ( enactment_id ) do
111+ opts = Keyword . put_new ( opts , :action_handler , __MODULE__ )
112+ ColouredFlow.Runner.Enactment.Supervisor . start_enactment ( enactment_id , opts )
113+ end
114+
115+ # credo:disable-for-next-line JetCredo.Checks.ExplicitAnyType
116+ @ spec __action_for__ ( String . t ( ) , Keyword . t ( ) , map ( ) , term ( ) ) :: any ( )
117+ unquote_splicing ( action_clauses )
118+ defp __action_for__ ( _transition_name , _binding , _ctx , _workitem ) , do: :ok
119+
120+ @ doc false
121+ @ impl ColouredFlow.Runner.ActionHandler
122+ def on_workitem_completed ( ctx , workitem , _occurrence ) do
123+ __action_for__ (
124+ workitem . binding_element . transition ,
125+ workitem . binding_element . binding ,
126+ ctx ,
127+ workitem
128+ )
129+
130+ :ok
131+ end
132+
133+ unquote_splicing ( lifecycle_clauses )
57134 end
58135
59136 { :error , exception } ->
@@ -307,4 +384,129 @@ defmodule ColouredFlow.DSL.Builder do
307384 end
308385
309386 defp resolve_step ( :unknown , descr , _colour_sets ) , do: descr
387+
388+ # Compile a single transition's `action do ... end` body into a
389+ # `__action_for__/4` clause. The body has access to:
390+ #
391+ # * `ctx` — `ColouredFlow.Runner.ActionHandler.ctx()`
392+ # * `workitem` — the just-completed `%Workitem{}`
393+ # * each free CPN variable (`:s`, `:x`, …) — plucked from the binding
394+ #
395+ # The wrapper unpacks the binding via `Keyword.fetch!/2` for each declared
396+ # free var and runs the body inside a `Task.Supervisor.start_child/2` call
397+ # (or unsupervised `Task.start/1` when no `:task_supervisor` was provided to
398+ # `use ColouredFlow.DSL`) so the runner never blocks on user side effects.
399+ defp compile_transition_action ( { transition_name , body , cpn_vars } , task_supervisor ) do
400+ var_assignments =
401+ Enum . map ( cpn_vars , fn var ->
402+ var_ast = Macro . var ( var , nil )
403+
404+ quote do
405+ unquote ( var_ast ) = Keyword . fetch! ( var! ( binding ) , unquote ( var ) )
406+ end
407+ end )
408+
409+ task_call = wrap_in_task ( body , task_supervisor )
410+
411+ quote do
412+ defp __action_for__ ( unquote ( transition_name ) , var! ( binding ) , var! ( ctx ) , var! ( workitem ) ) do
413+ _binding = var! ( binding )
414+ _ctx = var! ( ctx )
415+ _workitem = var! ( workitem )
416+ unquote_splicing ( var_assignments )
417+ unquote ( task_call )
418+ end
419+ end
420+ end
421+
422+ defp compile_lifecycle_hook ( { :on_enactment_start , body } , task_supervisor ) do
423+ task_call = wrap_in_task ( body , task_supervisor )
424+
425+ quote do
426+ @ impl ColouredFlow.Runner.ActionHandler
427+ def on_enactment_start ( var! ( ctx ) ) do
428+ _ctx = var! ( ctx )
429+ unquote ( task_call )
430+ :ok
431+ end
432+ end
433+ end
434+
435+ defp compile_lifecycle_hook ( { :on_enactment_terminate , body } , task_supervisor ) do
436+ task_call = wrap_in_task ( body , task_supervisor )
437+
438+ quote do
439+ @ impl ColouredFlow.Runner.ActionHandler
440+ def on_enactment_terminate ( var! ( ctx ) , var! ( reason ) ) do
441+ _ctx = var! ( ctx )
442+ _reason = var! ( reason )
443+ unquote ( task_call )
444+ :ok
445+ end
446+ end
447+ end
448+
449+ defp compile_lifecycle_hook ( { :on_enactment_exception , body } , task_supervisor ) do
450+ task_call = wrap_in_task ( body , task_supervisor )
451+
452+ quote do
453+ @ impl ColouredFlow.Runner.ActionHandler
454+ def on_enactment_exception ( var! ( ctx ) , var! ( reason ) ) do
455+ _ctx = var! ( ctx )
456+ _reason = var! ( reason )
457+ unquote ( task_call )
458+ :ok
459+ end
460+ end
461+ end
462+
463+ defp wrap_in_task ( body , nil ) do
464+ quote do
465+ Task . start ( fn -> unquote ( body ) end )
466+ end
467+ end
468+
469+ defp wrap_in_task ( body , task_supervisor ) do
470+ quote do
471+ Task.Supervisor . start_child ( unquote ( task_supervisor ) , fn -> unquote ( body ) end )
472+ end
473+ end
474+
475+ @ doc false
476+ # credo:disable-for-next-line JetCredo.Checks.ExplicitAnyType
477+ @ spec __setup_flow__! ( module ( ) , module ( ) | nil , String . t ( ) | nil ) :: term ( )
478+ def __setup_flow__! ( module , nil , _name ) do
479+ raise ArgumentError , """
480+ #{ inspect ( module ) } .setup_flow!/0 requires a `:storage` option on `use ColouredFlow.DSL`.
481+
482+ Example:
483+
484+ use ColouredFlow.DSL,
485+ storage: ColouredFlow.Runner.Storage.InMemory
486+
487+ """
488+ end
489+
490+ def __setup_flow__! ( module , _storage , nil ) do
491+ raise ArgumentError , """
492+ #{ inspect ( module ) } .setup_flow!/0 requires a `name "..."` declaration.
493+ """
494+ end
495+
496+ def __setup_flow__! ( module , storage , name ) when is_atom ( storage ) and is_binary ( name ) do
497+ storage . setup_flow! ( name , module . cpnet ( ) )
498+ end
499+
500+ @ doc false
501+ # credo:disable-for-next-line JetCredo.Checks.ExplicitAnyType
502+ @ spec __insert_enactment__! ( module ( ) | nil , term ( ) , [ Marking . t ( ) ] ) :: term ( )
503+ def __insert_enactment__! ( nil , _flow , _initial_markings ) do
504+ raise ArgumentError ,
505+ "insert_enactment!/2 requires a `:storage` option on `use ColouredFlow.DSL`."
506+ end
507+
508+ def __insert_enactment__! ( storage , flow , initial_markings )
509+ when is_atom ( storage ) and is_list ( initial_markings ) do
510+ storage . insert_enactment! ( flow , initial_markings )
511+ end
310512end
0 commit comments