Beseitigen Sie Redundanzen in RAML mit Ressourcentypen und -merkmalen

Dieser Artikel ist Teil einer Reihe: • Einführung in RAML - Die RESTful API-Modellierungssprache

• Beseitigen Sie Redundanzen in RAML mit Ressourcentypen und -merkmalen (aktueller Artikel). • Modulare RAML mithilfe von Includes, Bibliotheken, Overlays und Erweiterungen

• Definieren Sie benutzerdefinierte RAML-Eigenschaften mithilfe von Anmerkungen

1. Übersicht

In unserem RAML-Tutorial-Artikel haben wir die RESTful API Modeling Language eingeführt und eine einfache API-Definition erstellt, die auf einer einzelnen Entität namens Foo basiert . Stellen Sie sich nun eine reale API vor, in der Sie über mehrere Ressourcen vom Typ Entität verfügen, die alle dieselben oder ähnliche GET-, POST-, PUT- und DELETE-Operationen ausführen. Sie können sehen, wie Ihre API-Dokumentation schnell langweilig und sich wiederholend werden kann.

In diesem Artikel zeigen wir, wie durch die Verwendung der Funktionen für Ressourcentypen und -merkmale in RAML Redundanzen in Ressourcen- und Methodendefinitionen beseitigt werden können , indem allgemeine Abschnitte extrahiert und parametrisiert werden. Dadurch werden Fehler beim Kopieren und Einfügen vermieden und Ihre API-Definitionen präziser gestaltet.

2. Unsere API

Um die Vorteile von Ressourcentypen und -merkmalen zu demonstrieren , werden wir unsere ursprüngliche API erweitern, indem wir Ressourcen für einen zweiten Entitätstyp namens Bar hinzufügen . Hier sind die Ressourcen, aus denen unsere überarbeitete API besteht:

  • GET / api / v1 / foos
  • POST / api / v1 / foos
  • GET / api / v1 / foos / {fooId}
  • PUT / api / v1 / foos / {fooId}
  • DELETE / 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}
  • LÖSCHEN / api / v1 / Bars / {barId}
  • GET / api / v1 / Bars / fooId / {fooId}

3. Muster erkennen

Wenn wir die Liste der Ressourcen in unserer API durchlesen, sehen wir einige Muster. Beispielsweise gibt es ein Muster für die URIs und Methoden, die zum Erstellen, Lesen, Aktualisieren und Löschen einzelner Entitäten verwendet werden, und ein Muster für die URIs und Methoden zum Abrufen von Sammlungen von Entitäten. Das Sammlungs- und Sammlungselementmuster ist eines der am häufigsten verwendeten Muster zum Extrahieren von Ressourcentypen in RAML-Definitionen.

Schauen wir uns einige Abschnitte unserer API an:

[Hinweis: In den folgenden Codeausschnitten zeigt eine Zeile mit nur drei Punkten (…) an, dass einige Zeilen der Kürze halber übersprungen werden.]

/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

Wenn wir die RAML-Definitionen der Ressourcen / foos und / bar , einschließlich der verwendeten HTTP-Methoden, vergleichen, sehen wir mehrere Redundanzen zwischen den verschiedenen Eigenschaften der einzelnen, und es treten erneut Muster auf.

Überall dort, wo ein Muster in einer Ressourcen- oder Methodendefinition vorhanden ist, besteht die Möglichkeit, einen RAML- Ressourcentyp oder ein Merkmal zu verwenden .

4. Ressourcentypen

Um die in der API gefundenen Muster zu implementieren, verwenden Ressourcentypen reservierte und benutzerdefinierte Parameter, die von doppelten spitzen Klammern (<>) umgeben sind.

4.1 Reservierte Parameter

In Ressourcentypdefinitionen können zwei reservierte Parameter verwendet werden:

  • <> repräsentiert den gesamten URI (nach dem baseURI ) und
  • <> stellt den Teil des URI dar, der dem Schrägstrich ganz rechts (/) folgt, wobei geschweifte Klammern {} ignoriert werden.

Bei der Verarbeitung innerhalb einer Ressourcendefinition werden ihre Werte basierend auf der zu definierenden Ressource berechnet.

In Anbetracht der Ressource / der Foos würde beispielsweise <> mit "/ foos" und <> mit "foos" bewertet.

Angesichts der Ressource / foos / {fooId} , <> würden auswerten zu „/ foos / {fooId}“ und <> würden bewerten zu „Foos“.

4.2 Benutzerdefinierte Parameter

A resource type definition may also contain user-defined parameters. Unlike the reserved parameters, whose values are determined dynamically based on the resource being defined, user-defined parameters must be assigned values wherever the resource type containing them is used, and those values do not change.

User-defined parameters may be declared at the beginning of a resource type definition, although doing so is not required and is not common practice, as the reader can usually figure out their intended usage given their names and the contexts in which they are used.

4.3 Parameter Functions

A handful of useful text functions are available for use wherever a parameter is used in order to transform the expanded value of the parameter when it is processed in a resource definition.

Here are the functions available for parameter transformation:

  • !singularize
  • !pluralize
  • !uppercase
  • !lowercase
  • !uppercamelcase
  • !lowercamelcase
  • !upperunderscorecase
  • !lowerunderscorecase
  • !upperhyphencase
  • !lowerhyphencase

Functions are applied to a parameter using the following construct:

<<parameterName | !functionName>>

If you need to use more than one function to achieve the desired transformation, you would separate each function name with the pipe symbol (“|”) and prepend an exclamation point (!) before each function used.

For example, given the resource /foos, where <<resourcePathName>> evaluates to “foos”:

  • <<resourcePathName | !singularize>> ==> “foo”
  • <<resourcePathName | !uppercase>> ==> “FOOS”
  • <<resourcePathName | !singularize | !uppercase>> ==> “FOO”

And given the resource /bars/{barId}, where <<resourcePathName>> evaluates to “bars”:

  • <<resourcePathName | !uppercase>> ==> “BARS”
  • <<resourcePathName | !uppercamelcase>> ==> “Bar”

5. Extracting a Resource Type for Collections

Let's refactor the /foos and /bars resource definitions shown above, using a resource type to capture the common properties. We will use the reserved parameter <>, and the user-defined parameter <> to represent the data type used.

5.1 Definition

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.

Die vollständige Implementierung dieses Tutorials finden Sie im Github-Projekt.

Hier ist unsere endgültige RAML-API in ihrer Gesamtheit:

#%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 } ]
Weiter » Modulare RAML mit Includes, Bibliotheken, Overlays und Erweiterungen « Vorherige Einführung in RAML - Die RESTful API-Modellierungssprache