Herencia y composición de Swagger

81

En mi API "simplificada", todas las respuestas se derivan ( heredan ) de una clase de "respuesta" base. La clase de respuesta se compone de un encabezado lleno de metadatos y el cuerpo que contiene los datos básicos que solicita el usuario. La respuesta (en JSON) se presenta de manera que todos los metadatos están en la primera "capa" y el cuerpo es un único atributo llamado "cuerpo" como tal

response
|--metadata attribute 1 (string/int/object)
|--metadata attribute 2 (string/int/object)
|--body (object)
    |--body attribute 1 (string/int/object)
    |--body attribute 2 (string/int/object)

He intentado definir esta relación con arrogancia con el siguiente JSON:

{
    ...
    "definitions": {
        "response": {
            "allOf": [
                {
                    "$ref": "#/definitions/response_header"
                },
                {
                    "properties": {
                        "body": {
                            "description": "The body of the response (not metadata)",
                            "schema": {
                                "$ref": "#/definitions/response_body"
                            }
                        }
                    }
                }
            ]
        },
        "response_header": {
            "type": "object",
            "required": [
                "result"
            ],
            "properties": {
                "result": {
                    "type": "string",
                    "description": "value of 'success', for a successful response, or 'error' if there is an error",
                    "enum": [
                        "error",
                        "success"
                    ]
                },
                "message": {
                    "type": "string",
                    "description": "A suitable error message if something went wrong."
                }
            }
        },
        "response_body": {
            "type": "object"
        }
    }
}

Luego trato de crear diferentes respuestas creando las diversas clases de cuerpo / encabezado que heredan del cuerpo / encabezado, y luego creo clases de respuesta secundaria que se componen de las clases de encabezado / cuerpo relevantes (que se muestran en el código fuente en la parte inferior). Sin embargo, estoy seguro de que esta es la forma incorrecta de hacer las cosas o que mi implementación es incorrecta. No he podido encontrar un ejemplo de herencia en la especificación swagger 2.0 (que se muestra a continuación) pero encontré un ejemplo de composición .

ingrese la descripción de la imagen aquí

Estoy bastante seguro de que este "discriminador" tiene un papel importante que desempeñar, pero no estoy seguro de lo que tengo que hacer.

Pregunta

¿Podría alguien mostrarme cómo se supone que uno debe implementar la composición + herencia en swagger 2.0 (JSON), preferiblemente "arreglando" mi código de ejemplo a continuación? También sería genial si pudiera especificar una clase ErrorResponse que herede de la respuesta donde el atributo "resultado" en el encabezado siempre se establece en "error".

{
    "swagger": "2.0",
    "info": {
        "title": "Test API",
        "description": "Request data from the system.",
        "version": "1.0.0"
    },
    "host": "xxx.xxx.com",
    "schemes": [
        "https"
    ],
    "basePath": "/",
    "produces": [
        "application/json"
    ],
    "paths": {
        "/request_filename": {
            "post": {
                "summary": "Request Filename",
                "description": "Generates an appropriate filename for a given data request.",
                "responses": {
                    "200": {
                        "description": "A JSON response with the generated filename",
                        "schema": {
                            "$ref": "#/definitions/filename_response"
                        }
                    }
                }
            }
        }
    },
    "definitions": {
        "response": {
            "allOf": [
                {
                    "$ref": "#/definitions/response_header"
                },
                {
                    "properties": {
                        "body": {
                            "description": "The body of the response (not metadata)",
                            "schema": {
                                "$ref": "#/definitions/response_body"
                            }
                        }
                    }
                }
            ]
        },
        "response_header": {
            "type": "object",
            "required": [
                "result"
            ],
            "properties": {
                "result": {
                    "type": "string",
                    "description": "value of 'success', for a successful response, or 'error' if there is an error",
                    "enum": [
                        "error",
                        "success"
                    ]
                },
                "message": {
                    "type": "string",
                    "description": "A suitable error message if something went wrong."
                }
            }
        },
        "response_body": {
            "type": "object"
        },
        "filename_response": {
            "extends": "response",
            "allOf": [
                {
                    "$ref": "#definitions/response_header"
                },
                {
                    "properties": {
                        "body": {
                            "schema": {
                                "$ref": "#definitions/filename_response_body"
                            }
                        }
                    }
                }
            ]
        },
        "filename_response_body": {
            "extends": "#/definitions/response_body",
            "properties": {
                "filename": {
                    "type": "string",
                    "description": "The automatically generated filename"
                }
            }
        }
    }
}

