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

refactor: always break on first error

mary.my.id 6bfcc3bc 0b62fa37

verified
Changed files
+43 -145
lib
+4 -21
lib/mod.test.ts
··· 941 941 const result = p.tryDecode(Message, invalidData); 942 942 assert(!result.ok); 943 943 944 - assertEquals(result.issues.length, 3); // field1, field2, and field3 have wrong wire types 945 - 946 - const codes = result.issues.map((issue) => issue.code); 947 - assert(codes.every((code) => code === 'invalid_wire')); 948 - 949 - const fields = result.issues.map((issue) => issue.path[0]); 950 - assertArrayIncludes(fields, ['field1', 'field2', 'field3']); 951 - 952 - assertStringIncludes(result.message, '+2 other issue(s)'); 944 + assertEquals(result.message, 'invalid_wire at .field1 (expected wire type 2)'); 953 945 }); 954 946 955 947 Deno.test('missing required fields during decoding', () => { 956 948 const Message = p.message({ 957 949 required1: p.string(), 958 - required2: p.int32(), 959 950 optional: p.optional(p.string()), 960 951 }, { 961 952 required1: 1, ··· 974 965 975 966 const result = p.tryDecode(Message, encoded); 976 967 assert(!result.ok); 977 - 978 - const issues = result.issues; 979 - assertEquals(issues.length, 2); 980 - 981 - const missingCodes = issues.map((issue) => issue.code); 982 - assertArrayIncludes(missingCodes, ['missing_value', 'missing_value']); 983 - 984 - const missingKeys = issues.map((issue) => issue.path.join('.')); 985 - assertArrayIncludes(missingKeys, ['required1', 'required2']); 968 + assertEquals(result.message, 'missing_value at .required1 (required field is missing)'); 986 969 }); 987 970 988 971 Deno.test('empty buffer handling', () => { ··· 1033 1016 const result = p.tryDecode(Message, truncatedBuffer); 1034 1017 1035 1018 assert(!result.ok); 1036 - assertEquals(result.message, `unexpected_eof at .text (unexpected end of input) (+1 other issue(s))`); 1019 + assertEquals(result.message, `unexpected_eof at .text (unexpected end of input)`); 1037 1020 }); 1038 1021 1039 1022 Deno.test('buffer underrun during varint reading', () => { ··· 1048 1031 const result = p.tryDecode(Message, incompleteVarint); 1049 1032 1050 1033 assert(!result.ok); 1051 - assertEquals(result.message, `unexpected_eof at .value (unexpected end of input) (+1 other issue(s))`); 1034 + assertEquals(result.message, `unexpected_eof at .value (unexpected end of input)`); 1052 1035 }); 1053 1036 1054 1037 // #endregion
+39 -124
lib/mod.ts
··· 39 39 40 40 type IssueTree = 41 41 | IssueLeaf 42 - | { ok: false; code: 'prepend'; key: Key; tree: IssueTree } 43 - | { ok: false; code: 'join'; left: IssueTree; right: IssueTree }; 42 + | { ok: false; code: 'prepend'; key: Key; tree: IssueTree }; 44 43 45 44 export type Issue = Readonly< 46 45 | { code: 'unexpected_eof'; path: Key[] } ··· 68 67 const UINT64_RANGE_ISSUE: IssueLeaf = { ok: false, code: 'invalid_range', type: 'uint64' }; 69 68 70 69 // #__NO_SIDE_EFFECTS__ 71 - const joinIssues = (left: IssueTree | undefined, right: IssueTree): IssueTree => { 72 - return left ? { ok: false, code: 'join', left, right } : right; 73 - }; 74 - 75 - // #__NO_SIDE_EFFECTS__ 76 70 const prependPath = (key: Key, tree: IssueTree): IssueTree => { 77 71 return { ok: false, code: 'prepend', key, tree }; 78 72 }; ··· 88 82 const collectIssues = (tree: IssueTree, path: Key[] = [], issues: Issue[] = []): Issue[] => { 89 83 for (;;) { 90 84 switch (tree.code) { 91 - case 'join': { 92 - collectIssues(tree.left, path.slice(), issues); 93 - tree = tree.right; 94 - continue; 95 - } 96 85 case 'prepend': { 97 86 path.push(tree.key); 98 87 tree = tree.tree; ··· 106 95 } 107 96 }; 108 97 109 - const countIssues = (tree: IssueTree): number => { 110 - let count = 0; 111 - for (;;) { 112 - switch (tree.code) { 113 - case 'join': { 114 - count += countIssues(tree.left); 115 - tree = tree.right; 116 - continue; 117 - } 118 - case 'prepend': { 119 - tree = tree.tree; 120 - continue; 121 - } 122 - default: { 123 - return count + 1; 124 - } 125 - } 126 - } 127 - }; 128 - 129 98 const formatIssueTree = (tree: IssueTree): string => { 130 99 let path = ''; 131 - let count = 0; 132 100 for (;;) { 133 101 switch (tree.code) { 134 - case 'join': { 135 - count += countIssues(tree.right); 136 - tree = tree.left; 137 - continue; 138 - } 139 102 case 'prepend': { 140 103 path += `.${tree.key}`; 141 104 tree = tree.tree; ··· 168 131 break; 169 132 } 170 133 171 - let msg = `${tree.code} at ${path || '.'} (${message})`; 172 - if (count > 0) { 173 - msg += ` (+${count} other issue(s))`; 174 - } 175 - 176 - return msg; 134 + return `${tree.code} at ${path || '.'} (${message})`; 177 135 }; 178 136 179 137 // #endregion ··· 266 224 ): Result<InferOutput<TSchema>> => { 267 225 const state = createDecoderState(buffer); 268 226 269 - const result = schema['~~decode'](state, FLAG_EMPTY); 227 + const result = schema['~~decode'](state); 270 228 271 229 if (result.ok) { 272 230 return result as Ok<InferOutput<TSchema>>; ··· 310 268 ): InferOutput<TSchema> => { 311 269 const state = createDecoderState(buffer); 312 270 313 - const result = schema['~~decode'](state, FLAG_EMPTY); 271 + const result = schema['~~decode'](state); 314 272 315 273 if (result.ok) { 316 274 return result.value as InferOutput<TSchema>; ··· 541 499 declare const kObjectType: unique symbol; 542 500 type kObjectType = typeof kObjectType; 543 501 544 - // None set 545 - export const FLAG_EMPTY = 0; 546 - // Don't continue validation if an error is encountered 547 - export const FLAG_ABORT_EARLY = 1 << 0; 548 - 549 502 type RawResult<T = unknown> = Ok<T> | IssueTree; 550 503 551 - type Decoder = (this: void, state: DecoderState, flags: number) => RawResult; 504 + type Decoder = (this: void, state: DecoderState) => RawResult; 552 505 553 506 type Encoder = (this: void, state: EncoderState, input: unknown) => IssueTree | void; 554 507 ··· 580 533 kind: 'schema', 581 534 type: 'string', 582 535 wire: 2, 583 - '~decode'(state, _flags) { 536 + '~decode'(state) { 584 537 const length = readVarint(state); 585 538 if (!length.ok) { 586 539 return length; ··· 642 595 kind: 'schema', 643 596 type: 'bytes', 644 597 wire: 2, 645 - '~decode'(state, _flags) { 598 + '~decode'(state) { 646 599 const length = readVarint(state); 647 600 if (!length.ok) { 648 601 return length; ··· 681 634 kind: 'schema', 682 635 type: 'boolean', 683 636 wire: 0, 684 - '~decode'(state, _flags) { 637 + '~decode'(state) { 685 638 const result = readVarint(state); 686 639 if (!result.ok) { 687 640 return result; ··· 718 671 kind: 'schema', 719 672 type: 'double', 720 673 wire: 1, 721 - '~decode'(state, _flags) { 674 + '~decode'(state) { 722 675 const view = getDataView(state); 723 676 const value = view.getFloat64(state.p, true); 724 677 ··· 759 712 kind: 'schema', 760 713 type: 'float', 761 714 wire: 5, 762 - '~decode'(state, _flags) { 715 + '~decode'(state) { 763 716 const view = getDataView(state); 764 717 const value = view.getFloat32(state.p, true); 765 718 ··· 807 760 kind: 'schema', 808 761 type: 'int32', 809 762 wire: 0, 810 - '~decode'(state, _flags) { 763 + '~decode'(state) { 811 764 const result = readVarint(state); 812 765 if (!result.ok) { 813 766 return result; ··· 852 805 kind: 'schema', 853 806 type: 'int64', 854 807 wire: 0, 855 - '~decode'(state, _flags) { 808 + '~decode'(state) { 856 809 const buf = state.b; 857 810 let pos = state.p; 858 811 ··· 914 867 kind: 'schema', 915 868 type: 'uint32', 916 869 wire: 0, 917 - '~decode'(state, _flags) { 870 + '~decode'(state) { 918 871 const result = readVarint(state); 919 872 if (!result.ok) { 920 873 return result; ··· 957 910 kind: 'schema', 958 911 type: 'uint64', 959 912 wire: 0, 960 - '~decode'(state, _flags) { 913 + '~decode'(state) { 961 914 const buf = state.b; 962 915 let pos = state.p; 963 916 ··· 1007 960 kind: 'schema', 1008 961 type: 'sint32', 1009 962 wire: 0, 1010 - '~decode'(state, _flags) { 963 + '~decode'(state) { 1011 964 const result = readVarint(state); 1012 965 if (!result.ok) { 1013 966 return result; ··· 1050 1003 kind: 'schema', 1051 1004 type: 'sint64', 1052 1005 wire: 0, 1053 - '~decode'(state, _flags) { 1006 + '~decode'(state) { 1054 1007 const buf = state.b; 1055 1008 let pos = state.p; 1056 1009 ··· 1101 1054 kind: 'schema', 1102 1055 type: 'fixed32', 1103 1056 wire: 5, 1104 - '~decode'(state, _flags) { 1057 + '~decode'(state) { 1105 1058 const view = getDataView(state); 1106 1059 const value = view.getUint32(state.p, true); 1107 1060 ··· 1145 1098 kind: 'schema', 1146 1099 type: 'fixed64', 1147 1100 wire: 1, 1148 - '~decode'(state, _flags) { 1101 + '~decode'(state) { 1149 1102 const view = getDataView(state); 1150 1103 1151 1104 // Read as two 32-bit values and combine into a BigInt ··· 1196 1149 kind: 'schema', 1197 1150 type: 'sfixed32', 1198 1151 wire: 5, 1199 - '~decode'(state, _flags) { 1152 + '~decode'(state) { 1200 1153 const view = getDataView(state); 1201 1154 const value = view.getInt32(state.p, true); 1202 1155 ··· 1240 1193 kind: 'schema', 1241 1194 type: 'sfixed64', 1242 1195 wire: 1, 1243 - '~decode'(state, _flags) { 1196 + '~decode'(state) { 1244 1197 const view = getDataView(state); 1245 1198 1246 1199 // Read as two 32-bit values and combine into a BigInt ··· 1312 1265 get '~decode'() { 1313 1266 const shape = resolvedShape.value; 1314 1267 1315 - const decoder: Decoder = (state, flags) => { 1268 + const decoder: Decoder = (state) => { 1316 1269 const length = readVarint(state); 1317 1270 if (!length.ok) { 1318 1271 return length; ··· 1335 1288 let issues: IssueTree | undefined; 1336 1289 1337 1290 while (children.p < length.value) { 1338 - const r = shape['~decode'](children, flags); 1291 + const r = shape['~decode'](children); 1339 1292 1340 - if (r.ok) { 1341 - array.push(r.value); 1342 - } else { 1343 - issues = joinIssues(issues, prependPath(idx, r)); 1344 - 1345 - if (flags & FLAG_ABORT_EARLY) { 1346 - return issues; 1347 - } 1293 + if (!r.ok) { 1294 + return prependPath(idx, r); 1348 1295 } 1349 1296 1297 + array.push(r.value); 1350 1298 idx++; 1351 1299 } 1352 1300 ··· 1435 1383 wrapped: wrapped, 1436 1384 default: defaultValue, 1437 1385 wire: wrapped.wire, 1438 - '~decode'(state, flags) { 1439 - return wrapped['~decode'](state, flags); 1386 + '~decode'(state) { 1387 + return wrapped['~decode'](state); 1440 1388 }, 1441 1389 '~encode'(state, input) { 1442 1390 return wrapped['~encode'](state, input); ··· 1583 1531 const shape = resolvedEntries.value; 1584 1532 const len = Object.keys(shape).length; 1585 1533 1586 - const decoder: Decoder = (state, flags) => { 1534 + const decoder: Decoder = (state) => { 1587 1535 let seenBits: BitSet = 0; 1588 1536 let seenCount = 0; 1589 1537 ··· 1594 1542 while (state.p < end) { 1595 1543 const prelude = readVarint(state); 1596 1544 if (!prelude.ok) { 1597 - issues = joinIssues(issues, prelude); 1598 - if (flags & FLAG_ABORT_EARLY) { 1599 - return issues; 1600 - } 1601 - 1602 - break; 1545 + return prelude; 1603 1546 } 1604 1547 1605 1548 const magic = prelude.value; ··· 1612 1555 if (!entry) { 1613 1556 const result = skipField(state, wire); 1614 1557 if (!result.ok) { 1615 - issues = joinIssues(issues, result); 1616 - if (flags & FLAG_ABORT_EARLY) { 1617 - return issues; 1618 - } 1619 - 1620 - break; 1558 + return result; 1621 1559 } 1560 + 1622 1561 continue; 1623 1562 } 1624 1563 ··· 1630 1569 1631 1570 // It doesn't match with our wire, file an issue 1632 1571 if (entry.wire !== wire) { 1633 - issues = joinIssues(issues, entry.wireIssue); 1634 - if (flags & FLAG_ABORT_EARLY) { 1635 - return issues; 1636 - } 1637 - 1638 - const skip = skipField(state, wire); 1639 - if (!skip.ok) { 1640 - issues = joinIssues(issues, prependPath(entry.key, skip)); 1641 - if (flags & FLAG_ABORT_EARLY) { 1642 - return issues; 1643 - } 1644 - 1645 - break; 1646 - } 1647 - 1648 - continue; 1572 + return entry.wireIssue; 1649 1573 } 1650 1574 1651 1575 // Decode the value 1652 - const result = entry.schema['~decode'](state, flags); 1576 + const result = entry.schema['~decode'](state); 1653 1577 1654 1578 // Failed to decode, file an issue 1655 1579 if (!result.ok) { 1656 - issues = joinIssues(issues, prependPath(entry.key, result)); 1657 - if (flags & FLAG_ABORT_EARLY) { 1658 - return issues; 1659 - } 1660 - 1661 - continue; 1580 + return prependPath(entry.key, result); 1662 1581 } 1663 1582 1664 1583 /*#__INLINE__*/ set(obj, entry.key, result.value); ··· 1681 1600 /*#__INLINE__*/ set(obj, entry.key, defaultValue); 1682 1601 } 1683 1602 } else { 1684 - issues = joinIssues(issues, entry.missingIssue); 1685 - 1686 - if (flags & FLAG_ABORT_EARLY) { 1687 - return issues; 1688 - } 1603 + return entry.missingIssue; 1689 1604 } 1690 1605 } 1691 1606 } ··· 1703 1618 get '~decode'() { 1704 1619 const raw = this['~~decode']; 1705 1620 1706 - const decoder: Decoder = (state, flags) => { 1621 + const decoder: Decoder = (state) => { 1707 1622 const length = readVarint(state); 1708 1623 if (!length.ok) { 1709 1624 return length; ··· 1720 1635 v: null, 1721 1636 }; 1722 1637 1723 - return raw(child, flags); 1638 + return raw(child); 1724 1639 }; 1725 1640 1726 1641 return lazyProperty(this, '~decode', decoder); ··· 1834 1749 key, 1835 1750 value, 1836 1751 get '~decode'() { 1837 - const decoder: Decoder = (state, flags) => { 1838 - const result = Schema['~decode'](state, flags); 1752 + const decoder: Decoder = (state) => { 1753 + const result = Schema['~decode'](state); 1839 1754 if (!result.ok) { 1840 1755 return result; 1841 1756 }