| 1 | // Generic encode[T]/decode[T] coverage. Exercises every supported V |
| 2 | // type family — primitives, arrays, maps, structs (with attributes), |
| 3 | // optional fields, enums, RawMessage, Marshaler/Unmarshaler — and |
| 4 | // asserts byte-exact output for at least one case per family so we |
| 5 | // catch silent encoding drift. |
| 6 | module main |
| 7 | |
| 8 | import encoding.cbor |
| 9 | import encoding.hex |
| 10 | |
| 11 | fn h(s string) []u8 { |
| 12 | return hex.decode(s) or { panic('invalid hex: ${s}') } |
| 13 | } |
| 14 | |
| 15 | fn beq(a []u8, b []u8) bool { |
| 16 | if a.len != b.len { |
| 17 | return false |
| 18 | } |
| 19 | for i in 0 .. a.len { |
| 20 | if a[i] != b[i] { |
| 21 | return false |
| 22 | } |
| 23 | } |
| 24 | return true |
| 25 | } |
| 26 | |
| 27 | // --------------------------------------------------------------------- |
| 28 | // Primitive round-trips |
| 29 | // --------------------------------------------------------------------- |
| 30 | |
| 31 | fn test_round_trip_primitives() { |
| 32 | bytes_bool := cbor.encode[bool](true, cbor.EncodeOpts{})! |
| 33 | assert cbor.decode[bool](bytes_bool, cbor.DecodeOpts{})! == true |
| 34 | |
| 35 | bytes_i32 := cbor.encode[i32](-42, cbor.EncodeOpts{})! |
| 36 | assert cbor.decode[i32](bytes_i32, cbor.DecodeOpts{})! == -42 |
| 37 | |
| 38 | bytes_u64 := cbor.encode[u64](u64(0x1234_5678_9abc_def0), cbor.EncodeOpts{})! |
| 39 | assert cbor.decode[u64](bytes_u64, cbor.DecodeOpts{})! == 0x1234_5678_9abc_def0 |
| 40 | |
| 41 | bytes_f64 := cbor.encode[f64](3.141592653589793, cbor.EncodeOpts{})! |
| 42 | assert cbor.decode[f64](bytes_f64, cbor.DecodeOpts{})! == 3.141592653589793 |
| 43 | |
| 44 | bytes_str := cbor.encode[string]('hello, 世界', cbor.EncodeOpts{})! |
| 45 | assert cbor.decode[string](bytes_str, cbor.DecodeOpts{})! == 'hello, 世界' |
| 46 | } |
| 47 | |
| 48 | // --------------------------------------------------------------------- |
| 49 | // Arrays and maps |
| 50 | // --------------------------------------------------------------------- |
| 51 | |
| 52 | fn test_array_round_trip() { |
| 53 | src := [1, 2, 3, 4, 5] |
| 54 | bytes := cbor.encode[[]int](src, cbor.EncodeOpts{})! |
| 55 | got := cbor.decode[[]int](bytes, cbor.DecodeOpts{})! |
| 56 | assert got == src |
| 57 | } |
| 58 | |
| 59 | fn test_map_round_trip() { |
| 60 | src := { |
| 61 | 'a': 1 |
| 62 | 'b': 2 |
| 63 | 'c': 3 |
| 64 | } |
| 65 | bytes := cbor.encode[map[string]int](src, cbor.EncodeOpts{})! |
| 66 | got := cbor.decode[map[string]int](bytes, cbor.DecodeOpts{})! |
| 67 | for k, v in src { |
| 68 | assert got[k] == v |
| 69 | } |
| 70 | assert got.len == src.len |
| 71 | } |
| 72 | |
| 73 | fn test_nested_array_map() { |
| 74 | src := [ |
| 75 | { |
| 76 | 'k': 'v1' |
| 77 | }, |
| 78 | { |
| 79 | 'k': 'v2' |
| 80 | }, |
| 81 | ] |
| 82 | bytes := cbor.encode[[]map[string]string](src, cbor.EncodeOpts{})! |
| 83 | got := cbor.decode[[]map[string]string](bytes, cbor.DecodeOpts{})! |
| 84 | assert got.len == 2 |
| 85 | assert got[0]['k'] == 'v1' |
| 86 | assert got[1]['k'] == 'v2' |
| 87 | } |
| 88 | |
| 89 | // --------------------------------------------------------------------- |
| 90 | // Structs — attributes, optional, rename strategies |
| 91 | // --------------------------------------------------------------------- |
| 92 | |
| 93 | struct Person { |
| 94 | name string |
| 95 | age int |
| 96 | } |
| 97 | |
| 98 | fn test_struct_basic() { |
| 99 | p := Person{ |
| 100 | name: 'Alice' |
| 101 | age: 42 |
| 102 | } |
| 103 | bytes := cbor.encode[Person](p, cbor.EncodeOpts{})! |
| 104 | got := cbor.decode[Person](bytes, cbor.DecodeOpts{})! |
| 105 | assert got.name == 'Alice' |
| 106 | assert got.age == 42 |
| 107 | } |
| 108 | |
| 109 | struct WithAttrs { |
| 110 | user_id string @[cbor: 'uid'] |
| 111 | password string @[skip] |
| 112 | internal string @[cbor: '-'] |
| 113 | keep string |
| 114 | } |
| 115 | |
| 116 | fn test_struct_attributes() { |
| 117 | p := WithAttrs{ |
| 118 | user_id: 'u-1' |
| 119 | password: 'secret' |
| 120 | internal: 'hidden' |
| 121 | keep: 'visible' |
| 122 | } |
| 123 | bytes := cbor.encode[WithAttrs](p, cbor.EncodeOpts{})! |
| 124 | // Decode generically to inspect structure. |
| 125 | v := cbor.decode[cbor.Value](bytes, cbor.DecodeOpts{})! |
| 126 | assert v is cbor.Map |
| 127 | if v is cbor.Map { |
| 128 | mut keys := []string{} |
| 129 | for pair in v.pairs { |
| 130 | if pair.key is cbor.Text { |
| 131 | keys << pair.key.value |
| 132 | } |
| 133 | } |
| 134 | assert 'uid' in keys |
| 135 | assert 'keep' in keys |
| 136 | assert 'password' !in keys |
| 137 | assert 'internal' !in keys |
| 138 | } |
| 139 | got := cbor.decode[WithAttrs](bytes, cbor.DecodeOpts{})! |
| 140 | assert got.user_id == 'u-1' |
| 141 | assert got.keep == 'visible' |
| 142 | } |
| 143 | |
| 144 | struct WithOption { |
| 145 | name string |
| 146 | tag ?string |
| 147 | } |
| 148 | |
| 149 | fn test_struct_option_field() { |
| 150 | none_p := WithOption{ |
| 151 | name: 'a' |
| 152 | tag: none |
| 153 | } |
| 154 | bytes := cbor.encode[WithOption](none_p, cbor.EncodeOpts{})! |
| 155 | got := cbor.decode[WithOption](bytes, cbor.DecodeOpts{})! |
| 156 | assert got.name == 'a' |
| 157 | assert got.tag == none |
| 158 | |
| 159 | some_p := WithOption{ |
| 160 | name: 'b' |
| 161 | tag: ?string('hot') |
| 162 | } |
| 163 | bytes2 := cbor.encode[WithOption](some_p, cbor.EncodeOpts{})! |
| 164 | got2 := cbor.decode[WithOption](bytes2, cbor.DecodeOpts{})! |
| 165 | assert got2.name == 'b' |
| 166 | assert got2.tag != none |
| 167 | assert got2.tag or { '' } == 'hot' |
| 168 | } |
| 169 | |
| 170 | @[cbor_rename_all: 'kebab-case'] |
| 171 | struct WithRename { |
| 172 | user_name string |
| 173 | user_age int |
| 174 | } |
| 175 | |
| 176 | fn test_struct_rename_all() { |
| 177 | p := WithRename{ |
| 178 | user_name: 'Bob' |
| 179 | user_age: 30 |
| 180 | } |
| 181 | bytes := cbor.encode[WithRename](p, cbor.EncodeOpts{})! |
| 182 | v := cbor.decode[cbor.Value](bytes, cbor.DecodeOpts{})! |
| 183 | if v is cbor.Map { |
| 184 | mut keys := []string{} |
| 185 | for pair in v.pairs { |
| 186 | if pair.key is cbor.Text { |
| 187 | keys << pair.key.value |
| 188 | } |
| 189 | } |
| 190 | assert 'user-name' in keys |
| 191 | assert 'user-age' in keys |
| 192 | } |
| 193 | // And the rename round-trips. |
| 194 | got := cbor.decode[WithRename](bytes, cbor.DecodeOpts{})! |
| 195 | assert got.user_name == 'Bob' |
| 196 | assert got.user_age == 30 |
| 197 | } |
| 198 | |
| 199 | // --------------------------------------------------------------------- |
| 200 | // Enum |
| 201 | // --------------------------------------------------------------------- |
| 202 | |
| 203 | enum Color { |
| 204 | red |
| 205 | green |
| 206 | blue |
| 207 | } |
| 208 | |
| 209 | fn test_enum_round_trip() { |
| 210 | bytes := cbor.encode[Color](Color.green, cbor.EncodeOpts{})! |
| 211 | got := cbor.decode[Color](bytes, cbor.DecodeOpts{})! |
| 212 | assert got == Color.green |
| 213 | } |
| 214 | |
| 215 | // --------------------------------------------------------------------- |
| 216 | // RawMessage — preserves bytes byte-for-byte |
| 217 | // --------------------------------------------------------------------- |
| 218 | |
| 219 | fn test_raw_message_round_trip() { |
| 220 | original := h('a26161016162820203') |
| 221 | raw := cbor.decode[cbor.RawMessage](original, cbor.DecodeOpts{})! |
| 222 | again := cbor.encode[cbor.RawMessage](raw, cbor.EncodeOpts{})! |
| 223 | assert beq(again, original) |
| 224 | } |
| 225 | |
| 226 | // --------------------------------------------------------------------- |
| 227 | // Marshaler / Unmarshaler — user-controlled wire format |
| 228 | // --------------------------------------------------------------------- |
| 229 | |
| 230 | struct Ipv4 { |
| 231 | mut: |
| 232 | octets [4]u8 |
| 233 | } |
| 234 | |
| 235 | pub fn (ip Ipv4) to_cbor() []u8 { |
| 236 | mut p := cbor.new_packer(cbor.EncodeOpts{ initial_cap: 8 }) |
| 237 | p.pack_bytes([ip.octets[0], ip.octets[1], ip.octets[2], ip.octets[3]]) |
| 238 | return p.bytes().clone() |
| 239 | } |
| 240 | |
| 241 | pub fn (mut ip Ipv4) from_cbor(data []u8) ! { |
| 242 | mut u := cbor.new_unpacker(data, cbor.DecodeOpts{}) |
| 243 | bytes := u.unpack_bytes()! |
| 244 | if bytes.len != 4 { |
| 245 | return error('Ipv4: expected 4 bytes, got ${bytes.len}') |
| 246 | } |
| 247 | ip.octets[0] = bytes[0] |
| 248 | ip.octets[1] = bytes[1] |
| 249 | ip.octets[2] = bytes[2] |
| 250 | ip.octets[3] = bytes[3] |
| 251 | } |
| 252 | |
| 253 | fn test_marshaler_round_trip() { |
| 254 | ip := Ipv4{ |
| 255 | octets: [u8(192), 168, 1, 1]! |
| 256 | } |
| 257 | bytes := cbor.encode[Ipv4](ip, cbor.EncodeOpts{})! |
| 258 | // Wire bytes: 0x44 (bytes len 4) followed by 4 octets. |
| 259 | assert beq(bytes, h('44c0a80101')) |
| 260 | got := cbor.decode[Ipv4](bytes, cbor.DecodeOpts{})! |
| 261 | assert got.octets[0] == 192 |
| 262 | assert got.octets[1] == 168 |
| 263 | assert got.octets[2] == 1 |
| 264 | assert got.octets[3] == 1 |
| 265 | } |
| 266 | |
| 267 | // --------------------------------------------------------------------- |
| 268 | // Integer-range checks on decode |
| 269 | // --------------------------------------------------------------------- |
| 270 | |
| 271 | fn test_int_range_overflow_rejected() { |
| 272 | // 256 doesn't fit u8. |
| 273 | bytes := cbor.encode[u16](u16(256), cbor.EncodeOpts{})! |
| 274 | if _ := cbor.decode[u8](bytes, cbor.DecodeOpts{}) { |
| 275 | assert false, 'expected u8 range error' |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | fn test_negative_to_unsigned_rejected() { |
| 280 | bytes := cbor.encode[i64](-1, cbor.EncodeOpts{})! |
| 281 | if _ := cbor.decode[u64](bytes, cbor.DecodeOpts{}) { |
| 282 | assert false, 'expected u64 range error' |
| 283 | } |
| 284 | } |
| 285 | |