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.
This commit is contained in:
parent
a927113d8a
commit
ccf4635cca
31 changed files with 727 additions and 13 deletions
20
lib/gcode.ex
20
lib/gcode.ex
|
@ -1,18 +1,20 @@
|
||||||
defmodule Gcode do
|
defmodule Gcode do
|
||||||
|
alias Gcode.{Model.Program, Model.Serialise, Result}
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Documentation for `Gcode`.
|
Documentation for `Gcode`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Hello world.
|
Serialise a program to a String.
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> Gcode.hello()
|
|
||||||
:world
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def hello do
|
@spec serialise(Program.t()) :: Result.t(String.t(), {:serialise_error, any})
|
||||||
:world
|
def serialise(%Program{} = program) do
|
||||||
|
program
|
||||||
|
|> Serialise.serialise()
|
||||||
|
|> Result.Enum.map(fn block ->
|
||||||
|
{:ok, "#{block}\r\n"}
|
||||||
|
end)
|
||||||
|
|> Result.Enum.join("")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
98
lib/model/block.ex
Normal file
98
lib/model/block.ex
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
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
|
26
lib/model/block/serialise.ex
Normal file
26
lib/model/block/serialise.ex
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
defimpl Gcode.Model.Serialise, for: Gcode.Model.Block do
|
||||||
|
alias Gcode.{Model.Block, Model.Serialise, Result}
|
||||||
|
use Gcode.Option
|
||||||
|
use Gcode.Result
|
||||||
|
|
||||||
|
@spec serialise(Block.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||||
|
def serialise(%Block{words: words, comment: some(comment)}) do
|
||||||
|
words
|
||||||
|
|> encode_words()
|
||||||
|
|> Result.map(fn words -> ok(["#{words} #{comment}"]) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialise(%Block{words: words, comment: none()}) do
|
||||||
|
words
|
||||||
|
|> encode_words()
|
||||||
|
|> Result.map(fn words -> ok([words]) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp encode_words(words) when is_list(words) do
|
||||||
|
words
|
||||||
|
|> Enum.reverse()
|
||||||
|
|> ok()
|
||||||
|
|> Result.Enum.map(&Serialise.serialise/1)
|
||||||
|
|> Result.Enum.join(" ")
|
||||||
|
end
|
||||||
|
end
|
25
lib/model/comment.ex
Normal file
25
lib/model/comment.ex
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
defmodule Gcode.Model.Comment do
|
||||||
|
alias Gcode.Model.Comment
|
||||||
|
use Gcode.Option
|
||||||
|
defstruct comment: Option.none()
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
A G-code comment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@type t :: %Comment{
|
||||||
|
comment: Option.t(String.t())
|
||||||
|
}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initialise a comment.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> "Doc, in the carpark, with plutonium"
|
||||||
|
...> |> Comment.init()
|
||||||
|
%Comment{comment: some("Doc, in the carpark, with plutonium")}
|
||||||
|
"""
|
||||||
|
@spec init(String.t()) :: t
|
||||||
|
def init(comment) when is_binary(comment), do: %Comment{comment: Option.some(comment)}
|
||||||
|
end
|
16
lib/model/comment/serialise.ex
Normal file
16
lib/model/comment/serialise.ex
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
|
||||||
|
alias Gcode.Model.Comment
|
||||||
|
use Gcode.Option
|
||||||
|
use Gcode.Result
|
||||||
|
|
||||||
|
@spec serialise(Comment.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||||
|
def serialise(%Comment{comment: some(comment)}) do
|
||||||
|
comment
|
||||||
|
|> String.split(~r/(\r\n|\r|\n)/)
|
||||||
|
|> Enum.reject(&(byte_size(&1) == 0))
|
||||||
|
|> Enum.map(&"(#{&1})")
|
||||||
|
|> ok()
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialise(%Comment{comment: none()}), do: {:error, {:serialise_error, :empty_comment}}
|
||||||
|
end
|
42
lib/model/program.ex
Normal file
42
lib/model/program.ex
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
defmodule Gcode.Model.Program do
|
||||||
|
defstruct elements: []
|
||||||
|
alias Gcode.Model.{Block, Program, Tape}
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
A G-code program is the high level object which contains each of the G-code
|
||||||
|
blocks, comments, etc.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> Program.init()
|
||||||
|
...> |> Enum.count()
|
||||||
|
0
|
||||||
|
"""
|
||||||
|
|
||||||
|
@typedoc "A G-code program"
|
||||||
|
@type t :: %Program{
|
||||||
|
elements: [element]
|
||||||
|
}
|
||||||
|
|
||||||
|
@type element :: Block.t() | Comment.t() | Tape.t()
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initialise a new, empty G-code program.
|
||||||
|
|
||||||
|
iex> Program.init()
|
||||||
|
%Program{elements: []}
|
||||||
|
"""
|
||||||
|
@spec init :: t()
|
||||||
|
def init, do: %Program{}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Push a program element onto the end of the program.
|
||||||
|
|
||||||
|
iex> Program.init()
|
||||||
|
...> |> Program.push(Tape.init())
|
||||||
|
{:ok, %Program{elements: [%Tape{}]}}
|
||||||
|
"""
|
||||||
|
@spec push(t, element) :: {:ok, t} | {:error, any}
|
||||||
|
def push(%Program{elements: elements} = program, element),
|
||||||
|
do: {:ok, %Program{program | elements: [element | elements]}}
|
||||||
|
end
|
17
lib/model/program/enumerable.ex
Normal file
17
lib/model/program/enumerable.ex
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
defimpl Enumerable, for: Gcode.Model.Program do
|
||||||
|
alias Gcode.Model.Program
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
@spec count(Program.t()) :: {:ok, non_neg_integer()} | {:error, module}
|
||||||
|
def count(%Program{elements: elements}), do: {:ok, Enum.count(elements)}
|
||||||
|
|
||||||
|
@spec member?(Program.t(), any) :: {:error, module} | {:ok, boolean}
|
||||||
|
def member?(%Program{elements: elements}, element), do: Enumerable.member?(elements, element)
|
||||||
|
|
||||||
|
@spec reduce(Program.t(), Enumerable.acc(), Enumerable.reducer()) :: Enumerable.result()
|
||||||
|
def reduce(%Program{elements: elements}, acc, fun), do: Enumerable.reduce(elements, acc, fun)
|
||||||
|
|
||||||
|
@spec slice(Program.t()) ::
|
||||||
|
{:ok, non_neg_integer, Enumerable.slicing_fun()} | {:error, module}
|
||||||
|
def slice(%Program{elements: elements}), do: Enumerable.slice(elements)
|
||||||
|
end
|
12
lib/model/program/serialise.ex
Normal file
12
lib/model/program/serialise.ex
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
defimpl Gcode.Model.Serialise, for: Gcode.Model.Program do
|
||||||
|
alias Gcode.{Model.Program, Model.Serialise}
|
||||||
|
use Gcode.Result
|
||||||
|
|
||||||
|
@spec serialise(Program.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||||
|
def serialise(%Program{elements: elements}) do
|
||||||
|
with elements <- Enum.reverse(elements),
|
||||||
|
ok(result) <- Result.Enum.map(ok(elements), &Serialise.serialise/1) do
|
||||||
|
ok(List.flatten(result))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
lib/model/serialise.ex
Normal file
10
lib/model/serialise.ex
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
defprotocol Gcode.Model.Serialise do
|
||||||
|
alias Gcode.Result
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
A protocol which is used to serialise the model into a string.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@spec serialise(Serialise.t()) :: Result.t([String.t()])
|
||||||
|
def serialise(value)
|
||||||
|
end
|
36
lib/model/skip.ex
Normal file
36
lib/model/skip.ex
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
defmodule Gcode.Model.Skip do
|
||||||
|
alias Gcode.Model.Skip
|
||||||
|
use Gcode.Option
|
||||||
|
defstruct number: Option.none()
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
A G-code skip.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@type t :: %Skip{
|
||||||
|
number: Option.t(non_neg_integer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initialise a skip with a number.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> 13
|
||||||
|
...> |> Skip.init()
|
||||||
|
%Skip{number: some(13)}
|
||||||
|
"""
|
||||||
|
@spec init(non_neg_integer) :: t
|
||||||
|
def init(number) when is_number(number) and number >= 0, do: %Skip{number: Option.some(number)}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initialise a skip without a number.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> Skip.init()
|
||||||
|
%Skip{number: none()}
|
||||||
|
"""
|
||||||
|
@spec init :: t
|
||||||
|
def init, do: %Skip{}
|
||||||
|
end
|
10
lib/model/skip/serialise.ex
Normal file
10
lib/model/skip/serialise.ex
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
defimpl Gcode.Model.Serialise, for: Gcode.Model.Skip do
|
||||||
|
alias Gcode.{Model.Skip, Result}
|
||||||
|
use Gcode.Option
|
||||||
|
use Gcode.Result
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
@spec serialise(Skip.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||||
|
def serialise(%Skip{number: none()}), do: ok(["/"])
|
||||||
|
def serialise(%Skip{number: some(number)}), do: ok(["/#{number}"])
|
||||||
|
end
|
34
lib/model/tape.ex
Normal file
34
lib/model/tape.ex
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
defmodule Gcode.Model.Tape do
|
||||||
|
defstruct leader: :none
|
||||||
|
alias Gcode.{Model.Tape}
|
||||||
|
use Gcode.Option
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
The tape (`%`) denotes the beginning and end of the program and is not needed
|
||||||
|
by most controllers. Can optionally contain a comment, called a "leader".
|
||||||
|
"""
|
||||||
|
|
||||||
|
@type t :: %Tape{leader: Option.t(String.t())}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initialises a tape command, with no "leader"
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> Tape.init()
|
||||||
|
%Tape{leader: :none}
|
||||||
|
"""
|
||||||
|
@spec init :: t
|
||||||
|
def init, do: %Tape{leader: Option.none()}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initialises a tape command, with a "leader"
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> Tape.init("Marty in the Delorean with the Flux Capacitor")
|
||||||
|
%Tape{leader: {:ok, "Marty in the Delorean with the Flux Capacitor"}}
|
||||||
|
"""
|
||||||
|
@spec init(String.t()) :: t
|
||||||
|
def init(leader) when is_binary(leader), do: %Tape{leader: Option.some(leader)}
|
||||||
|
end
|
10
lib/model/tape/serialise.ex
Normal file
10
lib/model/tape/serialise.ex
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
defimpl Gcode.Model.Serialise, for: Gcode.Model.Tape do
|
||||||
|
alias Gcode.{Model.Tape, Result}
|
||||||
|
use Gcode.Option
|
||||||
|
use Gcode.Result
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
@spec serialise(Tape.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||||
|
def serialise(%Tape{leader: none()}), do: ok(["%"])
|
||||||
|
def serialise(%Tape{leader: some(leader)}), do: ok(["% #{leader}"])
|
||||||
|
end
|
27
lib/model/word.ex
Normal file
27
lib/model/word.ex
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Gcode.Model.Word do
|
||||||
|
alias Gcode.Model.Word
|
||||||
|
use Gcode.Option
|
||||||
|
|
||||||
|
defstruct word: none(), address: none()
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
A G-code word.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@type t :: %Word{
|
||||||
|
word: Option.some(String.t()),
|
||||||
|
address: Option.some(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initialise a word with a command and an address.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
iex> Word.init("G", 0)
|
||||||
|
%Word{word: {:ok, "G"}, address: {:ok, 0}}
|
||||||
|
"""
|
||||||
|
@spec init(String.t(), number) :: t
|
||||||
|
def init(word, address) when is_binary(word) and byte_size(word) == 1 and is_number(address),
|
||||||
|
do: %Word{word: some(word), address: some(address)}
|
||||||
|
end
|
16
lib/model/word/serialise.ex
Normal file
16
lib/model/word/serialise.ex
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
defimpl Gcode.Model.Serialise, for: Gcode.Model.Word do
|
||||||
|
alias Gcode.Model.Word
|
||||||
|
use Gcode.Option
|
||||||
|
use Gcode.Result
|
||||||
|
|
||||||
|
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
|
||||||
|
def serialise(%Word{word: some(word), address: some(address)}) when is_integer(address) do
|
||||||
|
ok(["#{word}#{address}"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialise(%Word{word: some(word), address: some(address)}) when is_float(address) do
|
||||||
|
ok(["#{word}#{address}"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def serialise(_word), do: error({:serialise_error, "invalid word"})
|
||||||
|
end
|
39
lib/option.ex
Normal file
39
lib/option.ex
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
defmodule Gcode.Option do
|
||||||
|
@moduledoc """
|
||||||
|
A helper which represents an optional type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
defmacro __using__(_) do
|
||||||
|
quote do
|
||||||
|
alias Gcode.Option
|
||||||
|
require Gcode.Option
|
||||||
|
import Gcode.Option, only: [some: 1, none: 0]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@type t(value) :: some(value) | opt_none
|
||||||
|
@type some(t) :: {:ok, t}
|
||||||
|
@type opt_none :: :none
|
||||||
|
|
||||||
|
@spec none?(t(any)) :: boolean
|
||||||
|
def none?(:none), do: true
|
||||||
|
def none?({:ok, _}), do: false
|
||||||
|
|
||||||
|
@spec some?(t(any)) :: boolean
|
||||||
|
def some?(:none), do: false
|
||||||
|
def some?({:ok, _}), do: true
|
||||||
|
|
||||||
|
@spec none :: Macro.t()
|
||||||
|
defmacro none do
|
||||||
|
quote do
|
||||||
|
:none
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec some(any) :: Macro.t()
|
||||||
|
defmacro some(pattern) do
|
||||||
|
quote do
|
||||||
|
{:ok, unquote(pattern)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
55
lib/result.ex
Normal file
55
lib/result.ex
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
defmodule Gcode.Result do
|
||||||
|
@moduledoc """
|
||||||
|
A helper which represents a result type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@type t :: t(any, any)
|
||||||
|
@type t(result) :: t(result, any)
|
||||||
|
@type t(result, error) :: ok(result) | error(error)
|
||||||
|
@type ok(result) :: {:ok, result}
|
||||||
|
@type error(error) :: {:error, error}
|
||||||
|
|
||||||
|
defmacro __using__(_) do
|
||||||
|
quote do
|
||||||
|
alias Gcode.Result
|
||||||
|
require Gcode.Result
|
||||||
|
import Gcode.Result, only: [ok: 1, error: 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec ok(any) :: Macro.t()
|
||||||
|
defmacro ok(result) do
|
||||||
|
quote do
|
||||||
|
{:ok, unquote(result)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec error(any) :: Macro.t()
|
||||||
|
defmacro error(error) do
|
||||||
|
quote do
|
||||||
|
{:error, unquote(error)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec ok?(t) :: boolean
|
||||||
|
def ok?({:ok, _}), do: true
|
||||||
|
def ok?({:error, _}), do: false
|
||||||
|
|
||||||
|
@spec error?(t) :: boolean
|
||||||
|
def error?({:ok, _}), do: false
|
||||||
|
def error?({:error, _}), do: true
|
||||||
|
|
||||||
|
@spec unwrap!(t) :: any | no_return
|
||||||
|
def unwrap!({:ok, result}), do: result
|
||||||
|
def unwrap!({:error, error}), do: raise(error)
|
||||||
|
|
||||||
|
@spec map(t, (any -> t)) :: t
|
||||||
|
def map({:ok, value}, mapper) when is_function(mapper, 1) do
|
||||||
|
case mapper.(value) do
|
||||||
|
{:ok, value} -> {:ok, value}
|
||||||
|
{:error, error} -> {:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def map({:error, value}, mapper) when is_function(mapper, 1), do: {:error, value}
|
||||||
|
end
|
44
lib/result/enum.ex
Normal file
44
lib/result/enum.ex
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
defmodule Gcode.Result.Enum do
|
||||||
|
@moduledoc false
|
||||||
|
use Gcode.Result
|
||||||
|
|
||||||
|
@type result :: Result.t()
|
||||||
|
@type result(result) :: Result.t(result)
|
||||||
|
@type result(result, error) :: Result.t(result, error)
|
||||||
|
|
||||||
|
@doc "As long as the result of the reducer is ok, continue reducing, otherwise short circuit"
|
||||||
|
@spec reduce_while_ok(
|
||||||
|
enumerable :: any,
|
||||||
|
accumulator :: any,
|
||||||
|
reducer :: (any, any -> result(any))
|
||||||
|
) :: result(any)
|
||||||
|
def reduce_while_ok(elements, acc, reducer) when is_function(reducer, 2) do
|
||||||
|
Enum.reduce_while(elements, {:ok, acc}, fn element, {:ok, acc} ->
|
||||||
|
case reducer.(element, acc) do
|
||||||
|
{:ok, acc} -> {:cont, {:ok, acc}}
|
||||||
|
{:error, reason} -> {:halt, {:error, reason}}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec map(result([any]), mapper :: (any -> result(any))) :: result([any])
|
||||||
|
def map({:ok, enumerable}, mapper) when is_function(mapper, 1) do
|
||||||
|
reduce_while_ok(enumerable, [], fn element, acc ->
|
||||||
|
case mapper.(element) do
|
||||||
|
{:ok, mapped} -> {:ok, [mapped | acc]}
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> reverse()
|
||||||
|
end
|
||||||
|
|
||||||
|
def map({:error, reason}, _mapper), do: {:error, reason}
|
||||||
|
|
||||||
|
@spec reverse(result([any])) :: result([any])
|
||||||
|
def reverse({:ok, enumerable}), do: {:ok, Enum.reverse(enumerable)}
|
||||||
|
def reverse({:error, reason}), do: {:error, reason}
|
||||||
|
|
||||||
|
@spec join(result([String.t()]), String.t()) :: result(String.t())
|
||||||
|
def join({:ok, strings}, joiner) when is_binary(joiner), do: {:ok, Enum.join(strings, joiner)}
|
||||||
|
def join({:error, reason}, _joiner), do: {:error, reason}
|
||||||
|
end
|
3
mix.exs
3
mix.exs
|
@ -14,7 +14,8 @@ defmodule Gcode.MixProject do
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
package: package(),
|
package: package(),
|
||||||
description: @description,
|
description: @description,
|
||||||
deps: deps()
|
deps: deps(),
|
||||||
|
consolidate_protocols: Mix.env() != :test
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,21 @@
|
||||||
defmodule GcodeTest do
|
defmodule GcodeTest do
|
||||||
use ExUnit.Case
|
use ExUnit.Case, async: true
|
||||||
|
use Gcode.Result
|
||||||
|
alias Gcode.Model.{Comment, Program, Tape}
|
||||||
doctest Gcode
|
doctest Gcode
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
test "greets the world" do
|
describe "serialise/1" do
|
||||||
assert Gcode.hello() == :world
|
test "it serialises a program correctly" do
|
||||||
|
program = Program.init()
|
||||||
|
ok(program) = Program.push(program, Tape.init())
|
||||||
|
ok(program) = Program.push(program, Comment.init("I am a very simple program"))
|
||||||
|
ok(program) = Program.push(program, Tape.init())
|
||||||
|
ok(actual) = Gcode.serialise(program)
|
||||||
|
|
||||||
|
expected = "%\r\n(I am a very simple program)\r\n%\r\n"
|
||||||
|
|
||||||
|
assert actual == expected
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
24
test/model/block/serialise_test.exs
Normal file
24
test/model/block/serialise_test.exs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
defmodule Gcode.Model.Block.SerialiseTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.{Block, Serialise, Word}
|
||||||
|
use Gcode.Result
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
describe "serialise/1" do
|
||||||
|
assert ok(block) =
|
||||||
|
with(
|
||||||
|
block <- Block.init(),
|
||||||
|
word <- Word.init("G", 0),
|
||||||
|
ok(block) <- Block.push(block, word),
|
||||||
|
word <- Word.init("N", 100),
|
||||||
|
ok(block) <- Block.push(block, word),
|
||||||
|
do: ok(block)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ok([actual]) = Serialise.serialise(block)
|
||||||
|
|
||||||
|
expected = "G0 N100"
|
||||||
|
|
||||||
|
assert actual == expected
|
||||||
|
end
|
||||||
|
end
|
8
test/model/block_test.exs
Normal file
8
test/model/block_test.exs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
defmodule Gcode.Model.BlockTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.{Block, Comment, Word}
|
||||||
|
use Gcode.Option
|
||||||
|
use Gcode.Result
|
||||||
|
doctest Gcode.Model.Block
|
||||||
|
@moduledoc false
|
||||||
|
end
|
23
test/model/comment/serialise_test.exs
Normal file
23
test/model/comment/serialise_test.exs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
defmodule Gcode.Model.Comment.SerialiseTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.{Comment, Serialise}
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
describe "serialise/1" do
|
||||||
|
test "each line of the comment is wrapped in brackets" do
|
||||||
|
{:ok, actual} =
|
||||||
|
"""
|
||||||
|
This
|
||||||
|
is
|
||||||
|
a
|
||||||
|
test
|
||||||
|
"""
|
||||||
|
|> Comment.init()
|
||||||
|
|> Serialise.serialise()
|
||||||
|
|
||||||
|
expected = ~w[(This) (is) (a) (test)]
|
||||||
|
|
||||||
|
assert actual == expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
7
test/model/comment_test.exs
Normal file
7
test/model/comment_test.exs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
defmodule Gcode.Model.CommentTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.Comment
|
||||||
|
use Gcode.Option
|
||||||
|
doctest Gcode.Model.Comment
|
||||||
|
@moduledoc false
|
||||||
|
end
|
51
test/model/program/serialise_test.exs
Normal file
51
test/model/program/serialise_test.exs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
defmodule Gcode.Model.Program.SerialiseTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
use Gcode.Result
|
||||||
|
alias Gcode.Model.{Block, Comment, Program, Serialise, Skip, Tape, Word}
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
describe "serialise/1" do
|
||||||
|
test "it formats correctly" do
|
||||||
|
actual =
|
||||||
|
with program <- Program.init(),
|
||||||
|
tape <- Tape.init("The beginning"),
|
||||||
|
ok(program) <- Program.push(program, tape),
|
||||||
|
comment <- Comment.init("I am a single line comment"),
|
||||||
|
ok(program) <- Program.push(program, comment),
|
||||||
|
block <- Block.init(),
|
||||||
|
word <- Word.init("G", 0),
|
||||||
|
ok(block) <- Block.push(block, word),
|
||||||
|
word <- Word.init("N", 100),
|
||||||
|
ok(block) <- Block.push(block, word),
|
||||||
|
ok(program) <- Program.push(program, block),
|
||||||
|
comment <- Comment.init("I\nam\na\nmultiline\ncomment"),
|
||||||
|
ok(program) <- Program.push(program, comment),
|
||||||
|
block <- Block.init(),
|
||||||
|
skip <- Skip.init(),
|
||||||
|
ok(block) <- Block.push(block, skip),
|
||||||
|
word <- Word.init("N", 200),
|
||||||
|
ok(block) <- Block.push(block, word),
|
||||||
|
ok(program) <- Program.push(program, block),
|
||||||
|
tape <- Tape.init("The end"),
|
||||||
|
ok(program) <- Program.push(program, tape),
|
||||||
|
ok(result) <- Serialise.serialise(program) do
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
"% The beginning",
|
||||||
|
"(I am a single line comment)",
|
||||||
|
"G0 N100",
|
||||||
|
"(I)",
|
||||||
|
"(am)",
|
||||||
|
"(a)",
|
||||||
|
"(multiline)",
|
||||||
|
"(comment)",
|
||||||
|
"/ N200",
|
||||||
|
"% The end"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert actual == expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
5
test/model/program_test.exs
Normal file
5
test/model/program_test.exs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
defmodule Gcode.Model.ProgramTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.{Program, Tape}
|
||||||
|
doctest Gcode.Model.Program
|
||||||
|
end
|
27
test/model/skip/serialise_test.exs
Normal file
27
test/model/skip/serialise_test.exs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Gcode.Model.Skip.SerialiseTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.{Serialise, Skip}
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
describe "serialise/1" do
|
||||||
|
test "when the skip has a number, it formats it correctly" do
|
||||||
|
{:ok, actual} =
|
||||||
|
Skip.init(0)
|
||||||
|
|> Serialise.serialise()
|
||||||
|
|
||||||
|
expected = ~w[/0]
|
||||||
|
|
||||||
|
assert actual == expected
|
||||||
|
end
|
||||||
|
|
||||||
|
test "when the skip has no number, it formats it correctly" do
|
||||||
|
{:ok, actual} =
|
||||||
|
Skip.init()
|
||||||
|
|> Serialise.serialise()
|
||||||
|
|
||||||
|
expected = ~w[/]
|
||||||
|
|
||||||
|
assert actual == expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
7
test/model/skip_test.exs
Normal file
7
test/model/skip_test.exs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
defmodule Gcode.Model.SkipTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.Skip
|
||||||
|
use Gcode.Option
|
||||||
|
doctest Gcode.Model.Skip
|
||||||
|
@moduledoc false
|
||||||
|
end
|
5
test/model/tape_test.exs
Normal file
5
test/model/tape_test.exs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
defmodule Gcode.Model.TapeTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.Tape
|
||||||
|
doctest Gcode.Model.Tape
|
||||||
|
end
|
18
test/model/word/serialise_test.exs
Normal file
18
test/model/word/serialise_test.exs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule Gcode.Model.Word.SerialiseTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.{Serialise, Word}
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
describe "serialise/1" do
|
||||||
|
test "formats the word and the address correctly" do
|
||||||
|
{:ok, actual} =
|
||||||
|
"G"
|
||||||
|
|> Word.init(0)
|
||||||
|
|> Serialise.serialise()
|
||||||
|
|
||||||
|
expected = ~w[G0]
|
||||||
|
|
||||||
|
assert actual == expected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
6
test/model/word_test.exs
Normal file
6
test/model/word_test.exs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
defmodule Gcode.Model.WordTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Gcode.Model.Word
|
||||||
|
doctest Gcode.Model.Word
|
||||||
|
@moduledoc false
|
||||||
|
end
|
Loading…
Reference in a new issue