Skip to content

Commit

Permalink
Upgrade OpenAPI Implementation (#101)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucemans authored Apr 17, 2024
1 parent 531f95e commit fdec4b7
Show file tree
Hide file tree
Showing 11 changed files with 636 additions and 279 deletions.
686 changes: 494 additions & 192 deletions server/Cargo.lock

Large diffs are not rendered by default.

67 changes: 39 additions & 28 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,64 @@ description = "enstate"
repository = "https://github.com/v3xlabs/enstate"
authors = [
"Luc van Kampen <[email protected]>",
"Jakob Helgesson <[email protected]>",
"Antonio Fran Trstenjak <[email protected]>",
"Miguel Piedrafita <[email protected]>",
]

[dependencies]
enstate_shared = { path = "../shared" }

ethers = "2"
axum = "0.6.18"
anyhow = "1.0.71"
tracing = "0.1.27"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
# Server
dotenvy = "0.15.7"
serde_json = "1.0.96"
serde = { version = "1.0", features = ["derive"] }
axum = "0.7.5"
anyhow = "1.0.71"
thiserror = "1.0.48"
futures = "0.3.29"
tokio = { version = "1.28.0", features = ["full", "tracing"] }
tokio-util = "0.7.10"
futures = "0.3.29"
utoipa = { version = "4.1.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "4.0.0", features = ["axum"] }
redis = { version = "0.23.0", features = ["connection-manager", "tokio-comp"] }
tower-http = { version = "0.4.4", features = ["cors", "tracing", "trace"] }
tokio-stream = "0.1.14"
tower-http = { version = "0.5.2", features = ["cors", "tracing", "trace"] }
rand = "0.8.5"
chrono = "0.4.31"
regex = "1.9.5"
hex-literal = "0.4.1"
axum-macros = "0.4.1"
lazy_static = "1.4.0"
rustc-hex = "2.0.1"

# Serde
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.96"
serde_with = "3.3.0"
serde_qs = "0.13.0"

# Logging & Tracing
tracing = "0.1.27"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
opentelemetry = "0.22.0"
opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] }
opentelemetry-otlp = "0.15.0"
tracing-opentelemetry = "0.23.0"

# Ethereum
ethers = "2"
ethers-contract = "2.0.9"
ethers-core = "2.0.9"
hex = "0.4.3"
thiserror = "1.0.48"
regex = "1.9.5"
rustls = "0.21.7"

# Hashing
bs58 = "0.5.0"
sha2 = "0.10.7"
digest = "0.10.7"
hex-literal = "0.4.1"
axum-macros = "0.3.8"
lazy_static = "1.4.0"
base32 = "0.4.0"
crc16 = "0.4.0"
blake2 = "0.10.6"
rustc-hex = "2.0.1"
serde_with = "3.3.0"
bech32 = "0.10.0-alpha"
crc32fast = "1.3.2"

# Other
hex = "0.4.3"
redis = { version = "0.25.3", features = ["connection-manager", "tokio-comp"] }
rustls = "0.23"
digest = "0.10.7"
ciborium = "0.2.1"
serde_qs = "0.12.0"
tokio-stream = "0.1.14"
opentelemetry = "0.22.0"
opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] }
opentelemetry-otlp = "0.15.0"
tracing-opentelemetry = "0.23.0"
utoipa = "4.2.0"
4 changes: 1 addition & 3 deletions server/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ impl CacheLayer for Redis {
.set_ex(
key,
value,
expires
.try_into()
.map_err(|x: TryFromIntError| CacheError::Other(x.to_string()))?,
expires.into(),
)
.await;

Expand Down
23 changes: 23 additions & 0 deletions server/src/docs/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>API Reference</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script
id="api-reference"
data-url="/docs/openapi.json"
></script>
<script>
var configuration = {
theme: "blue",
};

var apiReference = document.getElementById("api-reference");
apiReference.dataset.configuration = JSON.stringify(configuration);
</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions server/src/docs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use crate::models::bulk::{BulkResponse, ListResponse};
use crate::models::error::ErrorResponse;
use crate::models::profile::ENSProfile;
use utoipa::openapi::{ExternalDocs, License};
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(
info(
title = "enstate.rs",
description = "A hosted ENS API allowing for easy access to ENS data.",
),
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
),
components(schemas(ENSProfile, ListResponse<BulkResponse<ENSProfile>>, ErrorResponse))
)]
pub struct ApiDoc;

