From 4b1c431addfac8f11e287bb56d436d8fc57ca2e6 Mon Sep 17 00:00:00 2001 From: Oliver Linnarsson Date: Fri, 14 Jun 2024 14:32:43 +0200 Subject: [PATCH] chore: Parser should be Lexer, and Compiler should be Parser... -_- --- src/handles.gleam | 17 +- src/handles/compiler.gleam | 108 ------------- src/handles/engine.gleam | 14 +- src/handles/format.gleam | 21 ++- src/handles/lexer.gleam | 122 +++++++++++++++ src/handles/parser.gleam | 196 +++++++++++------------- test/api_test.gleam | 49 ++++++ test/unit_tests/compiler_test.gleam | 67 -------- test/unit_tests/engine_test.gleam | 28 ++-- test/unit_tests/format_test.gleam | 8 +- test/unit_tests/lexer_test.gleam | 70 +++++++++ test/unit_tests/parser_test.gleam | 85 +++++----- test/user_stories/helloworld_test.gleam | 17 -- test/user_stories/knattarna_test.gleam | 24 +-- 14 files changed, 427 insertions(+), 399 deletions(-) delete mode 100644 src/handles/compiler.gleam create mode 100644 src/handles/lexer.gleam create mode 100644 test/api_test.gleam delete mode 100644 test/unit_tests/compiler_test.gleam create mode 100644 test/unit_tests/lexer_test.gleam delete mode 100644 test/user_stories/helloworld_test.gleam diff --git a/src/handles.gleam b/src/handles.gleam index 7b85fff..be75f1c 100644 --- a/src/handles.gleam +++ b/src/handles.gleam @@ -1,27 +1,26 @@ import gleam/dynamic import gleam/result -import handles/compiler import handles/engine +import handles/lexer import handles/parser pub type TemplateError { - ParseError(error: parser.ParseError) - CompileError(error: List(compiler.CompileError)) + LexError(error: lexer.LexError) + ParseError(error: List(parser.ParseError)) } pub type Template { - Template(ast: List(compiler.AST)) + Template(ast: List(parser.AST)) } pub fn prepare(template: String) -> Result(Template, TemplateError) { use tokens <- result.try( - result.map_error(parser.parse(template), fn(err) { ParseError(err) }), + result.map_error(lexer.run(template), fn(err) { LexError(err) }), ) use ast <- result.try( - result.map_error( - compiler.compile(tokens, ["if", "unless", "each"]), - fn(err) { CompileError(err) }, - ), + result.map_error(parser.run(tokens, ["if", "unless", "each"]), fn(err) { + ParseError(err) + }), ) Ok(Template(ast)) } diff --git a/src/handles/compiler.gleam b/src/handles/compiler.gleam deleted file mode 100644 index 9f04870..0000000 --- a/src/handles/compiler.gleam +++ /dev/null @@ -1,108 +0,0 @@ -import gleam/list -import gleam/result -import handles/parser - -pub type CompileError { - UnbalancedBlock(start: Int, end: Int, kind: String) - UnknownBlockKind(start: Int, end: Int, kind: String) -} - -pub type AST { - Constant(value: String) - Property(path: List(String)) - Block(kind: String, path: List(String), children: List(AST)) -} - -fn validation_pass( - tokens: List(parser.Token), - valid_blocks: List(String), -) -> List(Result(parser.Token, CompileError)) { - tokens - |> list.map(fn(it) { - case it { - parser.BlockStart(start, end, kind, _) -> - case list.contains(valid_blocks, kind) { - True -> Ok(it) - False -> Error(UnknownBlockKind(start, end, kind)) - } - _ -> Ok(it) - } - }) -} - -fn balance_pass( - tokens: List(Result(parser.Token, CompileError)), -) -> List(Result(parser.Token, CompileError)) { - let #(out, stack) = - tokens - |> list.fold(#([], []), fn(acc, it) { - let #(out, stack) = acc - case it { - Ok(parser.BlockStart(_, _, kind, _)) -> #([it, ..out], [kind, ..stack]) - Ok(parser.BlockEnd(start, end, kind)) -> - case list.first(stack) { - Ok(top) if top == kind -> - case list.rest(stack) { - Ok(rest) -> #([it, ..out], rest) - _ -> #([Error(UnbalancedBlock(start, end, kind)), ..out], []) - } - _ -> - case list.rest(stack) { - Ok(rest) -> #([it, ..out], rest) - _ -> #([Error(UnbalancedBlock(start, end, kind)), ..out], []) - } - } - _ -> #([it, ..out], stack) - } - }) - - list.concat([ - out, - list.map(stack, fn(it) { Error(UnbalancedBlock(-1, -1, it)) }), - ]) -} - -fn ast_transform_pass( - tokens: List(parser.Token), - index: Int, - ast: List(AST), -) -> List(AST) { - case tokens { - [] -> list.reverse(ast) - [head, ..tail] -> { - case head { - parser.Constant(_, _, value) -> - ast_transform_pass(tail, index + 1, [Constant(value), ..ast]) - parser.Property(_, _, path) -> - ast_transform_pass(tail, index + 1, [Property(path), ..ast]) - parser.BlockStart(_, _, kind, path) -> { - let children = ast_transform_pass(tail, index + 1, []) - ast_transform_pass( - list.drop(tail, list.length(children)), - index + 1 + list.length(children), - [Block(kind, path, children), ..ast], - ) - } - parser.BlockEnd(_, _, _) -> list.reverse(ast) - } - } - } -} - -pub fn compile( - tokens: List(parser.Token), - valid_blocks: List(String), -) -> Result(List(AST), List(CompileError)) { - case - tokens - |> validation_pass(valid_blocks) - |> balance_pass - |> result.partition - { - #([parser.Constant(_, _, ""), ..tokens], []) -> - // Remove the leading empty string present when the template starts with a tag - Ok(tokens |> ast_transform_pass(0, [])) - #(tokens, []) -> Ok(tokens |> ast_transform_pass(0, [])) - #(_, err) -> Error(err) - } -} diff --git a/src/handles/engine.gleam b/src/handles/engine.gleam index 7b9461a..1b0cb13 100644 --- a/src/handles/engine.gleam +++ b/src/handles/engine.gleam @@ -6,7 +6,7 @@ import gleam/io import gleam/list import gleam/result import gleam/string_builder -import handles/compiler +import handles/parser pub type RuntimeError { UnexpectedTypeError(path: List(String), got: String, expected: List(String)) @@ -106,7 +106,7 @@ pub fn get_as_list( fn eval_if( condition: Bool, - children: List(compiler.AST), + children: List(parser.AST), ctx: dynamic.Dynamic, ) -> Result(String, RuntimeError) { case condition { @@ -117,7 +117,7 @@ fn eval_if( fn eval_each( ctx_list: List(dynamic.Dynamic), - children: List(compiler.AST), + children: List(parser.AST), ) -> Result(String, RuntimeError) { { use acc, ctx <- list.fold(ctx_list, Ok(string_builder.new())) @@ -129,18 +129,18 @@ fn eval_each( } pub fn run( - ast: List(compiler.AST), + ast: List(parser.AST), ctx: dynamic.Dynamic, ) -> Result(String, RuntimeError) { { use acc, it <- list.fold(ast, Ok(string_builder.new())) use acc <- result.try(acc) case it { - compiler.Constant(value) -> Ok(string_builder.append(acc, value)) - compiler.Property(path) -> + parser.Constant(value) -> Ok(string_builder.append(acc, value)) + parser.Property(path) -> get_as_string(ctx, path) |> result.map(string_builder.append(acc, _)) - compiler.Block(kind, path, children) -> + parser.Block(kind, path, children) -> case kind { "if" -> get_as_bool(ctx, path) diff --git a/src/handles/format.gleam b/src/handles/format.gleam index 986af7f..bd89b57 100644 --- a/src/handles/format.gleam +++ b/src/handles/format.gleam @@ -1,7 +1,7 @@ import gleam/int import gleam/list import gleam/string -import handles/parser +import handles/lexer type Position { Position(index: Int, row: Int, col: Int) @@ -39,9 +39,9 @@ fn resolve_position( } } -pub fn format_parse_error(error: parser.ParseError, template: String) -> String { +pub fn format_parse_error(error: lexer.LexError, template: String) -> String { case error { - parser.UnexpectedEof(index) -> + lexer.UnexpectedEof(index) -> case resolve_position(template, index, Position(0, 0, 0)) { Position(_, row, col) -> string.concat([ @@ -54,7 +54,7 @@ pub fn format_parse_error(error: parser.ParseError, template: String) -> String ]) OutOfBounds -> panic as "Unable to resolve error position in template" } - parser.UnexpectedToken(index, char) -> + lexer.UnexpectedToken(index, char) -> case resolve_position(template, index, Position(0, 0, 0)) { Position(_, row, col) -> string.concat([ @@ -68,7 +68,7 @@ pub fn format_parse_error(error: parser.ParseError, template: String) -> String ]) OutOfBounds -> panic as "Unable to resolve error position in template" } - parser.SyntaxError(errors) -> + lexer.SyntaxError(errors) -> errors |> list.fold("", fn(acc, err) { string.concat([acc, "\n", format_syntax_error(err, template)]) @@ -76,12 +76,9 @@ pub fn format_parse_error(error: parser.ParseError, template: String) -> String } } -pub fn format_syntax_error( - error: parser.SyntaxError, - template: String, -) -> String { +pub fn format_syntax_error(error: lexer.SyntaxError, template: String) -> String { case error { - parser.EmptyExpression(start, _) -> + lexer.EmptyExpression(start, _) -> case resolve_position(template, start, Position(0, 0, 0)) { Position(_, row, col) -> string.concat([ @@ -94,7 +91,7 @@ pub fn format_syntax_error( ]) OutOfBounds -> panic as "Unable to resolve error position in template" } - parser.MissingBlockKind(start, _) -> + lexer.MissingBlockKind(start, _) -> case resolve_position(template, start, Position(0, 0, 0)) { Position(_, row, col) -> string.concat([ @@ -107,7 +104,7 @@ pub fn format_syntax_error( ]) OutOfBounds -> panic as "Unable to resolve error position in template" } - parser.UnexpectedBlockArgument(start, _) -> + lexer.UnexpectedBlockArgument(start, _) -> case resolve_position(template, start, Position(0, 0, 0)) { Position(_, row, col) -> string.concat([ diff --git a/src/handles/lexer.gleam b/src/handles/lexer.gleam new file mode 100644 index 0000000..4fca0f7 --- /dev/null +++ b/src/handles/lexer.gleam @@ -0,0 +1,122 @@ +import gleam/result +import gleam/string + +pub type Token { + Constant(start: Int, end: Int, value: String) + Property(start: Int, end: Int, path: List(String)) + BlockStart(start: Int, end: Int, kind: String, path: List(String)) + BlockEnd(start: Int, end: Int, kind: String) +} + +pub type LexError { + UnexpectedToken(index: Int, str: String) + UnexpectedEof(index: Int) + SyntaxError(errors: List(SyntaxError)) +} + +pub type SyntaxError { + EmptyExpression(start: Int, end: Int) + MissingBlockKind(start: Int, end: Int) + UnexpectedBlockArgument(start: Int, end: Int) +} + +type ParserState { + Static(start: Int, end: Int, str: String) + Tag(start: Int, end: Int, str: String) + TagStart(start: Int) + TagEnd(start: Int) +} + +fn step( + state: ParserState, + acc: List(Result(Token, SyntaxError)), + input: String, +) -> Result(List(Result(Token, SyntaxError)), LexError) { + case state { + Static(start, end, str) -> + case string.first(input) { + Ok("{") -> + step( + TagStart(end + 1), + [Ok(Constant(start, end, str)), ..acc], + string.drop_left(input, 1), + ) + Ok(char) -> + step( + Static(start, end + 1, string.append(str, char)), + acc, + string.drop_left(input, 1), + ) + Error(_) -> Ok([Ok(Constant(start, end, str)), ..acc]) + } + Tag(start, end, value) -> + case string.first(input) { + Ok("}") -> + step( + TagEnd(end + 1), + [ + { + let val = string.trim(value) + case string.first(val) { + Ok("#") -> + case string.split_once(string.drop_left(val, 1), " ") { + Ok(#(kind, body)) -> + Ok(BlockStart(start, end, kind, string.split(body, "."))) + Error(_) -> Error(MissingBlockKind(start, end)) + } + Ok("/") -> + case string.split_once(string.drop_left(val, 1), " ") { + Ok(#(_, _)) -> Error(UnexpectedBlockArgument(start, end)) + Error(_) -> + Ok(BlockEnd(start, end, string.drop_left(val, 1))) + } + Ok(_) -> Ok(Property(start, end, string.split(value, "."))) + Error(_) -> Error(EmptyExpression(start, end)) + } + }, + ..acc + ], + string.drop_left(input, 1), + ) + Ok(char) -> + step( + Tag(start, end + 1, string.append(value, char)), + acc, + string.drop_left(input, 1), + ) + Error(_) -> Error(UnexpectedEof(end)) + } + TagStart(start) -> + case string.first(input) { + Ok("{") -> + step(Tag(start + 1, start + 1, ""), acc, string.drop_left(input, 1)) + Ok(char) -> Error(UnexpectedToken(start, char)) + Error(_) -> Error(UnexpectedEof(start)) + } + TagEnd(start) -> + case string.first(input) { + Ok("}") -> + step( + Static(start + 1, start + 1, ""), + acc, + string.drop_left(input, 1), + ) + Ok(char) -> Error(UnexpectedToken(start, char)) + Error(_) -> Error(UnexpectedEof(start)) + } + } +} + +pub fn run(template: String) -> Result(List(Token), LexError) { + case step(Static(0, 0, ""), [], template) { + Ok(tokens) -> + case + tokens + |> result.partition + { + #(ok, []) -> Ok(ok) + #(_, err) -> Error(SyntaxError(err)) + } + Error(err) -> Error(err) + } +} diff --git a/src/handles/parser.gleam b/src/handles/parser.gleam index 0bc43be..d9e59f9 100644 --- a/src/handles/parser.gleam +++ b/src/handles/parser.gleam @@ -1,122 +1,108 @@ +import gleam/list import gleam/result -import gleam/string - -pub type Token { - Constant(start: Int, end: Int, value: String) - Property(start: Int, end: Int, path: List(String)) - BlockStart(start: Int, end: Int, kind: String, path: List(String)) - BlockEnd(start: Int, end: Int, kind: String) -} +import handles/lexer pub type ParseError { - UnexpectedToken(index: Int, str: String) - UnexpectedEof(index: Int) - SyntaxError(errors: List(SyntaxError)) + UnbalancedBlock(start: Int, end: Int, kind: String) + UnknownBlockKind(start: Int, end: Int, kind: String) } -pub type SyntaxError { - EmptyExpression(start: Int, end: Int) - MissingBlockKind(start: Int, end: Int) - UnexpectedBlockArgument(start: Int, end: Int) +pub type AST { + Constant(value: String) + Property(path: List(String)) + Block(kind: String, path: List(String), children: List(AST)) } -type ParserState { - Static(start: Int, end: Int, str: String) - Tag(start: Int, end: Int, str: String) - TagStart(start: Int) - TagEnd(start: Int) +fn validation_pass( + tokens: List(lexer.Token), + valid_blocks: List(String), +) -> List(Result(lexer.Token, ParseError)) { + tokens + |> list.map(fn(it) { + case it { + lexer.BlockStart(start, end, kind, _) -> + case list.contains(valid_blocks, kind) { + True -> Ok(it) + False -> Error(UnknownBlockKind(start, end, kind)) + } + _ -> Ok(it) + } + }) } -fn step( - state: ParserState, - acc: List(Result(Token, SyntaxError)), - input: String, -) -> Result(List(Result(Token, SyntaxError)), ParseError) { - case state { - Static(start, end, str) -> - case string.first(input) { - Ok("{") -> - step( - TagStart(end + 1), - [Ok(Constant(start, end, str)), ..acc], - string.drop_left(input, 1), - ) - Ok(char) -> - step( - Static(start, end + 1, string.append(str, char)), - acc, - string.drop_left(input, 1), - ) - Error(_) -> Ok([Ok(Constant(start, end, str)), ..acc]) - } - Tag(start, end, value) -> - case string.first(input) { - Ok("}") -> - step( - TagEnd(end + 1), - [ - { - let val = string.trim(value) - case string.first(val) { - Ok("#") -> - case string.split_once(string.drop_left(val, 1), " ") { - Ok(#(kind, body)) -> - Ok(BlockStart(start, end, kind, string.split(body, "."))) - Error(_) -> Error(MissingBlockKind(start, end)) - } - Ok("/") -> - case string.split_once(string.drop_left(val, 1), " ") { - Ok(#(_, _)) -> Error(UnexpectedBlockArgument(start, end)) - Error(_) -> - Ok(BlockEnd(start, end, string.drop_left(val, 1))) - } - Ok(_) -> Ok(Property(start, end, string.split(value, "."))) - Error(_) -> Error(EmptyExpression(start, end)) - } - }, - ..acc - ], - string.drop_left(input, 1), - ) - Ok(char) -> - step( - Tag(start, end + 1, string.append(value, char)), - acc, - string.drop_left(input, 1), - ) - Error(_) -> Error(UnexpectedEof(end)) - } - TagStart(start) -> - case string.first(input) { - Ok("{") -> - step(Tag(start + 1, start + 1, ""), acc, string.drop_left(input, 1)) - Ok(char) -> Error(UnexpectedToken(start, char)) - Error(_) -> Error(UnexpectedEof(start)) +fn balance_pass( + tokens: List(Result(lexer.Token, ParseError)), +) -> List(Result(lexer.Token, ParseError)) { + let #(out, stack) = + tokens + |> list.fold(#([], []), fn(acc, it) { + let #(out, stack) = acc + case it { + Ok(lexer.BlockStart(_, _, kind, _)) -> #([it, ..out], [kind, ..stack]) + Ok(lexer.BlockEnd(start, end, kind)) -> + case list.first(stack) { + Ok(top) if top == kind -> + case list.rest(stack) { + Ok(rest) -> #([it, ..out], rest) + _ -> #([Error(UnbalancedBlock(start, end, kind)), ..out], []) + } + _ -> + case list.rest(stack) { + Ok(rest) -> #([it, ..out], rest) + _ -> #([Error(UnbalancedBlock(start, end, kind)), ..out], []) + } + } + _ -> #([it, ..out], stack) } - TagEnd(start) -> - case string.first(input) { - Ok("}") -> - step( - Static(start + 1, start + 1, ""), - acc, - string.drop_left(input, 1), + }) + + list.concat([ + out, + list.map(stack, fn(it) { Error(UnbalancedBlock(-1, -1, it)) }), + ]) +} + +fn ast_transform_pass( + tokens: List(lexer.Token), + index: Int, + ast: List(AST), +) -> List(AST) { + case tokens { + [] -> list.reverse(ast) + [head, ..tail] -> { + case head { + lexer.Constant(_, _, value) -> + ast_transform_pass(tail, index + 1, [Constant(value), ..ast]) + lexer.Property(_, _, path) -> + ast_transform_pass(tail, index + 1, [Property(path), ..ast]) + lexer.BlockStart(_, _, kind, path) -> { + let children = ast_transform_pass(tail, index + 1, []) + ast_transform_pass( + list.drop(tail, list.length(children)), + index + 1 + list.length(children), + [Block(kind, path, children), ..ast], ) - Ok(char) -> Error(UnexpectedToken(start, char)) - Error(_) -> Error(UnexpectedEof(start)) + } + lexer.BlockEnd(_, _, _) -> list.reverse(ast) } + } } } -pub fn parse(template: String) -> Result(List(Token), ParseError) { - case step(Static(0, 0, ""), [], template) { - Ok(tokens) -> - case - tokens - |> result.partition - { - #(ok, []) -> Ok(ok) - #(_, err) -> Error(SyntaxError(err)) - } - Error(err) -> Error(err) +pub fn run( + tokens: List(lexer.Token), + valid_blocks: List(String), +) -> Result(List(AST), List(ParseError)) { + case + tokens + |> validation_pass(valid_blocks) + |> balance_pass + |> result.partition + { + #([lexer.Constant(_, _, ""), ..tokens], []) -> + // Remove the leading empty string present when the template starts with a tag + Ok(tokens |> ast_transform_pass(0, [])) + #(tokens, []) -> Ok(tokens |> ast_transform_pass(0, [])) + #(_, err) -> Error(err) } } diff --git a/test/api_test.gleam b/test/api_test.gleam new file mode 100644 index 0000000..73b659f --- /dev/null +++ b/test/api_test.gleam @@ -0,0 +1,49 @@ +import gleam/dict +import gleam/dynamic +import gleam/list +import gleeunit/should +import handles + +pub fn api_hello_world_test() { + handles.prepare("Hello {{name}}") + |> should.be_ok + |> handles.run( + dict.new() + |> dict.insert("name", "Oliver") + |> dynamic.from, + ) + |> should.be_ok + |> should.equal("Hello Oliver") +} + +pub fn api_knattarna_test() { + handles.prepare("{{#each knattarna}}Hello {{name}}\n{{/each}}") + |> should.be_ok + |> handles.run( + dict.new() + |> dict.insert( + "knattarna", + list.new() + |> list.append([ + dict.new() + |> dict.insert("name", "Knatte") + |> dynamic.from, + dict.new() + |> dict.insert("name", "Fnatte") + |> dynamic.from, + dict.new() + |> dict.insert("name", "Tjatte") + |> dynamic.from, + ]) + |> dynamic.from, + ) + |> dynamic.from, + ) + |> should.be_ok + |> should.equal( + "Hello Knatte +Hello Fnatte +Hello Tjatte +", + ) +} diff --git a/test/unit_tests/compiler_test.gleam b/test/unit_tests/compiler_test.gleam deleted file mode 100644 index 1ff9396..0000000 --- a/test/unit_tests/compiler_test.gleam +++ /dev/null @@ -1,67 +0,0 @@ -import gleeunit/should -import handles/compiler -import handles/parser - -pub fn compiler_should_return_correct_when_compiling_no_tokens_test() { - [] - |> compiler.compile([]) - |> should.be_ok - |> should.equal([]) -} - -pub fn compiler_should_return_correct_when_compiling_hello_world_test() { - [ - parser.Constant(0, 6, "Hello "), - parser.Property(8, 12, ["name"]), - parser.Constant(14, 15, "!"), - ] - |> compiler.compile([]) - |> should.be_ok - |> should.equal([ - compiler.Constant("Hello "), - compiler.Property(["name"]), - compiler.Constant("!"), - ]) -} - -pub fn compiler_should_return_correct_when_compiling_one_constant_test() { - [parser.Constant(0, 11, "Hello World")] - |> compiler.compile([]) - |> should.be_ok - |> should.equal([compiler.Constant("Hello World")]) -} - -pub fn compiler_should_return_correct_when_compiling_one_property_test() { - [parser.Property(0, 7, ["foo", "bar"])] - |> compiler.compile([]) - |> should.be_ok - |> should.equal([compiler.Property(["foo", "bar"])]) -} - -pub fn compiler_should_return_correct_when_compiling_one_block_with_arg_test() { - [parser.BlockStart(0, 7, "foo", ["bar", "biz"]), parser.BlockEnd(0, 7, "foo")] - |> compiler.compile(["foo"]) - |> should.be_ok - |> should.equal([compiler.Block("foo", ["bar", "biz"], [])]) -} - -pub fn compiler_should_return_correct_when_compiling_one_without_arg_test() { - [parser.BlockStart(0, 7, "foo", []), parser.BlockEnd(0, 7, "foo")] - |> compiler.compile(["foo"]) - |> should.be_ok - |> should.equal([compiler.Block("foo", [], [])]) -} - -pub fn compiler_should_return_error_when_providing_no_end_block_test() { - [parser.BlockStart(0, 8, "foo", [])] - |> compiler.compile(["foo"]) - |> should.be_error - |> should.equal([compiler.UnbalancedBlock(-1, -1, "foo")]) -} - -pub fn compiler_should_return_error_when_providing_unknown_block_kind_test() { - [parser.BlockStart(0, 8, "foo", [])] - |> compiler.compile([]) - |> should.be_error - |> should.equal([compiler.UnknownBlockKind(0, 8, "foo")]) -} diff --git a/test/unit_tests/engine_test.gleam b/test/unit_tests/engine_test.gleam index 03e33c8..ece1fa6 100644 --- a/test/unit_tests/engine_test.gleam +++ b/test/unit_tests/engine_test.gleam @@ -2,18 +2,18 @@ import gleam/dict import gleam/dynamic import gleam/list import gleeunit/should -import handles/compiler import handles/engine +import handles/parser pub fn engine_should_return_correct_when_running_hello_world_test() { - engine.run([compiler.Constant("Hello World")], Nil |> dynamic.from) + engine.run([parser.Constant("Hello World")], Nil |> dynamic.from) |> should.be_ok |> should.equal("Hello World") } pub fn engine_should_return_correct_when_running_hello_name_test() { engine.run( - [compiler.Constant("Hello "), compiler.Property(["name"])], + [parser.Constant("Hello "), parser.Property(["name"])], dict.new() |> dict.insert("name", "Oliver") |> dynamic.from, @@ -24,7 +24,7 @@ pub fn engine_should_return_correct_when_running_hello_name_test() { pub fn engine_should_return_correct_when_accessing_nested_property_test() { engine.run( - [compiler.Property(["foo", "bar"])], + [parser.Property(["foo", "bar"])], dict.new() |> dict.insert( "foo", @@ -39,7 +39,7 @@ pub fn engine_should_return_correct_when_accessing_nested_property_test() { pub fn engine_should_return_correct_when_using_truthy_if_test() { engine.run( - [compiler.Block("if", ["bool"], [compiler.Property(["foo", "bar"])])], + [parser.Block("if", ["bool"], [parser.Property(["foo", "bar"])])], dict.new() |> dict.insert( "foo", @@ -60,7 +60,7 @@ pub fn engine_should_return_correct_when_using_truthy_if_test() { pub fn engine_should_return_correct_when_using_falsy_if_test() { engine.run( - [compiler.Block("if", ["bool"], [compiler.Property(["foo", "bar"])])], + [parser.Block("if", ["bool"], [parser.Property(["foo", "bar"])])], dict.new() |> dict.insert( "bool", @@ -75,7 +75,7 @@ pub fn engine_should_return_correct_when_using_falsy_if_test() { pub fn engine_should_return_correct_when_using_truthy_unless_test() { engine.run( - [compiler.Block("unless", ["bool"], [compiler.Property(["foo", "bar"])])], + [parser.Block("unless", ["bool"], [parser.Property(["foo", "bar"])])], dict.new() |> dict.insert( "bool", @@ -90,7 +90,7 @@ pub fn engine_should_return_correct_when_using_truthy_unless_test() { pub fn engine_should_return_correct_when_using_falsy_unless_test() { engine.run( - [compiler.Block("unless", ["bool"], [compiler.Property(["foo", "bar"])])], + [parser.Block("unless", ["bool"], [parser.Property(["foo", "bar"])])], dict.new() |> dict.insert( "foo", @@ -112,9 +112,9 @@ pub fn engine_should_return_correct_when_using_falsy_unless_test() { pub fn engine_should_return_correct_when_using_each_test() { engine.run( [ - compiler.Block("each", ["list"], [ - compiler.Property(["name"]), - compiler.Constant(", "), + parser.Block("each", ["list"], [ + parser.Property(["name"]), + parser.Constant(", "), ]), ], dict.new() @@ -143,9 +143,9 @@ pub fn engine_should_return_correct_when_using_each_test() { pub fn engine_should_return_correct_when_using_empty_each_test() { engine.run( [ - compiler.Block("each", ["list"], [ - compiler.Property(["name"]), - compiler.Constant(", "), + parser.Block("each", ["list"], [ + parser.Property(["name"]), + parser.Constant(", "), ]), ], dict.new() diff --git a/test/unit_tests/format_test.gleam b/test/unit_tests/format_test.gleam index d6fb4da..5e2356e 100644 --- a/test/unit_tests/format_test.gleam +++ b/test/unit_tests/format_test.gleam @@ -1,17 +1,17 @@ -import handles/format import gleeunit/should -import handles/parser +import handles/format +import handles/lexer pub fn format_should_return_correct_string_for_unexpected_token_test() { let template = "{{foo}d" - should.be_error(parser.parse(template)) + should.be_error(lexer.run(template)) |> format.format_parse_error(template) |> should.equal("Unexpected token (row=0, col=6): d") } pub fn format_should_return_correct_string_for_unexpected_end_of_template_test() { let template = "{{foo}" - should.be_error(parser.parse(template)) + should.be_error(lexer.run(template)) |> format.format_parse_error(template) |> should.equal("Unexpected end of template (row=0, col=6)") } diff --git a/test/unit_tests/lexer_test.gleam b/test/unit_tests/lexer_test.gleam new file mode 100644 index 0000000..5de98f4 --- /dev/null +++ b/test/unit_tests/lexer_test.gleam @@ -0,0 +1,70 @@ +import gleeunit/should +import handles/lexer + +pub fn lexer_should_return_correct_when_parsing_empty_string_test() { + lexer.run("") + |> should.be_ok + |> should.equal([lexer.Constant(0, 0, "")]) +} + +pub fn lexer_should_return_correct_when_parsing_hello_world_test() { + lexer.run("Hello {{name}}!") + |> should.be_ok + |> should.equal([ + lexer.Constant(0, 6, "Hello "), + lexer.Property(8, 12, ["name"]), + lexer.Constant(14, 15, "!"), + ]) +} + +pub fn lexer_should_return_correct_when_passed_one_tag_test() { + lexer.run("{{foo}}") + |> should.be_ok + |> should.equal([ + lexer.Constant(0, 0, ""), + lexer.Property(2, 5, ["foo"]), + lexer.Constant(7, 7, ""), + ]) +} + +pub fn lexer_should_return_correct_when_passed_two_tags_test() { + lexer.run("{{foo}} {{bar}}") + |> should.be_ok + |> should.equal([ + lexer.Constant(0, 0, ""), + lexer.Property(2, 5, ["foo"]), + lexer.Constant(7, 8, " "), + lexer.Property(10, 13, ["bar"]), + lexer.Constant(15, 15, ""), + ]) +} + +pub fn lexer_should_return_lex_error_when_unexpected_token_test() { + lexer.run("{{foo}d") + |> should.be_error + |> should.equal(lexer.UnexpectedToken(6, "d")) +} + +pub fn lexer_should_return_lex_error_when_unexpected_end_of_template_test() { + lexer.run("{{foo}") + |> should.be_error + |> should.equal(lexer.UnexpectedEof(6)) +} + +pub fn compiler_should_return_error_when_missing_block_kind_test() { + lexer.run("{{#}}") + |> should.be_error + |> should.equal(lexer.SyntaxError([lexer.MissingBlockKind(2, 3)])) +} + +pub fn compiler_should_return_error_when_providing_arguments_to_end_block_test() { + lexer.run("{{/foo bar}}") + |> should.be_error + |> should.equal(lexer.SyntaxError([lexer.UnexpectedBlockArgument(2, 10)])) +} + +pub fn compiler_should_return_error_when_providing_empty_expression_test() { + lexer.run("{{}}") + |> should.be_error + |> should.equal(lexer.SyntaxError([lexer.EmptyExpression(2, 2)])) +} diff --git a/test/unit_tests/parser_test.gleam b/test/unit_tests/parser_test.gleam index ec4da46..f1fbf75 100644 --- a/test/unit_tests/parser_test.gleam +++ b/test/unit_tests/parser_test.gleam @@ -1,70 +1,67 @@ import gleeunit/should +import handles/lexer import handles/parser -pub fn parser_should_return_correct_when_parsing_empty_string_test() { - parser.parse("") +pub fn parser_should_return_correct_when_compiling_no_tokens_test() { + [] + |> parser.run([]) |> should.be_ok - |> should.equal([parser.Constant(0, 0, "")]) + |> should.equal([]) } -pub fn parser_should_return_correct_when_parsing_hello_world_test() { - parser.parse("Hello {{name}}!") +pub fn parser_should_return_correct_when_compiling_hello_world_test() { + [ + lexer.Constant(0, 6, "Hello "), + lexer.Property(8, 12, ["name"]), + lexer.Constant(14, 15, "!"), + ] + |> parser.run([]) |> should.be_ok |> should.equal([ - parser.Constant(0, 6, "Hello "), - parser.Property(8, 12, ["name"]), - parser.Constant(14, 15, "!"), + parser.Constant("Hello "), + parser.Property(["name"]), + parser.Constant("!"), ]) } -pub fn parser_should_return_correct_when_passed_one_tag_test() { - parser.parse("{{foo}}") +pub fn parser_should_return_correct_when_compiling_one_constant_test() { + [lexer.Constant(0, 11, "Hello World")] + |> parser.run([]) |> should.be_ok - |> should.equal([ - parser.Constant(0, 0, ""), - parser.Property(2, 5, ["foo"]), - parser.Constant(7, 7, ""), - ]) + |> should.equal([parser.Constant("Hello World")]) } -pub fn parser_should_return_correct_when_passed_two_tags_test() { - parser.parse("{{foo}} {{bar}}") +pub fn parser_should_return_correct_when_compiling_one_property_test() { + [lexer.Property(0, 7, ["foo", "bar"])] + |> parser.run([]) |> should.be_ok - |> should.equal([ - parser.Constant(0, 0, ""), - parser.Property(2, 5, ["foo"]), - parser.Constant(7, 8, " "), - parser.Property(10, 13, ["bar"]), - parser.Constant(15, 15, ""), - ]) + |> should.equal([parser.Property(["foo", "bar"])]) } -pub fn parser_should_return_parse_error_when_unexpected_token_test() { - parser.parse("{{foo}d") - |> should.be_error - |> should.equal(parser.UnexpectedToken(6, "d")) -} - -pub fn parser_should_return_parse_error_when_unexpected_end_of_template_test() { - parser.parse("{{foo}") - |> should.be_error - |> should.equal(parser.UnexpectedEof(6)) +pub fn parser_should_return_correct_when_compiling_one_block_with_arg_test() { + [lexer.BlockStart(0, 7, "foo", ["bar", "biz"]), lexer.BlockEnd(0, 7, "foo")] + |> parser.run(["foo"]) + |> should.be_ok + |> should.equal([parser.Block("foo", ["bar", "biz"], [])]) } -pub fn compiler_should_return_error_when_missing_block_kind_test() { - parser.parse("{{#}}") - |> should.be_error - |> should.equal(parser.SyntaxError([parser.MissingBlockKind(2, 3)])) +pub fn parser_should_return_correct_when_compiling_one_without_arg_test() { + [lexer.BlockStart(0, 7, "foo", []), lexer.BlockEnd(0, 7, "foo")] + |> parser.run(["foo"]) + |> should.be_ok + |> should.equal([parser.Block("foo", [], [])]) } -pub fn compiler_should_return_error_when_providing_arguments_to_end_block_test() { - parser.parse("{{/foo bar}}") +pub fn parser_should_return_error_when_providing_no_end_block_test() { + [lexer.BlockStart(0, 8, "foo", [])] + |> parser.run(["foo"]) |> should.be_error - |> should.equal(parser.SyntaxError([parser.UnexpectedBlockArgument(2, 10)])) + |> should.equal([parser.UnbalancedBlock(-1, -1, "foo")]) } -pub fn compiler_should_return_error_when_providing_empty_expression_test() { - parser.parse("{{}}") +pub fn parser_should_return_error_when_providing_unknown_block_kind_test() { + [lexer.BlockStart(0, 8, "foo", [])] + |> parser.run([]) |> should.be_error - |> should.equal(parser.SyntaxError([parser.EmptyExpression(2, 2)])) + |> should.equal([parser.UnknownBlockKind(0, 8, "foo")]) } diff --git a/test/user_stories/helloworld_test.gleam b/test/user_stories/helloworld_test.gleam deleted file mode 100644 index 64b1871..0000000 --- a/test/user_stories/helloworld_test.gleam +++ /dev/null @@ -1,17 +0,0 @@ -import gleam/dict -import gleam/dynamic -import gleeunit/should -import handles - -pub fn handles_hello_world_test() { - handles.prepare("Hello {{name}}") - |> should.be_ok - |> handles.run( - dict.new() - |> dict.insert("name", "Oliver") - |> dynamic.from, - ) - |> should.be_ok - |> should.equal("Hello Oliver") -} - diff --git a/test/user_stories/knattarna_test.gleam b/test/user_stories/knattarna_test.gleam index cef9091..7f6cb64 100644 --- a/test/user_stories/knattarna_test.gleam +++ b/test/user_stories/knattarna_test.gleam @@ -2,26 +2,26 @@ import gleam/dict import gleam/dynamic import gleam/list import gleeunit/should -import handles/compiler import handles/engine +import handles/lexer import handles/parser const input_template = "{{#each knattarna}}Hello {{name}}\n{{/each}}" const expected_tokens = [ - parser.Constant(0, 0, ""), parser.BlockStart(2, 17, "each", ["knattarna"]), - parser.Constant(19, 25, "Hello "), parser.Property(27, 31, ["name"]), - parser.Constant(33, 34, "\n"), parser.BlockEnd(36, 41, "each"), - parser.Constant(43, 43, ""), + lexer.Constant(0, 0, ""), lexer.BlockStart(2, 17, "each", ["knattarna"]), + lexer.Constant(19, 25, "Hello "), lexer.Property(27, 31, ["name"]), + lexer.Constant(33, 34, "\n"), lexer.BlockEnd(36, 41, "each"), + lexer.Constant(43, 43, ""), ] const expected_ast = [ - compiler.Block( + parser.Block( "each", ["knattarna"], [ - compiler.Constant("Hello "), compiler.Property(["name"]), - compiler.Constant("\n"), + parser.Constant("Hello "), parser.Property(["name"]), + parser.Constant("\n"), ], ), ] @@ -31,14 +31,14 @@ Hello Fnatte Hello Tjatte " -pub fn parser_should_return_correct_for_user_story_knattarna_test() { - parser.parse(input_template) +pub fn lexer_should_return_correct_for_user_story_knattarna_test() { + lexer.run(input_template) |> should.be_ok |> should.equal(expected_tokens) } -pub fn compiler_should_return_correct_for_user_story_knattarna_test() { - compiler.compile(expected_tokens, ["each"]) +pub fn parser_should_return_correct_for_user_story_knattarna_test() { + parser.run(expected_tokens, ["each"]) |> should.be_ok |> should.equal(expected_ast) }