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

feat(cat-gateway): Add OpenAPI Examples and enforce error #1787

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4951f08
feat: lint example to error
apskhem Feb 5, 2025
27aae7d
feat: api-example-schema to error
apskhem Feb 6, 2025
58dcd3d
Merge branch 'main' into feat/openapi-examples
apskhem Feb 6, 2025
d06bdeb
Merge branch 'main' into feat/openapi-examples
apskhem Feb 7, 2025
66884b7
Merge branch 'main' into feat/openapi-examples
apskhem Feb 10, 2025
25a750a
feat: datetime api type
apskhem Feb 10, 2025
90211f1
feat: chain root object
apskhem Feb 10, 2025
c5a178c
refactor: use hash
apskhem Feb 10, 2025
645d0f2
feat: hash 128
apskhem Feb 10, 2025
6906c3a
feat: slot number
apskhem Feb 10, 2025
ecfb678
feat: apply new slot number type
apskhem Feb 10, 2025
c346233
fix: example-properties rule to omit example on sibling level
apskhem Feb 11, 2025
babe118
Merge branch 'main' into feat/openapi-examples
apskhem Feb 11, 2025
535ed6b
fix: example-schema to omit enum value
apskhem Feb 11, 2025
f3c1176
feat: add schema examples
apskhem Feb 11, 2025
6fa7e8a
fix: all issues
apskhem Feb 11, 2025
b382dba
chore: remove tmp dep
apskhem Feb 11, 2025
348394e
chore: issue
apskhem Feb 11, 2025
b51e14c
feat: add boolean newtypes
apskhem Feb 11, 2025
a1fec52
chore: update spectral rule description
apskhem Feb 12, 2025
0bf0ac6
Merge branch 'main' into feat/openapi-examples
apskhem Feb 12, 2025
28517c2
feat: error uuid
apskhem Feb 12, 2025
f243e25
feat: applying error msg
apskhem Feb 12, 2025
11ffce3
feat: slot no
apskhem Feb 12, 2025
022b2d2
feat: stake amount newtype
apskhem Feb 12, 2025
3e14e1d
feat: applying slot no
apskhem Feb 12, 2025
ddd639a
fix: inline hash types
apskhem Feb 12, 2025
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
6 changes: 6 additions & 0 deletions catalyst-gateway/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,11 @@ lint-generated-schema:
lint-generated-schema-local:
spectral lint --ruleset "./tests/openapi-v3.0-lints/.spectral.yml" "cat-gateway-api.json"

# temp
new-lint-generated-schema-local:
cargo b -r
./target/release/cat-gateway docs cat-gateway-api.json
spectral lint --ruleset "./tests/openapi-v3.0-lints/.spectral.yml" "cat-gateway-api.json"

# Do the minimal work needed to test the schema generated by cat-gateway
quick-schema-lint: generate-openapi-schema lint-generated-schema
4 changes: 3 additions & 1 deletion catalyst-gateway/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ poem-openapi = { version = "5.1.5", features = [
"url",
"chrono",
] }
uuid = { version = "1.11.0", features = ["v4", "v7", "serde"] }
uuid = { version = "1.12.1", features = ["v4", "v7", "serde"] }
ulid = { version = "1.1.3", features = ["serde", "uuid"] }
blake2b_simd = "1.0.2"
url = "2.5.3"
Expand All @@ -99,6 +99,8 @@ mime = "0.3.17"
stats_alloc = "0.1.10"
memory-stats = "1.0.0"
minicbor = "0.25.1"
derive_more = { version = "2.0.1", default-features = false, features = ["from", "from_str", "into"] }
orx-pseudo-default = "=1.4.0"

[dev-dependencies]
proptest = "1.5.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async fn test_get_invalid_registration_w_stake_addr() {

let mut row_stream = GetInvalidRegistrationQuery::execute(
&session,
GetInvalidRegistrationParams::new(vec![], SlotNo::from(u64::MAX)),
GetInvalidRegistrationParams::new(vec![], SlotNo::default()),
)
.await
.unwrap();
Expand Down
14 changes: 7 additions & 7 deletions catalyst-gateway/bin/src/service/api/cardano/cip36/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,14 @@ async fn get_all_registrations_from_stake_pub_key(
};