pub async fn openapi() -> String {
let mut doc = ApiDoc::openapi();

let license = License::new("GPLv3");

doc.info.license = Some(license);
doc.external_docs = Some(ExternalDocs::new("https://github.com/v3xlabs/enstate"));

doc.to_json().unwrap()
}
83 changes: 34 additions & 49 deletions server/src/http.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
use axum::response::{Html, Redirect};
use std::{net::SocketAddr, sync::Arc};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;

use axum::body::HttpBody;
use axum::routing::MethodRouter;
use axum::{routing::get, Router};
use tokio::net::TcpListener;
use tokio_util::sync::CancellationToken;
use tower_http::cors::CorsLayer;
use tower_http::trace::TraceLayer;
use tracing::info;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;

use crate::models::bulk::{BulkResponse, ListResponse};
use crate::models::error::ErrorResponse;
use crate::models::profile::ENSProfile;
use crate::routes;
use crate::state::AppState;

#[derive(OpenApi)]
#[openapi(
paths(routes::address::get, routes::name::get, routes::universal::get),
components(schemas(ENSProfile, ListResponse<BulkResponse<ENSProfile>>, ErrorResponse))
)]
pub struct ApiDoc;

pub struct App {
router: Router,
}
Expand All @@ -35,11 +25,14 @@ impl App {
) -> Result<(), anyhow::Error> {
let addr = SocketAddr::from(([0, 0, 0, 0], port));

let server = axum::Server::try_bind(&addr)?
.serve(self.router.into_make_service())
.with_graceful_shutdown(async {
shutdown_signal.cancelled().await;
});
let listener = TcpListener::bind(&addr).await?;

async fn await_shutdown(shutdown_signal: CancellationToken) {
shutdown_signal.cancelled().await;
}

let server = axum::serve(listener, self.router.into_make_service())
.with_graceful_shutdown(await_shutdown(shutdown_signal));

info!("Listening HTTP on {}", addr);

Expand All @@ -53,19 +46,24 @@ impl App {

pub fn setup(state: AppState) -> App {
let router = Router::new()
.merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi()))
.route("/", get(routes::root::get))
.directory_route("/a/:address", get(routes::address::get))
.directory_route("/n/:name", get(routes::name::get))
.directory_route("/u/:name_or_address", get(routes::universal::get))
.directory_route("/i/:name_or_address", get(routes::image::get))
.directory_route("/h/:name_or_address", get(routes::header::get))
.directory_route("/bulk/a", get(routes::address::get_bulk))
.directory_route("/bulk/n", get(routes::name::get_bulk))
.directory_route("/bulk/u", get(routes::universal::get_bulk))
.directory_route("/sse/a", get(routes::address::get_bulk_sse))
.directory_route("/sse/n", get(routes::name::get_bulk_sse))
.directory_route("/sse/u", get(routes::universal::get_bulk_sse))
.route(
"/",
get(|| async { Redirect::temporary("/docs") }),
)
.route("/docs", get(scalar_handler))
.route("/docs/openapi.json", get(crate::docs::openapi))
.route("/this", get(routes::root::get))
.route("/a/:address", get(routes::address::get))
.route("/n/:name", get(routes::name::get))
.route("/u/:name_or_address", get(routes::universal::get))
.route("/i/:name_or_address", get(routes::image::get))
.route("/h/:name_or_address", get(routes::header::get))
.route("/bulk/a", get(routes::address::get_bulk))
.route("/bulk/n", get(routes::name::get_bulk))
.route("/bulk/u", get(routes::universal::get_bulk))
.route("/sse/a", get(routes::address::get_bulk_sse))
.route("/sse/n", get(routes::name::get_bulk_sse))
.route("/sse/u", get(routes::universal::get_bulk_sse))
.fallback(routes::four_oh_four::handler)
.layer(CorsLayer::permissive())
.layer(TraceLayer::new_for_http())
Expand All @@ -74,21 +72,8 @@ pub fn setup(state: AppState) -> App {
App { router }
}

trait RouterExt<S, B>
where
B: HttpBody + Send + 'static,
S: Clone + Send + Sync + 'static,
{
fn directory_route(self, path: &str, method_router: MethodRouter<S, B>) -> Self;
}

impl<S, B> RouterExt<S, B> for Router<S, B>
where
B: HttpBody + Send + 'static,
S: Clone + Send + Sync + 'static,
{
fn directory_route(self, path: &str, method_router: MethodRouter<S, B>) -> Self {
self.route(path, method_router.clone())
.route(&format!("{path}/"), method_router)
}
// Loads from docs/index.html with headers html
async fn scalar_handler() -> Html<&'static str> {
let contents = include_str!("./docs/index.html");
axum::response::Html(contents)
}
1 change: 1 addition & 0 deletions server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use state::AppState;
mod abi;
mod cache;
mod database;
mod docs;
mod http;
mod models;
mod provider;
Expand Down
6 changes: 6 additions & 0 deletions server/src/models/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,26 @@ use utoipa::ToSchema;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)]
pub struct ENSProfile {
// Name
#[schema(example = "vitalik.eth")]
pub name: String,
// Ethereum Mainnet Address
#[schema(example = "0x225f137127d9067788314bc7fcc1f36746a3c3B5")]
pub address: Option<String>,
// Avatar URL
#[schema(example = "https://cloudflare-ipfs.com/ipfs/bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu")]
pub avatar: Option<String>,
// Preferred Capitalization of Name
#[schema(example = "LuC.eTh")]
pub display: String,
// Records
pub records: BTreeMap<String, String>,
// Addresses on different chains
pub chains: BTreeMap<String, String>,
// Unix Timestamp of date it was loaded
#[schema(example = "1713363899484")]
pub fresh: i64,
// Resolver the information was fetched from
#[schema(example = "0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41")]
pub resolver: String,
// Errors encountered while fetching & decoding
pub errors: BTreeMap<String, String>,
Expand Down
4 changes: 2 additions & 2 deletions server/src/routes/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ pub struct AddressGetBulkQuery {

#[utoipa::path(
get,
path = "/bulk/a/",
path = "/bulk/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>, Path, description = "Addresses to lookup name data for"),
("addresses[]" = Vec<String>, Query, description = "Addresses to lookup name data for"),
)
)]
pub async fn get_bulk(
Expand Down
4 changes: 2 additions & 2 deletions server/src/routes/name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ pub struct NameGetBulkQuery {

#[utoipa::path(
get,
path = "/bulk/n/",
path = "/bulk/n",
responses(
(status = 200, description = "Successfully found name.", body = ListButWithLength<BulkResponse<Profile>>),
(status = NOT_FOUND, description = "No name could be found.", body = ErrorResponse),
),
params(
("name" = String, Path, description = "Name to lookup the name data for."),
("names[]" = Vec<String>, Query, description = "Names to lookup name data for"),
)
)]
pub async fn get_bulk(
Expand Down
7 changes: 4 additions & 3 deletions server/src/routes/universal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use enstate_shared::core::{ENSService, Profile};
use futures::future::join_all;
use serde::Deserialize;
use tokio_stream::wrappers::UnboundedReceiverStream;
use utoipa::IntoParams;

use crate::models::bulk::{BulkResponse, ListResponse};
use crate::models::sse::SSEResponse;
Expand Down Expand Up @@ -51,7 +52,7 @@ pub async fn get(
})?
}

#[derive(Deserialize)]
#[derive(Deserialize, IntoParams)]
pub struct UniversalGetBulkQuery {
// TODO (@antony1060): remove when proper serde error handling
#[serde(default)]
Expand All @@ -63,14 +64,14 @@ pub struct UniversalGetBulkQuery {

#[utoipa::path(
get,
path = "/bulk/u/",
path = "/bulk/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(
("name_or_address" = String, Path, description = "Name or address to lookup the name data for."),
("queries[]" = Vec<String>, Query, description = "Names to lookup name data for"),
)
)]
pub async fn get_bulk(
Expand Down

0 comments on commit fdec4b7

Please sign in to comment.