Artículo
· 2 hr atrás Lectura de 7 min

Cómo añadir fácilmente una validación contra las especificaciones OpenAPI a vuestras APIs REST

En este artículo, pretendo demostrar un par de métodos para añadir fácilmente validación a las APIs REST en InterSystems IRIS Data Platform. Creo que un enfoque specification-first es una idea excelente para el desarrollo de APIs. IRIS ya dispone de funcionalidades para generar un esqueleto de implementación a partir de una especificación y publicar esa especificación para desarrolladores externos (usadlo junto con iris-web-swagger-ui para obtener los mejores resultados). Lo único importante que aún no está implementado en la plataforma es el validador de solicitudes. ¡Vamos a solucionarlo!

La tarea es la siguiente: todas las solicitudes entrantes deben validarse contra el esquema de la API descrito en formato OpenAPI. Como sabéis, la solicitud contiene: método (GET, POST, etc.), URL con parámetros, cabeceras (Content-Type, por ejemplo) y cuerpo (algún JSON). Todo ello puede comprobarse. Para resolver esta tarea, utilizaré Embedded Python, ya que la amplia biblioteca de código abierto en Python ya cuenta con dos proyectos adecuados: openapi-core y openapi-schema-validator. Una limitación aquí es que IRIS está utilizando Swagger 2.0, una versión obsoleta de OpenAPI. La mayoría de las herramientas no son compatibles con esta versión, por lo que la primera implementación de nuestro validador se limitará a comprobar únicamente el cuerpo de la solicitud.

Solución basada en openapi-schema-validator

Entradas clave:

  • La solución es totalmente compatible con el enfoque specification-first recomendado por InterSystems para el desarrollo de APIs. No necesitáis modificar las clases de la API generadas, salvo un pequeño detalle, del que hablaré más adelante.
  • Solo se valida el cuerpo de la solicitud.
  • Necesitamos extraer la definición del tipo de solicitud desde la especificación OpenAPI (clase spec.cls).
  • La correspondencia entre el JSON de la solicitud y la definición de la especificación se realiza estableciendo un tipo de contenido específico del proveedor.

Primero, necesitáis establecer un tipo de contenido específico del proveedor en la propiedad consumes de la especificación OpenAPI para vuestro endpoint. Debe tener un formato parecido a este: vnd.<company>.<project>.<api>.<request_type>+json. Por ejemplo, yo usaré:

