Skip to content

Commit

Permalink
Improve openapi documentation (#103)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucemans authored Apr 17, 2024
1 parent fcf86f9 commit 76c120b
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 35 deletions.
1 change: 1 addition & 0 deletions server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added server/src/docs/html/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title>API Reference</title>
<title>enstate.rs - Lightweight ENS API</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- favicon -->
<link rel="icon" href="/docs/favicon.png">
</head>
<body>
<script
Expand Down
31 changes: 27 additions & 4 deletions server/src/docs/mod.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
use crate::models::bulk::{BulkResponse, ListResponse};
use crate::models::error::ErrorResponse;
use crate::models::profile::ENSProfile;
use utoipa::openapi::{ExternalDocs, License};
use enstate_shared::meta::AppMeta;
use enstate_shared::utils::vec;
use utoipa::openapi::{ExternalDocs, License, Tag};
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(
info(
title = "enstate.rs",
description = "A hosted ENS API allowing for easy access to ENS data.",
description = "A hosted ENS API allowing for easy access to ENS data.\n\nYou can checkout the source code, contribute, or host a version for yourself at [v3xlabs/enstate](https://github.com/v3xlabs/enstate).",
),
paths(
crate::routes::address::get, crate::routes::name::get, crate::routes::universal::get,
crate::routes::address::get_bulk, crate::routes::name::get_bulk, crate::routes::universal::get_bulk
crate::routes::address::get_bulk, crate::routes::name::get_bulk, crate::routes::universal::get_bulk,
crate::routes::address::get_bulk_sse, crate::routes::name::get_bulk_sse, crate::routes::universal::get_bulk_sse,
crate::routes::header::get,
crate::routes::image::get,
crate::routes::root::get,
),
components(schemas(ENSProfile, ListResponse<BulkResponse<ENSProfile>>, ErrorResponse))
components(schemas(ENSProfile, ListResponse<BulkResponse<ENSProfile>>, ErrorResponse, AppMeta))
)]
pub struct ApiDoc;

Expand All @@ -26,5 +32,22 @@ pub async fn openapi() -> String {
doc.info.license = Some(license);
doc.external_docs = Some(ExternalDocs::new("https://github.com/v3xlabs/enstate"));

let mut tag1 = Tag::default();
tag1.name = "Single Profile".to_string();
tag1.description = Some("If you want to resolve a single ENS Name / Profile you can do a single lookup in one of the following ways.".to_string());
let mut tag2 = Tag::default();
tag2.name = "Bulk Profiles".to_string();
tag2.description = Some("In some cases you might want to resolve a list of names or addresses. In this case you can use the bulk endpoints. This endpoint waits for all names to be resolved before returning a result.\n\nNote: You might prefer to use the [SSE Streaming Endpoints](#tag/stream-profiles) for a more responsive feel.".to_string());
let mut tag3 = Tag::default();
tag3.name = "Stream Profiles".to_string();
tag3.description = Some("In some cases you might want to resolve a list of names or addresses but have access to the results immediately. This endpoint returns its output via [Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) as they are computed.\n\nNote: If you are looking for a simpler solution that return result aggregates checkout [Bulk Endpoints](#tag/bulk-profiles).".to_string());
let mut tag4 = Tag::default();
tag4.name = "Avatars & Banners".to_string();
tag4.description = Some("To save you the hassle of loading profiles, and extracting json fields, we have made a few endpoints that will make it easy for you to directly use image urls in your app.".to_string());
let mut tag5 = Tag::default();
tag5.name = "Deployment Information".to_string();

doc.tags = Some(vec![tag1, tag2, tag3, tag4, tag5]);

doc.to_json().unwrap()
}
15 changes: 12 additions & 3 deletions server/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ impl App {
}