let cip36 = Cip36Details {
slot_no: SlotNo::from(slot_no),
slot_no: SlotNo::try_from(slot_no)?,
stake_pub_key: Some(Ed25519HexEncodedPublicKey::try_from(row.stake_address)?),
vote_pub_key: Some(Ed25519HexEncodedPublicKey::try_from(row.vote_key)?),
nonce: Some(Nonce::from(nonce)),
txn: Some(TxnIndex::try_from(row.txn)?),
payment_address: Some(Cip19ShelleyAddress::try_from(row.payment_address)?),
is_payable: row.is_payable,
cip15: !row.cip36,
is_payable: row.is_payable.into(),
cip15: (!row.cip36).into(),
errors: vec![],
};

Expand Down Expand Up @@ -252,7 +252,7 @@ async fn get_invalid_registrations(
let slot_no = if let Some(slot_no) = slot_no {
slot_no
} else {
SlotNo::from(0)
SlotNo::default()
};

let mut invalid_registrations_iter = GetInvalidRegistrationQuery::execute(
Expand All @@ -271,8 +271,8 @@ async fn get_invalid_registrations(
nonce: None,
txn: None,
payment_address: Some(Cip19ShelleyAddress::try_from(row.payment_address)?),
is_payable: row.is_payable,
cip15: !row.cip36,
is_payable: row.is_payable.into(),
cip15: (!row.cip36).into(),
errors: row
.error_report
.iter()
Expand Down Expand Up @@ -420,7 +420,7 @@ pub async fn snapshot(session: Arc<CassandraSession>, slot_no: Option<SlotNo>) -

AllRegistration::With(Cip36Registration::Ok(poem_openapi::payload::Json(
Cip36RegistrationList {
slot: slot_no.unwrap_or(SlotNo::from(0)),
slot: slot_no.unwrap_or_default(),
voting_key: all_registrations_after_filtering,
invalid: all_invalids_after_filtering.into_iter().flatten().collect(),
page: None,
Expand Down
34 changes: 14 additions & 20 deletions catalyst-gateway/bin/src/service/api/cardano/cip36/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ pub(crate) struct Cip36RegistrationList {
impl Example for Cip36RegistrationList {
fn example() -> Self {
Self {
slot: (common::types::cardano::slot_no::EXAMPLE + 635).into(),
slot: (common::types::cardano::slot_no::EXAMPLE + 635)
.try_into()
.unwrap_or_default(),
voting_key: vec![Cip36RegistrationsForVotingPublicKey::example()],
invalid: vec![Cip36Details::invalid_example()],
page: Some(common::objects::generic::pagination::CurrentPage::example()),
Expand Down Expand Up @@ -101,11 +103,11 @@ pub(crate) struct Cip36Details {
#[oai(skip_serializing_if_is_none)]
pub payment_address: Option<common::types::cardano::cip19_shelley_address::Cip19ShelleyAddress>,
/// If the payment address is a script, then it can not be payed rewards.
#[oai(default = "is_payable_default")]
pub is_payable: bool,
#[oai(default = "common::types::cardano::boolean::IsPayable::default")]
pub is_payable: common::types::cardano::boolean::IsPayable,
/// If this field is set, then the registration was in CIP15 format.
#[oai(default = "cip15_default")]
pub cip15: bool,
#[oai(default = "common::types::cardano::boolean::IsCip15::default")]
pub cip15: common::types::cardano::boolean::IsCip15,
/// If there are errors with this registration, they are listed here.
/// This field is *NEVER* returned for a valid registration.
#[oai(
Expand All @@ -116,16 +118,6 @@ pub(crate) struct Cip36Details {
pub errors: Vec<common::types::generic::error_msg::ErrorMessage>,
}

/// Is the payment address payable by catalyst.
fn is_payable_default() -> bool {
true
}

/// Is the registration using CIP15 format.
fn cip15_default() -> bool {
false
}

impl Example for Cip36Details {
/// Example of a valid registration
fn example() -> Self {
Expand All @@ -142,8 +134,8 @@ impl Example for Cip36Details {
payment_address: Some(
common::types::cardano::cip19_shelley_address::Cip19ShelleyAddress::example(),
),
is_payable: true,
cip15: false,
is_payable: common::types::cardano::boolean::IsPayable::example(),
cip15: common::types::cardano::boolean::IsCip15::example(),
errors: Vec::<common::types::generic::error_msg::ErrorMessage>::new(),
}
}
Expand All @@ -153,16 +145,18 @@ impl Cip36Details {
/// Example of an invalid registration
fn invalid_example() -> Self {
Self {
slot_no: (common::types::cardano::slot_no::EXAMPLE + 135).into(),
slot_no: (common::types::cardano::slot_no::EXAMPLE + 135)
.try_into()
.unwrap_or_default(),
stake_pub_key: None,
vote_pub_key: Some(
common::types::generic::ed25519_public_key::Ed25519HexEncodedPublicKey::example(),
),
nonce: Some((common::types::cardano::nonce::EXAMPLE + 97).into()),
txn: Some(common::types::cardano::txn_index::TxnIndex::example()),
payment_address: None,
is_payable: false,
cip15: true,
is_payable: common::types::cardano::boolean::IsPayable::example(),
cip15: common::types::cardano::boolean::IsCip15::example(),
errors: vec!["Stake Public Key is required".into()],
}
}
Expand Down
28 changes: 13 additions & 15 deletions catalyst-gateway/bin/src/service/api/cardano/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ use poem_openapi::{
param::{Path, Query},
OpenApi,
};
use types::DateTime;

use crate::service::{
common::{
auth::none_or_rbac::NoneOrRBAC,
objects::cardano::network::Network,
objects::cardano::{
hash::{Hash128, Hash256},
network::Network,
},
tags::ApiTags,
types::{
cardano::cip19_stake_address::Cip19StakeAddress,
generic::ed25519_public_key::Ed25519HexEncodedPublicKey,
generic::{api_date_time::ApiDateTime, ed25519_public_key::Ed25519HexEncodedPublicKey},
},
},
utilities::middleware::schema_validation::schema_version_validation,
Expand Down Expand Up @@ -44,7 +46,7 @@ impl Api {
&self,
/// The date-time for which the slot number should be calculated.
/// If omitted current date time is used.
date_time: Query<Option<DateTime>>,
date_time: Query<Option<ApiDateTime>>,
/// Cardano network type.
/// If omitted `mainnet` network type is defined.
/// As `preprod` and `preview` network types in the stake address encoded as a
Expand All @@ -54,7 +56,7 @@ impl Api {
/// No Authorization required, but Token permitted.
_auth: NoneOrRBAC,
) -> date_time_to_slot_number_get::AllResponses {
date_time_to_slot_number_get::endpoint(date_time.0, network.0).await
date_time_to_slot_number_get::endpoint(date_time.0.map(Into::into), network.0).await
}

#[oai(
Expand Down Expand Up @@ -84,14 +86,12 @@ impl Api {
///
/// This endpoint returns the registrations for a given chain root.
async fn rbac_registrations_get(
&self,
/// Chain root to get the registrations for.
#[oai(validator(max_length = 66, min_length = 64, pattern = "0x[0-9a-f]{64}"))]
Path(chain_root): Path<String>,
&self, /// Chain root to get the registrations for.
Path(chain_root): Path<Hash256>,
/// No Authorization required, but Token permitted.
_auth: NoneOrRBAC,
) -> rbac::registrations_get::AllResponses {
rbac::registrations_get::endpoint(chain_root).await
rbac::registrations_get::endpoint(chain_root.to_string()).await
}

#[oai(
Expand All @@ -103,14 +103,12 @@ impl Api {
///
/// This endpoint returns the RBAC certificate chain root for a given role 0 key.
async fn rbac_role0_key_chain_root(
&self,
/// Role0 key to get the chain root for.
#[oai(validator(min_length = 34, max_length = 34, pattern = "0x[0-9a-f]{32}"))]
Path(role0_key): Path<String>,
&self, /// Role0 key to get the chain root for.
Path(role0_key): Path<Hash128>,
/// No Authorization required, but Token permitted.
_auth: NoneOrRBAC,
) -> rbac::role0_chain_root_get::AllResponses {
rbac::role0_chain_root_get::endpoint(role0_key).await
rbac::role0_chain_root_get::endpoint(role0_key.to_string()).await
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use anyhow::anyhow;
use der_parser::asn1_rs::ToDer;
use futures::StreamExt;
use poem_openapi::{payload::Json, ApiResponse, Object};
use poem_openapi::{payload::Json, types::Example, ApiResponse, Object};
use tracing::error;

use crate::{
Expand All @@ -11,6 +11,7 @@ use crate::{
session::CassandraSession,
},
service::common::{
objects::cardano::hash::Hash256,
responses::WithErrorResponses,
types::{
cardano::cip19_stake_address::Cip19StakeAddress, headers::retry_after::RetryAfterOption,
Expand All @@ -20,10 +21,10 @@ use crate::{

/// GET RBAC chain root response.
#[derive(Object)]
pub(crate) struct Response {
#[oai(example = true)]
pub(crate) struct ChainRootGetResponse {
/// RBAC certificate chain root.
#[oai(validator(max_length = 66, min_length = 64, pattern = "0x[0-9a-f]{64}"))]
chain_root: String,
chain_root: Hash256,
}

/// Endpoint responses.
Expand All @@ -33,7 +34,7 @@ pub(crate) enum Responses {
///
/// Success returns the chain root hash.
#[oai(status = 200)]
Ok(Json<Response>),
Ok(Json<ChainRootGetResponse>),
/// ## Not Found
///
/// No chain root found for the given stake address.
Expand Down Expand Up @@ -72,8 +73,8 @@ pub(crate) async fn endpoint(stake_address: Cip19StakeAddress) -> AllResponses {
},
};

let res = Response {
chain_root: format!("0x{}", hex::encode(row.chain_root)),
let res = ChainRootGetResponse {
chain_root: Hash256::from(row.chain_root),
};

Responses::Ok(Json(res)).into()
Expand All @@ -88,3 +89,11 @@ pub(crate) async fn endpoint(stake_address: Cip19StakeAddress) -> AllResponses {
},
}
}

impl Example for ChainRootGetResponse {
fn example() -> Self {
Self {
chain_root: Hash256::example(),
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Implementation of the GET `/rbac/registrations` endpoint.
use anyhow::anyhow;
use futures::StreamExt;
use poem_openapi::{payload::Json, ApiResponse, Object};
use poem_openapi::{payload::Json, types::Example, ApiResponse, Object};
use tracing::error;

use crate::{
Expand All @@ -12,20 +12,22 @@ use crate::{
session::CassandraSession,
},
service::common::{
objects::cardano::hash::Hash, responses::WithErrorResponses,
objects::cardano::hash::Hash256, responses::WithErrorResponses,
types::headers::retry_after::RetryAfterOption,
},
};

/// GET RBAC registrations by chain root response list item.
#[derive(Object)]
#[oai(example = true)]
pub(crate) struct RbacRegistration {
/// Registration transaction hash.
tx_hash: Hash,
tx_hash: Hash256,
}

/// GET RBAC registrations by chain root response.
#[derive(Object)]
#[oai(example = true)]
pub(crate) struct RbacRegistrationsResponse {
/// Registrations by RBAC chain root.
#[oai(validator(max_items = "100000"))]
Expand Down Expand Up @@ -99,3 +101,19 @@ pub(crate) async fn endpoint(chain_root: String) -> AllResponses {

Responses::Ok(Json(RbacRegistrationsResponse { registrations })).into()
}

impl Example for RbacRegistration {
fn example() -> Self {
Self {
tx_hash: Hash256::example(),
}
}
}

impl Example for RbacRegistrationsResponse {
fn example() -> Self {
Self {
registrations: vec![RbacRegistration::example()],
}
}
}
Loading
Loading