From 307b9db7b722a9b9acf43b367875045cfa9a5a9a Mon Sep 17 00:00:00 2001 From: Guillaume Hivert Date: Tue, 9 Jul 2024 12:24:35 +0200 Subject: [PATCH] fix: use correct Toml parser --- apps/backend/gleam.toml | 2 +- apps/backend/manifest.toml | 27 +- .../src/backend/postgres/postgres.gleam | 7 +- apps/frontend/src/data/search_result.gleam | 23 +- packages/pgo/src/gleam/pgo.gleam | 6 - packages/pgo/src/gleam/pgo/ssl.gleam | 15 - packages/pgo/src/gleam_pgo_ffi.erl | 16 +- packages/tom/.github/workflows/test.yml | 23 + packages/tom/.gitignore | 4 + packages/tom/CHANGELOG.md | 22 + packages/tom/README.md | 44 + packages/tom/gleam.toml | 13 + packages/tom/manifest.toml | 11 + packages/tom/src/tom.gleam | 1330 +++++++++++++++++ packages/tom/test/tom_test.gleam | 930 ++++++++++++ 15 files changed, 2423 insertions(+), 50 deletions(-) delete mode 100644 packages/pgo/src/gleam/pgo/ssl.gleam create mode 100644 packages/tom/.github/workflows/test.yml create mode 100644 packages/tom/.gitignore create mode 100644 packages/tom/CHANGELOG.md create mode 100644 packages/tom/README.md create mode 100644 packages/tom/gleam.toml create mode 100644 packages/tom/manifest.toml create mode 100644 packages/tom/src/tom.gleam create mode 100644 packages/tom/test/tom_test.gleam diff --git a/apps/backend/gleam.toml b/apps/backend/gleam.toml index f46178e..f6e9f85 100644 --- a/apps/backend/gleam.toml +++ b/apps/backend/gleam.toml @@ -20,7 +20,7 @@ prng = ">= 3.0.3 and < 4.0.0" radiate = ">= 0.4.0 and < 1.0.0" ranger = ">= 1.2.0 and < 2.0.0" simplifile = ">= 1.7.0 and < 2.0.0" -tom = ">= 1.0.0 and < 2.0.0" +tom = { path ="../../packages/tom" } verl = ">= 1.1.1 and < 2.0.0" wisp = "~> 0.14" cors_builder = ">= 1.0.0 and < 2.0.0" diff --git a/apps/backend/manifest.toml b/apps/backend/manifest.toml index 3993b18..df2073a 100644 --- a/apps/backend/manifest.toml +++ b/apps/backend/manifest.toml @@ -4,34 +4,35 @@ packages = [ { name = "aws4_request", version = "0.1.1", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_http", "gleam_stdlib"], otp_app = "aws4_request", source = "hex", outer_checksum = "90B1DB6E2A7F0396CD4713850B14B3A910331B5BA76D051E411D1499AAA2EA9A" }, { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, - { name = "birl", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "976CFF85D34D50F7775896615A71745FBE0C325E50399787088F941B539A0497" }, + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, { name = "chomp", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "chomp", source = "hex", outer_checksum = "C87304897B4D4DEA69420DB2FF88B087673AAE9EC09CA8A0FBF4675F605767C2" }, { name = "cors_builder", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "mist", "wisp"], otp_app = "cors_builder", source = "hex", outer_checksum = "951B5B648E958BD6181A6EED98BCA4EEB302B83DC7DCE2954B3462114209EC43" }, { name = "decipher", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_json", "gleam_stdlib", "stoiridh_version"], otp_app = "decipher", source = "hex", outer_checksum = "9F1B5C6FF0D798046E4E0EF87D09DD729324CB72BD7F0D4152B797324D51223E" }, { name = "dot_env", version = "0.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "simplifile"], otp_app = "dot_env", source = "hex", outer_checksum = "AF5C972D6129F67AF3BB00134AB2808D37111A8D61686CFA86F3ADF652548982" }, { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, - { name = "filespy", version = "0.4.0", build_tools = ["gleam"], requirements = ["fs", "gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "filespy", source = "hex", outer_checksum = "950469A2FA50265EB84530637D3E9597C2CA676A2EEABC98C69A83C77316709C" }, + { name = "filespy", version = "0.5.0", build_tools = ["gleam"], requirements = ["fs", "gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "filespy", source = "hex", outer_checksum = "F8E7A9C9CA86D68CCC25491125BFF36BEF7483892D7BEC24AA30D6B540504F06" }, { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, { name = "glam", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "66EC3BCD632E51EED029678F8DF419659C1E57B1A93D874C5131FE220DFAD2B2" }, { name = "gleam_bitwise", version = "1.3.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_bitwise", source = "hex", outer_checksum = "B36E1D3188D7F594C7FD4F43D0D2CE17561DE896202017548578B16FE1FE9EFC" }, { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, - { name = "gleam_hexpm", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], otp_app = "gleam_hexpm", source = "hex", outer_checksum = "A5DF5D32BFDE84003B2C2183700D98E106A969C6C6EDE0363A50BB421AFF50B7" }, + { name = "gleam_hexpm", version = "1.1.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], otp_app = "gleam_hexpm", source = "hex", outer_checksum = "D32439FD6AD683FE1094922737904EC2091E2D7B1F236AD23815935694A5221A" }, { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, - { name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" }, + { name = "gleam_package_interface", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "CF3BFC5D0997750D9550D8D73A90F4B8D71C6C081B20ED4E70FFBE1E99AFC3C2" }, { name = "gleam_pgo", version = "0.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "pgo"], source = "local", path = "../../packages/pgo" }, - { name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" }, - { name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" }, + { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, { name = "glexer", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "BD477AD657C2B637FEF75F2405FAEFFA533F277A74EF1A5E17B55B1178C228FB" }, { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, - { name = "logging", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "A996064F04EF6E67F0668FD0ACFB309830B05D0EE3A0C11BBBD2D4464334F792" }, - { name = "marceau", version = "1.1.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "1AAD727A30BE0F95562C3403BB9B27C823797AD90037714255EEBF617B1CDA81" }, - { name = "mist", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "7765E53DCC9ACCACF217B8E0CA3DE7E848C783BFAE5118B75011E81C2C80385C" }, + { name = "logging", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "FCB111401BDB4703A440A94FF8CC7DA521112269C065F219C2766998333E7738" }, + { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, + { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, { name = "opentelemetry_api", version = "1.3.0", build_tools = ["rebar3", "mix"], requirements = ["opentelemetry_semantic_conventions"], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "B9E5FF775FD064FA098DBA3C398490B77649A352B40B0B730A6B7DC0BDD68858" }, { name = "opentelemetry_semantic_conventions", version = "0.2.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_semantic_conventions", source = "hex", outer_checksum = "D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895" }, { name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" }, @@ -42,9 +43,9 @@ packages = [ { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, { name = "shellout", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "E2FCD18957F0E9F67E1F497FC9FF57393392F8A9BAEAEA4779541DE7A68DD7E0" }, { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, - { name = "stoiridh_version", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "stoiridh_version", source = "hex", outer_checksum = "298ABEA44DF37764A34C2E9190A84BF2770BC59DD9397C6DC7708040E5A0142B" }, + { name = "stoiridh_version", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "stoiridh_version", source = "hex", outer_checksum = "EEF8ADAB9755BD33EB202F169376F1A7797AEF90823FDCA671D8590D04FBF56B" }, { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, - { name = "tom", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "A5364613E3DBF77F38EFF81DA9F99324086D029EC2B2D44348762FBE38602311" }, + { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../../packages/tom" }, { name = "verl", version = "1.1.1", build_tools = ["rebar3"], requirements = [], otp_app = "verl", source = "hex", outer_checksum = "0925E51CD92A0A8BE271765B02430B2E2CFF8AC30EF24D123BD0D58511E8FB18" }, { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, ] @@ -66,7 +67,7 @@ gleam_package_interface = { version = ">= 1.0.0 and < 2.0.0" } gleam_pgo = { path = "../../packages/pgo" } gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } gleeunit = { version = "~> 1.0" } -glexer = { version = ">= 1.0.1 and < 2.0.0"} +glexer = { version = ">= 1.0.1 and < 2.0.0" } mist = { version = ">= 1.0.0 and < 2.0.0" } pgo = { version = "~> 0.14" } pprint = { version = ">= 1.0.3 and < 2.0.0" } @@ -74,6 +75,6 @@ prng = { version = ">= 3.0.3 and < 4.0.0" } radiate = { version = ">= 0.4.0 and < 1.0.0" } ranger = { version = ">= 1.2.0 and < 2.0.0" } simplifile = { version = ">= 1.7.0 and < 2.0.0" } -tom = { version = ">= 1.0.0 and < 2.0.0" } +tom = { path = "../../packages/tom" } verl = { version = ">= 1.1.1 and < 2.0.0" } wisp = { version = "~> 0.14" } diff --git a/apps/backend/src/backend/postgres/postgres.gleam b/apps/backend/src/backend/postgres/postgres.gleam index 6ca2eb4..65a31b5 100644 --- a/apps/backend/src/backend/postgres/postgres.gleam +++ b/apps/backend/src/backend/postgres/postgres.gleam @@ -4,7 +4,6 @@ import gleam/bool import gleam/list import gleam/option.{type Option, Some} import gleam/pgo.{Config} -import gleam/pgo/ssl import gleam/result import gleam/string import gleam/uri @@ -44,11 +43,7 @@ fn parse_database_url(database_url: String) { |> result.then(list.key_find(_, "sslmode")) |> result.map(fn(ssl) { let is_ssl_enabled = ssl != "disable" - let ssl_options = case is_ssl_enabled { - True -> [ssl.Verify(ssl.VerifyNone)] - False -> [] - } - Config(..cnf, ssl: is_ssl_enabled, ssl_options: ssl_options) + Config(..cnf, ssl: is_ssl_enabled) }) |> result.unwrap(cnf) }) diff --git a/apps/frontend/src/data/search_result.gleam b/apps/frontend/src/data/search_result.gleam index b8b32e6..9a029cf 100644 --- a/apps/frontend/src/data/search_result.gleam +++ b/apps/frontend/src/data/search_result.gleam @@ -3,6 +3,9 @@ import data/metadata.{type Metadata} import data/signature.{type Signature} import frontend/view/helpers import gleam/dynamic +import gleam/list +import gleam/option.{None, Some} +import gleam/result pub type SearchResult { SearchResult( @@ -42,6 +45,14 @@ pub fn decode_search_result(dyn) { dynamic.field("metadata", metadata.decode_metadata), dynamic.field("version", dynamic.string), )(dyn) + |> result.map(Some) + |> result.try_recover(fn(_) { Ok(None) }) +} + +pub fn decode_search_results_list(dyn) { + use data <- result.map(dynamic.list(decode_search_result)(dyn)) + use item <- list.filter_map(data) + option.to_result(item, "") } pub fn decode_search_results(dyn) { @@ -51,12 +62,12 @@ pub fn decode_search_results(dyn) { }), dynamic.decode6( SearchResults, - dynamic.field("exact-type-matches", dynamic.list(decode_search_result)), - dynamic.field("exact-matches", dynamic.list(decode_search_result)), - dynamic.field("matches", dynamic.list(decode_search_result)), - dynamic.field("searches", dynamic.list(decode_search_result)), - dynamic.field("docs-searches", dynamic.list(decode_search_result)), - dynamic.field("module-searches", dynamic.list(decode_search_result)), + dynamic.field("exact-type-matches", decode_search_results_list), + dynamic.field("exact-matches", decode_search_results_list), + dynamic.field("matches", decode_search_results_list), + dynamic.field("searches", decode_search_results_list), + dynamic.field("docs-searches", decode_search_results_list), + dynamic.field("module-searches", decode_search_results_list), ), ])(dyn) } diff --git a/packages/pgo/src/gleam/pgo.gleam b/packages/pgo/src/gleam/pgo.gleam index 519d92c..24fdb66 100644 --- a/packages/pgo/src/gleam/pgo.gleam +++ b/packages/pgo/src/gleam/pgo.gleam @@ -7,7 +7,6 @@ import gleam/dynamic.{type DecodeErrors, type Decoder, type Dynamic} import gleam/list import gleam/option.{type Option, None, Some} -import gleam/pgo/ssl import gleam/result import gleam/string import gleam/uri.{Uri} @@ -27,10 +26,6 @@ pub type Config { password: Option(String), /// (default: false): Whether to use SSL or not. ssl: Bool, - /// (default: []): Options to use when SSL is true. - /// With OTP 26, SSL is required, and Cacerts is required unless - /// VerifyNone is set. - ssl_options: List(ssl.Options), /// (default: []): List of 2-tuples, where key and value must be binary /// strings. You can include any Postgres connection parameter here, such as /// `#("application_name", "myappname")` and `#("timezone", "GMT")`. @@ -76,7 +71,6 @@ pub fn default_config() -> Config { user: "postgres", password: None, ssl: False, - ssl_options: [], connection_parameters: [], pool_size: 1, queue_target: 50, diff --git a/packages/pgo/src/gleam/pgo/ssl.gleam b/packages/pgo/src/gleam/pgo/ssl.gleam deleted file mode 100644 index fe6fc56..0000000 --- a/packages/pgo/src/gleam/pgo/ssl.gleam +++ /dev/null @@ -1,15 +0,0 @@ -/// Choose to verify, or skip verification for host. -pub type Verify { - VerifyNone - VerifyPeer -} - -/// Options for SSL connection. -pub type Options { - /// Verify or skip the peer. - Verify(Verify) - /// List of certificates to verify against. - Cacerts(certs: List(BitArray)) - /// Filename of certificate to verify against. - Cacertfile(filename: String) -} diff --git a/packages/pgo/src/gleam_pgo_ffi.erl b/packages/pgo/src/gleam_pgo_ffi.erl index fc74eef..4d88c80 100644 --- a/packages/pgo/src/gleam_pgo_ffi.erl +++ b/packages/pgo/src/gleam_pgo_ffi.erl @@ -23,7 +23,6 @@ connect(Config) -> user = User, password = Password, ssl = Ssl, - ssl_options = SslOptions, connection_parameters = ConnectionParameters, pool_size = PoolSize, queue_target = QueueTarget, @@ -32,6 +31,17 @@ connect(Config) -> trace = Trace, ip_version = IpVersion } = Config, + SslOptions = case Ssl of + false -> []; + true -> [ + {verify, verify_peer}, + {cacerts, public_key:cacerts_get()}, + {server_name_indication, binary_to_list(Host)}, + {customize_hostname_check, [ + {match_fun, public_key:pkix_verify_hostname_match_fun(https)} + ]} + ] + end, Options1 = #{ host => Host, port => Port, @@ -75,8 +85,8 @@ convert_error(none_available) -> convert_error({pgo_protocol, {parameters, Expected, Got}}) -> {unexpected_argument_count, Expected, Got}; convert_error({pgsql_error, #{ - message := Message, - constraint := Constraint, + message := Message, + constraint := Constraint, detail := Detail }}) -> {constraint_violated, Message, Constraint, Detail}; diff --git a/packages/tom/.github/workflows/test.yml b/packages/tom/.github/workflows/test.yml new file mode 100644 index 0000000..cf2096e --- /dev/null +++ b/packages/tom/.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@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "0.32.4" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/packages/tom/.gitignore b/packages/tom/.gitignore new file mode 100644 index 0000000..170cca9 --- /dev/null +++ b/packages/tom/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +build +erl_crash.dump diff --git a/packages/tom/CHANGELOG.md b/packages/tom/CHANGELOG.md new file mode 100644 index 0000000..0867008 --- /dev/null +++ b/packages/tom/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog + +## v0.4.0 - Unreleased + +- Added support for `\e`, `\f`, `\b`. + +## v0.3.0 - 2023-12-07 + +- Updated for Gleam v0.33.0. + +## v0.2.1 - 2023-11-20 + +- Documents with no trailing newline can now be parsed. + +## v0.2.0 - 2023-11-14 + +- The library can now parse full TOML documents, with the exception of the + string escape codes `\b`, `\f`, `\e`, `\xHH`, `\uHHHH`, and `\UHHHHHHHH`. + +## v0.1.0 - 2023-11-12 + +- Initial release diff --git a/packages/tom/README.md b/packages/tom/README.md new file mode 100644 index 0000000..cca986a --- /dev/null +++ b/packages/tom/README.md @@ -0,0 +1,44 @@ +# tom + +A Gleam TOML parser! + +[![Package Version](https://img.shields.io/hexpm/v/tom)](https://hex.pm/packages/tom) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/tom/) + + +```sh +gleam add tom +``` +```gleam +import tom + +const config = " + [person] + name = \"Lucy\" + is_cool = true +" + +pub fn main() { + // Parse a string of TOML + let assert Ok(parsed) = tom.parse(config) + + // Now you can work with the data directly, or you can use the `get_*` + // functions to retrieve values. + + tom.get_string(parsed, ["person", "name"]) + // -> Ok("Lucy") + + let is_cool = tom.get_bool(parsed, ["person", "is_cool"]) + // -> Ok(True) +} +``` + +Further documentation can be found at . + +## Status + +The following string escape sequences are not supported yet: + +- `\xHH` +- `\uHHHH` +- `\UHHHHHHHH` diff --git a/packages/tom/gleam.toml b/packages/tom/gleam.toml new file mode 100644 index 0000000..3bfe267 --- /dev/null +++ b/packages/tom/gleam.toml @@ -0,0 +1,13 @@ +name = "tom" +version = "0.3.0" + +description = "A pure Gleam TOML parser!" +licences = ["Apache-2.0"] +repository = { type = "github", user = "lpil", repo = "tom" } +links = [{ title = "TOML website", href = "https://toml.io/en/" }] + +[dependencies] +gleam_stdlib = "~> 0.33" + +[dev-dependencies] +gleeunit = "~> 1.0" diff --git a/packages/tom/manifest.toml b/packages/tom/manifest.toml new file mode 100644 index 0000000..2a00d86 --- /dev/null +++ b/packages/tom/manifest.toml @@ -0,0 +1,11 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.33.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "3CEAD7B153D896499C78390B22CC968620C27500C922AED3A5DD7B536F922B25" }, + { name = "gleeunit", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D3682ED8C5F9CAE1C928F2506DE91625588CC752495988CBE0F5653A42A6F334" }, +] + +[requirements] +gleam_stdlib = { version = "~> 0.33" } +gleeunit = { version = "~> 1.0" } diff --git a/packages/tom/src/tom.gleam b/packages/tom/src/tom.gleam new file mode 100644 index 0000000..3bd7e82 --- /dev/null +++ b/packages/tom/src/tom.gleam @@ -0,0 +1,1330 @@ +//// A pure Gleam TOML parser! +//// +//// ```gleam +//// import tom +//// +//// const config = " +//// [person] +//// name = \"Lucy\" +//// is_cool = true +//// " +//// +//// pub fn main() { +//// // Parse a string of TOML +//// let assert Ok(parsed) = tom.parse(config) +//// +//// // Now you can work with the data directly, or you can use the `get_*` +//// // functions to retrieve values. +//// +//// tom.get_string(parsed, ["person", "name"]) +//// // -> Ok("Lucy") +//// +//// let is_cool = tom.get_bool(parsed, ["person", "is_cool"]) +//// // -> Ok(True) +//// } +//// ``` + +import gleam/dict.{type Dict} +import gleam/float +import gleam/int +import gleam/list +import gleam/result +import gleam/string + +/// A TOML document. +pub type Toml { + Int(Int) + Float(Float) + /// Infinity is a valid number in TOML but Gleam does not support it, so this + /// variant represents the infinity values. + Infinity(Sign) + /// NaN is a valid number in TOML but Gleam does not support it, so this + /// variant represents the NaN values. + Nan(Sign) + Bool(Bool) + String(String) + Date(Date) + Time(Time) + DateTime(DateTime) + Array(List(Toml)) + ArrayOfTables(List(Dict(String, Toml))) + Table(Dict(String, Toml)) + InlineTable(Dict(String, Toml)) +} + +pub type DateTime { + DateTimeValue(date: Date, time: Time, offset: Offset) +} + +pub type Date { + DateValue(year: Int, month: Int, day: Int) +} + +pub type Time { + TimeValue(hour: Int, minute: Int, second: Int, millisecond: Int) +} + +pub type Offset { + Local + Offset(direction: Sign, hours: Int, minutes: Int) +} + +pub type Sign { + Positive + Negative +} + +/// 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)) +} + +type Tokens = + List(String) + +type Parsed(a) = + Result(#(a, Tokens), ParseError) + +/// A number of any kind, returned by the `get_number` function. +pub type Number { + NumberInt(Int) + NumberFloat(Float) + NumberInfinity(Sign) + NumberNan(Sign) +} + +/// An error that can occur when retrieving a value from a TOML document with +/// one of the `get_*` functions. +pub type GetError { + /// There was no value at the given key. + NotFound(key: List(String)) + /// The value at the given key was not of the expected type. + WrongType(key: List(String), expected: String, got: String) +} + +// TODO: test +/// Get a value of any type from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = 1") +/// get(parsed, ["a", "b", "c"]) +/// // -> Ok(Int(1)) +/// ``` +/// +pub fn get( + toml: Dict(String, Toml), + key: List(String), +) -> Result(Toml, GetError) { + case key { + [] -> Error(NotFound([])) + [k] -> result.replace_error(dict.get(toml, k), NotFound([k])) + [k, ..key] -> { + case dict.get(toml, k) { + Ok(Table(t)) -> push_key(get(t, key), k) + Ok(InlineTable(t)) -> push_key(get(t, key), k) + Ok(other) -> Error(WrongType([k], "Table", classify(other))) + Error(_) -> Error(NotFound([k])) + } + } + } +} + +// TODO: test +/// Get an int from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = 1") +/// get_int(parsed, ["a", "b", "c"]) +/// // -> Ok(1) +/// ``` +/// +pub fn get_int( + toml: Dict(String, Toml), + key: List(String), +) -> Result(Int, GetError) { + case get(toml, key) { + Ok(Int(i)) -> Ok(i) + Ok(other) -> Error(WrongType(key, "Int", classify(other))) + Error(e) -> Error(e) + } +} + +// TODO: test +/// Get a float from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = 1.1") +/// get_float(parsed, ["a", "b", "c"]) +/// // -> Ok(1.1) +/// ``` +/// +pub fn get_float( + toml: Dict(String, Toml), + key: List(String), +) -> Result(Float, GetError) { + case get(toml, key) { + Ok(Float(i)) -> Ok(i) + Ok(other) -> Error(WrongType(key, "Float", classify(other))) + Error(e) -> Error(e) + } +} + +// TODO: test +/// Get a bool from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = true") +/// get_bool(parsed, ["a", "b", "c"]) +/// // -> Ok(True) +/// ``` +/// +pub fn get_bool( + toml: Dict(String, Toml), + key: List(String), +) -> Result(Bool, GetError) { + case get(toml, key) { + Ok(Bool(i)) -> Ok(i) + Ok(other) -> Error(WrongType(key, "Bool", classify(other))) + Error(e) -> Error(e) + } +} + +// TODO: test +/// Get a string from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = \"ok\"") +/// get_string(parsed, ["a", "b", "c"]) +/// // -> Ok("ok") +/// ``` +/// +pub fn get_string( + toml: Dict(String, Toml), + key: List(String), +) -> Result(String, GetError) { + case get(toml, key) { + Ok(String(i)) -> Ok(i) + Ok(other) -> Error(WrongType(key, "String", classify(other))) + Error(e) -> Error(e) + } +} + +// TODO: test +/// Get a date from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = 1979-05-27") +/// get_date(parsed, ["a", "b", "c"]) +/// // -> Ok("1979-05-27") +/// ``` +/// +pub fn get_date( + toml: Dict(String, Toml), + key: List(String), +) -> Result(Date, GetError) { + case get(toml, key) { + Ok(Date(i)) -> Ok(i) + Ok(other) -> Error(WrongType(key, "Date", classify(other))) + Error(e) -> Error(e) + } +} + +// TODO: test +/// Get a time from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = 07:32:00") +/// get_time(parsed, ["a", "b", "c"]) +/// // -> Ok("07:32:00") +/// ``` +/// +pub fn get_time( + toml: Dict(String, Toml), + key: List(String), +) -> Result(Time, GetError) { + case get(toml, key) { + Ok(Time(i)) -> Ok(i) + Ok(other) -> Error(WrongType(key, "Time", classify(other))) + Error(e) -> Error(e) + } +} + +// TODO: test +/// Get a date-time from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = 1979-05-27T07:32:00") +/// get_date_time(parsed, ["a", "b", "c"]) +/// // -> Ok("1979-05-27T07:32:00") +/// ``` +/// +pub fn get_date_time( + toml: Dict(String, Toml), + key: List(String), +) -> Result(DateTime, GetError) { + case get(toml, key) { + Ok(DateTime(i)) -> Ok(i) + Ok(other) -> Error(WrongType(key, "DateTime", classify(other))) + Error(e) -> Error(e) + } +} + +// TODO: test +/// Get an array from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = [1, 2]") +/// get_array(parsed, ["a", "b", "c"]) +/// // -> Ok([Int(1), Int(2)]) +/// ``` +/// +pub fn get_array( + toml: Dict(String, Toml), + key: List(String), +) -> Result(List(Toml), GetError) { + case get(toml, key) { + Ok(Array(i)) -> Ok(i) + Ok(ArrayOfTables(i)) -> Ok(list.map(i, Table)) + Ok(other) -> Error(WrongType(key, "Array", classify(other))) + Error(e) -> Error(e) + } +} + +// TODO: test +/// Get a table from a TOML document. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = { d = 1 }") +/// get_table(parsed, ["a", "b", "c"]) +/// // -> Ok(dict.from_list([#("d", Int(1))])) +/// ``` +/// +pub fn get_table( + toml: Dict(String, Toml), + key: List(String), +) -> Result(Dict(String, Toml), GetError) { + case get(toml, key) { + Ok(Table(i)) -> Ok(i) + Ok(InlineTable(i)) -> Ok(i) + Ok(other) -> Error(WrongType(key, "Table", classify(other))) + Error(e) -> Error(e) + } +} + +// TODO: test +/// Get a number of any kind from a TOML document. +/// This could be an int, a float, a NaN, or an infinity. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(parsed) = parse("a.b.c = { d = inf }") +/// get_number(parsed, ["a", "b", "c"]) +/// // -> Ok(NumberInfinity(Positive))) +/// ``` +/// +pub fn get_number( + toml: Dict(String, Toml), + key: List(String), +) -> Result(Number, GetError) { + case get(toml, key) { + Ok(Int(x)) -> Ok(NumberInt(x)) + Ok(Float(x)) -> Ok(NumberFloat(x)) + Ok(Nan(x)) -> Ok(NumberNan(x)) + Ok(Infinity(x)) -> Ok(NumberInfinity(x)) + Ok(other) -> Error(WrongType(key, "Number", classify(other))) + Error(e) -> Error(e) + } +} + +fn classify(toml: Toml) -> String { + case toml { + Int(_) -> "Int" + Float(_) -> "Float" + Nan(Positive) -> "NaN" + Nan(Negative) -> "Negative NaN" + Infinity(Positive) -> "Infinity" + Infinity(Negative) -> "Negative Infinity" + Bool(_) -> "Bool" + String(_) -> "String" + Date(_) -> "Date" + Time(_) -> "Time" + DateTime(_) -> "DateTime" + Array(_) -> "Array" + ArrayOfTables(_) -> "Array" + Table(_) -> "Table" + InlineTable(_) -> "Table" + } +} + +fn push_key(result: Result(t, GetError), key: String) -> Result(t, GetError) { + case result { + Ok(t) -> Ok(t) + Error(NotFound(path)) -> Error(NotFound([key, ..path])) + Error(WrongType(path, expected, got)) -> + Error(WrongType([key, ..path], expected, got)) + } +} + +pub fn parse(input: String) -> Result(Dict(String, Toml), ParseError) { + let input = string.to_graphemes(input) + let input = drop_comments(input, [], False) + let input = skip_whitespace(input) + use toml, input <- do(parse_table(input, dict.new())) + case parse_tables(input, toml) { + Ok(toml) -> Ok(reverse_arrays_of_tables_table(toml)) + Error(e) -> Error(e) + } +} + +fn parse_tables( + input: Tokens, + toml: Dict(String, Toml), +) -> Result(Dict(String, Toml), ParseError) { + case input { + ["[", "[", ..input] -> { + case parse_array_of_tables(input) { + Error(e) -> Error(e) + Ok(#(#(key, table), input)) -> { + case insert(toml, key, ArrayOfTables([table])) { + Ok(toml) -> parse_tables(input, toml) + Error(e) -> Error(e) + } + } + } + } + ["[", ..input] -> { + case parse_table_and_header(input) { + Error(e) -> Error(e) + Ok(#(#(key, table), input)) -> { + case insert(toml, key, Table(table)) { + Ok(toml) -> parse_tables(input, toml) + Error(e) -> Error(e) + } + } + } + } + [g, ..] -> Error(Unexpected(g, "[")) + [] -> Ok(toml) + } +} + +fn parse_array_of_tables( + input: Tokens, +) -> Parsed(#(List(String), Dict(String, Toml))) { + let input = skip_line_whitespace(input) + use key, input <- do(parse_key(input, [])) + use input <- expect(input, "]") + use input <- expect(input, "]") + use table, input <- do(parse_table(input, dict.new())) + Ok(#(#(key, table), input)) +} + +fn parse_table_header(input: Tokens) -> Parsed(List(String)) { + let input = skip_line_whitespace(input) + use key, input <- do(parse_key(input, [])) + use input <- expect(input, "]") + let input = skip_line_whitespace(input) + use input <- expect_end_of_line(input) + Ok(#(key, input)) +} + +fn parse_table_and_header( + input: Tokens, +) -> Parsed(#(List(String), Dict(String, Toml))) { + use key, input <- do(parse_table_header(input)) + use table, input <- do(parse_table(input, dict.new())) + Ok(#(#(key, table), input)) +} + +fn parse_table( + input: Tokens, + toml: Dict(String, Toml), +) -> Parsed(Dict(String, Toml)) { + let input = skip_whitespace(input) + case input { + ["[", ..] | [] -> Ok(#(toml, input)) + _ -> + case parse_key_value(input, toml) { + Ok(#(toml, input)) -> + case skip_line_whitespace(input) { + [] -> Ok(#(toml, [])) + ["\n", ..in] | ["\r\n", ..in] -> parse_table(in, toml) + [g, ..] -> Error(Unexpected(g, "\n")) + } + e -> e + } + } +} + +fn parse_key_value( + input: Tokens, + toml: Dict(String, Toml), +) -> Parsed(Dict(String, Toml)) { + use key, input <- do(parse_key(input, [])) + let input = skip_line_whitespace(input) + use input <- expect(input, "=") + let input = skip_line_whitespace(input) + use value, input <- do(parse_value(input)) + case insert(toml, key, value) { + Ok(toml) -> Ok(#(toml, input)) + Error(e) -> Error(e) + } +} + +fn insert( + table: Dict(String, Toml), + key: List(String), + value: Toml, +) -> Result(Dict(String, Toml), ParseError) { + case insert_loop(table, key, value) { + Ok(table) -> Ok(table) + Error(path) -> Error(KeyAlreadyInUse(path)) + } +} + +fn insert_loop( + table: Dict(String, Toml), + key: List(String), + value: Toml, +) -> Result(Dict(String, Toml), List(String)) { + case key { + [] -> panic as "unreachable" + [k] -> { + case dict.get(table, k) { + Error(Nil) -> Ok(dict.insert(table, k, value)) + Ok(old) -> merge(table, k, old, value) + } + } + [k, ..key] -> { + case dict.get(table, k) { + Error(Nil) -> { + case insert_loop(dict.new(), key, value) { + Ok(inner) -> Ok(dict.insert(table, k, Table(inner))) + Error(path) -> Error([k, ..path]) + } + } + Ok(ArrayOfTables([inner, ..rest])) -> { + case insert_loop(inner, key, value) { + Ok(inner) -> + Ok(dict.insert(table, k, ArrayOfTables([inner, ..rest]))) + Error(path) -> Error([k, ..path]) + } + } + Ok(Table(inner)) -> { + case insert_loop(inner, key, value) { + Ok(inner) -> Ok(dict.insert(table, k, Table(inner))) + Error(path) -> Error([k, ..path]) + } + } + Ok(_) -> Error([k]) + } + } + } +} + +fn merge( + table: Dict(String, Toml), + key: String, + old: Toml, + new: Toml, +) -> Result(Dict(String, Toml), List(String)) { + case old, new { + // When both are arrays of tables then they are merged together + ArrayOfTables(tables), ArrayOfTables(new) -> + Ok(dict.insert(table, key, ArrayOfTables(list.append(new, tables)))) + + _, _ -> Error([key]) + } +} + +fn expect_end_of_line(input: Tokens, next: fn(Tokens) -> Parsed(a)) -> Parsed(a) { + case input { + ["\n", ..input] -> next(input) + ["\r\n", ..input] -> next(input) + [g, ..] -> Error(Unexpected(g, "\n")) + [] -> Error(Unexpected("EOF", "\n")) + } +} + +fn parse_value(input) -> Parsed(Toml) { + case input { + ["t", "r", "u", "e", ..input] -> Ok(#(Bool(True), input)) + ["f", "a", "l", "s", "e", ..input] -> Ok(#(Bool(False), input)) + + ["n", "a", "n", ..input] -> Ok(#(Nan(Positive), input)) + ["+", "n", "a", "n", ..input] -> Ok(#(Nan(Positive), input)) + ["-", "n", "a", "n", ..input] -> Ok(#(Nan(Negative), input)) + + ["i", "n", "f", ..input] -> Ok(#(Infinity(Positive), input)) + ["+", "i", "n", "f", ..input] -> Ok(#(Infinity(Positive), input)) + ["-", "i", "n", "f", ..input] -> Ok(#(Infinity(Negative), input)) + + ["[", ..input] -> parse_array(input, []) + ["{", ..input] -> parse_inline_table(input, dict.new()) + + ["0", "x", ..input] -> parse_hex(input, 0, Positive) + ["+", "0", "x", ..input] -> parse_hex(input, 0, Positive) + ["-", "0", "x", ..input] -> parse_hex(input, 0, Negative) + + ["0", "o", ..input] -> parse_octal(input, 0, Positive) + ["+", "0", "o", ..input] -> parse_octal(input, 0, Positive) + ["-", "0", "o", ..input] -> parse_octal(input, 0, Negative) + + ["0", "b", ..input] -> parse_binary(input, 0, Positive) + ["+", "0", "b", ..input] -> parse_binary(input, 0, Positive) + ["-", "0", "b", ..input] -> parse_binary(input, 0, Negative) + + ["+", ..input] -> parse_number(input, 0, Positive) + ["-", ..input] -> parse_number(input, 0, Negative) + ["0", ..] + | ["1", ..] + | ["2", ..] + | ["3", ..] + | ["4", ..] + | ["5", ..] + | ["6", ..] + | ["7", ..] + | ["8", ..] + | ["9", ..] -> parse_number(input, 0, Positive) + + ["\"", "\"", "\"", ..input] -> parse_multi_line_string(input, "") + ["\"", ..input] -> parse_string(input, "") + + ["'", "'", "'", ..input] -> parse_multi_line_literal_string(input, "") + ["'", ..input] -> parse_literal_string(input, "") + + [g, ..] -> Error(Unexpected(g, "value")) + [] -> Error(Unexpected("EOF", "value")) + } +} + +fn parse_key(input: Tokens, segments: List(String)) -> Parsed(List(String)) { + use segment, input <- do(parse_key_segment(input)) + let segments = [segment, ..segments] + let input = skip_line_whitespace(input) + + case input { + [".", ..input] -> parse_key(input, segments) + _ -> Ok(#(list.reverse(segments), input)) + } +} + +fn parse_key_segment(input: Tokens) -> Parsed(String) { + let input = skip_line_whitespace(input) + case input { + ["=", ..] -> Error(Unexpected("=", "Key")) + ["\n", ..] -> Error(Unexpected("\n", "Key")) + ["\r\n", ..] -> Error(Unexpected("\r\n", "Key")) + ["[", ..] -> Error(Unexpected("[", "Key")) + ["\"", ..input] -> parse_key_quoted(input, "\"", "") + ["'", ..input] -> parse_key_quoted(input, "'", "") + _ -> parse_key_bare(input, "") + } +} + +fn parse_key_quoted( + input: Tokens, + close: String, + name: String, +) -> Parsed(String) { + case input { + [g, ..input] if g == close -> Ok(#(name, input)) + [g, ..input] -> parse_key_quoted(input, close, name <> g) + [] -> Error(Unexpected("EOF", close)) + } +} + +fn parse_key_bare(input: Tokens, name: String) -> Parsed(String) { + case input { + [" ", ..input] if name != "" -> Ok(#(name, input)) + ["=", ..] if name != "" -> Ok(#(name, input)) + [".", ..] if name != "" -> Ok(#(name, input)) + ["]", ..] if name != "" -> Ok(#(name, input)) + [",", ..] if name != "" -> Error(Unexpected(",", "=")) + ["\n", ..] if name != "" -> Error(Unexpected("\n", "=")) + ["\r\n", ..] if name != "" -> Error(Unexpected("\r\n", "=")) + ["\n", ..] -> Error(Unexpected("\n", "key")) + ["\r\n", ..] -> Error(Unexpected("\r\n", "key")) + ["]", ..] -> Error(Unexpected("]", "key")) + [",", ..] -> Error(Unexpected(",", "key")) + [g, ..input] -> parse_key_bare(input, name <> g) + [] -> Error(Unexpected("EOF", "key")) + } +} + +fn skip_line_whitespace(input: Tokens) -> Tokens { + list.drop_while(input, fn(g) { g == " " || g == "\t" }) +} + +fn skip_whitespace(input: Tokens) -> Tokens { + case input { + [" ", ..input] -> skip_whitespace(input) + ["\t", ..input] -> skip_whitespace(input) + ["\n", ..input] -> skip_whitespace(input) + ["\r\n", ..input] -> skip_whitespace(input) + input -> input + } +} + +fn drop_comments(input: Tokens, acc: Tokens, in_string: Bool) -> Tokens { + case input { + ["\\", "\"", ..input] if in_string -> + drop_comments(input, ["\"", "\\", ..acc], in_string) + ["\"", ..input] -> drop_comments(input, ["\"", ..acc], !in_string) + ["#", ..input] if in_string -> drop_comments(input, ["#", ..acc], in_string) + ["#", ..input] if !in_string -> + input + |> list.drop_while(fn(g) { g != "\n" }) + |> drop_comments(acc, in_string) + [g, ..input] -> drop_comments(input, [g, ..acc], in_string) + [] -> list.reverse(acc) + } +} + +fn do( + result: Result(#(a, Tokens), ParseError), + next: fn(a, Tokens) -> Result(b, ParseError), +) -> Result(b, ParseError) { + case result { + Ok(#(a, input)) -> next(a, input) + Error(e) -> Error(e) + } +} + +fn expect( + input: Tokens, + expected: String, + next: fn(Tokens) -> Parsed(a), +) -> Parsed(a) { + case input { + [g, ..input] if g == expected -> next(input) + [g, ..] -> Error(Unexpected(g, expected)) + [] -> Error(Unexpected("EOF", expected)) + } +} + +fn parse_inline_table( + input: Tokens, + properties: Dict(String, Toml), +) -> Parsed(Toml) { + let input = skip_whitespace(input) + case input { + ["}", ..input] -> Ok(#(InlineTable(properties), input)) + _ -> + case parse_inline_table_property(input, properties) { + Ok(#(properties, input)) -> { + let input = skip_whitespace(input) + case input { + ["}", ..input] -> Ok(#(InlineTable(properties), input)) + [",", ..input] -> { + let input = skip_whitespace(input) + parse_inline_table(input, properties) + } + [g, ..] -> Error(Unexpected(g, "}")) + [] -> Error(Unexpected("EOF", "}")) + } + } + Error(e) -> Error(e) + } + } +} + +fn parse_inline_table_property( + input: Tokens, + properties: Dict(String, Toml), +) -> Parsed(Dict(String, Toml)) { + let input = skip_whitespace(input) + use key, input <- do(parse_key(input, [])) + let input = skip_line_whitespace(input) + use input <- expect(input, "=") + let input = skip_line_whitespace(input) + use value, input <- do(parse_value(input)) + case insert(properties, key, value) { + Ok(properties) -> Ok(#(properties, input)) + Error(e) -> Error(e) + } +} + +fn parse_array(input: Tokens, elements: List(Toml)) -> Parsed(Toml) { + let input = skip_whitespace(input) + case input { + ["]", ..input] -> Ok(#(Array(list.reverse(elements)), input)) + _ -> { + use element, input <- do(parse_value(input)) + let elements = [element, ..elements] + let input = skip_whitespace(input) + case input { + ["]", ..input] -> Ok(#(Array(list.reverse(elements)), input)) + [",", ..input] -> { + let input = skip_whitespace(input) + parse_array(input, elements) + } + [g, ..] -> Error(Unexpected(g, "]")) + [] -> Error(Unexpected("EOF", "]")) + } + } + } +} + +fn parse_hex(input: Tokens, number: Int, sign: Sign) -> Parsed(Toml) { + case input { + ["_", ..input] -> parse_hex(input, number, sign) + ["0", ..input] -> parse_hex(input, number * 16 + 0, sign) + ["1", ..input] -> parse_hex(input, number * 16 + 1, sign) + ["2", ..input] -> parse_hex(input, number * 16 + 2, sign) + ["3", ..input] -> parse_hex(input, number * 16 + 3, sign) + ["4", ..input] -> parse_hex(input, number * 16 + 4, sign) + ["5", ..input] -> parse_hex(input, number * 16 + 5, sign) + ["6", ..input] -> parse_hex(input, number * 16 + 6, sign) + ["7", ..input] -> parse_hex(input, number * 16 + 7, sign) + ["8", ..input] -> parse_hex(input, number * 16 + 8, sign) + ["9", ..input] -> parse_hex(input, number * 16 + 9, sign) + ["a", ..input] -> parse_hex(input, number * 16 + 10, sign) + ["b", ..input] -> parse_hex(input, number * 16 + 11, sign) + ["c", ..input] -> parse_hex(input, number * 16 + 12, sign) + ["d", ..input] -> parse_hex(input, number * 16 + 13, sign) + ["e", ..input] -> parse_hex(input, number * 16 + 14, sign) + ["f", ..input] -> parse_hex(input, number * 16 + 15, sign) + ["A", ..input] -> parse_hex(input, number * 16 + 10, sign) + ["B", ..input] -> parse_hex(input, number * 16 + 11, sign) + ["C", ..input] -> parse_hex(input, number * 16 + 12, sign) + ["D", ..input] -> parse_hex(input, number * 16 + 13, sign) + ["E", ..input] -> parse_hex(input, number * 16 + 14, sign) + ["F", ..input] -> parse_hex(input, number * 16 + 15, sign) + + // Anything else and the number is terminated + input -> { + let number = case sign { + Positive -> number + Negative -> -number + } + Ok(#(Int(number), input)) + } + } +} + +fn parse_octal(input: Tokens, number: Int, sign: Sign) -> Parsed(Toml) { + case input { + ["_", ..input] -> parse_octal(input, number, sign) + ["0", ..input] -> parse_octal(input, number * 8 + 0, sign) + ["1", ..input] -> parse_octal(input, number * 8 + 1, sign) + ["2", ..input] -> parse_octal(input, number * 8 + 2, sign) + ["3", ..input] -> parse_octal(input, number * 8 + 3, sign) + ["4", ..input] -> parse_octal(input, number * 8 + 4, sign) + ["5", ..input] -> parse_octal(input, number * 8 + 5, sign) + ["6", ..input] -> parse_octal(input, number * 8 + 6, sign) + ["7", ..input] -> parse_octal(input, number * 8 + 7, sign) + + // Anything else and the number is terminated + input -> { + let number = case sign { + Positive -> number + Negative -> -number + } + Ok(#(Int(number), input)) + } + } +} + +fn parse_binary(input: Tokens, number: Int, sign: Sign) -> Parsed(Toml) { + case input { + ["_", ..input] -> parse_binary(input, number, sign) + ["0", ..input] -> parse_binary(input, number * 2 + 0, sign) + ["1", ..input] -> parse_binary(input, number * 2 + 1, sign) + + // Anything else and the number is terminated + input -> { + let number = case sign { + Positive -> number + Negative -> -number + } + Ok(#(Int(number), input)) + } + } +} + +fn parse_number(input: Tokens, number: Int, sign: Sign) -> Parsed(Toml) { + case input { + ["_", ..input] -> parse_number(input, number, sign) + ["0", ..input] -> parse_number(input, number * 10 + 0, sign) + ["1", ..input] -> parse_number(input, number * 10 + 1, sign) + ["2", ..input] -> parse_number(input, number * 10 + 2, sign) + ["3", ..input] -> parse_number(input, number * 10 + 3, sign) + ["4", ..input] -> parse_number(input, number * 10 + 4, sign) + ["5", ..input] -> parse_number(input, number * 10 + 5, sign) + ["6", ..input] -> parse_number(input, number * 10 + 6, sign) + ["7", ..input] -> parse_number(input, number * 10 + 7, sign) + ["8", ..input] -> parse_number(input, number * 10 + 8, sign) + ["9", ..input] -> parse_number(input, number * 10 + 9, sign) + + ["-", ..input] -> parse_date(input, number) + [":", ..input] if number < 24 -> parse_time_minute(input, number) + + [".", ..input] -> parse_float(input, int.to_float(number), sign, 0.1) + + ["e", "+", ..input] -> + parse_exponent(input, int.to_float(number), sign, 0, Positive) + ["e", "-", ..input] -> + parse_exponent(input, int.to_float(number), sign, 0, Negative) + ["e", ..input] -> + parse_exponent(input, int.to_float(number), sign, 0, Positive) + ["E", "+", ..input] -> + parse_exponent(input, int.to_float(number), sign, 0, Positive) + ["E", "-", ..input] -> + parse_exponent(input, int.to_float(number), sign, 0, Negative) + ["E", ..input] -> + parse_exponent(input, int.to_float(number), sign, 0, Positive) + + // Anything else and the number is terminated + input -> { + let number = case sign { + Positive -> number + Negative -> -number + } + Ok(#(Int(number), input)) + } + } +} + +fn parse_exponent( + input: Tokens, + n: Float, + n_sign: Sign, + ex: Int, + ex_sign: Sign, +) -> Parsed(Toml) { + case input { + ["_", ..input] -> parse_exponent(input, n, n_sign, ex, ex_sign) + ["0", ..input] -> parse_exponent(input, n, n_sign, ex * 10, ex_sign) + ["1", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 1, ex_sign) + ["2", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 2, ex_sign) + ["3", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 3, ex_sign) + ["4", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 4, ex_sign) + ["5", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 5, ex_sign) + ["6", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 6, ex_sign) + ["7", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 7, ex_sign) + ["8", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 8, ex_sign) + ["9", ..input] -> parse_exponent(input, n, n_sign, ex * 10 + 9, ex_sign) + + // Anything else and the number is terminated + input -> { + let number = case n_sign { + Positive -> n + Negative -> n *. -1.0 + } + let exponent = + int.to_float(case ex_sign { + Positive -> ex + Negative -> -ex + }) + let multiplier = case float.power(10.0, exponent) { + Ok(multiplier) -> multiplier + Error(_) -> 1.0 + } + Ok(#(Float(number *. multiplier), input)) + } + } +} + +fn parse_float( + input: Tokens, + number: Float, + sign: Sign, + unit: Float, +) -> Parsed(Toml) { + case input { + ["_", ..input] -> parse_float(input, number, sign, unit) + ["0", ..input] -> parse_float(input, number, sign, unit *. 0.1) + ["1", ..input] -> + parse_float(input, number +. 1.0 *. unit, sign, unit *. 0.1) + ["2", ..input] -> + parse_float(input, number +. 2.0 *. unit, sign, unit *. 0.1) + ["3", ..input] -> + parse_float(input, number +. 3.0 *. unit, sign, unit *. 0.1) + ["4", ..input] -> + parse_float(input, number +. 4.0 *. unit, sign, unit *. 0.1) + ["5", ..input] -> + parse_float(input, number +. 5.0 *. unit, sign, unit *. 0.1) + ["6", ..input] -> + parse_float(input, number +. 6.0 *. unit, sign, unit *. 0.1) + ["7", ..input] -> + parse_float(input, number +. 7.0 *. unit, sign, unit *. 0.1) + ["8", ..input] -> + parse_float(input, number +. 8.0 *. unit, sign, unit *. 0.1) + ["9", ..input] -> + parse_float(input, number +. 9.0 *. unit, sign, unit *. 0.1) + + ["e", "+", ..input] -> parse_exponent(input, number, sign, 0, Positive) + ["e", "-", ..input] -> parse_exponent(input, number, sign, 0, Negative) + ["e", ..input] -> parse_exponent(input, number, sign, 0, Positive) + ["E", "+", ..input] -> parse_exponent(input, number, sign, 0, Positive) + ["E", "-", ..input] -> parse_exponent(input, number, sign, 0, Negative) + ["E", ..input] -> parse_exponent(input, number, sign, 0, Positive) + + // Anything else and the number is terminated + input -> { + let number = case sign { + Positive -> number + Negative -> number *. -1.0 + } + Ok(#(Float(number), input)) + } + } +} + +fn parse_string(input: Tokens, string: String) -> Parsed(Toml) { + case input { + ["\"", ..input] -> Ok(#(String(string), input)) + ["\\", "t", ..input] -> parse_string(input, string <> "\t") + ["\\", "e", ..input] -> parse_string(input, string <> "\u{001b}") + ["\\", "b", ..input] -> parse_string(input, string <> "\u{0008}") + ["\\", "n", ..input] -> parse_string(input, string <> "\n") + ["\\", "r", ..input] -> parse_string(input, string <> "\r") + ["\\", "f", ..input] -> parse_string(input, string <> "\f") + ["\\", "\"", ..input] -> parse_string(input, string <> "\"") + ["\\", "\\", ..input] -> parse_string(input, string <> "\\") + [] -> Error(Unexpected("EOF", "\"")) + ["\n", ..] -> Error(Unexpected("\n", "\"")) + ["\r\n", ..] -> Error(Unexpected("\r\n", "\"")) + [g, ..input] -> parse_string(input, string <> g) + } +} + +fn parse_multi_line_string(input: Tokens, string: String) -> Parsed(Toml) { + case input { + ["\"", "\"", "\"", ..input] -> Ok(#(String(string), input)) + ["\\", "\n", ..input] -> + parse_multi_line_string(skip_whitespace(input), string) + ["\\", "\r\n", ..input] -> + parse_multi_line_string(skip_whitespace(input), string) + ["\r\n", ..input] if string == "" -> parse_multi_line_string(input, string) + ["\n", ..input] if string == "" -> parse_multi_line_string(input, string) + ["\r\n", ..input] if string == "" -> parse_multi_line_string(input, string) + ["\\", "t", ..input] -> parse_multi_line_string(input, string <> "\t") + ["\\", "n", ..input] -> parse_multi_line_string(input, string <> "\n") + ["\\", "r", ..input] -> parse_multi_line_string(input, string <> "\r") + ["\\", "\"", ..input] -> parse_multi_line_string(input, string <> "\"") + ["\\", "\\", ..input] -> parse_multi_line_string(input, string <> "\\") + [] -> Error(Unexpected("EOF", "\"")) + [g, ..input] -> parse_multi_line_string(input, string <> g) + } +} + +fn parse_multi_line_literal_string( + input: Tokens, + string: String, +) -> Parsed(Toml) { + case input { + [] -> Error(Unexpected("EOF", "\"")) + ["'", "'", "'", "'", ..] -> Error(Unexpected("''''", "'''")) + ["'", "'", "'", ..input] -> Ok(#(String(string), input)) + ["\n", ..input] if string == "" -> + parse_multi_line_literal_string(input, string) + ["\r\n", ..input] if string == "" -> + parse_multi_line_literal_string(input, string) + [g, ..input] -> parse_multi_line_literal_string(input, string <> g) + } +} + +fn parse_literal_string(input: Tokens, string: String) -> Parsed(Toml) { + case input { + [] -> Error(Unexpected("EOF", "\"")) + ["\n", ..] -> Error(Unexpected("\n", "'")) + ["\r\n", ..] -> Error(Unexpected("\r\n", "'")) + ["'", ..input] -> Ok(#(String(string), input)) + [g, ..input] -> parse_literal_string(input, string <> g) + } +} + +fn reverse_arrays_of_tables(toml: Toml) -> Toml { + case toml { + ArrayOfTables(tables) -> + ArrayOfTables(reverse_arrays_of_tables_array(tables, [])) + + Table(table) -> Table(reverse_arrays_of_tables_table(table)) + + _ -> toml + } +} + +fn reverse_arrays_of_tables_table( + table: Dict(String, Toml), +) -> Dict(String, Toml) { + dict.map_values(table, fn(_, v) { reverse_arrays_of_tables(v) }) +} + +fn reverse_arrays_of_tables_array( + array: List(Dict(String, Toml)), + acc: List(Dict(String, Toml)), +) -> List(Dict(String, Toml)) { + case array { + [] -> acc + [first, ..rest] -> { + let first = reverse_arrays_of_tables_table(first) + reverse_arrays_of_tables_array(rest, [first, ..acc]) + } + } +} + +fn parse_time_minute(input: Tokens, hours: Int) -> Parsed(Toml) { + use minutes, input <- do(parse_number_under_60(input, "minutes")) + use #(seconds, ms), input <- do(parse_time_s_ms(input)) + let time = TimeValue(hours, minutes, seconds, ms) + Ok(#(Time(time), input)) +} + +fn parse_hour_minute(input: Tokens) -> Parsed(#(Int, Int)) { + use hours, input <- do(case input { + ["0", "0", ":", ..input] -> Ok(#(0, input)) + ["0", "1", ":", ..input] -> Ok(#(1, input)) + ["0", "2", ":", ..input] -> Ok(#(2, input)) + ["0", "3", ":", ..input] -> Ok(#(3, input)) + ["0", "4", ":", ..input] -> Ok(#(4, input)) + ["0", "5", ":", ..input] -> Ok(#(5, input)) + ["0", "6", ":", ..input] -> Ok(#(6, input)) + ["0", "7", ":", ..input] -> Ok(#(7, input)) + ["0", "8", ":", ..input] -> Ok(#(8, input)) + ["0", "9", ":", ..input] -> Ok(#(9, input)) + ["1", "0", ":", ..input] -> Ok(#(10, input)) + ["1", "1", ":", ..input] -> Ok(#(11, input)) + ["1", "2", ":", ..input] -> Ok(#(12, input)) + ["1", "3", ":", ..input] -> Ok(#(13, input)) + ["1", "4", ":", ..input] -> Ok(#(14, input)) + ["1", "5", ":", ..input] -> Ok(#(15, input)) + ["1", "6", ":", ..input] -> Ok(#(16, input)) + ["1", "7", ":", ..input] -> Ok(#(17, input)) + ["1", "8", ":", ..input] -> Ok(#(18, input)) + ["1", "9", ":", ..input] -> Ok(#(19, input)) + ["2", "0", ":", ..input] -> Ok(#(20, input)) + ["2", "1", ":", ..input] -> Ok(#(21, input)) + ["2", "2", ":", ..input] -> Ok(#(22, input)) + ["2", "3", ":", ..input] -> Ok(#(23, input)) + [g, ..] -> Error(Unexpected(g, "time")) + [] -> Error(Unexpected("EOF", "time")) + }) + + use minutes, input <- do(parse_number_under_60(input, "minutes")) + Ok(#(#(hours, minutes), input)) +} + +fn parse_time_value(input: Tokens) -> Parsed(Time) { + use #(hours, minutes), input <- do(parse_hour_minute(input)) + use #(seconds, ms), input <- do(parse_time_s_ms(input)) + let time = TimeValue(hours, minutes, seconds, ms) + Ok(#(time, input)) +} + +fn parse_time_s_ms(input: Tokens) -> Parsed(#(Int, Int)) { + case input { + [":", ..input] -> { + use seconds, input <- do(parse_number_under_60(input, "seconds")) + case input { + [".", ..input] -> parse_time_ms(input, seconds, 0) + _ -> Ok(#(#(seconds, 0), input)) + } + } + + _ -> Ok(#(#(0, 0), input)) + } +} + +fn parse_time_ms(input: Tokens, seconds: Int, ms: Int) -> Parsed(#(Int, Int)) { + case input { + ["0", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 0) + ["1", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 1) + ["2", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 2) + ["3", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 3) + ["4", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 4) + ["5", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 5) + ["6", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 6) + ["7", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 7) + ["8", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 8) + ["9", ..input] if ms < 100_000 -> parse_time_ms(input, seconds, ms * 10 + 9) + + // Anything else and the number is terminated + _ -> Ok(#(#(seconds, ms), input)) + } +} + +fn parse_number_under_60(input: Tokens, expected: String) -> Parsed(Int) { + case input { + ["0", "0", ..input] -> Ok(#(0, input)) + ["0", "1", ..input] -> Ok(#(1, input)) + ["0", "2", ..input] -> Ok(#(2, input)) + ["0", "3", ..input] -> Ok(#(3, input)) + ["0", "4", ..input] -> Ok(#(4, input)) + ["0", "5", ..input] -> Ok(#(5, input)) + ["0", "6", ..input] -> Ok(#(6, input)) + ["0", "7", ..input] -> Ok(#(7, input)) + ["0", "8", ..input] -> Ok(#(8, input)) + ["0", "9", ..input] -> Ok(#(9, input)) + ["1", "0", ..input] -> Ok(#(10, input)) + ["1", "1", ..input] -> Ok(#(11, input)) + ["1", "2", ..input] -> Ok(#(12, input)) + ["1", "3", ..input] -> Ok(#(13, input)) + ["1", "4", ..input] -> Ok(#(14, input)) + ["1", "5", ..input] -> Ok(#(15, input)) + ["1", "6", ..input] -> Ok(#(16, input)) + ["1", "7", ..input] -> Ok(#(17, input)) + ["1", "8", ..input] -> Ok(#(18, input)) + ["1", "9", ..input] -> Ok(#(19, input)) + ["2", "0", ..input] -> Ok(#(20, input)) + ["2", "1", ..input] -> Ok(#(21, input)) + ["2", "2", ..input] -> Ok(#(22, input)) + ["2", "3", ..input] -> Ok(#(23, input)) + ["2", "4", ..input] -> Ok(#(24, input)) + ["2", "5", ..input] -> Ok(#(25, input)) + ["2", "6", ..input] -> Ok(#(26, input)) + ["2", "7", ..input] -> Ok(#(27, input)) + ["2", "8", ..input] -> Ok(#(28, input)) + ["2", "9", ..input] -> Ok(#(29, input)) + ["3", "0", ..input] -> Ok(#(30, input)) + ["3", "1", ..input] -> Ok(#(31, input)) + ["3", "2", ..input] -> Ok(#(32, input)) + ["3", "3", ..input] -> Ok(#(33, input)) + ["3", "4", ..input] -> Ok(#(34, input)) + ["3", "5", ..input] -> Ok(#(35, input)) + ["3", "6", ..input] -> Ok(#(36, input)) + ["3", "7", ..input] -> Ok(#(37, input)) + ["3", "8", ..input] -> Ok(#(38, input)) + ["3", "9", ..input] -> Ok(#(39, input)) + ["4", "0", ..input] -> Ok(#(40, input)) + ["4", "1", ..input] -> Ok(#(41, input)) + ["4", "2", ..input] -> Ok(#(42, input)) + ["4", "3", ..input] -> Ok(#(43, input)) + ["4", "4", ..input] -> Ok(#(44, input)) + ["4", "5", ..input] -> Ok(#(45, input)) + ["4", "6", ..input] -> Ok(#(46, input)) + ["4", "7", ..input] -> Ok(#(47, input)) + ["4", "8", ..input] -> Ok(#(48, input)) + ["4", "9", ..input] -> Ok(#(49, input)) + ["5", "0", ..input] -> Ok(#(50, input)) + ["5", "1", ..input] -> Ok(#(51, input)) + ["5", "2", ..input] -> Ok(#(52, input)) + ["5", "3", ..input] -> Ok(#(53, input)) + ["5", "4", ..input] -> Ok(#(54, input)) + ["5", "5", ..input] -> Ok(#(55, input)) + ["5", "6", ..input] -> Ok(#(56, input)) + ["5", "7", ..input] -> Ok(#(57, input)) + ["5", "8", ..input] -> Ok(#(58, input)) + ["5", "9", ..input] -> Ok(#(59, input)) + + [g, ..] -> Error(Unexpected(g, expected)) + [] -> Error(Unexpected("EOF", expected)) + } +} + +fn parse_date(input: Tokens, year: Int) -> Parsed(Toml) { + case input { + ["0", "1", "-", ..input] -> parse_date_day(input, year, 1) + ["0", "2", "-", ..input] -> parse_date_day(input, year, 2) + ["0", "3", "-", ..input] -> parse_date_day(input, year, 3) + ["0", "4", "-", ..input] -> parse_date_day(input, year, 4) + ["0", "5", "-", ..input] -> parse_date_day(input, year, 5) + ["0", "6", "-", ..input] -> parse_date_day(input, year, 6) + ["0", "7", "-", ..input] -> parse_date_day(input, year, 7) + ["0", "8", "-", ..input] -> parse_date_day(input, year, 8) + ["0", "9", "-", ..input] -> parse_date_day(input, year, 9) + ["1", "0", "-", ..input] -> parse_date_day(input, year, 10) + ["1", "1", "-", ..input] -> parse_date_day(input, year, 11) + ["1", "2", "-", ..input] -> parse_date_day(input, year, 12) + + [g, ..] -> Error(Unexpected(g, "date month")) + [] -> Error(Unexpected("EOF", "date month")) + } +} + +fn parse_date_day(input: Tokens, year: Int, month: Int) -> Parsed(Toml) { + case input { + ["0", "1", ..input] -> parse_date_end(input, year, month, 1) + ["0", "2", ..input] -> parse_date_end(input, year, month, 2) + ["0", "3", ..input] -> parse_date_end(input, year, month, 3) + ["0", "4", ..input] -> parse_date_end(input, year, month, 4) + ["0", "5", ..input] -> parse_date_end(input, year, month, 5) + ["0", "6", ..input] -> parse_date_end(input, year, month, 6) + ["0", "7", ..input] -> parse_date_end(input, year, month, 7) + ["0", "8", ..input] -> parse_date_end(input, year, month, 8) + ["0", "9", ..input] -> parse_date_end(input, year, month, 9) + ["1", "0", ..input] -> parse_date_end(input, year, month, 10) + ["1", "1", ..input] -> parse_date_end(input, year, month, 11) + ["1", "2", ..input] -> parse_date_end(input, year, month, 12) + ["1", "3", ..input] -> parse_date_end(input, year, month, 13) + ["1", "4", ..input] -> parse_date_end(input, year, month, 14) + ["1", "5", ..input] -> parse_date_end(input, year, month, 15) + ["1", "6", ..input] -> parse_date_end(input, year, month, 16) + ["1", "7", ..input] -> parse_date_end(input, year, month, 17) + ["1", "8", ..input] -> parse_date_end(input, year, month, 18) + ["1", "9", ..input] -> parse_date_end(input, year, month, 19) + ["2", "0", ..input] -> parse_date_end(input, year, month, 20) + ["2", "1", ..input] -> parse_date_end(input, year, month, 21) + ["2", "2", ..input] -> parse_date_end(input, year, month, 22) + ["2", "3", ..input] -> parse_date_end(input, year, month, 23) + ["2", "4", ..input] -> parse_date_end(input, year, month, 24) + ["2", "5", ..input] -> parse_date_end(input, year, month, 25) + ["2", "6", ..input] -> parse_date_end(input, year, month, 26) + ["2", "7", ..input] -> parse_date_end(input, year, month, 27) + ["2", "8", ..input] -> parse_date_end(input, year, month, 28) + ["2", "9", ..input] -> parse_date_end(input, year, month, 29) + ["3", "0", ..input] -> parse_date_end(input, year, month, 30) + ["3", "1", ..input] -> parse_date_end(input, year, month, 31) + + [g, ..] -> Error(Unexpected(g, "date day")) + [] -> Error(Unexpected("EOF", "date day")) + } +} + +fn parse_date_end( + input: Tokens, + year: Int, + month: Int, + day: Int, +) -> Parsed(Toml) { + let date = DateValue(year, month, day) + case input { + [" ", ..input] | ["T", ..input] -> { + use time, input <- do(parse_time_value(input)) + use offset, input <- do(parse_offset(input)) + Ok(#(DateTime(DateTimeValue(date, time, offset)), input)) + } + + _ -> Ok(#(Date(date), input)) + } +} + +fn parse_offset(input: Tokens) -> Parsed(Offset) { + case input { + ["Z", ..input] -> Ok(#(Offset(Positive, 0, 0), input)) + ["+", ..input] -> parse_offset_hours(input, Positive) + ["-", ..input] -> parse_offset_hours(input, Negative) + _ -> Ok(#(Local, input)) + } +} + +fn parse_offset_hours(input: Tokens, sign: Sign) -> Parsed(Offset) { + use #(hours, minutes), input <- do(parse_hour_minute(input)) + Ok(#(Offset(sign, hours, minutes), input)) +} diff --git a/packages/tom/test/tom_test.gleam b/packages/tom/test/tom_test.gleam new file mode 100644 index 0000000..ce51743 --- /dev/null +++ b/packages/tom/test/tom_test.gleam @@ -0,0 +1,930 @@ +import gleam/dict +import gleeunit +import gleeunit/should +import tom + +pub fn main() { + gleeunit.main() +} + +pub fn parse_empty_test() { + "" + |> tom.parse + |> should.equal(Ok(dict.from_list([]))) +} + +pub fn parse_spaces_test() { + " " + |> tom.parse + |> should.equal(Ok(dict.from_list([]))) +} + +pub fn parse_newline_test() { + "\n" + |> tom.parse + |> should.equal(Ok(dict.from_list([]))) +} + +pub fn parse_crlf_test() { + "\r\n" + |> tom.parse + |> should.equal(Ok(dict.from_list([]))) +} + +pub fn parse_quoted_key_test() { + let expected = dict.from_list([#(" ", tom.Bool(True))]) + "\" \" = true\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_single_key_test() { + let expected = dict.from_list([#("", tom.Bool(True))]) + "'' = true\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_true_test() { + let expected = dict.from_list([#("cool", tom.Bool(True))]) + "cool = true\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_false_test() { + let expected = dict.from_list([#("cool", tom.Bool(False))]) + "cool = false\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_unicode_key_test() { + let expected = dict.from_list([#("பெண்", tom.Bool(False))]) + "பெண் = false\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_int_test() { + let expected = dict.from_list([#("it", tom.Int(1))]) + "it = 1\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_int_underscored_test() { + let expected = dict.from_list([#("it", tom.Int(1_000_009))]) + "it = 1_000_0__0_9\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_int_positive_test() { + let expected = dict.from_list([#("it", tom.Int(234))]) + "it = +234\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_int_negative_test() { + let expected = dict.from_list([#("it", tom.Int(-234))]) + "it = -234\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_string_test() { + let expected = dict.from_list([#("hello", tom.String("Joe"))]) + "hello = \"Joe\"\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_string_escaped_quote_test() { + let expected = dict.from_list([#("hello", tom.String("\""))]) + "hello = \"\\\"\"\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_string_tab_test() { + let expected = dict.from_list([#("hello", tom.String("\t"))]) + "hello = \"\\t\"\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_string_newline_test() { + let expected = dict.from_list([#("hello", tom.String("\n"))]) + "hello = \"\\n\"\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_string_linefeed_test() { + let expected = dict.from_list([#("hello", tom.String("\r"))]) + "hello = \"\\r\"\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_escaped_slash_test() { + let expected = dict.from_list([#("hello", tom.String("\\"))]) + "hello = \"\\\\\"\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_float_test() { + let expected = dict.from_list([#("it", tom.Float(1.0))]) + "it = 1.0\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_bigger_float_test() { + let expected = dict.from_list([#("it", tom.Float(123_456_789.9876))]) + "it = 123456789.9876\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multi_segment_key_test() { + let expected = + dict.from_list([ + #( + "one", + tom.Table( + dict.from_list([ + #("two", tom.Table(dict.from_list([#("three", tom.Bool(True))]))), + ]), + ), + ), + ]) + "one.two.three = true\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multi_segment_key_with_spaeces_test() { + let expected = + dict.from_list([ + #( + "one", + tom.Table( + dict.from_list([ + #("two", tom.Table(dict.from_list([#("three", tom.Bool(True))]))), + ]), + ), + ), + ]) + "one . two . three = true\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multi_segment_key_quotes_test() { + let expected = + dict.from_list([ + #( + "1", + tom.Table( + dict.from_list([ + #("two", tom.Table(dict.from_list([#("3", tom.Bool(True))]))), + ]), + ), + ), + ]) + "\"1\".two.\"3\" = true\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multiple_keys_test() { + let expected = dict.from_list([#("a", tom.Int(1)), #("b", tom.Int(2))]) + "a = 1\nb = 2\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_duplicate_key_test() { + "a = 1\na = 2\n" + |> tom.parse + |> should.equal(Error(tom.KeyAlreadyInUse(["a"]))) +} + +pub fn parse_conflicting_keys_test() { + "a = 1\na.b = 2\n" + |> tom.parse + |> should.equal(Error(tom.KeyAlreadyInUse(["a"]))) +} + +pub fn parse_empty_array_test() { + let expected = dict.from_list([#("a", tom.Array([]))]) + "a = []\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_array_test() { + let expected = dict.from_list([#("a", tom.Array([tom.Int(1), tom.Int(2)]))]) + "a = [1, 2]\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multi_line_array_test() { + let expected = dict.from_list([#("a", tom.Array([tom.Int(1), tom.Int(2)]))]) + "a = [\n 1 \n ,\n 2,\n]\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_table_test() { + let expected = dict.from_list([#("a", tom.Table(dict.from_list([])))]) + "[a]\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_table_with_values_test() { + let expected = + dict.from_list([ + #( + "a", + tom.Table( + dict.from_list([ + #("a", tom.Int(1)), + #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), + ]), + ), + ), + ]) + "[a] +a = 1 +b.c = 2 +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_table_with_values_before_test() { + let expected = + dict.from_list([ + #("name", tom.String("Joe")), + #("size", tom.Int(123)), + #( + "a", + tom.Table( + dict.from_list([ + #("a", tom.Int(1)), + #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), + ]), + ), + ), + ]) + "name = \"Joe\" +size = 123 + +[a] +a = 1 +b.c = 2 +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multiple_tables_test() { + let expected = + dict.from_list([ + #("name", tom.String("Joe")), + #("size", tom.Int(123)), + #( + "a", + tom.Table( + dict.from_list([ + #("a", tom.Int(1)), + #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), + ]), + ), + ), + #("b", tom.Table(dict.from_list([#("a", tom.Int(1))]))), + ]) + "name = \"Joe\" +size = 123 + +[a] +a = 1 +b.c = 2 + +[b] +a = 1 +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_inline_table_empty_test() { + let expected = dict.from_list([#("a", tom.InlineTable(dict.from_list([])))]) + "a = {}\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_inline_table_test() { + let expected = + dict.from_list([ + #( + "a", + tom.InlineTable( + dict.from_list([ + #("a", tom.Int(1)), + #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), + ]), + ), + ), + ]) + "a = { + a = 1, + b.c = 2 +} +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_inline_trailing_comma_table_test() { + let expected = + dict.from_list([ + #( + "a", + tom.InlineTable( + dict.from_list([ + #("a", tom.Int(1)), + #("b", tom.Table(dict.from_list([#("c", tom.Int(2))]))), + ]), + ), + ), + ]) + "a = { + a = 1, + b.c = 2, +} +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_invalid_newline_in_string_test() { + "a = \"\n\"" + |> tom.parse + |> should.equal(Error(tom.Unexpected("\n", "\""))) +} + +pub fn parse_invalid_newline_windows_in_string_test() { + "a = \"\r\n\"" + |> tom.parse + |> should.equal(Error(tom.Unexpected("\r\n", "\""))) +} + +pub fn parse_array_of_tables_empty_test() { + let expected = + dict.from_list([ + #( + "a", + tom.ArrayOfTables([ + dict.from_list([]), + dict.from_list([]), + dict.from_list([]), + ]), + ), + ]) + "[[a]] +[[a]] +[[a]] +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_array_of_tables_nonempty_test() { + let expected = + dict.from_list([ + #( + "a", + tom.ArrayOfTables([ + dict.from_list([#("a", tom.Int(1))]), + dict.from_list([#("a", tom.Int(2))]), + dict.from_list([#("a", tom.Int(3))]), + ]), + ), + ]) + "[[a]] +a = 1 + +[[a]] +a = 2 + +[[a]] +a = 3 +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_array_of_tables_with_subtable_test() { + let expected = + dict.from_list([ + #( + "fruits", + tom.ArrayOfTables([ + dict.from_list([]), + dict.from_list([ + #("name", tom.String("apple")), + #( + "physical", + tom.Table( + dict.from_list([ + #("color", tom.String("red")), + #("shape", tom.String("round")), + ]), + ), + ), + ]), + ]), + ), + ]) + "[[fruits]] + +[[fruits]] +name = \"apple\" + +[fruits.physical] # subtable +color = \"red\" +shape = \"round\" +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_single_quote_string_test() { + let expected = dict.from_list([#("a", tom.String("\\n"))]) + "a = '\\n'\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multi_line_string_test() { + let expected = dict.from_list([#("a", tom.String("hello\nworld"))]) + "a = \"\"\" +hello +world\"\"\" +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multi_line_single_quote_string_test() { + let expected = dict.from_list([#("a", tom.String("hello\\n\nworld"))]) + "a = ''' +hello\\n +world''' +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multi_line_single_quote_string_too_many_quotes_test() { + "a = ''' +'''' +''' +" + |> tom.parse + |> should.equal(Error(tom.Unexpected("''''", "'''"))) +} + +pub fn parse_multi_line_string_escape_newline_test() { + let expected = + dict.from_list([ + #("a", tom.String("The quick brown fox jumps over the lazy dog.")), + ]) + "a = \"\"\" +The quick brown \\ + + + fox jumps over \\ + the lazy dog.\"\"\" +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_multi_line_string_escape_newline_windows_test() { + let expected = + dict.from_list([ + #("a", tom.String("The quick brown fox jumps over the lazy dog.")), + ]) + "a = \"\"\" +The quick brown \\\r\n + + + fox jumps over \\\r\n + the lazy dog.\"\"\" +" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_nan_test() { + let expected = dict.from_list([#("a", tom.Nan(tom.Positive))]) + "a = nan\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_positive_nan_test() { + let expected = dict.from_list([#("a", tom.Nan(tom.Positive))]) + "a = +nan\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_negative_nan_test() { + let expected = dict.from_list([#("a", tom.Nan(tom.Negative))]) + "a = -nan\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_infinity_test() { + let expected = dict.from_list([#("a", tom.Infinity(tom.Positive))]) + "a = inf\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_positive_infinity_test() { + let expected = dict.from_list([#("a", tom.Infinity(tom.Positive))]) + "a = +inf\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_negative_infinity_test() { + let expected = dict.from_list([#("a", tom.Infinity(tom.Negative))]) + "a = -inf\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_write_to_key_that_does_not_exist_test() { + let expected = + dict.from_list([ + #("apple", tom.Table(dict.from_list([#("smooth", tom.Bool(True))]))), + ]) + "apple.smooth = true\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_binary_test() { + let expected = dict.from_list([#("a", tom.Int(0b101010))]) + "a = 0b101010\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_binary_positive_test() { + let expected = dict.from_list([#("a", tom.Int(0b101010))]) + "a = +0b101010\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_binary_negative_test() { + let expected = dict.from_list([#("a", tom.Int(0b101010 * -1))]) + "a = -0b101010\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_binary_underscores_test() { + let expected = dict.from_list([#("a", tom.Int(0b101010))]) + "a = 0b1__010___1_0\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_octal_test() { + let expected = dict.from_list([#("a", tom.Int(0o1234567))]) + "a = 0o1234567\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_octal_positive_test() { + let expected = dict.from_list([#("a", tom.Int(0o1234567))]) + "a = +0o1234567\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_octal_negative_test() { + let expected = dict.from_list([#("a", tom.Int(0o1234567 * -1))]) + "a = -0o1234567\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_octal_underscores_test() { + let expected = dict.from_list([#("a", tom.Int(0o1234567))]) + "a = 0o1_23_45__6_7\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_hex_test() { + let expected = dict.from_list([#("a", tom.Int(0xdeadbeef))]) + "a = 0xdeadbeef\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_hex_positive_test() { + let expected = dict.from_list([#("a", tom.Int(0xdeadbeef))]) + "a = +0xdeadbeef\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_hex_negative_test() { + let expected = dict.from_list([#("a", tom.Int(0xdeadbeef * -1))]) + "a = -0xdeadbeef\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_hex_underscores_test() { + let expected = dict.from_list([#("a", tom.Int(0xdeadbeef))]) + "a = 0xd_e_a_d__b___e____e______f\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_hex_uppercase_test() { + let expected = dict.from_list([#("a", tom.Int(0xdeadbeef))]) + "a = +0xDEADBEEF\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_float_exponent_test() { + let expected = dict.from_list([#("a", tom.Float(1.0e6))]) + "a = 1e6\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_float_exponent_uppercase_test() { + let expected = dict.from_list([#("a", tom.Float(1.0e6))]) + "a = 1E6\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_float_exponent_postive_test() { + let expected = dict.from_list([#("a", tom.Float(5.0e22))]) + "a = 5e+22\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_float_exponent_negative_test() { + let expected = dict.from_list([#("a", tom.Float(-2.0e-22))]) + "a = -2e-22\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_float_decimal_and_exponent_test() { + let expected = dict.from_list([#("a", tom.Float(6.626e25))]) + "a = 6.626e25\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_float_decimal_and_exponent_positive_test() { + let expected = dict.from_list([#("a", tom.Float(6.626e25))]) + "a = 6.626e+25\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_float_decimal_and_exponent_negative_test() { + let expected = dict.from_list([#("a", tom.Float(6.626e-25))]) + "a = 6.626e-25\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_date_test() { + let expected = dict.from_list([#("a", tom.Date(tom.DateValue(1979, 5, 27)))]) + "a = 1979-05-27\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_time_test() { + let expected = dict.from_list([#("a", tom.Time(tom.TimeValue(7, 32, 1, 0)))]) + "a = 07:32:01\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_time_zero_minute_test() { + let expected = dict.from_list([#("a", tom.Time(tom.TimeValue(7, 0, 1, 0)))]) + "a = 07:00:01\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_time_milliseconds_test() { + let expected = + dict.from_list([#("a", tom.Time(tom.TimeValue(7, 32, 1, 999_999)))]) + "a = 07:32:01.999999\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_time_milliseconds_1_test() { + let expected = + dict.from_list([#("a", tom.Time(tom.TimeValue(7, 32, 1, 9179)))]) + "a = 07:32:01.09179\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_time_no_seconds_test() { + let expected = dict.from_list([#("a", tom.Time(tom.TimeValue(7, 32, 0, 0)))]) + "a = 07:32\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_date_time_test() { + let expected = + dict.from_list([ + #( + "a", + tom.DateTime(tom.DateTimeValue( + tom.DateValue(1979, 5, 27), + tom.TimeValue(7, 32, 0, 0), + offset: tom.Local, + )), + ), + ]) + "a = 1979-05-27T07:32:00\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_date_time_space_test() { + let expected = + dict.from_list([ + #( + "a", + tom.DateTime(tom.DateTimeValue( + tom.DateValue(1979, 5, 27), + tom.TimeValue(7, 0, 1, 0), + offset: tom.Local, + )), + ), + ]) + "a = 1979-05-27 07:00:01\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_offset_z_date_time_test() { + let expected = + dict.from_list([ + #( + "a", + tom.DateTime(tom.DateTimeValue( + tom.DateValue(1979, 5, 27), + tom.TimeValue(7, 32, 0, 0), + offset: tom.Offset(tom.Positive, 0, 0), + )), + ), + ]) + "a = 1979-05-27T07:32:00Z\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_offset_z_date_time_space_test() { + let expected = + dict.from_list([ + #( + "a", + tom.DateTime(tom.DateTimeValue( + tom.DateValue(1979, 5, 27), + tom.TimeValue(7, 0, 1, 0), + offset: tom.Offset(tom.Positive, 0, 0), + )), + ), + ]) + "a = 1979-05-27 07:00:01Z\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_offset_positive_date_time_space_test() { + let expected = + dict.from_list([ + #( + "a", + tom.DateTime(tom.DateTimeValue( + tom.DateValue(1979, 5, 27), + tom.TimeValue(7, 0, 1, 0), + offset: tom.Offset(tom.Positive, 7, 40), + )), + ), + ]) + "a = 1979-05-27 07:00:01+07:40\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_offset_negative_date_time_space_test() { + let expected = + dict.from_list([ + #( + "a", + tom.DateTime(tom.DateTimeValue( + tom.DateValue(1979, 5, 27), + tom.TimeValue(7, 0, 1, 0), + offset: tom.Offset(tom.Negative, 7, 1), + )), + ), + ]) + "a = 1979-05-27 07:00:01-07:01\n" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_no_trailing_newline_test() { + let expected = dict.from_list([#("a", tom.Int(1))]) + "a = 1" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_trailing_whitespace_test() { + let expected = dict.from_list([#("a", tom.Int(1))]) + "a = 1 " + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_trailing_other_test() { + "a = 1 b" + |> tom.parse + |> should.equal(Error(tom.Unexpected("b", "\n"))) +} + +pub fn parse_sequence_e_test() { + "a = \"\\e\"" + |> tom.parse + |> should.equal(Ok(dict.from_list([#("a", tom.String("\u{001b}"))]))) +} + +pub fn parse_sequence_f_test() { + "a = \"\\f\"" + |> tom.parse + |> should.equal(Ok(dict.from_list([#("a", tom.String("\f"))]))) +} + +pub fn parse_sequence_b_test() { + "a = \"\\b\"" + |> tom.parse + |> should.equal(Ok(dict.from_list([#("a", tom.String("\u{0008}"))]))) +} + +pub fn parse_ignore_comments_test() { + let expected = dict.from_list([#("field", tom.String("#"))]) + "# This should be ignored +field = \"#\"" + |> tom.parse + |> should.equal(Ok(expected)) +} + +pub fn parse_not_remove_hash_in_string_test() { + let content = tom.Table(dict.from_list([#("field", tom.String("#"))])) + let expected = dict.from_list([#("section", content)]) + "[section] +field = \"#\"" + |> tom.parse + |> should.equal(Ok(expected)) +}