diff --git a/vlib/toml/tests/encode_and_decode_test.v b/vlib/toml/tests/encode_and_decode_test.v index 12ab0b6cbd..6f57bf7473 100644 --- a/vlib/toml/tests/encode_and_decode_test.v +++ b/vlib/toml/tests/encode_and_decode_test.v @@ -1,13 +1,40 @@ import toml enum JobTitle { - manager - executive worker + executive + manager } +struct Pet { + name string + nicknames []string + age u64 + income int + height f32 + has_furr bool + title JobTitle + address Address + // *¹ Currently it is only possible to decode a single nested struct generically. + // As soon as we decode another nested struct (e.g. within this struct, like `contact` below) + // or only one nested struct within another struct, it results in wrong values or errors. + // Related issue: https://github.com/vlang/v/issues/18110 + // contact Contact +} + +struct Address { + street string + city string +} + +// *¹ +/* +struct Contact { + phone string +}*/ + struct Employee { -pub mut: +mut: name string age int salary f32 @@ -15,13 +42,49 @@ pub mut: title JobTitle } +struct Arrs { + strs []string + bools []bool + ints []int + i64s []i64 + u64s []u64 + f32s []f32 + f64s []f64 + dts []toml.DateTime + dates []toml.Date + times []toml.Time +} + +fn test_encode_and_decode() { + // *¹ + // p := Pet{'Mr. Scratchy McEvilPaws', ['Freddy', 'Fred', 'Charles'], 8, -1, 0.8, true, .manager, Address{'1428 Elm Street', 'Springwood'}, Contact{'123-456-7890'}} + p := Pet{'Mr. Scratchy McEvilPaws', ['Freddy', 'Fred', 'Charles'], 8, -1, 0.8, true, .manager, Address{'1428 Elm Street', 'Springwood'}} + s := 'name = "Mr. Scratchy McEvilPaws" +nicknames = [ + "Freddy", + "Fred", + "Charles" +] +age = 8 +income = -1 +height = 0.8 +has_furr = true +title = 2 +address = { street = "1428 Elm Street", city = "Springwood" }' + // contact = { phone = "123-456-7890" }' // *¹ + + assert toml.encode[Pet](p) == s + assert toml.decode[Pet](s)! == p +} + pub fn (e Employee) to_toml() string { mut mp := map[string]toml.Any{} mp['name'] = toml.Any(e.name) mp['age'] = toml.Any(e.age) - mp['salary'] = toml.Any(e.salary) mp['is_human'] = toml.Any(e.is_human) - mp['title'] = toml.Any(int(e.title)) + // Change some values to assert that the custom method is used instead of generic encoding. + mp['salary'] = toml.Any(f32(e.salary) + 5000.0) + mp['title'] = toml.Any(int(e.title) + 1) return mp.to_toml() } @@ -29,19 +92,20 @@ pub fn (mut e Employee) from_toml(any toml.Any) { mp := any.as_map() e.name = mp['name'] or { toml.Any('') }.string() e.age = mp['age'] or { toml.Any(0) }.int() - e.salary = mp['salary'] or { toml.Any(0) }.f32() e.is_human = mp['is_human'] or { toml.Any(false) }.bool() - e.title = unsafe { JobTitle(mp['title'] or { toml.Any(0) }.int()) } + // Change some values to assert that the custom method is used instead of generic decoding. + e.salary = mp['salary'] or { toml.Any(0) }.f32() - 15000.0 + e.title = unsafe { JobTitle(mp['title'] or { toml.Any(0) }.int() - 2) } } -fn test_encode_and_decode() { - x := Employee{'Peter', 28, 95000.5, true, .worker} +fn test_custom_encode_and_decode() { + x := Employee{'Peter', 28, 95000.5, true, .executive} s := toml.encode[Employee](x) eprintln('Employee x: ${s}') assert s == r'name = "Peter" age = 28 -salary = 95000.5 is_human = true +salary = 100000.5 title = 2' y := toml.decode[Employee](s) or { @@ -52,7 +116,64 @@ title = 2' eprintln('Employee y: ${y}') assert y.name == 'Peter' assert y.age == 28 - assert y.salary == 95000.5 + assert y.salary == 85000.5 assert y.is_human == true assert y.title == .worker } + +fn test_array_encode_decode() { + a := Arrs{ + strs: ['foo', 'bar'] + bools: [true, false] + ints: [-1, 2] + i64s: [i64(-2)] + u64s: [u64(123)] + f32s: [f32(1.0), f32(2.5)] + f64s: [100000.5, -123.0] + dts: [toml.DateTime{'1979-05-27T07:32:00Z'}, toml.DateTime{'1979-05-27T07:32:00Z'}] + dates: [toml.Date{'1979-05-27'}, toml.Date{'2022-12-31'}] + times: [toml.Time{'07:32:59'}, toml.Time{'17:32:04'}] + } + + s := 'strs = [ + "foo", + "bar" +] +bools = [ + true, + false +] +ints = [ + -1, + 2 +] +i64s = [ + -2 +] +u64s = [ + 123 +] +f32s = [ + 1.0, + 2.5 +] +f64s = [ + 100000.5, + -123.0 +] +dts = [ + 1979-05-27T07:32:00Z, + 1979-05-27T07:32:00Z +] +dates = [ + 1979-05-27, + 2022-12-31 +] +times = [ + 07:32:59, + 17:32:04 +]' + + assert toml.encode[Arrs](a) == s + assert toml.decode[Arrs](s)! == a +} diff --git a/vlib/toml/toml.v b/vlib/toml/toml.v index dd783f3264..ad94095c38 100644 --- a/vlib/toml/toml.v +++ b/vlib/toml/toml.v @@ -13,17 +13,99 @@ pub struct Null { } // decode decodes a TOML `string` into the target type `T`. +// If `T` has a custom `.from_toml()` method, it will be used instead of the default. pub fn decode[T](toml_txt string) !T { doc := parse_text(toml_txt)! mut typ := T{} - typ.from_toml(doc.to_any()) + $for method in T.methods { + $if method.name == 'from_toml' { + typ.$method(doc.to_any()) + return typ + } + } + decode_struct[T](doc.to_any(), mut typ) return typ } +fn decode_struct[T](doc Any, mut typ T) { + $for field in T.fields { + value := doc.value(field.name) + $if field.is_enum { + typ.$(field.name) = value.int() + } $else $if field.typ is string { + typ.$(field.name) = value.string() + } $else $if field.typ is bool { + typ.$(field.name) = value.bool() + } $else $if field.typ is int { + typ.$(field.name) = value.int() + } $else $if field.typ is i64 { + typ.$(field.name) = value.i64() + } $else $if field.typ is u64 { + typ.$(field.name) = value.u64() + } $else $if field.typ is f32 { + typ.$(field.name) = value.f32() + } $else $if field.typ is f64 { + typ.$(field.name) = value.f64() + } $else $if field.typ is DateTime { + typ.$(field.name) = value.datetime() + } $else $if field.typ is Date { + typ.$(field.name) = value.date() + } $else $if field.typ is Time { + typ.$(field.name) = value.time() + } $else $if field.is_array { + arr := value.array() + match typeof(typ.$(field.name)).name { + '[]string' { typ.$(field.name) = arr.as_strings() } + '[]int' { typ.$(field.name) = arr.map(it.int()) } + '[]i64' { typ.$(field.name) = arr.map(it.i64()) } + '[]u64' { typ.$(field.name) = arr.map(it.u64()) } + '[]f32' { typ.$(field.name) = arr.map(it.f32()) } + '[]f64' { typ.$(field.name) = arr.map(it.f64()) } + '[]bool' { typ.$(field.name) = arr.map(it.bool()) } + '[]toml.DateTime' { typ.$(field.name) = arr.map(it.datetime()) } + '[]toml.Date' { typ.$(field.name) = arr.map(it.date()) } + '[]toml.Time' { typ.$(field.name) = arr.map(it.time()) } + else {} + } + } $else $if field.is_struct { + mut s := typ.$(field.name) + decode_struct(value, mut s) + typ.$(field.name) = s + } + } +} + // encode encodes the type `T` into a TOML string. -// Currently encode expects the method `.to_toml()` exists on `T`. +// If `T` has a custom `.to_toml()` method, it will be used instead of the default. pub fn encode[T](typ T) string { - return typ.to_toml() + $for method in T.methods { + $if method.name == 'to_toml' { + return typ.$method() + } + } + mp := encode_struct[T](typ) + return mp.to_toml() +} + +fn encode_struct[T](typ T) map[string]Any { + mut mp := map[string]Any{} + $for field in T.fields { + value := typ.$(field.name) + $if field.is_enum { + mp[field.name] = Any(int(value)) + } $else $if field.is_struct { + mp[field.name] = encode_struct(value) + } $else $if field.is_array { + mut arr := []Any{} + for v in value { + arr << Any(v) + } + mp[field.name] = arr + } $else { + mp[field.name] = Any(value) + } + } + return mp } // DateTime is the representation of an RFC 3339 datetime string.