Dans l'exemple de code ci-dessous, nous supposons que nous devons représenter les besoins commerciaux suivants :
Graphiquement, cela peut être exprimé comme suit :
En code, on obtiendra le résultat suivant, où chaque objet requête, réponse et modèle
est une classe de données sous-classant une classe appelée Model
.
# -*- coding: utf-8 -*-
# stdlib
from dataclasses import dataclass
# Zato
from zato.common.typing_ import list_
from zato.server.service import Model, Service
# ###########################################################################
@dataclass(init=False)
class Phone(Model):
imei: str
owner_id: int
owner_name: str
# ###########################################################################
@dataclass(init=False)
class GetPhoneListRequest(Model):
client_id: int
@dataclass(init=False)
class GetPhoneListResponse(Model):
phone_list: list_[Phone]
response_type: str
# ###########################################################################
class GetPhoneDetails(Service):
class SimpleIO:
input = GetPhoneListRequest
output = GetPhoneListResponse
def handle(self):
# Enable type checking and type completion
request = self.request.input # type: GetPhoneListRequest
# Log details of our request
self.logger.info('Processing client `%s`', request.client_id)
# Build our response now - in a full service this information
# would be read from an exteran system or database.
# Our list of phones to return
phone_list = []
# Build the fist phone ..
phone1 = Phone()
phone1.imei = '123'
phone1.owner_id = 456
phone1.owner_name = 'John Doe'
# .. the second one ..
phone2 = Phone()
phone2.imei = '789'
phone2.owner_id = 999
phone2.owner_name = 'Jane Doe'
# .. populate the container for phones tha we return ..
phone_list.append(phone1)
phone_list.append(phone2)
# .. build the top-level response element ..
response = GetPhoneListResponse()
response.response_type = 'RZH'
response.phone_list = phone_list
# .. and return the response to our caller
self.response.payload = response
# ###########################################################################
Ce service est prêt à être invoqué avec :
$ curl http://pubapi:<password>@localhost:17010/zato/api/invoke/phone.get-phone-list \
-d '{"client_id":789}'
{"response_type":"RZH",
"phone_list": [
{"imei":"123","owner_id":456,"owner_name":"John Doe"},
{"imei":"789","owner_id":999,"owner_name":"Jane Doe"}]}
$
Pour indiquer qu'un champ particulier dans un modèle est facultatif, utilisez le code suivant:
# stdlib
from dataclasses import dataclass
# Zato
from zato.common.typing_ import optional_ # Note the underscores
from zato.server.service import Model
@dataclass(init=False)
class Customer(Model):
email: optional_[str] # This field is optional, its value may be None
Votre modèle peut post-traiter les données qu'il reçoit après que Zato ait déjà désérialisé une requête ou une entrée vers une classe de données - cela peut être utilisé pour la validation des données reçues ou pour un post-traitement supplémentaire, par exemple, vous pouvez avoir un champ email déclaré comme une chaîne et votre modèle peut confirmer que c'est bien une chaîne mais qu'elle correspond au format email ou au domaine attendu.
Le post-traitement est effectué dans une méthode appelée after_created
, déclarée comme suit :
L'objet ctx
possède trois attributs d'intérêt :
Notez que la méthode est invoquée pour chaque JSON ou dict reçu, et non pour chaque champ individuel. Cela signifie que, si la méthode est invoquée, elle peut supposer que la validation et le traitement communs par Zato se sont déroulés avec succès, c'est-à-dire que tous les champs non facultatifs sont sûrs d'exister à ce stade.
Par exemple, si vous envoyez cette information dans une demande ..
.. the ctx.data
dict will be {'client_id': 123, 'client_name':'John Done'}
rather than
'client_id'
and 'client_name'
individually.
Le ctx.service
est le même service dont la méthode handle
est invoquée. Parce que c'est un service,
cela signifie qu'il a accès à tout ce qu'un service peut faire. Par exemple, pour chaque classe de données reçue, vous pouvez effectuer la validation des entrées en utilisant un appel REST externe. Vous pouvez également appliquer votre propre masquage des données, par exemple en remplaçant les numéros de carte de crédit par des signes *** . Il n'y a aucune limite à ce qu'un service peut faire.
La méthode after_created
doit modifier ctx.data
en place, sans rien retourner lorsqu'elle est appelée.
from model import Client
.server.log
.
Chaque modèle possède deux méthodes de commodité : .to_dict()
et .to_json()
qui sérialisent ce modèle vers un dict ou un JSON, respectivement.
Grâce à ces méthodes, les modèles peuvent être utilisés pour exprimer des dictionnaires complexes en utilisant des classes de données typées statiquement qui peuvent être plus faciles à créer et à maintenir.
Par exemple, lorsque vous invoquez ElasticSearch, vous pouvez être amené à construire un dict complexe pour exprimer divers filtres de requête. Au lieu de cela, créez vos propres modèles qui représentent les filtres ElasticSearch et sérialisez-les en utilisant .to_dict()
.
Il y a aussi de nombreuses situations où une chaîne JSON est nécessaire - construisez vos données en utilisant des modèles et appelez
.to_json()
au moment de la sérialisation.
Les modèles peuvent être utilisés lorsque vous invoquez un service Zato depuis un autre, par exemple :
# -*- coding: utf-8 -*-
# stdlib
from dataclasses import dataclass
# Zato
from zato.server.service import Model, Service
# ###########################################################################
@dataclass(init=False)
class GetClientRequest(Model):
client_id: int
@dataclass(init=False)
class GetClientResponse(Model):
client_name: str
# ###########################################################################
class APIService(Service):
def handle(self):
# The service that will be invoking
name = 'my.api.get-client'
# Build our request ..
request = GetClientRequest()
request.client_id = 123
# .. and get client details - note that we indicate in a type hint
# .. what the actual model we have in the response.
response = self.invoke(name, request) # type: GetClientResponse
# ###########################################################################
class GetClient(Service)
class SimpleIO:
input = GetClientRequest
output = GetClientResponse
def handle(self):
# Enable type checking and type completion
request = self.request.input # type: GetClientRequest
# Log what we are doing ..
self.logger.info('Returning data for %s', request.client_id)
# .. build our response ..
response = GetClientResponse()
response.client_name = 'Jane Doe'
# .. and return it to our caller.
self.response.payload = response
# ###########################################################################
Les modèles peuvent être utilisés pour invoquer directement les API REST, sans avoir besoin de les sérialiser en dicts ou en JSON.
# -*- coding: utf-8 -*-
# stdlib
from dataclasses import dataclass
# Zato
from zato.server.service import Model, Service
# ###########################################################################
@dataclass(init=False)
class GetClientRequest(Model):
client_id: int
@dataclass(init=False)
class GetClientResponse(Model):
client_name: str
# ###########################################################################
class APIService(Service):
def handle(self):
# The REST endpoint
name = 'My Client API'
# Build our request ..
request = GetClientRequest()
request.client_id = 123
# .. invoke the endpoint ..
response = self.out.rest.post(self.cid, request)
# .. read the response data + enable type checking and type completion ..
response = response.data # type: GetClientResponse
# .. and log what we have received.
self.loger.info('Data received %s', response.client_name)
# ###########################################################################
Les définitions des services qui utilisent des modèles peuvent être exportées vers OpenAPI.
Notez que l'intégration avec OpenAPI est au niveau des services, qu'il y ait ou non des canaux REST directs pour eux. Cela signifie que vous pouvez avoir, par exemple, un service qui n'est accessible qu'à travers des WebSockets, mais vous pouvez toujours être capable d'y accéder à travers des clients OpenAPI, ce qui peut être utile car maintenant vous pouvez invoquer vos services à partir de n'importe quel outil compatible OpenAPI, comme Postman ci-dessous.
Exporter des services vers OpenAPI :
Invoquez l'un d'entre eux dans Postman:
En plus de l'OpenAPI, il est également possible de générer des spécifications d'API complètes qui incluent également une documentation HTML.
Ceci est utile si vous avez besoin d'offrir une documentation statique pour votre API, en particulier si vos serveurs sont internes et que vos partenaires techniques ne peuvent pas y accéder directement - il suffit de générer une spécification d'API et de la rendre disponible en tant que site statique sans avoir besoin d'ouvrir l'accès à vos serveurs internes.
Créez une spécification d'API complète :
Nous pouvons maintenant l'ouvrir dans un navigateur - notez que la définition OpenAPI est toujours là et peut être téléchargée également.
Page principale:
Détails d'un service particulier :