Packages
finitomata
0.29.2
0.41.0
0.40.0
0.35.0
0.34.0
0.33.0
0.32.0
0.31.1
0.30.3
0.30.2
0.30.1
0.30.0
0.29.10
0.29.9
0.29.8
0.29.7
0.29.6
0.29.5
0.29.4
0.29.3
0.29.2
0.29.1
0.29.0
0.28.1
0.28.0
0.27.1
0.27.0
0.26.4
0.26.3
0.26.2
0.26.1
0.26.0
0.25.0
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.7
0.23.6
0.23.5
0.23.4
0.23.3
0.23.2
0.23.1
0.23.0
0.22.1
0.22.0
0.21.4
0.21.3
0.21.2
0.21.1
0.21.0
0.20.2
0.20.1
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.0
0.15.1
0.15.0
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.0
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.1
0.1.0
The FSM implementation generated from PlantUML textual representation.
Current section
Files
Jump to
Current section
Files
lib/finitomata/parser/mermaid.ex
defmodule Finitomata.Mermaid do
@moduledoc false
import NimbleParsec
alias Finitomata.Parser
@behaviour Parser
@alphanumeric [?a..?z, ?A..?Z, ?0..?9, ?_]
blankspace = ignore(ascii_string([?\s], min: 1))
semicolon = ignore(string(";"))
transition_op = string("-->")
identifier =
ascii_char([?a..?z])
|> optional(ascii_string(@alphanumeric, min: 1))
|> optional(ascii_char([??, ?!]))
|> reduce({IO, :iodata_to_binary, []})
state = identifier
target_states = state |> concat(ignore(string(","))) |> times(min: 0) |> concat(state)
event = ignore(string("|")) |> concat(identifier) |> ignore(string("|"))
mermaid_line =
optional(blankspace)
|> concat(state)
|> ignore(blankspace)
|> ignore(transition_op)
|> ignore(blankspace)
|> concat(event)
|> ignore(blankspace)
|> concat(target_states)
|> optional(blankspace)
|> optional(semicolon)
|> ignore(choice([times(string("\n"), min: 1), times(string("\r\n"), min: 1), eos()]))
|> tag(:transition)
malformed =
optional(utf8_string([not: ?\n], min: 1))
|> string("\n")
|> pre_traverse(:abort)
@doc ~S"""
iex> {:ok, result, _, _, _, _} = Finitomata.Mermaid.transition("state1 --> |succeeded| state2")
iex> result
[transition: ["state1", "succeeded", "state2"]]
iex> {:ok, result, _, _, _, _} = Finitomata.Mermaid.transition("state1 --> |succeeded| state2,state3")
iex> result
[transition: ["state1", "succeeded", "state2", "state3"]]
iex> {:error, message, _, _, _, _} = Finitomata.Mermaid.transition("state1 --> |succeeded| State2")
iex> String.slice(message, 0..13)
"expected ASCII"
"""
defparsec(:transition, mermaid_line)
@doc ~S"""
iex> Finitomata.Mermaid.transitions("state1 --> |succeeded| state2,state3")
[transition: ["state1", "succeeded", "state2"], transition: ["state1", "succeeded", "state3"]]
"""
def transitions({:transition, [from, event | tos]}) do
for to <- tos, do: {:transition, [from, event, to]}
end
def transitions(mermaid_line) do
with {:ok, [{:transition, _ts} = transitions], _, _, _, _} <- transition(mermaid_line) do
transitions(transitions)
end
end
@doc ~S"""
iex> "state1 --> |succeeded| state2,state3" |> Finitomata.Mermaid.transitions() |> Enum.map(&Finitomata.Mermaid.reshape_transition/1)
[transition: ["state1", "state2", "succeeded"], transition: ["state1", "state3", "succeeded"]]
"""
def reshape_transition({:transition, [from, event, to]}),
do: {:transition, reshape_transition([from, event, to])}
def reshape_transition([from, event, to]),
do: [from, to, event]
@doc ~S"""
iex> {:ok, result, _, _, _, _} = Finitomata.Mermaid.do_fsm("s1 --> |ok| s2;\ns2 --> |ko| s3")
iex> result
[transition: ["s1", "ok", "s2"], transition: ["s2", "ko", "s3"]]
"""
defparsec(:do_fsm, times(choice([mermaid_line, malformed]), min: 1))
@doc ~S"""
iex> {:ok, result, _, _, _, _} = Finitomata.Mermaid.fsm("s1 --> |ok| s2;\ns2 --> |ko| s3")
iex> result
[transition: ["s1", "ok", "s2"], transition: ["s2", "ko", "s3"]]
"""
def fsm(input) when is_binary(input) do
with {:ok, result, rest, opts, pos, count} <- do_fsm(input),
do: {:ok, Enum.flat_map(result, &transitions/1), rest, opts, pos, count}
end
@doc ~S"""
iex> {:ok, result, _, _, _, _} = Finitomata.Mermaid.fsm("s1 --> |ok| s2\ns2 --> |ko| s3")
...> result |> Enum.map(&Finitomata.Mermaid.reshape_transition/1) |> Finitomata.Mermaid.validate()
{:ok,
[
%Finitomata.Transition{event: :__start__, from: :*, to: :s1},
%Finitomata.Transition{event: :ok, from: :s1, to: :s2},
%Finitomata.Transition{event: :ko, from: :s2, to: :s3},
%Finitomata.Transition{event: :__end__, from: :s3, to: :*}
]}
"""
@impl Parser
def validate(parsed, env \\ __ENV__) do
from_states = parsed |> Enum.map(fn {:transition, [from, _, _]} -> from end) |> Enum.uniq()
to_states = parsed |> Enum.map(fn {:transition, [_, to, _]} -> to end) |> Enum.uniq()
start_states =
Enum.map(from_states -- to_states, fn from -> {:transition, ["[*]", from, "__start__"]} end)
final_states =
Enum.map(to_states -- from_states, fn to -> {:transition, [to, "[*]", "__end__"]} end)
amended = start_states ++ parsed ++ final_states
Finitomata.validate(amended, env)
end
@doc ~S"""
iex> Finitomata.Mermaid.parse("s1 --> |ok| s2\ns2 --> |ko| s3")
{:ok,
[
%Finitomata.Transition{event: :__start__, from: :*, to: :s1},
%Finitomata.Transition{event: :ok, from: :s1, to: :s2},
%Finitomata.Transition{event: :ko, from: :s2, to: :s3},
%Finitomata.Transition{event: :__end__, from: :s3, to: :*}
]}
iex> Finitomata.Mermaid.parse("s1 --> |ok| s2\ns2 --> |ko| s2,s3")
{:ok,
[
%Finitomata.Transition{event: :__start__, from: :*, to: :s1},
%Finitomata.Transition{event: :ok, from: :s1, to: :s2},
%Finitomata.Transition{event: :ko, from: :s2, to: :s2},
%Finitomata.Transition{event: :ko, from: :s2, to: :s3},
%Finitomata.Transition{event: :__end__, from: :s3, to: :*}
]}
"""
@impl Parser
def parse(input, env \\ __ENV__) do
case fsm(input) do
{:ok, result, _, _, _, _} ->
result
|> Enum.map(&reshape_transition/1)
|> validate(env)
{:error, "[line: " <> _ = msg, _rest, context, _, _} ->
[numbers, msg] = String.split(msg, "|||")
{numbers, []} = Code.eval_string(numbers)
{:error, msg, numbers[:content], context, {env.file, env.line, 0}, numbers[:offset]}
error ->
error
end
end
@impl Parser
def lint(input) when is_binary(input) do
case parse(input) do
{:ok, transitions} ->
["graph TD" | transitions] |> Enum.reject(&is_nil/1) |> Enum.map_join("\n ", &dump/1)
{:error, error} ->
"‹ERROR› " <> inspect(error)
end
end
@spec dump(Finitomata.Transition.t() | binary()) :: String.t()
defp dump(text) when is_binary(text), do: text
defp dump(%Finitomata.Transition{from: :*}),
do: nil
defp dump(%Finitomata.Transition{to: :*}),
do: nil
defp dump(%Finitomata.Transition{from: from, to: to, event: event}),
do: "#{from} --> |#{event}| #{to}"
@spec abort(
String.t(),
[String.t()],
map(),
{non_neg_integer, non_neg_integer},
non_neg_integer
) :: {:error, binary()}
defp abort(rest, content, _context, {line, column}, offset) do
content = content |> Enum.reverse() |> Enum.join() |> String.trim()
meta = inspect(line: line, column: column, offset: offset, rest: rest, content: content)
{:error,
meta <>
"|||malformed FSM transition (line: #{line}, column: #{column - offset}), expected `from --> |event| to`"}
end
end