litestar icon indicating copy to clipboard operation
litestar copied to clipboard

Enhancement: Generate `raises` `HTTPException`s as OpenAPI components

Open tuukkamustonen opened this issue 2 years ago • 9 comments

Summary

@get(raises=[NotFoundException])
def endpoint(): ...

...seems to inject each declared raises entry as duplication into the generated OpenAPI spec:

          "404": {
            "description": "Nothing matches the given URI",
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "status_code": {
                      "type": "integer"
                    },
                    "detail": {
                      "type": "string"
                    },
                    "extra": {
                      "additionalProperties": {},
                      "type": [
                        "null",
                        "object",
                        "array"
                      ]
                    }
                  },
                  "type": "object",
                  "required": [
                    "detail",
                    "status_code"
                  ],
                  "description": "Not Found Exception",
                  "examples": {
                    "NotFoundException": {
                      "value": {
                        "status_code": 404,
                        "detail": "Not Found",
                        "extra": {}
                      }
                    }
                  }
                }
              }
            }
          },

That's quite lengthy, consider 100 endpoints and it's going to be there 100 times.

Basic Example

Could these be injected into the components section, rather:

    "components": {
        "schemas": {
            "NotFoundException": {
              "description": "Not Found Exception",
              "type": "object",
              "properties": {
                "status_code": {
                  "type": "integer"
                },
                "detail": {
                  "type": "string"
                },
                "extra": {
                  "additionalProperties": {},
                  "type": [
                    "null",
                    "object",
                    "array"
                  ]
                }
              },
              "required": [
                "detail",
                "status_code"
              ],
              "examples": {
                "NotFoundException": {
                  "value": {
                    "status_code": 404,
                    "detail": "Not Found",
                    "extra": {}
                  }
                }
              }

Then the references much become much shorter:

                    "404": {
                        "description": "Nothing matches the given URI",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/NotFoundException"
                                }
                            }
                        }
                    },

Drawbacks and Impact

No response

Unresolved questions

No response


[!NOTE]
While we are open for sponsoring on GitHub Sponsors and OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.

Check out all issues funded or available for funding on our Polar.sh dashboard

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
  • This, along with engagement in the community, helps us know which features are a priority to our users.
Fund with Polar

tuukkamustonen avatar Jan 24 '24 10:01 tuukkamustonen

@guacs

provinzkraut avatar Jan 24 '24 17:01 provinzkraut

I think this seems reasonable.

I think there was some discussion related to this with @peterschutt. I think his idea was to move everything into components (i.e. all complex types including user defined types) and only have references to them within the schema.

vkcku avatar Jan 25 '24 00:01 vkcku

Nah, I want to go the other way with the DTO schemas that are 1 to 1 with a route. Currently we end up with referenced schema's for types like CreateThingReturnModel, UpdateThingDataModel etc etc, and would rather we just inline those.

I'm all for putting duplicated schemas into components though.

peterschutt avatar Jan 25 '24 00:01 peterschutt

Aah, I misremembered then. Was there some reason you wanted to inline them?

vkcku avatar Jan 25 '24 00:01 vkcku

It is that we have to come up with these long unique identifying model names for the DTO generated models that means that the named components get cluttered with names like CreateUserUserRequestBody, for every data and return type for which a DTO is applied, and then the reference to the named component is only used once anyway. This is the logic to create the model names:

    def _create_transfer_model_name(self, model_name: str) -> str:
        long_name_prefix = self.handler_id.split("::")[0]
        short_name_prefix = _camelize(long_name_prefix.split(".")[-1], True)

        name_suffix = "RequestBody" if self.is_data_field else "ResponseBody"

        if (short_name := f"{short_name_prefix}{model_name}{name_suffix}") not in self._seen_model_names:
            name = short_name
        elif (long_name := f"{long_name_prefix}{model_name}{name_suffix}") not in self._seen_model_names:
            name = long_name
        else:
            name = unique_name_for_scope(long_name, self._seen_model_names)

        self._seen_model_names.add(name)

        return name

peterschutt avatar Jan 25 '24 00:01 peterschutt

That makes sense to me. How about we move everything to components except for DTOs created models? The reason I was thinking of moving everything into the components was because we don't really have a way of knowing ahead of time if a model is used only once or multiple times.

Also, should we just do the inlining even if DTOs are used across multiple handlers?

vkcku avatar Jan 25 '24 02:01 vkcku

Also, should we just do the inlining even if DTOs are used across multiple handlers?

I'm pretty sure that we don't make any effort to de-duplicate dto transfer models, that is, for the same dto type, applied to the same data model type across multiple handlers will create multiple transfer models that have the handler's id somewhere in the model name. So they still show as multiple components even though its the same DTO type, with same config and same applied model.

So I'd say yes, unless we change the above (or I've misremembered).

peterschutt avatar Jan 27 '24 09:01 peterschutt

Okay so we can go with the following implementation?

  • inline DTOs
  • other object response types should be kept as references by default
    • it would be nice if we could do this only for the types that are reused, but I'm not sure if we can figure that out ahead of time

Another option is to special case exceptions (i.e. all the exceptions become references by default since they'll be reused in almost all the routes). In this case, we keep the behavior for the object response types as it is currently.

vkcku avatar Feb 05 '24 05:02 vkcku

Okay so we can go with the following implementation?

* inline DTOs

* other object response types should be kept as references by default
  
  * it would be nice if we could do this only for the types that are reused, but I'm not sure if we can figure that out ahead of time

Sounds good to me

provinzkraut avatar Feb 05 '24 18:02 provinzkraut