forked from raml-org/raml-org
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathraml-200-tutorial.html
824 lines (821 loc) · 44.5 KB
/
raml-200-tutorial.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
---
layout: page
title: RAML 200 Tutorial
permalink: /developers/raml-200-tutorial
---
<div class="tutorial-intro"><p><strong>Objective:</strong> Once you’re familiar with the <a href="https://raml.org/developers/raml-100-tutorial">basics of RAML</a>, it's time to dig into the more complex features of the language itself and take full advantage of what RAML can offer.</p>
<p><strong>Introduction</strong></p>
<p>This tutorial will guide you through a complete use case for a jukebox API. You’ll learn how to optimize and reuse your code by applying concepts such as resourceTypes and traits, and RAML utilities such as !includes. The tutorial will also demonstrate how RAML uses schemas, and show how to use them to validate an HTTP body.</p><p>Please click <a href="https://github.com/raml-org/raml-examples/tree/master/others/tutorial-jukebox-api" target="_blank">here</a>, if you want to look at the complete RAML you will build in this tutorial.</p>
<p><strong>Assumptions</strong></p>
<p>You know the <a href="https://raml.org/developers/raml-100-tutorial">basics of RAML</a>: how to write a RAML file with resources, parameters, methods, and responses.</p>
</div><h2 id="use-case">USE CASE</h2>
<p>Build a music Jukebox. While the physical device will be responsible for displaying the information and capturing the user input, it will be relying on your API to retrieve the information requested. The Jukebox needs to be able to:</p>
<ul>
<li>Show the full list of artists.</li>
<li>Show the full list of albums.</li>
<li>Show the list of artists by nationality.</li>
<li>Show the list of albums by genre.</li>
<li>Search for a song by title.</li>
<li>Show a particular artist's albums collection.</li>
<li>Show a particular album's songs list.</li>
<li>Play a song (by specifying the song id).</li>
<li>Enter new Artists, Albums and Songs (only authenticated users).</li>
</ul>
<p><strong>Consideration:</strong> This is a jukebox, not a command line. People in pubs might be unable to type lots of characters, so a user friendly UI (paging, image-based, etc) would be very appreciated.</p>
<h2 id="base-raml-file">BASE RAML FILE</h2>
<p>If you have read the <a href="https://raml.org/developers/raml-100-tutorial">RAML 100 Tutorial</a>, you should be able to understand our base RAML API definition without major difficulties. Its basic structure could be described as:</p>
<pre><code>/songs:
get:
post:
/{songId}:
get:
/file-content:
get:
post:
/artists:
get:
post:
/{artistId}:
get:
/albums
get:
/albums:
get:
post:
/{albumId}:
get:
/songs:
get:
</code></pre>
<p>As you can see in the following example, the resource "/songs" doesn't have a well defined POST: body parameters are missing.</p>
<pre><code class="lang-yaml">/songs:
description: Collection of available songs in Jukebox
get:
description: Get a list of songs based on the song title.
queryParameters:
songTitle:
description: "The title of the song to search (it is case insensitive and doesn't need to match the whole title)"
required: true
minLength: 3
type: string
example: "Get L"
responses:
200:
body:
application/json:
example: |
{
"songs": [
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky"
},
{
"songId": "550e8400-e29b-41d4-a716-446655440111",
"songTitle": "Loose yourself to dance"
},
{
"songId": "550e8400-e29b-41d4-a716-446655440222",
"songTitle": "Gio sorgio by Moroder"
}
]
}
/{songId}:
description: Song entity
get:
description: Get the song with `songId = {songId}`
responses:
200:
body:
application/json:
example: |
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky",
"duration": "6:07",
"artist": {
"artistId": "110e8300-e32b-41d4-a716-664400445500"
"artistName": "Daft Punk",
"imageURL": "http://travelhymns.com/wp-content/uploads/2013/06/random-access-memories1.jpg"
},
"album": {
"albumId": "183100e3-0e2b-4404-a716-66104d440550",
"albumName": "Random Access Memories",
"imageURL": "http://upload.wikimedia.org/wikipedia/en/a/a7/Random_Access_Memories.jpg"
}
}
404:
body:
application/json:
example: |
{"message": "Song not found"}
/file-content:
description: The file to be reproduced by the client
get:
description: Get the file content
responses:
200:
post:
post:
</code></pre>
<h2 id="body-parameters">BODY PARAMETERS</h2>
<p><strong>Form Parameters</strong></p>
<p>There are several ways of defining the body parameters for an HTTP method. For example:</p>
<pre><code class="lang-yaml">/file-content:
description: The file to be reproduced by the client
get:
description: Get the file content
responses:
200:
body:
binary/octet-stream:
example: !include heybulldog.mp3
post:
description: |
Enters the file content for an existing song entity.
Use the "binary/octet-stream" content type to specify the content from any consumer (excepting web-browsers).
Use the "multipart-form/data" content type to upload a file which content will become the file-content
body:
binary/octet-stream:
multipart/form-data:
properties:
file:
description: The file to be uploaded
required: true
type: file
</code></pre>
<p><code>/file-content</code> resource represents the file to reproduce when a Jukebox user select a particular song, although, there are tons of ways of modeling this scenario on a RESTful API. We've chosen this one for this tutorial purposes. It doesn't mean it's a best practice at all.</p>
<p>As you can see in the POST definition, its body contains two possible content-types.
The <code>binary/octet-stream</code> simply expects <code>file-content</code> to be sent as a parameter. It's a valid and popular technique for APIs that supporting files. Unfortunately, it makes the API impossible to call from a web browser (at least with the purpose of uploading a file).</p>
<p>For the <code>multipart/form-data</code> (and also the <code>application/x-www-form-urlencoded</code>), it is possible to define a map of "formParameters", defining this map the same way that the rest of the RAML ones (in this case, the "file" field is required and of type "file").</p>
<p><strong>Schemas</strong></p>
<p>A body also can be of <code>application/json</code> content-type (among others, like <code>application/xml</code>) and for these, the expected body parameter will be a string with a valid JSON (or XML). So, this is another way of defining a method's body parameter.
One of the RAML supported features is the possibility of defining schemas and apply these to the body parameters as well, as shown in the example below.</p>
<pre><code class="lang-yaml">body:
application/json:
type: |
{
"type": "object",
"$schema": "http://json-schema.org/draft-03/schema",
"id": "http://jsonschema.net",
"required": true,
"properties": {
"songTitle": {
"type": "string",
"required": true
},
"albumId": {
"type": "string",
"required": true,
"minLength": 36,
"maxLength": 36
}
}
}
example: |
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky",
"albumId": "183100e3-0e2b-4404-a716-66104d440550"
}
</code></pre>
<p>What the example is basically saying is: "The expected parameter is a valid json, and for valid, it needs to fulfill the specified schema definition". In this case, the represented object has:</p>
<ul>
<li>"songTitle" property of type "string", and it's required</li>
<li>"albumId" property of type "string", and not only is it required, but it also needs to be 36 characters long.</li>
</ul>
<p>It's not the intention of this tutorial explain how JSON and XML schemas work, but you can learn more at <a href="http://json-schema.org/">http://json-schema.org/</a> and <a href="http://json-schema.org/">http://www.w3.org/XML/Schema.html</a>.</p>
<h2 id="extract-schemas">EXTRACT SCHEMAS</h2>
<p>One interesting RAML feature is the ability to extract the schemas and reference them by name. There are three major advantages of doing this, and the first two might look a bit obvious:</p>
<ul>
<li>Improve RAML readability</li>
<li>Allow reusing the schemas in several sections.</li>
</ul>
<p>The third advantage will become clear in following sections, when trying to use "resource types" and parameterize these.</p>
<pre><code class="lang-yaml">types:
song: |
{
"type": "object",
"$schema": "http://json-schema.org/draft-03/schema",
"id": "http://jsonschema.net",
"required": true,
"properties": {
"songTitle": {
"type": "string",
"required": true
},
"albumId": {
"type": "string",
"required": true,
"minLength": 36,
"maxLength": 36
}
}
}
</code></pre>
<pre><code class="lang-yaml">body:
application/json:
type: song
example: |
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky",
"albumId": "183100e3-0e2b-4404-a716-66104d440550"
}
</code></pre>
<p>As you can see in the code example, the schema described in previous sections is now being defined and referenced by the name "song". The name choice is not random, and the correct convention will allow you to parameterize resource types and reuse a lot of code (this will be explained in following sections).</p>
<h2 id="resource-types">RESOURCE TYPES</h2>
<p><strong>The "collection/collection-item" pattern</strong></p>
<p><strong>We are definitively not saying that all RESTful APIs are the same.</strong> I don’t want to even suggest it. But there are absolutely some common behaviors. For example, if we are trying to represent resources that could be inferred from a business model, it will likely be analogous with the CRUD model. Given a resource, you can <strong>c</strong>reate a new one, <strong>r</strong>etrieve one or all of them and <strong>u</strong>pdate or <strong>d</strong>elete an existing one.</p>
<p>In that sense, we can easily identify an existing resource (to be fetched, deleted or updated), a new one (to be added to a collection) and the collection itself (to be retrieved).</p>
<pre><code class="lang-yaml">#%RAML 1.0
title:
/resources:
get:
post:
/{resourceId}:
get:
put:
delete:
</code></pre>
<p>So, we found two different type of resources. The item (represented by an id), and the collection (containing all the items). It would be nice to be able to define these types, and declare the resources of those types. Luckily, there is a way to do this in RAML.
<strong>Resource Types in RAML</strong></p>
<p>Similar to the last example code, where we only showed the resources and supported methods, this step consists in just creating the "resourceTypes" with their supported methods.</p>
<pre><code class="lang-yaml">resourceTypes:
collection:
get:
post:
collection-item:
get:
</code></pre>
<p>As you may notice, the PUT and DELETE methods are not defined for the collection-item resourceType. This is basically because the use case does not request any resource to be deleted or updated.
So, what this version is saying is "There are two resource types: collection, which has the GET and POST methods defined, and collection-item which has the GET method defined". Standing alone, it doesn't really seem to be very useful. However, it's important to understand as the first step of defining good resourceTypes and reusing patterns in the code.</p>
<p><strong>Defining and parameterizing resourceTypes</strong></p>
<p>What do we know about our collections thus far? Let's check what "/songs", "/artists", and "/albums" have in common:</p>
<ul>
<li>Description</li>
<li>GET method with:<ul>
<li>description</li>
<li>response for HTTP status 200 (which body's content type is "application/json")</li>
</ul>
</li>
<li>POST method with:<ul>
<li>description</li>
<li>"access_token" queryParameter</li>
<li>bodyParameter with "application/json" contentType and validated by a Schema</li>
<li>response with HTTP status 200 (which body's content type is "application/json")</li>
</ul>
</li>
</ul>
<p>So, let's extract this from one of the resources (I will take "/songs" for this example, but we will end up parameterizing the resourceType, so it doesn't matter which one you choose to start).</p>
<pre><code class="lang-yaml">resourceTypes:
collection:
description: Collection of available songs in Jukebox
get:
description: Get a list of songs based on the song title.
responses:
200:
body:
application/json:
post:
description: |
Add a new song to Jukebox.
queryParameters:
access_token:
description: "The access token provided by the authentication application"
example: AABBCCDD
required: true
type: string
body:
application/json:
type: song
responses:
200:
body:
application/json:
example: |
{ "message": "The song has been properly entered" }
</code></pre>
<p>With the <code>collection</code> resourceType as it is right now, there is not much we can do. Applying it to the <code>/songs</code> resource is a possibility, but we don't want those descriptions, schemas, or even the POST response to be applied to all the resources since the collection is specific to <code>/songs</code>.
Parameters are useful here. Suppose that you can write a "placeholder" on the resourceType to be filled with a value specified on the resource. For instance:</p>
<pre><code>description: Collection of available <<resource>> in Jukebox
</code></pre><p>with <code><<resource>></code> receiving "songs", "artists", or "albums" depending on the resource.</p>
<p>While this is possible (and very useful for most scenarios), for this particular case it's not necessary for the resource to even pass the parameter thanks to <strong>Reserved Parameters</strong>.</p>
<p>A Reserved Parameter simply is a parameter with a value automatically specified by its context. For the resourceTypes case, there are two Reserved Parameters: resourcePath and resourcePathName. For the <code>/songs</code> example, the values will be "/songs" and "songs" respectively.</p>
<p>Now, if you are looking at the last code snippet, you will realize that we need the values to be "songs" in some cases and "song" in others.
Here is where <strong>Parameters Transformers</strong> become handy.</p>
<p>There are two Parameters Transformers we could use for this example: <code>!singularize</code> and <code>!pluralize</code> (note: The only locale supported by the current version of RAML is "United States English").</p>
<p>So combining this, let's update our latest code snippet:</p>
<pre><code class="lang-yaml">resourceTypes:
collection:
description: Collection of available <<resourcePathName>> in Jukebox
get:
description: Get a list of <<resourcePathName>> based on the song title.
responses:
200:
body:
application/json:
post:
description: |
Add a new <<resourcePathName|!singularize>> to Jukebox.
queryParameters:
access_token:
description: "The access token provided by the authentication application"
example: AABBCCDD
required: true
type: string
body:
application/json:
type: <<resourcePathName|!singularize>>
responses:
200:
body:
application/json:
example: |
{ "message": "The <<resourcePathName|!singularize>> has been properly entered" }
</code></pre>
<pre><code class="lang-yaml">/songs:
type: collection
get:
queryParameters:
songTitle:
description: "The title of the song to search (it is case insensitive and doesn't need to match the whole title)"
required: true
minLength: 3
type: string
example: "Get L"
responses:
200:
body:
application/json:
example: |
{
"songs": [
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky"
},
{
"songId": "550e8400-e29b-41d4-a716-446655440111",
"songTitle": "Loose yourself to dance"
},
{
"songId": "550e8400-e29b-41d4-a716-446655440222",
"songTitle": "Gio sorgio by Moroder"
}
]
}
post:
body:
application/json:
example: |
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky",
"albumId": "183100e3-0e2b-4404-a716-66104d440550"
}
</code></pre>
<p>Note that even the Schema name is specified with this parameter (singular in this case). We mentioned before that the schema name was not random - this is why.
Another important aspect to stress is that defining and applying a resourceType to a resource doesn't forbid you from overwriting any of the map's elements. In this example, we still see that GET method is present in both, resource and resourceType (the same for the responses, POST, etc). Not only is this allowed, but also is the way of redefining something that changes from one resource to other. <strong>If you think this looks like OOP inheritance, you’re right!</strong></p>
<p>Now, let's work with the "collection-item" resourceType.</p>
<p>There is nothing new with this code. More resourceType definitions, parameterization, and usage:</p>
<pre><code class="lang-yaml">collection-item:
description: Entity representing a <<resourcePathName|!singularize>>
get:
description: |
Get the <<resourcePathName|!singularize>>
with <<resourcePathName|!singularize>>Id =
{<<resourcePathName|!singularize>>Id}
responses:
200:
body:
application/json:
404:
body:
application/json:
example: |
{"message": "<<resourcePathName|!singularize>> not found" }
</code></pre>
<pre><code class="lang-yaml">/songs:
...
/{songId}:
type: collection-item
get:
responses:
200:
body:
application/json:
example: |
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky",
"duration": "6:07",
"artist": {
"artistId": "110e8300-e32b-41d4-a716-664400445500"
"artistName": "Daft Punk",
"imageURL": "http://travelhymns.com/wp-content/uploads/2013/06/random-access-memories1.jpg"
},
"album": {
"albumId": "183100e3-0e2b-4404-a716-66104d440550",
"albumName": "Random Access Memories",
"imageURL": "http://upload.wikimedia.org/wikipedia/en/a/a7/Random_Access_Memories.jpg"
}
}
</code></pre>
<p>But as you can see, we are still repeating lot of code. Specifically:</p>
<pre><code class="lang-yaml">get:
responses:
200:
body:
application/json:
example: |
</code></pre>
<p>Basically, every piece of code needed to define the <strong>examples</strong>. And this is basically because we have only learned how to use Reserved Parameters. However, we have also mentioned that the idea of parameterizing is to specify "placeholder" to be filled with a specified value.
That would solve our "examples problem".</p>
<h2 id="parameters">PARAMETERS</h2>
<p>At the moment of defining the parameter in the resourceType (with the placeholder), there is no difference between a parameter and a reserved parameter. The actual difference only appears when passing the parameter at the resource level. For instance, a parameter named as <code>exampleItem</code> will need to be passed this way:</p>
<pre><code class="lang-yaml">/{songId}:
type:
collection-item:
exampleItem: THIS IS THE EXAMPLE
</code></pre>
<p>In "human language", it's basically saying that <code>/{songId}</code> resource is of <code>collection-item</code> type. But now, it's also indicating that the value for the <code>collection-item</code> parameter <code>exampleItem</code> is "THIS IS THE EXAMPLE". Since this is a string, all the YAML rules for strings are valid.
Having said that, let's take a look at some relevant code pieces.</p>
<pre><code class="lang-yaml">resourceTypes:
collection:
description: Collection of available <<resourcePathName>> in Jukebox
get:
description: Get a list of <<resourcePathName>> based on the song title.
responses:
200:
body:
application/json:
post:
description: |
Add a new <<resourcePathName|!singularize>> to Jukebox.
queryParameters:
access_token:
description: "The access token provided by the authentication application"
example: AABBCCDD
required: true
type: string
body:
application/json:
type: <<resourcePathName|!singularize>>
responses:
200:
body:
application/json:
example: |
{ "message": "The <<resourcePathName|!singularize>> has been properly entered" }
collection-item:
description: Entity representing a <<resourcePathName|!singularize>>
get:
description: |
Get the <<resourcePathName|!singularize>>
with <<resourcePathName|!singularize>>Id =
{<<resourcePathName|!singularize>>Id}
responses:
200:
body:
application/json:
404:
body:
application/json:
example: |
{"message": "<<resourcePathName|!singularize>> not found" }
</code></pre>
<pre><code class="lang-yaml">/songs:
type:
collection:
exampleCollection: |
[
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky"
},
{
"songId": "550e8400-e29b-41d4-a716-446655440111",
"songTitle": "Loose yourself to dance"
},
{
"songId": "550e8400-e29b-41d4-a716-446655440222",
"songTitle": "Gio sorgio by Morodera"
}
]
exampleItem: |
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky",
"albumId": "183100e3-0e2b-4404-a716-66104d440550"
}
get:
queryParameters:
songTitle:
description: "The title of the song to search (it is case insensitive and doesn't need to match the whole title)"
required: true
minLength: 3
type: string
example: "Get L"
/{songId}:
type:
collection-item:
exampleItem: |
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky",
"duration": "6:07",
"artist": {
"artistId": "110e8300-e32b-41d4-a716-664400445500"
"artistName": "Daft Punk",
"imageURL": "http://travelhymns.com/wp-content/uploads/2013/06/random-access-memories1.jpg"
},
"album": {
"albumId": "183100e3-0e2b-4404-a716-66104d440550",
"albumName": "Random Access Memories",
"imageURL": "http://upload.wikimedia.org/wikipedia/en/a/a7/Random_Access_Memories.jpg"
}
}
</code></pre>
<p>As you can see, the same concept shown in the previous example was applied to both the <code>/songs</code>, and <code>/songs/{songId}</code> resources.
In a previous example, the code that was repeated at the end and is now completely within the resourceType at the point that the POST definition directly disappeared from the resources. <strong>That's correct. Now, every <code>collection-item</code> typed resources will have a valid (generic) POST definition without you ever writing it.</strong></p>
<h2 id="includes">INCLUDES</h2>
<p>We have improved our RAML definition a lot during the last step with resourceTypes. We were able to extract common components of the resources and encapsulate these with a structure that grants inheritance-like capabilities.</p>
<p>Nevertheless, the RAML file still contains lot of information that could be considered as "not API-describing". Sort of "economy-class" members, if you will. <strong>Equally important, but not necessarily part of the main RAML file</strong>.</p>
<p>Through <code>!includes</code>, RAML allows us to build file-distributed API definitions, which is not only useful to encourage code reuse but also improves readability.</p>
<p>Here, we will extract the examples used for <code>/songs</code> resource to different files and include these in the main RAML definition.</p>
<pre><code class="lang-json">{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky",
"albumId": "183100e3-0e2b-4404-a716-66104d440550"
}
</code></pre>
<pre><code class="lang-json">{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky",
"duration": "6:07",
"artist": {
"artistId": "110e8300-e32b-41d4-a716-664400445500"
"artistName": "Daft Punk",
"imageURL": "http://travelhymns.com/wp-content/uploads/2013/06/random-access-memories1.jpg"
},
"album": {
"albumId": "183100e3-0e2b-4404-a716-66104d440550",
"albumName": "Random Access Memories",
"imageURL": "http://upload.wikimedia.org/wikipedia/en/a/a7/Random_Access_Memories.jpg"
}
}
</code></pre>
<pre><code class="lang-json">[
{
"songId": "550e8400-e29b-41d4-a716-446655440000",
"songTitle": "Get Lucky"
},
{
"songId": "550e8400-e29b-41d4-a716-446655440111",
"songTitle": "Loose yourself to dance"
},
{
"songId": "550e8400-e29b-41d4-a716-446655440222",
"songTitle": "Gio sorgio by Morodera"
}
]
</code></pre>
<p>As you can see, the extracted files contain raw strings. It's important to stress that every included file is treated as a string by RAML, which presents some well known restrictions regarding how to distribute the definition among files. More than limitations, these restrictions attempt to define a common way to work with !includes and avoid free-form defined APIs. Remember that one of RAML’s major goals is to unify criteria and encourage best-practices.
The following code snippet shows how to include or "call" the extracted files from the main definition.</p>
<pre><code class="lang-yaml">/songs:
type:
collection:
exampleCollection: !include jukebox-include-songs.sample
exampleItem: !include jukebox-include-song-new.sample
/{songId}:
type:
collection-item:
exampleItem: !include jukebox-include-song-retrieve.sample
</code></pre>
<p>As shown in the last snippet, RAML features encourage you to reduce the quantity of code you write, while making it more reusable and maintainable.</p>
<h2 id="refactor">REFACTOR</h2>
<p>We have introduced several features and made great progress with our API definition, but aren't we missing something? We have just focused on the "/songs" resource (and its descending branch). If you check your RAML file right now, you will discover that all other resources are still not taking advantage of the work we have done.
Let's solve that right now! Repeat the same procedures for all the resources:</p>
<ul>
<li>identify and apply the collection and collection-item pattern</li>
<li>pass the correct parameters</li>
<li>extract the belonging examples into separated files</li>
</ul>
<p>As you might notice, the quantity of lines in the RAML file has been significantly reduced and there are more files than before. Most important: It's visibly simpler!
But not everything went so smoothly. If you look carefully, there is a problem with sub-collections (<code>/artists/{artistId}/albums</code> and <code>/albums/{albumId}/songs</code>). Since these aren't the main collections of each resource, we decided not to allow new elements to be created on them. In other words, these collections were READ-ONLY. When applying the <code>collection</code> resourceType, we also automatically added the "POST" method. As an additional consequence, the RAML definition now requires the <code>exampleItem</code> parameter to be passed for those resources too (which we have temporarily resolved by passing <code>{}</code>).</p>
<pre><code class="lang-yaml">/artists:
/{artistId}:
/albums:
type:
collection:
exampleCollection: !include jukebox-include-artist-albums.sample
exampleItem: {}
description: Collection of albulms belonging to the artist
get:
description: Get a specific artist's albums list
</code></pre>
<p>While that's awkward, it’s not a big deal and it will actually help us go further in order to solve it.</p>
<p>Let's create another resourceType called <code>readOnlyCollection</code>. It will be similar to <code>collection</code> but without the "POST method". And let's apply this new resourceType to its corresponding collections: <code>artists/{artistId}/albums</code> and <code>/albums/{albumId}/songs</code>:</p>
<pre><code class="lang-yaml">readOnlyCollection:
description: Collection of available <<resourcePathName>> in Jukebox.
get:
description: Get a list of <<resourcePathName>>.
responses:
200:
body:
application/json:
example: |
<<exampleCollection>>
</code></pre>
<pre><code class="lang-yaml">/artists:
/{artistId}:
/albums:
type:
readOnlyCollection:
exampleCollection: !include jukebox-include-artist-albums.sample
description: Collection of albums belonging to the artist
get:
description: Get a specific artist's albums list
/albums:
/{albumId}:
/songs:
type:
readOnlyCollection:
exampleCollection: !include jukebox-include-album-songs.sample
get:
description: Get the list of songs for the album with `albumId = {albumId}`
</code></pre>
<p>If you are following the code in detail, you will have already noticed something: <code>collection</code> and <code>readOnlyCollection</code> resourceTypes are repeating some code. Actually, <code>readOnlyCollection</code> code is completely included in <code>collection</code> code. That’s correct! And there is a way of making this even more efficient. It's all about "types composing" and it will be totally covered in a later tutorial.</p>
<h2 id="traits">TRAITS</h2>
<p>We are almost done! We are busy fulfilling all the requirements for the described use case. As usual however, we’ve discovered something while building, and this tutorial cannot be the exception.
Will I be able to sort my collections? Shouldn't my API give users the chance of paging these? Is the strategy we chose for searching a collection good enough? What if we need to enhance and make more complex queries in the future?
Let's tackle these issues. But first, we need to understand them correctly</p>
<p><strong>Understanding our resources</strong></p>
<p>Let's build a simple table to discover and agree about each collection capabilities:</p>
<table>
<thead>
<tr>
<th>Collection/Capabilities</th>
<th style="text-align:center">Searchable</th>
<th style="text-align:center">Sorteable</th>
<th style="text-align:center">Pageable</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/songs</code></td>
<td style="text-align:center">YES</td>
<td style="text-align:center">YES</td>
<td style="text-align:center">YES</td>
</tr>
<tr>
<td><code>/artists</code></td>
<td style="text-align:center">YES</td>
<td style="text-align:center">YES</td>
<td style="text-align:center">YES</td>
</tr>
<tr>
<td><code>/albums</code></td>
<td style="text-align:center">YES</td>
<td style="text-align:center">YES</td>
<td style="text-align:center">YES</td>
</tr>
<tr>
<td><code>/artists/{aId}/albums</code></td>
<td style="text-align:center">NO</td>
<td style="text-align:center">YES</td>
<td style="text-align:center">YES</td>
</tr>
<tr>
<td><code>/albums/{aId}/songs</code></td>
<td style="text-align:center">NO</td>
<td style="text-align:center">YES</td>
<td style="text-align:center">NO</td>
</tr>
</tbody>
</table>
<p>If we consider who will be consuming the API, this table would probably look very different (small collections can be filtered, ordered and paged on the client side). For the purposes of this tutorial, we are keeping it anyway.</p>
<p><strong>Fixing the Searchable collections</strong></p>
<p>Before getting involved with the Traits concept, let's enhance the Searchable fixed parameters by applying a generic "query" queryParameter.</p>
<pre><code class="lang-yaml">/songs:
type:
collection:
exampleCollection: !include jukebox-include-songs.sample
exampleItem: !include jukebox-include-song-new.sample
get:
queryParameters:
songTitle:
description: "The title of the song to search (it is case insensitive and doesn't need to match the whole title)"
required: true
minLength: 3
type: string
example: "Get L"
</code></pre>
<pre><code class="lang-yaml">/songs:
type:
collection:
exampleCollection: !include jukebox-include-songs.sample
exampleItem: !include jukebox-include-song-new.sample
get:
queryParameters:
query:
description: |
JSON array [{"field1","value1","operator1"},{"field2","value2","operator2"},...,{"fieldN","valueN","operatorN"}] with valid searchable fields: songTitle
example: |
["songTitle", "Get L", "like"]
</code></pre>
<p><strong>Searchable Trait</strong></p>
<p>The same way that several resources might utilize a specific resourceType, it's possible to define and reuse similar behavior with traits. This is one of these concepts that are better explained by code:</p>
<pre><code class="lang-yaml">/songs:
type:
collection:
exampleCollection: !include jukebox-include-songs.sample
exampleItem: !include jukebox-include-song-new.sample
get:
queryParameters:
query:
description: |
JSON array [{"field1","value1","operator1"},{"field2","value2","operator2"},...,{"fieldN","valueN","operatorN"}] with valid searchable fields: songTitle
example: |
["songTitle", "Get L", "like"]
</code></pre>
<pre><code class="lang-yaml">traits:
searchable:
queryParameters:
query:
description: |
JSON array [{"field1","value1","operator1"},{"field2","value2","operator2"},...,{"fieldN","valueN","operatorN"}] <<description>>
example: |
<<example>>
</code></pre>
<p>As you can see, this Searchable trait is comprised of a name and an applicable parameter. It is also evident in the example above that traits can be parameterized. Let's check how the trait can be applied to a method:</p>
<pre><code class="lang-yaml">/songs:
type:
collection:
exampleCollection: !include jukebox-include-songs.sample
exampleItem: !include jukebox-include-song-new.sample
get:
is: [searchable: {description: "with valid searchable fields: songTitle", example: "[\"songTitle\", \"Get L\", \"like\"]"}]
</code></pre>
<p>So, what the definition is really saying is that there is a trait called "Searchable" and that the "/songs" resource utilizes it. Furthermore, the trait is applied to the GET method itself, since the "Searchable" contract should only be applied to that particular method. In other cases, you could apply a trait to the whole resource, and even more: <strong>traits can also be applied to resourceTypes.</strong> This topic should and will be covered in a separate tutorial (types composition). Feel free to try this out anyway, and always remember that you can:</p>
<p>Note that we have already applied the Searchable trait to <code>/songs</code>, <code>/artists</code> and <code>/albums</code> resources.</p>
<p><strong>Other traits</strong></p>
<p>Considering our table, we need to create 2 additional traits: Orderable and Pageable. The creation is trivial, and when applied we confirm something that you might have noticed during the previous step: traits are a collection (that's why they are applied within an array).</p>
<pre><code class="lang-yaml">orderable:
queryParameters:
orderBy:
description: |
Order by field: <<fieldsList>>
type: string
required: false
order:
description: Order
enum: [desc, asc]
default: desc
required: false
pageable:
queryParameters:
offset:
description: Skip over a number of elements by specifying an offset value for the query
type: integer
required: false
example: 20
default: 0
limit:
description: Limit the number of elements on the response
type: integer
required: false
example: 80
default: 10
</code></pre>
<pre><code class="lang-yaml">/songs:
type:
collection:
exampleCollection: !include jukebox-include-songs.sample
exampleItem: !include jukebox-include-song-new.sample
get:
is: [
searchable: {description: "with valid searchable fields: songTitle", example: "[\"songTitle\", \"Get L\", \"like\"]"},
orderable: {fieldsList: "songTitle"},
pageable
]
</code></pre>
<p>In this case, you can see that the "Pageable" trait receives no parameter.</p>
<p>Go ahead! Apply the proper traits to the proper resources as we defined in the table.</p>
<h2 id="final-tuning">FINAL TUNING</h2>
<p>We could say that our RAML file has been properly refactored and is now much more readable, reusable, and maintainable. Maybe a last step would be to double-check which parts of the RAML definition could now be extracted to other files (the same way we have done with the "examples").
Starting at the root, we find the schemas, and it seems a no-brainer that each JSON (in this case) could be extracted and included as we have learned.</p>
<pre><code class="lang-yaml">types:
song: !include jukebox-include-song.schema
artist: !include jukebox-include-artist.schema
album: !include jukebox-include-album.schema
</code></pre>
<p>and of course, three new files will appear in your file system.</p>
<p>While this doesn't seem to be a revelation (it isn't), let's keep checking our RAML file to discover what else can be extracted. Honestly, resourceTypes and traits are really tempting. But if you try to follow the same strategy, you will surely fail. Remember in previous sections that we explained that the <code>!include</code> function would just take the content of the file and embed its contents as a string? That’s precisely what we wanted to do with the examples and the schemas. However, if we look at the resourceTypes and traits again, we will notice that they are not just strings, but maps (just like the rest of the RAML file). So basically, NO! You CANNOT extract these with the same approach you used to extract examples and schemas.</p>
<p>However, you could extract all the resourceTypes to a file (and do the same with the traits).</p>
<pre><code>resourceTypes: !include jukebox-includes-resourceTypes.inc
</code></pre><p>While this is not a restriction, it’s good to note it doesn't mean it's a recommended practice. In some cases, you will need to compromise. For example: if we had 2000 lines of resourceTypes definition, we probably would like to extract this to a separate file. But if the resourceTypes are not really complicating the readability, it could also be nice to be able to see how they are defined without going to an external file. As usual, it's a matter of good judgment.</p>
<h2 id="conclusion">CONCLUSION</h2>
<p>In this tutorial, we learned how to optimize our RAML file from a code reuse and maintainability point of view, Traits, resourceTypes, and includes were introduced and a full use case was developed and refactored.</p>
<p>Finally, just like in every discipline, we need to use good judgment. Always remember that over engineering is never a good idea. Ever.</p>