Éliminez les redondances dans la RAML avec les types de ressources et les traits

Cet article fait partie d'une série: • Introduction à RAML - Le langage de modélisation d'API RESTful

• Éliminer les redondances dans la RAML avec les types de ressources et les caractéristiques (article actuel) • RAML modulaire à l'aide d'includes, de bibliothèques, de superpositions et d'extensions

• Définir des propriétés RAML personnalisées à l’aide d’annotations

1. Vue d'ensemble

Dans notre article du didacticiel RAML, nous avons présenté le langage de modélisation d'API RESTful et créé une définition d'API simple basée sur une seule entité appelée Foo . Imaginez maintenant une API du monde réel dans laquelle vous disposez de plusieurs ressources de type entité, ayant toutes des opérations GET, POST, PUT et DELETE identiques ou similaires. Vous pouvez voir comment la documentation de votre API peut rapidement devenir fastidieuse et répétitive.

Dans cet article, nous montrons comment l'utilisation des types de ressources et des caractéristiques des traits dans RAML peut éliminer les redondances dans les définitions de ressources et de méthodes en extrayant et en paramétrant des sections communes, éliminant ainsi les erreurs de copier-coller tout en rendant vos définitions d'API plus concises.

2. Notre API

Afin de démontrer les avantages des types de ressources et des caractéristiques , nous étendrons notre API d'origine en ajoutant des ressources pour un deuxième type d'entité appelé Bar . Voici les ressources qui constitueront notre API révisée:

  • GET / api / v1 / foos
  • POST / api / v1 / foos
  • GET / api / v1 / foos / {fooId}
  • PUT / api / v1 / foos / {fooId}
  • SUPPRIMER / api / v1 / foos / {fooId}
  • GET / api / v1 / foos / name / {name}
  • GET / api / v1 / foos? Name = {name} & ownerName = {ownerName}
  • GET / api / v1 / bars
  • POST / api / v1 / bars
  • GET / api / v1 / bars / {barId}
  • PUT / api / v1 / bars / {barId}
  • SUPPRIMER / api / v1 / bars / {barId}
  • GET / api / v1 / bars / fooId / {fooId}

3. Reconnaître les modèles

En parcourant la liste des ressources de notre API, nous commençons à voir des modèles émerger. Par exemple, il existe un modèle pour les URI et les méthodes utilisées pour créer, lire, mettre à jour et supprimer des entités uniques, et il existe un modèle pour les URI et les méthodes utilisées pour récupérer des collections d'entités. Le modèle de collection et d'élément de collection est l'un des modèles les plus courants utilisés pour extraire les types de ressources dans les définitions RAML.

Examinons quelques sections de notre API:

[Remarque: dans les extraits de code ci-dessous, une ligne contenant seulement trois points (…) indique que certaines lignes sont ignorées par souci de concision.]

/foos: get: description: | List all foos matching query criteria, if provided; otherwise list all foos queryParameters: name?: string ownerName?: string responses: 200: body: application/json: type: Foo[] post: description: Create a new foo body: application/json: type: Foo responses: 201: body: application/json: type: Foo ... /bars: get: description: | List all bars matching query criteria, if provided; otherwise list all bars queryParameters: name?: string ownerName?: string responses: 200: body: application/json: type: Bar[] post: description: Create a new bar body: application/json: type: Bar responses: 201: body: application/json: type: Bar

Lorsque nous comparons les définitions RAML des ressources / foos et / bars , y compris les méthodes HTTP utilisées, nous pouvons voir plusieurs redondances entre les différentes propriétés de chacune, et nous voyons à nouveau des modèles commencer à émerger.

Partout où il y a un modèle dans une définition de ressource ou de méthode, il est possible d'utiliser un type ou un trait de ressource RAML .

4. Types de ressources

Afin d'implémenter les modèles trouvés dans l'API, les types de ressources utilisent des paramètres réservés et définis par l'utilisateur entourés de doubles crochets (<>).

4.1 Paramètres réservés

Deux paramètres réservés peuvent être utilisés dans les définitions de type de ressource:

  • <> Représente l'ensemble URI (suivant la baseURI ), et
  • <> représente la partie de l'URI qui suit la barre oblique la plus à droite (/), en ignorant les accolades {}.

Lorsqu'elles sont traitées dans une définition de ressource, leurs valeurs sont calculées en fonction de la ressource en cours de définition.

Étant donné la ressource / foos , par exemple, <> serait évalué à «/ foos» et <> serait évalué à «foos».

Étant donné la ressource / foos / {fooId} , <> serait évalué à "/ foos / {fooId}" et <> serait évalué à "foos".

4.2 Paramètres définis par l'utilisateur

Une définition de type de ressource peut également contenir des paramètres définis par l'utilisateur. Contrairement aux paramètres réservés, dont les valeurs sont déterminées dynamiquement en fonction de la ressource définie, les paramètres définis par l'utilisateur doivent se voir attribuer des valeurs partout où le type de ressource les contenant est utilisé, et ces valeurs ne changent pas.

