@@ -53,7 +53,7 @@ import {
5353 verifyUniqueness ,
5454 parseJsDocTags ,
5555 deepEqual ,
56- sourceLocation , sortTypeDefinitions
56+ sourceLocation , sortTypeDefinitions , parseDeprecation
5757} from './utils'
5858
5959const jsonSpec = buildJsonSpec ( )
@@ -210,14 +210,6 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
210210 if ( mapping == null ) {
211211 throw new Error ( `Cannot find url template for ${ namespace } , very likely the specification folder does not follow the rest-api-spec` )
212212 }
213- // list of unique dynamic parameters
214- const urlTemplateParams = [ ...new Set (
215- mapping . urls . flatMap ( url => url . path . split ( '/' )
216- . filter ( part => part . includes ( '{' ) )
217- . map ( part => part . slice ( 1 , - 1 ) )
218- )
219- ) ]
220- const methods = [ ...new Set ( mapping . urls . flatMap ( url => url . methods ) ) ]
221213
222214 let pathMember : Node | null = null
223215 let bodyProperties : model . Property [ ] = [ ]
@@ -226,39 +218,50 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
226218
227219 // collect path/query/body properties
228220 for ( const member of declaration . getMembers ( ) ) {
229- // we are visiting `path_parts, `query_parameters` or `body`
221+ // we are visiting `urls`, ` path_parts, `query_parameters` or `body`
230222 assert (
231223 member ,
232224 Node . isPropertyDeclaration ( member ) || Node . isPropertySignature ( member ) ,
233225 'Class and interfaces can only have property declarations or signatures'
234226 )
235- const property = visitRequestOrResponseProperty ( member )
236- if ( property . name === 'path_parts' ) {
227+ const name = member . getName ( )
228+ if ( name === 'urls' ) {
229+ // Overwrite the endpoint urls read from the json-rest-spec
230+ // TODO: once all spec files are using it, make it mandatory.
231+ mapping . urls = visitUrls ( member )
232+ } else if ( name === 'path_parts' ) {
233+ const property = visitRequestOrResponseProperty ( member )
237234 assert ( member , property . properties . length > 0 , 'There is no need to declare an empty object path_parts, just remove the path_parts declaration.' )
238235 pathMember = member
239236 type . path = property . properties
240- } else if ( property . name === 'query_parameters' ) {
237+ } else if ( name === 'query_parameters' ) {
238+ const property = visitRequestOrResponseProperty ( member )
241239 assert ( member , property . properties . length > 0 , 'There is no need to declare an empty object query_parameters, just remove the query_parameters declaration.' )
242240 type . query = property . properties
243- } else if ( property . name === 'body' ) {
241+ } else if ( name === 'body' ) {
242+ const property = visitRequestOrResponseProperty ( member )
244243 bodyMember = member
245- assert (
246- member ,
247- methods . some ( method => [ 'POST' , 'PUT' , 'DELETE' ] . includes ( method ) ) ,
248- `${ namespace } .${ name } can't have a body, allowed methods: ${ methods . join ( ', ' ) } `
249- )
250244 if ( property . valueOf != null ) {
251245 bodyValue = property . valueOf
252246 } else {
253247 assert ( member , property . properties . length > 0 , 'There is no need to declare an empty object body, just remove the body declaration.' )
254248 bodyProperties = property . properties
255249 }
256250 } else {
257- assert ( member , false , `Unknown request property: ${ property . name } ` )
251+ assert ( member , false , `Unknown request property: ${ name } ` )
258252 }
259253 }
260254
261255 // validate path properties
256+ // list of unique dynamic parameters
257+ const urlTemplateParams = [ ...new Set (
258+ mapping . urls . flatMap ( url => url . path . split ( '/' )
259+ . filter ( part => part . includes ( '{' ) )
260+ . map ( part => part . slice ( 1 , - 1 ) )
261+ )
262+ ) ]
263+ const methods = [ ...new Set ( mapping . urls . flatMap ( url => url . methods ) ) ]
264+
262265 for ( const part of type . path ) {
263266 assert (
264267 pathMember as Node ,
@@ -282,6 +285,13 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
282285 }
283286
284287 // validate body
288+ if ( bodyMember != null ) {
289+ assert (
290+ bodyMember ,
291+ methods . some ( method => [ 'POST' , 'PUT' , 'DELETE' ] . includes ( method ) ) ,
292+ `${ namespace } .${ name } can't have a body, allowed methods: ${ methods . join ( ', ' ) } `
293+ )
294+ }
285295 // the body can either be a value (eg Array<string> or an object with properties)
286296 if ( bodyValue != null ) {
287297 // Propagate required body value nature based on TS question token being present.
@@ -587,3 +597,80 @@ function visitRequestOrResponseProperty (member: PropertyDeclaration | PropertyS
587597
588598 return { name, properties, valueOf }
589599}
600+
601+ /**
602+ * Parse the 'urls' property of a request definition. Format is:
603+ * ```
604+ * urls: [
605+ * {
606+ * /** @deprecated 1.2.3 Use something else
607+ * path: '/some/path',
608+ * methods: ["GET", "POST"]
609+ * }
610+ * ]
611+ * ```
612+ */
613+ function visitUrls ( member : PropertyDeclaration | PropertySignature ) : model . UrlTemplate [ ] {
614+ const value = member . getTypeNode ( )
615+
616+ // Literal arrays are exposed as tuples by ts-morph
617+ assert ( value , Node . isTupleTypeNode ( value ) , '"urls" should be an array' )
618+
619+ const result : model . UrlTemplate [ ] = [ ]
620+
621+ value . forEachChild ( urlNode => {
622+ assert ( urlNode , Node . isTypeLiteral ( urlNode ) , '"urls" members should be objects' )
623+
624+ const urlTemplate : any = { }
625+
626+ urlNode . forEachChild ( node => {
627+ assert ( node , Node . isPropertySignature ( node ) , "Expecting 'path' and 'methods' properties" )
628+
629+ const name = node . getName ( )
630+ const propValue = node . getTypeNode ( )
631+
632+ if ( name === 'path' ) {
633+ assert ( propValue , Node . isLiteralTypeNode ( propValue ) , '"path" should be a string' )
634+
635+ const pathLit = propValue . getLiteral ( )
636+ assert ( pathLit , Node . isStringLiteral ( pathLit ) , '"path" should be a string' )
637+
638+ urlTemplate . path = pathLit . getLiteralValue ( )
639+
640+ // Deprecation
641+ const jsDoc = node . getJsDocs ( )
642+ const tags = parseJsDocTags ( jsDoc )
643+ const deprecation = parseDeprecation ( tags , jsDoc )
644+ if ( deprecation != null ) {
645+ urlTemplate . deprecation = deprecation
646+ }
647+ if ( Object . keys ( tags ) . length > 0 ) {
648+ assert ( jsDoc , false , `Unknown annotations: ${ Object . keys ( tags ) . join ( ', ' ) } ` )
649+ }
650+ } else if ( name === 'methods' ) {
651+ assert ( propValue , Node . isTupleTypeNode ( propValue ) , '"methods" should be an array' )
652+
653+ const methods : string [ ] = [ ]
654+ propValue . forEachChild ( node => {
655+ assert ( node , Node . isLiteralTypeNode ( node ) , '"methods" should contain strings' )
656+
657+ const nodeLit = node . getLiteral ( )
658+ assert ( nodeLit , Node . isStringLiteral ( nodeLit ) , '"methods" should contain strings' )
659+
660+ methods . push ( nodeLit . getLiteralValue ( ) )
661+ } )
662+ assert ( node , methods . length > 0 , "'methods' should not be empty" )
663+ urlTemplate . methods = methods
664+ } else {
665+ assert ( node , false , "Expecting 'path' or 'methods'" )
666+ }
667+ } )
668+
669+ assert ( urlTemplate , urlTemplate . path , "Missing required property 'path'" )
670+ assert ( urlTemplate , urlTemplate . methods , "Missing required property 'methods'" )
671+
672+ result . push ( urlTemplate )
673+ } )
674+
675+ return result
676+ }
0 commit comments