diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b82b75f..b6238ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,14 +9,29 @@ on: jobs: test: + name: Run Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: otp-version: "28" - gleam-version: "1.10.0" + gleam-version: "1.13.0" + rebar3-version: "3" + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: gleam test --target erlang + - run: gleam test --target javascript + + check-format: + name: Check Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: erlef/setup-beam@v1 + with: + otp-version: "28" + gleam-version: "1.13.0" rebar3-version: "3" - # elixir-version: "1.15.4" - - run: gleam test - run: gleam format --check src test diff --git a/CHANGELOG.md b/CHANGELOG.md index 0798736..e2001c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Added parsing of TOML documents to `Dynamic`. + ## v2.0.0 - 2025-05-31 - Added [gleam_time](https://hexdocs.pm/gleam_time/index.html) as a dependency. diff --git a/gleam.toml b/gleam.toml index 448ca5e..a557aac 100644 --- a/gleam.toml +++ b/gleam.toml @@ -7,7 +7,7 @@ repository = { type = "github", user = "lpil", repo = "tom" } links = [{ title = "TOML website", href = "https://toml.io/en/" }] [dependencies] -gleam_stdlib = ">= 0.33.0 and < 3.0.0" +gleam_stdlib = ">= 0.53.0 and < 3.0.0" gleam_time = ">= 1.2.0 and < 2.0.0" [dev-dependencies] diff --git a/manifest.toml b/manifest.toml index 0fee6cb..9adb623 100644 --- a/manifest.toml +++ b/manifest.toml @@ -8,6 +8,6 @@ packages = [ ] [requirements] -gleam_stdlib = { version = ">= 0.33.0 and < 3.0.0" } +gleam_stdlib = { version = ">= 0.53.0 and < 3.0.0" } gleam_time = { version = ">= 1.2.0 and < 2.0.0" } gleeunit = { version = "~> 1.0" } diff --git a/src/tom.gleam b/src/tom.gleam index ecb73f0..ac2c6fc 100644 --- a/src/tom.gleam +++ b/src/tom.gleam @@ -25,6 +25,8 @@ //// ``` import gleam/dict.{type Dict} +import gleam/dynamic.{type Dynamic} +import gleam/dynamic/decode.{type Decoder} import gleam/float import gleam/int import gleam/list @@ -65,6 +67,11 @@ pub type Sign { Negative } +/// A date time value, decoded by the `decode_datetime` function +pub type DateTimeValue { + DateTimeValue(date: calendar.Date, time: calendar.TimeOfDay, offset: Offset) +} + /// An error that can occur when parsing a TOML document. pub type ParseError { /// An unexpected character was encountered when parsing the document. @@ -79,7 +86,7 @@ type Tokens = type Parsed(a) = Result(#(a, Tokens), ParseError) -/// A number of any kind, returned by the `get_number` function. +/// A number of any kind, returned by the `get_number`/`number_decoder` functions. pub type Number { NumberInt(Int) NumberFloat(Float) @@ -96,6 +103,16 @@ pub type GetError { WrongType(key: List(String), expected: String, got: String) } +@internal +pub type NanValue { + NanValue(sign: Sign) +} + +@internal +pub type InfinityValue { + InfinityValue(sign: Sign) +} + // TODO: test /// Get a value of any type from a TOML document dictionary. /// @@ -125,11 +142,63 @@ pub fn get( } } +/// Convert a parsed TOML document into a `Dynamic`. This can be used to build +/// complex decoders based on TOML data, using +/// [`gleam/dynamic/decode`](https://hexdocs.pm/gleam_stdlib/0.68.1/gleam/dynamic/decode.html). +/// Decoders are provided in this library for TOML-specific types. +/// +/// ## Examples +/// +/// ```gleam +/// let config = "name = \"Lucy\"\npoints = 5" +/// let assert Ok(parsed) = parse(config) +/// let dynamic = to_dynamic(parsed) +/// +/// let decoder = { +/// use name <- decode.field("name", decode.string) +/// use points <- decode.field("points", decode.int) +/// decode.success(#(name, points)) +/// } +/// +/// decode.run(dynamic, decoder) +/// // -> Ok(#("Lucy", 5)) +/// ``` +pub fn to_dynamic(toml: Dict(String, Toml)) -> Dynamic { + table_to_dynamic(toml) +} + +/// A convenience for parsing a TOML document and immediately converting it to +/// a `Dynamic`. This can be used to build complex decoders based on TOML data, +/// using +/// [`gleam/dynamic/decode`](https://hexdocs.pm/gleam_stdlib/0.68.1/gleam/dynamic/decode.html). +/// Decoders are provided in this library for TOML-specific types. +/// +/// ## Examples +/// +/// ```gleam +/// let config = "name = \"Lucy\"\npoints = 5" +/// let assert Ok(dynamic) = parse_to_dynamic(config) +/// +/// let decoder = { +/// use name <- decode.field("name", decode.string) +/// use points <- decode.field("points", decode.int) +/// decode.success(#(name, points)) +/// } +/// +/// decode.run(dynamic, decoder) +/// // -> Ok(#("Lucy", 5)) +/// ``` +pub fn parse_to_dynamic(input: String) -> Result(Dynamic, ParseError) { + input + |> parse() + |> result.map(to_dynamic) +} + // TODO: test /// Get an int from a TOML document dictionary. /// /// ## Examples -/// +/// /// ```gleam /// let assert Ok(parsed) = parse("a.b.c = 1") /// get_int(parsed, ["a", "b", "c"]) @@ -151,7 +220,7 @@ pub fn get_int( /// Get a float from a TOML document dictionary. /// /// ## Examples -/// +/// /// ```gleam /// let assert Ok(parsed) = parse("a.b.c = 1.1") /// get_float(parsed, ["a", "b", "c"]) @@ -173,7 +242,7 @@ pub fn get_float( /// Get a bool from a TOML document dictionary. /// /// ## Examples -/// +/// /// ```gleam /// let assert Ok(parsed) = parse("a.b.c = true") /// get_bool(parsed, ["a", "b", "c"]) @@ -195,7 +264,7 @@ pub fn get_bool( /// Get a string from a TOML document dictionary. /// /// ## Examples -/// +/// /// ```gleam /// let assert Ok(parsed) = parse("a.b.c = \"ok\"") /// get_string(parsed, ["a", "b", "c"]) @@ -216,7 +285,7 @@ pub fn get_string( /// Get a date from a TOML document dictionary. /// /// ## Examples -/// +/// /// ```gleam /// let assert Ok(parsed) = parse("a.b.c = 1979-05-27") /// get_date(parsed, ["a", "b", "c"]) @@ -1597,3 +1666,279 @@ pub fn as_number(toml: Toml) -> Result(Number, GetError) { other -> Error(WrongType([], "Number", classify(other))) } } + +/// A decoder that decodes TOML numbers into a `Number`. This could be an int, +/// a float, a NaN, or an infinity. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(toml) = tom.parse_to_dynamic("lucy = 1337") +/// decode.run(toml, decode.dict(decode.string, tom.number_decoder()))) +/// // -> Ok(dict.from_list([#("lucy", tom.NumberInt(1337))])) +/// ``` +pub fn number_decoder() -> Decoder(Number) { + decode.one_of(decode.map(decode.int, NumberInt), or: [ + decode.map(nan_decoder(), fn(nan) { NumberNan(nan.sign) }), + decode.map(infinity_decoder(), fn(infinity) { + NumberInfinity(infinity.sign) + }), + // This must come _after_ the infinity decoder, as the stdlib implementation + // will attempt to decode Infinity as a float + decode.map(decode.float, NumberFloat), + ]) +} + +/// A decoder that decodes TOML date into a `calendar.Date`. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(toml) = tom.parse_to_dynamic("future = 2015-10-21") +/// decode.run(toml, decode.dict(decode.string, tom.date_decoder()))) +/// // -> Ok( +/// // dict.from_list([ +/// // #("future", calendar.Date( +/// // year: 2015, month: calendar.October, day: 21 +/// // )), +/// // ]) +/// // ) +/// ``` +/// +pub fn date_decoder() -> Decoder(calendar.Date) { + use year <- decode.field("year", decode.int) + use month <- decode.field("month", month_decoder()) + use day <- decode.field("day", decode.int) + + decode.success(calendar.Date(year:, month:, day:)) +} + +/// A decoder that decodes TOML time into a `calendar.TimeOfDay`. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(toml) = tom.parse_to_dynamic("time = 07:28:00") +/// decode.run(toml, decode.dict(decode.string, tom.time_decoder()))) +/// // -> Ok( +/// // dict.from_list([ +/// // #("time", calendar.TimeOfDay( +/// // hours: 7, minutes: 28, seconds: 0, nanoseconds: 0 +/// // )) +/// // ]) +/// // ) +/// ``` +/// +pub fn time_decoder() -> Decoder(calendar.TimeOfDay) { + use hours <- decode.field("hours", decode.int) + use minutes <- decode.field("minutes", decode.int) + use seconds <- decode.field("seconds", decode.int) + use nanoseconds <- decode.field("nanoseconds", decode.int) + + decode.success(calendar.TimeOfDay(hours:, minutes:, seconds:, nanoseconds:)) +} + +/// A decoder that decodes TOML datetime into corresponding date, time, and +/// offset parts. +/// +/// ## Examples +/// +/// ```gleam +/// let assert Ok(toml) = tom.parse_to_dynamic("datetime = 2015-10-21T07:28:00") +/// decode.run(toml, decode.dict(decode.string, tom.datetime_decoder())) +/// // -> Ok( +/// // dict.from_list([ +/// // #("datetime", tom.DateTimeValue( +/// // date: calendar.Date( +/// // year: 2015, month: calendar.October, day: 21 +/// // ), +/// // time: calendar.TimeOfDay( +/// // hours: 7, minutes: 28, seconds: 0, nanoseconds: 0 +/// // ), +/// // offset: tom.Local +/// // )) +/// // ]) +/// // ) +/// ``` +/// +pub fn datetime_decoder() -> Decoder(DateTimeValue) { + use date <- decode.field("date", date_decoder()) + use time <- decode.field("time", time_decoder()) + use offset <- decode.field("offset", offset_decoder()) + + decode.success(DateTimeValue(date:, time:, offset:)) +} + +fn value_to_dynamic(value: Toml) -> Dynamic { + case value { + Int(x) -> dynamic.int(x) + Float(x) -> dynamic.float(x) + Bool(x) -> dynamic.bool(x) + String(x) -> dynamic.string(x) + Nan(sign) -> nan_to_dynamic(NanValue(sign)) + Infinity(sign) -> infinity_to_dynamic(InfinityValue(sign)) + Date(x) -> date_to_dynamic(x) + Time(x) -> time_to_dynamic(x) + DateTime(date, time, offset) -> datetime_to_dynamic(date, time, offset) + Table(x) -> table_to_dynamic(x) + InlineTable(x) -> table_to_dynamic(x) + Array(x) -> + x + |> list.map(value_to_dynamic) + |> dynamic.list() + ArrayOfTables(x) -> + x + |> list.map(table_to_dynamic) + |> dynamic.list() + } +} + +fn table_to_dynamic(toml: Dict(String, Toml)) -> Dynamic { + toml + |> dict.to_list() + |> list.map(fn(entry) { + let #(key, value) = entry + + #(dynamic.string(key), value_to_dynamic(value)) + }) + |> dynamic.properties() +} + +fn nan_decoder() -> Decoder(NanValue) { + decode.new_primitive_decoder("NanValue", nan_from_dynamic) +} + +fn infinity_decoder() -> Decoder(InfinityValue) { + decode.new_primitive_decoder("InfinityValue", infinity_from_dynamic) +} + +fn month_decoder() -> Decoder(calendar.Month) { + use month_name <- decode.then(decode.string) + + case month_name { + "January" -> decode.success(calendar.January) + "February" -> decode.success(calendar.February) + "March" -> decode.success(calendar.March) + "April" -> decode.success(calendar.April) + "May" -> decode.success(calendar.May) + "June" -> decode.success(calendar.June) + "July" -> decode.success(calendar.July) + "August" -> decode.success(calendar.August) + "September" -> decode.success(calendar.September) + "October" -> decode.success(calendar.October) + "November" -> decode.success(calendar.November) + "December" -> decode.success(calendar.December) + _ -> decode.failure(calendar.January, "Month") + } +} + +fn offset_decoder() -> Decoder(Offset) { + use variant <- decode.field("type", decode.string) + + case variant { + "Local" -> decode.success(Local) + "Offset" -> { + use duration <- decode.field("duration", duration_decoder()) + + decode.success(Offset(duration)) + } + + _ -> decode.failure(Local, "Offset") + } +} + +fn duration_decoder() -> Decoder(duration.Duration) { + use seconds <- decode.field("seconds", decode.int) + use nanos <- decode.field("nanoseconds", decode.int) + + let seconds_duration = duration.seconds(seconds) + let nanos_duration = duration.nanoseconds(nanos) + let full_duration = duration.add(seconds_duration, nanos_duration) + + decode.success(full_duration) +} + +@external(erlang, "tom_ffi", "nan_to_dynamic") +@external(javascript, "./tom_ffi.mjs", "nan_to_dynamic") +fn nan_to_dynamic(nan: NanValue) -> Dynamic + +@external(erlang, "tom_ffi", "nan_from_dynamic") +@external(javascript, "./tom_ffi.mjs", "nan_from_dynamic") +fn nan_from_dynamic(dynamic: Dynamic) -> Result(NanValue, NanValue) + +@external(erlang, "tom_ffi", "infinity_to_dynamic") +@external(javascript, "./tom_ffi.mjs", "infinity_to_dynamic") +fn infinity_to_dynamic(infinity: InfinityValue) -> Dynamic + +@external(erlang, "tom_ffi", "infinity_from_dynamic") +@external(javascript, "./tom_ffi.mjs", "infinity_from_dynamic") +fn infinity_from_dynamic( + infinity: Dynamic, +) -> Result(InfinityValue, InfinityValue) + +fn date_to_dynamic(date: calendar.Date) -> Dynamic { + dynamic.properties([ + #(dynamic.string("day"), dynamic.int(date.day)), + #(dynamic.string("month"), month_to_dynamic(date.month)), + #(dynamic.string("year"), dynamic.int(date.year)), + ]) +} + +fn month_to_dynamic(month: calendar.Month) -> Dynamic { + case month { + calendar.January -> dynamic.string("January") + calendar.February -> dynamic.string("February") + calendar.March -> dynamic.string("March") + calendar.April -> dynamic.string("April") + calendar.May -> dynamic.string("May") + calendar.June -> dynamic.string("June") + calendar.July -> dynamic.string("July") + calendar.August -> dynamic.string("August") + calendar.September -> dynamic.string("September") + calendar.October -> dynamic.string("October") + calendar.November -> dynamic.string("November") + calendar.December -> dynamic.string("December") + } +} + +fn time_to_dynamic(time: calendar.TimeOfDay) -> Dynamic { + dynamic.properties([ + #(dynamic.string("hours"), dynamic.int(time.hours)), + #(dynamic.string("minutes"), dynamic.int(time.minutes)), + #(dynamic.string("seconds"), dynamic.int(time.seconds)), + #(dynamic.string("nanoseconds"), dynamic.int(time.nanoseconds)), + ]) +} + +fn datetime_to_dynamic( + date: calendar.Date, + time: calendar.TimeOfDay, + offset: Offset, +) -> Dynamic { + dynamic.properties([ + #(dynamic.string("date"), date_to_dynamic(date)), + #(dynamic.string("time"), time_to_dynamic(time)), + #(dynamic.string("offset"), offset_to_dynamic(offset)), + ]) +} + +fn offset_to_dynamic(offset: Offset) -> Dynamic { + case offset { + Local -> + dynamic.properties([#(dynamic.string("type"), dynamic.string("Local"))]) + Offset(duration) -> + dynamic.properties([ + #(dynamic.string("type"), dynamic.string("Offset")), + #(dynamic.string("duration"), duration_to_dynamic(duration)), + ]) + } +} + +fn duration_to_dynamic(duration: duration.Duration) -> Dynamic { + let #(seconds, nanos) = duration.to_seconds_and_nanoseconds(duration) + + dynamic.properties([ + #(dynamic.string("seconds"), dynamic.int(seconds)), + #(dynamic.string("nanoseconds"), dynamic.int(nanos)), + ]) +} diff --git a/src/tom_ffi.erl b/src/tom_ffi.erl new file mode 100644 index 0000000..57038ed --- /dev/null +++ b/src/tom_ffi.erl @@ -0,0 +1,34 @@ +-module(tom_ffi). +-export([ + infinity_to_dynamic/1, + infinity_from_dynamic/1, + nan_to_dynamic/1, + nan_from_dynamic/1 +]). + +nan_to_dynamic({nan_value, positive}) -> + positive_nan; +nan_to_dynamic({nan_value, negative}) -> + negative_nan. + +nan_from_dynamic(positive_nan) -> + {ok, {nan_value, positive}}; +nan_from_dynamic(negative_nan) -> + {ok, {nan_value, negative}}; +nan_from_dynamic(_Other) -> + % Value here is a placeholder + {error, {nan_value, positive}}. + +infinity_to_dynamic({infinity_value, positive}) -> + positive_infinity; +infinity_to_dynamic({infinity_value, negative}) -> + negative_infinity. + +infinity_from_dynamic(positive_infinity) -> + {ok, {infinity_value, positive}}; +infinity_from_dynamic(negative_infinity) -> + {ok, {infinity_value, negative}}; +infinity_from_dynamic(_Other) -> + % Value here is a placeholder + {error, {infinity_value, positive}}. + diff --git a/src/tom_ffi.mjs b/src/tom_ffi.mjs new file mode 100644 index 0000000..25c024a --- /dev/null +++ b/src/tom_ffi.mjs @@ -0,0 +1,63 @@ +import { Result$Ok, Result$Error } from "./gleam.mjs"; +import { + Sign$Positive, + Sign$Negative, + Sign$isPositive, + Sign$isNegative, + InfinityValue$InfinityValue$sign, + InfinityValue$InfinityValue, + NanValue$NanValue$sign, + NanValue$NanValue, +} from "./tom.mjs"; + +// We can't represent positive/negative NaNs in JS so we must have a representation for them +const negativeNaN = Symbol("-NaN"); +const positiveNaN = Symbol("+NaN"); + +export function nan_to_dynamic(nan_value) { + let sign = NanValue$NanValue$sign(nan_value); + + if (Sign$isPositive(sign)) { + return positiveNaN; + } else if (Sign$isNegative(sign)) { + return negativeNaN; + } else { + // Should never happen by the type system + throw "value is not a nan"; + } +} + +export function nan_from_dynamic(value) { + if (value == positiveNaN) { + return Result$Ok(NanValue$NanValue(Sign$Positive())); + } else if (value == negativeNaN) { + return Result$Ok(NanValue$NanValue(Sign$Negative())); + } else { + // Value here is a placeholder + return Result$Error(NanValue$NanValue(Sign$Positive())); + } +} + +export function infinity_to_dynamic(infinity_value) { + let sign = InfinityValue$InfinityValue$sign(infinity_value); + + if (Sign$isPositive(sign)) { + return Infinity; + } else if (Sign$isNegative(sign)) { + return -Infinity; + } else { + // Should never happen by the type system + throw "value is not an infinity"; + } +} + +export function infinity_from_dynamic(value) { + if (value == Infinity) { + return Result$Ok(InfinityValue$InfinityValue(Sign$Positive())); + } else if (value == -Infinity) { + return Result$Ok(InfinityValue$InfinityValue(Sign$Negative())); + } else { + // Value here is a placeholder + return Result$Error(NanValue$NanValue(Sign$Positive())); + } +} diff --git a/test/tom_test.gleam b/test/tom_test.gleam index 47d6b95..af0f8ed 100644 --- a/test/tom_test.gleam +++ b/test/tom_test.gleam @@ -1,5 +1,10 @@ import gleam/dict +import gleam/dynamic/decode +import gleam/int +import gleam/list +import gleam/order import gleam/result +import gleam/string import gleam/time/calendar import gleam/time/duration import gleam/time/timestamp @@ -1163,3 +1168,307 @@ pub fn get_timestamp_test() { tom.get_timestamp(parsed, ["a", "b", "c"]) |> should.equal(Error(tom.WrongType(["a"], "Table", "Int"))) } + +pub fn to_dynamic_integer_test() { + let assert Ok(parsed) = tom.parse("a = 5") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_field <- decode.field("a", decode.int) + decode.success(a_field) + } + assert decode.run(dynamic, decoder) == Ok(5) +} + +pub fn to_dynamic_nan_test() { + let assert Ok(parsed) = tom.parse("a = nan") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a <- decode.field("a", tom.number_decoder()) + decode.success(a) + } + assert decode.run(dynamic, decoder) == Ok(tom.NumberNan(tom.Positive)) +} + +pub fn to_dynamic_nan_positive_test() { + let assert Ok(parsed) = tom.parse("a = +nan") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a <- decode.field("a", tom.number_decoder()) + decode.success(a) + } + assert decode.run(dynamic, decoder) == Ok(tom.NumberNan(tom.Positive)) +} + +pub fn to_dynamic_nan_negative_test() { + let assert Ok(parsed) = tom.parse("a = -nan") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a <- decode.field("a", tom.number_decoder()) + decode.success(a) + } + assert decode.run(dynamic, decoder) == Ok(tom.NumberNan(tom.Negative)) +} + +pub fn to_dynamic_infinity_test() { + let assert Ok(parsed) = tom.parse("a = inf") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a <- decode.field("a", tom.number_decoder()) + decode.success(a) + } + assert decode.run(dynamic, decoder) == Ok(tom.NumberInfinity(tom.Positive)) +} + +pub fn to_dynamic_infinity_positive_test() { + let assert Ok(parsed) = tom.parse("a = +inf") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a <- decode.field("a", tom.number_decoder()) + decode.success(a) + } + assert decode.run(dynamic, decoder) == Ok(tom.NumberInfinity(tom.Positive)) +} + +pub fn to_dynamic_infinity_negative_test() { + let assert Ok(parsed) = tom.parse("a = -inf") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a <- decode.field("a", tom.number_decoder()) + decode.success(a) + } + assert decode.run(dynamic, decoder) == Ok(tom.NumberInfinity(tom.Negative)) +} + +pub fn to_dynamic_string_test() { + let assert Ok(parsed) = tom.parse("a = \"Hello, Joe\"") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_field <- decode.field("a", decode.string) + decode.success(a_field) + } + + assert decode.run(dynamic, decoder) == Ok("Hello, Joe") +} + +pub fn to_dynamic_bool_test() { + let assert Ok(parsed) = tom.parse("a = false") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_field <- decode.field("a", decode.bool) + decode.success(a_field) + } + + assert decode.run(dynamic, decoder) == Ok(False) +} + +pub fn to_dynamic_array_test() { + let assert Ok(parsed) = tom.parse("a = [1, 2, 3]") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_field <- decode.field("a", decode.list(decode.int)) + decode.success(a_field) + } + + assert decode.run(dynamic, decoder) == Ok([1, 2, 3]) +} + +pub fn to_dynamic_table_test() { + let assert Ok(parsed) = + tom.parse( + " + [a] + a = 1 + b = 2 + c = 3 + ", + ) + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_table_field <- decode.field( + "a", + decode.dict(decode.string, decode.int), + ) + + decode.success(a_table_field) + } + + let decoded = + dynamic + |> decode.run(decoder) + |> result.map(fn(value) { + value + |> dict.to_list() + |> list.sort(fn(a, b) { + let #(a_1, a_2) = a + let #(b_1, b_2) = b + + order.break_tie(string.compare(a_1, b_1), int.compare(a_2, b_2)) + }) + }) + + assert decoded == Ok([#("a", 1), #("b", 2), #("c", 3)]) +} + +pub fn to_dynamic_inline_table_test() { + let assert Ok(parsed) = tom.parse("a = {a = 1, b = 2, c = 3}") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_table_field <- decode.field( + "a", + decode.dict(decode.string, decode.int), + ) + + decode.success(a_table_field) + } + + let decoded = + dynamic + |> decode.run(decoder) + |> result.map(fn(value) { + value + |> dict.to_list() + |> list.sort(fn(a, b) { + let #(a_1, a_2) = a + let #(b_1, b_2) = b + + order.break_tie(string.compare(a_1, b_1), int.compare(a_2, b_2)) + }) + }) + + assert decoded == Ok([#("a", 1), #("b", 2), #("c", 3)]) +} + +pub fn to_dynamic_array_of_tables_test() { + let assert Ok(parsed) = + tom.parse( + "[[a]] + a = 1 + b = 2 + c = 3 + [[a]] + a = 4 + b = 5 + c = 6 + [[a]] + a = 7 + b = 8 + c = 9 + ", + ) + + let dynamic = tom.to_dynamic(parsed) + let decode_table = { + use a_field <- decode.field("a", decode.int) + use b_field <- decode.field("b", decode.int) + use c_field <- decode.field("c", decode.int) + + decode.success(#(a_field, b_field, c_field)) + } + + let decode = { + use a_field <- decode.field("a", decode.list(decode_table)) + decode.success(a_field) + } + + let decoded = + dynamic + |> decode.run(decode) + |> result.map(fn(value) { + list.sort(value, fn(a, b) { + let #(a_1, a_2, a_3) = a + let #(b_1, b_2, b_3) = b + + int.compare(a_1, b_1) + |> order.break_tie(int.compare(a_2, b_2)) + |> order.break_tie(int.compare(a_3, b_3)) + }) + }) + assert decoded == Ok([#(1, 2, 3), #(4, 5, 6), #(7, 8, 9)]) +} + +pub fn to_dynamic_date_test() { + let assert Ok(parsed) = tom.parse("a = 1979-05-27") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_field <- decode.field("a", tom.date_decoder()) + decode.success(a_field) + } + + assert decode.run(dynamic, decoder) + == Ok(calendar.Date(year: 1979, month: calendar.May, day: 27)) +} + +pub fn to_dynamic_time_test() { + let assert Ok(parsed) = tom.parse("a = 07:32:01") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_field <- decode.field("a", tom.time_decoder()) + decode.success(a_field) + } + + assert decode.run(dynamic, decoder) + == Ok(calendar.TimeOfDay(hours: 7, minutes: 32, seconds: 1, nanoseconds: 0)) +} + +pub fn to_dynamic_datetime_test() { + let assert Ok(parsed) = tom.parse("a = 1979-05-27T07:32:00Z") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_field <- decode.field("a", tom.datetime_decoder()) + decode.success(a_field) + } + + assert decode.run(dynamic, decoder) + == Ok(tom.DateTimeValue( + date: calendar.Date(year: 1979, month: calendar.May, day: 27), + time: calendar.TimeOfDay( + hours: 7, + minutes: 32, + seconds: 0, + nanoseconds: 0, + ), + offset: tom.Offset(calendar.utc_offset), + )) +} + +pub fn to_dynamic_datetime_local_offset_test() { + let assert Ok(parsed) = tom.parse("a = 1979-05-27T07:32:00") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_field <- decode.field("a", tom.datetime_decoder()) + decode.success(a_field) + } + + assert decode.run(dynamic, decoder) + == Ok(tom.DateTimeValue( + date: calendar.Date(year: 1979, month: calendar.May, day: 27), + time: calendar.TimeOfDay( + hours: 7, + minutes: 32, + seconds: 0, + nanoseconds: 0, + ), + offset: tom.Local, + )) +} + +pub fn to_dynamic_datetime_numeric_offset_offset_test() { + let assert Ok(parsed) = tom.parse("a = 1979-05-27T07:32:00-05:00") + let dynamic = tom.to_dynamic(parsed) + let decoder = { + use a_field <- decode.field("a", tom.datetime_decoder()) + decode.success(a_field) + } + + assert decode.run(dynamic, decoder) + == Ok(tom.DateTimeValue( + date: calendar.Date(year: 1979, month: calendar.May, day: 27), + time: calendar.TimeOfDay( + hours: 7, + minutes: 32, + seconds: 0, + nanoseconds: 0, + ), + offset: tom.Offset(duration.hours(-5)), + )) +}