Les paramètres définis par l'utilisateur peuvent être déclarés au début d'une définition de type de ressource , bien que cela ne soit pas obligatoire et ne soit pas une pratique courante, car le lecteur peut généralement déterminer leur utilisation prévue compte tenu de leurs noms et des contextes dans lesquels ils sont utilisés.

4.3 Fonctions des paramètres

Une poignée de fonctions de texte utiles sont disponibles pour une utilisation partout où un paramètre est utilisé afin de transformer la valeur étendue du paramètre lorsqu'il est traité dans une définition de ressource.

Voici les fonctions disponibles pour la transformation des paramètres:

  • ! singulariser
  • ! pluraliser
  • ! majuscule
  • ! minuscule
  • ! casquette
  • ! minuscule
  • ! casier supérieur
  • ! cas inférieur
  • ! casse supérieure
  • ! minuscule

Les fonctions sont appliquées à un paramètre à l'aide de la construction suivante:

<< parameterName | ! functionName >>

Si vous devez utiliser plus d'une fonction pour obtenir la transformation souhaitée, séparez chaque nom de fonction par le symbole de la barre verticale («|») et ajoutez un point d'exclamation (!) Avant chaque fonction utilisée.

Par exemple, étant donné la ressource / foos , où << resourcePathName >> est évalué à «foos»:

  • << resourcePathName | ! singularize >> ==> "toto"
  • << resourcePathName | ! majuscule >> ==> "FOOS"
  • << resourcePathName | ! singulariser | ! majuscule >> ==> "FOO"

Et étant donné la ressource / bars / {barId} , où << resourcePathName >> s'évalue en «barres»:

  • << resourcePathName | ! majuscule >> ==> "BARRES"
  • << resourcePathName | ! uppercamelcase >> ==> "Bar"

5. Extraction d'un type de ressource pour les collections

Refactorisons les définitions de ressources / foos et / bars présentées ci-dessus, en utilisant un type de ressource pour capturer les propriétés communes. Nous utiliserons le paramètre réservé <> et le paramètre défini par l'utilisateur <> pour représenter le type de données utilisé.

5.1 Définition

Here is a resource type definition representing a collection of items:

resourceTypes: collection: usage: Use this resourceType to represent any collection of items description: A collection of <> get: description: Get all <>, optionally filtered responses: 200: body: application/json: type: <>[] post: description: Create a new <> responses: 201: body: application/json: type: <>

Note that in our API, because our data types are merely capitalized, singular versions of our base resources' names, we could have applied functions to the <<resourcePathName>> parameter, instead of introducing the user-defined <<typeName>> parameter, to achieve the same result for this portion of the API:

resourceTypes: collection: ... get: ... type: <>[] post: ... type: <>

5.2 Application

Using the above definition that incorporates the <<typeName>> parameter, here is how you would apply the “collection” resource type to the resources /foos and /bars:

/foos: type: { collection: { "typeName": "Foo" } } get: queryParameters: name?: string ownerName?: string ... /bars: type: { collection: { "typeName": "Bar" } }

Notice that we are still able to incorporate the differences between the two resources — in this case, the queryParameters section — while still taking advantage of all that the resource type definition has to offer.

6. Extracting a Resource Type for Single Items of a Collection

Let's focus now on the portion of our API dealing with single items of a collection: the /foos/{fooId} and /bars/{barId} resources. Here is the code for/foos/{fooId}:

/foos: ... /{fooId}: get: description: Get a Foo responses: 200: body: application/json: type: Foo 404: body: application/json: type: Error example: !include examples/Error.json put: description: Update a Foo body: application/json: type: Foo responses: 200: body: application/json: type: Foo 404: body: application/json: type: Error example: !include examples/Error.json delete: description: Delete a Foo responses: 204: 404: body: application/json: type: Error example: !include examples/Error.json

The /bars/{barId} resource definition also has GET, PUT, and DELETE methods and is identical to the /foos/{fooId} definition, other than the occurrences of the strings “foo” and “bar” (and their respective pluralized and/or capitalized forms).

6.1 Definition

Extracting the pattern we just identified, here is how we define a resource type for single items of a collection:

resourceTypes: ... item: usage: Use this resourceType to represent any single item description: A single <> get: description: Get a <> responses: 200: body: application/json: type: <> 404: body: application/json: type: Error example: !include examples/Error.json put: description: Update a <> body: application/json: type: <> responses: 200: body: application/json: type: <> 404: body: application/json: type: Error example: !include examples/Error.json delete: description: Delete a <> responses: 204: 404: body: application/json: type: Error example: !include examples/Error.json

6.2 Application

And here is how we apply the “item” resource type:

/foos: ... /{fooId}: type: { item: { "typeName": "Foo" } }
... /bars: ... /{barId}: type: { item: { "typeName": "Bar" } }

7. Traits

