Skip to content

Commit

Permalink
doc: add the traffic light example (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
fahchen authored Oct 31, 2024
1 parent 33cc98e commit 21cb1f8
Show file tree
Hide file tree
Showing 13 changed files with 1,061 additions and 32 deletions.
680 changes: 680 additions & 0 deletions examples/traffic_light.livemd

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions lib/coloured_flow/definition/colour_set/descr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,46 @@ defmodule ColouredFlow.Definition.ColourSet.Descr do
defp valid?({:list, type}), do: valid?(type)

defp valid?(_descr), do: false

@doc """
Convert the `descr` to quoted expression.
"""
@spec to_quoted(t()) :: Macro.t()
def to_quoted(descr)
# primitive
def to_quoted({:integer, []}), do: {:integer, [], []}
def to_quoted({:float, []}), do: {:float, [], []}
def to_quoted({:boolean, []}), do: {:boolean, [], []}
def to_quoted({:binary, []}), do: {:binary, [], []}
def to_quoted({:unit, []}), do: {:{}, [], []}

# tuple
def to_quoted({:tuple, types}) do
{:{}, [], Enum.map(types, &to_quoted/1)}
end

# map
def to_quoted({:map, types}) do
{:%{}, [], Enum.map(types, fn {key, type} -> {key, to_quoted(type)} end)}
end

# enum
def to_quoted({:enum, items}) do
items
|> Enum.reverse()
|> Enum.reduce(fn item, acc -> {:|, [], [item, acc]} end)
end

# union
def to_quoted({:union, types}) do
types
|> Enum.reverse()
|> Enum.map(fn {tag, type} -> {tag, to_quoted(type)} end)
|> Enum.reduce(fn {tag, quoted}, acc -> {:|, [], [{tag, quoted}, acc]} end)
end

# list
def to_quoted({:list, type}) do
{:list, [], [to_quoted(type)]}
end
end
86 changes: 86 additions & 0 deletions lib/coloured_flow/definition/presentation/presentation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
defmodule ColouredFlow.Definition.Presentation do
@moduledoc """
Generate a graph representation of the petri net.
"""

alias ColouredFlow.Definition.Arc
alias ColouredFlow.Definition.ColourSet
alias ColouredFlow.Definition.ColouredPetriNet
alias ColouredFlow.Definition.Place
alias ColouredFlow.Definition.Transition

@new_line "\n"
@indent " "

@doc """
Generate a [mermaid](https://mermaid.js.org/) representation of the coloured petri net.
"""
@spec to_mermaid(ColouredPetriNet.t()) :: String.t()
def to_mermaid(%ColouredPetriNet{} = cpnet) do
colsets = cpnet.colour_sets |> Enum.sort_by(& &1.name) |> Enum.map(&to_mermaid_colour_set/1)
places = cpnet.places |> Enum.sort_by(& &1.name) |> Enum.map(&to_mermaid_place/1)

transitions =
cpnet.transitions |> Enum.sort_by(& &1.name) |> Enum.map(&to_mermaid_transition/1)

arcs =
cpnet.arcs
|> Enum.sort_by(fn %Arc{} = arc -> {arc.place, arc.orientation, arc.transition} end)
|> Enum.map(&to_mermaid_arc/1)

line_sep = @new_line <> @indent

"""
flowchart TB
#{Enum.join(colsets, line_sep)}
%% places
#{Enum.join(places, line_sep)}
%% transitions
#{Enum.join(transitions, line_sep)}
%% arcs
#{Enum.join(arcs, line_sep)}
"""
end

defp to_mermaid_colour_set(%ColourSet{name: name, type: type}) do
alias ColouredFlow.Definition.ColourSet.Descr

"%% colset #{compose_call(name)} :: #{type |> Descr.to_quoted() |> Macro.to_string()}"
end

defp to_mermaid_place(%Place{name: name, colour_set: colour_set}) do
"#{name}((#{name}<br>:#{colour_set}:))"
end

defp to_mermaid_transition(%Transition{name: name}) do
"#{name}[#{name}]"
end

defp to_mermaid_arc(%Arc{
label: label,
place: place,
transition: transition,
orientation: :p_to_t,
expression: expression
}) do
"#{place} --#{label || expression.code}--> #{transition}"
end

defp to_mermaid_arc(%Arc{
label: label,
place: place,
transition: transition,
orientation: :t_to_p,
expression: expression
}) do
"#{transition} --#{label || expression.code}--> #{place}"
end

@spec compose_call(atom()) :: String.t()
defp compose_call(name) when is_atom(name) do
Macro.to_string({name, [], []})
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,12 @@ defmodule ColouredFlow.Runner.Storage.Schemas.JsonInstance.Codec do

defp do_decode({:struct, module, fields_spec}, value)
when is_atom(module) and is_list(fields_spec) and is_map(value) do
fields_spec = Map.new(fields_spec, fn {key, spec} -> {Atom.to_string(key), {key, spec}} end)
fields_spec =
fields_spec
|> Enum.flat_map(fn {key, spec} ->
[{key, {key, spec}}, {Atom.to_string(key), {key, spec}}]
end)
|> Map.new()

value
|> Enum.map(fn {key, value} ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ defmodule ColouredFlow.Runner.Storage.Schemas.JsonInstance.Codec.Marking do
end)
end

defp decode_tokens(data) when is_struct(data, MultiSet) do
data
end

defp decode_tokens(data) when is_list(data) do
pairs =
Enum.map(data, fn %{"coefficient" => coefficient, "value" => value} ->
Expand Down
6 changes: 2 additions & 4 deletions lib/coloured_flow/runner/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ defmodule ColouredFlow.Runner.Supervisor do
```mermaid
flowchart TB
S["Supervisor"]
S --> DS["Definition Storage"]
S --> EDS["Enactment (Dyn) Supervisor"]
EDS --> E1["Enactment (1)"]
EDS --> EN["Enactment (N)"]
Expand All @@ -15,9 +14,8 @@ defmodule ColouredFlow.Runner.Supervisor do

use Supervisor

# credo:disable-for-next-line JetCredo.Checks.ExplicitAnyType
@spec start_link(term()) :: Supervisor.on_start()
def start_link(init_arg) do
@spec start_link(Keyword.t()) :: Supervisor.on_start()
def start_link(init_arg \\ []) when is_list(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end

Expand Down
50 changes: 37 additions & 13 deletions lib/coloured_flow/runner/worklist/workitem_stream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ defmodule ColouredFlow.Runner.Worklist.WorkitemStream do
The workitem stream in the coloured_flow runner.
"""

alias ColouredFlow.Runner.Enactment.Workitem

alias ColouredFlow.Runner.Storage.Repo
alias ColouredFlow.Runner.Storage.Schemas

Expand All @@ -17,19 +15,25 @@ defmodule ColouredFlow.Runner.Worklist.WorkitemStream do

@typep cursor() :: %{updated_at: NaiveDateTime.t(), id: Schemas.Types.id()}

@live_states ColouredFlow.Runner.Enactment.Workitem.__live_states__()
@default_limit 100

@doc """
Constructs a query to list the live workitems.
## Options
* `:after_cursor` - The cursor to start listing workitems from.
* `:limit` - The maximum number of workitems to list.
"""
@spec live_query(list_options()) :: Ecto.Query.t()
def live_query(options) do
def live_query(options \\ []) when is_list(options) do
options = Keyword.validate!(options, [:after_cursor, :limit])
limit = Keyword.get(options, :limit, @default_limit)
after_cursor = options |> Keyword.get(:after_cursor) |> decode_cursor()

Schemas.Workitem
|> where([w], w.state in ^Workitem.__live_states__())
|> where([w], w.state in ^@live_states)
|> filter_by_cursor(after_cursor)
|> order_by(asc: :updated_at, asc: :id)
|> limit(^limit)
Expand All @@ -38,23 +42,43 @@ defmodule ColouredFlow.Runner.Worklist.WorkitemStream do
@doc """
Lists the live workitems.
## Options
* `:after_cursor` - The cursor to start listing workitems from.
* `:limit` - The maximum number of workitems to list.
## Examples
```elixir
# build a stream to list live workitems
Stream.resource(
fn -> nil end,
fn cursor ->
[after_cursor: cursor]
|> WorkitemStream.live_query()
# filter by enactment_id if needed
# |> where(enactment_id: ^enactment_id)
|> WorkitemStream.list_live()
|> case do
:end_of_stream ->
# simulate a delay for the next iteration
Process.sleep(500)
{[], cursor}
{workitems, cursor} ->
{workitems, cursor}
end
end,
fn _cursor -> :ok end
)
```
"""
@spec list_live(list_options()) :: {[Workitem.t()], cursor} | :end_of_stream
def list_live(options \\ []) do
options
|> live_query()
@spec list_live(Ecto.Queryable.t()) :: {[Schemas.Workitem.t()], cursor} | :end_of_stream
def list_live(queryable) do
queryable
|> Repo.all()
|> then(fn
[] ->
:end_of_stream

workitems ->
cursor = encode_cursor(workitems |> List.last() |> Map.take([:updated_at, :id]))
workitems = Enum.map(workitems, &Schemas.Workitem.to_workitem/1)

{workitems, cursor}
end)
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ defmodule ColouredFlow.MixProject do
{:dialyxir, "~> 1.0", only: [:dev], runtime: false},
{:ex_doc, "~> 0.31", only: :dev, runtime: false},
{:jet_credo, [github: "Byzanteam/jet_credo", only: [:dev, :test], runtime: false]},
{:typed_structor, "~> 0.4"}
{:typed_structor, "~> 0.4"},
{:kino, "~> 0.14.1", only: [:dev, :test]}
]
end

Expand Down
3 changes: 3 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
"ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"fss": {:hex, :fss, "0.1.1", "9db2344dbbb5d555ce442ac7c2f82dd975b605b50d169314a20f08ed21e08642", [:mix], [], "hexpm", "78ad5955c7919c3764065b21144913df7515d52e228c09427a004afe9c1a16b0"},
"jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
"jet_credo": {:git, "https://github.com/Byzanteam/jet_credo.git", "7e5855de2e8b41abfb0a1f5870bbc768a325f4e8", []},
"kino": {:hex, :kino, "0.14.2", "46c5da03f2d62dc119ec5e1c1493f409f08998eac26015ecdfae322ffff46d76", [:mix], [{:fss, "~> 0.1.0", [hex: :fss, repo: "hexpm", optional: false]}, {:nx, "~> 0.1", [hex: :nx, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:table, "~> 0.1.2", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "f54924dd0800ee8b291fe437f942889e90309eb3541739578476f53c1d79c968"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"postgrex": {:hex, :postgrex, "0.19.1", "73b498508b69aded53907fe48a1fee811be34cc720e69ef4ccd568c8715495ea", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8bac7885a18f381e091ec6caf41bda7bb8c77912bb0e9285212829afe5d8a8f8"},
"table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"typed_structor": {:hex, :typed_structor, "0.5.0", "a813fd8bafbff993c0839914c5c32ac0e5c5594ed96f7621813a8831a1991162", [:mix], [], "hexpm", "649cebfeac71c6bf2be2deaa2642af07fb88a1163fa026f84925470ea428004a"},
}
82 changes: 82 additions & 0 deletions test/coloured_flow/definition/colour_set/descr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,93 @@ defmodule ColouredFlow.Definition.ColourSet.DescrTest do
end
end

describe "to_quoted/1" do
test "works" do
assert_to_quoted({:integer, []}, "integer()")
assert_to_quoted({:float, []}, "float()")
assert_to_quoted({:boolean, []}, "boolean()")
assert_to_quoted({:binary, []}, "binary()")
assert_to_quoted({:unit, []}, "{}")
end

test "tuple" do
assert_to_quoted(
{:tuple, [{:integer, []}, {:integer, []}]},
"{integer(), integer()}"
)

assert_to_quoted(
{:tuple, [{:integer, []}, {:integer, []}, {:integer, []}]},
"{integer(), integer(), integer()}"
)
end

test "map" do
assert_to_quoted({:map, %{name: {:binary, []}}}, "%{name: binary()}")

assert_to_quoted(
{:map, %{name: {:binary, []}, age: {:integer, []}}},
"%{name: binary(), age: integer()}"
)
end

test "enum" do
assert_to_quoted({:enum, [:female, :male]}, ":female | :male")
end

test "union" do
assert_to_quoted(
{:union, %{integer: {:integer, []}, unit: {:unit, []}}},
"{:integer, integer()} | {:unit, {}}"
)
end

test "list" do
assert_to_quoted({:list, {:integer, []}}, "list(integer())")
assert_to_quoted({:list, {:list, {:integer, []}}}, "list(list(integer()))")
end

test "complex" do
descr =
{:union,
%{
integer: {:integer, []},
unit: {:unit, []},
list: {:list, {:list, {:integer, []}}},
map: {
:map,
%{
name: {:binary, []},
age: {:integer, []},
list: {:list, {:integer, []}},
enum: {:enum, [:female, :male]}
}
}
}}

assert_to_quoted(
descr,
"""
{:integer, integer()}
| {:list, list(list(integer()))}
| {:unit, {}}
| {:map, %{list: list(integer()), name: binary(), enum: :female | :male, age: integer()}}
"""
)
end
end

defp assert_of_descr(descr) do
assert {:ok, descr} == Descr.of_descr(descr)
end

defp refute_of_descr(descr) do
assert :error == Descr.of_descr(descr)
end

defp assert_to_quoted(descr, expected) do
expected = String.trim(expected)

assert expected === descr |> Descr.to_quoted() |> Macro.to_string()
end
end
Loading

0 comments on commit 21cb1f8

Please sign in to comment.