From 8f35ff9b66fb803c3152ae54b885f8ac749f5ed8 Mon Sep 17 00:00:00 2001 From: Daniel Porteous Date: Tue, 12 Jul 2022 08:58:49 -0700 Subject: [PATCH] [API] Add support for index with Poem backend --- Cargo.lock | 4 +- api/Cargo.toml | 2 +- api/src/poem_backend/api.rs | 67 +++++++++++++++++++++++++++++++-- api/src/poem_backend/mod.rs | 3 ++ api/src/poem_backend/runtime.rs | 37 ++++++++++++------ api/src/runtime.rs | 6 +-- api/types/Cargo.toml | 1 + api/types/src/index.rs | 10 +++-- api/types/src/ledger_info.rs | 3 +- api/types/src/move_types.rs | 3 +- config/Cargo.toml | 1 + config/src/config/mod.rs | 3 +- 12 files changed, 114 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9f06d1edd570..f538c24253f03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,7 +187,7 @@ dependencies = [ [[package]] name = "aptos-api" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "aptos-api-types", @@ -249,6 +249,7 @@ dependencies = [ "bcs", "hex", "move-deps", + "poem-openapi", "serde 1.0.137", "serde_json", "warp", @@ -280,6 +281,7 @@ dependencies = [ "bcs", "get_if_addrs", "mirai-annotations", + "poem-openapi", "rand 0.7.3", "serde 1.0.137", "serde_yaml", diff --git a/api/Cargo.toml b/api/Cargo.toml index f4cb1be46ce3c..ff3fe25177fb1 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aptos-api" -version = "0.1.0" +version = "0.2.0" authors = ["Aptos Labs "] description = "Aptos REST API" repository = "https://github.com/aptos-labs/aptos-core" diff --git a/api/src/poem_backend/api.rs b/api/src/poem_backend/api.rs index cb90e8914897e..52e2c58464fd9 100644 --- a/api/src/poem_backend/api.rs +++ b/api/src/poem_backend/api.rs @@ -1,9 +1,70 @@ // Copyright (c) Aptos // SPDX-License-Identifier: Apache-2.0 -use poem_openapi::OpenApi; +use crate::context::Context; +use aptos_api_types::IndexResponse; +use poem::{http::StatusCode, Error as PoemError, Result as PoemResult}; +use poem_openapi::{ + payload::{Html, Json}, + OpenApi, Tags, +}; -pub struct Api {} +const OPEN_API_HTML: &str = include_str!("../../doc/spec.html"); + +pub struct Api { + context: Context, +} + +impl Api { + pub fn new(context: Context) -> Self { + Self { context } + } +} + +// TODO: Move these impls throughout each of the files in the parent directory. +// The only reason I do it here right now is the existing handler functions return +// opaque reply objects and therefore I can't re-use them, so I'd have to pollute +// those files with these impls below. + +// TODO: Consider using swagger UI here instead since it's built in, though +// the UI is much worse. I could look into adding the Elements UI to Poem. + +#[derive(Tags)] +enum ApiTags { + /// General information. + General, +} #[OpenApi] -impl Api {} +impl Api { + /// get_ledger_info + /// + /// Get the latest ledger information, including data such as chain ID, role type, ledger versions, epoch, etc. + #[oai( + path = "/", + method = "get", + operation_id = "get_ledger_info", + tag = "ApiTags::General" + )] + async fn get_ledger_info(&self) -> PoemResult> { + let ledger_info = self.context.get_latest_ledger_info().map_err(|e| { + PoemError::from((StatusCode::INTERNAL_SERVER_ERROR, anyhow::anyhow!(e))) + })?; + let node_role = self.context.node_role(); + let index_response = IndexResponse::new(ledger_info, node_role); + Ok(Json(index_response)) + } + + /// openapi + /// + /// Provides a UI that you can use to explore the API. You can also retrieve the API directly at `/openapi.yaml` and `/openapi.json`. + #[oai( + path = "/openapi", + method = "get", + operation_id = "openapi", + tag = "ApiTags::General" + )] + async fn openapi(&self) -> Html { + Html(OPEN_API_HTML.to_string()) + } +} diff --git a/api/src/poem_backend/mod.rs b/api/src/poem_backend/mod.rs index 87e1ad1c7b2cb..e1c64fe8a8f0e 100644 --- a/api/src/poem_backend/mod.rs +++ b/api/src/poem_backend/mod.rs @@ -1,2 +1,5 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + pub mod api; pub mod runtime; diff --git a/api/src/poem_backend/runtime.rs b/api/src/poem_backend/runtime.rs index c184c15fd6fd2..9fe95cdd21fcc 100644 --- a/api/src/poem_backend/runtime.rs +++ b/api/src/poem_backend/runtime.rs @@ -1,17 +1,21 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use super::api::Api; +use crate::context::Context; use aptos_config::config::NodeConfig; use poem::{listener::TcpListener, Route, Server}; -use poem_openapi::OpenApiService; +use poem_openapi::{LicenseObject, OpenApiService}; use tokio::runtime::Runtime; -use super::api::Api; - -pub fn attach_poem_to_runtime(runtime: &Runtime, config: &NodeConfig) -> anyhow::Result<()> { - let api = Api {}; +pub fn attach_poem_to_runtime( + runtime: &Runtime, + context: Context, + config: &NodeConfig, +) -> anyhow::Result<()> { + let api = Api::new(context); - // todo make this configurable - let api_endpoint = "/".to_string(); let api_service = build_openapi_service(api); - let ui = api_service.swagger_ui(); let spec_json = api_service.spec_endpoint(); let spec_yaml = api_service.spec_endpoint_yaml(); @@ -21,10 +25,12 @@ pub fn attach_poem_to_runtime(runtime: &Runtime, config: &NodeConfig) -> anyhow: Server::new(TcpListener::bind(address)) .run( Route::new() - .nest(api_endpoint, api_service) - .nest("/spec.html", ui) - .at("/spec.yaml", spec_json) - .at("/spec.json", spec_yaml), + .nest("/", api_service) + // TODO: I prefer "spec" here but it's not backwards compatible. + // Consider doing it later if we cut over to this entirely. + // TODO: Consider making these part of the API itself. + .at("/openapi.json", spec_json) + .at("/openapi.yaml", spec_yaml), ) .await .map_err(anyhow::Error::msg) @@ -34,6 +40,13 @@ pub fn attach_poem_to_runtime(runtime: &Runtime, config: &NodeConfig) -> anyhow: } pub fn build_openapi_service(api: Api) -> OpenApiService { + // TODO: This returns the version of the top level crate, not this crate. + // We should find another way to do versioning than just the crate. let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.1.0".to_string()); + let license = + LicenseObject::new("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0.html"); OpenApiService::new(api, "Aptos Node API", version) + .description("The Aptos Node API is a RESTful API for client applications to interact with the Aptos blockchain.") + .license(license) + .external_document("https://github.com/aptos-labs/aptos-core") } diff --git a/api/src/runtime.rs b/api/src/runtime.rs index 366905410c424..49b1ad4a30e28 100644 --- a/api/src/runtime.rs +++ b/api/src/runtime.rs @@ -27,14 +27,14 @@ pub fn bootstrap( .enable_all() .build() .context("[api] failed to create runtime")?; + let context = Context::new(chain_id, db, mp_sender, config.clone()); if config.api.use_poem_backend { - attach_poem_to_runtime(&runtime, config).context("Failed to attach poem to runtime")?; + attach_poem_to_runtime(&runtime, context, config) + .context("Failed to attach poem to runtime")?; } else { let api = WebServer::from(config.api.clone()); - let node_config = config.clone(); runtime.spawn(async move { - let context = Context::new(chain_id, db, mp_sender, node_config); let routes = index::routes(context); api.serve(routes).await; }); diff --git a/api/types/Cargo.toml b/api/types/Cargo.toml index 6fe8a4715246c..856c74982bf4b 100644 --- a/api/types/Cargo.toml +++ b/api/types/Cargo.toml @@ -13,6 +13,7 @@ edition = "2018" anyhow = "1.0.57" bcs = "0.1.3" hex = "0.4.3" +poem-openapi = { version = "2.0.3", features = ["swagger-ui", "url"] } serde = { version = "1.0.137", default-features = false } serde_json = "1.0.81" warp = { version = "0.3.2", features = ["default"] } diff --git a/api/types/src/index.rs b/api/types/src/index.rs index bc33f3bd41a6b..52eb732b3b1aa 100644 --- a/api/types/src/index.rs +++ b/api/types/src/index.rs @@ -3,13 +3,17 @@ use crate::LedgerInfo; use aptos_config::config::RoleType; +use poem_openapi::Object as PoemObject; use serde::{Deserialize, Serialize}; +// The data in IndexResponse is flattened into a single JSON map to offer +// easier parsing for clients. + /// The struct holding all data returned to the client by the -/// index endpoint (i.e., GET "/"). The data is flattened into -/// a single JSON map to offer easier parsing for clients. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +/// index endpoint (i.e., GET "/"). +#[derive(Clone, Debug, Deserialize, PartialEq, PoemObject, Serialize)] pub struct IndexResponse { + #[oai(flatten)] #[serde(flatten)] pub ledger_info: LedgerInfo, pub node_role: RoleType, diff --git a/api/types/src/ledger_info.rs b/api/types/src/ledger_info.rs index 5e092c26e952b..7a2e43dcc1daa 100644 --- a/api/types/src/ledger_info.rs +++ b/api/types/src/ledger_info.rs @@ -4,10 +4,11 @@ use crate::U64; use aptos_types::{chain_id::ChainId, ledger_info::LedgerInfoWithSignatures}; +use poem_openapi::Object as PoemObject; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PoemObject)] pub struct LedgerInfo { pub chain_id: u8, pub epoch: u64, diff --git a/api/types/src/move_types.rs b/api/types/src/move_types.rs index fee8f0be29849..ecf871af85cf5 100644 --- a/api/types/src/move_types.rs +++ b/api/types/src/move_types.rs @@ -23,6 +23,7 @@ use move_deps::{ move_resource_viewer::{AnnotatedMoveStruct, AnnotatedMoveValue}, }; +use poem_openapi::NewType as PoemNewType; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ collections::BTreeMap, @@ -50,7 +51,7 @@ impl TryFrom for MoveResource { } } -#[derive(Clone, Debug, PartialEq, Copy)] +#[derive(Clone, Debug, PartialEq, Copy, PoemNewType)] pub struct U64(pub u64); impl U64 { diff --git a/config/Cargo.toml b/config/Cargo.toml index 94e6c77458ba4..e387d38ad5d6f 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -14,6 +14,7 @@ anyhow = "1.0.57" bcs = "0.1.3" get_if_addrs = { version = "0.5.3", default-features = false } mirai-annotations = "1.12.0" +poem-openapi = { version = "2.0.3", features = ["swagger-ui", "url"] } rand = "0.7.3" serde = { version = "1.0.137", features = ["rc"], default-features = false } serde_yaml = "0.8.24" diff --git a/config/src/config/mod.rs b/config/src/config/mod.rs index 8e242d792e3db..ce027d4712824 100644 --- a/config/src/config/mod.rs +++ b/config/src/config/mod.rs @@ -44,6 +44,7 @@ mod api_config; pub use api_config::*; use aptos_crypto::{bls12381, ed25519::Ed25519PrivateKey, x25519}; use aptos_types::account_address::AccountAddress; +use poem_openapi::Enum as PoemEnum; /// Represents a deprecated config that provides no field verification. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] @@ -189,7 +190,7 @@ impl IdentityBlob { } } -#[derive(Clone, Copy, Deserialize, Eq, PartialEq, Serialize)] +#[derive(Clone, Copy, Deserialize, Eq, PartialEq, PoemEnum, Serialize)] #[serde(rename_all = "snake_case")] pub enum RoleType { Validator,