prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey

update docs and add bsky profile test for fromJSON

Tyler d5d3143d df46810b

+89 -116
+5
.changeset/cold-bugs-turn.md
··· 1 + --- 2 + "prototypey": patch 3 + --- 4 + 5 + update docs - we're featured!
+1 -1
packages/prototypey/README.md
··· 1 1 # prototypey 2 2 3 - A (soon-to-be) fully-featured sdk for developing lexicons with typescript. 3 + A fully-featured sdk for developing lexicons with typescript. 4 4 5 5 ## Installation 6 6
+83 -115
packages/prototypey/core/tests/from-json-infer.test.ts
··· 1 1 import { test } from "vitest"; 2 2 import { attest } from "@ark/attest"; 3 3 import { fromJSON } from "../lib.ts"; 4 + import { Infer } from "../infer.ts"; 4 5 5 6 test("fromJSON InferNS produces expected type shape", () => { 6 7 const exampleLexicon = fromJSON({ ··· 503 504 // REF TYPE TESTS 504 505 // ============================================================================ 505 506 506 - test("fromJSON InferRef handles basic reference", () => { 507 + test("fromJSON InferRef handles external reference (unknown)", () => { 507 508 const lexicon = fromJSON({ 508 509 id: "test.ref", 509 510 defs: { ··· 524 525 }`); 525 526 }); 526 527 527 - test("fromJSON InferRef handles required reference", () => { 528 - const lexicon = fromJSON({ 529 - id: "test.refRequired", 530 - defs: { 531 - main: { 532 - type: "object", 533 - properties: { 534 - author: { type: "ref", ref: "com.example.user", required: true }, 535 - }, 536 - required: ["author"], 537 - }, 538 - }, 539 - }); 540 - 541 - attest(lexicon["~infer"]).type.toString.snap(`{ 542 - $type: "test.refRequired" 543 - author: { 544 - [x: string]: unknown 545 - $type: "com.example.user" 546 - } 547 - }`); 548 - }); 549 - 550 - test("fromJSON InferRef handles nullable reference", () => { 551 - const lexicon = fromJSON({ 552 - id: "test.refNullable", 553 - defs: { 554 - main: { 555 - type: "object", 556 - properties: { 557 - parent: { type: "ref", ref: "com.example.node", nullable: true }, 558 - }, 559 - nullable: ["parent"], 560 - }, 561 - }, 562 - }); 563 - 564 - attest(lexicon["~infer"]).type.toString.snap(`{ 565 - $type: "test.refNullable" 566 - parent?: 567 - | { [x: string]: unknown; $type: "com.example.node" } 568 - | null 569 - | undefined 570 - }`); 571 - }); 572 - 573 528 // ============================================================================ 574 529 // UNION TYPE TESTS 575 530 // ============================================================================ 576 531 577 - test("fromJSON InferUnion handles basic union", () => { 532 + test("fromJSON InferUnion handles external union (unknown)", () => { 578 533 const lexicon = fromJSON({ 579 534 id: "test.union", 580 535 defs: { ··· 599 554 }`); 600 555 }); 601 556 602 - test("fromJSON InferUnion handles required union", () => { 603 - const lexicon = fromJSON({ 604 - id: "test.unionRequired", 605 - defs: { 606 - main: { 607 - type: "object", 608 - properties: { 609 - media: { 610 - type: "union", 611 - refs: ["com.example.video", "com.example.audio"], 612 - required: true, 613 - }, 614 - }, 615 - required: ["media"], 616 - }, 617 - }, 618 - }); 619 - 620 - attest(lexicon["~infer"]).type.toString.snap(`{ 621 - $type: "test.unionRequired" 622 - media: 623 - | { [x: string]: unknown; $type: "com.example.video" } 624 - | { [x: string]: unknown; $type: "com.example.audio" } 625 - }`); 626 - }); 627 - 628 - test("fromJSON InferUnion handles union with many types", () => { 629 - const lexicon = fromJSON({ 630 - id: "test.unionMultiple", 631 - defs: { 632 - main: { 633 - type: "object", 634 - properties: { 635 - attachment: { 636 - type: "union", 637 - refs: [ 638 - "com.example.image", 639 - "com.example.video", 640 - "com.example.audio", 641 - "com.example.document", 642 - ], 643 - }, 644 - }, 645 - }, 646 - }, 647 - }); 648 - 649 - attest(lexicon["~infer"]).type.toString.snap(`{ 650 - $type: "test.unionMultiple" 651 - attachment?: 652 - | { [x: string]: unknown; $type: "com.example.image" } 653 - | { [x: string]: unknown; $type: "com.example.video" } 654 - | { [x: string]: unknown; $type: "com.example.audio" } 655 - | { 656 - [x: string]: unknown 657 - $type: "com.example.document" 658 - } 659 - | undefined 660 - }`); 661 - }); 662 - 663 557 // ============================================================================ 664 558 // PARAMS TYPE TESTS 665 559 // ============================================================================ ··· 869 763 const lexicon = fromJSON({ 870 764 id: "test.arrayOfRefs", 871 765 defs: { 766 + user: { 767 + type: "object", 768 + properties: { 769 + name: { type: "string", required: true }, 770 + handle: { type: "string", required: true }, 771 + }, 772 + required: ["name", "handle"], 773 + }, 872 774 main: { 873 775 type: "object", 874 776 properties: { 875 777 followers: { 876 778 type: "array", 877 - items: { type: "ref", ref: "com.example.user" }, 779 + items: { type: "ref", ref: "#user" }, 878 780 }, 879 781 }, 880 782 }, ··· 884 786 attest(lexicon["~infer"]).type.toString.snap(`{ 885 787 $type: "test.arrayOfRefs" 886 788 followers?: 887 - | { [x: string]: unknown; $type: "com.example.user" }[] 789 + | { handle: string; name: string; $type: "#user" }[] 888 790 | undefined 889 791 }`); 890 792 }); ··· 897 799 const lexicon = fromJSON({ 898 800 id: "test.complex", 899 801 defs: { 802 + text: { 803 + type: "object", 804 + properties: { 805 + content: { type: "string", required: true }, 806 + }, 807 + required: ["content"], 808 + }, 809 + image: { 810 + type: "object", 811 + properties: { 812 + url: { type: "string", required: true }, 813 + alt: { type: "string" }, 814 + }, 815 + required: ["url"], 816 + }, 900 817 main: { 901 818 type: "object", 902 819 properties: { ··· 912 829 }, 913 830 content: { 914 831 type: "union", 915 - refs: ["com.example.text", "com.example.image"], 832 + refs: ["#text", "#image"], 916 833 }, 917 834 tags: { type: "array", items: { type: "string" }, maxLength: 10 }, 918 835 metadata: { ··· 933 850 $type: "test.complex" 934 851 tags?: string[] | undefined 935 852 content?: 936 - | { [x: string]: unknown; $type: "com.example.text" } 937 - | { [x: string]: unknown; $type: "com.example.image" } 853 + | { content: string; $type: "#text" } 854 + | { 855 + alt?: string | undefined 856 + url: string 857 + $type: "#image" 858 + } 938 859 | undefined 939 860 author?: 940 861 | { ··· 982 903 type: "object", 983 904 properties: { 984 905 text: { type: "string", required: true }, 985 - author: { type: "ref", ref: "com.example.user" }, 906 + author: { type: "ref", ref: "#user" }, 986 907 }, 987 908 required: ["text"], 988 909 }, ··· 1256 1177 author: "[Reference not found: #user]" 1257 1178 }`); 1258 1179 }); 1180 + 1181 + // ============================================================================ 1182 + // REAL-WORLD EXAMPLE: BLUESKY PROFILE 1183 + // ============================================================================ 1184 + 1185 + test("fromJSON Real-world example: app.bsky.actor.profile", () => { 1186 + const lexicon = fromJSON({ 1187 + id: "app.bsky.actor.profile", 1188 + defs: { 1189 + main: { 1190 + type: "record", 1191 + key: "self", 1192 + record: { 1193 + type: "object", 1194 + properties: { 1195 + displayName: { 1196 + type: "string", 1197 + maxLength: 64, 1198 + maxGraphemes: 64, 1199 + }, 1200 + description: { 1201 + type: "string", 1202 + maxLength: 256, 1203 + maxGraphemes: 256, 1204 + }, 1205 + }, 1206 + }, 1207 + }, 1208 + }, 1209 + }); 1210 + 1211 + type Profile = Infer<typeof lexicon>; 1212 + 1213 + const george: Profile = { 1214 + $type: "app.bsky.actor.profile", 1215 + description: "George", 1216 + }; 1217 + 1218 + lexicon.validate({ foo: "bar" }); // will fail 1219 + lexicon.validate(george); // will pass 🎉 1220 + 1221 + attest(lexicon["~infer"]).type.toString.snap(`{ 1222 + $type: "app.bsky.actor.profile" 1223 + displayName?: string | undefined 1224 + description?: string | undefined 1225 + }`); 1226 + });