From c77ccddcb3ecaaa84d42f0316ad4d01ccfa5a40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 3 May 2022 12:25:53 +0200 Subject: [PATCH] Add swagger for validator-api (#1239) * validator-api: add swagger openapi * Lock file --- Cargo.lock | 3 + .../mixnet-contract/src/interval.rs | 36 ++++++++++++ validator-api/Cargo.toml | 13 +++-- validator-api/src/contract_cache/mod.rs | 3 +- validator-api/src/contract_cache/routes.rs | 9 +++ validator-api/src/main.rs | 13 +++++ validator-api/src/node_status_api/mod.rs | 5 +- validator-api/src/node_status_api/models.rs | 56 +++++++++++++++++-- validator-api/src/node_status_api/routes.rs | 11 ++++ .../validator-api-requests/Cargo.toml | 4 +- .../validator-api-requests/src/models.rs | 12 ++-- 11 files changed, 143 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97694d3c18..aaca4ea673 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3243,6 +3243,7 @@ dependencies = [ "mixnet-contract-common", "nymcoconut", "nymsphinx", + "okapi", "pin-project", "pretty_env_logger", "rand 0.7.3", @@ -3250,7 +3251,9 @@ dependencies = [ "reqwest", "rocket", "rocket_cors", + "rocket_okapi", "rocket_sync_db_pools", + "schemars", "serde", "serde_json", "sqlx", diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index 1062980cdb..9d005aa6cf 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -2,6 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_std::Env; +use schemars::gen::SchemaGenerator; +use schemars::schema::{InstanceType, Schema, SchemaObject}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::convert::TryInto; use std::fmt::{Display, Formatter}; @@ -64,6 +67,39 @@ pub struct Interval { length: Duration, } +impl JsonSchema for Interval { + fn schema_name() -> String { + "Interval".to_owned() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + let mut schema_object = SchemaObject { + instance_type: Some(InstanceType::Object.into()), + ..SchemaObject::default() + }; + + let object_validation = schema_object.object(); + object_validation + .properties + .insert("id".to_owned(), gen.subschema_for::()); + object_validation.required.insert("id".to_owned()); + + // PrimitiveDateTime does not implement JsonSchema. However it has a custom + // serialization to string, so we just specify the schema to be String. + object_validation + .properties + .insert("start".to_owned(), gen.subschema_for::()); + object_validation.required.insert("start".to_owned()); + + object_validation + .properties + .insert("length".to_owned(), gen.subschema_for::()); + object_validation.required.insert("length".to_owned()); + + Schema::Object(schema_object) + } +} + impl Interval { /// Initialize epoch in the contract with default values. pub fn init_epoch(env: Env) -> Self { diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index f087e2b79a..0215ee6608 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -24,18 +24,18 @@ humantime-serde = "1.0" log = "0.4" pin-project = "1.0" pretty_env_logger = "0.4" -rand-07 = { package = "rand", version = "0.7" } # required for compatibility rand = "0.8" +rand-07 = { package = "rand", version = "0.7" } # required for compatibility reqwest = { version = "0.11", features = ["json"] } rocket = { version = "0.5.0-rc.1", features = ["json"] } +rocket_cors = { git="https://github.com/lawliet89/rocket_cors", rev="dfd3662c49e2f6fc37df35091cb94d82f7fb5915" } serde = "1.0" serde_json = "1.0" -tokio = { version = "1.4", features = ["rt-multi-thread", "macros", "signal", "time"] } -tokio-stream = "0.1.8" -rocket_cors = { git="https://github.com/lawliet89/rocket_cors", rev="dfd3662c49e2f6fc37df35091cb94d82f7fb5915" } -url = "2.2" thiserror = "1" time = { version = "0.3", features = ["serde-human-readable", "parsing"]} +tokio = { version = "1.4", features = ["rt-multi-thread", "macros", "signal", "time"] } +tokio-stream = "0.1.8" +url = "2.2" anyhow = "1" getset = "0.1.1" @@ -43,6 +43,9 @@ getset = "0.1.1" rocket_sync_db_pools = { version = "0.1.0-rc.1", default-features = false } sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]} +okapi = { version = "0.7.0-rc.1", features = ["impl_json_schema"] } +rocket_okapi = { version = "0.8.0-rc.1", features = ["swagger"] } +schemars = { version = "0.8", features = ["preserve_order"] } ## internal coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } diff --git a/validator-api/src/contract_cache/mod.rs b/validator-api/src/contract_cache/mod.rs index 81b01c5c8f..533a4375ad 100644 --- a/validator-api/src/contract_cache/mod.rs +++ b/validator-api/src/contract_cache/mod.rs @@ -9,6 +9,7 @@ use mixnet_contract_common::reward_params::EpochRewardParams; use mixnet_contract_common::{ GatewayBond, IdentityKey, IdentityKeyRef, Interval, MixNodeBond, RewardedSetNodeStatus, }; +use rocket_okapi::openapi_get_routes; use rocket::fairing::AdHoc; use serde::Serialize; @@ -191,7 +192,7 @@ impl ValidatorCache { rocket.manage(Self::new()).mount( // this format! is so ugly... format!("/{}", VALIDATOR_API_VERSION), - routes![ + openapi_get_routes![ routes::get_mixnodes, routes::get_gateways, routes::get_active_set, diff --git a/validator-api/src/contract_cache/routes.rs b/validator-api/src/contract_cache/routes.rs index e54593c183..c2d0f075db 100644 --- a/validator-api/src/contract_cache/routes.rs +++ b/validator-api/src/contract_cache/routes.rs @@ -6,28 +6,34 @@ use mixnet_contract_common::reward_params::EpochRewardParams; use mixnet_contract_common::{GatewayBond, Interval, MixNodeBond}; use rocket::serde::json::Json; use rocket::State; +use rocket_okapi::openapi; use std::collections::HashSet; +#[openapi(tag = "contract-cache")] #[get("/mixnodes")] pub async fn get_mixnodes(cache: &State) -> Json> { Json(cache.mixnodes().await) } +#[openapi(tag = "contract-cache")] #[get("/gateways")] pub async fn get_gateways(cache: &State) -> Json> { Json(cache.gateways().await) } +#[openapi(tag = "contract-cache")] #[get("/mixnodes/rewarded")] pub async fn get_rewarded_set(cache: &State) -> Json> { Json(cache.rewarded_set().await.value) } +#[openapi(tag = "contract-cache")] #[get("/mixnodes/active")] pub async fn get_active_set(cache: &State) -> Json> { Json(cache.active_set().await.value) } +#[openapi(tag = "contract-cache")] #[get("/mixnodes/blacklisted")] pub async fn get_blacklisted_mixnodes( cache: &State, @@ -35,6 +41,7 @@ pub async fn get_blacklisted_mixnodes( Json(cache.mixnodes_blacklist().await.map(|c| c.value)) } +#[openapi(tag = "contract-cache")] #[get("/gateways/blacklisted")] pub async fn get_blacklisted_gateways( cache: &State, @@ -42,11 +49,13 @@ pub async fn get_blacklisted_gateways( Json(cache.gateways_blacklist().await.map(|c| c.value)) } +#[openapi(tag = "contract-cache")] #[get("/epoch/reward_params")] pub async fn get_epoch_reward_params(cache: &State) -> Json { Json(cache.epoch_reward_params().await.value) } +#[openapi(tag = "contract-cache")] #[get("/epoch/current")] pub async fn get_current_epoch(cache: &State) -> Json> { Json(cache.current_epoch().await.value) diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index 7e8288e161..612356dfe8 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -19,6 +19,8 @@ use rocket::fairing::AdHoc; use rocket::http::Method; use rocket::{Ignite, Rocket}; use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; +use rocket_okapi::settings::UrlObject; +use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig}; use std::process; use std::sync::Arc; use std::time::Duration; @@ -398,6 +400,7 @@ async fn setup_rocket( ) -> Result> { // let's build our rocket! let rocket = rocket::build() + .mount("/swagger", make_swagger_ui(&get_docs())) .attach(setup_cors()?) .attach(setup_liftoff_notify(liftoff_notify)) .attach(ValidatorCache::stage()); @@ -436,6 +439,16 @@ async fn setup_rocket( } } +pub(crate) fn get_docs() -> SwaggerUIConfig { + SwaggerUIConfig { + urls: vec![ + UrlObject::new("Contract cache", "../v1/openapi.json"), + UrlObject::new("Node status", "../v1/status/openapi.json"), + ], + ..SwaggerUIConfig::default() + } +} + async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { let system_version = env!("CARGO_PKG_VERSION"); diff --git a/validator-api/src/node_status_api/mod.rs b/validator-api/src/node_status_api/mod.rs index 166e4ad61d..8c81bd45fc 100644 --- a/validator-api/src/node_status_api/mod.rs +++ b/validator-api/src/node_status_api/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use rocket::fairing::AdHoc; +use rocket_okapi::openapi_get_routes; use std::time::Duration; pub(crate) mod local_guard; @@ -18,7 +19,7 @@ pub(crate) fn stage_full() -> AdHoc { AdHoc::on_ignite("Node Status API Stage", |rocket| async { rocket.mount( "/v1/status", - routes![ + openapi_get_routes![ routes::mixnode_report, routes::gateway_report, routes::mixnode_uptime_history, @@ -40,7 +41,7 @@ pub(crate) fn stage_minimal() -> AdHoc { AdHoc::on_ignite("Node Status API Stage", |rocket| async { rocket.mount( "/v1/status", - routes![ + openapi_get_routes![ routes::get_mixnode_status, routes::get_mixnode_stake_saturation, routes::get_mixnode_inclusion_probability, diff --git a/validator-api/src/node_status_api/models.rs b/validator-api/src/node_status_api/models.rs index 2120e3158d..75590f817b 100644 --- a/validator-api/src/node_status_api/models.rs +++ b/validator-api/src/node_status_api/models.rs @@ -3,9 +3,16 @@ use crate::node_status_api::utils::NodeUptimes; use crate::storage::models::NodeStatus; +use okapi::openapi3::{Responses, SchemaObject}; use rocket::http::{ContentType, Status}; use rocket::response::{self, Responder, Response}; use rocket::Request; +use rocket_okapi::gen::OpenApiGenerator; +use rocket_okapi::response::OpenApiResponderInner; +use rocket_okapi::util::ensure_status_code_exists; +use schemars::gen::SchemaGenerator; +use schemars::schema::{InstanceType, Schema}; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::fmt::{self, Display, Formatter}; @@ -17,7 +24,7 @@ use time::OffsetDateTime; pub struct InvalidUptime; // value in range 0-100 -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Default)] +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Default, JsonSchema)] pub struct Uptime(u8); impl Uptime { @@ -97,7 +104,7 @@ impl TryFrom for Uptime { } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeStatusReport { pub(crate) identity: String, pub(crate) owner: String, @@ -134,7 +141,7 @@ impl MixnodeStatusReport { } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayStatusReport { pub(crate) identity: String, pub(crate) owner: String, @@ -171,7 +178,7 @@ impl GatewayStatusReport { } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeUptimeHistory { pub(crate) identity: String, pub(crate) owner: String, @@ -189,7 +196,7 @@ impl MixnodeUptimeHistory { } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayUptimeHistory { pub(crate) identity: String, pub(crate) owner: String, @@ -207,7 +214,7 @@ impl GatewayUptimeHistory { } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct HistoricalUptime { // ISO 8601 date string // I think this is more than enough, we don't need the uber precision of timezone offsets, etc @@ -240,6 +247,43 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse { } } +impl JsonSchema for ErrorResponse { + fn schema_name() -> String { + "ErrorResponse".to_owned() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + let mut schema_object = SchemaObject { + instance_type: Some(InstanceType::Object.into()), + ..SchemaObject::default() + }; + + let object_validation = schema_object.object(); + object_validation + .properties + .insert("error_message".to_owned(), gen.subschema_for::()); + object_validation + .required + .insert("error_message".to_owned()); + + // Status does not implement JsonSchema so we just explicitly specify the inner type. + object_validation + .properties + .insert("status".to_owned(), gen.subschema_for::()); + object_validation.required.insert("status".to_owned()); + + Schema::Object(schema_object) + } +} + +impl OpenApiResponderInner for ErrorResponse { + fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { + let mut responses = Responses::default(); + ensure_status_code_exists(&mut responses, 404); + Ok(responses) + } +} + #[derive(Debug, thiserror::Error)] pub enum ValidatorApiStorageError { MixnodeReportNotFound(String), diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index d62bd3b463..e8dcb0a23f 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -11,6 +11,7 @@ use mixnet_contract_common::reward_params::{NodeRewardParams, RewardParams}; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; +use rocket_okapi::openapi; use validator_api_requests::models::{ CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, @@ -18,6 +19,7 @@ use validator_api_requests::models::{ use super::models::Uptime; +#[openapi(tag = "mixnode")] #[get("/mixnode//report")] pub(crate) async fn mixnode_report( storage: &State, @@ -30,6 +32,7 @@ pub(crate) async fn mixnode_report( .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } +#[openapi(tag = "mixnode")] #[get("/gateway//report")] pub(crate) async fn gateway_report( storage: &State, @@ -42,6 +45,7 @@ pub(crate) async fn gateway_report( .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } +#[openapi(tag = "mixnode")] #[get("/mixnode//history")] pub(crate) async fn mixnode_uptime_history( storage: &State, @@ -54,6 +58,7 @@ pub(crate) async fn mixnode_uptime_history( .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } +#[openapi(tag = "mixnode")] #[get("/gateway//history")] pub(crate) async fn gateway_uptime_history( storage: &State, @@ -66,6 +71,7 @@ pub(crate) async fn gateway_uptime_history( .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } +#[openapi(tag = "mixnode")] #[get("/mixnode//core-status-count?")] pub(crate) async fn mixnode_core_status_count( storage: &State, @@ -83,6 +89,7 @@ pub(crate) async fn mixnode_core_status_count( }) } +#[openapi(tag = "mixnode")] #[get("/gateway//core-status-count?")] pub(crate) async fn gateway_core_status_count( storage: &State, @@ -100,6 +107,7 @@ pub(crate) async fn gateway_core_status_count( }) } +#[openapi(tag = "mixnode")] #[get("/mixnode//status")] pub(crate) async fn get_mixnode_status( cache: &State, @@ -110,6 +118,7 @@ pub(crate) async fn get_mixnode_status( }) } +#[openapi(tag = "mixnode")] #[get("/mixnode//reward-estimation")] pub(crate) async fn get_mixnode_reward_estimation( cache: &State, @@ -165,6 +174,7 @@ pub(crate) async fn get_mixnode_reward_estimation( } } +#[openapi(tag = "mixnode")] #[get("/mixnode//stake-saturation")] pub(crate) async fn get_mixnode_stake_saturation( cache: &State, @@ -193,6 +203,7 @@ pub(crate) async fn get_mixnode_stake_saturation( } } +#[openapi(tag = "mixnode")] #[get("/mixnode//inclusion-probability")] pub(crate) async fn get_mixnode_inclusion_probability( cache: &State, diff --git a/validator-api/validator-api-requests/Cargo.toml b/validator-api/validator-api-requests/Cargo.toml index 36cb221331..f9702f39ac 100644 --- a/validator-api/validator-api-requests/Cargo.toml +++ b/validator-api/validator-api-requests/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde = "1.0" mixnet-contract-common = { path= ".../../../../common/cosmwasm-smart-contracts/mixnet-contract" } schemars = { version = "0.8", features = ["preserve_order"] } +serde = "1.0" [dev-dependencies] -ts-rs = "6.1.2" \ No newline at end of file +ts-rs = "6.1.2" diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index c0dfa69c3c..ba8f5da923 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -6,7 +6,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr( test, @@ -26,7 +26,7 @@ impl MixnodeStatus { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr( test, @@ -40,7 +40,7 @@ pub struct CoreNodeStatusResponse { pub count: i32, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr( test, @@ -53,7 +53,7 @@ pub struct MixnodeStatusResponse { pub status: MixnodeStatus, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] pub struct RewardEstimationResponse { pub estimated_total_node_reward: u64, pub estimated_operator_reward: u64, @@ -63,7 +63,7 @@ pub struct RewardEstimationResponse { pub as_at: i64, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr( test, @@ -118,7 +118,7 @@ impl fmt::Display for SelectionChance { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr( test,