From a1ceec90da789cc5bc61f88cb6dbe8fdb6c04c14 Mon Sep 17 00:00:00 2001 From: Gareth Pendleton Date: Fri, 26 Sep 2025 16:26:11 +0100 Subject: [PATCH] initial commit --- .github/workflows/test.yml | 23 + .gitignore | 4 + .gitmodules | 3 + README.md | 24 + gleam.toml | 23 + manifest.toml | 35 + src/TOML.abnf | 247 ++++++++ src/gleam-prod-javascript.lock | 0 src/gltoml.gleam | 1089 ++++++++++++++++++++++++++++++++ test/gltoml_test.gleam | 46 ++ test/scratch.gleam | 16 + toml-test | 1 + 12 files changed, 1511 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 gleam.toml create mode 100644 manifest.toml create mode 100644 src/TOML.abnf create mode 100644 src/gleam-prod-javascript.lock create mode 100644 src/gltoml.gleam create mode 100644 test/gltoml_test.gleam create mode 100644 test/scratch.gleam create mode 160000 toml-test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4a7fe22 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "27.1.2" + gleam-version: "1.12.0" + rebar3-version: "3" + # elixir-version: "1" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f3e16a4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "toml-test"] + path = toml-test + url = https://github.com/toml-lang/toml-test.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..89d1811 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# gltoml + +[![Package Version](https://img.shields.io/hexpm/v/gltoml)](https://hex.pm/packages/gltoml) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gltoml/) + +```sh +gleam add gltoml@1 +``` +```gleam +import gltoml + +pub fn main() -> Nil { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..45688c7 --- /dev/null +++ b/gleam.toml @@ -0,0 +1,23 @@ +name = "gltoml" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" +tom = ">= 2.0.0 and < 3.0.0" +gleam_time = ">= 1.4.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" +startest = ">= 0.7.0 and < 1.0.0" +simplifile = ">= 2.3.0 and < 3.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..c6cbe5e --- /dev/null +++ b/manifest.toml @@ -0,0 +1,35 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "bigben", version = "1.0.1", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "bigben", source = "hex", outer_checksum = "190E489610A80D76C48BACC75EB8314BD184FF0220AB0F251ABE760B993B91BB" }, + { name = "birl", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_regexp", "gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "2AC7BA26F998E3DFADDB657148BD5DDFE966958AD4D6D6957DD0D22E5B56C400" }, + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, + { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, + { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, + { name = "gleam_otp", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7987CBEBC8060B88F14575DEF546253F3116EBE2A5DA6FD82F38243FCE97C54B" }, + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, + { name = "gleam_stdlib", version = "0.63.2", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "962B25C667DA07F4CAB32001F44D3C41C1A89E58E3BBA54F183B482CF6122150" }, + { name = "gleam_time", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "DCDDC040CE97DA3D2A925CDBBA08D8A78681139745754A83998641C8A3F6587E" }, + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, + { name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" }, + { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" }, + { name = "ranger", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "ranger", source = "hex", outer_checksum = "C8988E8F8CDBD3E7F4D8F2E663EF76490390899C2B2885A6432E942495B3E854" }, + { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, + { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" }, + { name = "startest", version = "0.7.0", build_tools = ["gleam"], requirements = ["argv", "bigben", "birl", "exception", "gleam_community_ansi", "gleam_erlang", "gleam_javascript", "gleam_regexp", "gleam_stdlib", "glint", "simplifile", "tom"], otp_app = "startest", source = "hex", outer_checksum = "71B9CB82C4B8779A4BD54C7151DF7D0B0F778D0DDE805B782B44EFA7BA8F50DA" }, + { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleam_time = { version = ">= 1.4.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +simplifile = { version = ">= 2.3.0 and < 3.0.0" } +startest = { version = ">= 0.7.0 and < 1.0.0" } +tom = { version = ">= 2.0.0 and < 3.0.0" } diff --git a/src/TOML.abnf b/src/TOML.abnf new file mode 100644 index 0000000..3a53991 --- /dev/null +++ b/src/TOML.abnf @@ -0,0 +1,247 @@ +;; This document describes TOML's syntax, using the ABNF format (defined in +;; RFC 5234 -- https://www.ietf.org/rfc/rfc5234.txt). +;; +;; Although a TOML document must be valid UTF-8, this grammar refers to the +;; Unicode Code Points you get after you decode the UTF-8 input. +;; +;; All valid TOML documents will match this description, however certain +;; invalid documents would need to be rejected as per the semantics described +;; in the supporting text description. + +;; It is possible to try this grammar interactively, using instaparse. +;; http://instaparse.mojombo.com/ +;; +;; To do so, in the lower right, click on Options and change `:input-format` to +;; ':abnf'. Then paste this entire ABNF document into the grammar entry box +;; (above the options). Then you can type or paste a sample TOML document into +;; the beige box on the left. Tada! + +;; Overall Structure + +toml = expression *( newline expression ) + +expression = ws [ comment ] +expression =/ ws keyval ws [ comment ] +expression =/ ws table ws [ comment ] + +;; Whitespace + +ws = *wschar +wschar = %x20 ; Space +wschar =/ %x09 ; Horizontal tab + +;; Newline + +newline = %x0A ; LF +newline =/ %x0D.0A ; CRLF + +;; Comment + +comment-start-symbol = %x23 ; # +non-ascii = %x80-D7FF / %xE000-10FFFF +non-eol = %x09 / %x20-7E / non-ascii + +comment = comment-start-symbol *non-eol + +;; Key-Value pairs + +keyval = key keyval-sep val +key = simple-key / dotted-key +val = string / boolean / array / inline-table / date-time / float / integer + +simple-key = quoted-key / unquoted-key +unquoted-key = 1*( ALPHA / DIGIT / %x2D / %x5F ) ; A-Z / a-z / 0-9 / - / _ + +;; Quoted and dotted key + +quoted-key = basic-string / literal-string +dotted-key = simple-key 1*( dot-sep simple-key ) + +dot-sep = ws %x2E ws ; . Period +keyval-sep = ws %x3D ws ; = + +;; String + +string = ml-basic-string / basic-string / ml-literal-string / literal-string + +;; Basic String + +basic-string = quotation-mark *basic-char quotation-mark + +quotation-mark = %x22 ; " + +basic-char = basic-unescaped / escaped +basic-unescaped = wschar / %x21 / %x23-5B / %x5D-7E / non-ascii +escaped = escape escape-seq-char + +escape = %x5C ; \ +escape-seq-char = %x22 ; " quotation mark U+0022 +escape-seq-char =/ %x5C ; \ reverse solidus U+005C +escape-seq-char =/ %x62 ; b backspace U+0008 +escape-seq-char =/ %x65 ; e escape U+001B +escape-seq-char =/ %x66 ; f form feed U+000C +escape-seq-char =/ %x6E ; n line feed U+000A +escape-seq-char =/ %x72 ; r carriage return U+000D +escape-seq-char =/ %x74 ; t tab U+0009 +escape-seq-char =/ %x78 2HEXDIG ; xHH U+00HH +escape-seq-char =/ %x75 4HEXDIG ; uHHHH U+HHHH +escape-seq-char =/ %x55 8HEXDIG ; UHHHHHHHH U+HHHHHHHH + +;; Multiline Basic String + +ml-basic-string = ml-basic-string-delim [ newline ] ml-basic-body + ml-basic-string-delim +ml-basic-string-delim = 3quotation-mark +ml-basic-body = *mlb-content *( mlb-quotes 1*mlb-content ) [ mlb-quotes ] + +mlb-content = basic-char / newline / mlb-escaped-nl +mlb-quotes = 1*2quotation-mark +mlb-escaped-nl = escape ws newline *( wschar / newline ) + +;; Literal String + +literal-string = apostrophe *literal-char apostrophe + +apostrophe = %x27 ; ' apostrophe + +literal-char = %x09 / %x20-26 / %x28-7E / non-ascii + +;; Multiline Literal String + +ml-literal-string = ml-literal-string-delim [ newline ] ml-literal-body + ml-literal-string-delim +ml-literal-string-delim = 3apostrophe +ml-literal-body = *mll-content *( mll-quotes 1*mll-content ) [ mll-quotes ] + +mll-content = literal-char / newline +mll-quotes = 1*2apostrophe + +;; Integer + +integer = dec-int / hex-int / oct-int / bin-int + +minus = %x2D ; - +plus = %x2B ; + +underscore = %x5F ; _ +digit1-9 = %x31-39 ; 1-9 +digit0-7 = %x30-37 ; 0-7 +digit0-1 = %x30-31 ; 0-1 + +hex-prefix = %x30.78 ; 0x +oct-prefix = %x30.6F ; 0o +bin-prefix = %x30.62 ; 0b + +dec-int = [ minus / plus ] unsigned-dec-int +unsigned-dec-int = DIGIT / digit1-9 1*( DIGIT / underscore DIGIT ) + +hex-int = hex-prefix HEXDIG *( HEXDIG / underscore HEXDIG ) +oct-int = oct-prefix digit0-7 *( digit0-7 / underscore digit0-7 ) +bin-int = bin-prefix digit0-1 *( digit0-1 / underscore digit0-1 ) + +;; Float + +float = float-int-part ( exp / frac [ exp ] ) +float =/ special-float + +float-int-part = dec-int +frac = decimal-point zero-prefixable-int +decimal-point = %x2E ; . +zero-prefixable-int = DIGIT *( DIGIT / underscore DIGIT ) + +exp = "e" float-exp-part +float-exp-part = [ minus / plus ] zero-prefixable-int + +special-float = [ minus / plus ] ( inf / nan ) +inf = %x69.6E.66 ; inf +nan = %x6E.61.6E ; nan + +;; Boolean + +boolean = true / false + +true = %x74.72.75.65 ; true +false = %x66.61.6C.73.65 ; false + +;; Date and Time (as defined in RFC 3339) + +date-time = offset-date-time / local-date-time / local-date / local-time + +date-fullyear = 4DIGIT +date-month = 2DIGIT ; 01-12 +date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on month/year +time-delim = "T" / %x20 ; T, t, or space +time-hour = 2DIGIT ; 00-23 +time-minute = 2DIGIT ; 00-59 +time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules +time-secfrac = "." 1*DIGIT +time-numoffset = ( "+" / "-" ) time-hour ":" time-minute +time-offset = "Z" / time-numoffset + +partial-time = time-hour ":" time-minute [ ":" time-second [ time-secfrac ] ] +full-date = date-fullyear "-" date-month "-" date-mday +full-time = partial-time time-offset + +;; Offset Date-Time + +offset-date-time = full-date time-delim full-time + +;; Local Date-Time + +local-date-time = full-date time-delim partial-time + +;; Local Date + +local-date = full-date + +;; Local Time + +local-time = partial-time + +;; Array + +array = array-open [ array-values ] ws-comment-newline array-close + +array-open = %x5B ; [ +array-close = %x5D ; ] + +array-values = ws-comment-newline val ws-comment-newline array-sep array-values +array-values =/ ws-comment-newline val ws-comment-newline [ array-sep ] + +array-sep = %x2C ; , Comma + +ws-comment-newline = *( wschar / [ comment ] newline ) + +;; Table + +table = std-table / array-table + +;; Standard Table + +std-table = std-table-open key std-table-close + +std-table-open = %x5B ws ; [ Left square bracket +std-table-close = ws %x5D ; ] Right square bracket + +;; Inline Table + +inline-table = inline-table-open [ inline-table-keyvals ] ws-comment-newline inline-table-close + +inline-table-open = %x7B ; { +inline-table-close = %x7D ; } +inline-table-sep = %x2C ; , Comma + +inline-table-keyvals = ws-comment-newline keyval ws-comment-newline inline-table-sep inline-table-keyvals +inline-table-keyvals =/ ws-comment-newline keyval ws-comment-newline [ inline-table-sep ] + +;; Array Table + +array-table = array-table-open key array-table-close + +array-table-open = %x5B.5B ws ; [[ Double left square bracket +array-table-close = ws %x5D.5D ; ]] Double right square bracket + +;; Built-in ABNF terms, reproduced here for clarity + +ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +DIGIT = %x30-39 ; 0-9 +HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" \ No newline at end of file diff --git a/src/gleam-prod-javascript.lock b/src/gleam-prod-javascript.lock new file mode 100644 index 0000000..e69de29 diff --git a/src/gltoml.gleam b/src/gltoml.gleam new file mode 100644 index 0000000..38ecdd4 --- /dev/null +++ b/src/gltoml.gleam @@ -0,0 +1,1089 @@ +import gleam/bool +import gleam/dict.{type Dict} +import gleam/float +import gleam/int +import gleam/list +import gleam/result +import gleam/string +import gleam/time/calendar +import gleam/time/duration +import tom.{type Toml} + +/// An error that can occur when parsing a TOML document. +pub type ParseError { + /// An unexpected character was encountered when parsing the document. + Unexpected(got: String, expected: String) + /// More than one items have the same key in the document. + KeyAlreadyInUse(key: List(String)) + Internal + EOF +} + +pub fn parse(input: String) -> Result(Dict(String, Toml), ParseError) { + do_parse_toml(input) +} + +fn do_parse_toml(input: String) -> Result(Dict(String, Toml), ParseError) { + use #(toml, input) <- result.try(do_parse_expression(input, [], dict.new())) + case input { + "" -> Ok(toml) + input -> Error(Unexpected(input, "EOF")) + } +} + +fn do_parse_expression( + input: String, + current_path: List(String), + toml: Dict(String, Toml), +) -> Result(#(Dict(String, Toml), String), ParseError) { + let input = trim_whitespace(input) + case do_parse_table(input) { + Ok(#(new_root, input)) -> { + case exists(toml, new_root) { + True -> { + Error( + KeyAlreadyInUse(list.append( + current_path, + string.split(new_root, "."), + )), + ) + } + False -> { + case input { + "" -> Ok(#(toml, "")) + "\n" <> input | "\r\n" <> input -> + do_parse_expression( + input, + list.append(current_path, string.split(new_root, ".")), + toml, + ) + _ -> Error(Unexpected(input, "EOF")) + } + } + } + } + Error(_) -> { + let new_input = do_parse_comment(input) + use #(input, toml) <- result.try(case new_input == input { + True -> { + use #(key, val, input) <- result.try(do_parse_keyval(new_input)) + let input = trim_whitespace(input) + let full_path = list.append(current_path, string.split(key, ".")) + use toml <- result.try(insert_into_dict( + toml, + full_path, + val, + full_path, + )) + Ok(#(do_parse_comment(input), toml)) + } + False -> Ok(#(new_input, toml)) + }) + + case input { + "" -> Ok(#(toml, "")) + "\n" <> input | "\r\n" <> input -> + do_parse_expression(input, current_path, toml) + _ -> Error(Unexpected(input, "EOF")) + } + } + } +} + +fn insert_into_dict( + toml: Dict(String, Toml), + key: List(String), + val: Toml, + full_path: List(String), +) -> Result(dict.Dict(String, Toml), ParseError) { + key |> echo + toml |> echo + case key { + [key] -> { + case dict.has_key(toml, key) { + True -> Error(KeyAlreadyInUse(full_path)) + False -> Ok(dict.insert(toml, key, val)) + } + } + [node, ..rest] -> { + case dict.get(toml, node) |> echo { + Ok(tom.Table(d)) -> { + use new_d <- result.try(insert_into_dict(d, rest, val, full_path)) + Ok(dict.insert(toml, node, tom.Table(new_d))) + } + Ok(_) -> Error(KeyAlreadyInUse(full_path)) + Error(_) -> { + use new_d <- result.try(insert_into_dict( + dict.new(), + rest, + val, + full_path, + )) + Ok(dict.insert(toml, node, tom.Table(new_d))) + } + } + } + [] -> panic + } +} + +fn exists(toml: Dict(String, Toml), path: String) -> Bool { + let #(_, exists) = + list.fold_until(string.split(path, "."), #(toml, False), fn(acc, path_part) { + case dict.get(acc.0, path_part) { + Ok(tom.Table(d)) | Ok(tom.InlineTable(d)) -> { + case dict.size(d) { + 0 -> list.Stop(#(d, True)) + _ -> list.Continue(#(d, False)) + } + } + Ok(_) -> list.Stop(#(toml, True)) + Error(_) -> list.Stop(#(toml, False)) + } + }) + exists +} + +// fn do_parse_expression_old( +// input: String, +// root: Dict(String, Toml), +// toml: Dict(String, Toml), +// ) -> Result(#(Dict(String, Toml), String), ParseError) { +// let input = trim_whitespace(input) +// use #(toml2, input) <- result.try(try_parsers( +// [ +// do_parse_keyval, +// do_parse_table, +// parse_optional_result( +// _, +// fn(i) { +// let input = do_parse_comment(i) +// Ok(#(dict.new(), input)) +// }, +// fn() { dict.new() }, +// ), +// ], +// input, +// )) +// let input = trim_whitespace(input) +// let input = do_parse_comment(input) +// case input { +// "" -> Ok(#(merge(toml, toml2), "")) +// "\n" <> input | "\r\n" <> input -> +// do_parse_expression(input, dict.merge(toml, toml2)) +// _ -> Error(Unexpected(input, "EOF")) +// } +// } + +fn merge( + d1: dict.Dict(String, Toml), + d2: dict.Dict(String, Toml), +) -> dict.Dict(String, Toml) { + dict.merge(d1, d2) +} + +fn trim_whitespace(input: String) -> String { + case input { + " " <> input | "\t" <> input -> trim_whitespace(input) + _ -> input + } +} + +fn do_parse_keyval(input: String) -> Result(#(String, Toml, String), ParseError) { + use #(key, input) <- result.try(parse_key(input)) + + let input = trim_whitespace(input) + case input { + "=" <> input -> { + let input = trim_whitespace(input) + use #(val, input) <- result.try(parse_val(input)) + + Ok(#(key, val, input)) + } + input -> Error(Unexpected(string.slice(input, 0, 1), "=")) + } +} + +fn parse_key(input: String) { + use #(key, input) <- result.try(do_parse_simple_key(input)) + + Ok(case input { + "." <> _ -> do_parse_dotted_key(input, key) + _ -> #(key, input) + }) +} + +fn parse_val(input: String) -> Result(#(Toml, String), ParseError) { + try_parsers( + [ + parse_string_to_toml, + parse_boolean_to_toml, + parse_date_to_toml, + parse_float_to_toml, + parse_integer_to_toml, + parse_array_to_toml, + parse_inline_table_to_toml, + ], + input, + ) +} + +fn parse_inline_table_to_toml( + input: String, +) -> Result(#(Toml, String), ParseError) { + case input { + "{" <> input -> { + use #(array, input) <- result.try( + parse_optional_result( + input, + parse_inline_table_keyvals(_, dict.new()), + fn() { tom.InlineTable(dict.new()) }, + ), + ) + let input = parse_ws_comment(input) + input |> echo + case input { + "}" <> input -> Ok(#(array, input)) + _ -> Error(Unexpected(string.slice(input, 0, 1), "]")) + } + } + _ -> Error(Internal) + } +} + +fn parse_inline_table_keyvals( + input: String, + table: dict.Dict(String, Toml), +) -> Result(#(Toml, String), ParseError) { + let input = parse_ws_comment(input) + + use #(key, val, input) <- result.try(do_parse_keyval(input)) + key |> echo + table |> echo + let input = parse_ws_comment(input) + + case input { + "," <> input -> { + let full_path = string.split(key, ".") + use new_table <- result.try(insert_into_dict( + table, + full_path, + val, + full_path, + )) + case parse_inline_table_keyvals(input, new_table) { + Ok(#(table, input)) -> Ok(#(table, input)) + Error(_) -> { + let input = parse_ws_comment(input) + Ok(#(tom.InlineTable(new_table), input)) + } + } + } + _ -> { + let full_path = string.split(key, ".") + use new_d <- result.try(insert_into_dict(table, full_path, val, full_path)) + Ok(#(tom.InlineTable(new_d), input)) + } + } +} + +// fn create_dict(key: String, val: Toml) -> dict.Dict(String, Toml) { +// case string.split_once(key, ".") { +// Ok(#(key, tail)) -> { +// dict.insert(dict.new(), key, tom.Table(create_dict(tail, val))) +// } +// Error(_) -> dict.insert(dict.new(), key, val) +// } +// } + +fn parse_array_to_toml(input: String) -> Result(#(Toml, String), ParseError) { + case input { + "[" <> input -> { + use #(array, input) <- result.try( + parse_optional_result(input, parse_array_values(_, []), fn() { + tom.Array([]) + }), + ) + let input = parse_ws_comment(input) + case input { + "]" <> input -> Ok(#(array, input)) + _ -> Error(Unexpected(string.slice(input, 0, 1), "]")) + } + } + _ -> Error(Internal) + } +} + +fn parse_array_values( + input: String, + array: List(Toml), +) -> Result(#(Toml, String), ParseError) { + let input = parse_ws_comment(input) + use #(val, input) <- result.try(parse_val(input)) + let input = parse_ws_comment(input) + case input { + "," <> input -> { + let new_array = [val, ..array] + case parse_array_values(input, new_array) { + Ok(#(arr, input)) -> Ok(#(arr, input)) + Error(_) -> { + let input = parse_ws_comment(input) + Ok(#(tom.Array(new_array |> list.reverse), input)) + } + } + } + _ -> { + Ok(#(tom.Array([val, ..array] |> list.reverse), input)) + } + } +} + +fn parse_ws_comment(input: String) -> String { + case + parse_min_max( + input, + 0, + -1, + fn(i) { + let new_i = do_parse_wscomment_newline(i) + case i == new_i { + True -> Error(Nil) + False -> Ok(#(Nil, new_i)) + } + }, + Nil, + fn(_, _) { Nil }, + ) + { + Ok(#(_, input)) -> input + _ -> input + } +} + +fn do_parse_simple_key(input: String) -> Result(#(String, String), ParseError) { + try_parsers([do_parse_quoted_key, do_parse_unquoted_key], input) +} + +fn parse_string_to_toml(input: String) -> Result(#(Toml, String), ParseError) { + use #(str, input) <- result.try(try_parsers( + [ + parse_basic_string, + parse_literal_string, + parse_ml_basic_string, + parse_ml_literal_string, + ], + input, + )) + + Ok(#(tom.String(str), input)) +} + +fn parse_boolean_to_toml(input: String) -> Result(#(Toml, String), ParseError) { + case input { + "true" <> input -> Ok(#(tom.Bool(True), input)) + "false" <> input -> Ok(#(tom.Bool(False), input)) + _ -> Error(Internal) + } +} + +fn parse_date_to_toml(input: String) -> Result(#(Toml, String), ParseError) { + parse_date_time(input) +} + +fn parse_date_time(input: String) -> Result(#(Toml, String), ParseError) { + use #(date, input) <- result.try(parse_full_date(input)) + case input { + "T" <> input | "t" <> input | " " <> input -> { + use #(time, offset, input) <- result.try(parse_time(input)) + Ok(#(tom.DateTime(date, time, offset), input)) + } + _ -> Ok(#(tom.Date(date), input)) + } +} + +fn parse_time( + input: String, +) -> Result(#(calendar.TimeOfDay, tom.Offset, String), ParseError) { + use #(partial_time, input) <- result.try( + parse_this_then(input, [ + parse_min_max(_, 2, 2, parse_digit, "", string.append), + fn(i) { + case i { + ":" <> input -> Ok(#(":", input)) + _ -> Error(Unexpected(string.slice(input, 0, 1), "-")) + } + }, + parse_min_max(_, 2, 2, parse_digit, "", string.append), + parse_optional_result( + _, + parse_this_then(_, [ + fn(i) { + case i { + ":" <> input -> Ok(#(":", input)) + _ -> Error(Unexpected(string.slice(input, 0, 1), ":")) + } + }, + parse_min_max(_, 2, 2, parse_digit, "", string.append), + parse_optional_result( + _, + parse_this_then(_, [ + fn(i) { + case i { + "." <> input -> Ok(#(".", input)) + _ -> + Error(Unexpected( + string.first(input) |> result.unwrap(""), + ".", + )) + } + }, + parse_min_max(_, 1, -1, parse_digit, "", string.append), + ]), + fn() { "" }, + ), + ]), + fn() { "" }, + ), + ]), + ) + + let split_time = string.split(partial_time, ":") + let #(hour, minute, second, ns) = case split_time { + [hour, minute] -> { + let assert Ok(hour) = int.parse(hour) + let assert Ok(minute) = int.parse(minute) + #(hour, minute, 0, 0) + } + [hour, minute, seconds] -> { + let assert Ok(hour) = int.parse(hour) + let assert Ok(minute) = int.parse(minute) + case string.split(seconds, ".") { + [second] -> { + let assert Ok(second) = int.parse(second) + #(hour, minute, second, 0) + } + [second, ns] -> { + let assert Ok(ns) = int.parse(ns) + let assert Ok(second) = int.parse(second) + #(hour, minute, second, ns) + } + _ -> panic + } + } + _ -> panic + } + + use #(offset, input) <- result.try(case input { + "Z" <> input -> Ok(#(tom.Offset(calendar.utc_offset), input)) + "+" as dir <> input | "-" as dir <> input -> { + use #(offset, input) <- result.try( + parse_this_then(input, [ + parse_min_max(_, 2, 2, parse_digit, "", string.append), + fn(i) { + case i { + ":" <> input -> Ok(#(":", input)) + _ -> Error(Unexpected(string.slice(input, 0, 1), ":")) + } + }, + parse_min_max(_, 2, 2, parse_digit, "", string.append), + ]), + ) + let assert [hours, minutes] = string.split(offset, ":") + let assert Ok(hours) = int.parse(hours) + let assert Ok(minutes) = int.parse(minutes) + let duration = case dir { + "+" -> duration.add(duration.hours(hours), duration.minutes(minutes)) + "-" -> duration.add(duration.hours(-hours), duration.minutes(-minutes)) + _ -> panic + } + Ok(#(tom.Offset(duration), input)) + } + _ -> Ok(#(tom.Local, input)) + }) + + Ok(#(calendar.TimeOfDay(hour, minute, second, ns), offset, input)) +} + +fn parse_full_date( + input: String, +) -> Result(#(calendar.Date, String), ParseError) { + use #(offset_date, input) <- result.try( + parse_this_then(input, [ + parse_min_max(_, 4, 4, parse_digit, "", string.append), + fn(i) { + case i { + "-" <> input -> Ok(#("-", input)) + _ -> Error(Unexpected(string.slice(input, 0, 1), "-")) + } + }, + parse_min_max(_, 2, 2, parse_digit, "", string.append), + fn(i) { + case i { + "-" <> input -> Ok(#("-", input)) + _ -> Error(Unexpected(string.slice(input, 0, 1), "-")) + } + }, + parse_min_max(_, 2, 2, parse_digit, "", string.append), + ]), + ) + let assert [year, month, day] = string.split(offset_date, "-") + let assert Ok(year) = int.parse(year) + let assert Ok(month) = int.parse(month) + use month <- result.try( + calendar.month_from_int(month) + |> result.replace_error(Unexpected(offset_date, "Month")), + ) + let assert Ok(day) = int.parse(day) + + let date = calendar.Date(year, month, day) + use <- bool.guard( + when: !calendar.is_valid_date(date), + return: Error(Unexpected(offset_date, "Date")), + ) + + Ok(#(date, input)) +} + +fn parse_float_to_toml(input: String) -> Result(#(Toml, String), ParseError) { + case input { + "-inf" <> input -> Ok(#(tom.Infinity(tom.Negative), input)) + "+inf" <> input | "inf" <> input -> Ok(#(tom.Infinity(tom.Positive), input)) + "-nan" <> input -> Ok(#(tom.Nan(tom.Negative), input)) + "+nan" <> input | "nan" <> input -> Ok(#(tom.Nan(tom.Positive), input)) + input -> { + use #(int, input) <- result.try(parse_dec_int(input)) + use #(exp, frac, input) <- result.try(case input { + "e" <> input -> { + use #(exp, input) <- result.try(parse_int( + input, + parse_digit, + parse_digit, + )) + Ok(#(exp, "0", input)) + } + "." <> input -> { + use #(frac, input) <- result.try(parse_int( + input, + parse_digit, + parse_digit, + )) + case input { + "e" <> input -> { + use #(exp, input) <- result.try(parse_int( + input, + parse_digit, + parse_digit, + )) + Ok(#(exp, frac, input)) + } + _ -> Ok(#("0.0", frac, input)) + } + } + _ -> Error(Internal) + }) + use float <- result.try( + float.parse(int <> "." <> frac) + |> result.replace_error(Unexpected(int <> "." <> frac, "decimal")), + ) + use exp <- result.try( + case string.contains(exp, ".") { + True -> float.parse(exp) + False -> { + use exp <- result.try(int.parse(exp)) + int.to_float(exp) |> Ok + } + } + |> result.replace_error(Unexpected(exp, "decimal")), + ) + let multiplier = case float.power(10.0, exp) { + Ok(multiplier) -> multiplier + Error(_) -> 1.0 + } + Ok(#(tom.Float(float *. multiplier), input)) + } + } +} + +fn parse_integer_to_toml(input: String) -> Result(#(Toml, String), ParseError) { + case input { + "0x" <> input -> { + use #(int, input) <- result.try(parse_int( + input, + parse_hex_digit, + parse_hex_digit, + )) + use i <- result.try( + int.base_parse(int, 16) |> result.replace_error(Unexpected(int, "hex")), + ) + Ok(#(tom.Int(i), input)) + } + "0o" <> input -> { + use #(int, input) <- result.try(parse_int( + input, + parse_oct_digit, + parse_oct_digit, + )) + use i <- result.try( + int.base_parse(int, 8) + |> result.replace_error(Unexpected(int, "octal")), + ) + Ok(#(tom.Int(i), input)) + } + "0b" <> input -> { + use #(int, input) <- result.try(parse_int( + input, + parse_bin_digit, + parse_bin_digit, + )) + use i <- result.try( + int.base_parse(int, 2) + |> result.replace_error(Unexpected(int, "binary")), + ) + Ok(#(tom.Int(i), input)) + } + _ -> { + use #(int, input) <- result.try(parse_dec_int(input)) + + use i <- result.try( + int.base_parse(int, 10) + |> result.replace_error(Unexpected(int, "decimal")), + ) + Ok(#(tom.Int(i), input)) + } + } +} + +fn parse_dec_int(input: String) -> Result(#(String, String), ParseError) { + let #(lead, input) = case input { + "-" <> input -> #("-", input) + "+" <> input | input -> #("", input) + } + case input { + "0" <> input -> Ok(#(lead <> "0", input)) + input -> { + parse_int(input, parse_digit_nz, parse_digit) + |> result.map(fn(i) { #(lead <> i.0, i.1) }) + } + } +} + +fn parse_int( + input: String, + initial_type_fn: fn(String) -> Result(#(String, String), ParseError), + type_fn: fn(String) -> Result(#(String, String), ParseError), +) -> Result(#(String, String), ParseError) { + use #(digit1, input) <- result.try(initial_type_fn(input)) + let #(digit2, input) = + parse_multiple_optional(input, try_parsers( + [ + type_fn, + parse_this_then(_, [ + fn(input) { + case input { + "_" <> input -> Ok(#("", input)) + _ -> Error(Internal) + } + }, + type_fn, + ]), + ], + _, + )) + Ok(#(digit1 <> digit2, input)) +} + +pub fn parse_hex_digit(str: String) -> Result(#(String, String), ParseError) { + case string.pop_grapheme(str) { + Ok(#(char, tail)) -> { + let assert [codepoint] = string.to_utf_codepoints(char) + let i = string.utf_codepoint_to_int(codepoint) + case i { + _ if i >= 0x30 && i <= 0x39 -> Ok(#(char, tail)) + _ if i >= 0x41 && i <= 0x46 -> Ok(#(char, tail)) + _ if i >= 0x61 && i <= 0x66 -> Ok(#(char, tail)) + _ -> Error(Internal) + } + } + Error(_) -> Error(Internal) + } +} + +pub fn parse_oct_digit(str: String) -> Result(#(String, String), ParseError) { + case string.pop_grapheme(str) { + Ok(#(char, tail)) -> { + let assert [codepoint] = string.to_utf_codepoints(char) + let i = string.utf_codepoint_to_int(codepoint) + case i { + _ if i >= 0x30 && i <= 0x37 -> Ok(#(char, tail)) + _ -> Error(Internal) + } + } + Error(_) -> Error(Internal) + } +} + +fn parse_bin_digit(input: String) -> Result(#(String, String), ParseError) { + case input { + "0" as d <> tail | "1" as d <> tail -> Ok(#(d, tail)) + _ -> Error(Internal) + } +} + +fn do_parse_dotted_key(input: String, key: String) -> #(String, String) { + case + parse_min_max( + input, + 1, + -1, + parse_this_then(_, [ + fn(input) { + case input { + "." <> input -> Ok(#(".", input)) + _ -> Error(Internal) + } + }, + do_parse_simple_key, + ]), + "", + string.append, + ) + { + Error(_) -> #(key, input) + Ok(#(k, v)) -> #(key <> k, v) + } +} + +fn do_parse_quoted_key(input: String) -> Result(#(String, String), ParseError) { + try_parsers([parse_basic_string, parse_literal_string], input) +} + +fn do_parse_unquoted_key(input: String) -> Result(#(String, String), ParseError) { + parse_multiple(input, try_parsers( + [ + parse_alpha, + parse_digit, + fn(i) { + case i { + "-" as c <> tail | "_" as c <> tail -> Ok(#(c, tail)) + _ -> Error(Internal) + } + }, + ], + _, + )) +} + +fn parse_basic_string(input: String) -> Result(#(String, String), ParseError) { + case input { + "\"" <> input -> { + let #(str, input) = parse_multiple_optional(input, parse_basic_char) + case input { + "\"" <> input -> Ok(#(str, input)) + _ -> Error(Unexpected(string.slice(input, 0, 1), "\"")) + } + } + _ -> Error(Internal) + } +} + +fn parse_literal_string(input: String) -> Result(#(String, String), ParseError) { + case input { + "'" <> input -> { + let #(str, input) = parse_multiple_optional(input, parse_literal_char) + case input { + "'" <> input -> Ok(#(str, input)) + _ -> Error(Unexpected(string.slice(input, 0, 1), "'")) + } + } + _ -> Error(Internal) + } +} + +fn parse_ml_basic_string(input: String) -> Result(#(String, String), ParseError) { + Error(Internal) +} + +fn parse_ml_literal_string( + input: String, +) -> Result(#(String, String), ParseError) { + Error(Internal) +} + +fn parse_basic_char(input: String) -> Result(#(String, String), ParseError) { + case string.pop_grapheme(input) { + Ok(#(char, tail)) -> { + let assert [codepoint] = string.to_utf_codepoints(char) + case string.utf_codepoint_to_int(codepoint) { + i if i >= 0x23 && i <= 0x5B -> Ok(#(char, tail)) + i if i >= 0x5D && i <= 0x7E -> Ok(#(char, tail)) + i if i == 0x20 -> Ok(#(char, tail)) + i if i == 0x09 -> Ok(#(char, tail)) + i if i == 0x21 -> Ok(#(char, tail)) + i if i >= 0x80 -> Ok(#(char, tail)) + _ -> Error(Internal) + } + } + Error(_) -> Error(Internal) + } +} + +fn parse_literal_char(input: String) -> Result(#(String, String), ParseError) { + case string.pop_grapheme(input) { + Ok(#(char, tail)) -> { + let assert [codepoint] = string.to_utf_codepoints(char) + case string.utf_codepoint_to_int(codepoint) { + i if i >= 0x28 && i <= 0x7E -> Ok(#(char, tail)) + i if i == 0x09 -> Ok(#(char, tail)) + i if i >= 0x20 && i <= 0x26 -> Ok(#(char, tail)) + i if i >= 0x80 -> Ok(#(char, tail)) + _ -> Error(Internal) + } + } + Error(_) -> Error(Internal) + } +} + +fn do_parse_table(input: String) -> Result(#(String, String), ParseError) { + case input { + "[[" <> input -> { + let input = trim_whitespace(input) + use #(key, input) <- result.try(parse_array_table(input)) + let input = trim_whitespace(input) + case input { + "]]" <> input -> { + Ok(#(key, input)) + } + _ -> Error(Unexpected(string.slice(input, 0, 2), "]]")) + } + } + "[" <> input -> { + let input = trim_whitespace(input) + use #(key, input) <- result.try(parse_key(input)) + let input = trim_whitespace(input) + case input { + "]" <> input -> { + Ok(#(key, input)) + } + _ -> Error(Unexpected(string.slice(input, 0, 1), "]")) + } + } + _ -> Error(Internal) + } +} + +fn parse_array_table(input: String) -> Result(#(String, String), ParseError) { + todo +} + +fn do_parse_comment(input: String) -> String { + case input { + "#" <> input -> { + do_parse_comment_parts(input) + } + _ -> input + } +} + +fn do_parse_wscomment_newline(input: String) -> String { + let input = trim_whitespace(input) + let input = do_parse_comment(input) + case input { + "\n" <> input -> input + _ -> input + } +} + +fn do_parse_comment_parts(input: String) -> String { + case parse_non_eol(input) { + Ok(#(_, input)) -> do_parse_comment_parts(input) + Error(_) -> input + } +} + +fn parse_non_eol(str: String) -> Result(#(String, String), ParseError) { + case string.pop_grapheme(str) { + Ok(#(char, tail)) -> { + let assert [codepoint] = string.to_utf_codepoints(char) + case string.utf_codepoint_to_int(codepoint) { + i if i >= 0x20 && i <= 0x7E -> Ok(#(char, tail)) + i if i >= 0x80 -> Ok(#(char, tail)) + i if i == 0x09 -> Ok(#(char, tail)) + _ -> Error(Internal) + } + } + Error(_) -> Error(Internal) + } +} + +fn try_parsers( + over list: List(fn(String) -> Result(#(a, String), ParseError)), + against static_data: String, +) -> Result(#(a, String), ParseError) { + case list { + [] -> Error(Internal) + [first, ..rest] -> + case first(static_data) { + Error(_) -> try_parsers(rest, static_data) + Ok(r) -> Ok(r) + } + } +} + +fn parse_optional( + to_parse str: String, + with opt_fn: fn(String) -> Result(#(r, String), ParseError), + default def: fn() -> r, +) -> #(r, String) { + case opt_fn(str) { + Error(_) -> #(def(), str) + Ok(a) -> a + } +} + +fn parse_optional_result( + to_parse str: String, + with opt_fn: fn(String) -> Result(#(r, String), ParseError), + default def: fn() -> r, +) -> Result(#(r, String), ParseError) { + parse_optional(str, opt_fn, def) |> Ok +} + +pub fn parse_multiple_optional( + to_parse str: String, + with to_run: fn(String) -> Result(#(String, String), ParseError), +) -> #(String, String) { + case do_parse_multiple(str, to_run, "") { + Error(_) -> #("", str) + Ok(#(r, rest)) -> #(r, rest) + } +} + +pub fn parse_multiple( + to_parse str: String, + with to_run: fn(String) -> Result(#(String, String), ParseError), +) -> Result(#(String, String), ParseError) { + case do_parse_multiple(str, to_run, "") { + Ok(#("", _)) -> Error(Internal) + Error(e) -> Error(e) + Ok(#(r, rest)) -> Ok(#(r, rest)) + } +} + +fn do_parse_multiple( + to_parse str: String, + with to_run: fn(String) -> Result(#(String, String), ParseError), + acc ret: String, +) -> Result(#(String, String), ParseError) { + case str { + "" -> Ok(#(ret, str)) + _ -> + case to_run(str) { + Ok(#(r, rest)) -> do_parse_multiple(rest, to_run, ret <> r) + Error(_) -> Ok(#(ret, str)) + } + } +} + +pub fn parse_this_then( + to_parse str: String, + with parsers: List(fn(String) -> Result(#(String, String), ParseError)), +) -> Result(#(String, String), ParseError) { + do_parse_this_then(str, "", parsers) +} + +fn do_parse_this_then( + to_parse str: String, + from initial: String, + with parsers: List(fn(String) -> Result(#(String, String), ParseError)), +) -> Result(#(String, String), ParseError) { + case parsers { + [] -> Ok(#(initial, str)) + [head, ..tail] -> { + case head(str) { + Ok(#(res, rest)) -> do_parse_this_then(rest, initial <> res, tail) + Error(e) -> Error(e) + } + } + } +} + +// ALPHA = %x41–5A | %x61–7A +fn parse_alpha(str: String) -> Result(#(String, String), ParseError) { + case string.pop_grapheme(str) { + Ok(#(char, tail)) -> { + let assert [codepoint] = string.to_utf_codepoints(char) + case string.utf_codepoint_to_int(codepoint) { + i if i >= 0x41 && i <= 0x5A -> Ok(#(char, tail)) + i if i >= 0x61 && i <= 0x7A -> Ok(#(char, tail)) + _ -> Error(Internal) + } + } + Error(_) -> Error(Internal) + } +} + +// DIGIT = %x30–39 +fn parse_digit(str: String) -> Result(#(String, String), ParseError) { + case string.pop_grapheme(str) { + Ok(#(char, tail)) -> { + let assert [codepoint] = string.to_utf_codepoints(char) + case string.utf_codepoint_to_int(codepoint) { + i if i >= 0x30 && i <= 0x39 -> Ok(#(char, tail)) + _ -> Error(Internal) + } + } + Error(_) -> Error(Internal) + } +} + +// DIGIT (non-zero) = %x31–39 +fn parse_digit_nz(str: String) -> Result(#(String, String), ParseError) { + case string.pop_grapheme(str) { + Ok(#(char, tail)) -> { + let assert [codepoint] = string.to_utf_codepoints(char) + case string.utf_codepoint_to_int(codepoint) { + i if i >= 0x31 && i <= 0x39 -> Ok(#(char, tail)) + _ -> Error(Internal) + } + } + Error(_) -> Error(Internal) + } +} + +pub fn parse_min_max( + str: f, + min: Int, + max: Int, + parse_fn: fn(f) -> Result(#(r, f), g), + initial: r, + collator: fn(r, r) -> r, +) -> Result(#(r, f), ParseError) { + do_parse_min_max(str, initial, min, max, parse_fn, collator) +} + +pub fn do_parse_min_max( + str: d, + acc: r, + min: Int, + max: Int, + parse_fn: fn(d) -> Result(#(r, d), e), + collator: fn(r, r) -> r, +) -> Result(#(r, d), ParseError) { + case parse_fn(str) { + Error(_) -> { + case min > 0 { + True -> Error(Internal) + False -> Ok(#(acc, str)) + } + } + Ok(#(l, rest)) -> { + let new_acc = collator(acc, l) + case max { + 1 -> Ok(#(new_acc, rest)) + -1 -> do_parse_min_max(rest, new_acc, min - 1, max, parse_fn, collator) + _ -> + do_parse_min_max(rest, new_acc, min - 1, max - 1, parse_fn, collator) + } + } + } +} diff --git a/test/gltoml_test.gleam b/test/gltoml_test.gleam new file mode 100644 index 0000000..9bf4356 --- /dev/null +++ b/test/gltoml_test.gleam @@ -0,0 +1,46 @@ +import gleam/list +import gleam/string +import gltoml +import simplifile +import startest +import startest/expect + +pub fn main() -> Nil { + startest.run(startest.default_config()) +} + +// gleeunit test functions end in `_test` +pub fn files_tests() { + let assert Ok(test_file) = + simplifile.read("./toml-test/tests/files-toml-1.1.0") + let files = + string.split(test_file, "\n") + |> list.filter(fn(filename) { string.ends_with(filename, ".toml") }) + // We need to omit the encoding tests because these generally require a different method to + // load as simplifile doesn't appear to open non-valid utf 8/16 files + |> list.filter(fn(filename) { + !string.starts_with(filename, "invalid/encoding") + }) + files |> list.length |> echo + + list.map(files, fn(filename) { + startest.it(filename, fn() { + let toml = + simplifile.read("./toml-test/tests/" <> filename) |> expect.to_be_ok + + let tom = gltoml.parse(toml) + case filename { + "valid/" <> _ -> { + expect.to_be_ok(tom) + Nil + } + "invalid/" <> _ -> { + expect.to_be_error(tom) + Nil + } + _ -> Nil + } + }) + }) + |> startest.describe("files test", _) +} diff --git a/test/scratch.gleam b/test/scratch.gleam new file mode 100644 index 0000000..1f88f28 --- /dev/null +++ b/test/scratch.gleam @@ -0,0 +1,16 @@ +import gleam/string +import gleam/time/calendar +import gltoml + +pub fn main() { + calendar.Date(25, calendar.March, 2025) + |> echo + |> calendar.is_valid_date + |> echo + gltoml.parse( + //"#hello\n 'a.b' = '1'\nc = true\ng=2025-03-25T10:03:23.500000000+01:00\nh=12308_123\ni=-2\nf = 0o751\nd=0b1_1_0_1\ne=0x1ab_4ef\nj=-nan\nk=1.5e2", + // "tab = { inner.table = [{}], inner.table.val = \"bad\" }", + "a = { a.b = 1 }", + ) + |> echo +} diff --git a/toml-test b/toml-test new file mode 160000 index 0000000..1d35870 --- /dev/null +++ b/toml-test @@ -0,0 +1 @@ +Subproject commit 1d35870ef6783d86366ba55d7df703f3f60b3b55