tamasfe/aide

MissingPointerError: Token "AppError" does not exist in example

marclave opened this issue · 8 comments

great library! I noticed for the example swagger spec, it renders the spec below. However, the AppError response is undefined in the rendering of Redocly + Swagger UI since AppError doesnt exist inside of the Schemas for the OAS spec

there's also more errors on https://editor.swagger.io
image

image image
openapi: 3.1.0
info:
  title: Aide axum Open API
  summary: An example Todo application
  description: |
    # Todo API

    A very simple Todo server with documentation.

    The purpose is to showcase the documentation workflow of Aide rather
    than a correct implementation.
  version: ''
paths:
  /todo/:
    get:
      description: List all Todo items.
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoList'
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
    post:
      description: Create a new incomplete Todo item.
      requestBody:
        description: New Todo details.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewTodo'
        required: true
      responses:
        '201':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoCreated'
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
  /todo/{id}:
    get:
      description: Get a single Todo item.
      parameters:
        - in: path
          name: id
          description: The ID of the Todo.
          required: true
          schema:
            description: The ID of the Todo.
            type: string
            format: uuid
          style: simple
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TodoItem'
              example:
                complete: false
                description: fix bugs
                id: 00000000-0000-0000-0000-000000000000
        '404':
          description: todo was not found
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
    delete:
      description: Delete a Todo item.
      parameters:
        - in: path
          name: id
          description: The ID of the Todo.
          required: true
          schema:
            description: The ID of the Todo.
            type: string
            format: uuid
          style: simple
      responses:
        '204':
          description: The Todo has been deleted.
        '404':
          description: The todo was not found
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
  /todo/{id}/complete:
    put:
      description: Complete a Todo.
      parameters:
        - in: path
          name: id
          description: The ID of the Todo.
          required: true
          schema:
            description: The ID of the Todo.
            type: string
            format: uuid
          style: simple
      responses:
        '204':
          description: no content
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
  /docs/:
    get:
      description: This documentation page.
      responses:
        '200':
          description: HTML content
          content:
            text/html:
              schema:
                type: string
        default:
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppError'
              example:
                error: some error happened
                error_id: 00000000-0000-0000-0000-000000000000
      security:
        - ApiKey: []
components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-Auth-Key
      description: A key that is ignored.
  schemas:
    NewTodo:
      description: New Todo details.
      type: object
      required:
        - description
      properties:
        description:
          description: The description for the new Todo.
          type: string
    SelectTodo:
      type: object
      required:
        - id
      properties:
        id:
          description: The ID of the Todo.
          type: string
          format: uuid
    TodoCreated:
      description: New Todo details.
      type: object
      required:
        - id
      properties:
        id:
          description: The ID of the new Todo.
          type: string
          format: uuid
    TodoItem:
      description: A single Todo item.
      type: object
      required:
        - complete
        - description
        - id
      properties:
        complete:
          description: Whether the item was completed.
          type: boolean
        description:
          description: The description of the item.
          type: string
        id:
          type: string
          format: uuid
    TodoList:
      type: object
      required:
        - todo_ids
      properties:
        todo_ids:
          type: array
          items:
            type: string
            format: uuid
tags:
  - name: todo
    description: Todo Management

The errors at openapi and info , might be because the client does not support this version. If you click on the Try our new Editor button on their website, these two errors would not appear. This is also true for this error:

Structural error at paths./todo/search/{query}.get.parameters.1.schema.type
should be string

The other errors related to AppError not being included in components structure, seem to show a bug with default_response_with?

The errors at openapi and info , might be because the client does not support this version. If you click on the Try our new Editor button on their website, these two errors would not appear. This is also true for this error:


Structural error at paths./todo/search/{query}.get.parameters.1.schema.type

should be string

The other errors related to AppError not being included in components structure, seem to show a bug with default_response_with?

Yeah I think it's a bug with default_response_with since it doesn't get rendered in the schemas :)

@marclave Quick fix for now:

  • implement OperationOutput for AppError
impl aide::OperationOutput for AppError {
    type Inner = Self;

    fn operation_response(
        ctx: &mut aide::gen::GenContext,
        operation: &mut aide::openapi::Operation,
    ) -> Option<aide::openapi::Response> {
        <axum::Json<AppError> as aide::OperationOutput>::operation_response(ctx, operation)
    }

    fn inferred_responses(
        ctx: &mut aide::gen::GenContext,
        operation: &mut aide::openapi::Operation,
    ) -> Vec<(Option<u16>, aide::openapi::Response)> {
        if let Some(res) = Self::operation_response(ctx, operation) {
            vec![(None, res)]
        } else {
            Vec::new()
        }
    }
}

Note: vec![(None, res)], the None here means status code can be any status code, in other words, a default response.

  • In main use aide::gen::all_error_responses(true)
  • Use custom extractors, with axum_macros like so:
#[derive(FromRequestParts, OperationIo)]
#[from_request(via(axum::extract::Path), rejection(AppError))]
#[aide(
    input_with = "axum::extract::Path<T>",
    // output_with = "axum::extract::Path<T>", // not needed for path
    json_schema
)]
pub struct Path<T>(pub T);
  • Use Results on your routes like so:
async fn my_route(Path(input): Path<MyRouteInput>) -> impl IntoApiResponse {
  // you can retrun Ok(axum::Json(YourType));
  // YourType must implement `OperationOutput` if not using `axum::Json`
  Return Err(AppError::new("error"));
  // ofc you use other error types with their own `OperationOutput` implementation
}

Note axum::Json is used. I don't see a value in using the Json, in the example, as an output, but it is definitely useful as an input.

Amazing! Thanks so much

I am not sure if there is a reason default_response_with doesn't add a schema in components. Is it intentional?

An ugly modification of default_response_with, could be a breaking change:

    pub fn default_response_with<R, F>(mut self, transform: F) -> Self
    where
        R: OperationOutput + JsonSchema,
        F: Fn(TransformResponse<R::Inner>) -> TransformResponse<R::Inner> + Clone,
    {
        if let Some(p) = &mut self.api.paths {
            for (_, p) in &mut p.paths {
                let p = match p {
                    ReferenceOr::Reference { .. } => continue,
                    ReferenceOr::Item(p) => p,
                };

                for (_, op) in iter_operations_mut(p) {
                    let _ = TransformOperation::new(op)
                        .default_response_with::<R, F>(transform.clone());
                }
            }
        }

        if let Some(c) = &mut self.inner_mut().components {
            let mut schema = in_context(|ctx| {
                // ctx.schema.subschema_for::<R>()
                ctx.schema.root_schema_for::<R>()
            });
            let name = schema.schema.metadata().title.as_ref().unwrap().clone();
            // let name = type_name::<R>().to_owned();
            c.schemas.insert(
                name,
                crate::openapi::SchemaObject {
                    json_schema: schemars::schema::Schema::Object(schema.schema),
                    example: None,
                    external_docs: None,
                },
            );
        }

        self
    }

Honestly, not a big fan of TransformOperation and friends, they add so much confusion. The OperationOutput and OperationInput traits way of doing things make much more sense to me.

Wicpar commented

Edited. it is indeed not the examples responsibility to set the schema, will post a fix tomorrow.

Wicpar commented

if extract shema is set to false in the example it works. The bug comes from Jsonschema that knows it must extract the schema and it thinks it is already being extracted somewhere else and thus just places the rest.

Wicpar commented

Well, turns out it was in finish_api_with, the generated paths were necessary for applying the default responses, but the schema extraction and context reset happened before the trasform function was applied.

Fix is in master.