From bef29ad581651f72227ac803ffc6e99d38b4c3cf Mon Sep 17 00:00:00 2001 From: Alistair Israel Date: Sun, 15 Mar 2026 10:31:47 -0400 Subject: [PATCH 1/5] Flattening workspace --- Cargo.lock | 36 ++-- Cargo.toml | 33 +++- {flt/features => features/ast}/ast.feature | 0 {flt/features => features/ast}/binary.feature | 0 .../ast}/comments.feature | 0 features/ast/maps.feature | 26 +++ {flt/features => features/ast}/unary.feature | 0 {flt-cli/features => features}/cli.feature | 0 .../features => features}/repl/basic.feature | 0 flt-cli/Cargo.toml | 31 ---- flt/Cargo.toml | 27 --- ...uage_evolution.md => language_evolution.md | 0 {flt/src => src}/ast.rs | 0 {flt/src => src}/ast/expr.rs | 22 +++ {flt/src => src}/ast/identifier.rs | 3 +- {flt/src => src}/ast/literal.rs | 0 {flt/src => src}/ast/number.rs | 0 {flt/src => src}/ast/operands.rs | 0 flt-cli/src/main.rs => src/bin/flt.rs | 4 +- {flt/src => src}/errors.rs | 0 {flt-cli/src => src}/eval/mod.rs | 25 +-- {flt/src => src}/lib.rs | 3 +- {flt/src => src}/parser.rs | 1 + {flt/src => src}/parser/boolean.rs | 0 {flt/src => src}/parser/comment.rs | 0 {flt/src => src}/parser/expr.rs | 2 + {flt/src => src}/parser/function.rs | 0 {flt/src => src}/parser/identifier.rs | 0 {flt/src => src}/parser/literal.rs | 0 src/parser/map.rs | 154 ++++++++++++++++++ {flt/src => src}/parser/number.rs | 0 {flt/src => src}/parser/operands.rs | 0 {flt/src => src}/parser/string.rs | 0 {flt/src => src}/parser/symbol.rs | 0 {flt/src => src}/utils.rs | 0 {flt-cli/tests => tests}/cli.rs | 0 {flt/tests => tests}/features.rs | 79 ++++++++- {flt-cli/tests => tests}/repl.rs | 0 38 files changed, 338 insertions(+), 108 deletions(-) rename {flt/features => features/ast}/ast.feature (100%) rename {flt/features => features/ast}/binary.feature (100%) rename {flt/features => features/ast}/comments.feature (100%) create mode 100644 features/ast/maps.feature rename {flt/features => features/ast}/unary.feature (100%) rename {flt-cli/features => features}/cli.feature (100%) rename {flt-cli/features => features}/repl/basic.feature (100%) delete mode 100644 flt-cli/Cargo.toml delete mode 100644 flt/Cargo.toml rename flt/language_evolution.md => language_evolution.md (100%) rename {flt/src => src}/ast.rs (100%) rename {flt/src => src}/ast/expr.rs (88%) rename {flt/src => src}/ast/identifier.rs (96%) rename {flt/src => src}/ast/literal.rs (100%) rename {flt/src => src}/ast/number.rs (100%) rename {flt/src => src}/ast/operands.rs (100%) rename flt-cli/src/main.rs => src/bin/flt.rs (98%) rename {flt/src => src}/errors.rs (100%) rename {flt-cli/src => src}/eval/mod.rs (96%) rename {flt/src => src}/lib.rs (61%) rename {flt/src => src}/parser.rs (98%) rename {flt/src => src}/parser/boolean.rs (100%) rename {flt/src => src}/parser/comment.rs (100%) rename {flt/src => src}/parser/expr.rs (99%) rename {flt/src => src}/parser/function.rs (100%) rename {flt/src => src}/parser/identifier.rs (100%) rename {flt/src => src}/parser/literal.rs (100%) create mode 100644 src/parser/map.rs rename {flt/src => src}/parser/number.rs (100%) rename {flt/src => src}/parser/operands.rs (100%) rename {flt/src => src}/parser/string.rs (100%) rename {flt/src => src}/parser/symbol.rs (100%) rename {flt/src => src}/utils.rs (100%) rename {flt-cli/tests => tests}/cli.rs (100%) rename {flt/tests => tests}/features.rs (67%) rename {flt-cli/tests => tests}/repl.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 4eb5a29..80a849b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -443,38 +443,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.59.0", ] [[package]] name = "flt" version = "0.0.2" -dependencies = [ - "bigdecimal", - "ctor", - "cucumber", - "env_logger", - "log", - "nom", - "regex", - "thiserror", - "tokio", -] - -[[package]] -name = "flt-cli" -version = "0.0.2" dependencies = [ "bigdecimal", "clap", + "ctor", "cucumber", "env_logger", "eyre", - "flt", "futures", "log", + "nom", + "regex", "rustyline", + "thiserror", "tokio", ] @@ -653,9 +641,9 @@ dependencies = [ [[package]] name = "indenter" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "inflections" @@ -719,9 +707,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "lock_api" @@ -1047,14 +1035,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] diff --git a/Cargo.toml b/Cargo.toml index bba6b31..38bbcff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,31 @@ -[workspace] -resolver = "2" +[package] +name = "flt" +version = "0.0.2" +edition = "2021" +description = "a 'lite' functional language" +license = "MIT" -members = [ - "flt", - "flt-cli" -] +[dependencies] +bigdecimal = "0.4" +env_logger = "0.11.6" +log = "0.4.22" +nom = "8.0.0" +regex = "1.11.1" +thiserror = "2.0.9" +clap = { version = "4.5.23", features = ["cargo", "derive"] } +rustyline = "17.0" +eyre = "0.6.12" + +[dev-dependencies] +bigdecimal = "0.4" +ctor = "0.6.3" +cucumber = "0.22.1" +tokio = { version = "1.42.0", features = ["full"] } +futures = "0.3.31" + +[[test]] +name = "features" +harness = false [workspace.lints.clippy] unwrap_used = "deny" diff --git a/flt/features/ast.feature b/features/ast/ast.feature similarity index 100% rename from flt/features/ast.feature rename to features/ast/ast.feature diff --git a/flt/features/binary.feature b/features/ast/binary.feature similarity index 100% rename from flt/features/binary.feature rename to features/ast/binary.feature diff --git a/flt/features/comments.feature b/features/ast/comments.feature similarity index 100% rename from flt/features/comments.feature rename to features/ast/comments.feature diff --git a/features/ast/maps.feature b/features/ast/maps.feature new file mode 100644 index 0000000..5902b99 --- /dev/null +++ b/features/ast/maps.feature @@ -0,0 +1,26 @@ +Feature: Maps + + Scenario: Parsing an empty map + Given the input "{}" + When I parse the input + Then the output should be an empty map + + Scenario: Parsing a map with a bare key and string value + Given the input '{ foo: "bar" }' + When I parse the input + Then the output should be a map with key "foo" and string value "bar" + + Scenario: Parsing a map with a bare key and number value + Given the input "{ abc123: 456 }" + When I parse the input + Then the output should be a map with key "abc123" and number value 456 + + Scenario: Parsing a map with a quoted key + Given the input '{ "spaced out": (1 + 1) }' + When I parse the input + Then the output should be a map with 1 entry + + Scenario: Parsing a map with multiple entries + Given the input '{ name: "Alice", age: 30 }' + When I parse the input + Then the output should be a map with 2 entries diff --git a/flt/features/unary.feature b/features/ast/unary.feature similarity index 100% rename from flt/features/unary.feature rename to features/ast/unary.feature diff --git a/flt-cli/features/cli.feature b/features/cli.feature similarity index 100% rename from flt-cli/features/cli.feature rename to features/cli.feature diff --git a/flt-cli/features/repl/basic.feature b/features/repl/basic.feature similarity index 100% rename from flt-cli/features/repl/basic.feature rename to features/repl/basic.feature diff --git a/flt-cli/Cargo.toml b/flt-cli/Cargo.toml deleted file mode 100644 index d9052b5..0000000 --- a/flt-cli/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "flt-cli" -version = "0.0.2" -edition = "2021" -description = "the flt CLI" -license = "MIT" - -[lints] -workspace = true - -[dependencies] -bigdecimal = "0.4" -clap = { version = "4.5.23", features = ["cargo", "derive"] } -flt = { version = "0.0.2", path = "../flt" } -rustyline = "17.0" -env_logger = "0.11.6" -eyre = "0.6.12" -log = "0.4.22" - -[dev-dependencies] -cucumber = "0.22.1" -futures = "0.3.31" -tokio = { version = "1.42.0", features = ["full"] } - -[[test]] -name = "cli" -harness = false - -[[test]] -name = "repl" -harness = false diff --git a/flt/Cargo.toml b/flt/Cargo.toml deleted file mode 100644 index 5704628..0000000 --- a/flt/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "flt" -version = "0.0.2" -edition = "2021" -description = "a 'lite' functional language" -license = "MIT" - -[lints] -workspace = true - -[dependencies] -bigdecimal = "0.4" -env_logger = "0.11.6" -log = "0.4.22" -nom = "8.0.0" -regex = "1.11.1" -thiserror = "2.0.9" - -[dev-dependencies] -bigdecimal = "0.4" -ctor = "0.6.3" -cucumber = "0.22.1" -tokio = { version = "1.42.0", features = ["full"] } - -[[test]] -name = "features" -harness = false diff --git a/flt/language_evolution.md b/language_evolution.md similarity index 100% rename from flt/language_evolution.md rename to language_evolution.md diff --git a/flt/src/ast.rs b/src/ast.rs similarity index 100% rename from flt/src/ast.rs rename to src/ast.rs diff --git a/flt/src/ast/expr.rs b/src/ast/expr.rs similarity index 88% rename from flt/src/ast/expr.rs rename to src/ast/expr.rs index fb8d787..a379140 100644 --- a/flt/src/ast/expr.rs +++ b/src/ast/expr.rs @@ -6,6 +6,7 @@ use super::identifier::Identifier; use super::literal::Literal; use super::operands::BinaryOp; use super::operands::UnaryOp; +use crate::utils::escape_string; /// An expression in the language. #[derive(Clone, Debug, PartialEq)] @@ -22,6 +23,8 @@ pub enum Expr { FunctionCall(Identifier, Vec), /// A parenthesized expression. Parenthesized(Box), + /// A map literal: `{ key: value, ... }`. + MapLiteral(Vec<(String, Expr)>), } impl Display for Expr { @@ -40,6 +43,20 @@ impl Display for Expr { write!(f, "{name}({args})") } Expr::Parenthesized(expr) => write!(f, "({expr})"), + Expr::MapLiteral(entries) => { + write!(f, "{{ ")?; + for (i, (key, value)) in entries.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + if key.contains(|c: char| !c.is_alphanumeric() && c != '_') { + write!(f, "\"{}\": {}", escape_string(key), value)?; + } else { + write!(f, "{key}: {value}")?; + } + } + write!(f, " }}") + } } } } @@ -92,6 +109,11 @@ impl Expr { pub fn parenthesized(expr: Expr) -> Self { Expr::Parenthesized(Box::new(expr)) } + + /// Constructs a map literal expression. + pub fn map_literal(entries: Vec<(impl Into, Expr)>) -> Self { + Expr::MapLiteral(entries.into_iter().map(|(k, v)| (k.into(), v)).collect()) + } } #[cfg(test)] diff --git a/flt/src/ast/identifier.rs b/src/ast/identifier.rs similarity index 96% rename from flt/src/ast/identifier.rs rename to src/ast/identifier.rs index 5683e03..d1c0090 100644 --- a/flt/src/ast/identifier.rs +++ b/src/ast/identifier.rs @@ -35,8 +35,7 @@ impl TryFrom<&str> for Identifier { fn try_from(s: &str) -> Result { let mut chars = s.chars(); if let Some(first) = chars.next() { - if first.is_alphabetic() && chars.all(|c| c.is_alphanumeric() || c == '-' || c == '_') - { + if first.is_alphabetic() && chars.all(|c| c.is_alphanumeric() || c == '-' || c == '_') { return Ok(Identifier(s.to_string())); } } diff --git a/flt/src/ast/literal.rs b/src/ast/literal.rs similarity index 100% rename from flt/src/ast/literal.rs rename to src/ast/literal.rs diff --git a/flt/src/ast/number.rs b/src/ast/number.rs similarity index 100% rename from flt/src/ast/number.rs rename to src/ast/number.rs diff --git a/flt/src/ast/operands.rs b/src/ast/operands.rs similarity index 100% rename from flt/src/ast/operands.rs rename to src/ast/operands.rs diff --git a/flt-cli/src/main.rs b/src/bin/flt.rs similarity index 98% rename from flt-cli/src/main.rs rename to src/bin/flt.rs index 381b421..cdc0249 100644 --- a/flt-cli/src/main.rs +++ b/src/bin/flt.rs @@ -1,8 +1,6 @@ -mod eval; - use std::process::ExitCode; -use eval::eval; +use flt::eval::eval; use flt::parser::parse_expr; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; diff --git a/flt/src/errors.rs b/src/errors.rs similarity index 100% rename from flt/src/errors.rs rename to src/errors.rs diff --git a/flt-cli/src/eval/mod.rs b/src/eval/mod.rs similarity index 96% rename from flt-cli/src/eval/mod.rs rename to src/eval/mod.rs index e9ad1c7..83fdaea 100644 --- a/flt-cli/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -1,11 +1,11 @@ +use crate::ast::BinaryOp; +use crate::ast::Expr; +use crate::ast::Literal; +use crate::ast::UnaryOp; +use crate::errors::Error; +use crate::errors::RuntimeError; +use crate::utils::escape_string; use bigdecimal::BigDecimal; -use flt::ast::BinaryOp; -use flt::ast::Expr; -use flt::ast::Literal; -use flt::ast::UnaryOp; -use flt::errors::Error; -use flt::errors::RuntimeError; -use flt::utils::escape_string; pub fn eval(expr: &Expr) -> Result { let lit = eval_to_literal(expr)?; @@ -22,6 +22,7 @@ fn eval_to_literal(expr: &Expr) -> Result { Expr::BinaryExpr(left, op, right) => eval_binary_expr(left, *op, right), Expr::FunctionCall(_, _) => Err(Error::RuntimeError(RuntimeError::UnsupportedFunctionCall)), Expr::Parenthesized(inner) => eval_to_literal(inner), + Expr::MapLiteral(_) => Err(Error::RuntimeError(RuntimeError::InvalidOperandType)), } } @@ -130,11 +131,11 @@ fn binary_string(l: &Literal, r: &Literal) -> Result { #[cfg(test)] mod tests { - use flt::ast::BinaryOp; - use flt::ast::Expr; - use flt::ast::UnaryOp; - use flt::errors::Error; - use flt::errors::RuntimeError; + use crate::ast::BinaryOp; + use crate::ast::Expr; + use crate::ast::UnaryOp; + use crate::errors::Error; + use crate::errors::RuntimeError; use super::eval; diff --git a/flt/src/lib.rs b/src/lib.rs similarity index 61% rename from flt/src/lib.rs rename to src/lib.rs index 9993c89..7ebf75b 100644 --- a/flt/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ -#[doc = include_str!("../../README.md")] +#[doc = include_str!("../README.md")] pub mod ast; pub mod errors; +pub mod eval; pub mod parser; pub mod utils; diff --git a/flt/src/parser.rs b/src/parser.rs similarity index 98% rename from flt/src/parser.rs rename to src/parser.rs index d74e0d8..55e69c3 100644 --- a/flt/src/parser.rs +++ b/src/parser.rs @@ -6,6 +6,7 @@ mod expr; mod function; mod identifier; mod literal; +mod map; mod number; mod operands; mod string; diff --git a/flt/src/parser/boolean.rs b/src/parser/boolean.rs similarity index 100% rename from flt/src/parser/boolean.rs rename to src/parser/boolean.rs diff --git a/flt/src/parser/comment.rs b/src/parser/comment.rs similarity index 100% rename from flt/src/parser/comment.rs rename to src/parser/comment.rs diff --git a/flt/src/parser/expr.rs b/src/parser/expr.rs similarity index 99% rename from flt/src/parser/expr.rs rename to src/parser/expr.rs index e758806..f2b5dd0 100644 --- a/flt/src/parser/expr.rs +++ b/src/parser/expr.rs @@ -11,6 +11,7 @@ use super::comment::multispace0_or_comment; use super::function::parse_function_call; use super::identifier::parse_identifier; use super::literal::parse_literal; +use super::map::parse_map_literal; use super::operands::parse_binary_op; use super::operands::parse_unary_op; use super::string::parse_interpolated_string; @@ -26,6 +27,7 @@ fn parse_primary(input: &str) -> IResult<&str, Expr> { Expr::FunctionCall(name, args) }), map(parse_identifier, Expr::ident), + parse_map_literal(parse_or), map( delimited( (multispace0_or_comment, tag("("), multispace0_or_comment), diff --git a/flt/src/parser/function.rs b/src/parser/function.rs similarity index 100% rename from flt/src/parser/function.rs rename to src/parser/function.rs diff --git a/flt/src/parser/identifier.rs b/src/parser/identifier.rs similarity index 100% rename from flt/src/parser/identifier.rs rename to src/parser/identifier.rs diff --git a/flt/src/parser/literal.rs b/src/parser/literal.rs similarity index 100% rename from flt/src/parser/literal.rs rename to src/parser/literal.rs diff --git a/src/parser/map.rs b/src/parser/map.rs new file mode 100644 index 0000000..737b54e --- /dev/null +++ b/src/parser/map.rs @@ -0,0 +1,154 @@ +use std::borrow::Cow; + +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::bytes::complete::take_while; +use nom::bytes::complete::take_while_m_n; +use nom::combinator::map; +use nom::combinator::opt; +use nom::combinator::recognize; +use nom::multi::separated_list0; +use nom::sequence::pair; +use nom::sequence::separated_pair; +use nom::IResult; +use nom::Parser; + +use super::comment::multispace0_or_comment; +use super::string::parse_string; +use crate::ast::Expr; + +/// Parses a bare map key: starts with a letter, followed by alphanumeric or `_`. +fn parse_bare_key(input: &str) -> IResult<&str, &str> { + recognize(pair( + take_while_m_n(1, 1, |c: char| c.is_alphabetic()), + take_while(|c: char| c.is_alphanumeric() || c == '_'), + )) + .parse(input) +} + +/// Parses a map key: bare identifier or quoted string. +fn parse_map_key(input: &str) -> IResult<&str, Cow<'_, str>> { + alt(( + map(parse_string, Cow::Owned), + map(parse_bare_key, Cow::Borrowed), + )) + .parse(input) +} + +/// Parses a map literal: `{ key: value, ... }`. +/// +/// Takes an expression parser as parameter to break the circular dependency +/// between the map parser and expression parser. +pub fn parse_map_literal<'a>( + expr_parser: fn(&'a str) -> IResult<&'a str, Expr>, +) -> impl FnMut(&'a str) -> IResult<&'a str, Expr> { + move |input: &'a str| { + let (input, _) = tag("{").parse(input)?; + let (input, _) = multispace0_or_comment(input)?; + + let (input, entries) = separated_list0( + (multispace0_or_comment, tag(","), multispace0_or_comment), + separated_pair( + parse_map_key, + (multispace0_or_comment, tag(":"), multispace0_or_comment), + expr_parser, + ), + ) + .parse(input)?; + + let (input, _) = multispace0_or_comment(input)?; + let (input, _) = opt(tag(",")).parse(input)?; + let (input, _) = multispace0_or_comment(input)?; + let (input, _) = tag("}").parse(input)?; + + let entries: Vec<(String, Expr)> = entries + .into_iter() + .map(|(k, v)| (k.into_owned(), v)) + .collect(); + + Ok((input, Expr::MapLiteral(entries))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::BinaryOp; + use crate::ast::Expr; + use crate::parser::expr::parse_expr; + + fn parse_map(input: &str) -> IResult<&str, Expr> { + parse_map_literal(parse_expr)(input) + } + + #[test] + fn test_parse_empty_map() { + assert_eq!(parse_map("{}"), Ok(("", Expr::MapLiteral(vec![])))); + assert_eq!(parse_map("{ }"), Ok(("", Expr::MapLiteral(vec![])))); + } + + #[test] + fn test_parse_single_bare_key_string_value() { + assert_eq!( + parse_map(r#"{ foo: "bar" }"#), + Ok(( + "", + Expr::map_literal(vec![("foo", Expr::literal_string("bar"))]) + )) + ); + } + + #[test] + fn test_parse_single_bare_key_number_value() { + assert_eq!( + parse_map("{ abc123: 456 }"), + Ok(( + "", + Expr::map_literal(vec![("abc123", Expr::literal_number(456))]) + )) + ); + } + + #[test] + fn test_parse_quoted_key() { + assert_eq!( + parse_map(r#"{ "spaced out": (1 + 1) }"#), + Ok(( + "", + Expr::map_literal(vec![( + "spaced out", + Expr::parenthesized(Expr::binary_expr( + Expr::literal_number(1), + BinaryOp::Add, + Expr::literal_number(1) + )) + )]) + )) + ); + } + + #[test] + fn test_parse_multiple_entries() { + assert_eq!( + parse_map(r#"{ name: "Alice", age: 30 }"#), + Ok(( + "", + Expr::map_literal(vec![ + ("name", Expr::literal_string("Alice")), + ("age", Expr::literal_number(30)), + ]) + )) + ); + } + + #[test] + fn test_parse_trailing_comma() { + assert_eq!( + parse_map(r#"{ foo: 1, }"#), + Ok(( + "", + Expr::map_literal(vec![("foo", Expr::literal_number(1))]) + )) + ); + } +} diff --git a/flt/src/parser/number.rs b/src/parser/number.rs similarity index 100% rename from flt/src/parser/number.rs rename to src/parser/number.rs diff --git a/flt/src/parser/operands.rs b/src/parser/operands.rs similarity index 100% rename from flt/src/parser/operands.rs rename to src/parser/operands.rs diff --git a/flt/src/parser/string.rs b/src/parser/string.rs similarity index 100% rename from flt/src/parser/string.rs rename to src/parser/string.rs diff --git a/flt/src/parser/symbol.rs b/src/parser/symbol.rs similarity index 100% rename from flt/src/parser/symbol.rs rename to src/parser/symbol.rs diff --git a/flt/src/utils.rs b/src/utils.rs similarity index 100% rename from flt/src/utils.rs rename to src/utils.rs diff --git a/flt-cli/tests/cli.rs b/tests/cli.rs similarity index 100% rename from flt-cli/tests/cli.rs rename to tests/cli.rs diff --git a/flt/tests/features.rs b/tests/features.rs similarity index 67% rename from flt/tests/features.rs rename to tests/features.rs index 40f7197..7f25661 100644 --- a/flt/tests/features.rs +++ b/tests/features.rs @@ -1,8 +1,8 @@ use std::path::Path; use bigdecimal::BigDecimal; -use cucumber::given; use cucumber::gherkin::Step; +use cucumber::given; use cucumber::then; use cucumber::when; use cucumber::World; @@ -114,7 +114,11 @@ fn then_output_should_be_identifier(world: &mut AstWorld, expected: String) { #[then(expr = "parsing should fail")] fn then_parsing_should_fail(world: &mut AstWorld) { let output = world.output.take().expect("output should be set"); - assert!(output.is_err(), "expected parsing to fail, got {:?}", output); + assert!( + output.is_err(), + "expected parsing to fail, got {:?}", + output + ); } #[then(expr = r"the output should parse to interpolated string {string} {word} {string}")] @@ -138,6 +142,77 @@ fn then_output_should_be_interpolated_string( assert_eq!(expr, expected, "expected interpolated string expr"); } +#[then(expr = "the output should be an empty map")] +fn then_output_should_be_empty_map(world: &mut AstWorld) { + let output = world.output.take().expect("output should be set"); + let expr = output.expect("parse should succeed"); + match &expr { + Expr::MapLiteral(entries) => { + assert!(entries.is_empty(), "expected empty map, got {entries:?}"); + } + _ => panic!("expected map literal, got {expr:?}"), + } +} + +#[then(regex = r#"^the output should be a map with (\d+) entr(?:y|ies)$"#)] +fn then_output_should_be_map_with_n_entries(world: &mut AstWorld, count: usize) { + let output = world.output.take().expect("output should be set"); + let expr = output.expect("parse should succeed"); + match &expr { + Expr::MapLiteral(entries) => { + assert_eq!( + entries.len(), + count, + "expected {count} entries, got {}", + entries.len() + ); + } + _ => panic!("expected map literal, got {expr:?}"), + } +} + +#[then(expr = r#"the output should be a map with key {string} and string value {string}"#)] +fn then_output_should_be_map_with_key_string_value( + world: &mut AstWorld, + key: String, + value: String, +) { + let output = world.output.take().expect("output should be set"); + let expr = output.expect("parse should succeed"); + match &expr { + Expr::MapLiteral(entries) => { + assert_eq!(entries.len(), 1, "expected single-entry map"); + let (k, v) = &entries[0]; + assert_eq!(k, &key, "expected key {key:?}"); + assert_eq!( + *v, + Expr::literal_string(&value), + "expected string value {value:?}" + ); + } + _ => panic!("expected map literal, got {expr:?}"), + } +} + +#[then(regex = r#"^the output should be a map with key "([^"]*)" and number value (\d+)$"#)] +fn then_output_should_be_map_with_key_number_value(world: &mut AstWorld, key: String, value: i64) { + let output = world.output.take().expect("output should be set"); + let expr = output.expect("parse should succeed"); + match &expr { + Expr::MapLiteral(entries) => { + assert_eq!(entries.len(), 1, "expected single-entry map"); + let (k, v) = &entries[0]; + assert_eq!(k, &key, "expected key {key:?}"); + assert_eq!( + *v, + Expr::literal_number(value), + "expected number value {value}" + ); + } + _ => panic!("expected map literal, got {expr:?}"), + } +} + #[then(expr = r"the output should parse to string concat {string} and {string}")] fn then_output_should_be_string_concat(world: &mut AstWorld, left: String, right: String) { let output = world.output.take().expect("output should be set"); diff --git a/flt-cli/tests/repl.rs b/tests/repl.rs similarity index 100% rename from flt-cli/tests/repl.rs rename to tests/repl.rs From 97dd426cabf651b7933f28ae5cc412c5b28cd49f Mon Sep 17 00:00:00 2001 From: Alistair Israel Date: Sun, 15 Mar 2026 11:03:41 -0400 Subject: [PATCH 2/5] Add CI --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ecc2ce1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +cname: CI + +on: + push: + paths: + - "src/**" + - "tests/**" + - "features/**" + - "Cargo.*" + - ".rustfmt.toml" + - "README.md" + - ".github/workflows/ci.yml" + +env: + # We use environment variables to specify the Rust version and other settings once + RUST_TOOLCHAIN: 1.94.0 + CARGO_TERM_COLOR: always + +jobs: + check: + name: Lint & Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + components: rustfmt, clippy + + - name: Check formatting + run: cargo +nightly fmt -- --check + + - name: Run clippy + run: cargo clippy --all-targets -- -D warnings + + - name: Run tests + run: cargo test From b7a3ea44f9e3a5e7175e7a00becc66d0a89ac929 Mon Sep 17 00:00:00 2001 From: Alistair Israel Date: Sun, 15 Mar 2026 11:04:22 -0400 Subject: [PATCH 3/5] typo --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecc2ce1..8ed289e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -cname: CI +name: CI on: push: From 3ea61fb59596e94caf38103f172c4ea0688a9d76 Mon Sep 17 00:00:00 2001 From: Alistair Israel Date: Sun, 15 Mar 2026 11:10:31 -0400 Subject: [PATCH 4/5] Try without commas --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ed289e..dc2e215 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: ${{ env.RUST_TOOLCHAIN }} - components: rustfmt, clippy + components: rustfmt clippy - name: Check formatting run: cargo +nightly fmt -- --check From 72423bea254015839c8bbca30d949d3367cb4ae4 Mon Sep 17 00:00:00 2001 From: Alistair Israel Date: Sun, 15 Mar 2026 11:12:13 -0400 Subject: [PATCH 5/5] Don't use nightly --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc2e215..ad93c79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: components: rustfmt clippy - name: Check formatting - run: cargo +nightly fmt -- --check + run: cargo fmt -- --check - name: Run clippy run: cargo clippy --all-targets -- -D warnings