"paths":{
      "post":{
        "consumes":[
          "application/vnd.validator.sample_api.test_post_req+json"
        ],
...

A continuación, necesitamos una clase base para nuestra clase de dispatch. Aquí está el código completo de esta clase; el código también está disponible en Git.

Class SwaggerValidator.Core.REST Extends %CSP.REST
{

Parameter UseSession As Integer = 1;
ClassMethod OnPreDispatch(pUrl As %String, pMethod As %String, ByRef pContinue As %Boolean) As %Status
{
	Set tSC = ..ValidateRequest()
    
    If $$$ISERR(tSC) {
        Do ..ReportHttpStatusCode(##class(%CSP.REST).#HTTP400BADREQUEST, tSC)
        Set pContinue = 0
    }

    Return $$$OK
}

ClassMethod ValidateRequest() As %Status
{
    Set tSC = ##class(%REST.API).GetApplication($REPLACE($CLASSNAME(),".disp",""), .spec)
    Return:$$$ISERR(tSC) tSC

    Set defName = $PIECE($PIECE(%request.ContentType, "+", 1), ".", *)
    Return:defName="" $$$ERROR($$$GeneralError, $$$FormatText("No definition name found in Content-Type = %1", %request.ContentType))
    
    Set type = spec.definitions.%Get(defName)
    Return:type="" $$$ERROR($$$GeneralError, $$$FormatText("No definition found in specification by name = %1", defName))
    
    Set schema = type.%ToJSON() 
    Set body = %request.Content.Read()

    Try {Set tSC = ..ValidateImpl(schema, body)} Catch ex {Set tSC = ex.AsStatus()}

    Return tSC
}

ClassMethod ValidateImpl(schema As %String, body As %String) As %Status [ Language = python ]
{
    try:
        validate(json.loads(body), json.loads(schema))
    except Exception as e:
        return iris.system.Status.Error(5001, f"Request body is invalid: {e}")

    return iris.system.Status.OK()
}

XData %import [ MimeType = application/python ]
{
import iris, json
from openapi_schema_validator import validate
}

}

Aquí estamos haciendo las siguientes cosas:

  1. Se sobrescribe OnPreDispatch() para añadir la validación. Este código se ejecutará en cada llamada a nuestra API.
  2. Se utiliza##class(%REST.API).GetApplication()para obtener la especificación en un objeto dinámico (JSON).
  3. Se extrae el nombre de la definición desde la cabecera Content-Type.
  4. Se obtiene el esquema de la solicitud mediante el nombre de la definición: spec.definitions.%Get(defName)
  5. Se envían el esquema de la solicitud y el cuerpo de la solicitud al código Python para su validación.

Como veis, todo es bastante sencillo. Ahora solo necesitáis cambiar la sección Extends de vuestra disp.clsa SwaggerValidator.Core.REST. Y, por supuesto, instalar la librería Python openapi-schema-validatoren el servidor (tal como se describe aquí).

Solución basada en openapi-core

Entradas clave:

  • Esta solución funciona con una interfaz REST codificada a mano. No usamos herramientas de API Management para generar el código a partir de la especificación OpenAPI. Solo tenemos un servicio REST como subclase de %CSP.REST.
  • Por lo tanto, no estamos limitados a la versión 2.0/JSON y utilizaremos OpenAPI 3.0 en formato YAML. Esta versión ofrece más posibilidades, y encuentro que YAML es más legible.
  • Se comprobarán los siguientes elementos: parámetros de ruta y consulta en la URL, Content-Type y cuerpo de la solicitud.

Para empezar, tomemos nuestra especificación ubicada en <servidor>/api/mgmnt/v1/<namespace>/spec/<aplicación-web>. Sí, tenemos una especificación OpenAPI generada incluso para APIs REST codificadas manualmente. Esta no es una especificación completa porque no contiene los esquemas de solicitudes y respuestas (el generador no sabe de dónde obtenerlos). Pero la plataforma ya ha hecho la mitad del trabajo por nosotros. Coloquemos esta especificación en un bloque XData llamado OpenAPI   en la clase Spec.cls A continuación, necesitamos convertir la especificación a formato OpenAPI 3.0/YAML y añadir definiciones para solicitudes y respuestas. Podéis usar un convertidor o simplemente pedirlo a Codex:

Por favor, convertid la especificación de la clase @Spec.clsa la versión Swagger 3.0 y al formato YAML.

De la misma manera, podemos pedir a Codex que genere los esquemas de solicitudes/respuestas basándose en ejemplos JSON.

Por cierto, el vibe coding funciona bastante bien en el desarrollo con IRIS, pero eso es un tema para otra ocasión. ¡Decidme si os resulta interesante!

Como en la solución anterior, debemos crear una clase base para nuestro %CSP.REST. Esta clase es muy similar:

Class SwaggerValidator.Core.RESTv2 Extends %CSP.REST
{

Parameter UseSession As Integer = 1;
ClassMethod OnPreDispatch(pUrl As %String, pMethod As %String, ByRef pContinue As %Boolean) As %Status
{
	Set tSC = ..ValidateRequest()
    
    If $$$ISERR(tSC) {
        Do ..ReportHttpStatusCode(##class(%CSP.REST).#HTTP400BADREQUEST, tSC)
        Set pContinue = 0
    }

    Return $$$OK
}

ClassMethod ValidateRequest() As %Status
{
    Set tSC = ..GetSpec(.swagger) 
    Return:$$$ISERR(tSC)||(swagger="") tSC

    Set canonicalURI = %request.CgiEnvs("REQUEST_SCHEME")_"://"_%request.CgiEnvs("HTTP_HOST")_%request.CgiEnvs("REQUEST_URI")
    Set httpBody = $SELECT($ISOBJECT(%request.Content)&&(%request.Content.Size>0):%request.Content.Read(), 1:"")
    Set httpMethod = %request.CgiEnvs("REQUEST_METHOD")
    Set httpContentType = %request.ContentType
    Try {
        Set tSC = ..ValidateImpl(swagger, canonicalURI, httpMethod, httpBody, httpContentType)
    } Catch ex {
        Set tSC = ex.AsStatus()
    }

    Return tSC
}

/// The class Spec.cls must be located in the same package as the %CSP.REST implementation
/// The class Spec.cls must contain an XData block named 'OpenAPI' with swagger 3.0 specification (in YAML format) 
ClassMethod GetSpec(Output specification As %String, xdataName As %String = "OpenAPI") As %Status
{
    Set specification = ""
    Set specClassName = $CLASSNAME()
    Set $PIECE(specClassName, ".", *) = "Spec"
    Return:'##class(%Dictionary.ClassDefinition).%Exists($LISTBUILD(specClassName)) $$$OK
    Set xdata = ##class(%Dictionary.XDataDefinition).%OpenId(specClassName_"||"_xdataName,,.tSC)
    If $$$ISOK(tSC),'$ISOBJECT(xdata)||'$ISOBJECT(xdata.Data)||(xdata.Data.Size=0) {
		Set tSC = $$$ERROR($$$RESTNoRESTSpec, xdataName, specClassName)
	}
    Return:$$$ISERR(tSC) tSC
    
    Set specification = xdata.Data.Read()
    Return tSC
}

ClassMethod ValidateImpl(swagger As %String, url As %String, method As %String, body As %String, contentType As %String) As %Status [ Language = python ]
{
    spec = Spec.from_dict(yaml.safe_load(swagger))
    data = json.loads(body) if (body != "") else None
    headers = {"Content-Type": contentType}
    
    req = requests.Request(method=method, url=url, json=data, headers=headers).prepare()
    openapi_req = RequestsOpenAPIRequest(req)

    try:
        validate_request(openapi_req, spec=spec)
    except Exception as ex:
        return iris.system.Status.Error(5001, f"Request validation failed: {ex.__cause__ if ex.__cause__ else ex}")

    return iris.system.Status.OK()
}

XData %import [ MimeType = application/python ]
{
import iris, json, requests, yaml
from openapi_core import Spec, validate_request
from openapi_core.contrib.requests import RequestsOpenAPIRequest
}

}

A tener en cuenta: una clase que contenga la especificación debe llamarse Spec.cls y estar ubicada en el mismo paquete que vuestra implementación %CSP.REST. La clase de especificación se ve así:

Class Sample.API.Spec Extends %RegisteredObject
{

XData OpenAPI [ MimeType = application/yaml ]
{
    ... your YAML specification ...
}
}

Para habilitar la validación, solo necesitáis extender vuestra clase de API heredando de SwaggerValidator.Core.RESTv2 y colocar el archivo Spec.cls junto a ella.

Eso es todo lo que quería contaros sobre la validación con Swagger. No dudéis en hacerme preguntas.

Comentarios (0)1
Inicie sesión o regístrese para continuar