protobuf codec with static type inference jsr.io/@mary/protobuf
typescript jsr

Compare changes

Choose any two refs to compare.

Changed files
+572 -286
lib
+2 -13
README.md
··· 1 1 # protobuf 2 2 3 + [JSR](https://jsr.io/@mary/protobuf) | [source code](https://tangled.sh/mary.my.id/pkg-protobuf) 4 + 3 5 protobuf codec with static type inference. 4 6 5 7 ```typescript ··· 69 71 value: 1, 70 72 next: 2, 71 73 }); 72 - } 73 - 74 - // maps 75 - { 76 - const Scoreboard = p.message({ 77 - scores: p.map(p.string(), p.int32()), 78 - }, { 79 - scores: 1, 80 - }); 81 - 82 - const data: p.InferInput<typeof Scoreboard> = { 83 - scores: new Map([['alice', 100], ['bob', 95]]), 84 - }; 85 74 } 86 75 ``` 87 76
+1 -1
deno.json
··· 1 1 { 2 2 "name": "@mary/protobuf", 3 - "version": "0.2.0", 3 + "version": "0.2.1", 4 4 "license": "0BSD", 5 5 "exports": "./lib/mod.ts", 6 6 "imports": {
+569 -272
lib/mod.test.ts
··· 1 - import { 2 - assert, 3 - assertAlmostEquals, 4 - assertArrayIncludes, 5 - assertEquals, 6 - assertStringIncludes, 7 - assertThrows, 8 - } from '@std/assert'; 1 + import { assert, assertAlmostEquals, assertEquals, assertStringIncludes, assertThrows } from '@std/assert'; 9 2 import { nanoid } from 'nanoid/non-secure'; 10 3 11 4 import * as p from './mod.ts'; ··· 16 9 const Message = p.message({ text: p.string() }, { text: 1 }); 17 10 18 11 const cases = [ 19 - '', 20 - 'hello world', 21 - 'hello ๐Ÿš€', 22 - 'a'.repeat(1000), 23 - 'Cafรฉ', 24 - 'ใŠใฏใ‚ˆใ†ใ”ใ–ใ„ใพใ™โ˜€๏ธ', 25 - 'เคจเคฎเคธเฅเคคเฅ‡', 26 - 'ะ—ะดั€ะฐะฒัั‚ะฒัƒะนั‚ะต', 27 - 'ไฝ '.repeat(43), 28 - '๐ŸŒŸ'.repeat(32), 29 - '๐Ÿš€๐ŸŒŸ๐Ÿ’ป', 30 - '๐Ÿณ๏ธโ€๐ŸŒˆ๐Ÿณ๏ธโ€โšง๏ธ', 12 + { 13 + text: '', 14 + expected: Uint8Array.from([0x0a, 0x00]), 15 + }, // field 1, length 0 16 + { 17 + text: 'hello world', 18 + expected: Uint8Array.from([ 19 + 0x0a, 20 + 0x0b, 21 + 0x68, 22 + 0x65, 23 + 0x6c, 24 + 0x6c, 25 + 0x6f, 26 + 0x20, 27 + 0x77, 28 + 0x6f, 29 + 0x72, 30 + 0x6c, 31 + 0x64, 32 + ]), 33 + }, // field 1, length 11 34 + { 35 + text: 'hello ๐Ÿš€', 36 + expected: Uint8Array.from([0x0a, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0xf0, 0x9f, 0x9a, 0x80]), 37 + }, // field 1, length 10, UTF-8 rocket 38 + { 39 + text: 'Cafรฉ', 40 + expected: Uint8Array.from([0x0a, 0x05, 0x43, 0x61, 0x66, 0xc3, 0xa9]), 41 + }, // field 1, length 5, UTF-8 cafรฉ 42 + 43 + { 44 + text: 'a'.repeat(1000), 45 + expected: null, 46 + }, 47 + { 48 + text: 'ใŠใฏใ‚ˆใ†ใ”ใ–ใ„ใพใ™โ˜€๏ธ', 49 + expected: null, 50 + }, 51 + { 52 + text: 'เคจเคฎเคธเฅเคคเฅ‡', 53 + expected: null, 54 + }, 55 + { 56 + text: 'ะ—ะดั€ะฐะฒัั‚ะฒัƒะนั‚ะต', 57 + expected: null, 58 + }, 59 + { 60 + text: 'ไฝ '.repeat(43), 61 + expected: null, 62 + }, 63 + { 64 + text: '๐ŸŒŸ'.repeat(32), 65 + expected: null, 66 + }, 67 + { 68 + text: '๐Ÿš€๐ŸŒŸ๐Ÿ’ป', 69 + expected: null, 70 + }, 71 + { 72 + text: '๐Ÿณ๏ธโ€๐ŸŒˆ๐Ÿณ๏ธโ€โšง๏ธ', 73 + expected: null, 74 + }, 31 75 ]; 32 76 33 - for (const text of cases) { 77 + for (const { text, expected } of cases) { 34 78 const encoded = p.encode(Message, { text }); 35 - const decoded = p.decode(Message, encoded); 79 + if (expected !== null) { 80 + assertEquals(encoded, expected); 81 + } 36 82 83 + const decoded = p.decode(Message, encoded); 37 84 assertEquals(decoded, { text }); 38 85 } 39 86 }); ··· 42 89 const Message = p.message({ value: p.int32() }, { value: 1 }); 43 90 44 91 const cases = [ 45 - 0, 46 - 1, 47 - -1, 48 - 127, 49 - -128, 50 - 255, 51 - -256, 52 - 32767, 53 - -32768, 54 - 65535, 55 - -65536, 56 - 2147483647, // max int32 57 - -2147483648, // min int32 92 + { // field 1, varint 0 93 + value: 0, 94 + expected: Uint8Array.from([0x08, 0x00]), 95 + }, 96 + { // field 1, varint 1 97 + value: 1, 98 + expected: Uint8Array.from([0x08, 0x01]), 99 + }, 100 + { // field 1, varint -1 (int32) 101 + value: -1, 102 + expected: Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0x0f]), 103 + }, 104 + { // field 1, varint 127 105 + value: 127, 106 + expected: Uint8Array.from([0x08, 0x7f]), 107 + }, 108 + { // field 1, varint -128 109 + value: -128, 110 + expected: Uint8Array.from([0x08, 0x80, 0xff, 0xff, 0xff, 0x0f]), 111 + }, 112 + { // field 1, varint 255 113 + value: 255, 114 + expected: Uint8Array.from([0x08, 0xff, 0x01]), 115 + }, 116 + { // field 1, varint -256 117 + value: -256, 118 + expected: Uint8Array.from([0x08, 0x80, 0xfe, 0xff, 0xff, 0x0f]), 119 + }, 120 + { // field 1, varint 32767 121 + value: 32767, 122 + expected: Uint8Array.from([0x08, 0xff, 0xff, 0x01]), 123 + }, 124 + { // field 1, varint -32768 125 + value: -32768, 126 + expected: Uint8Array.from([0x08, 0x80, 0x80, 0xfe, 0xff, 0x0f]), 127 + }, 128 + { // field 1, varint 65535 129 + value: 65535, 130 + expected: Uint8Array.from([0x08, 0xff, 0xff, 0x03]), 131 + }, 132 + { // field 1, varint -65536 133 + value: -65536, 134 + expected: Uint8Array.from([0x08, 0x80, 0x80, 0xfc, 0xff, 0x0f]), 135 + }, 136 + { // max int32 137 + value: 2147483647, 138 + expected: null, 139 + }, 140 + { // min int32 141 + value: -2147483648, 142 + expected: null, 143 + }, 58 144 ]; 59 145 60 - for (const value of cases) { 146 + for (const { value, expected } of cases) { 61 147 const encoded = p.encode(Message, { value }); 62 - const decoded = p.decode(Message, encoded); 148 + if (expected !== null) { 149 + assertEquals(encoded, expected); 150 + } 63 151 152 + const decoded = p.decode(Message, encoded); 64 153 assertEquals(decoded, { value }); 65 154 } 66 155 }); ··· 69 158 const Message = p.message({ value: p.int64() }, { value: 1 }); 70 159 71 160 const cases = [ 72 - 0n, 73 - 1n, 74 - -1n, 75 - 127n, 76 - -128n, 77 - 9223372036854775807n, // max int64 78 - -9223372036854775808n, // min int64 161 + { // field 1, varint 0 162 + value: 0n, 163 + expected: Uint8Array.from([0x08, 0x00]), 164 + }, 165 + { // field 1, varint 1 166 + value: 1n, 167 + expected: Uint8Array.from([0x08, 0x01]), 168 + }, 169 + { // field 1, varint -1 (int64) 170 + value: -1n, 171 + expected: Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]), 172 + }, 173 + { // field 1, varint 127 174 + value: 127n, 175 + expected: Uint8Array.from([0x08, 0x7f]), 176 + }, 177 + { // field 1, varint -128 178 + value: -128n, 179 + expected: Uint8Array.from([0x08, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]), 180 + }, 181 + { // max int64 182 + value: 9223372036854775807n, 183 + expected: null, 184 + }, 185 + { // min int64 186 + value: -9223372036854775808n, 187 + expected: null, 188 + }, 79 189 ]; 80 190 81 - for (const value of cases) { 191 + for (const { value, expected } of cases) { 82 192 const encoded = p.encode(Message, { value }); 193 + if (expected !== null) { 194 + assertEquals(encoded, expected); 195 + } 196 + 83 197 const decoded = p.decode(Message, encoded); 84 - 85 198 assertEquals(decoded, { value }); 86 199 } 87 200 }); ··· 90 203 const Message = p.message({ value: p.uint32() }, { value: 1 }); 91 204 92 205 const cases = [ 93 - 0, 94 - 1, 95 - 127, 96 - 255, 97 - 32767, 98 - 65535, 99 - 2147483647, 100 - 4294967295, // max uint32 206 + { value: 0, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, varint 0 207 + { value: 1, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, varint 1 208 + { value: 127, expected: Uint8Array.from([0x08, 0x7f]) }, // field 1, varint 127 209 + { value: 255, expected: Uint8Array.from([0x08, 0xff, 0x01]) }, // field 1, varint 255 210 + { value: 32767, expected: Uint8Array.from([0x08, 0xff, 0xff, 0x01]) }, // field 1, varint 32767 211 + { value: 65535, expected: Uint8Array.from([0x08, 0xff, 0xff, 0x03]) }, // field 1, varint 65535 212 + { value: 2147483647, expected: Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0x07]) }, // field 1, varint max int32 213 + { value: 4294967295, expected: Uint8Array.from([0x08, 0xff, 0xff, 0xff, 0xff, 0x0f]) }, // max uint32 101 214 ]; 102 215 103 - for (const value of cases) { 216 + for (const { value, expected } of cases) { 104 217 const encoded = p.encode(Message, { value }); 218 + if (expected !== null) { 219 + assertEquals(encoded, expected); 220 + } 221 + 105 222 const decoded = p.decode(Message, encoded); 106 - 107 223 assertEquals(decoded, { value }); 108 224 } 109 225 }); ··· 112 228 const Message = p.message({ value: p.uint64() }, { value: 1 }); 113 229 114 230 const cases = [ 115 - 0n, 116 - 1n, 117 - 127n, 118 - 255n, 119 - 18446744073709551615n, // max uint64 231 + { value: 0n, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, varint 0 232 + { value: 1n, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, varint 1 233 + { value: 127n, expected: Uint8Array.from([0x08, 0x7f]) }, // field 1, varint 127 234 + { value: 255n, expected: Uint8Array.from([0x08, 0xff, 0x01]) }, // field 1, varint 255 235 + { value: 18446744073709551615n, expected: null }, // max uint64 120 236 ]; 121 237 122 - for (const value of cases) { 238 + for (const { value, expected } of cases) { 123 239 const encoded = p.encode(Message, { value }); 240 + if (expected !== null) { 241 + assertEquals(encoded, expected); 242 + } 243 + 124 244 const decoded = p.decode(Message, encoded); 125 - 126 245 assertEquals(decoded, { value }); 127 246 } 128 247 }); ··· 131 250 const schema = p.message({ value: p.sint32() }, { value: 1 }); 132 251 133 252 const testCases = [ 134 - 0, 135 - 1, 136 - -1, 137 - 2, 138 - -2, 139 - 127, 140 - -128, 141 - 2147483647, // max int32 142 - -2147483648, // min int32 253 + { value: 0, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, zigzag 0 -> varint 0 254 + { value: 1, expected: Uint8Array.from([0x08, 0x02]) }, // field 1, zigzag 1 -> varint 2 255 + { value: -1, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, zigzag -1 -> varint 1 256 + { value: 2, expected: Uint8Array.from([0x08, 0x04]) }, // field 1, zigzag 2 -> varint 4 257 + { value: -2, expected: Uint8Array.from([0x08, 0x03]) }, // field 1, zigzag -2 -> varint 3 258 + { value: 127, expected: Uint8Array.from([0x08, 0xfe, 0x01]) }, // field 1, zigzag 127 -> varint 254 259 + { value: -128, expected: Uint8Array.from([0x08, 0xff, 0x01]) }, // field 1, zigzag -128 -> varint 255 260 + { value: 2147483647, expected: null }, // max int32 261 + { value: -2147483648, expected: null }, // min int32 143 262 ]; 144 263 145 - for (const value of testCases) { 264 + for (const { value, expected } of testCases) { 146 265 const encoded = p.encode(schema, { value }); 147 - const decoded = p.decode(schema, encoded); 266 + if (expected !== null) { 267 + assertEquals(encoded, expected); 268 + } 148 269 270 + const decoded = p.decode(schema, encoded); 149 271 assertEquals(decoded, { value }); 150 272 } 151 273 }); ··· 154 276 const Message = p.message({ value: p.sint64() }, { value: 1 }); 155 277 156 278 const cases = [ 157 - 0n, 158 - 1n, 159 - -1n, 160 - 2n, 161 - -2n, 162 - 127n, 163 - -128n, 164 - 9223372036854775807n, // max int64 165 - -9223372036854775808n, // min int64 279 + { value: 0n, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, zigzag 0 -> varint 0 280 + { value: 1n, expected: Uint8Array.from([0x08, 0x02]) }, // field 1, zigzag 1 -> varint 2 281 + { value: -1n, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, zigzag -1 -> varint 1 282 + { value: 2n, expected: Uint8Array.from([0x08, 0x04]) }, // field 1, zigzag 2 -> varint 4 283 + { value: -2n, expected: Uint8Array.from([0x08, 0x03]) }, // field 1, zigzag -2 -> varint 3 284 + { value: 127n, expected: Uint8Array.from([0x08, 0xfe, 0x01]) }, // field 1, zigzag 127 -> varint 254 285 + { value: -128n, expected: Uint8Array.from([0x08, 0xff, 0x01]) }, // field 1, zigzag -128 -> varint 255 286 + { value: 9223372036854775807n, expected: null }, // max int64 287 + { value: -9223372036854775808n, expected: null }, // min int64 166 288 ]; 167 289 168 - for (const value of cases) { 290 + for (const { value, expected } of cases) { 169 291 const encoded = p.encode(Message, { value }); 170 - const decoded = p.decode(Message, encoded); 292 + if (expected !== null) { 293 + assertEquals(encoded, expected); 294 + } 171 295 296 + const decoded = p.decode(Message, encoded); 172 297 assertEquals(decoded, { value }); 173 298 } 174 299 }); ··· 177 302 const Message = p.message({ value: p.float() }, { value: 1 }); 178 303 179 304 const cases = [ 180 - 0.0, 181 - 1.0, 182 - -1.0, 183 - 3.14159, 184 - -3.14159, 185 - 1.5e10, 186 - -1.5e10, 187 - 3.4028235e38, // close to max float32 188 - 1.175494e-38, // close to min positive float32 189 - Infinity, 190 - -Infinity, 191 - NaN, 305 + { value: 0.0, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x00, 0x00]) }, // field 1, float32 0.0 (little-endian) 306 + { value: 1.0, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x80, 0x3f]) }, // field 1, float32 1.0 (little-endian) 307 + { value: -1.0, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x80, 0xbf]) }, // field 1, float32 -1.0 (little-endian) 308 + { value: 3.14159, expected: null }, 309 + { value: -3.14159, expected: null }, 310 + { value: 1.5e10, expected: null }, 311 + { value: -1.5e10, expected: null }, 312 + { value: 3.4028235e38, expected: null }, // close to max float32 313 + { value: 1.175494e-38, expected: null }, // close to min positive float32 314 + { value: Infinity, expected: null }, 315 + { value: -Infinity, expected: null }, 316 + { value: NaN, expected: null }, 192 317 ]; 193 318 194 - for (const value of cases) { 319 + for (const { value, expected } of cases) { 195 320 const encoded = p.encode(Message, { value }); 321 + if (expected !== null) { 322 + assertEquals(encoded, expected); 323 + } 324 + 196 325 const decoded = p.decode(Message, encoded); 197 326 198 327 // Special handling for infinity values ··· 212 341 const Message = p.message({ value: p.double() }, { value: 1 }); 213 342 214 343 const cases = [ 215 - 0.0, 216 - 1.0, 217 - -1.0, 218 - 3.141592653589793, 219 - -3.141592653589793, 220 - 1.7976931348623157e+308, // close to max double 221 - 2.2250738585072014e-308, // close to min positive double 222 - Infinity, 223 - -Infinity, 224 - NaN, 344 + { // field 1, double 0.0 (little-endian) 345 + value: 0.0, 346 + expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 347 + }, 348 + { // field 1, double 1.0 (little-endian) 349 + value: 1.0, 350 + expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f]), 351 + }, 352 + { // field 1, double -1.0 (little-endian) 353 + value: -1.0, 354 + expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xbf]), 355 + }, 356 + { 357 + value: 3.141592653589793, 358 + expected: null, 359 + }, 360 + { 361 + value: -3.141592653589793, 362 + expected: null, 363 + }, 364 + { // close to max double 365 + value: 1.7976931348623157e+308, 366 + expected: null, 367 + }, 368 + { // close to min positive double 369 + value: 2.2250738585072014e-308, 370 + expected: null, 371 + }, 372 + { 373 + value: Infinity, 374 + expected: null, 375 + }, 376 + { 377 + value: -Infinity, 378 + expected: null, 379 + }, 380 + { 381 + value: NaN, 382 + expected: null, 383 + }, 225 384 ]; 226 385 227 - for (const value of cases) { 386 + for (const { value, expected } of cases) { 228 387 const encoded = p.encode(Message, { value }); 229 - const decoded = p.decode(Message, encoded); 388 + if (expected !== null) { 389 + assertEquals(encoded, expected); 390 + } 230 391 392 + const decoded = p.decode(Message, encoded); 231 393 assertEquals(decoded, { value }); 232 394 } 233 395 }); ··· 290 452 Deno.test('boolean encoding/decoding', () => { 291 453 const Message = p.message({ value: p.boolean() }, { value: 1 }); 292 454 293 - const cases = [true, false]; 455 + const cases = [ 456 + { value: true, expected: Uint8Array.from([0x08, 0x01]) }, // field 1, varint 1 457 + { value: false, expected: Uint8Array.from([0x08, 0x00]) }, // field 1, varint 0 458 + ]; 294 459 295 - for (const value of cases) { 460 + for (const { value, expected } of cases) { 296 461 const encoded = p.encode(Message, { value }); 297 - const decoded = p.decode(Message, encoded); 462 + assertEquals(encoded, expected); 298 463 464 + const decoded = p.decode(Message, encoded); 299 465 assertEquals(decoded, { value }); 300 466 } 301 467 }); ··· 304 470 const Message = p.message({ data: p.bytes() }, { data: 1 }); 305 471 306 472 const cases = [ 307 - Uint8Array.from([]), 308 - Uint8Array.from([0]), 309 - Uint8Array.from([1, 2, 3, 4, 5]), 310 - Uint8Array.from([255, 254, 253]), 311 - new Uint8Array(Array.from({ length: 1000 }, (_, i) => i % 256)), // large array 473 + { // field 1, length 0 474 + data: new Uint8Array(0), 475 + expected: Uint8Array.from([0x0a, 0x00]), 476 + }, 477 + { // field 1, length 1, byte 0 478 + data: Uint8Array.from([0]), 479 + expected: Uint8Array.from([0x0a, 0x01, 0x00]), 480 + }, 481 + { // field 1, length 5 482 + data: Uint8Array.from([1, 2, 3, 4, 5]), 483 + expected: Uint8Array.from([0x0a, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05]), 484 + }, 485 + { // field 1, length 3 486 + data: Uint8Array.from([255, 254, 253]), 487 + expected: Uint8Array.from([0x0a, 0x03, 0xff, 0xfe, 0xfd]), 488 + }, 489 + { // large array 490 + data: new Uint8Array(Array.from({ length: 1000 }, (_, i) => i % 256)), 491 + expected: null, 492 + }, 312 493 ]; 313 494 314 - for (const data of cases) { 495 + for (const { data, expected } of cases) { 315 496 const encoded = p.encode(Message, { data }); 497 + if (expected !== null) { 498 + assertEquals(encoded, expected); 499 + } 500 + 316 501 const decoded = p.decode(Message, encoded); 317 - 318 502 assertEquals(decoded, { data }); 319 503 } 320 504 }); ··· 323 507 const Message = p.message({ value: p.fixed32() }, { value: 1 }); 324 508 325 509 const cases = [ 326 - 0, 327 - 1, 328 - 255, 329 - 65535, 330 - 4294967295, // max uint32 510 + { // field 1, fixed32 0 (little-endian) 511 + value: 0, 512 + expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x00, 0x00]), 513 + }, 514 + { // field 1, fixed32 1 (little-endian) 515 + value: 1, 516 + expected: Uint8Array.from([0x0d, 0x01, 0x00, 0x00, 0x00]), 517 + }, 518 + { // field 1, fixed32 255 (little-endian) 519 + value: 255, 520 + expected: Uint8Array.from([0x0d, 0xff, 0x00, 0x00, 0x00]), 521 + }, 522 + { // field 1, fixed32 65535 (little-endian) 523 + value: 65535, 524 + expected: Uint8Array.from([0x0d, 0xff, 0xff, 0x00, 0x00]), 525 + }, 526 + { // field 1, fixed32 max uint32 (little-endian) 527 + value: 4294967295, 528 + expected: Uint8Array.from([0x0d, 0xff, 0xff, 0xff, 0xff]), 529 + }, 331 530 ]; 332 531 333 - for (const value of cases) { 532 + for (const { value, expected } of cases) { 334 533 const encoded = p.encode(Message, { value }); 534 + assertEquals(encoded, expected); 535 + 335 536 const decoded = p.decode(Message, encoded); 336 - 337 537 assertEquals(decoded, { value }); 338 538 } 339 539 }); ··· 342 542 const Message = p.message({ value: p.fixed64() }, { value: 1 }); 343 543 344 544 const cases = [ 345 - 0n, 346 - 1n, 347 - 255n, 348 - 65535n, 349 - 18446744073709551615n, // max uint64 545 + { // field 1, fixed64 0 (little-endian) 546 + value: 0n, 547 + expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 548 + }, 549 + { // field 1, fixed64 1 (little-endian) 550 + value: 1n, 551 + expected: Uint8Array.from([0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 552 + }, 553 + { // field 1, fixed64 255 (little-endian) 554 + value: 255n, 555 + expected: Uint8Array.from([0x09, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 556 + }, 557 + { // field 1, fixed64 65535 (little-endian) 558 + value: 65535n, 559 + expected: Uint8Array.from([0x09, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 560 + }, 561 + { // field 1, fixed64 max uint64 (little-endian) 562 + value: 18446744073709551615n, 563 + expected: Uint8Array.from([0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 564 + }, 350 565 ]; 351 566 352 - for (const value of cases) { 567 + for (const { value, expected } of cases) { 353 568 const encoded = p.encode(Message, { value }); 569 + assertEquals(encoded, expected); 570 + 354 571 const decoded = p.decode(Message, encoded); 355 - 356 572 assertEquals(decoded, { value }); 357 573 } 358 574 }); ··· 361 577 const Message = p.message({ value: p.sfixed32() }, { value: 1 }); 362 578 363 579 const cases = [ 364 - 0, 365 - 1, 366 - -1, 367 - 2147483647, // max int32 368 - -2147483648, // min int32 580 + { value: 0, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x00, 0x00]) }, // field 1, sfixed32 0 (little-endian) 581 + { value: 1, expected: Uint8Array.from([0x0d, 0x01, 0x00, 0x00, 0x00]) }, // field 1, sfixed32 1 (little-endian) 582 + { value: -1, expected: Uint8Array.from([0x0d, 0xff, 0xff, 0xff, 0xff]) }, // field 1, sfixed32 -1 (little-endian, two's complement) 583 + { value: 2147483647, expected: Uint8Array.from([0x0d, 0xff, 0xff, 0xff, 0x7f]) }, // field 1, sfixed32 max int32 (little-endian) 584 + { value: -2147483648, expected: Uint8Array.from([0x0d, 0x00, 0x00, 0x00, 0x80]) }, // field 1, sfixed32 min int32 (little-endian) 369 585 ]; 370 586 371 - for (const value of cases) { 587 + for (const { value, expected } of cases) { 372 588 const encoded = p.encode(Message, { value }); 373 - const decoded = p.decode(Message, encoded); 589 + assertEquals(encoded, expected); 374 590 591 + const decoded = p.decode(Message, encoded); 375 592 assertEquals(decoded, { value }); 376 593 } 377 594 }); ··· 380 597 const Message = p.message({ value: p.sfixed64() }, { value: 1 }); 381 598 382 599 const cases = [ 383 - 0n, 384 - 1n, 385 - -1n, 386 - 9223372036854775807n, // max int64 387 - -9223372036854775808n, // min int64 600 + { // field 1, sfixed64 0 (little-endian) 601 + value: 0n, 602 + expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 603 + }, 604 + { // field 1, sfixed64 1 (little-endian) 605 + value: 1n, 606 + expected: Uint8Array.from([0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), 607 + }, 608 + { // field 1, sfixed64 -1 (little-endian, two's complement) 609 + value: -1n, 610 + expected: Uint8Array.from([0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 611 + }, 612 + { // field 1, sfixed64 max int64 (little-endian) 613 + value: 9223372036854775807n, 614 + expected: Uint8Array.from([0x09, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f]), 615 + }, 616 + { // field 1, sfixed64 min int64 (little-endian) 617 + value: -9223372036854775808n, 618 + expected: Uint8Array.from([0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]), 619 + }, 388 620 ]; 389 621 390 - for (const value of cases) { 622 + for (const { value, expected } of cases) { 391 623 const encoded = p.encode(Message, { value }); 624 + assertEquals(encoded, expected); 625 + 392 626 const decoded = p.decode(Message, encoded); 393 - 394 627 assertEquals(decoded, { value }); 395 628 } 396 629 }); ··· 402 635 Deno.test('repeated fields', () => { 403 636 const cases = [ 404 637 { 405 - numbers: [], 406 - strings: [], 638 + data: { numbers: [], strings: [] }, 639 + unpacked: new Uint8Array(0), // empty message 640 + packed: Uint8Array.from([0x08, 0x00, 0x12, 0x00]), // field 1: tag + length 0, field 2: tag + length 0 407 641 }, 408 642 { 409 - numbers: [1], 410 - strings: ['hello'], 643 + data: { numbers: [1], strings: ['hello'] }, 644 + unpacked: Uint8Array.from([0x08, 0x01, 0x12, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), // field 1: varint 1, field 2: length 5 + "hello" 645 + packed: Uint8Array.from([0x08, 0x01, 0x01, 0x12, 0x06, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), // field 1: tag + length 1 + varint 1, field 2: tag + length 6 + (length 5 + "hello") 411 646 }, 412 647 { 413 - numbers: [1, 2, 3, -1, -2], 414 - strings: ['hello', 'world', ''], 648 + data: { numbers: [1, 2, 3], strings: ['hi'] }, 649 + unpacked: Uint8Array.from([0x08, 0x01, 0x08, 0x02, 0x08, 0x03, 0x12, 0x02, 0x68, 0x69]), // field 1: 1,2,3, field 2: "hi" 650 + packed: Uint8Array.from([0x08, 0x03, 0x01, 0x02, 0x03, 0x12, 0x03, 0x02, 0x68, 0x69]), // field 1: tag + length 3 + varints 1,2,3, field 2: tag + length 3 + (length 2 + "hi") 415 651 }, 416 652 { 417 - numbers: Array.from({ length: 100 }, (_, i) => i), 418 - strings: Array.from({ length: 100 }, (_, i) => `item${i}`), 653 + data: { 654 + numbers: Array.from({ length: 100 }, (_, i) => i), 655 + strings: Array.from({ length: 100 }, (_, i) => `item${i}`), 656 + }, 657 + unpacked: null, // too large for expected bytes 658 + packed: null, // too large for expected bytes 419 659 }, 420 660 ]; 421 661 662 + // Test non-packed repeated fields 422 663 { 423 664 const Message = p.message({ 424 665 numbers: p.repeated(p.int32(), false), ··· 428 669 strings: 2, 429 670 }); 430 671 431 - for (const data of cases) { 672 + for (const { data, unpacked } of cases) { 432 673 const encoded = p.encode(Message, data); 674 + if (unpacked !== null) { 675 + assertEquals(encoded, unpacked); 676 + } 677 + 433 678 const decoded = p.decode(Message, encoded); 434 - 435 679 assertEquals(decoded, data); 436 680 } 437 681 } 438 682 683 + // Test packed repeated fields (different wire format) 439 684 { 440 685 const Message = p.message({ 441 686 numbers: p.repeated(p.int32(), true), ··· 445 690 strings: 2, 446 691 }); 447 692 448 - for (const data of cases) { 693 + for (const { data, packed } of cases) { 449 694 const encoded = p.encode(Message, data); 450 - const decoded = p.decode(Message, encoded); 695 + if (packed !== null) { 696 + assertEquals(encoded, packed); 697 + } 451 698 699 + const decoded = p.decode(Message, encoded); 452 700 assertEquals(decoded, data); 453 701 } 454 702 } ··· 467 715 withoutDefault: 4, 468 716 }); 469 717 470 - { 471 - const full = { 472 - required: 'hello', 473 - withDefault: 'custom', 474 - withFunctionDefault: 99, 475 - withoutDefault: 'present', 476 - }; 477 - 478 - const encoded = p.encode(Message, full); 479 - const decoded = p.decode(Message, encoded); 480 - 481 - assertEquals(decoded, full); 482 - } 483 - { 484 - const minimal = { required: 'hello' }; 485 - 486 - const encoded = p.encode(Message, minimal); 487 - const decoded = p.decode(Message, encoded); 488 - 489 - assertEquals(decoded, { 490 - required: 'hello', 491 - withDefault: 'default_value', 492 - withFunctionDefault: 42, 493 - // withoutDefault should be undefined (not present) 494 - }); 495 - } 718 + const cases = [ 719 + { 720 + data: { 721 + required: 'hello', 722 + withDefault: 'custom', 723 + withFunctionDefault: 99, 724 + withoutDefault: 'present', 725 + }, 726 + expected: Uint8Array.from([ 727 + 0x0a, 728 + 0x05, 729 + 0x68, 730 + 0x65, 731 + 0x6c, 732 + 0x6c, 733 + 0x6f, // field 1: "hello" 734 + 0x12, 735 + 0x06, 736 + 0x63, 737 + 0x75, 738 + 0x73, 739 + 0x74, 740 + 0x6f, 741 + 0x6d, // field 2: "custom" 742 + 0x18, 743 + 0x63, // field 3: varint 99 744 + 0x22, 745 + 0x07, 746 + 0x70, 747 + 0x72, 748 + 0x65, 749 + 0x73, 750 + 0x65, 751 + 0x6e, 752 + 0x74, // field 4: "present" 753 + ]), 754 + }, 755 + { 756 + data: { required: 'hello' }, 757 + expected: Uint8Array.from([0x0a, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]), // field 1: "hello" only 758 + }, 759 + { 760 + data: { 761 + required: 'hello', 762 + withDefault: 'custom_value', 763 + }, 764 + expected: Uint8Array.from([ 765 + 0x0a, 766 + 0x05, 767 + 0x68, 768 + 0x65, 769 + 0x6c, 770 + 0x6c, 771 + 0x6f, // field 1: "hello" 772 + 0x12, 773 + 0x0c, 774 + 0x63, 775 + 0x75, 776 + 0x73, 777 + 0x74, 778 + 0x6f, 779 + 0x6d, 780 + 0x5f, 781 + 0x76, 782 + 0x61, 783 + 0x6c, 784 + 0x75, 785 + 0x65, // field 2: "custom_value" 786 + ]), 787 + }, 788 + ]; 496 789 497 - { 498 - const partial = { 499 - required: 'hello', 500 - withDefault: 'custom_value', 501 - }; 790 + for (const { data, expected } of cases) { 791 + const encoded = p.encode(Message, data); 792 + assertEquals(encoded, expected); 502 793 503 - const encoded = p.encode(Message, partial); 504 794 const decoded = p.decode(Message, encoded); 505 795 506 - assertEquals(decoded, { 507 - required: 'hello', 508 - withDefault: 'custom_value', 509 - withFunctionDefault: 42, 510 - // withoutDefault should be undefined (not present) 511 - }); 796 + // Handle default values in decoded output 797 + if (!data.withDefault && !data.withFunctionDefault && !data.withoutDefault) { 798 + assertEquals(decoded, { 799 + required: data.required, 800 + withDefault: 'default_value', 801 + withFunctionDefault: 42, 802 + // withoutDefault should be undefined (not present) 803 + }); 804 + } else if (!data.withFunctionDefault && !data.withoutDefault) { 805 + assertEquals(decoded, { 806 + required: data.required, 807 + withDefault: data.withDefault, 808 + withFunctionDefault: 42, 809 + // withoutDefault should be undefined (not present) 810 + }); 811 + } else { 812 + assertEquals(decoded, data); 813 + } 512 814 } 513 815 }); 514 816 ··· 518 820 const encoded = p.encode(Message, {}); 519 821 const decoded = p.decode(Message, encoded); 520 822 823 + // Empty message should encode to empty buffer 824 + assertEquals(encoded, new Uint8Array(0)); 521 825 assertEquals(decoded, {}); 522 826 }); 523 827 524 828 Deno.test('nested messages', () => { 525 - const Address = p.message({ 526 - street: p.string(), 527 - city: p.string(), 528 - zipCode: p.optional(p.string()), 529 - }, { 530 - street: 1, 531 - city: 2, 532 - zipCode: 3, 533 - }); 534 - 535 - const Person = p.message({ 536 - name: p.string(), 537 - age: p.int32(), 538 - address: Address, 539 - addresses: p.repeated(Address), 540 - }, { 541 - name: 1, 542 - age: 2, 543 - address: 3, 544 - addresses: 4, 545 - }); 829 + { 830 + const Simple = p.message({ 831 + inner: p.optional(p.message({ value: p.int32() }, { value: 1 })), 832 + }, { inner: 2 }); 546 833 547 - const data = { 548 - name: 'John Doe', 549 - age: 30, 550 - address: { 551 - street: '123 Main St', 552 - city: 'Anytown', 553 - zipCode: '12345', 554 - }, 555 - addresses: [ 834 + const cases = [ 556 835 { 557 - street: '456 Oak Ave', 558 - city: 'Other City', 836 + data: { inner: { value: 42 } }, 837 + expected: Uint8Array.from([0x12, 0x02, 0x08, 0x2a]), // field 2: length 2, field 1: varint 42 559 838 }, 560 839 { 561 - street: '789 Pine Rd', 562 - city: 'Another City', 563 - zipCode: '67890', 840 + data: {}, 841 + expected: new Uint8Array(0), // empty message 564 842 }, 565 - ], 566 - }; 843 + ]; 567 844 568 - const encoded = p.encode(Person, data); 569 - const decoded = p.decode(Person, encoded); 845 + for (const { data, expected } of cases) { 846 + const encoded = p.encode(Simple, data); 847 + assertEquals(encoded, expected); 570 848 571 - assertEquals(decoded, data); 849 + const decoded = p.decode(Simple, encoded); 850 + assertEquals(decoded, data); 851 + } 852 + } 853 + 854 + { 855 + const Address = p.message({ 856 + street: p.string(), 857 + city: p.string(), 858 + zipCode: p.optional(p.string()), 859 + }, { 860 + street: 1, 861 + city: 2, 862 + zipCode: 3, 863 + }); 864 + 865 + const Person = p.message({ 866 + name: p.string(), 867 + age: p.int32(), 868 + address: Address, 869 + addresses: p.repeated(Address), 870 + }, { 871 + name: 1, 872 + age: 2, 873 + address: 3, 874 + addresses: 4, 875 + }); 876 + 877 + const data = { 878 + name: 'John Doe', 879 + age: 30, 880 + address: { 881 + street: '123 Main St', 882 + city: 'Anytown', 883 + zipCode: '12345', 884 + }, 885 + addresses: [ 886 + { 887 + street: '456 Oak Ave', 888 + city: 'Other City', 889 + }, 890 + { 891 + street: '789 Pine Rd', 892 + city: 'Another City', 893 + zipCode: '67890', 894 + }, 895 + ], 896 + }; 897 + 898 + const encoded = p.encode(Person, data); 899 + const decoded = p.decode(Person, encoded); 900 + 901 + assertEquals(decoded, data); 902 + } 572 903 }); 573 904 574 905 Deno.test('self-referential messages', () => { ··· 824 1155 const decoded = p.decode(Message, buffer); 825 1156 826 1157 assertEquals(decoded, { value: 24 }); 827 - }); 828 - 829 - Deno.test('encoding produces correct wire format', () => { 830 - // Test that our encoding matches expected protobuf wire format 831 - const schema = p.message({ 832 - a: p.int32(), 833 - b: p.string(), 834 - }, { 835 - a: 1, 836 - b: 2, 837 - }); 838 - 839 - const encoded = p.encode(schema, { a: 150, b: 'testing' }); 840 - 841 - // Manual verification of wire format: 842 - // Field 1 (a=150): tag=1<<3|0=8, value=150 (varint) = [8, 150, 1] 843 - // Field 2 (b="testing"): tag=2<<3|2=18, length=7, "testing" = [18, 7, 116, 101, 115, 116, 105, 110, 103] 844 - 845 - const expected = Uint8Array.from([ 846 - 8, 847 - 150, 848 - 1, // field 1: int32 value 150 849 - 18, 850 - 7, 851 - 116, 852 - 101, 853 - 115, 854 - 116, 855 - 105, 856 - 110, 857 - 103, // field 2: string "testing" 858 - ]); 859 - 860 - assertEquals(encoded, expected); 861 1158 }); 862 1159 863 1160 // #endregion