GREsau / okapi

OpenAPI (AKA Swagger) document generation for Rust projects
MIT License
634 stars 112 forks source link

How to add custom paths #82

Closed antondc closed 12 months ago

antondc commented 2 years ago

I have a working example with one route and custom schema:

use rocket::get;
use rocket::{Build, Rocket};
use rocket_okapi::okapi::openapi3::OpenApi;
use rocket_okapi::openapi;
use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
use rocket_okapi::{mount_endpoints_and_merged_docs, openapi_get_routes, swagger_ui::*};

#[openapi] // Let okapi know that we want to document this endpoing
#[get("/test_route")]
fn test_route() -> &'static str {
  "test_route"
}

pub fn get_routes_and_docs(settings: &OpenApiSettings) -> (Vec<rocket::Route>, OpenApi) {
  openapi_get_routes_spec![settings: test_route]
}

fn custom_openapi_spec() -> OpenApi {
  use rocket_okapi::okapi::openapi3::*;
  OpenApi {
    openapi: OpenApi::default_version(),
    info: Info {
      title: "The best API ever".to_owned(),
      description: Some("This is the best API every, please use me!".to_owned()),
      version: "1.2.3".to_owned(),
      ..Default::default()
    },
    servers: vec![Server {
      url: "http://127.0.0.1:8000/".to_owned(),
      description: Some("Localhost".to_owned()),
      ..Default::default()
    }],
    // paths: {
    // ...
    // },
    ..Default::default()
  }
}

pub fn create_server() -> Rocket<Build> {
  let mut building_rocket = rocket::build()
    .mount("/", openapi_get_routes![test_route])
    .mount(
      "/swagger-ui/",
      make_swagger_ui(&SwaggerUIConfig {
        url: "../v1/openapi.json".to_owned(),
        ..Default::default()
      }),
    );

  let openapi_settings = rocket_okapi::settings::OpenApiSettings::default();
  let custom_route_spec = (vec![], custom_openapi_spec());
  mount_endpoints_and_merged_docs! {
      building_rocket, "/v1".to_owned(), openapi_settings,
      "/" => custom_route_spec,
  };

  building_rocket
}

#[rocket::main]
async fn main() {
  let launch_result = create_server().launch().await;
  match launch_result {
    Ok(()) => println!("Rocket shut down gracefully."),
    Err(err) => println!("Rocket had an error: {}", err),
  };
}

I am looking for a way to add paths into my schema, so I don't necessarily depend on the structs provided by the app. Cant see the way, and there are no examples in the library: any help will be welcome.

ralpha commented 2 years ago

You are already on the right track. You can just add the paths to the custom spec:

fn custom_openapi_spec() -> OpenApi {
  use indexmap::indexmap;
  use rocket_okapi::okapi::openapi3::*;
  use rocket_okapi::okapi::schemars::schema::*;
  OpenApi {
    openapi: OpenApi::default_version(),
    info: Info {
      title: "The best API ever".to_owned(),
      description: Some("This is the best API every, please use me!".to_owned()),
      version: "1.2.3".to_owned(),
      ..Default::default()
    },
    servers: vec![Server {
      url: "http://127.0.0.1:8000/".to_owned(),
      description: Some("Localhost".to_owned()),
      ..Default::default()
    }],
    // Add paths that do not exist in Rocket (or add extra into to existing paths)
    paths: {
        indexmap! {
            "home".to_owned() => PathItem{
            get: Some(
                Operation {
                tags: vec!["HomePage".to_owned()],
                summary: Some("This is my homepage".to_owned()),
                responses: Responses{
                    responses: indexmap!{
                    "200".to_owned() => RefOr::Object(
                        Response{
                        description: "Return the page, no error.".to_owned(),
                        content: indexmap!{
                            "text/html".to_owned() => MediaType{
                            schema: Some(SchemaObject{
                                instance_type: Some(SingleOrVec::Single(Box::new(
                                InstanceType::String
                                ))),
                                ..Default::default()
                            }),
                            ..Default::default()
                            }
                        },
                        ..Default::default()
                        }
                    )
                    },
                    ..Default::default()
                },
                ..Default::default()
                }
            ),
            ..Default::default()
            }
        }
    },
    ..Default::default()
  }
}

The easiest way to figure out the structure is to look at the docs. OpenApi -> paths: Map<String, PathItem>

Map is the Schemars::Map. This one depends on the feature flags you use:

#[cfg(not(feature = "preserve_order"))]
pub type Map<K, V> = std::collections::BTreeMap<K, V>;
#[cfg(feature = "preserve_order")]
pub type Map<K, V> = indexmap::IndexMap<K, V>;

Source: https://github.com/GREsau/schemars/blob/9464118c3a3894c95037b3e25cb3ac9116eaf849/schemars/src/lib.rs#L292-L295 In the example above I'll use IndexMap (as this is usually the case), I will use the macro: https://docs.rs/indexmap/latest/indexmap/macro.indexmap.html (to make it easier)

This is basically a 1 to 1 mapping between the json output and the structure (except for some small exceptions).

I hope this helps, if you have more questions let me know.

I also add this code to the example for future reference: https://github.com/GREsau/okapi/blob/9a57d2f716ba57c472624a4fdd79f7feebdd2b43/examples/custom_schema/src/main.rs#L100-L136

ralpha commented 12 months ago

Code example solves this issue. Closing issue.