Whereas a resource type is used to extract patterns from resource definitions, a trait is used to extract patterns from method definitions that are common across resources.

7.1 Parameters

Along with <<resourcePath>> and <<resourcePathName>>, one additional reserved parameter is available for use in trait definitions: <<methodName>> evaluates to the HTTP method (GET, POST, PUT, DELETE, etc) for which the trait is defined. User-defined parameters may also appear within a trait definition, and where applied, take on the value of the resource in which they are being applied.

7.2 Definition

Notice that the “item” resource type is still full of redundancies. Let's see how traits can help eliminate them. We'll start by extracting a trait for any method containing a request body:

traits: hasRequestItem: body: application/json: type: <>

Now let's extract traits for methods whose normal responses contain bodies:

 hasResponseItem: responses: 200: body: application/json: type: <> hasResponseCollection: responses: 200: body: application/json: type: <>[]

Finally, here's a trait for any method that could return a 404 error response:

 hasNotFound: responses: 404: body: application/json: type: Error example: !include examples/Error.json

7.3 Application

We then apply this trait to our resource types:

resourceTypes: collection: usage: Use this resourceType to represent any collection of items description: A collection of <> get: description: | Get all <>, optionally filtered is: [ hasResponseCollection: { typeName: <> } ] post: description: Create a new <> is: [ hasRequestItem: { typeName: <> } ] item: usage: Use this resourceType to represent any single item description: A single <> get: description: Get a <> is: [ hasResponseItem: { typeName: <> }, hasNotFound ] put: description: Update a <> is: | [ hasRequestItem: { typeName: <> }, hasResponseItem: { typeName: <> }, hasNotFound ] delete: description: Delete a <> is: [ hasNotFound ] responses: 204:

We can also apply traits to methods defined within resources. This is especially useful for “one-off” scenarios where a resource-method combination matches one or more traits but does not match any defined resource type:

/foos: ... /name/{name}: get: description: List all foos with a certain name is: [ hasResponseCollection: { typeName: Foo } ]

8. Conclusion

In this tutorial, we've shown how to significantly reduce or, in some cases, eliminate redundancies from a RAML API definition.

First, we identified the redundant sections of our resources, recognized their patterns, and extracted resource types. Then we did the same for the methods that were common across resources to extract traits. Then we were able to eliminate further redundancies by applying traits to our resource types and to “one-off” resource-method combinations that did not strictly match one of our defined resource types.

As a result, our simple API with resources for only two entities, was reduced from 177 to just over 100 lines of code. To learn more about RAML resource types and traits, visit the RAML.org 1.0 spec.

L' implémentation complète de ce tutoriel se trouve dans le projet github.

Voici notre API RAML finale dans son intégralité:

#%RAML 1.0 title: Baeldung Foo REST Services API version: v1 protocols: [ HTTPS ] baseUri: //rest-api.baeldung.com/api/{version} mediaType: application/json securedBy: basicAuth securitySchemes: basicAuth: description: | Each request must contain the headers necessary for basic authentication type: Basic Authentication describedBy: headers: Authorization: description: | Used to send the Base64 encoded "username:password" credentials type: string responses: 401: description: | Unauthorized. Either the provided username and password combination is invalid, or the user is not allowed to access the content provided by the requested URL. types: Foo: !include types/Foo.raml Bar: !include types/Bar.raml Error: !include types/Error.raml resourceTypes: collection: usage: Use this resourceType to represent a collection of items description: A collection of <> get: description: | Get all <>, optionally filtered is: [ hasResponseCollection: { typeName: <> } ] post: description: | Create a new <> is: [ hasRequestItem: { typeName: <> } ] item: usage: Use this resourceType to represent any single item description: A single <> get: description: Get a <> is: [ hasResponseItem: { typeName: <> }, hasNotFound ] put: description: Update a <> is: [ hasRequestItem: { typeName: <> }, hasResponseItem: { typeName: <> }, hasNotFound ] delete: description: Delete a <> is: [ hasNotFound ] responses: 204: traits: hasRequestItem: body: application/json: type: <> hasResponseItem: responses: 200: body: application/json: type: <> hasResponseCollection: responses: 200: body: application/json: type: <>[] hasNotFound: responses: 404: body: application/json: type: Error example: !include examples/Error.json /foos: type: { collection: { typeName: Foo } } get: queryParameters: name?: string ownerName?: string /{fooId}: type: { item: { typeName: Foo } } /name/{name}: get: description: List all foos with a certain name is: [ hasResponseCollection: { typeName: Foo } ] /bars: type: { collection: { typeName: Bar } } /{barId}: type: { item: { typeName: Bar } } /fooId/{fooId}: get: description: Get all bars for the matching fooId is: [ hasResponseCollection: { typeName: Bar } ]
Suivant » RAML modulaire utilisant des includes, des bibliothèques, des superpositions et des extensions « Précédent Introduction à RAML - Le langage de modélisation d'API RESTful