An experimental TypeSpec syntax for Lexicon

fix validation of @required for op params

+83 -33
+54 -4
packages/emitter/src/emitter.ts
··· 707 707 const required: string[] = []; 708 708 709 709 for (const [paramName, param] of operation.parameters.properties) { 710 + // Check for conflicting @required on optional property 711 + if (param.optional && isRequired(this.program, param)) { 712 + this.program.reportDiagnostic({ 713 + code: "required-on-optional", 714 + message: 715 + `Parameter "${paramName}" has conflicting markers: @required decorator with optional "?". ` + 716 + `Either remove @required to make it optional (preferred), or remove the "?".`, 717 + target: param, 718 + severity: "error", 719 + }); 720 + } 721 + 722 + if (!param.optional) { 723 + if (!isRequired(this.program, param)) { 724 + this.program.reportDiagnostic({ 725 + code: "parameter-missing-required", 726 + message: 727 + `Required parameter "${paramName}" must be explicitly marked with @required decorator. ` + 728 + `In atproto, required fields are discouraged and must be intentional. ` + 729 + `Either add @required to the parameter or make it optional with "?".`, 730 + target: param, 731 + severity: "error", 732 + }); 733 + } 734 + required.push(paramName); 735 + } 736 + 710 737 const paramDef = this.typeToLexiconDefinition(param.type, param); 711 738 if (paramDef && this.isXrpcParameterProperty(paramDef)) { 712 739 properties[paramName] = paramDef; 713 - if (!param.optional) required.push(paramName); 714 740 } 715 741 } 716 742 ··· 846 872 const required: string[] = []; 847 873 848 874 for (const [propName, prop] of parametersModel.properties) { 875 + // Check for conflicting @required on optional property 876 + if (prop.optional && isRequired(this.program, prop)) { 877 + this.program.reportDiagnostic({ 878 + code: "required-on-optional", 879 + message: 880 + `Parameter "${propName}" has conflicting markers: @required decorator with optional "?". ` + 881 + `Either remove @required to make it optional (preferred), or remove the "?".`, 882 + target: prop, 883 + severity: "error", 884 + }); 885 + } 886 + 887 + if (!prop.optional) { 888 + if (!isRequired(this.program, prop)) { 889 + this.program.reportDiagnostic({ 890 + code: "parameter-missing-required", 891 + message: 892 + `Required parameter "${propName}" must be explicitly marked with @required decorator. ` + 893 + `In atproto, required fields are discouraged and must be intentional. ` + 894 + `Either add @required to the parameter or make it optional with "?".`, 895 + target: prop, 896 + severity: "error", 897 + }); 898 + } 899 + required.push(propName); 900 + } 901 + 849 902 const propDef = this.typeToLexiconDefinition(prop.type, prop); 850 903 if (propDef && this.isXrpcParameterProperty(propDef)) { 851 904 properties[propName] = propDef; 852 - if (!prop.optional) { 853 - required.push(propName); 854 - } 855 905 } 856 906 } 857 907
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/actor/getProfile.tsp
··· 5 5 @query 6 6 op main( 7 7 /** Handle or DID of account to fetch profile of. */ 8 - actor: atIdentifier 8 + @required actor: atIdentifier 9 9 ): app.bsky.actor.defs.ProfileViewDetailed; 10 10 }
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/actor/getProfiles.tsp
··· 5 5 @query 6 6 op main( 7 7 @maxItems(25) 8 - actors: atIdentifier[] 8 + @required actors: atIdentifier[] 9 9 ): { 10 10 @required profiles: app.bsky.actor.defs.ProfileViewDetailed[]; 11 11 };
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getActorFeeds.tsp
··· 4 4 /** Get a list of feeds (feed generator records) created by the actor (in the actor's repo). */ 5 5 @query 6 6 op main( 7 - actor: atIdentifier, 7 + @required actor: atIdentifier, 8 8 9 9 @minValue(1) 10 10 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getActorLikes.tsp
··· 8 8 @query 9 9 @errors(BlockedActor, BlockedByActor) 10 10 op main( 11 - actor: atIdentifier, 11 + @required actor: atIdentifier, 12 12 13 13 @minValue(1) 14 14 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getAuthorFeed.tsp
··· 8 8 @query 9 9 @errors(BlockedActor, BlockedByActor) 10 10 op main( 11 - actor: atIdentifier, 11 + @required actor: atIdentifier, 12 12 13 13 @minValue(1) 14 14 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeed.tsp
··· 7 7 @query 8 8 @errors(UnknownFeed) 9 9 op main( 10 - feed: atUri, 10 + @required feed: atUri, 11 11 12 12 @minValue(1) 13 13 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedGenerator.tsp
··· 5 5 @query 6 6 op main( 7 7 /** AT-URI of the feed generator record. */ 8 - feed: atUri 8 + @required feed: atUri 9 9 ): { 10 10 @required view: app.bsky.feed.defs.GeneratorView; 11 11
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedGenerators.tsp
··· 4 4 /** Get information about a list of feed generators. */ 5 5 @query 6 6 op main( 7 - feeds: atUri[] 7 + @required feeds: atUri[] 8 8 ): { 9 9 @required feeds: app.bsky.feed.defs.GeneratorView[]; 10 10 };
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedSkeleton.tsp
··· 8 8 @errors(UnknownFeed) 9 9 op main( 10 10 /** Reference to feed generator record describing the specific feed being requested. */ 11 - feed: atUri, 11 + @required feed: atUri, 12 12 13 13 @minValue(1) 14 14 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getLikes.tsp
··· 5 5 @query 6 6 op main( 7 7 /** AT-URI of the subject (eg, a post record). */ 8 - uri: atUri, 8 + @required uri: atUri, 9 9 10 10 /** CID of the subject record (aka, specific version of record), to filter likes. */ 11 11 cid?: cid,
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getListFeed.tsp
··· 8 8 @errors(UnknownList) 9 9 op main( 10 10 /** Reference (AT-URI) to the list record. */ 11 - list: atUri, 11 + @required list: atUri, 12 12 13 13 @minValue(1) 14 14 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getPostThread.tsp
··· 8 8 @errors(NotFound) 9 9 op main( 10 10 /** Reference (AT-URI) to post record. */ 11 - uri: atUri, 11 + @required uri: atUri, 12 12 13 13 /** How many levels of reply depth should be included in response. */ 14 14 @minValue(0)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getPosts.tsp
··· 6 6 op main( 7 7 /** List of post AT-URIs to return hydrated views for. */ 8 8 @maxItems(25) 9 - uris: atUri[] 9 + @required uris: atUri[] 10 10 ): { 11 11 @required posts: app.bsky.feed.defs.PostView[]; 12 12 };
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getQuotes.tsp
··· 5 5 @query 6 6 op main( 7 7 /** Reference (AT-URI) of post record */ 8 - uri: atUri, 8 + @required uri: atUri, 9 9 10 10 /** If supplied, filters to quotes of specific version (by CID) of the post record. */ 11 11 cid?: cid,
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/getRepostedBy.tsp
··· 5 5 @query 6 6 op main( 7 7 /** Reference (AT-URI) of post record */ 8 - uri: atUri, 8 + @required uri: atUri, 9 9 10 10 /** If supplied, filters to reposts of specific version (by CID) of the post record. */ 11 11 cid?: cid,
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/feed/searchPosts.tsp
··· 12 12 @errors(BadQueryString) 13 13 op main( 14 14 /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */ 15 - q: string, 15 + @required q: string, 16 16 17 17 /** Specifies the ranking order of results. */ 18 18 sort?: "top" | "latest" | string = "latest",
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/graph/getFollowers.tsp
··· 4 4 /** Enumerates accounts which follow a specified account (actor). */ 5 5 @query 6 6 op main( 7 - actor: atIdentifier, 7 + @required actor: atIdentifier, 8 8 9 9 @minValue(1) 10 10 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/graph/getFollows.tsp
··· 4 4 /** Enumerates accounts which a specified account (actor) follows. */ 5 5 @query 6 6 op main( 7 - actor: atIdentifier, 7 + @required actor: atIdentifier, 8 8 9 9 @minValue(1) 10 10 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/graph/getKnownFollowers.tsp
··· 4 4 /** Enumerates accounts which follow a specified account (actor) and are followed by the viewer. */ 5 5 @query 6 6 op main( 7 - actor: atIdentifier, 7 + @required actor: atIdentifier, 8 8 9 9 @minValue(1) 10 10 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/graph/getList.tsp
··· 5 5 @query 6 6 op main( 7 7 /** Reference (AT-URI) of the list record to hydrate. */ 8 - list: atUri, 8 + @required list: atUri, 9 9 10 10 @minValue(1) 11 11 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/graph/getLists.tsp
··· 5 5 @query 6 6 op main( 7 7 /** The account (actor) to enumerate lists from. */ 8 - actor: atIdentifier, 8 + @required actor: atIdentifier, 9 9 10 10 @minValue(1) 11 11 @maxValue(100)
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/graph/getSuggestedFollowsByActor.tsp
··· 4 4 /** Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account. */ 5 5 @query 6 6 op main( 7 - actor: atIdentifier 7 + @required actor: atIdentifier 8 8 ): { 9 9 @required 10 10 suggestions: app.bsky.actor.defs.ProfileView[];
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchActorsSkeleton.tsp
··· 8 8 @errors(BadQueryString) 9 9 op main( 10 10 /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax. */ 11 - q: string, 11 + @required q: string, 12 12 13 13 /** DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking. */ 14 14 viewer?: did,
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchPostsSkeleton.tsp
··· 12 12 @errors(BadQueryString) 13 13 op main( 14 14 /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */ 15 - q: string, 15 + @required q: string, 16 16 17 17 /** Specifies the ranking order of results. */ 18 18 sort?: "top" | "latest" | string = "latest",
+1 -1
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchStarterPacksSkeleton.tsp
··· 8 8 @errors(BadQueryString) 9 9 op main( 10 10 /** Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. */ 11 - q: string, 11 + @required q: string, 12 12 13 13 /** DID of the account making the request (not included for public/unauthenticated queries). */ 14 14 viewer?: did,
+1 -1
packages/emitter/test/integration/atproto/input/com/atproto/temp/checkHandleAvailability.tsp
··· 27 27 @errors(InvalidEmail) 28 28 op main( 29 29 /** Tentative handle. Will be checked for availability or used to build handle suggestions. */ 30 - handle: handle, 30 + @required handle: handle, 31 31 32 32 /** User-provided email. Might be used to build handle suggestions. */ 33 33 email?: string,
+1 -1
packages/emitter/test/integration/atproto/input/com/atproto/temp/dereferenceScope.tsp
··· 9 9 @errors(InvalidScopeReference) 10 10 op main( 11 11 /** The scope reference (starts with 'ref:') */ 12 - scope: string 12 + @required scope: string 13 13 ): { 14 14 /** The full oauth permission scope */ 15 15 @required
+1 -1
packages/emitter/test/integration/lexicon-examples/input/com/atproto/temp/checkHandleAvailability.tsp
··· 27 27 @errors(InvalidEmail) 28 28 op main( 29 29 /** Tentative handle. Will be checked for availability or used to build handle suggestions. */ 30 - handle: handle, 30 + @required handle: handle, 31 31 32 32 /** User-provided email. Might be used to build handle suggestions. */ 33 33 email?: string,
+1 -1
packages/emitter/test/integration/lexicon-examples/input/com/atproto/temp/dereferenceScope.tsp
··· 9 9 @errors(InvalidScopeReference) 10 10 op main( 11 11 /** The scope reference (starts with 'ref:') */ 12 - scope: string 12 + @required scope: string 13 13 ): { 14 14 /** The full oauth permission scope */ 15 15 @required