From b8ae83147f9af17af9a44493af5da619ddb78b87 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Tue, 14 Jan 2025 21:57:01 +0700 Subject: [PATCH 1/6] feat(cat-gateway): Add document index endpoint (request side only) --- catalyst-gateway/bin/Cargo.toml | 4 +- .../bin/src/service/api/documents/mod.rs | 37 +++- .../post_document_index_query/mod.rs | 44 +++++ .../post_document_index_query/query_filter.rs | 149 ++++++++++++++ .../put_document}/bad_put_request.rs | 0 .../{put_document.rs => put_document/mod.rs} | 7 +- .../service/common/objects/document/mod.rs | 4 +- .../service/common/types/document/doc_ref.rs | 107 ++++++++++ .../service/common/types/document/doc_type.rs | 101 ++++++++++ .../src/service/common/types/document/id.rs | 151 +++++++++++++++ .../src/service/common/types/document/mod.rs | 6 + .../src/service/common/types/document/ver.rs | 183 ++++++++++++++++++ .../src/service/common/types/generic/mod.rs | 1 + .../service/common/types/generic/uuidv4.rs | 94 +++++++++ .../service/common/types/generic/uuidv7.rs | 2 +- .../bin/src/service/common/types/mod.rs | 1 + 16 files changed, 874 insertions(+), 17 deletions(-) create mode 100644 catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs create mode 100644 catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs rename catalyst-gateway/bin/src/service/{common/objects/document => api/documents/put_document}/bad_put_request.rs (100%) rename catalyst-gateway/bin/src/service/api/documents/{put_document.rs => put_document/mod.rs} (89%) create mode 100644 catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs create mode 100644 catalyst-gateway/bin/src/service/common/types/document/doc_type.rs create mode 100644 catalyst-gateway/bin/src/service/common/types/document/id.rs create mode 100644 catalyst-gateway/bin/src/service/common/types/document/mod.rs create mode 100644 catalyst-gateway/bin/src/service/common/types/document/ver.rs create mode 100644 catalyst-gateway/bin/src/service/common/types/generic/uuidv4.rs diff --git a/catalyst-gateway/bin/Cargo.toml b/catalyst-gateway/bin/Cargo.toml index de35cfcce8d..f55833967ad 100644 --- a/catalyst-gateway/bin/Cargo.toml +++ b/catalyst-gateway/bin/Cargo.toml @@ -68,8 +68,8 @@ rust_decimal = { version = "1.36.0", features = [ "serde-with-float", "db-tokio-postgres", ] } -poem = { version = "3.1.3", features = ["embed", "prometheus", "compression"] } -poem-openapi = { version = "5.1.2", features = [ +poem = { version = "3.1.6", features = ["embed", "prometheus", "compression"] } +poem-openapi = { version = "5.1.5", features = [ "openapi-explorer", "rapidoc", "redoc", diff --git a/catalyst-gateway/bin/src/service/api/documents/mod.rs b/catalyst-gateway/bin/src/service/api/documents/mod.rs index 4782c9d299a..53535d241d0 100644 --- a/catalyst-gateway/bin/src/service/api/documents/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/mod.rs @@ -7,12 +7,12 @@ use poem_openapi::{ payload::Json, OpenApi, }; -use put_document::MAXIMUM_DOCUMENT_SIZE; +use post_document_index_query::query_filter::DocumentIndexQueryFilterBody; +use put_document::{bad_put_request::PutDocumentBadRequest, MAXIMUM_DOCUMENT_SIZE}; use crate::service::{ common::{ auth::{none_or_rbac::NoneOrRBAC, rbac::scheme::CatalystRBACSecurityScheme}, - objects::document::bad_put_request::PutDocumentBadRequest, tags::ApiTags, types::{generic::uuidv7::UUIDv7, payload::cbor::Cbor}, }, @@ -20,6 +20,7 @@ use crate::service::{ }; mod get_document; +mod post_document_index_query; mod put_document; /// Cardano Follower API Endpoints @@ -76,12 +77,32 @@ impl DocumentApi { match document.0.into_bytes_limit(MAXIMUM_DOCUMENT_SIZE).await { Ok(document) => put_document::endpoint(document).await, Err(ReadBodyError::PayloadTooLarge) => put_document::Responses::PayloadTooLarge.into(), - Err(_err) => { - put_document::Responses::BadRequest(Json(PutDocumentBadRequest::new( - "Failed to read document from the request", - ))) - .into() - }, + Err(_err) => put_document::Responses::BadRequest(Json(PutDocumentBadRequest::new( + "Failed to read document from the request", + ))) + .into(), } } + + /// Post A Signed Document Index Query. + /// + /// This endpoint produces a summary of signed documents that meet the criteria + /// defined in the request body. + /// + /// It does not return the actual documents, just an index of the document identifiers + /// which allows the documents to be retrieved by the `GET document` endpoint. + #[oai( + path = "/draft/document/index", + method = "post", + operation_id = "postDocument", + transform = "schema_version_validation" + )] + async fn post_document( + &self, /// The Query Filter Specification + query: Json, + /// Authorization required. + _auth: CatalystRBACSecurityScheme, + ) -> post_document_index_query::AllResponses { + post_document_index_query::endpoint(query.0.0).await + } } diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs new file mode 100644 index 00000000000..3317b0d7cc5 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs @@ -0,0 +1,44 @@ +//! Document Index Query + +use poem::Body; +use poem_openapi::{ApiResponse, Object}; +use query_filter::DocumentIndexQueryFilter; + +use crate::service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor}; + +pub(crate) mod query_filter; + +/// Endpoint responses. +#[derive(ApiResponse)] +#[allow(dead_code)] +pub(crate) enum Responses { + /// ## OK + /// + /// The Index of documents which match the query filter. + #[oai(status = 200)] + Ok(Cbor), + /// ## Not Found + /// + /// No documents were found which match the query filter. + #[oai(status = 404)] + NotFound, +} + +/// All responses. +pub(crate) type AllResponses = WithErrorResponses; + +/// Update user schema +#[derive(Debug, Object, Clone, Eq, PartialEq)] +pub(crate) struct QueryDocumentIndex { + /// Name + name: Option, +} + +/// # POST `/document/index` +#[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] +pub(crate) async fn endpoint(filter: DocumentIndexQueryFilter) -> AllResponses { + let _filter = filter; + + // We return this when the filter results in no documents found. + Responses::NotFound.into() +} diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs new file mode 100644 index 00000000000..4e29eb4bb37 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs @@ -0,0 +1,149 @@ +//! CIP36 object + +// TODO: This is NOT common, remove it once the rationalized endpoint is implemented. +// Retained to keep the existing code from breaking only. + +use poem_openapi::{types::Example, NewType, Object}; + +use crate::service::common::types::document::{ + doc_ref::IdAndVerRef, doc_type::DocumentType, id::EqOrRangedId, ver::EqOrRangedVer, +}; + +/// Query Filter for the generation of a signed document index. +/// +/// The Query works as a filter which acts like a sieve to filter out documents +/// which do not strictly match the metadata or payload fields included in the query +/// itself. +#[allow(clippy::doc_markdown)] +#[derive(Object, Default)] +#[oai(example = true)] +pub(crate) struct DocumentIndexQueryFilter { + /// ## Signed Document Type. + /// + /// The document type must match one of the + /// [Registered Document Types](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/) + /// + /// UUIDv4 Formatted 128bit value. + #[oai(rename = "type", skip_serializing_if_is_none)] + doc_type: Option, + /// ## Document ID + /// + /// Either an absolute single Document ID or a range of + /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) + #[oai(skip_serializing_if_is_none)] + id: Option, + /// ## Document Version + /// + /// Either an absolute single Document Version or a range of + /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) + #[oai(skip_serializing_if_is_none)] + ver: Option, + /// ## Document Reference + /// + /// A [reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#ref-document-reference) + /// to another signed document. This fields can match any reference that matches the defined + /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) + /// and/or + /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) + /// + /// The kind of document that the reference refers to is defined by the + /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/) + #[oai(rename = "ref", skip_serializing_if_is_none)] + doc_ref: Option, + /// ## Document Template + /// + /// Documents that are created based on a template include the + /// [template reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#template-template-reference) + /// to another signed document. This fields can match any template reference that matches the defined + /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) + /// and/or + /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) + /// + /// The kind of document that the reference refers to is defined by the + /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/) + /// however, it will always be a template type document that matches the document itself. + #[oai(skip_serializing_if_is_none)] + template: Option, + /// ## Document Reply + /// + /// This is a + /// [reply reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#reply-reply-reference) + /// which links one document to another, when acting as a reply to it. + /// Replies typically reference the same kind of document. + /// This fields can match any reply reference that matches the defined + /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) + /// and/or + /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) + /// + /// The kind of document that the reference refers to is defined by the + /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/). + #[oai(skip_serializing_if_is_none)] + reply: Option, + /// ## Brand + /// + /// This is a + /// [brand reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#brand_id) + /// to a brand document which defines the brand the document falls under. + /// This fields can match any brand reference that matches the defined + /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) + /// and/or + /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) + /// + /// Whether a Document Type has a brand reference is defined by its + /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/). + #[oai(skip_serializing_if_is_none)] + brand: Option, + /// ## Campaign + /// + /// This is a + /// [campaign reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#campaign_id) + /// to a campaign document which defines the campaign the document falls under. + /// This fields can match any campaign reference that matches the defined + /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) + /// and/or + /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) + /// + /// Whether a Document Type has a campaign reference is defined by its + /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/). + #[oai(skip_serializing_if_is_none)] + campaign: Option, + /// ## Category + /// + /// This is a + /// [category reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#category_id) + /// to a category document which defines the category the document falls under. + /// This fields can match any category reference that matches the defined + /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) + /// and/or + /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) + /// + /// Whether a Document Type has a category reference is defined by its + /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/). + #[oai(skip_serializing_if_is_none)] + category: Option, +} + +impl Example for DocumentIndexQueryFilter { + fn example() -> Self { + Self { + doc_type: Some(DocumentType::example()), + id: Some(EqOrRangedId::example()), + ver: Some(EqOrRangedVer::example()), + doc_ref: Some(IdAndVerRef::example()), + ..Default::default() + } + } +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +#[derive(NewType)] +#[oai(from_multipart = false, from_parameter = false, to_header = false)] +/// Document Index Query Filter +/// +/// A Query Filter which causes documents whose metadata matches the provided +/// fields to be returned in the index list response. +/// +/// Fields which are not set, are not used to filter documents based on those metadata fields. +/// This is equivalent to returning documents where those metadata fields either do not exist, +/// or do exist, but have any value. +pub(crate) struct DocumentIndexQueryFilterBody(pub(crate) DocumentIndexQueryFilter); diff --git a/catalyst-gateway/bin/src/service/common/objects/document/bad_put_request.rs b/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs similarity index 100% rename from catalyst-gateway/bin/src/service/common/objects/document/bad_put_request.rs rename to catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs diff --git a/catalyst-gateway/bin/src/service/api/documents/put_document.rs b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs similarity index 89% rename from catalyst-gateway/bin/src/service/api/documents/put_document.rs rename to catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs index 40af02c31fc..630a57b4b80 100644 --- a/catalyst-gateway/bin/src/service/api/documents/put_document.rs +++ b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs @@ -1,11 +1,12 @@ //! Implementation of the PUT `/document` endpoint +use bad_put_request::PutDocumentBadRequest; use bytes::Bytes; use poem_openapi::{payload::Json, ApiResponse}; -use crate::service::common::{ - objects::document::bad_put_request::PutDocumentBadRequest, responses::WithErrorResponses, -}; +use crate::service::common::responses::WithErrorResponses; + +pub(crate) mod bad_put_request; /// Maximum size of a Signed Document (1MB) pub(crate) const MAXIMUM_DOCUMENT_SIZE: usize = 1_048_576; diff --git a/catalyst-gateway/bin/src/service/common/objects/document/mod.rs b/catalyst-gateway/bin/src/service/common/objects/document/mod.rs index 86a6718f699..683f91959c2 100644 --- a/catalyst-gateway/bin/src/service/common/objects/document/mod.rs +++ b/catalyst-gateway/bin/src/service/common/objects/document/mod.rs @@ -1,3 +1 @@ -//! Signed Document Endpoint Objects - -pub(crate) mod bad_put_request; +//! Signed Document Objects diff --git a/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs b/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs new file mode 100644 index 00000000000..d744dbbf96b --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs @@ -0,0 +1,107 @@ +//! Signed Document Reference +//! +//! A Reference is used by the `ref` metadata, and any other reference to another document. + +use poem_openapi::{types::Example, NewType, Object, Union}; + +use super::{id::EqOrRangedId, ver::EqOrRangedVer}; + +#[derive(Object, Debug, PartialEq)] +/// A Reference to a Document ID/s and their version/s. +pub(crate) struct IdRefOnly { + /// Document ID, or range of Document IDs + id: EqOrRangedId, +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +#[derive(NewType, Debug, PartialEq)] +#[oai(from_multipart = false, from_parameter = false, to_header = false)] +/// Document ID Reference +/// +/// A Reference to the Document ID Only. +/// +/// This will match any document that matches the defined Document ID only. +/// The Document Version is not considered, and will match any version. +pub(crate) struct IdRefOnlyDocumented(IdRefOnly); + +#[derive(Object, Debug, PartialEq)] +/// A Reference to a Document ID/s and their version/s. +pub(crate) struct VerRefWithOptionalId { + /// Document ID, or range of Document IDs + #[oai(skip_serializing_if_is_none)] + id: Option, + /// Document Version, or Range of Document Versions + ver: EqOrRangedVer, +} + +impl Example for VerRefWithOptionalId { + fn example() -> Self { + Self { + id: None, + ver: EqOrRangedVer::example(), + } + } +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +#[derive(NewType, Debug, PartialEq)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Document Version Reference +/// +/// A Reference to the Document Version, and optionally also the Document ID. +/// +/// This will match any document that matches the defined Document Version and if +/// specified the Document ID. +/// If the Document ID is not specified, then all documents that match the version will be +/// returned in the index. +pub(crate) struct VerRefWithOptionalIdDocumented(VerRefWithOptionalId); + +impl Example for VerRefWithOptionalIdDocumented { + fn example() -> Self { + Self(VerRefWithOptionalId::example()) + } +} + +#[derive(Union, Debug, PartialEq)] +#[oai(one_of)] +/// Either a Single Document ID, or a Range of Document IDs +pub(crate) enum IdAndVerRefInner { + /// Document ID Reference ONLY + IdRefOnly(IdRefOnlyDocumented), + /// Version Reference with Optional Document ID Reference + IdAndVerRef(VerRefWithOptionalIdDocumented), +} + +impl Example for IdAndVerRefInner { + fn example() -> Self { + Self::IdAndVerRef(VerRefWithOptionalIdDocumented::example()) + } +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +#[derive(NewType, Debug, PartialEq)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Document Reference +/// +/// A Signed Documents +/// [Reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#ref-document-reference) +/// to another Documents ID and/or Version. +/// +/// *Note: at least one of `id` or `ver` must be defined.* +pub(crate) struct IdAndVerRef(IdAndVerRefInner); + +impl Example for IdAndVerRef { + fn example() -> Self { + Self(IdAndVerRefInner::example()) + } +} diff --git a/catalyst-gateway/bin/src/service/common/types/document/doc_type.rs b/catalyst-gateway/bin/src/service/common/types/document/doc_type.rs new file mode 100644 index 00000000000..ec3b9526434 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/doc_type.rs @@ -0,0 +1,101 @@ +//! Signed Document Type +//! +//! `UUIDv4` Encoded Document Type. + +use std::{ + borrow::Cow, + ops::{Deref, DerefMut}, + sync::LazyLock, +}; + +use anyhow::bail; +use poem_openapi::{ + registry::{MetaExternalDocument, MetaSchema, MetaSchemaRef}, + types::{Example, ParseError, ParseFromJSON, ParseFromParameter, ParseResult, ToJSON, Type}, +}; +use serde_json::Value; + +use crate::service::common::types::{generic, string_types::impl_string_types}; + +use self::generic::uuidv4; + +/// Title. +const TITLE: &str = "Signed Document Type"; +/// Description. +const DESCRIPTION: &str = "Document Type. UUIDv4 Formatted 128bit value."; +/// Example. +pub(crate) const EXAMPLE: &str = "7808d2ba-d511-40af-84e8-c0d1625fdfdc"; +/// External Documentation URI +const URI: &str = + "https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#type"; +/// Description of the URI +const URI_DESCRIPTION: &str = "Specification"; +/// Length of the hex encoded string +pub(crate) const ENCODED_LENGTH: usize = uuidv4::ENCODED_LENGTH; +/// Validation Regex Pattern +pub(crate) const PATTERN: &str = uuidv4::PATTERN; +/// Format +pub(crate) const FORMAT: &str = uuidv4::FORMAT; + +/// Schema +static SCHEMA: LazyLock = LazyLock::new(|| MetaSchema { + title: Some(TITLE.to_owned()), + description: Some(DESCRIPTION), + example: Some(Value::String(EXAMPLE.to_string())), + max_length: Some(ENCODED_LENGTH), + min_length: Some(ENCODED_LENGTH), + pattern: Some(PATTERN.to_string()), + external_docs: Some(MetaExternalDocument { + url: URI.to_owned(), + description: Some(URI_DESCRIPTION.to_owned()), + }), + + ..poem_openapi::registry::MetaSchema::ANY +}); + +/// Because ALL the constraints are defined above, we do not ever need to define them in +/// the API. BUT we do need to make a validator. +/// This helps enforce uniform validation. +fn is_valid(uuid: &str) -> bool { + uuidv4::UUIDv4::try_from(uuid).is_ok() +} + +impl_string_types!( + DocumentType, + "string", + FORMAT, + Some(SCHEMA.clone()), + is_valid +); + +impl Example for DocumentType { + /// An example. + fn example() -> Self { + Self(EXAMPLE.to_owned()) + } +} + +impl TryFrom<&str> for DocumentType { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + value.to_string().try_into() + } +} + +impl TryFrom for DocumentType { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + if !is_valid(&value) { + bail!("Invalid DocumentID, must be a valid UUIDv4") + } + Ok(Self(value)) + } +} + +impl From for DocumentType { + fn from(value: uuidv4::UUIDv4) -> Self { + Self(value.to_string()) + } +} diff --git a/catalyst-gateway/bin/src/service/common/types/document/id.rs b/catalyst-gateway/bin/src/service/common/types/document/id.rs new file mode 100644 index 00000000000..130748ef70a --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/id.rs @@ -0,0 +1,151 @@ +//! Signed Document ID +//! +//! `UUIDv7` Encoded Document ID. + +use std::{ + borrow::Cow, + ops::{Deref, DerefMut}, + sync::LazyLock, +}; + +use anyhow::bail; +use poem_openapi::Union; +use poem_openapi::{ + registry::{MetaExternalDocument, MetaSchema, MetaSchemaRef}, + types::{Example, ParseError, ParseFromJSON, ParseFromParameter, ParseResult, ToJSON, Type}, +}; +use poem_openapi::{NewType, Object}; +use serde_json::Value; + +use crate::service::common::types::{generic, string_types::impl_string_types}; + +use self::generic::uuidv7; + +/// Title. +const TITLE: &str = "Signed Document ID"; +/// Description. +const DESCRIPTION: &str = "Unique [Document ID](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id). + +UUIDv7 Formatted 128bit value."; +/// Example. +const EXAMPLE: &str = "01944e87-e68c-7f22-9df1-816863cfa5ff"; +/// External Documentation URI +const URI: &str = + "https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id"; +/// Description of the URI +const URI_DESCRIPTION: &str = "Specification"; +/// Length of the hex encoded string +pub(crate) const ENCODED_LENGTH: usize = uuidv7::ENCODED_LENGTH; +/// Validation Regex Pattern +pub(crate) const PATTERN: &str = uuidv7::PATTERN; +/// Format +pub(crate) const FORMAT: &str = uuidv7::FORMAT; + +/// Schema +static SCHEMA: LazyLock = LazyLock::new(|| MetaSchema { + title: Some(TITLE.to_owned()), + description: Some(DESCRIPTION), + example: Some(Value::String(EXAMPLE.to_string())), + max_length: Some(ENCODED_LENGTH), + min_length: Some(ENCODED_LENGTH), + pattern: Some(PATTERN.to_string()), + external_docs: Some(MetaExternalDocument { + url: URI.to_owned(), + description: Some(URI_DESCRIPTION.to_owned()), + }), + + ..poem_openapi::registry::MetaSchema::ANY +}); + +/// Because ALL the constraints are defined above, we do not ever need to define them in +/// the API. BUT we do need to make a validator. +/// This helps enforce uniform validation. +fn is_valid(uuid: &str) -> bool { + uuidv7::UUIDv7::try_from(uuid).is_ok() +} + +impl_string_types!(DocumentId, "string", FORMAT, Some(SCHEMA.clone()), is_valid); + +impl Example for DocumentId { + /// An example. + fn example() -> Self { + Self(EXAMPLE.to_owned()) + } +} + +impl TryFrom<&str> for DocumentId { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + value.to_string().try_into() + } +} + +impl TryFrom for DocumentId { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + if !is_valid(&value) { + bail!("Invalid DocumentID, must be a valid UUIDv7") + } + Ok(Self(value)) + } +} + +impl From for DocumentId { + fn from(value: uuidv7::UUIDv7) -> Self { + Self(value.to_string()) + } +} + +#[derive(Object, Debug, PartialEq)] +/// A range of Document IDs. +pub(crate) struct IdRangeInner { + /// Minimum Document ID to find (inclusive) + min: DocumentId, + /// Maximum Document ID to find (inclusive) + max: DocumentId, +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +#[derive(NewType, Debug, PartialEq)] +#[oai(from_multipart = false, from_parameter = false, to_header = false)] +/// ID Range +/// +/// A range of +/// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id). +pub(crate) struct IdRange(IdRangeInner); + +#[derive(Union, Debug, PartialEq)] +#[oai(one_of)] +/// Either a Single Document ID, or a Range of Document IDs +pub(crate) enum EqOrRangedIdInner { + /// This exact Document ID + Eq(DocumentId), + /// Document IDs in this range + Range(IdRange), +} + +impl Example for EqOrRangedIdInner { + fn example() -> Self { + Self::Eq(DocumentId::example()) + } +} + +#[derive(NewType, Debug, PartialEq)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Document ID Selector +/// +/// Either a absolute single Document ID or a range of Document IDs +pub(crate) struct EqOrRangedId(EqOrRangedIdInner); + +impl Example for EqOrRangedId { + fn example() -> Self { + Self(EqOrRangedIdInner::example()) + } +} diff --git a/catalyst-gateway/bin/src/service/common/types/document/mod.rs b/catalyst-gateway/bin/src/service/common/types/document/mod.rs new file mode 100644 index 00000000000..d3c56bac1fa --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/mod.rs @@ -0,0 +1,6 @@ +//! Signed Document Types + +pub(crate) mod doc_type; +pub(crate) mod id; +pub(crate) mod ver; +pub(crate) mod doc_ref; \ No newline at end of file diff --git a/catalyst-gateway/bin/src/service/common/types/document/ver.rs b/catalyst-gateway/bin/src/service/common/types/document/ver.rs new file mode 100644 index 00000000000..2f58f0b418e --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/ver.rs @@ -0,0 +1,183 @@ +//! Signed Document Version +//! +//! `UUIDv7` Encoded Document Version. + +use std::{ + borrow::Cow, + ops::{Deref, DerefMut}, + sync::LazyLock, +}; + +use anyhow::bail; +use poem_openapi::Union; +use poem_openapi::{ + registry::{MetaExternalDocument, MetaSchema, MetaSchemaRef}, + types::{Example, ParseError, ParseFromJSON, ParseFromParameter, ParseResult, ToJSON, Type}, +}; +use poem_openapi::{NewType, Object}; +use serde_json::Value; + +use crate::service::common::types::{generic, string_types::impl_string_types}; + +use self::generic::uuidv7; + +/// Title. +const TITLE: &str = "Signed Document Version"; +/// Description. +const DESCRIPTION: &str = "Unique [Document Version](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver). + +UUIDv7 Formatted 128bit value."; +/// Example. +const EXAMPLE: &str = "01944e87-e68c-7f22-9df1-816863cfa5ff"; +/// Example - Range min. +const EXAMPLE_MAX: &str = "01944e87-e68c-7f22-9df1-000000000000"; +/// Example. +const EXAMPLE_MIN: &str = "01944e87-e68c-7f22-9df1-ffffffffffff"; +/// External Documentation URI +const URI: &str = + "https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver"; +/// Description of the URI +const URI_DESCRIPTION: &str = "Specification"; +/// Length of the hex encoded string +pub(crate) const ENCODED_LENGTH: usize = uuidv7::ENCODED_LENGTH; +/// Validation Regex Pattern +pub(crate) const PATTERN: &str = uuidv7::PATTERN; +/// Format +pub(crate) const FORMAT: &str = uuidv7::FORMAT; + +/// Schema +static SCHEMA: LazyLock = LazyLock::new(|| MetaSchema { + title: Some(TITLE.to_owned()), + description: Some(DESCRIPTION), + example: Some(Value::String(EXAMPLE.to_string())), + max_length: Some(ENCODED_LENGTH), + min_length: Some(ENCODED_LENGTH), + pattern: Some(PATTERN.to_string()), + external_docs: Some(MetaExternalDocument { + url: URI.to_owned(), + description: Some(URI_DESCRIPTION.to_owned()), + }), + + ..poem_openapi::registry::MetaSchema::ANY +}); + +/// Because ALL the constraints are defined above, we do not ever need to define them in +/// the API. BUT we do need to make a validator. +/// This helps enforce uniform validation. +fn is_valid(uuid: &str) -> bool { + uuidv7::UUIDv7::try_from(uuid).is_ok() +} + +impl_string_types!( + DocumentVer, + "string", + FORMAT, + Some(SCHEMA.clone()), + is_valid +); + +impl Example for DocumentVer { + /// An example. + fn example() -> Self { + Self(EXAMPLE.to_owned()) + } +} + +impl TryFrom<&str> for DocumentVer { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + value.to_string().try_into() + } +} + +impl TryFrom for DocumentVer { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + if !is_valid(&value) { + bail!("Invalid DocumentID, must be a valid UUIDv7") + } + Ok(Self(value)) + } +} + +impl From for DocumentVer { + fn from(value: uuidv7::UUIDv7) -> Self { + Self(value.to_string()) + } +} + +#[derive(Object, Debug, PartialEq)] +#[oai(example = true)] +/// Version Range +pub(crate) struct VerRangeInner { + /// Minimum Document Version to find (inclusive) + min: DocumentVer, + /// Maximum Document Version to find (inclusive) + max: DocumentVer, +} + +impl Example for VerRangeInner { + fn example() -> Self { + Self { + min: DocumentVer(EXAMPLE_MIN.to_owned()), + max: DocumentVer(EXAMPLE_MAX.to_owned()), + } + } +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +#[derive(NewType, Debug, PartialEq)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Version Range +/// +/// A range of [Document Versions](). +pub(crate) struct VerRange(VerRangeInner); + +impl Example for VerRange { + fn example() -> Self { + Self(VerRangeInner::example()) + } +} + +#[derive(Union, Debug, PartialEq)] +#[oai(one_of)] +/// Document or Range of Documents +/// +/// Either a Single Document Version, or a Range of Document Versions +pub(crate) enum EqOrRangedVerInner { + /// This exact Document ID + Eq(DocumentVer), + /// Document Versions in this range + Range(VerRange), +} + +impl Example for EqOrRangedVerInner { + fn example() -> Self { + Self::Range(VerRange::example()) + } +} + +#[derive(NewType, Debug, PartialEq)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Document Version Selector +/// +/// Either a absolute single Document Version or a range of Document Versions +pub(crate) struct EqOrRangedVer(EqOrRangedVerInner); + +impl Example for EqOrRangedVer { + fn example() -> Self { + Self(EqOrRangedVerInner::example()) + } +} diff --git a/catalyst-gateway/bin/src/service/common/types/generic/mod.rs b/catalyst-gateway/bin/src/service/common/types/generic/mod.rs index 90df0ed62eb..9e3a892713e 100644 --- a/catalyst-gateway/bin/src/service/common/types/generic/mod.rs +++ b/catalyst-gateway/bin/src/service/common/types/generic/mod.rs @@ -5,4 +5,5 @@ pub(crate) mod ed25519_public_key; pub(crate) mod error_msg; pub(crate) mod query; +pub(crate) mod uuidv4; pub(crate) mod uuidv7; diff --git a/catalyst-gateway/bin/src/service/common/types/generic/uuidv4.rs b/catalyst-gateway/bin/src/service/common/types/generic/uuidv4.rs new file mode 100644 index 00000000000..c47f84ecc82 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/generic/uuidv4.rs @@ -0,0 +1,94 @@ +//! `UUIDv7` Type. +//! +//! String Encoded `UUIDv7` + +use std::{ + borrow::Cow, + ops::{Deref, DerefMut}, + sync::LazyLock, +}; + +use anyhow::bail; +use poem_openapi::{ + registry::{MetaSchema, MetaSchemaRef}, + types::{Example, ParseError, ParseFromJSON, ParseFromParameter, ParseResult, ToJSON, Type}, +}; +use serde_json::Value; + +use crate::service::common::types::string_types::impl_string_types; + +/// Title. +const TITLE: &str = "UUIDv4"; +/// Description. +const DESCRIPTION: &str = "128 Bit UUID Version 4 - Random"; +/// Example. +const EXAMPLE: &str = "c9993e54-1ee1-41f7-ab99-3fdec865c744 "; +/// Length of the hex encoded string +pub(crate) const ENCODED_LENGTH: usize = EXAMPLE.len(); +/// Validation Regex Pattern +pub(crate) const PATTERN: &str = + "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA0F]{3}-[0-9a-fA-F]{12}$"; +/// Format +pub(crate) const FORMAT: &str = "uuid"; + +/// Schema +static SCHEMA: LazyLock = LazyLock::new(|| { + MetaSchema { + title: Some(TITLE.to_owned()), + description: Some(DESCRIPTION), + example: Some(Value::String(EXAMPLE.to_string())), + max_length: Some(ENCODED_LENGTH), + min_length: Some(ENCODED_LENGTH), + pattern: Some(PATTERN.to_string()), + ..poem_openapi::registry::MetaSchema::ANY + } +}); + +/// Because ALL the constraints are defined above, we do not ever need to define them in +/// the API. BUT we do need to make a validator. +/// This helps enforce uniform validation. +fn is_valid(uuidv4: &str) -> bool { + // Just check the string can be safely converted into the type. + // All the necessary validation is done in that process. + if let Ok(uuid) = uuid::Uuid::parse_str(uuidv4) { + uuid.get_version() == Some(uuid::Version::Random) + } else { + false + } +} + +impl_string_types!(UUIDv4, "string", FORMAT, Some(SCHEMA.clone()), is_valid); + +impl Example for UUIDv4 { + /// An example `UUIDv4` + fn example() -> Self { + Self(EXAMPLE.to_owned()) + } +} + +impl TryFrom<&str> for UUIDv4 { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + value.to_string().try_into() + } +} + +impl TryFrom for UUIDv4 { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + if !is_valid(&value) { + bail!("Invalid UUIDv4") + } + Ok(Self(value)) + } +} + +impl TryInto for UUIDv4 { + type Error = uuid::Error; + + fn try_into(self) -> Result { + uuid::Uuid::parse_str(&self.0) + } +} diff --git a/catalyst-gateway/bin/src/service/common/types/generic/uuidv7.rs b/catalyst-gateway/bin/src/service/common/types/generic/uuidv7.rs index a3027485005..a464e08fdc1 100644 --- a/catalyst-gateway/bin/src/service/common/types/generic/uuidv7.rs +++ b/catalyst-gateway/bin/src/service/common/types/generic/uuidv7.rs @@ -29,7 +29,7 @@ pub(crate) const ENCODED_LENGTH: usize = EXAMPLE.len(); pub(crate) const PATTERN: &str = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-7[0-9a-fA-F]{3}-[89abAB][0-9a-fA0F]{3}-[0-9a-fA-F]{12}$"; /// Format -pub(crate) const FORMAT: &str = "uuid"; +pub(crate) const FORMAT: &str = "uuidv7"; /// Schema static SCHEMA: LazyLock = LazyLock::new(|| { diff --git a/catalyst-gateway/bin/src/service/common/types/mod.rs b/catalyst-gateway/bin/src/service/common/types/mod.rs index 451bea08862..37541878149 100644 --- a/catalyst-gateway/bin/src/service/common/types/mod.rs +++ b/catalyst-gateway/bin/src/service/common/types/mod.rs @@ -9,6 +9,7 @@ //! or integer. pub(crate) mod cardano; +pub(crate) mod document; pub(crate) mod generic; pub(crate) mod headers; pub(crate) mod payload; From bebf5ab979546c15fd55ba1304b09ac57a439d40 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Wed, 15 Jan 2025 00:40:32 +0700 Subject: [PATCH 2/6] feat(cat-gateway): Add response type for document index endpoint --- .../bin/src/service/api/documents/mod.rs | 5 +- .../post_document_index_query/mod.rs | 23 ++- .../post_document_index_query/response.rs | 166 ++++++++++++++++++ .../service/common/types/document/doc_ref.rs | 32 +++- 4 files changed, 214 insertions(+), 12 deletions(-) create mode 100644 catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs diff --git a/catalyst-gateway/bin/src/service/api/documents/mod.rs b/catalyst-gateway/bin/src/service/api/documents/mod.rs index 53535d241d0..35554ac2cd2 100644 --- a/catalyst-gateway/bin/src/service/api/documents/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/mod.rs @@ -12,6 +12,7 @@ use put_document::{bad_put_request::PutDocumentBadRequest, MAXIMUM_DOCUMENT_SIZE use crate::service::{ common::{ + self, auth::{none_or_rbac::NoneOrRBAC, rbac::scheme::CatalystRBACSecurityScheme}, tags::ApiTags, types::{generic::uuidv7::UUIDv7, payload::cbor::Cbor}, @@ -100,9 +101,11 @@ impl DocumentApi { async fn post_document( &self, /// The Query Filter Specification query: Json, + page: Query>, + limit: Query>, /// Authorization required. _auth: CatalystRBACSecurityScheme, ) -> post_document_index_query::AllResponses { - post_document_index_query::endpoint(query.0.0).await + post_document_index_query::endpoint(query.0 .0, page.0, limit.0).await } } diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs index 3317b0d7cc5..38195d56123 100644 --- a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs @@ -1,24 +1,27 @@ //! Document Index Query -use poem::Body; -use poem_openapi::{ApiResponse, Object}; +use poem_openapi::{payload::Json, ApiResponse, Object}; use query_filter::DocumentIndexQueryFilter; +use response::DocumentIndexListDocumented; -use crate::service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor}; +use crate::service::common::responses::WithErrorResponses; + +use super::common; pub(crate) mod query_filter; +pub(crate) mod response; /// Endpoint responses. #[derive(ApiResponse)] #[allow(dead_code)] pub(crate) enum Responses { /// ## OK - /// + /// /// The Index of documents which match the query filter. #[oai(status = 200)] - Ok(Cbor), + Ok(Json), /// ## Not Found - /// + /// /// No documents were found which match the query filter. #[oai(status = 404)] NotFound, @@ -36,8 +39,14 @@ pub(crate) struct QueryDocumentIndex { /// # POST `/document/index` #[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] -pub(crate) async fn endpoint(filter: DocumentIndexQueryFilter) -> AllResponses { +pub(crate) async fn endpoint( + filter: DocumentIndexQueryFilter, + page: Option, + limit: Option, +) -> AllResponses { let _filter = filter; + let _page = page; + let _limit = limit; // We return this when the filter results in no documents found. Responses::NotFound.into() diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs new file mode 100644 index 00000000000..e1e5b959500 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs @@ -0,0 +1,166 @@ +//! Cip36 Registration Query Endpoint Response +use poem_openapi::{types::Example, NewType, Object}; + +use crate::service::common; + +use self::common::types::document::{ + doc_ref::DocumentReference, doc_type::DocumentType, id::DocumentId, ver::DocumentVer, +}; + +/// A single page of documents. +/// +/// The page limit is defined by the number of document versions, +/// not the number of Document IDs. +#[derive(Object)] +#[oai(example = true)] +pub(crate) struct DocumentIndexList { + /// List of documents that matched the filter. + /// + /// Documents are listed in ascending order. + #[oai(validator(max_items = "100"))] + pub docs: Vec, + /// Current Page + pub page: Option, +} + +impl Example for DocumentIndexList { + fn example() -> Self { + Self { + docs: vec![IndexedDocumentDocumented::example()], + page: Some(common::objects::generic::pagination::CurrentPage::example()), + } + } +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +#[derive(NewType)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Document Index List +/// +/// A list of all matching documents, limited by the paging parameters. +/// Documents are listed in Ascending order. +/// The Paging limit refers to the number fo document versions, not the number +/// of unique Document IDs. +pub(crate) struct DocumentIndexListDocumented(pub(crate) DocumentIndexList); + +impl Example for DocumentIndexListDocumented { + fn example() -> Self { + Self(DocumentIndexList::example()) + } +} + +/// List of Documents that matched the filter +#[derive(Object)] +#[oai(example = true)] +pub(crate) struct IndexedDocument { + /// Document ID that matches the filter + #[oai(rename = "id")] + pub doc_id: DocumentId, + /// List of matching versions of the document. + /// + /// Versions are listed in ascending order. + #[oai(validator(max_items = "100"))] + pub ver: Vec, +} + +impl Example for IndexedDocument { + fn example() -> Self { + Self { + doc_id: DocumentId::example(), + ver: vec![IndexedDocumentVersionDocumented::example()], + } + } +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +#[derive(NewType)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Individual Indexed Document +/// +/// An Individual Indexed Document and its Versions. +/// Document Versions are listed in Ascending order. +pub(crate) struct IndexedDocumentDocumented(pub(crate) IndexedDocument); + +impl Example for IndexedDocumentDocumented { + fn example() -> Self { + Self(IndexedDocument::example()) + } +} + +/// List of Documents that matched the filter +#[derive(Object)] +#[oai(example = true)] +pub(crate) struct IndexedDocumentVersion { + /// Document Version that matches the filter + pub ver: DocumentVer, + /// Document Type that matches the filter + #[oai(rename = "type")] + pub doc_type: DocumentType, + /// Document Reference that matches the filter + #[oai(rename = "ref", skip_serializing_if_is_none)] + pub doc_ref: Option, + /// Document Reply Reference that matches the filter + #[oai(skip_serializing_if_is_none)] + pub reply: Option, + /// Document Template Reference that matches the filter + #[oai(skip_serializing_if_is_none)] + pub template: Option, + /// Document Brand Reference that matches the filter + #[oai(skip_serializing_if_is_none)] + pub brand: Option, + /// Document Campaign Reference that matches the filter + #[oai(skip_serializing_if_is_none)] + pub campaign: Option, + /// Document Category Reference that matches the filter + #[oai(skip_serializing_if_is_none)] + pub category: Option, +} + +impl Example for IndexedDocumentVersion { + fn example() -> Self { + Self { + ver: DocumentVer::example(), + doc_type: DocumentType::example(), + doc_ref: Some(DocumentReference::example()), + reply: None, + template: None, + brand: None, + campaign: None, + category: None, + } + } +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +#[derive(NewType)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Individual Document Version +/// +/// A Matching version of the document. +/// +/// Metadata fields which are not set in the document, are not included in the version information. +/// used to filter documents based on those metadata fields. +/// This is equivalent to returning documents where those metadata fields either do not exist, +/// or do exist, but have any value. +pub(crate) struct IndexedDocumentVersionDocumented(pub(crate) IndexedDocumentVersion); + +impl Example for IndexedDocumentVersionDocumented { + fn example() -> Self { + Self(IndexedDocumentVersion::example()) + } +} diff --git a/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs b/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs index d744dbbf96b..f817c399e59 100644 --- a/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs +++ b/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs @@ -4,7 +4,10 @@ use poem_openapi::{types::Example, NewType, Object, Union}; -use super::{id::EqOrRangedId, ver::EqOrRangedVer}; +use super::{ + id::{DocumentId, EqOrRangedId}, + ver::{DocumentVer, EqOrRangedVer}, +}; #[derive(Object, Debug, PartialEq)] /// A Reference to a Document ID/s and their version/s. @@ -22,7 +25,7 @@ pub(crate) struct IdRefOnly { /// /// This will match any document that matches the defined Document ID only. /// The Document Version is not considered, and will match any version. -pub(crate) struct IdRefOnlyDocumented(IdRefOnly); +pub(crate) struct IdRefOnlyDocumented(pub(crate) IdRefOnly); #[derive(Object, Debug, PartialEq)] /// A Reference to a Document ID/s and their version/s. @@ -59,7 +62,7 @@ impl Example for VerRefWithOptionalId { /// specified the Document ID. /// If the Document ID is not specified, then all documents that match the version will be /// returned in the index. -pub(crate) struct VerRefWithOptionalIdDocumented(VerRefWithOptionalId); +pub(crate) struct VerRefWithOptionalIdDocumented(pub(crate) VerRefWithOptionalId); impl Example for VerRefWithOptionalIdDocumented { fn example() -> Self { @@ -98,10 +101,31 @@ impl Example for IdAndVerRefInner { /// to another Documents ID and/or Version. /// /// *Note: at least one of `id` or `ver` must be defined.* -pub(crate) struct IdAndVerRef(IdAndVerRefInner); +pub(crate) struct IdAndVerRef(pub(crate) IdAndVerRefInner); impl Example for IdAndVerRef { fn example() -> Self { Self(IdAndVerRefInner::example()) } } + +#[derive(Object, Debug, PartialEq)] +#[oai(example = true)] +/// A Reference to another Signed Document +pub(crate) struct DocumentReference { + /// Document ID Reference + #[oai(rename = "id")] + doc_id: DocumentId, + /// Document Version + #[oai(skip_serializing_if_is_none)] + ver: Option, +} + +impl Example for DocumentReference { + fn example() -> Self { + Self { + doc_id: DocumentId::example(), + ver: Some(DocumentVer::example()), + } + } +} From ce855ec4bdcbfe167d1b9aedb940f2683255b391 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Wed, 15 Jan 2025 10:23:44 +0700 Subject: [PATCH 3/6] fix(cat-gateway): code format --- .../bin/src/service/api/documents/mod.rs | 10 +++-- .../post_document_index_query/mod.rs | 3 +- .../post_document_index_query/query_filter.rs | 20 +++++----- .../post_document_index_query/response.rs | 20 +++++----- .../service/common/types/document/doc_ref.rs | 12 ++++-- .../service/common/types/document/doc_type.rs | 29 ++++++++------- .../src/service/common/types/document/id.rs | 37 ++++++++++--------- .../src/service/common/types/document/mod.rs | 2 +- .../src/service/common/types/document/ver.rs | 37 ++++++++++--------- 9 files changed, 91 insertions(+), 79 deletions(-) diff --git a/catalyst-gateway/bin/src/service/api/documents/mod.rs b/catalyst-gateway/bin/src/service/api/documents/mod.rs index 35554ac2cd2..5714232ac31 100644 --- a/catalyst-gateway/bin/src/service/api/documents/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/mod.rs @@ -78,10 +78,12 @@ impl DocumentApi { match document.0.into_bytes_limit(MAXIMUM_DOCUMENT_SIZE).await { Ok(document) => put_document::endpoint(document).await, Err(ReadBodyError::PayloadTooLarge) => put_document::Responses::PayloadTooLarge.into(), - Err(_err) => put_document::Responses::BadRequest(Json(PutDocumentBadRequest::new( - "Failed to read document from the request", - ))) - .into(), + Err(_err) => { + put_document::Responses::BadRequest(Json(PutDocumentBadRequest::new( + "Failed to read document from the request", + ))) + .into() + }, } } diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs index 38195d56123..26ca8c004ac 100644 --- a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs @@ -4,9 +4,8 @@ use poem_openapi::{payload::Json, ApiResponse, Object}; use query_filter::DocumentIndexQueryFilter; use response::DocumentIndexListDocumented; -use crate::service::common::responses::WithErrorResponses; - use super::common; +use crate::service::common::responses::WithErrorResponses; pub(crate) mod query_filter; pub(crate) mod response; diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs index 4e29eb4bb37..21c85e54082 100644 --- a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs @@ -41,8 +41,8 @@ pub(crate) struct DocumentIndexQueryFilter { /// ## Document Reference /// /// A [reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#ref-document-reference) - /// to another signed document. This fields can match any reference that matches the defined - /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) + /// to another signed document. This fields can match any reference that matches the + /// defined [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) /// and/or /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) /// @@ -54,14 +54,15 @@ pub(crate) struct DocumentIndexQueryFilter { /// /// Documents that are created based on a template include the /// [template reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#template-template-reference) - /// to another signed document. This fields can match any template reference that matches the defined - /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) + /// to another signed document. This fields can match any template reference that + /// matches the defined [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) /// and/or /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) /// /// The kind of document that the reference refers to is defined by the /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/) - /// however, it will always be a template type document that matches the document itself. + /// however, it will always be a template type document that matches the document + /// itself. #[oai(skip_serializing_if_is_none)] template: Option, /// ## Document Reply @@ -135,7 +136,8 @@ impl Example for DocumentIndexQueryFilter { } } -// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. #[derive(NewType)] #[oai(from_multipart = false, from_parameter = false, to_header = false)] /// Document Index Query Filter @@ -143,7 +145,7 @@ impl Example for DocumentIndexQueryFilter { /// A Query Filter which causes documents whose metadata matches the provided /// fields to be returned in the index list response. /// -/// Fields which are not set, are not used to filter documents based on those metadata fields. -/// This is equivalent to returning documents where those metadata fields either do not exist, -/// or do exist, but have any value. +/// Fields which are not set, are not used to filter documents based on those metadata +/// fields. This is equivalent to returning documents where those metadata fields either +/// do not exist, or do exist, but have any value. pub(crate) struct DocumentIndexQueryFilterBody(pub(crate) DocumentIndexQueryFilter); diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs index e1e5b959500..fdd46f0f6ba 100644 --- a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs @@ -1,11 +1,10 @@ //! Cip36 Registration Query Endpoint Response use poem_openapi::{types::Example, NewType, Object}; -use crate::service::common; - use self::common::types::document::{ doc_ref::DocumentReference, doc_type::DocumentType, id::DocumentId, ver::DocumentVer, }; +use crate::service::common; /// A single page of documents. /// @@ -32,7 +31,8 @@ impl Example for DocumentIndexList { } } -// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. #[derive(NewType)] #[oai( from_multipart = false, @@ -77,7 +77,8 @@ impl Example for IndexedDocument { } } -// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. #[derive(NewType)] #[oai( from_multipart = false, @@ -141,7 +142,8 @@ impl Example for IndexedDocumentVersion { } } -// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. #[derive(NewType)] #[oai( from_multipart = false, @@ -153,10 +155,10 @@ impl Example for IndexedDocumentVersion { /// /// A Matching version of the document. /// -/// Metadata fields which are not set in the document, are not included in the version information. -/// used to filter documents based on those metadata fields. -/// This is equivalent to returning documents where those metadata fields either do not exist, -/// or do exist, but have any value. +/// Metadata fields which are not set in the document, are not included in the version +/// information. used to filter documents based on those metadata fields. +/// This is equivalent to returning documents where those metadata fields either do not +/// exist, or do exist, but have any value. pub(crate) struct IndexedDocumentVersionDocumented(pub(crate) IndexedDocumentVersion); impl Example for IndexedDocumentVersionDocumented { diff --git a/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs b/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs index f817c399e59..9ffa6507f94 100644 --- a/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs +++ b/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs @@ -1,6 +1,7 @@ //! Signed Document Reference //! -//! A Reference is used by the `ref` metadata, and any other reference to another document. +//! A Reference is used by the `ref` metadata, and any other reference to another +//! document. use poem_openapi::{types::Example, NewType, Object, Union}; @@ -16,7 +17,8 @@ pub(crate) struct IdRefOnly { id: EqOrRangedId, } -// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. #[derive(NewType, Debug, PartialEq)] #[oai(from_multipart = false, from_parameter = false, to_header = false)] /// Document ID Reference @@ -46,7 +48,8 @@ impl Example for VerRefWithOptionalId { } } -// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. #[derive(NewType, Debug, PartialEq)] #[oai( from_multipart = false, @@ -86,7 +89,8 @@ impl Example for IdAndVerRefInner { } } -// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. #[derive(NewType, Debug, PartialEq)] #[oai( from_multipart = false, diff --git a/catalyst-gateway/bin/src/service/common/types/document/doc_type.rs b/catalyst-gateway/bin/src/service/common/types/document/doc_type.rs index ec3b9526434..561fbf93b16 100644 --- a/catalyst-gateway/bin/src/service/common/types/document/doc_type.rs +++ b/catalyst-gateway/bin/src/service/common/types/document/doc_type.rs @@ -15,9 +15,8 @@ use poem_openapi::{ }; use serde_json::Value; -use crate::service::common::types::{generic, string_types::impl_string_types}; - use self::generic::uuidv4; +use crate::service::common::types::{generic, string_types::impl_string_types}; /// Title. const TITLE: &str = "Signed Document Type"; @@ -38,19 +37,21 @@ pub(crate) const PATTERN: &str = uuidv4::PATTERN; pub(crate) const FORMAT: &str = uuidv4::FORMAT; /// Schema -static SCHEMA: LazyLock = LazyLock::new(|| MetaSchema { - title: Some(TITLE.to_owned()), - description: Some(DESCRIPTION), - example: Some(Value::String(EXAMPLE.to_string())), - max_length: Some(ENCODED_LENGTH), - min_length: Some(ENCODED_LENGTH), - pattern: Some(PATTERN.to_string()), - external_docs: Some(MetaExternalDocument { - url: URI.to_owned(), - description: Some(URI_DESCRIPTION.to_owned()), - }), +static SCHEMA: LazyLock = LazyLock::new(|| { + MetaSchema { + title: Some(TITLE.to_owned()), + description: Some(DESCRIPTION), + example: Some(Value::String(EXAMPLE.to_string())), + max_length: Some(ENCODED_LENGTH), + min_length: Some(ENCODED_LENGTH), + pattern: Some(PATTERN.to_string()), + external_docs: Some(MetaExternalDocument { + url: URI.to_owned(), + description: Some(URI_DESCRIPTION.to_owned()), + }), - ..poem_openapi::registry::MetaSchema::ANY + ..poem_openapi::registry::MetaSchema::ANY + } }); /// Because ALL the constraints are defined above, we do not ever need to define them in diff --git a/catalyst-gateway/bin/src/service/common/types/document/id.rs b/catalyst-gateway/bin/src/service/common/types/document/id.rs index 130748ef70a..8cbca026790 100644 --- a/catalyst-gateway/bin/src/service/common/types/document/id.rs +++ b/catalyst-gateway/bin/src/service/common/types/document/id.rs @@ -9,17 +9,15 @@ use std::{ }; use anyhow::bail; -use poem_openapi::Union; use poem_openapi::{ registry::{MetaExternalDocument, MetaSchema, MetaSchemaRef}, types::{Example, ParseError, ParseFromJSON, ParseFromParameter, ParseResult, ToJSON, Type}, + NewType, Object, Union, }; -use poem_openapi::{NewType, Object}; use serde_json::Value; -use crate::service::common::types::{generic, string_types::impl_string_types}; - use self::generic::uuidv7; +use crate::service::common::types::{generic, string_types::impl_string_types}; /// Title. const TITLE: &str = "Signed Document ID"; @@ -42,19 +40,21 @@ pub(crate) const PATTERN: &str = uuidv7::PATTERN; pub(crate) const FORMAT: &str = uuidv7::FORMAT; /// Schema -static SCHEMA: LazyLock = LazyLock::new(|| MetaSchema { - title: Some(TITLE.to_owned()), - description: Some(DESCRIPTION), - example: Some(Value::String(EXAMPLE.to_string())), - max_length: Some(ENCODED_LENGTH), - min_length: Some(ENCODED_LENGTH), - pattern: Some(PATTERN.to_string()), - external_docs: Some(MetaExternalDocument { - url: URI.to_owned(), - description: Some(URI_DESCRIPTION.to_owned()), - }), - - ..poem_openapi::registry::MetaSchema::ANY +static SCHEMA: LazyLock = LazyLock::new(|| { + MetaSchema { + title: Some(TITLE.to_owned()), + description: Some(DESCRIPTION), + example: Some(Value::String(EXAMPLE.to_string())), + max_length: Some(ENCODED_LENGTH), + min_length: Some(ENCODED_LENGTH), + pattern: Some(PATTERN.to_string()), + external_docs: Some(MetaExternalDocument { + url: URI.to_owned(), + description: Some(URI_DESCRIPTION.to_owned()), + }), + + ..poem_openapi::registry::MetaSchema::ANY + } }); /// Because ALL the constraints are defined above, we do not ever need to define them in @@ -107,7 +107,8 @@ pub(crate) struct IdRangeInner { max: DocumentId, } -// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. #[derive(NewType, Debug, PartialEq)] #[oai(from_multipart = false, from_parameter = false, to_header = false)] /// ID Range diff --git a/catalyst-gateway/bin/src/service/common/types/document/mod.rs b/catalyst-gateway/bin/src/service/common/types/document/mod.rs index d3c56bac1fa..3a1263a6fe6 100644 --- a/catalyst-gateway/bin/src/service/common/types/document/mod.rs +++ b/catalyst-gateway/bin/src/service/common/types/document/mod.rs @@ -1,6 +1,6 @@ //! Signed Document Types +pub(crate) mod doc_ref; pub(crate) mod doc_type; pub(crate) mod id; pub(crate) mod ver; -pub(crate) mod doc_ref; \ No newline at end of file diff --git a/catalyst-gateway/bin/src/service/common/types/document/ver.rs b/catalyst-gateway/bin/src/service/common/types/document/ver.rs index 2f58f0b418e..dcc20772a8a 100644 --- a/catalyst-gateway/bin/src/service/common/types/document/ver.rs +++ b/catalyst-gateway/bin/src/service/common/types/document/ver.rs @@ -9,17 +9,15 @@ use std::{ }; use anyhow::bail; -use poem_openapi::Union; use poem_openapi::{ registry::{MetaExternalDocument, MetaSchema, MetaSchemaRef}, types::{Example, ParseError, ParseFromJSON, ParseFromParameter, ParseResult, ToJSON, Type}, + NewType, Object, Union, }; -use poem_openapi::{NewType, Object}; use serde_json::Value; -use crate::service::common::types::{generic, string_types::impl_string_types}; - use self::generic::uuidv7; +use crate::service::common::types::{generic, string_types::impl_string_types}; /// Title. const TITLE: &str = "Signed Document Version"; @@ -46,19 +44,21 @@ pub(crate) const PATTERN: &str = uuidv7::PATTERN; pub(crate) const FORMAT: &str = uuidv7::FORMAT; /// Schema -static SCHEMA: LazyLock = LazyLock::new(|| MetaSchema { - title: Some(TITLE.to_owned()), - description: Some(DESCRIPTION), - example: Some(Value::String(EXAMPLE.to_string())), - max_length: Some(ENCODED_LENGTH), - min_length: Some(ENCODED_LENGTH), - pattern: Some(PATTERN.to_string()), - external_docs: Some(MetaExternalDocument { - url: URI.to_owned(), - description: Some(URI_DESCRIPTION.to_owned()), - }), - - ..poem_openapi::registry::MetaSchema::ANY +static SCHEMA: LazyLock = LazyLock::new(|| { + MetaSchema { + title: Some(TITLE.to_owned()), + description: Some(DESCRIPTION), + example: Some(Value::String(EXAMPLE.to_string())), + max_length: Some(ENCODED_LENGTH), + min_length: Some(ENCODED_LENGTH), + pattern: Some(PATTERN.to_string()), + external_docs: Some(MetaExternalDocument { + url: URI.to_owned(), + description: Some(URI_DESCRIPTION.to_owned()), + }), + + ..poem_openapi::registry::MetaSchema::ANY + } }); /// Because ALL the constraints are defined above, we do not ever need to define them in @@ -127,7 +127,8 @@ impl Example for VerRangeInner { } } -// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the openapi docs on an object. +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. #[derive(NewType, Debug, PartialEq)] #[oai( from_multipart = false, From 35296e22e8227f385e7d4921a8f0c7bc3528fca8 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Wed, 15 Jan 2025 12:53:48 +0700 Subject: [PATCH 4/6] fix api lint errors --- .../post_document_index_query/query_filter.rs | 27 +++--- .../service/common/types/document/doc_ref.rs | 86 ++++++++++++++--- .../src/service/common/types/document/id.rs | 94 ++++++++++++++++--- .../src/service/common/types/document/ver.rs | 83 ++++++++++++---- .../service/common/types/generic/uuidv4.rs | 2 +- 5 files changed, 239 insertions(+), 53 deletions(-) diff --git a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs index 21c85e54082..7c82ae2d859 100644 --- a/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs @@ -5,8 +5,9 @@ use poem_openapi::{types::Example, NewType, Object}; +use super::common::types::document::ver::EqOrRangedVerDocumented; use crate::service::common::types::document::{ - doc_ref::IdAndVerRef, doc_type::DocumentType, id::EqOrRangedId, ver::EqOrRangedVer, + doc_ref::IdAndVerRefDocumented, doc_type::DocumentType, id::EqOrRangedIdDocumented, }; /// Query Filter for the generation of a signed document index. @@ -31,13 +32,13 @@ pub(crate) struct DocumentIndexQueryFilter { /// Either an absolute single Document ID or a range of /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id) #[oai(skip_serializing_if_is_none)] - id: Option, + id: Option, /// ## Document Version /// /// Either an absolute single Document Version or a range of /// [Document Versions](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver) #[oai(skip_serializing_if_is_none)] - ver: Option, + ver: Option, /// ## Document Reference /// /// A [reference](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/meta/#ref-document-reference) @@ -49,7 +50,7 @@ pub(crate) struct DocumentIndexQueryFilter { /// The kind of document that the reference refers to is defined by the /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/) #[oai(rename = "ref", skip_serializing_if_is_none)] - doc_ref: Option, + doc_ref: Option, /// ## Document Template /// /// Documents that are created based on a template include the @@ -64,7 +65,7 @@ pub(crate) struct DocumentIndexQueryFilter { /// however, it will always be a template type document that matches the document /// itself. #[oai(skip_serializing_if_is_none)] - template: Option, + template: Option, /// ## Document Reply /// /// This is a @@ -79,7 +80,7 @@ pub(crate) struct DocumentIndexQueryFilter { /// The kind of document that the reference refers to is defined by the /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/). #[oai(skip_serializing_if_is_none)] - reply: Option, + reply: Option, /// ## Brand /// /// This is a @@ -93,7 +94,7 @@ pub(crate) struct DocumentIndexQueryFilter { /// Whether a Document Type has a brand reference is defined by its /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/). #[oai(skip_serializing_if_is_none)] - brand: Option, + brand: Option, /// ## Campaign /// /// This is a @@ -107,7 +108,7 @@ pub(crate) struct DocumentIndexQueryFilter { /// Whether a Document Type has a campaign reference is defined by its /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/). #[oai(skip_serializing_if_is_none)] - campaign: Option, + campaign: Option, /// ## Category /// /// This is a @@ -121,16 +122,18 @@ pub(crate) struct DocumentIndexQueryFilter { /// Whether a Document Type has a category reference is defined by its /// [Document Type](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/types/). #[oai(skip_serializing_if_is_none)] - category: Option, + category: Option, } impl Example for DocumentIndexQueryFilter { fn example() -> Self { Self { doc_type: Some(DocumentType::example()), - id: Some(EqOrRangedId::example()), - ver: Some(EqOrRangedVer::example()), - doc_ref: Some(IdAndVerRef::example()), + id: Some(EqOrRangedIdDocumented::example()), + ver: Some(EqOrRangedVerDocumented::example()), + doc_ref: Some(IdAndVerRefDocumented::example_id_ref()), + template: Some(IdAndVerRefDocumented::example_id_and_ver_ref()), + reply: Some(IdAndVerRefDocumented::example()), ..Default::default() } } diff --git a/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs b/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs index 9ffa6507f94..f9310bbe1a8 100644 --- a/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs +++ b/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs @@ -6,21 +6,35 @@ use poem_openapi::{types::Example, NewType, Object, Union}; use super::{ - id::{DocumentId, EqOrRangedId}, - ver::{DocumentVer, EqOrRangedVer}, + id::{DocumentId, EqOrRangedIdDocumented}, + ver::{DocumentVer, EqOrRangedVerDocumented}, }; #[derive(Object, Debug, PartialEq)] +#[oai(example = true)] /// A Reference to a Document ID/s and their version/s. pub(crate) struct IdRefOnly { /// Document ID, or range of Document IDs - id: EqOrRangedId, + id: EqOrRangedIdDocumented, +} + +impl Example for IdRefOnly { + fn example() -> Self { + Self { + id: EqOrRangedIdDocumented::example(), + } + } } // Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the // openapi docs on an object. #[derive(NewType, Debug, PartialEq)] -#[oai(from_multipart = false, from_parameter = false, to_header = false)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] /// Document ID Reference /// /// A Reference to the Document ID Only. @@ -29,21 +43,37 @@ pub(crate) struct IdRefOnly { /// The Document Version is not considered, and will match any version. pub(crate) struct IdRefOnlyDocumented(pub(crate) IdRefOnly); +impl Example for IdRefOnlyDocumented { + fn example() -> Self { + Self(IdRefOnly::example()) + } +} + #[derive(Object, Debug, PartialEq)] /// A Reference to a Document ID/s and their version/s. pub(crate) struct VerRefWithOptionalId { /// Document ID, or range of Document IDs #[oai(skip_serializing_if_is_none)] - id: Option, + id: Option, /// Document Version, or Range of Document Versions - ver: EqOrRangedVer, + ver: EqOrRangedVerDocumented, } impl Example for VerRefWithOptionalId { fn example() -> Self { Self { id: None, - ver: EqOrRangedVer::example(), + ver: EqOrRangedVerDocumented::example(), + } + } +} + +impl VerRefWithOptionalId { + /// Returns an example of this type that includes both an `id` and `ver` + fn example_id_and_ver_ref() -> Self { + Self { + id: Some(EqOrRangedIdDocumented::example()), + ver: EqOrRangedVerDocumented::example(), } } } @@ -73,22 +103,40 @@ impl Example for VerRefWithOptionalIdDocumented { } } +impl VerRefWithOptionalIdDocumented { + /// Returns an example of this type that includes both an `id` and `ver` + fn example_id_and_ver_ref() -> Self { + Self(VerRefWithOptionalId::example_id_and_ver_ref()) + } +} + #[derive(Union, Debug, PartialEq)] -#[oai(one_of)] /// Either a Single Document ID, or a Range of Document IDs -pub(crate) enum IdAndVerRefInner { +pub(crate) enum IdAndVerRef { /// Document ID Reference ONLY IdRefOnly(IdRefOnlyDocumented), /// Version Reference with Optional Document ID Reference IdAndVerRef(VerRefWithOptionalIdDocumented), } -impl Example for IdAndVerRefInner { +impl Example for IdAndVerRef { fn example() -> Self { Self::IdAndVerRef(VerRefWithOptionalIdDocumented::example()) } } +impl IdAndVerRef { + /// Returns an example of this type that only an `id` + fn example_id_ref() -> Self { + Self::IdRefOnly(IdRefOnlyDocumented::example()) + } + + /// Returns an example of this type that includes both an `id` and `ver` + fn example_id_and_ver_ref() -> Self { + Self::IdAndVerRef(VerRefWithOptionalIdDocumented::example_id_and_ver_ref()) + } +} + // Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the // openapi docs on an object. #[derive(NewType, Debug, PartialEq)] @@ -105,11 +153,23 @@ impl Example for IdAndVerRefInner { /// to another Documents ID and/or Version. /// /// *Note: at least one of `id` or `ver` must be defined.* -pub(crate) struct IdAndVerRef(pub(crate) IdAndVerRefInner); +pub(crate) struct IdAndVerRefDocumented(pub(crate) IdAndVerRef); -impl Example for IdAndVerRef { +impl Example for IdAndVerRefDocumented { fn example() -> Self { - Self(IdAndVerRefInner::example()) + Self(IdAndVerRef::example()) + } +} + +impl IdAndVerRefDocumented { + /// Returns an example of this type that includes only an `id` + pub(crate) fn example_id_ref() -> Self { + Self(IdAndVerRef::example_id_ref()) + } + + /// Returns an example of this type that includes both an `id` and `ver` + pub(crate) fn example_id_and_ver_ref() -> Self { + Self(IdAndVerRef::example_id_and_ver_ref()) } } diff --git a/catalyst-gateway/bin/src/service/common/types/document/id.rs b/catalyst-gateway/bin/src/service/common/types/document/id.rs index 8cbca026790..c47c96d43b0 100644 --- a/catalyst-gateway/bin/src/service/common/types/document/id.rs +++ b/catalyst-gateway/bin/src/service/common/types/document/id.rs @@ -27,6 +27,10 @@ const DESCRIPTION: &str = "Unique [Document ID](https://input-output-hk.github.i UUIDv7 Formatted 128bit value."; /// Example. const EXAMPLE: &str = "01944e87-e68c-7f22-9df1-816863cfa5ff"; +/// Example minimum - Timestamp retained, random value set to all `0` +const EXAMPLE_MIN: &str = "01944e87-e68c-7000-8000-000000000000"; +/// Example maximum - Timestamp retained, random value set to all `f` +const EXAMPLE_MAX: &str = "01944e87-e68c-7fff-bfff-ffffffffffff"; /// External Documentation URI const URI: &str = "https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id"; @@ -73,6 +77,18 @@ impl Example for DocumentId { } } +impl DocumentId { + /// An example of a minimum Document ID when specifying ranges + fn example_min() -> Self { + Self(EXAMPLE_MIN.to_owned()) + } + + /// An example of a maximum Document ID when specifying ranges + fn example_max() -> Self { + Self(EXAMPLE_MAX.to_owned()) + } +} + impl TryFrom<&str> for DocumentId { type Error = anyhow::Error; @@ -99,37 +115,93 @@ impl From for DocumentId { } #[derive(Object, Debug, PartialEq)] +#[oai(example = true)] /// A range of Document IDs. -pub(crate) struct IdRangeInner { +pub(crate) struct IdRange { /// Minimum Document ID to find (inclusive) min: DocumentId, /// Maximum Document ID to find (inclusive) max: DocumentId, } +impl Example for IdRange { + fn example() -> Self { + Self { + min: DocumentId::example_min(), + max: DocumentId::example_max(), + } + } +} + +#[derive(Object, Debug, PartialEq)] +#[oai(example = true)] +/// A single Document IDs. +pub(crate) struct IdEq { + /// The exact Document ID to match against. + eq: DocumentId, +} + +impl Example for IdEq { + fn example() -> Self { + Self { + eq: DocumentId::example(), + } + } +} + // Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the // openapi docs on an object. #[derive(NewType, Debug, PartialEq)] -#[oai(from_multipart = false, from_parameter = false, to_header = false)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// ID Equals +/// +/// A specific single +/// [Document ID](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id). +pub(crate) struct IdEqDocumented(IdEq); +impl Example for IdEqDocumented { + fn example() -> Self { + Self(IdEq::example()) + } +} + +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. +#[derive(NewType, Debug, PartialEq)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] /// ID Range /// /// A range of /// [Document IDs](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#id). -pub(crate) struct IdRange(IdRangeInner); +pub(crate) struct IdRangeDocumented(IdRange); +impl Example for IdRangeDocumented { + fn example() -> Self { + Self(IdRange::example()) + } +} #[derive(Union, Debug, PartialEq)] #[oai(one_of)] /// Either a Single Document ID, or a Range of Document IDs -pub(crate) enum EqOrRangedIdInner { +pub(crate) enum EqOrRangedId { /// This exact Document ID - Eq(DocumentId), + Eq(IdEqDocumented), /// Document IDs in this range - Range(IdRange), + Range(IdRangeDocumented), } -impl Example for EqOrRangedIdInner { +impl Example for EqOrRangedId { fn example() -> Self { - Self::Eq(DocumentId::example()) + Self::Eq(IdEqDocumented::example()) } } @@ -143,10 +215,10 @@ impl Example for EqOrRangedIdInner { /// Document ID Selector /// /// Either a absolute single Document ID or a range of Document IDs -pub(crate) struct EqOrRangedId(EqOrRangedIdInner); +pub(crate) struct EqOrRangedIdDocumented(pub(crate) EqOrRangedId); -impl Example for EqOrRangedId { +impl Example for EqOrRangedIdDocumented { fn example() -> Self { - Self(EqOrRangedIdInner::example()) + Self(EqOrRangedId::example()) } } diff --git a/catalyst-gateway/bin/src/service/common/types/document/ver.rs b/catalyst-gateway/bin/src/service/common/types/document/ver.rs index dcc20772a8a..12a7aba76d0 100644 --- a/catalyst-gateway/bin/src/service/common/types/document/ver.rs +++ b/catalyst-gateway/bin/src/service/common/types/document/ver.rs @@ -29,7 +29,7 @@ UUIDv7 Formatted 128bit value."; const EXAMPLE: &str = "01944e87-e68c-7f22-9df1-816863cfa5ff"; /// Example - Range min. const EXAMPLE_MAX: &str = "01944e87-e68c-7f22-9df1-000000000000"; -/// Example. +/// Example - Ranged max const EXAMPLE_MIN: &str = "01944e87-e68c-7f22-9df1-ffffffffffff"; /// External Documentation URI const URI: &str = @@ -83,6 +83,18 @@ impl Example for DocumentVer { } } +impl DocumentVer { + /// An example of a minimum Document ID when specifying ranges + fn example_min() -> Self { + Self(EXAMPLE_MIN.to_owned()) + } + + /// An example of a maximum Document ID when specifying ranges + fn example_max() -> Self { + Self(EXAMPLE_MAX.to_owned()) + } +} + impl TryFrom<&str> for DocumentVer { type Error = anyhow::Error; @@ -111,22 +123,61 @@ impl From for DocumentVer { #[derive(Object, Debug, PartialEq)] #[oai(example = true)] /// Version Range -pub(crate) struct VerRangeInner { +/// +/// A Range of document versions from minimum to maximum inclusive. +pub(crate) struct VerRange { /// Minimum Document Version to find (inclusive) min: DocumentVer, /// Maximum Document Version to find (inclusive) max: DocumentVer, } -impl Example for VerRangeInner { +impl Example for VerRange { + fn example() -> Self { + Self { + min: DocumentVer::example_min(), + max: DocumentVer::example_max(), + } + } +} + +#[derive(Object, Debug, PartialEq)] +#[oai(example = true)] +/// A single Document IDs. +pub(crate) struct VerEq { + /// The exact Document ID to match against. + eq: DocumentVer, +} + +impl Example for VerEq { fn example() -> Self { Self { - min: DocumentVer(EXAMPLE_MIN.to_owned()), - max: DocumentVer(EXAMPLE_MAX.to_owned()), + eq: DocumentVer::example(), } } } +// Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the +// openapi docs on an object. +#[derive(NewType, Debug, PartialEq)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Version Equals +/// +/// A specific single +/// [Document Version](https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/signed_doc/spec/#ver). +pub(crate) struct VerEqDocumented(VerEq); + +impl Example for VerEqDocumented { + fn example() -> Self { + Self(VerEq::example()) + } +} + // Note: We need to do this, because POEM doesn't give us a way to set `"title"` for the // openapi docs on an object. #[derive(NewType, Debug, PartialEq)] @@ -139,11 +190,11 @@ impl Example for VerRangeInner { /// Version Range /// /// A range of [Document Versions](). -pub(crate) struct VerRange(VerRangeInner); +pub(crate) struct VerRangeDocumented(VerRange); -impl Example for VerRange { +impl Example for VerRangeDocumented { fn example() -> Self { - Self(VerRangeInner::example()) + Self(VerRange::example()) } } @@ -152,16 +203,16 @@ impl Example for VerRange { /// Document or Range of Documents /// /// Either a Single Document Version, or a Range of Document Versions -pub(crate) enum EqOrRangedVerInner { +pub(crate) enum EqOrRangedVer { /// This exact Document ID - Eq(DocumentVer), + Eq(VerEqDocumented), /// Document Versions in this range - Range(VerRange), + Range(VerRangeDocumented), } -impl Example for EqOrRangedVerInner { +impl Example for EqOrRangedVer { fn example() -> Self { - Self::Range(VerRange::example()) + Self::Range(VerRangeDocumented::example()) } } @@ -175,10 +226,10 @@ impl Example for EqOrRangedVerInner { /// Document Version Selector /// /// Either a absolute single Document Version or a range of Document Versions -pub(crate) struct EqOrRangedVer(EqOrRangedVerInner); +pub(crate) struct EqOrRangedVerDocumented(EqOrRangedVer); -impl Example for EqOrRangedVer { +impl Example for EqOrRangedVerDocumented { fn example() -> Self { - Self(EqOrRangedVerInner::example()) + Self(EqOrRangedVer::example()) } } diff --git a/catalyst-gateway/bin/src/service/common/types/generic/uuidv4.rs b/catalyst-gateway/bin/src/service/common/types/generic/uuidv4.rs index c47f84ecc82..5853594a0f3 100644 --- a/catalyst-gateway/bin/src/service/common/types/generic/uuidv4.rs +++ b/catalyst-gateway/bin/src/service/common/types/generic/uuidv4.rs @@ -22,7 +22,7 @@ const TITLE: &str = "UUIDv4"; /// Description. const DESCRIPTION: &str = "128 Bit UUID Version 4 - Random"; /// Example. -const EXAMPLE: &str = "c9993e54-1ee1-41f7-ab99-3fdec865c744 "; +const EXAMPLE: &str = "c9993e54-1ee1-41f7-ab99-3fdec865c744"; /// Length of the hex encoded string pub(crate) const ENCODED_LENGTH: usize = EXAMPLE.len(); /// Validation Regex Pattern From 69cbdd998a29d9263e5311bf2481fd8035652c63 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Wed, 15 Jan 2025 13:23:15 +0700 Subject: [PATCH 5/6] fix(cat-gateway): Response Documentation consistency for document endpoints --- .../bin/src/service/api/documents/get_document.rs | 4 ++++ .../src/service/api/documents/put_document/mod.rs | 13 +++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/catalyst-gateway/bin/src/service/api/documents/get_document.rs b/catalyst-gateway/bin/src/service/api/documents/get_document.rs index adf80bf32ae..00c5ba4b9ab 100644 --- a/catalyst-gateway/bin/src/service/api/documents/get_document.rs +++ b/catalyst-gateway/bin/src/service/api/documents/get_document.rs @@ -9,9 +9,13 @@ use crate::service::common::{responses::WithErrorResponses, types::payload::cbor #[derive(ApiResponse)] #[allow(dead_code)] pub(crate) enum Responses { + /// ## OK + /// /// The Document that was requested. #[oai(status = 200)] Ok(Cbor), + /// ## Not Found + /// /// The document could not be found. #[oai(status = 404)] NotFound, diff --git a/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs index 630a57b4b80..ddef4f7fa65 100644 --- a/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs @@ -15,20 +15,25 @@ pub(crate) const MAXIMUM_DOCUMENT_SIZE: usize = 1_048_576; #[derive(ApiResponse)] #[allow(dead_code)] pub(crate) enum Responses { + /// ## Created + /// /// The Document was stored OK for the first time. #[oai(status = 201)] Created, + /// ## No Content + /// /// The Document was already stored, and has not changed. #[oai(status = 204)] NoContent, - /// Error Response + /// ## Bad Request /// - /// The document submitted is invalid. + /// Error Response. The document submitted is invalid. #[oai(status = 400)] BadRequest(Json), - /// Payload Too Large + /// ## Content Too Large /// - /// The document exceeds the maximum size of a legitimate single document. + /// Payload Too Large. The document exceeds the maximum size of a legitimate single + /// document. #[oai(status = 413)] PayloadTooLarge, } From a510c469b8324128364631155cbddb9d0c6c8ef0 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Wed, 15 Jan 2025 13:48:47 +0700 Subject: [PATCH 6/6] fix(cat-gateway): merge conflicts --- .../bin/src/service/api/documents/get_document.rs | 2 +- .../src/service/api/documents/put_document/mod.rs | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/catalyst-gateway/bin/src/service/api/documents/get_document.rs b/catalyst-gateway/bin/src/service/api/documents/get_document.rs index 1ea2b7a6dc6..ebbf95e8f92 100644 --- a/catalyst-gateway/bin/src/service/api/documents/get_document.rs +++ b/catalyst-gateway/bin/src/service/api/documents/get_document.rs @@ -15,7 +15,7 @@ pub(crate) enum Responses { /// /// The Document that was requested. #[oai(status = 200)] - Ok(Cbor), + Ok(Cbor), /// ## Not Found /// /// The document could not be found. diff --git a/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs index 14d49966ff3..5674cd9d9d1 100644 --- a/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs +++ b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs @@ -1,20 +1,14 @@ //! Implementation of the PUT `/document` endpoint use anyhow::anyhow; +use bad_put_request::PutDocumentBadRequest; use catalyst_signed_doc::CatalystSignedDocument; use poem_openapi::{payload::Json, ApiResponse}; use crate::{ db::event::signed_docs::{FullSignedDoc, SignedDocBody}, - service::common::{ - objects::document::bad_put_request::PutDocumentBadRequest, responses::WithErrorResponses, - }, + service::common::responses::WithErrorResponses, }; -use bad_put_request::PutDocumentBadRequest; -use bytes::Bytes; -use poem_openapi::{payload::Json, ApiResponse}; - -use crate::service::common::responses::WithErrorResponses; pub(crate) mod bad_put_request; @@ -102,6 +96,6 @@ pub(crate) async fn endpoint(doc_bytes: Vec) -> AllResponses { Err(err) => AllResponses::handle_error(&err), } }, - Err(e) => Responses::BadRequest(Json(PutDocumentBadRequest::new(e.to_string()))).into(), + Err(e) => Responses::BadRequest(Json(PutDocumentBadRequest::new(format!("{e:?}")))).into(), } }