Current section

Files

Jump to
finitomata lib finitomata parser plant_uml.ex
Raw

lib/finitomata/parser/plant_uml.ex

defmodule Finitomata.PlantUML do
@moduledoc false
import NimbleParsec
alias Finitomata.Parser
@behaviour Parser
@alphanumeric [?a..?z, ?A..?Z, ?0..?9, ?_]
blankspace = ignore(ascii_string([?\s], min: 1))
transition_op = string("-->")
event_op = string(":")
event =
ascii_char([?a..?z])
|> optional(ascii_string(@alphanumeric, min: 1))
|> optional(ascii_char([??, ?!]))
|> reduce({IO, :iodata_to_binary, []})
state = choice([string("[*]"), event])
plant_line =
optional(blankspace)
|> concat(state)
|> ignore(blankspace)
|> ignore(transition_op)
|> ignore(blankspace)
|> concat(state)
|> ignore(blankspace)
|> ignore(event_op)
|> ignore(blankspace)
|> concat(event)
|> optional(blankspace)
|> ignore(choice([times(string("\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.PlantUML.transition("state1 --> state2 : succeeded")
iex> result
[transition: ["state1", "state2", "succeeded"]]
iex> {:error, message, _, _, _, _} = Finitomata.PlantUML.transition("state1 --> State2 : succeeded")
iex> String.slice(message, 0..14)
"expected string"
"""
defparsec(:transition, plant_line)
@doc ~S"""
iex> {:ok, result, _, _, _, _} = Finitomata.PlantUML.fsm("s1 --> s2 : ok\ns2 --> [*] : ko")
iex> result
[transition: ["s1", "s2", "ok"], transition: ["s2", "[*]", "ko"]]
"""
defparsec(:fsm, times(choice([plant_line, malformed]), min: 1))
@doc ~S"""
iex> {:ok, result, _, _, _, _} = Finitomata.PlantUML.fsm("s1 --> s2 : ok\ns2 --> [*] : ko")
...> Finitomata.PlantUML.validate(result)
{:error, :initial_state}
iex> {:ok, result, _, _, _, _} = Finitomata.PlantUML.fsm("[*] --> s1 : foo\ns1 --> s2 : ok\ns2 --> [*] : ko")
...> Finitomata.PlantUML.validate(result)
{:ok,
[
%Finitomata.Transition{event: :foo, from: :*, to: :s1},
%Finitomata.Transition{event: :ok, from: :s1, to: :s2},
%Finitomata.Transition{event: :ko, from: :s2, to: :*}
]}
"""
@impl Parser
def validate(parsed, env \\ __ENV__), do: Finitomata.validate(parsed, env)
@doc ~S"""
iex> Finitomata.PlantUML.parse("[*] --> s1 : ok\ns2 --> [*] : ko")
{:error, :orphan_from_state}
iex> Finitomata.PlantUML.parse("[*] --> s1 : foo\ns1 --> s2 : ok\ns2 --> [*] : ko")
{:ok,
[
%Finitomata.Transition{event: :foo, from: :*, to: :s1},
%Finitomata.Transition{event: :ok, from: :s1, to: :s2},
%Finitomata.Transition{event: :ko, from: :s2, to: :*}
]}
"""
@impl Parser
def parse(input, env \\ __ENV__) do
case fsm(input) do
{:ok, result, _, _, _, _} ->
validate(result, 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: "@startuml\n\n" <> input <> "\n@enduml"
@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 --> to : event`"}
end
end