Actualización del diagrama

Para tratar de aclarar lo que quiero, he creado el diagrama muy básico a continuación que tiene como objetivo mostrar que todas las respuestas son instancias del objeto "respuesta" que han sido construidas por (composición) usando cualquier combinación de objetos response_header y response_body. Los objetos response_header y response_body pueden extenderse e insertarse en cualquier objeto de respuesta, lo que se hace en el caso de un filename_response que usa el hijo filename_response_body de la clase base response_body. Tanto el error como las respuestas correctas utilizan el objeto "respuesta".

ingrese la descripción de la imagen aquí

Programador
fuente
1
No es una muestra de la composición, pero es tan malo que no vale la pena compartir. Trabajaré en cómo debería verse su especificación. Tenga en cuenta que la interfaz de usuario actualmente no lo admite, pero lo hará cuando esté disponible el soporte completo para 2.0.
Ron
1
Y antes de sumergirme, una cosa más: ¿estás buscando composición o herencia? La composición es básicamente decir I have the properties of X and my own properties.. La herencia sugiere una relación X is my parent. I have its properties and my own.. La herencia es útil si quiere decir que un determinado conjunto de modelos son aplicables del padre que se está utilizando.
Ron
1
Más bien esperaba demostrar el uso de la herencia y la composición de una sola vez con este ejemplo. Obviamente, me doy cuenta de que uno podría usar fácilmente cualquiera de los dos por sí solo, pero en este caso todas las respuestas son hijos de la clase base "respuesta". Y la clase de respuesta está "compuesta" por otros dos objetos, el encabezado y el cuerpo.
Programster
2
Puede que no haya sido claro. La herencia es una extensión de la composición. Si hay herencia, hay composición. Si hay composición, no necesariamente hay herencia. Además, en su muestra, el modelo de "respuesta" no se utiliza en ninguna parte. ¿Debo ignorar eso y simplemente mostrar cómo debería verse?
Ron
ah, no me di cuenta de esa relación entre herencia y composición. Así que usa la herencia para mostrar ambos. En lo que respecta al modelo de respuesta que no se está utilizando, debe utilizarse con las "extensiones" en el hijo filename_response con el que responde la solicitud.
Programster

Respuestas:

113

Como principiante en fanfarronería, no encuentro la documentación oficial sobre polimorfismo y composición fácil de entender, porque carece de un ejemplo . Cuando busqué en la red, hay muchos buenos ejemplos que se refieren a swagger 1.2 cuando extendsera válido.

Para swagger 2.0 encontré un buen ejemplo en las fuentes de especificaciones de swagger en github a través de este grupo de Google

Según las fuentes anteriores, aquí hay un breve ejemplo de herencia válida en YAML:

definitions:
  Pet:
    discriminator: petType
    required:
      - name
      - petType # required for inheritance to work
    properties:
      name: 
        type: string
      petType:
        type: string
  Cat:
    allOf:
      - $ref: '#/definitions/Pet' # Cat has all properties of a Pet
      - properties: # extra properties only for cats
          huntingSkill:
            type: string
            default: lazy
            enum:
              - lazy
              - aggressive
  Dog:
    allOf:
      - $ref: '#/definitions/Pet' # Dog has all properties of a Pet
      - properties: # extra properties only for dogs
          packSize:
            description: The size of the pack the dog is from
            type: integer
Tomasz Sętkowski
fuente
¡Realmente gracias! Esto funciona para mi. En editor.swagger.io, veo un pequeño error: en la sección de modelos, veo el Petmodelo varias veces. El contenido de estos modelos está bien. Solo los nombres están equivocados.
schellingerht
@schellingerht En editor2.swagger.iono verá este problema
Shiplu Mokaddim
El único problema que encontré con esta forma de definir la herencia es que la propiedad petType es un poco inútil en la clase generada. Estará vacío. Pero al menos genera la jerarquía de clases como pensé que lo haría. ¡Gracias!
xarlymg89
Para crear el json de herencia como se indicó anteriormente, debe anotar sus clases de padres e hijos de la siguiente manera: @ApiModel (discriminator = "type", subTypes = {Cat.class, Dog.class}) public abstract class Animal {} @ ApiModel (parent = Animal.class) public calss Cat extiende Animal {}
Janet
¿El discriminador se usa solo cuando implementamos una interfaz? Pet¿Qué tal si la clase A extiende la clase B, también deberíamos usarla? Gracias
Bionix1441
23

