feat(parse_transition): first version of parse_transition, with tests
This commit is contained in:
9 files changed
+342
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
-- supports requiring .moon files directly
|
||||||
|
require "moonscript".register!
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
require "src.util.parse_transition"
|
||||||
|
|
||||||
|
describe "parse_transition", ->
|
||||||
|
|
||||||
|
-- ### Valid Cases ###
|
||||||
|
it "parses a valid 'on' transition", ->
|
||||||
|
result = parse_transition "stateA on eventX"
|
||||||
|
assert.same {target: "stateA", event: "eventX"}, result
|
||||||
|
|
||||||
|
it "parses a valid 'after' transition", ->
|
||||||
|
result = parse_transition "stateB after 3.14"
|
||||||
|
assert.same {target: "stateB", after: 3.14}, result
|
||||||
|
|
||||||
|
it "parses a valid 'every' transition", ->
|
||||||
|
result = parse_transition "stateC every 42"
|
||||||
|
assert.same {target: "stateC", every: 42}, result
|
||||||
|
|
||||||
|
-- ### Basic Error Cases ###
|
||||||
|
it "errors when input is not a string", ->
|
||||||
|
assert.has_error (-> parse_transition 123), "Transition must be a string"
|
||||||
|
|
||||||
|
it "errors when input does not have exactly three words", ->
|
||||||
|
assert.has_error (-> parse_transition "two words"), "Transition must consist of three words"
|
||||||
|
|
||||||
|
it "errors when transition type is unknown", ->
|
||||||
|
assert.has_error (-> parse_transition "stateD unknown value"), "Unknown transition type"
|
||||||
|
|
||||||
|
it "errors when number conversion fails for 'after' transition", ->
|
||||||
|
assert.has_error (-> parse_transition "stateE after not_a_number"), "Invalid time value in 'after' transition type"
|
||||||
|
|
||||||
|
it "errors when number conversion fails for 'every' transition", ->
|
||||||
|
assert.has_error (-> parse_transition "stateF every not_a_number"), "Invalid time value in 'every' transition type"
|
||||||
|
|
||||||
|
-- ### Additional Tests for Nil and Bad Numbers ###
|
||||||
|
it "errors when nil is passed as transition", ->
|
||||||
|
assert.has_error (-> parse_transition nil), "Transition must be a string"
|
||||||
|
|
||||||
|
it "errors for a negative number in an 'after' transition", ->
|
||||||
|
-- Assuming negative times are invalid.
|
||||||
|
assert.has_error (-> parse_transition "stateNeg after -5"), "Invalid time value in 'after' transition type"
|
||||||
|
|
||||||
|
it "errors for a negative number in an 'every' transition", ->
|
||||||
|
assert.has_error (-> parse_transition "stateNeg every -5"), "Invalid time value in 'every' transition type"
|
||||||
|
|
||||||
|
it "errors for zero in an 'after' transition", ->
|
||||||
|
-- Zero is considered invalid.
|
||||||
|
assert.has_error (-> parse_transition "stateZero after 0"), "Invalid time value in 'after' transition type"
|
||||||
|
|
||||||
|
it "errors for zero in an 'every' transition", ->
|
||||||
|
assert.has_error (-> parse_transition "stateZero every 0"), "Invalid time value in 'every' transition type"
|
||||||
|
|
||||||
|
it "errors when the state name contains a space", ->
|
||||||
|
-- e.g. "state A" is not a valid Lua identifier.
|
||||||
|
assert.has_error (-> parse_transition "state A on eventX"), "Transition must consist of three words"
|
||||||
|
|
||||||
|
it "errors when the event name contains a space", ->
|
||||||
|
-- e.g. "event X" is not a valid Lua identifier.
|
||||||
|
assert.has_error (-> parse_transition "stateA on event X"), "Transition must consist of three words"
|
||||||
|
|
||||||
|
it "errors when the state name starts with a number", ->
|
||||||
|
-- Lua identifiers cannot start with a digit.
|
||||||
|
assert.has_error (-> parse_transition "1state on eventX"), "Invalid state name"
|
||||||
|
|
||||||
|
it "errors when the event name contains an invalid character", ->
|
||||||
|
-- For example, a dash is not allowed.
|
||||||
|
assert.has_error (-> parse_transition "stateA on event-X"), "Invalid event name"
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
require "src.util.string"
|
||||||
|
|
||||||
|
describe "string.split", ->
|
||||||
|
it "returns an empty table when given an empty string", ->
|
||||||
|
result = split ""
|
||||||
|
assert.is_table result
|
||||||
|
assert.are.equal 0, #result
|
||||||
|
|
||||||
|
it "returns an empty table when given a string containing only whitespace", ->
|
||||||
|
result = split " \t \n"
|
||||||
|
assert.is_table result
|
||||||
|
assert.are.equal 0, #result
|
||||||
|
|
||||||
|
it "splits a single word correctly", ->
|
||||||
|
result = split "hello"
|
||||||
|
assert.are.same { "hello" }, result
|
||||||
|
|
||||||
|
it "splits two words separated by a single space", ->
|
||||||
|
result = split "hello world"
|
||||||
|
assert.are.same { "hello", "world" }, result
|
||||||
|
|
||||||
|
it "handles multiple spaces between words", ->
|
||||||
|
result = split "hello world"
|
||||||
|
assert.are.same { "hello", "world" }, result
|
||||||
|
|
||||||
|
it "handles leading and trailing whitespace", ->
|
||||||
|
result = split " hello world "
|
||||||
|
assert.are.same { "hello", "world" }, result
|
||||||
|
|
||||||
|
it "handles tabs and newline characters as whitespace", ->
|
||||||
|
result = split "hello\tworld\nLua"
|
||||||
|
assert.are.same { "hello", "world", "Lua" }, result
|
||||||
|
|
||||||
|
it "treats punctuation as part of the word", ->
|
||||||
|
result = split "hello, world!"
|
||||||
|
assert.are.same { "hello,", "world!" }, result
|
||||||
|
|
||||||
|
it "splits a string with various whitespace characters", ->
|
||||||
|
result = split " one \t two\n three \n\t four"
|
||||||
|
assert.are.same { "one", "two", "three", "four" }, result
|
||||||
|
|
||||||
|
it "returns a new table on each call", ->
|
||||||
|
result1 = split "a b c"
|
||||||
|
result2 = split "a b c"
|
||||||
|
assert.not_equal result1, result2
|
||||||
|
|
||||||
|
it "errors when the input is not a string", ->
|
||||||
|
assert.has_error => split nil
|
||||||
|
assert.has_error => split 123
|
||||||
|
|
||||||
|
it "does not modify the input string", ->
|
||||||
|
s = " immutable test "
|
||||||
|
original = s
|
||||||
|
split s
|
||||||
|
assert.are.equal original, s
|
||||||
|
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
require "src.util.validators"
|
||||||
|
|
||||||
|
describe "validators", ->
|
||||||
|
describe "is_valid_timer", ->
|
||||||
|
it "returns falsy when passed nil", ->
|
||||||
|
result = is_valid_timer nil
|
||||||
|
-- nil is falsy in Lua.
|
||||||
|
assert.falsy result
|
||||||
|
|
||||||
|
it "returns false when passed false", ->
|
||||||
|
result = is_valid_timer false
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false when passed 0", ->
|
||||||
|
result = is_valid_timer 0
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false when passed a negative number", ->
|
||||||
|
result = is_valid_timer -1
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns true when passed a positive integer", ->
|
||||||
|
result = is_valid_timer 1
|
||||||
|
assert.is_true result
|
||||||
|
|
||||||
|
it "returns true when passed a positive decimal", ->
|
||||||
|
result = is_valid_timer 3.14
|
||||||
|
assert.is_true result
|
||||||
|
|
||||||
|
it "errors when passed a non-number (string)", ->
|
||||||
|
assert.has_error => is_valid_timer "10"
|
||||||
|
|
||||||
|
it "errors when passed a non-number (boolean true)", ->
|
||||||
|
assert.has_error => is_valid_timer true
|
||||||
|
|
||||||
|
it "errors when passed a non-number (table)", ->
|
||||||
|
assert.has_error => is_valid_timer { foo: "bar" }
|
||||||
|
|
||||||
|
describe "is_valid_symbol", ->
|
||||||
|
-- When the input is not a string, it should immediately return false.
|
||||||
|
it "returns false when passed nil", ->
|
||||||
|
result = is_valid_symbol nil
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false when passed a number", ->
|
||||||
|
result = is_valid_symbol 123
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false when passed a table", ->
|
||||||
|
result = is_valid_symbol { }
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false when passed a boolean", ->
|
||||||
|
result = is_valid_symbol false
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
-- When the input is a string:
|
||||||
|
it "returns false for an empty string", ->
|
||||||
|
result = is_valid_symbol ""
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns true for a single lowercase letter", ->
|
||||||
|
result = is_valid_symbol "a"
|
||||||
|
assert.is_true result
|
||||||
|
|
||||||
|
it "returns true for a single uppercase letter", ->
|
||||||
|
result = is_valid_symbol "Z"
|
||||||
|
assert.is_true result
|
||||||
|
|
||||||
|
it "returns true for a string of only letters", ->
|
||||||
|
result = is_valid_symbol "HelloWorld"
|
||||||
|
assert.is_true result
|
||||||
|
|
||||||
|
it "returns true for a string with mixed case letters", ->
|
||||||
|
result = is_valid_symbol "AbCdEf"
|
||||||
|
assert.is_true result
|
||||||
|
|
||||||
|
it "returns false for a string with numbers", ->
|
||||||
|
result = is_valid_symbol "abc123"
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false for a string with punctuation", ->
|
||||||
|
result = is_valid_symbol "hello!"
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false for a string with an underscore", ->
|
||||||
|
result = is_valid_symbol "a_b"
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false for a string containing whitespace", ->
|
||||||
|
result = is_valid_symbol "abc def"
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false for a string with leading/trailing whitespace", ->
|
||||||
|
result = is_valid_symbol " abc "
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false for a string with non-ASCII letters", ->
|
||||||
|
result = is_valid_symbol "éclair"
|
||||||
|
assert.is_false result
|
||||||
|
|
||||||
|
it "returns false for a string with a hyphen", ->
|
||||||
|
result = is_valid_symbol "A-B"
|
||||||
|
assert.is_false result
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
-- Recursively prints the contents of a table in a human-readable format.
|
||||||
|
--
|
||||||
|
-- This function traverses the provided table `tbl` and prints each key-value
|
||||||
|
-- pair to the standard output. For nested tables, it calls itself recursively,
|
||||||
|
-- increasing the indentation level to visually represent the table hierarchy.
|
||||||
|
--
|
||||||
|
-- @param tbl table: The table whose contents are to be printed.
|
||||||
|
-- @param indent number: (Optional) The current indentation level. Defaults to 0.
|
||||||
|
-- @return nil
|
||||||
|
export print_table = (tbl, indent) ->
|
||||||
|
-- Use the “?=” operator to give a default value.
|
||||||
|
unless indent
|
||||||
|
indent = 0
|
||||||
|
-- Omit the extra “do” after the loop header.
|
||||||
|
for k, v in pairs tbl
|
||||||
|
formatting = string.rep(" ", indent) .. k .. ": "
|
||||||
|
if type(v) == "table"
|
||||||
|
print formatting
|
||||||
|
print_table v, indent + 1
|
||||||
|
elseif type(v) == "boolean"
|
||||||
|
print formatting .. tostring(v)
|
||||||
|
else
|
||||||
|
print formatting .. v
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
require "src.util.string"
|
||||||
|
require "src.util.validators"
|
||||||
|
|
||||||
|
---
|
||||||
|
-- parse_transition
|
||||||
|
-- Parses a transition string into a table with:
|
||||||
|
-- • target: the target state name,
|
||||||
|
-- • type: "after" | "every" | "on",
|
||||||
|
-- • time: number (if timer-based),
|
||||||
|
-- • event: string (if event-based).
|
||||||
|
export parse_transition = (trans) ->
|
||||||
|
unless type(trans) == "string"
|
||||||
|
error "Transition must be a string"
|
||||||
|
|
||||||
|
words = split trans
|
||||||
|
unless #words == 3
|
||||||
|
error "Transition must consist of three words"
|
||||||
|
|
||||||
|
unless is_valid_symbol words[1]
|
||||||
|
error "Invalid state name"
|
||||||
|
|
||||||
|
switch words[2]
|
||||||
|
when "on"
|
||||||
|
unless is_valid_symbol words[3]
|
||||||
|
error "Invalid event name"
|
||||||
|
{target: words[1], event: words[3]}
|
||||||
|
|
||||||
|
when "after"
|
||||||
|
num = tonumber words[3]
|
||||||
|
unless is_valid_timer num
|
||||||
|
error "Invalid time value in 'after' transition type"
|
||||||
|
{target: words[1], after: num}
|
||||||
|
|
||||||
|
when "every"
|
||||||
|
num = tonumber words[3]
|
||||||
|
unless is_valid_timer num
|
||||||
|
error "Invalid time value in 'every' transition type"
|
||||||
|
{target: words[1], every: num}
|
||||||
|
|
||||||
|
else
|
||||||
|
error "Unknown transition type"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
-- Splits the input string into an array of non-whitespace substrings.
|
||||||
|
--
|
||||||
|
-- This function uses Lua's pattern matching to iterate over the provided
|
||||||
|
-- string `s` and collects each contiguous sequence of non-whitespace
|
||||||
|
-- characters into a new table. If the input is not a string, the function
|
||||||
|
-- raises an error.
|
||||||
|
--
|
||||||
|
-- @param s string: The string to be split. Must be a valid string.
|
||||||
|
-- @return table: An array of substrings obtained by splitting `s` on whitespace.
|
||||||
|
-- @raise error if the input parameter `s` is not a string.
|
||||||
|
export split = (s) ->
|
||||||
|
unless type(s) == "string"
|
||||||
|
error "split() requires a string parameter"
|
||||||
|
chunks = {}
|
||||||
|
for substr in string.gmatch s, '%S+'
|
||||||
|
table.insert chunks, substr
|
||||||
|
chunks
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
-- Determines if the provided timer value is valid.
|
||||||
|
--
|
||||||
|
-- A valid timer is defined as a number greater than zero.
|
||||||
|
--
|
||||||
|
-- @param x number: The timer value to validate.
|
||||||
|
-- @return boolean: Returns `true` if `x` is a positive number; otherwise, returns `false` or `nil`.
|
||||||
|
export is_valid_timer = (x) ->
|
||||||
|
x and x > 0
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Checks whether the given string is a valid symbol.
|
||||||
|
--
|
||||||
|
-- A valid symbol is defined as a non-empty string that contains only
|
||||||
|
-- alphabetic characters (A-Z and a-z). If the input is not a string,
|
||||||
|
-- the function returns `false`.
|
||||||
|
--
|
||||||
|
-- @param s string: The string to be validated as a symbol.
|
||||||
|
-- @return boolean: Returns `true` if `s` contains only letters; otherwise, returns `false`.
|
||||||
|
export is_valid_symbol = (s) ->
|
||||||
|
-- Return false immediately if s is not a string.
|
||||||
|
unless type(s) == "string"
|
||||||
|
return false
|
||||||
|
-- Use Lua's pattern matching to ensure the string only contains letters.
|
||||||
|
if string.match s, "^[A-Za-z]+$"
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
Reference in new issue
Block a user