Skip to content

Commit fdec4b7

Browse files
authored
Upgrade OpenAPI Implementation (#101)
1 parent 531f95e commit fdec4b7

File tree

11 files changed

+636
-279
lines changed

11 files changed

+636
-279
lines changed

server/Cargo.lock

+494-192
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/Cargo.toml

+39-28
Original file line numberDiff line numberDiff line change
@@ -8,53 +8,64 @@ description = "enstate"
88
repository = "https://github.com/v3xlabs/enstate"
99
authors = [
1010
"Luc van Kampen <[email protected]>",
11+
"Jakob Helgesson <[email protected]>",
1112
"Antonio Fran Trstenjak <[email protected]>",
1213
"Miguel Piedrafita <[email protected]>",
1314
]
1415

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

18-
ethers = "2"
19-
axum = "0.6.18"
20-
anyhow = "1.0.71"
21-
tracing = "0.1.27"
22-
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
19+
# Server
2320
dotenvy = "0.15.7"
24-
serde_json = "1.0.96"
25-
serde = { version = "1.0", features = ["derive"] }
21+
axum = "0.7.5"
22+
anyhow = "1.0.71"
23+
thiserror = "1.0.48"
24+
futures = "0.3.29"
2625
tokio = { version = "1.28.0", features = ["full", "tracing"] }
2726
tokio-util = "0.7.10"
28-
futures = "0.3.29"
29-
utoipa = { version = "4.1.0", features = ["axum_extras"] }
30-
utoipa-swagger-ui = { version = "4.0.0", features = ["axum"] }
31-
redis = { version = "0.23.0", features = ["connection-manager", "tokio-comp"] }
32-
tower-http = { version = "0.4.4", features = ["cors", "tracing", "trace"] }
27+
tokio-stream = "0.1.14"
28+
tower-http = { version = "0.5.2", features = ["cors", "tracing", "trace"] }
3329
rand = "0.8.5"
3430
chrono = "0.4.31"
31+
regex = "1.9.5"
32+
hex-literal = "0.4.1"
33+
axum-macros = "0.4.1"
34+
lazy_static = "1.4.0"
35+
rustc-hex = "2.0.1"
36+
37+
# Serde
38+
serde = { version = "1.0", features = ["derive"] }
39+
serde_json = "1.0.96"
40+
serde_with = "3.3.0"
41+
serde_qs = "0.13.0"
42+
43+
# Logging & Tracing
44+
tracing = "0.1.27"
45+
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
46+
opentelemetry = "0.22.0"
47+
opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] }
48+
opentelemetry-otlp = "0.15.0"
49+
tracing-opentelemetry = "0.23.0"
50+
51+
# Ethereum
52+
ethers = "2"
3553
ethers-contract = "2.0.9"
3654
ethers-core = "2.0.9"
37-
hex = "0.4.3"
38-
thiserror = "1.0.48"
39-
regex = "1.9.5"
40-
rustls = "0.21.7"
55+
56+
# Hashing
4157
bs58 = "0.5.0"
4258
sha2 = "0.10.7"
43-
digest = "0.10.7"
44-
hex-literal = "0.4.1"
45-
axum-macros = "0.3.8"
46-
lazy_static = "1.4.0"
4759
base32 = "0.4.0"
4860
crc16 = "0.4.0"
4961
blake2 = "0.10.6"
50-
rustc-hex = "2.0.1"
51-
serde_with = "3.3.0"
5262
bech32 = "0.10.0-alpha"
5363
crc32fast = "1.3.2"
64+
65+
# Other
66+
hex = "0.4.3"
67+
redis = { version = "0.25.3", features = ["connection-manager", "tokio-comp"] }
68+
rustls = "0.23"
69+
digest = "0.10.7"
5470
ciborium = "0.2.1"
55-
serde_qs = "0.12.0"
56-
tokio-stream = "0.1.14"
57-
opentelemetry = "0.22.0"
58-
opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] }
59-
opentelemetry-otlp = "0.15.0"
60-
tracing-opentelemetry = "0.23.0"
71+
utoipa = "4.2.0"