Descubrí que la composición funciona bien incluso sin una definición de discriminator.

Por ejemplo, base Response:

definitions:
  Response:
    description: Default API response
    properties:
      status:
        description: Response status `success` or `error`
        type: string
        enum: ["success", "error"]
      error_details:
        description: Exception message if called
        type: ["string", "object", "null"]
      error_message:
        description: Human readable error message
        type: ["string", "null"]
      result:
        description: Result body
        type: ["object", "null"]
      timestamp:
        description: UTC timestamp in ISO 8601 format
        type: string
    required:
      - status
      - timestamp
      - error_details
      - error_message
      - result

Se representa como:

Visualización de respuesta

Y podemos extenderlo para refinar el esquema personalizado de resultcampo:

  FooServiceResponse:
    description: Response for Foo service
    allOf:
      - $ref: '#/definitions/Response'
      - properties:
          result:
            type: object
            properties:
              foo_field:
                type: integer
                format: int32
              bar_field:
                type: string
        required:
          - result

Y se representará correctamente como:

Visualización de FooServiceResponse

Tenga en cuenta que eso allOfes suficiente para que esto funcione y no discriminatorse utilice ningún campo. Esto es bueno, porque funciona y esto es importante, creo que las herramientas podrán generar código sin discriminatorcampo.

oblalex
fuente
También he usado allOf, pero de alguna manera en el openapi.yaml, encuentro que las subclases contienen las propiedades de la superclase de manera redundante, ¿es correcto?
Bionix1441
8

Todas las respuestas aquí ya son excelentes, pero solo quiero agregar una nota menor sobre la composición versus la herencia . Según Swagger / OpenAPI Spec , para implementar la composiciónallOf basta con usar la propiedad, como bien señala @oblalex . Sin embargo, para implementar la herencia , debe usar allOfcon discriminator, como en el ejemplo de @ TomaszSętkowski .

Además, encontré algunos ejemplos más de Swagger tanto de composición como de herencia en API Handyman. Son parte de una excelente serie de tutoriales Swagger / OpenAPI de Arnaud Lauret que creo que todos deberían ver.

DynamicDispatch
fuente
1
@Aunque publicar enlaces relevantes es un buen comienzo, para ser una respuesta útil, también debes citar el texto relevante que se encontrará en el enlace. No se recomiendan las respuestas de solo enlace, ya que los enlaces se desconectan con frecuencia.
Stijn de Witt
3

El ejemplo estándar de Swagger 2.0 que ha compartido muestra una relación de composición, específicamente captura una relación "es una especie de" supertipo / subtipo, sin embargo, no es polimorfismo en sí mismo.

Lo sería si pudiera hacer referencia a la definición base de Pet como un parámetro de entrada, luego elegir Cat o ingresar un objeto Cat JSON como el valor para la solicitud de entrada, y tener esto aceptable para Swagger UI.

No pude hacer que esto funcione directamente.

Lo mejor que pude hacer fue establecer additionalProperties en verdadero en el objeto base (por ejemplo, Pet), especificar Pet usando la referencia de puntero JSON como esquema de entrada y finalmente copiar y pegar mi objeto de valor Cat JSON en Swagger UI. Dado que las propiedades adicionales están permitidas, Swagger UI generó una carga útil de solicitud de entrada válida.

usuario6317389
fuente
No hace polimorfismo a través del cable (ni expone sus entidades de datos) ... a menos que desee acoplarlo estrechamente al truco específico para que funcione.
user1496062
El polimorfismo está habilitado por herencia, pero no es necesario para usar la herencia. Lógicamente, la herencia es una relación "es-un" mientras que la composición es una relación "tiene-un". La línea entre los dos puede ser borrosa según el lenguaje de implementación y los casos de uso del dominio. Pero ese es el punto de partida. Fwiw, el discriminador permite la deserialización de tipos polimórficos. Hay otros enfoques (por ejemplo, incluidos los nombres de clases de Java). Pero, de acuerdo, estos pueden ser torpes y no portátiles. Por ejemplo, ¿qué hará un cliente de Python con los nombres de clases de Java?
Charlie Reitzel