gcode/lib/model/block.ex
James Harton ccf4635cca feat(model,serialise): Implement a basic G-Code model and serialiser.
It's not very thorough at the moment, but it should work for now.
2021-01-04 22:14:40 +13:00

98 lines
3.2 KiB
Elixir

defmodule Gcode.Model.Block do
use Gcode.Option
use Gcode.Result
defstruct words: [], comment: none()
alias Gcode.Model.{Block, Comment, Skip, Word}
@moduledoc """
A sequence of G-code words.
"""
@type t :: %Block{words: [block_contents], comment: Option.t(Comment)}
@typedoc "Any error results in this module will return this type"
@type block_error :: {:block_error, String.t()}
@type block_contents :: Word.t() | Skip.t()
@doc """
Initialise a new empty G-code program.
## Example
iex> Block.init()
%Block{words: [], comment: none()}
"""
@spec init :: t
def init, do: %Block{words: [], comment: none()}
@doc """
Set a comment on the block (this is just a sugar to make sure that the comment
is rendered on the same line as the block).
*Note:* Once a block has a comment set, it cannot be overwritten.
## Examples
iex> comment = Comment.init("Jen, in the swing seat, with her night terrors")
...> block = Block.init()
...> {:ok, block} = Block.comment(block, comment)
...> Result.ok?(block.comment)
true
iex> comment = Comment.init("Jen, in the swing seat, with her night terrors")
...> block = Block.init()
...> {:ok, block} = Block.comment(block, comment)
...> Block.comment(block, comment)
{:error, {:block_error, "Block already contains a comment"}}
"""
@spec comment(t, Comment.t()) :: Result.t(t, block_error)
def comment(%Block{comment: none()} = block, %Comment{comment: some(_)} = comment),
do: {:ok, %Block{block | comment: some(comment)}}
def comment(%Block{comment: some(_)}, _comment),
do: {:error, {:block_error, "Block already contains a comment"}}
@doc """
Pushes a `Word` onto the word list.
*Note:* `Block` stores the words in reverse order because of Erlang list
semantics, you should pretty much always use `words/1` to retrieve them in the
correct order.
## Example
iex> block = Block.init()
...> {:ok, block} = Block.push(block, Word.init("G", 0))
...> Block.push(block, Word.init("N", 100))
{:ok, %Block{words: [Word.init("N", 100), Word.init("G", 0)]}}
"""
@spec push(t, Word.t()) :: Result.t(t, block_error)
def push(%Block{words: words} = block, word)
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words),
do: {:ok, %Block{block | words: [word | words]}}
def push(%Block{words: words}, word)
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words),
do:
{:error,
{:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}}
@doc """
An accessor which returns the block's words in the correct order.
iex> block = Block.init()
...> {:ok, block} = Block.push(block, Word.init("G", 0))
...> {:ok, block} = Block.push(block, Word.init("N", 100))
...> Block.words(block)
{:ok, [Word.init("G", 0), Word.init("N", 100)]}
"""
@spec words(t) :: Result.t([Word.t()], {:block_error, String.t()})
def words(%Block{words: words}) when is_list(words), do: {:ok, Enum.reverse(words)}
def words(%Block{words: words}),
do:
{:error,
{:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}}
end