server/src/cache.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ impl CacheLayer for Redis {
3535
.set_ex(
3636
key,
3737
value,
38-
expires
39-
.try_into()
40-
.map_err(|x: TryFromIntError| CacheError::Other(x.to_string()))?,
38+
expires.into(),
4139
)
4240
.await;
4341

server/src/docs/index.html

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>API Reference</title>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
</head>
8+
<body>
9+
<script
10+
id="api-reference"
11+
data-url="/docs/openapi.json"
12+
></script>
13+
<script>
14+
var configuration = {
15+
theme: "blue",
16+
};
17+
18+
var apiReference = document.getElementById("api-reference");
19+
apiReference.dataset.configuration = JSON.stringify(configuration);
20+
</script>
21+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
22+
</body>
23+
</html>

server/src/docs/mod.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use crate::models::bulk::{BulkResponse, ListResponse};
2+
use crate::models::error::ErrorResponse;
3+
use crate::models::profile::ENSProfile;
4+
use utoipa::openapi::{ExternalDocs, License};
5+
use utoipa::OpenApi;
6+
7+
#[derive(OpenApi)]
8+
#[openapi(
9+
info(
10+
title = "enstate.rs",
11+
description = "A hosted ENS API allowing for easy access to ENS data.",
12+
),
13+
paths(
14+
crate::routes::address::get, crate::routes::name::get, crate::routes::universal::get,
15+
crate::routes::address::get_bulk, crate::routes::name::get_bulk, crate::routes::universal::get_bulk
16+
),
17+
components(schemas(ENSProfile, ListResponse<BulkResponse<ENSProfile>>, ErrorResponse))
18+
)]
19+
pub struct ApiDoc;
20+
21+
pub async fn openapi() -> String {
22+
let mut doc = ApiDoc::openapi();
23+
24+
let license = License::new("GPLv3");
25+
26+
doc.info.license = Some(license);
27+
doc.external_docs = Some(ExternalDocs::new("https://github.com/v3xlabs/enstate"));
28+
29+
doc.to_json().unwrap()
30+
}

server/src/http.rs

+34-49
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,18 @@
1+
use axum::response::{Html, Redirect};
12
use std::{net::SocketAddr, sync::Arc};
3+
use utoipa::OpenApi;
4+
use utoipa_swagger_ui::SwaggerUi;
25

3-
use axum::body::HttpBody;
4-
use axum::routing::MethodRouter;
56
use axum::{routing::get, Router};
7+
use tokio::net::TcpListener;
68
use tokio_util::sync::CancellationToken;
79
use tower_http::cors::CorsLayer;
810
use tower_http::trace::TraceLayer;
911
use tracing::info;
10-
use utoipa::OpenApi;
11-
use utoipa_swagger_ui::SwaggerUi;
1212

13-
use crate::models::bulk::{BulkResponse, ListResponse};
14-
use crate::models::error::ErrorResponse;
15-
use crate::models::profile::ENSProfile;
1613
use crate::routes;
1714
use crate::state::AppState;
1815

