@@ -53,7 +53,7 @@ import {
53
53
verifyUniqueness ,
54
54
parseJsDocTags ,
55
55
deepEqual ,
56
- sourceLocation , sortTypeDefinitions
56
+ sourceLocation , sortTypeDefinitions , parseDeprecation
57
57
} from './utils'
58
58
59
59
const jsonSpec = buildJsonSpec ( )
@@ -210,14 +210,6 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
210
210
if ( mapping == null ) {
211
211
throw new Error ( `Cannot find url template for ${ namespace } , very likely the specification folder does not follow the rest-api-spec` )
212
212
}
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 ) ) ]
221
213
222
214
let pathMember : Node | null = null
223
215
let bodyProperties : model . Property [ ] = [ ]
@@ -226,39 +218,50 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
226
218
227
219
// collect path/query/body properties
228
220
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`
230
222
assert (
231
223
member ,
232
224
Node . isPropertyDeclaration ( member ) || Node . isPropertySignature ( member ) ,
233
225
'Class and interfaces can only have property declarations or signatures'
234
226
)
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 )
237
234
assert ( member , property . properties . length > 0 , 'There is no need to declare an empty object path_parts, just remove the path_parts declaration.' )
238
235
pathMember = member
239
236
type . path = property . properties
240
- } else if ( property . name === 'query_parameters' ) {
237
+ } else if ( name === 'query_parameters' ) {
238
+ const property = visitRequestOrResponseProperty ( member )
241
239
assert ( member , property . properties . length > 0 , 'There is no need to declare an empty object query_parameters, just remove the query_parameters declaration.' )
242
240
type . query = property . properties
243
- } else if ( property . name === 'body' ) {
241
+ } else if ( name === 'body' ) {
242
+ const property = visitRequestOrResponseProperty ( member )
244
243
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
- )
250
244
if ( property . valueOf != null ) {
251
245
bodyValue = property . valueOf
252
246
} else {
253
247
assert ( member , property . properties . length > 0 , 'There is no need to declare an empty object body, just remove the body declaration.' )
254
248
bodyProperties = property . properties
255
249
}
256
250
} else {
257
- assert ( member , false , `Unknown request property: ${ property . name } ` )
251
+ assert ( member , false , `Unknown request property: ${ name } ` )
258
252
}
259
253
}
260
254
261
255
// 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
+
262
265
for ( const part of type . path ) {
263
266
assert (
264
267
pathMember as Node ,
@@ -282,6 +285,13 @@ function compileClassOrInterfaceDeclaration (declaration: ClassDeclaration | Int
282
285
}
283
286
284
287
// 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
+ }
285
295
// the body can either be a value (eg Array<string> or an object with properties)
286
296
if ( bodyValue != null ) {
287
297
// Propagate required body value nature based on TS question token being present.
@@ -587,3 +597,80 @@ function visitRequestOrResponseProperty (member: PropertyDeclaration | PropertyS
587
597
588
598
return { name, properties, valueOf }
589
599
}
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