fix(model): Change all model init functions to return a result.

This commit is contained in:
James Harton 2021-01-05 08:42:43 +13:00
parent e45c18ea58
commit 5b273e4356
13 changed files with 159 additions and 83 deletions

View file

@ -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}),

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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)
)

View file

@ -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)]

View file

@ -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

View file

@ -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

View file

@ -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[/]

View file

@ -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]