You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
# lib/coloured_flow/runtime/module_resolver.exdefmoduleColouredFlow.Runtime.ModuleResolverdo@moduledoc""" Behaviour for module resolution. Implement this behaviour to provide custom module loading strategies. The resolver is configured externally and passed to enactment as MFA. """aliasColouredFlow.Definition.ModulealiasColouredFlow.Runtime.ModuleReference@typedoc""" Context for module resolution. This is initialized once by the application and passed as part of MFA. Contains configuration like database repo, cache settings, etc. """@typecontext()::term()@typedoc""" Runtime information from enactment. This is provided at resolution time and contains dynamic information like enactment_id, current state, etc. """@typeruntime_info()::%{optional(:enactment_id)=>term(),optional(:parent_transition)=>binary(),optional(atom())=>term()}@typemodule_ref()::ModuleReference.t()@doc""" Resolves a module reference to a concrete Module. ## Parameters - `module_ref`: The module reference to resolve - `context`: Resolver context (initialized externally) - `runtime_info`: Runtime information from enactment ## Returns - `{:ok, module}`: Successfully resolved module - `{:error, reason}`: Failed to resolve ## Examples # Called by enactment via MFA @impl true def resolve(module_ref, context, runtime_info) do # Use context for configuration (repo, cache, etc.) # Use runtime_info for dynamic decisions {:ok, module} end """@callbackresolve(module_ref(),context(),runtime_info())::{:ok,Module.t()}|{:error,term()}end
2. MFA Type Definition
# lib/coloured_flow/runtime/module_resolver.exdefmoduleColouredFlow.Runtime.ModuleResolverdo# ... behaviour definition ...@typedoc""" Module resolver configuration. The tuple contains: - module: Module implementing ModuleResolver behaviour - args: List containing [context] (pre-initialized) Since the behaviour defines resolve/3, the function name is implicit. When calling: apply(module, :resolve, [module_ref] ++ args ++ [runtime_info]) """@typeresolver()::{module(),[context()]}@doc""" Helper to create resolver configuration from module and context. ## Examples context = MyResolver.init(repo: MyApp.Repo) resolver = ModuleResolver.new(MyResolver, context) # Returns: {MyResolver, [context]} """@specnew(module(),context())::resolver()defnew(resolver_module,context)whenis_atom(resolver_module)do{resolver_module,[context]}end@doc""" Call resolver with module reference and runtime info. ## Examples resolver = {MyResolver, [context]} runtime_info = %{enactment_id: 123} {:ok, module} = ModuleResolver.resolve(resolver, module_ref, runtime_info) """@specresolve(resolver(),module_ref(),runtime_info())::{:ok,Module.t()}|{:error,term()}defresolve({resolver_module,[context]},module_ref,runtime_info)doapply(resolver_module,:resolve,[module_ref,context,runtime_info])endend
3. Default Resolver Implementation
# lib/coloured_flow/runtime/module_resolver/default.exdefmoduleColouredFlow.Runtime.ModuleResolver.Defaultdo@moduledoc""" Default module resolver implementation. Loads modules from the database using FlowConverter. """@behaviourColouredFlow.Runtime.ModuleResolveraliasColouredFlow.Builder.FlowConverteraliasColouredFlow.Runner.Storage.Schemas@typedoc""" Context for default resolver. """@typecontext()::%{repo: module(),cache: boolean(),cache_store: map()}@doc""" Initialize resolver context. ## Options - `:repo` (required) - Ecto repo for loading flows - `:cache` (optional) - Enable caching (default: false) ## Examples context = Default.init(repo: MyApp.Repo) mfa = ModuleResolver.to_mfa(Default, context) """@specinit(Keyword.t())::context()definit(opts)dorepo=Keyword.fetch!(opts,:repo)%{repo: repo,cache: Keyword.get(opts,:cache,false),cache_store: %{}}end@implColouredFlow.Runtime.ModuleResolverdefresolve(module_ref,context,runtime_info)# Resolve by flow_id with port_specsdefresolve({:module_ref,opts},context,runtime_info)whenis_list(opts)doconddoKeyword.has_key?(opts,:flow_id)andKeyword.has_key?(opts,:port_specs)->flow_id=Keyword.fetch!(opts,:flow_id)port_specs=Keyword.fetch!(opts,:port_specs)# Can use runtime_info to customize module namemodule_name=build_module_name("flow_#{flow_id}",runtime_info)load_from_flow_id(flow_id,module_name,port_specs,context)Keyword.has_key?(opts,:flow_id)andKeyword.has_key?(opts,:module_name)->flow_id=Keyword.fetch!(opts,:flow_id)module_name=Keyword.fetch!(opts,:module_name)load_from_flow_id_auto(flow_id,module_name,context)Keyword.has_key?(opts,:flow_name)andKeyword.has_key?(opts,:port_specs)->flow_name=Keyword.fetch!(opts,:flow_name)port_specs=Keyword.fetch!(opts,:port_specs)module_name=build_module_name(flow_name,runtime_info)load_from_flow_name(flow_name,module_name,port_specs,context)Keyword.has_key?(opts,:flow_name)andKeyword.has_key?(opts,:module_name)->flow_name=Keyword.fetch!(opts,:flow_name)module_name=Keyword.fetch!(opts,:module_name)load_from_flow_name_auto(flow_name,module_name,context)true->{:error,{:invalid_module_ref,"Unknown module reference format"}}endenddefpbuild_module_name(base_name,runtime_info)do# Can incorporate enactment_id to create unique module namescaseMap.get(runtime_info,:enactment_id)donil->"#{base_name}_module"enactment_id->"#{base_name}_enactment_#{enactment_id}_module"endenddefpload_from_flow_id(flow_id,module_name,port_specs,context)dowith{:ok,flow}<-fetch_flow_by_id(flow_id,context),{:ok,module}<-convert_flow_to_module(flow,module_name,port_specs)do{:ok,module}endenddefpload_from_flow_id_auto(flow_id,module_name,context)dowith{:ok,flow}<-fetch_flow_by_id(flow_id,context)domodule=FlowConverter.flow_to_module_auto(flow.definition,module_name){:ok,module}endenddefpload_from_flow_name(flow_name,module_name,port_specs,context)dowith{:ok,flow}<-fetch_flow_by_name(flow_name,context),{:ok,module}<-convert_flow_to_module(flow,module_name,port_specs)do{:ok,module}endenddefpload_from_flow_name_auto(flow_name,module_name,context)dowith{:ok,flow}<-fetch_flow_by_name(flow_name,context)domodule=FlowConverter.flow_to_module_auto(flow.definition,module_name){:ok,module}endenddefpfetch_flow_by_id(flow_id,%{repo: repo})docaserepo.get(Schemas.Flow,flow_id)donil->{:error,{:flow_not_found,flow_id}}flow->{:ok,flow}endenddefpfetch_flow_by_name(flow_name,%{repo: repo})doimportEcto.Queryquery=fromfinSchemas.Flow,where: f.name==^flow_namecaserepo.one(query)donil->{:error,{:flow_not_found,flow_name}}flow->{:ok,flow}endenddefpconvert_flow_to_module(flow,module_name,port_specs)domodule=FlowConverter.flow_to_module(flow.definition,name: module_name,port_specs: port_specs){:ok,module}rescueerror->{:error,{:conversion_failed,error}}endend
4. Usage in Enactment
# When starting an enactment# 1. Application level: Initialize resolverresolver_context=ModuleResolver.Default.init(repo: MyApp.Repo)resolver=ModuleResolver.new(ModuleResolver.Default,resolver_context)# 2. Pass to enactment{:ok,enactment_pid}=Enactment.start_link(cpnet: cpnet,resolver: resolver,# ... other options)# 3. Inside Enactment GenServerdefmoduleColouredFlow.Runner.Enactmentdodefinit(opts)dostate=%{cpnet: opts[:cpnet],resolver: opts[:resolver],enactment_id: generate_id(),# ...}{:ok,state}end# When a substitution transition firesdefpfire_substitution_transition(transition,state)doifis_module_ref?(transition.subst)doruntime_info=%{enactment_id: state.enactment_id,parent_transition: transition.name}caseModuleResolver.resolve(state.resolver,transition.subst,runtime_info)do{:ok,module}-># Use the resolved moduleexecute_module(module,...){:error,reason}->{:error,{:module_resolution_failed,reason}}endelse# Static module referencemodule=find_module_in_cpnet(state.cpnet,transition.subst)execute_module(module,...)endendend
Complete Example
# 1. Application startupdefmoduleMyApp.Applicationdodefstart(_type,_args)do# Initialize resolverresolver_context=ModuleResolver.Default.init(repo: MyApp.Repo)resolver=ModuleResolver.new(ModuleResolver.Default,resolver_context)# Store in application env or pass to supervisorApplication.put_env(:my_app,:module_resolver,resolver)# Start supervisors...endend# 2. Create CPN with module referencecpnet=%ColouredPetriNet{places: [...],transitions: [%Transition{name: "authenticate",subst: {:module_ref,flow_id: 123,port_specs: [{"credentials",:input},{"success",:output},{"failure",:output}]},socket_assignments: [...]}]}# 3. Start enactment with resolverresolver=Application.get_env(:my_app,:module_resolver){:ok,enactment_pid}=Enactment.start_link(cpnet: cpnet,resolver: resolver,initial_marking: [...])# 4. When transition fires, enactment calls:# ModuleResolver.resolve(# {ModuleResolver.Default, [resolver_context]},# {:module_ref, flow_id: 123, port_specs: [...]},# %{enactment_id: "abc123", parent_transition: "authenticate"}# )# Which internally does:# apply(ModuleResolver.Default, :resolve, [# {:module_ref, flow_id: 123, port_specs: [...]},# resolver_context,# %{enactment_id: "abc123", parent_transition: "authenticate"}# ])
Benefits
Lazy Resolution: Modules resolved only when needed (at transition fire time)
Runtime Context: Resolver can use enactment info for decisions
No Global State: Resolver configured per enactment
Testable: Easy to mock MFA in tests
Flexible: Can use different resolvers for different enactments
Key Design Points
Behaviour signature: resolve/3 with module_ref, context, runtime_info
Third parameter: runtime_info with enactment context
Resolver format: {Module, [context]} - function name implicit from behaviour
Helper functions: new/2 and resolve/3 for convenience
Lazy loading: Resolution happens at runtime, not upfront
No redundancy: Function name omitted since behaviour defines it