Current section
Files
Jump to
Current section
Files
lib/tla_connect/emitter.ex
defmodule TlaConnect.Emitter do
@moduledoc """
Approach 3, part 1: NDJSON state emitter.
Writes state transitions as newline-delimited JSON to a file.
Each line is `{"action":"...", ...state}`.
"""
alias TlaConnect.ValidationError
defstruct [:path, :io_device, count: 0, finished: false]
@type t :: %__MODULE__{
path: Path.t(),
io_device: IO.device() | nil,
count: non_neg_integer(),
finished: boolean()
}
@doc "Create a new emitter writing to the given file path."
@spec new(Path.t()) :: t()
def new(file_path) do
{:ok, io} = File.open(file_path, [:write, :utf8])
%__MODULE__{path: file_path, io_device: io}
end
@doc "Emit a state transition as an NDJSON line."
@spec emit(t(), String.t(), map()) :: t()
def emit(%{finished: true}, _action, _state) do
raise ValidationError, message: "Cannot emit after finish()"
end
def emit(_emitter, _action, state) when not is_map(state) or is_struct(state) do
raise ValidationError, message: "State must be a plain map"
end
def emit(emitter, action, state) do
line = Map.put(state, "action", action) |> Jason.encode!()
IO.write(emitter.io_device, line <> "\n")
%{emitter | count: emitter.count + 1}
end
@doc """
Finish emitting. Returns `{count, emitter}` where count is lines written.
No further calls to `emit/3` are allowed.
"""
@spec finish(t()) :: {non_neg_integer(), t()}
def finish(%{finished: true} = emitter), do: {emitter.count, emitter}
def finish(emitter) do
if emitter.io_device, do: File.close(emitter.io_device)
emitter = %{emitter | finished: true, io_device: nil}
{emitter.count, emitter}
end
end