pub fn setup(state: AppState) -> App {
let docs = Router::new()
.route("/openapi.json", get(crate::docs::openapi))
.route("/", get(scalar_handler))
.route("/favicon.png", get(scalar_favicon_handler));

let router = Router::new()
.route(
"/",
get(|| async { Redirect::temporary("/docs") }),
)
.route("/docs", get(scalar_handler))
.route("/docs/openapi.json", get(crate::docs::openapi))
.nest("/docs", docs)
.route("/this", get(routes::root::get))
.route("/a/:address", get(routes::address::get))
.route("/n/:name", get(routes::name::get))
Expand All @@ -72,6 +76,11 @@ pub fn setup(state: AppState) -> App {

// Loads from docs/index.html with headers html
async fn scalar_handler() -> Html<&'static str> {
let contents = include_str!("./docs/index.html");
let contents = include_str!("./docs/html/index.html");
axum::response::Html(contents)
}

async fn scalar_favicon_handler() -> impl axum::response::IntoResponse {
let contents = include_bytes!("./docs/html/favicon.png");
contents
}
16 changes: 16 additions & 0 deletions server/src/routes/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::routes::{

#[utoipa::path(
get,
tag = "Single Profile",
path = "/a/{address}",
responses(
(status = 200, description = "Successfully found address.", body = ENSProfile),
Expand Down Expand Up @@ -68,6 +69,7 @@ pub struct AddressGetBulkQuery {

#[utoipa::path(
get,
tag = "Bulk Profiles",
path = "/bulk/a",
responses(
(status = 200, description = "Successfully found address.", body = BulkResponse<ENSProfile>),
Expand Down Expand Up @@ -106,6 +108,20 @@ pub async fn get_bulk(
Ok(Json(joined))
}

#[utoipa::path(
get,
tag = "Stream Profiles",
path = "/sse/a",
responses(
(status = 200, description = "Successfully found address.", body = BulkResponse<ENSProfile>),
(status = BAD_REQUEST, description = "Invalid address.", body = ErrorResponse),
(status = NOT_FOUND, description = "No name was associated with this address.", body = ErrorResponse),
(status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse),
),
params(
("addresses[]" = Vec<String>, Query, description = "Addresses to lookup name data for"),
)
)]
pub async fn get_bulk_sse(
Qs(query): Qs<AddressGetBulkQuery>,
State(state): State<Arc<crate::AppState>>,
Expand Down
37 changes: 24 additions & 13 deletions server/src/routes/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,30 @@ use enstate_shared::models::lookup::ENSLookup;

use crate::routes::{FreshQuery, http_simple_status_error, profile_http_error_mapper, RouteError};

// #[utoipa::path(
// get,
// path = "/h/{name_or_address}",
// responses(
// TODO: figure out body
// (status = 200, description = "Successfully found name or address.", body = ENSProfile),
// (status = NOT_FOUND, description = "No name or address could be found."),
// (status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse),
// ),
// params(
// ("name_or_address" = String, Path, description = "Name or address to lookup the header for."),
// )
// )]
/// Banner Endpoint
///
/// This is the endpoint for getting an avatar image.
/// It performs some pre-compute on the image to ensure it is `<img />` tag friendly.
///
/// To use in your app, you can use the following HTML:
/// ```html
/// <img src="https://enstate.rs/i/luc.eth" alt="luc.eth" />
/// ```
///
/// Note: you should probably still have a fallback image in case the image is not found.
#[utoipa::path(
head,
tag = "Avatars & Banners",
path = "/h/{name_or_address}",
responses(
(status = 303, description = "Redirects to the header image."),
(status = NOT_FOUND, description = "No name or address could be found."),
(status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse),
),
params(
("name_or_address" = String, Path, description = "Name or address to lookup the header for."),
)
)]
pub async fn get(
Path(name_or_address): Path<String>,
Query(query): Query<FreshQuery>,
Expand Down
37 changes: 24 additions & 13 deletions server/src/routes/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,30 @@ use enstate_shared::models::lookup::ENSLookup;

use crate::routes::{FreshQuery, http_simple_status_error, profile_http_error_mapper, RouteError};

// #[utoipa::path(
// get,
// path = "/i/{name_or_address}",
// responses(
// TODO: figure out body
// (status = 200, description = "Successfully found name or address.", body = ENSProfile),
// (status = NOT_FOUND, description = "No name or address could be found."),
// (status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse),
// ),
// params(
// ("name_or_address" = String, Path, description = "Name or address to lookup the image for."),
// )
// )]
/// Avatar Endpoint
///
/// This is the endpoint for getting an avatar image.
/// It performs some pre-compute on the image to ensure it is `<img />` tag friendly.
///
/// To use in your app, you can use the following HTML:
/// ```html
/// <img src="https://enstate.rs/i/luc.eth" alt="luc.eth" />
/// ```
///
/// Note: you should probably still have a fallback image in case the image is not found.
#[utoipa::path(
head,
tag = "Avatars & Banners",
path = "/i/{name_or_address}",
responses(
(status = 303, description = "Redirects to the avatar image."),
(status = NOT_FOUND, description = "No name or address could be found."),
(status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse),
),
params(
("name_or_address" = String, Path, description = "Name or address to lookup the image for."),
)
)]
pub async fn get(
Path(name_or_address): Path<String>,
Query(query): Query<FreshQuery>,
Expand Down
14 changes: 14 additions & 0 deletions server/src/routes/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::routes::{profile_http_error_mapper, validate_bulk_input, FreshQuery,

#[utoipa::path(
get,
tag = "Single Profile",
path = "/n/{name}",
responses(
(status = 200, description = "Successfully found name.", body = ENSProfile),
Expand Down Expand Up @@ -61,6 +62,7 @@ pub struct NameGetBulkQuery {

#[utoipa::path(
get,
tag = "Bulk Profiles",
path = "/bulk/n",
responses(
(status = 200, description = "Successfully found name.", body = ListButWithLength<BulkResponse<Profile>>),
Expand Down Expand Up @@ -90,6 +92,18 @@ pub async fn get_bulk(
Ok(Json(joined))
}

#[utoipa::path(
get,
tag = "Stream Profiles",
path = "/sse/n",
responses(
(status = 200, description = "Successfully found name.", body = ListButWithLength<BulkResponse<Profile>>),
(status = NOT_FOUND, description = "No name could be found.", body = ErrorResponse),
),
params(
("names[]" = Vec<String>, Query, description = "Names to lookup name data for"),
)
)]
pub async fn get_bulk_sse(
Qs(query): Qs<NameGetBulkQuery>,
State(state): State<Arc<crate::AppState>>,
Expand Down
11 changes: 11 additions & 0 deletions server/src/routes/root.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
use axum::Json;
use enstate_shared::meta::{AppMeta, APP_META};

/// Me Endpoint
///
/// This Endpoint returns the build information of the running process.
#[utoipa::path(
get,
tag = "Deployment Information",
path = "/this",
responses(
(status = 200, description = "", body = AppMeta),
)
)]
#[allow(clippy::unused_async)]
pub async fn get() -> Json<AppMeta> {
Json(APP_META.clone())
Expand Down
15 changes: 15 additions & 0 deletions server/src/routes/universal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::routes::{profile_http_error_mapper, validate_bulk_input, FreshQuery,

#[utoipa::path(
get,
tag = "Single Profile",
path = "/u/{name_or_address}",
responses(
(status = 200, description = "Successfully found name or address.", body = ENSProfile),
Expand Down Expand Up @@ -64,6 +65,7 @@ pub struct UniversalGetBulkQuery {

#[utoipa::path(
get,
tag = "Bulk Profiles",
path = "/bulk/u",
responses(
(status = 200, description = "Successfully found name or address.", body = BulkResponse<ENSProfile>),
Expand Down Expand Up @@ -92,6 +94,19 @@ pub async fn get_bulk(
Ok(Json(joined))
}

#[utoipa::path(
get,
tag = "Stream Profiles",
path = "/sse/u",
responses(
(status = 200, description = "Successfully found name or address.", body = BulkResponse<ENSProfile>),
(status = NOT_FOUND, description = "No name or address could be found.", body = ErrorResponse),
(status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse),
),
params(
("queries[]" = Vec<String>, Query, description = "Names to lookup name data for"),
)
)]
pub async fn get_bulk_sse(
Qs(query): Qs<UniversalGetBulkQuery>,
State(state): State<Arc<crate::AppState>>,
Expand Down
25 changes: 25 additions & 0 deletions shared/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ getrandom = { version = "0.2", features = ["js"] }
wasm-bindgen-futures = "0.4.36"
wasm-bindgen = { version = "0.2.86", features = ["serde-serialize"] }
web-sys = { version = "0.3.63", features = ["console"] }
utoipa = "4.2.0"

[dev-dependencies]
tokio = { version = "1", features = ["full"] }
Expand Down
7 changes: 6 additions & 1 deletion shared/src/meta.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use lazy_static::lazy_static;
use utoipa::ToSchema;

#[derive(Debug, Clone, serde::Serialize)]
#[derive(Debug, Clone, serde::Serialize, ToSchema)]
pub struct AppMeta {
#[schema(example = "fcf86f91")]
pub rev: String,
#[schema(example = "enstate")]
pub name: String,
#[schema(example = "git:fcf86f91")]
pub version: String,
#[schema(example = "2024-04-17 16:27:49.963738487 UTC")]
pub compile_time: String,
}

Expand Down
Loading

0 comments on commit 76c120b

Please sign in to comment.