Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade OpenAPI Implementation #101

Merged
merged 8 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading