fix(model): Change all model init functions to return a result.
This commit is contained in:
parent
e45c18ea58
commit
5b273e4356
13 changed files with 159 additions and 83 deletions
|
@ -19,10 +19,10 @@ defmodule Gcode.Model.Block do
|
|||
## Example
|
||||
|
||||
iex> Block.init()
|
||||
%Block{words: [], comment: none()}
|
||||
{:ok, %Block{words: [], comment: none()}}
|
||||
"""
|
||||
@spec init :: t
|
||||
def init, do: %Block{words: [], comment: none()}
|
||||
@spec init :: Result.t(t)
|
||||
def init, do: ok(%Block{words: [], comment: none()})
|
||||
|
||||
@doc """
|
||||
Set a comment on the block (this is just a sugar to make sure that the comment
|
||||
|
@ -32,14 +32,14 @@ defmodule Gcode.Model.Block do
|
|||
|
||||
## Examples
|
||||
|
||||
iex> comment = Comment.init("Jen, in the swing seat, with her night terrors")
|
||||
...> block = Block.init()
|
||||
iex> {:ok, comment} = Comment.init("Jen, in the swing seat, with her night terrors")
|
||||
...> {:ok, 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()
|
||||
iex> {:ok, comment} = Comment.init("Jen, in the swing seat, with her night terrors")
|
||||
...> {:ok, block} = Block.init()
|
||||
...> {:ok, block} = Block.comment(block, comment)
|
||||
...> Block.comment(block, comment)
|
||||
{:error, {:block_error, "Block already contains a comment"}}
|
||||
|
@ -61,12 +61,14 @@ defmodule Gcode.Model.Block do
|
|||
|
||||
## 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)]}}
|
||||
iex> {:ok, block} = Block.init()
|
||||
...> {:ok, word} = Word.init("G", 0)
|
||||
...> {:ok, block} = Block.push(block, word)
|
||||
...> {:ok, word} = Word.init("N", 100)
|
||||
...> Block.push(block, word)
|
||||
{:ok, %Block{words: [%Word{word: some("N"), address: some(100)}, %Word{word: some("G"), address: some(0)}]}}
|
||||
"""
|
||||
@spec push(t, Word.t()) :: Result.t(t, block_error)
|
||||
@spec push(t, block_contents) :: 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]}}
|
||||
|
@ -81,13 +83,15 @@ defmodule Gcode.Model.Block do
|
|||
@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))
|
||||
iex> {:ok, block} = Block.init()
|
||||
...> {:ok, word} = Word.init("G", 0)
|
||||
...> {:ok, block} = Block.push(block, word)
|
||||
...> {:ok, word} = Word.init("N", 100)
|
||||
...> {:ok, block} = Block.push(block, word)
|
||||
...> Block.words(block)
|
||||
{:ok, [Word.init("G", 0), Word.init("N", 100)]}
|
||||
{:ok, [%Word{word: some("G"), address: some(0)}, %Word{word: some("N"), address: some(100)}]}
|
||||
"""
|
||||
@spec words(t) :: Result.t([Word.t()], {:block_error, String.t()})
|
||||
@spec words(t) :: Result.t([Word.t()], block_error)
|
||||
def words(%Block{words: words}) when is_list(words), do: {:ok, Enum.reverse(words)}
|
||||
|
||||
def words(%Block{words: words}),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
defmodule Gcode.Model.Comment do
|
||||
alias Gcode.Model.Comment
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
defstruct comment: Option.none()
|
||||
|
||||
@moduledoc """
|
||||
|
@ -11,6 +12,8 @@ defmodule Gcode.Model.Comment do
|
|||
comment: Option.t(String.t())
|
||||
}
|
||||
|
||||
@type error :: {:comment_error, String.t()}
|
||||
|
||||
@doc """
|
||||
Initialise a comment.
|
||||
|
||||
|
@ -18,8 +21,24 @@ defmodule Gcode.Model.Comment do
|
|||
|
||||
iex> "Doc, in the carpark, with plutonium"
|
||||
...> |> Comment.init()
|
||||
%Comment{comment: some("Doc, in the carpark, with plutonium")}
|
||||
{:ok, %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)}
|
||||
@spec init(String.t()) :: Result.t(t, error)
|
||||
def init(comment) when is_binary(comment) do
|
||||
if String.printable?(comment) do
|
||||
ok(%Comment{comment: some(comment)})
|
||||
else
|
||||
error(
|
||||
{:comment_error,
|
||||
"Expected comment should be a valid UTF-8 string, received #{inspect(comment)}"}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def init(comment),
|
||||
do:
|
||||
error(
|
||||
{:comment_error,
|
||||
"Expected comment should be a valid UTF-8 string, received #{inspect(comment)}"}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
defmodule Gcode.Model.Program do
|
||||
defstruct elements: []
|
||||
alias Gcode.Model.{Block, Program, Tape}
|
||||
alias Gcode.Model.{Block, Comment, Program, Skip, Tape}
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
A G-code program is the high level object which contains each of the G-code
|
||||
|
@ -9,6 +10,7 @@ defmodule Gcode.Model.Program do
|
|||
## Example
|
||||
|
||||
iex> Program.init()
|
||||
...> |> Result.unwrap!()
|
||||
...> |> Enum.count()
|
||||
0
|
||||
"""
|
||||
|
@ -18,25 +20,39 @@ defmodule Gcode.Model.Program do
|
|||
elements: [element]
|
||||
}
|
||||
|
||||
@type element :: Block.t() | Comment.t() | Tape.t()
|
||||
@type element :: Block.t() | Comment.t() | Skip.t() | Tape.t()
|
||||
@type error :: {:program_error, String.t()}
|
||||
|
||||
@doc """
|
||||
Initialise a new, empty G-code program.
|
||||
|
||||
iex> Program.init()
|
||||
%Program{elements: []}
|
||||
{:ok, %Program{elements: []}}
|
||||
"""
|
||||
@spec init :: t()
|
||||
def init, do: %Program{}
|
||||
@spec init :: Result.t(t())
|
||||
def init, do: ok(%Program{})
|
||||
|
||||
@doc """
|
||||
Push a program element onto the end of the program.
|
||||
|
||||
iex> Program.init()
|
||||
...> |> Program.push(Tape.init())
|
||||
iex> {:ok, program} = Program.init()
|
||||
...> {:ok, tape} = Tape.init()
|
||||
...> Program.push(program, tape)
|
||||
{: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]}}
|
||||
@spec push(t, element) :: Result.t(t, error)
|
||||
def push(%Program{elements: elements} = program, element)
|
||||
when is_list(elements) and
|
||||
(is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Skip) or
|
||||
is_struct(element, Tape)),
|
||||
do: ok(%Program{program | elements: [element | elements]})
|
||||
|
||||
def push(%Program{elements: elements}, _element) when not is_list(elements),
|
||||
do: error({:program_error, "Program elements is not a list"})
|
||||
|
||||
def push(%Program{}, element),
|
||||
do: error({:program_error, "Expected a valid program element, received #{inspect(element)}"})
|
||||
|
||||
def push(program, _element),
|
||||
do: error({:program_error, "Expected a valid program, received #{inspect(program)}"})
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
defmodule Gcode.Model.Skip do
|
||||
alias Gcode.Model.Skip
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
defstruct number: Option.none()
|
||||
|
||||
@moduledoc """
|
||||
|
@ -11,6 +12,8 @@ defmodule Gcode.Model.Skip do
|
|||
number: Option.t(non_neg_integer)
|
||||
}
|
||||
|
||||
@type error :: {:skip_error, String.t()}
|
||||
|
||||
@doc """
|
||||
Initialise a skip with a number.
|
||||
|
||||
|
@ -18,10 +21,14 @@ defmodule Gcode.Model.Skip do
|
|||
|
||||
iex> 13
|
||||
...> |> Skip.init()
|
||||
%Skip{number: some(13)}
|
||||
{:ok, %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)}
|
||||
@spec init(non_neg_integer) :: Result.t(t, error)
|
||||
def init(number) when is_integer(number) and number >= 0,
|
||||
do: ok(%Skip{number: Option.some(number)})
|
||||
|
||||
def init(number),
|
||||
do: error({:skip_error, "Expected a positive integer, received #{inspect(number)}"})
|
||||
|
||||
@doc """
|
||||
Initialise a skip without a number.
|
||||
|
@ -29,8 +36,8 @@ defmodule Gcode.Model.Skip do
|
|||
## Example
|
||||
|
||||
iex> Skip.init()
|
||||
%Skip{number: none()}
|
||||
{:ok, %Skip{number: none()}}
|
||||
"""
|
||||
@spec init :: t
|
||||
def init, do: %Skip{}
|
||||
@spec init :: Result.t(t)
|
||||
def init, do: ok(%Skip{})
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule Gcode.Model.Tape do
|
|||
defstruct leader: :none
|
||||
alias Gcode.{Model.Tape}
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
@moduledoc """
|
||||
The tape (`%`) denotes the beginning and end of the program and is not needed
|
||||
|
@ -9,6 +10,7 @@ defmodule Gcode.Model.Tape do
|
|||
"""
|
||||
|
||||
@type t :: %Tape{leader: Option.t(String.t())}
|
||||
@type error :: {:tape_error, String.t()}
|
||||
|
||||
@doc """
|
||||
Initialises a tape command, with no "leader"
|
||||
|
@ -16,10 +18,10 @@ defmodule Gcode.Model.Tape do
|
|||
## Example
|
||||
|
||||
iex> Tape.init()
|
||||
%Tape{leader: :none}
|
||||
{:ok, %Tape{leader: :none}}
|
||||
"""
|
||||
@spec init :: t
|
||||
def init, do: %Tape{leader: Option.none()}
|
||||
@spec init :: Result.t(t)
|
||||
def init, do: ok(%Tape{leader: Option.none()})
|
||||
|
||||
@doc """
|
||||
Initialises a tape command, with a "leader"
|
||||
|
@ -27,8 +29,22 @@ defmodule Gcode.Model.Tape do
|
|||
## Example
|
||||
|
||||
iex> Tape.init("Marty in the Delorean with the Flux Capacitor")
|
||||
%Tape{leader: {:ok, "Marty in the Delorean with the Flux Capacitor"}}
|
||||
{:ok, %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)}
|
||||
@spec init(String.t()) :: Result.t(t, error)
|
||||
def init(leader) when is_binary(leader) do
|
||||
if String.printable?(leader) do
|
||||
ok(%Tape{leader: some(leader)})
|
||||
else
|
||||
error(
|
||||
{:tape_error, "Expected leader to be a valid UTF-8 string, recevied #{inspect(leader)}"}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def init(leader),
|
||||
do:
|
||||
error(
|
||||
{:tape_error, "Expected leader to be a valid UTF-8 string, recevied #{inspect(leader)}"}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
defmodule Gcode.Model.Word do
|
||||
alias Gcode.Model.Word
|
||||
use Gcode.Option
|
||||
use Gcode.Result
|
||||
|
||||
defstruct word: none(), address: none()
|
||||
|
||||
|
@ -19,9 +20,20 @@ defmodule Gcode.Model.Word do
|
|||
## Example
|
||||
|
||||
iex> Word.init("G", 0)
|
||||
%Word{word: {:ok, "G"}, address: {:ok, 0}}
|
||||
{:ok, %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)}
|
||||
@spec init(String.t(), number) :: Result.t(t)
|
||||
def init(word, address) when is_binary(word) and is_number(address) do
|
||||
if Regex.match?(~r/^[A-Z]$/, word) do
|
||||
ok(%Word{word: some(word), address: some(address)})
|
||||
else
|
||||
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
|
||||
end
|
||||
end
|
||||
|
||||
def init(word, address) when is_number(address),
|
||||
do: error({:word_error, "Expected word to be a string, received #{inspect(word)}"})
|
||||
|
||||
def init(_word, address),
|
||||
do: error({:word_error, "Expected address to be a number, received #{inspect(address)}"})
|
||||
end
|
||||
|
|
|
@ -7,10 +7,13 @@ defmodule GcodeTest do
|
|||
|
||||
describe "serialise/1" do
|
||||
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(program) = Program.init()
|
||||
ok(tape) = Tape.init()
|
||||
ok(program) = Program.push(program, tape)
|
||||
ok(comment) = Comment.init("I am a very simple program")
|
||||
ok(program) = Program.push(program, comment)
|
||||
ok(tape) = Tape.init()
|
||||
ok(program) = Program.push(program, tape)
|
||||
ok(actual) = Gcode.serialise(program)
|
||||
|
||||
expected = "%\r\n(I am a very simple program)\r\n%\r\n"
|
||||
|
|
|
@ -7,10 +7,10 @@ defmodule Gcode.Model.Block.SerialiseTest do
|
|||
describe "serialise/1" do
|
||||
assert ok(block) =
|
||||
with(
|
||||
block <- Block.init(),
|
||||
word <- Word.init("G", 0),
|
||||
ok(block) <- Block.init(),
|
||||
ok(word) <- Word.init("G", 0),
|
||||
ok(block) <- Block.push(block, word),
|
||||
word <- Word.init("N", 100),
|
||||
ok(word) <- Word.init("N", 100),
|
||||
ok(block) <- Block.push(block, word),
|
||||
do: ok(block)
|
||||
)
|
||||
|
|
|
@ -5,15 +5,17 @@ defmodule Gcode.Model.Comment.SerialiseTest do
|
|||
|
||||
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()
|
||||
comment = """
|
||||
This
|
||||
is
|
||||
a
|
||||
test
|
||||
"""
|
||||
|
||||
actual =
|
||||
with {:ok, comment} <- Comment.init(comment),
|
||||
{:ok, comment} <- Serialise.serialise(comment),
|
||||
do: comment
|
||||
|
||||
expected = ~w[(This) (is) (a) (test)]
|
||||
|
||||
|
|
|
@ -7,26 +7,26 @@ defmodule Gcode.Model.Program.SerialiseTest do
|
|||
describe "serialise/1" do
|
||||
test "it formats correctly" do
|
||||
actual =
|
||||
with program <- Program.init(),
|
||||
tape <- Tape.init("The beginning"),
|
||||
with ok(program) <- Program.init(),
|
||||
ok(tape) <- Tape.init("The beginning"),
|
||||
ok(program) <- Program.push(program, tape),
|
||||
comment <- Comment.init("I am a single line comment"),
|
||||
ok(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.init(),
|
||||
ok(word) <- Word.init("G", 0),
|
||||
ok(block) <- Block.push(block, word),
|
||||
word <- Word.init("N", 100),
|
||||
ok(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(comment) <- Comment.init("I\nam\na\nmultiline\ncomment"),
|
||||
ok(program) <- Program.push(program, comment),
|
||||
block <- Block.init(),
|
||||
skip <- Skip.init(),
|
||||
ok(block) <- Block.init(),
|
||||
ok(skip) <- Skip.init(),
|
||||
ok(block) <- Block.push(block, skip),
|
||||
word <- Word.init("N", 200),
|
||||
ok(word) <- Word.init("N", 200),
|
||||
ok(block) <- Block.push(block, word),
|
||||
ok(program) <- Program.push(program, block),
|
||||
tape <- Tape.init("The end"),
|
||||
ok(tape) <- Tape.init("The end"),
|
||||
ok(program) <- Program.push(program, tape),
|
||||
ok(result) <- Serialise.serialise(program) do
|
||||
result
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
defmodule Gcode.Model.ProgramTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias Gcode.Model.{Program, Tape}
|
||||
use Gcode.Result
|
||||
doctest Gcode.Model.Program
|
||||
@moduledoc false
|
||||
end
|
||||
|
|
|
@ -5,9 +5,8 @@ defmodule Gcode.Model.Skip.SerialiseTest do
|
|||
|
||||
describe "serialise/1" do
|
||||
test "when the skip has a number, it formats it correctly" do
|
||||
{:ok, actual} =
|
||||
Skip.init(0)
|
||||
|> Serialise.serialise()
|
||||
actual =
|
||||
with {:ok, skip} <- Skip.init(0), {:ok, skip} <- Serialise.serialise(skip), do: skip
|
||||
|
||||
expected = ~w[/0]
|
||||
|
||||
|
@ -15,9 +14,7 @@ defmodule Gcode.Model.Skip.SerialiseTest do
|
|||
end
|
||||
|
||||
test "when the skip has no number, it formats it correctly" do
|
||||
{:ok, actual} =
|
||||
Skip.init()
|
||||
|> Serialise.serialise()
|
||||
actual = with {:ok, skip} <- Skip.init(), {:ok, skip} <- Serialise.serialise(skip), do: skip
|
||||
|
||||
expected = ~w[/]
|
||||
|
||||
|
|
|
@ -5,10 +5,8 @@ defmodule Gcode.Model.Word.SerialiseTest do
|
|||
|
||||
describe "serialise/1" do
|
||||
test "formats the word and the address correctly" do
|
||||
{:ok, actual} =
|
||||
"G"
|
||||
|> Word.init(0)
|
||||
|> Serialise.serialise()
|
||||
actual =
|
||||
with {:ok, word} <- Word.init("G", 0), {:ok, word} <- Serialise.serialise(word), do: word
|
||||
|
||||
expected = ~w[G0]
|
||||
|
||||
|
|
Loading…
Reference in a new issue