Current section
Files
Jump to
Current section
Files
lib/ash_computer.ex
defmodule AshComputer do
@moduledoc """
Spark-powered DSL that wraps the `Computer` library with an Ash-style interface.
Use this module to declare computers inside an Elixir module. Each computer definition
is translated into helper functions that build the underlying `Computer` struct and
optionally spawn instances or run events that mutate it.
"""
use Spark.Dsl,
default_extensions: [extensions: [AshComputer.Dsl]]
alias AshComputer.Builder
alias AshComputer.Info
@doc "Computer names declared in this module."
def computers(module) do
Info.computer_names(module)
end
@doc "Build a computer spec for the default computer."
def computer_spec(module) do
name = Info.default_computer_name(module)
computer_spec(module, name)
end
@doc "Build a computer spec by name."
def computer_spec(module, name) do
case Info.computer(module, name) do
nil ->
raise ArgumentError,
"Unknown computer #{inspect(name)} for #{inspect(module)}. " <>
"Known computers: #{inspect(Info.computer_names(module))}"
definition ->
builder = Builder.build_builder(definition, module)
builder.()
end
end
@doc "Get event names for a computer."
def events(module, name \\ nil) do
name = name || Info.default_computer_name(module)
case Info.computer(module, name) do
nil ->
raise ArgumentError,
"Unknown computer #{inspect(name)} for #{inspect(module)}. " <>
"Known computers: #{inspect(Info.computer_names(module))}"
definition ->
Enum.map(definition.events, & &1.name)
end
end
@doc "Apply an event to an executor."
def apply_event(module, event_name, executor, payload \\ nil) do
name = Info.default_computer_name(module)
do_apply_event(module, name, event_name, executor, payload)
end
def apply_event(module, name, event_name, executor, payload) do
do_apply_event(module, name, event_name, executor, payload)
end
defp do_apply_event(module, computer_name, event_name, executor, payload) do
case Info.computer(module, computer_name) do
nil ->
raise ArgumentError,
"Unknown computer #{inspect(computer_name)} for #{inspect(module)}. " <>
"Known computers: #{inspect(Info.computer_names(module))}"
definition ->
event = Enum.find(definition.events, &(&1.name == event_name))
unless event do
known_events = Enum.map(definition.events, & &1.name)
raise ArgumentError,
"Unknown event #{inspect(event_name)} for #{inspect(computer_name)} in #{inspect(module)}. " <>
"Known events: #{inspect(known_events)}"
end
apply_event_handler(event.handle, executor, computer_name, payload, event_name)
end
end
defp apply_event_handler(handler, executor, computer_name, payload, event_name) do
values = AshComputer.Executor.current_values(executor, computer_name)
changes =
cond do
is_function(handler, 1) ->
handler.(values)
is_function(handler, 2) ->
handler.(values, payload)
true ->
raise ArgumentError,
"Event #{inspect(event_name)} for #{inspect(computer_name)} expects a capture of arity 1 or 2, got: #{inspect(handler)}"
end
unless is_map(changes) do
raise ArgumentError,
"Event #{inspect(event_name)} for #{inspect(computer_name)} must return a map of input changes, got: #{inspect(changes)}"
end
computer = executor.computers[computer_name]
invalid_keys = Map.keys(changes) -- Map.keys(computer.inputs)
unless invalid_keys == [] do
raise ArgumentError,
"Event #{inspect(event_name)} for #{inspect(computer_name)} tried to modify non-input values: #{inspect(invalid_keys)}. " <>
"Only inputs can be modified. Available inputs: #{inspect(Map.keys(computer.inputs))}"
end
executor
|> AshComputer.Executor.start_frame()
|> apply_changes(computer_name, changes)
|> AshComputer.Executor.commit_frame()
end
defp apply_changes(executor, computer_name, changes) do
Enum.reduce(changes, executor, fn {input_name, value}, acc ->
AshComputer.Executor.set_input(acc, computer_name, input_name, value)
end)
end
end