19-
#[derive(OpenApi)]
20-
#[openapi(
21-
paths(routes::address::get, routes::name::get, routes::universal::get),
22-
components(schemas(ENSProfile, ListResponse<BulkResponse<ENSProfile>>, ErrorResponse))
23-
)]
24-
pub struct ApiDoc;
25-
2616
pub struct App {
2717
router: Router,
2818
}
@@ -35,11 +25,14 @@ impl App {
3525
) -> Result<(), anyhow::Error> {
3626
let addr = SocketAddr::from(([0, 0, 0, 0], port));
3727

38-
let server = axum::Server::try_bind(&addr)?
39-
.serve(self.router.into_make_service())
40-
.with_graceful_shutdown(async {
41-
shutdown_signal.cancelled().await;
42-
});
28+
let listener = TcpListener::bind(&addr).await?;
29+
30+
async fn await_shutdown(shutdown_signal: CancellationToken) {
31+
shutdown_signal.cancelled().await;
32+
}
33+
34+
let server = axum::serve(listener, self.router.into_make_service())
35+
.with_graceful_shutdown(await_shutdown(shutdown_signal));
4336

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

@@ -53,19 +46,24 @@ impl App {
5346

5447
pub fn setup(state: AppState) -> App {
5548
let router = Router::new()
56-
.merge(SwaggerUi::new("/docs").url("/docs/openapi.json", ApiDoc::openapi()))
57-
.route("/", get(routes::root::get))
58-
.directory_route("/a/:address", get(routes::address::get))
59-
.directory_route("/n/:name", get(routes::name::get))
60-
.directory_route("/u/:name_or_address", get(routes::universal::get))
61-
.directory_route("/i/:name_or_address", get(routes::image::get))
62-
.directory_route("/h/:name_or_address", get(routes::header::get))
63-
.directory_route("/bulk/a", get(routes::address::get_bulk))
64-
.directory_route("/bulk/n", get(routes::name::get_bulk))
65-
.directory_route("/bulk/u", get(routes::universal::get_bulk))
66-
.directory_route("/sse/a", get(routes::address::get_bulk_sse))
67-
.directory_route("/sse/n", get(routes::name::get_bulk_sse))
68-
.directory_route("/sse/u", get(routes::universal::get_bulk_sse))
49+
.route(
50+
"/",
51+
get(|| async { Redirect::temporary("/docs") }),
52+
)
53+
.route("/docs", get(scalar_handler))
54+
.route("/docs/openapi.json", get(crate::docs::openapi))
55+
.route("/this", get(routes::root::get))
56+
.route("/a/:address", get(routes::address::get))
57+
.route("/n/:name", get(routes::name::get))
58+
.route("/u/:name_or_address", get(routes::universal::get))
59+
.route("/i/:name_or_address", get(routes::image::get))
60+
.route("/h/:name_or_address", get(routes::header::get))
61+
.route("/bulk/a", get(routes::address::get_bulk))
62+
.route("/bulk/n", get(routes::name::get_bulk))
63+
.route("/bulk/u", get(routes::universal::get_bulk))
64+
.route("/sse/a", get(routes::address::get_bulk_sse))
65+
.route("/sse/n", get(routes::name::get_bulk_sse))
66+
.route("/sse/u", get(routes::universal::get_bulk_sse))
6967
.fallback(routes::four_oh_four::handler)
7068
.layer(CorsLayer::permissive())
7169
.layer(TraceLayer::new_for_http())
@@ -74,21 +72,8 @@ pub fn setup(state: AppState) -> App {
7472
App { router }
7573
}
7674

77-
trait RouterExt<S, B>
78-
where
79-
B: HttpBody + Send + 'static,
80-
S: Clone + Send + Sync + 'static,
81-
{
82-
fn directory_route(self, path: &str, method_router: MethodRouter<S, B>) -> Self;
83-
}
84-
85-
impl<S, B> RouterExt<S, B> for Router<S, B>
86-
where
87-
B: HttpBody + Send + 'static,
88-
S: Clone + Send + Sync + 'static,
89-
{
90-
fn directory_route(self, path: &str, method_router: MethodRouter<S, B>) -> Self {
91-
self.route(path, method_router.clone())
92-
.route(&format!("{path}/"), method_router)
93-
}
75+
// Loads from docs/index.html with headers html
76+
async fn scalar_handler() -> Html<&'static str> {
77+
let contents = include_str!("./docs/index.html");
78+
axum::response::Html(contents)
9479
}

server/src/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use state::AppState;
1313
mod abi;
1414
mod cache;
1515
mod database;
16+
mod docs;
1617
mod http;
1718
mod models;
1819
mod provider;

server/src/models/profile.rs

+6
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,26 @@ use utoipa::ToSchema;
66
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)]
77
pub struct ENSProfile {
88
// Name
9+
#[schema(example = "vitalik.eth")]
910
pub name: String,
1011
// Ethereum Mainnet Address
12+
#[schema(example = "0x225f137127d9067788314bc7fcc1f36746a3c3B5")]
1113
pub address: Option<String>,
1214
// Avatar URL
15+
#[schema(example = "https://cloudflare-ipfs.com/ipfs/bafkreifnrjhkl7ccr2ifwn2n7ap6dh2way25a6w5x2szegvj5pt4b5nvfu")]
1316
pub avatar: Option<String>,
1417
// Preferred Capitalization of Name
18+
#[schema(example = "LuC.eTh")]
1519
pub display: String,
1620
// Records
1721
pub records: BTreeMap<String, String>,
1822
// Addresses on different chains
1923
pub chains: BTreeMap<String, String>,
2024
// Unix Timestamp of date it was loaded
25+
#[schema(example = "1713363899484")]
2126
pub fresh: i64,
2227
// Resolver the information was fetched from
28+
#[schema(example = "0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41")]
2329
pub resolver: String,
2430
// Errors encountered while fetching & decoding
2531
pub errors: BTreeMap<String, String>,

server/src/routes/address.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,15 @@ pub struct AddressGetBulkQuery {
6868

6969
#[utoipa::path(
7070
get,
71-
path = "/bulk/a/",
71+
path = "/bulk/a",
7272
responses(
7373
(status = 200, description = "Successfully found address.", body = BulkResponse<ENSProfile>),
7474
(status = BAD_REQUEST, description = "Invalid address.", body = ErrorResponse),
7575
(status = NOT_FOUND, description = "No name was associated with this address.", body = ErrorResponse),
7676
(status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse),
7777
),
7878
params(
79-
("addresses" = Vec<String>, Path, description = "Addresses to lookup name data for"),
79+
("addresses[]" = Vec<String>, Query, description = "Addresses to lookup name data for"),
8080
)
8181
)]
8282
pub async fn get_bulk(

server/src/routes/name.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ pub struct NameGetBulkQuery {
6161

6262
#[utoipa::path(
6363
get,
64-
path = "/bulk/n/",
64+
path = "/bulk/n",
6565
responses(
6666
(status = 200, description = "Successfully found name.", body = ListButWithLength<BulkResponse<Profile>>),
6767
(status = NOT_FOUND, description = "No name could be found.", body = ErrorResponse),
6868
),
6969
params(
70-
("name" = String, Path, description = "Name to lookup the name data for."),
70+
("names[]" = Vec<String>, Query, description = "Names to lookup name data for"),
7171
)
7272
)]
7373
pub async fn get_bulk(

server/src/routes/universal.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use enstate_shared::core::{ENSService, Profile};
1414
use futures::future::join_all;
1515
use serde::Deserialize;
1616
use tokio_stream::wrappers::UnboundedReceiverStream;
17+
use utoipa::IntoParams;
1718

1819
use crate::models::bulk::{BulkResponse, ListResponse};
1920
use crate::models::sse::SSEResponse;
@@ -51,7 +52,7 @@ pub async fn get(
5152
})?
5253
}
5354

54-
#[derive(Deserialize)]
55+
#[derive(Deserialize, IntoParams)]
5556
pub struct UniversalGetBulkQuery {
5657
// TODO (@antony1060): remove when proper serde error handling
5758
#[serde(default)]
@@ -63,14 +64,14 @@ pub struct UniversalGetBulkQuery {
6364

6465
#[utoipa::path(
6566
get,
66-
path = "/bulk/u/",
67+
path = "/bulk/u",
6768
responses(
6869
(status = 200, description = "Successfully found name or address.", body = BulkResponse<ENSProfile>),
6970
(status = NOT_FOUND, description = "No name or address could be found.", body = ErrorResponse),
7071
(status = UNPROCESSABLE_ENTITY, description = "Reverse record not owned by this address.", body = ErrorResponse),
7172
),
7273
params(
73-
("name_or_address" = String, Path, description = "Name or address to lookup the name data for."),
74+
("queries[]" = Vec<String>, Query, description = "Names to lookup name data for"),
7475
)
7576
)]
7677
pub async fn get_bulk(

0 commit comments

Comments
 (0)