From 4fe321dacefe9c2313729e8cd483a84073aaf271 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler <51096731+LynxLynxx@users.noreply.github.com> Date: Tue, 14 Jan 2025 13:41:10 +0100 Subject: [PATCH 1/4] feat(cat-voices): Yes/No choice component (#1508) * feat: create ui layout * feat: adding logic to widget * feat: creating segmented button formfield * feat: changing type of yes no choice form field * chore: ading main axis size to min --- .../yes_no_choice_widget.dart | 175 ++++++++++++++++++ .../tiles/document_builder_section_tile.dart | 16 +- .../definitions/yes_no_choice_definition.dart | 1 + .../lib/src/document/document_validator.dart | 11 +- 4 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 catalyst_voices/apps/voices/lib/widgets/document_builder/yes_no_choice_widget.dart diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/yes_no_choice_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/yes_no_choice_widget.dart new file mode 100644 index 00000000000..4c3fcf12e60 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/yes_no_choice_widget.dart @@ -0,0 +1,175 @@ +import 'package:catalyst_voices/common/ext/document_property_ext.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; + +class YesNoChoiceWidget extends StatefulWidget { + final DocumentProperty property; + final ValueChanged onChanged; + final bool isEditMode; + final bool isRequired; + + const YesNoChoiceWidget({ + super.key, + required this.property, + required this.onChanged, + required this.isEditMode, + required this.isRequired, + }); + + @override + State createState() => _YesNoChoiceWidgetState(); +} + +class _YesNoChoiceWidgetState extends State { + late bool? selectedValue; + + String get _description => widget.property.formattedDescription; + + @override + void initState() { + super.initState(); + + _handleInitialValue(); + } + + @override + void didUpdateWidget(covariant YesNoChoiceWidget oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.property.value != widget.property.value) { + _handleInitialValue(); + } + + if (oldWidget.isEditMode != widget.isEditMode && + widget.isEditMode == false) { + _handleInitialValue(); + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (_description.isNotEmpty) ...[ + Text( + _description, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 8), + ], + _YesNoChoiceSegmentButton( + context, + value: selectedValue, + enabled: widget.isEditMode, + onChanged: _handleValueChanged, + validator: (value) { + // TODO(dtscalac): add validation + final result = widget.property.schema.validatePropertyValue(value); + + return LocalizedDocumentValidationResult.from(result) + .message(context); + }, + ), + ], + ); + } + + void _handleInitialValue() { + selectedValue = widget.property.value; + } + + void _handleValueChanged(bool? value) { + setState(() { + selectedValue = value; + }); + if (value == null && widget.property.value != value) { + _notifyChangeListener(value); + } + } + + void _notifyChangeListener(bool? value) { + widget.onChanged( + DocumentChange( + nodeId: widget.property.schema.nodeId, + value: value, + ), + ); + } +} + +class _YesNoChoiceSegmentButton extends FormField { + final bool? value; + final ValueChanged? onChanged; + + _YesNoChoiceSegmentButton( + BuildContext context, { + super.key, + required this.value, + required this.onChanged, + super.validator, + super.enabled, + AutovalidateMode autovalidateMode = AutovalidateMode.onUserInteraction, + }) : super( + initialValue: value, + autovalidateMode: autovalidateMode, + builder: (field) { + void onChangedHandler(Set selected) { + final newValue = selected.isEmpty ? null : selected.first; + field.didChange(newValue); + onChanged?.call(newValue); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + IgnorePointer( + ignoring: !enabled, + child: VoicesSegmentedButton( + key: key, + segments: [ + ButtonSegment( + value: true, + label: Text(context.l10n.yes), + ), + ButtonSegment( + value: false, + label: Text(context.l10n.no), + ), + ], + selected: value != null ? {value} : {}, + onChanged: onChangedHandler, + emptySelectionAllowed: true, + style: _getButtonStyle(field), + ), + ), + if (field.hasError) + Text( + field.errorText ?? context.l10n.snackbarErrorLabelText, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: Theme.of(context).colorScheme.error), + ), + ], + ); + }, + ); + + static ButtonStyle? _getButtonStyle(FormFieldState field) { + if (field.errorText == null) return null; + + return ButtonStyle( + side: WidgetStatePropertyAll( + BorderSide( + color: Theme.of(field.context).colorScheme.error, + ), + ), + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart index 88f0465f37e..87cb6fe62d9 100644 --- a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart @@ -3,6 +3,7 @@ import 'package:catalyst_voices/widgets/document_builder/document_token_value_wi import 'package:catalyst_voices/widgets/document_builder/single_dropdown_selection_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_grouped_tag_selector_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_line_https_url_widget.dart.dart'; +import 'package:catalyst_voices/widgets/document_builder/yes_no_choice_widget.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; @@ -113,7 +114,11 @@ class _DocumentBuilderSectionTileState void _toggleEditMode() { setState(() { _isEditMode = !_isEditMode; - _pendingChanges.clear(); + if (!_isEditMode) { + _pendingChanges.clear(); + _editedSection = widget.section; + _builder = _editedSection.toBuilder(); + } }); } @@ -218,7 +223,6 @@ class _PropertyBuilder extends StatelessWidget { case TagGroupDefinition(): case TagSelectionDefinition(): case DurationInMonthsDefinition(): - case YesNoChoiceDefinition(): case SPDXLicenceOrUrlDefinition(): case LanguageCodeDefinition(): throw UnimplementedError('${definition.type} not implemented'); @@ -269,6 +273,14 @@ class _PropertyBuilder extends StatelessWidget { isEditMode: isEditMode, onChanged: onChanged, ); + case YesNoChoiceDefinition(): + final castProperty = definition.castProperty(property); + return YesNoChoiceWidget( + property: castProperty, + onChanged: onChanged, + isEditMode: isEditMode, + isRequired: castProperty.schema.isRequired, + ); } } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/definitions/yes_no_choice_definition.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/definitions/yes_no_choice_definition.dart index 45144c62874..6cefc153d3a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/definitions/yes_no_choice_definition.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/definitions/yes_no_choice_definition.dart @@ -16,6 +16,7 @@ final class YesNoChoiceDefinition extends BaseDocumentDefinition { DocumentSchemaProperty schema, bool? value, ) { + // TODO(dtscalac): validate yes no choice return DocumentValidator.validateBool(schema, value); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_validator.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_validator.dart index ac141a8dafe..2e71bba9d48 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_validator.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_validator.dart @@ -95,7 +95,16 @@ final class DocumentValidator { // ignore: avoid_positional_boolean_parameters bool? value, ) { - return validateBasic(schema, value); + final result = validateBasic(schema, value); + + if (result.isInvalid) { + return result; + } + + if (value == null) { + return MissingRequiredDocumentValue(invalidNodeId: schema.nodeId); + } + return const SuccessfulDocumentValidation(); } static DocumentValidationResult validatePattern( From e169a08c36418e9927a3bda141cbce793f987ec0 Mon Sep 17 00:00:00 2001 From: Apisit Ritruengroj <38898766+apskhem@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:21:57 +0700 Subject: [PATCH 2/4] feat(cat-gateway): Improve observability and metrics (#1495) * refactor(cat-gateway): Restructure cat-gateway metrics module (#1483) * feat: initial files * refactor: new structure * chore: clean unused clippy lint Co-authored-by: Alex Pozhylenkov --------- Co-authored-by: Steven Johnson Co-authored-by: Alex Pozhylenkov --- catalyst-gateway/bin/src/main.rs | 1 + .../bin/src/metrics/chain_follower.rs | 1 + .../bin/src/metrics/chain_indexer.rs | 1 + catalyst-gateway/bin/src/metrics/endpoint.rs | 60 +++++++++++++ catalyst-gateway/bin/src/metrics/memory.rs | 1 + catalyst-gateway/bin/src/metrics/mod.rs | 18 ++++ .../bin/src/service/poem_service.rs | 3 +- .../utilities/middleware/tracing_mw.rs | 84 ++----------------- 8 files changed, 93 insertions(+), 76 deletions(-) create mode 100644 catalyst-gateway/bin/src/metrics/chain_follower.rs create mode 100644 catalyst-gateway/bin/src/metrics/chain_indexer.rs create mode 100644 catalyst-gateway/bin/src/metrics/endpoint.rs create mode 100644 catalyst-gateway/bin/src/metrics/memory.rs create mode 100644 catalyst-gateway/bin/src/metrics/mod.rs diff --git a/catalyst-gateway/bin/src/main.rs b/catalyst-gateway/bin/src/main.rs index 7faf44550bc..1d7e3965961 100644 --- a/catalyst-gateway/bin/src/main.rs +++ b/catalyst-gateway/bin/src/main.rs @@ -7,6 +7,7 @@ mod cli; mod db; mod jinja; mod logger; +mod metrics; mod service; mod settings; mod utils; diff --git a/catalyst-gateway/bin/src/metrics/chain_follower.rs b/catalyst-gateway/bin/src/metrics/chain_follower.rs new file mode 100644 index 00000000000..26d7e51282e --- /dev/null +++ b/catalyst-gateway/bin/src/metrics/chain_follower.rs @@ -0,0 +1 @@ +//! Metrics related to Chain Follower analytics. diff --git a/catalyst-gateway/bin/src/metrics/chain_indexer.rs b/catalyst-gateway/bin/src/metrics/chain_indexer.rs new file mode 100644 index 00000000000..ee8728166ab --- /dev/null +++ b/catalyst-gateway/bin/src/metrics/chain_indexer.rs @@ -0,0 +1 @@ +//! Metrics related to Chain Indexer analytics. diff --git a/catalyst-gateway/bin/src/metrics/endpoint.rs b/catalyst-gateway/bin/src/metrics/endpoint.rs new file mode 100644 index 00000000000..5f625bc7302 --- /dev/null +++ b/catalyst-gateway/bin/src/metrics/endpoint.rs @@ -0,0 +1,60 @@ +//! Metrics related to endpoint analytics. + +use std::sync::LazyLock; + +use prometheus::{register_histogram_vec, register_int_counter_vec, HistogramVec, IntCounterVec}; + +/// Labels for the metrics +const METRIC_LABELS: [&str; 3] = ["endpoint", "method", "status_code"]; +/// Labels for the client metrics +const CLIENT_METRIC_LABELS: [&str; 2] = ["client", "status_code"]; + +// Prometheus Metrics maintained by the service + +/// HTTP Request duration histogram. +pub(crate) static HTTP_REQ_DURATION_MS: LazyLock = LazyLock::new(|| { + register_histogram_vec!( + "http_request_duration_ms", + "Duration of HTTP requests in milliseconds", + &METRIC_LABELS + ) + .unwrap() +}); + +/// HTTP Request CPU Time histogram. +pub(crate) static HTTP_REQ_CPU_TIME_MS: LazyLock = LazyLock::new(|| { + register_histogram_vec!( + "http_request_cpu_time_ms", + "CPU Time of HTTP requests in milliseconds", + &METRIC_LABELS + ) + .unwrap() +}); + +// No Tacho implemented to enable this. +// static ref HTTP_REQUEST_RATE: GaugeVec = register_gauge_vec!( +// "http_request_rate", +// "Rate of HTTP requests per second", +// &METRIC_LABELS +// ) +// .unwrap(); + +/// HTTP Request count histogram. +pub(crate) static HTTP_REQUEST_COUNT: LazyLock = LazyLock::new(|| { + register_int_counter_vec!( + "http_request_count", + "Number of HTTP requests", + &METRIC_LABELS + ) + .unwrap() +}); + +/// Client Request Count histogram. +pub(crate) static CLIENT_REQUEST_COUNT: LazyLock = LazyLock::new(|| { + register_int_counter_vec!( + "client_request_count", + "Number of HTTP requests per client", + &CLIENT_METRIC_LABELS + ) + .unwrap() +}); diff --git a/catalyst-gateway/bin/src/metrics/memory.rs b/catalyst-gateway/bin/src/metrics/memory.rs new file mode 100644 index 00000000000..7da8bd333e9 --- /dev/null +++ b/catalyst-gateway/bin/src/metrics/memory.rs @@ -0,0 +1 @@ +//! Metrics related to memory analytics. diff --git a/catalyst-gateway/bin/src/metrics/mod.rs b/catalyst-gateway/bin/src/metrics/mod.rs new file mode 100644 index 00000000000..ba8d71503da --- /dev/null +++ b/catalyst-gateway/bin/src/metrics/mod.rs @@ -0,0 +1,18 @@ +//! This module contains submodules related to metrics report and analytics. + +use prometheus::{default_registry, Registry}; + +pub(crate) mod chain_follower; +pub(crate) mod chain_indexer; +pub(crate) mod endpoint; +pub(crate) mod memory; + +/// Initialize Prometheus metrics. +/// +/// ## Returns +/// +/// Returns the default prometheus registry. +#[must_use] +pub(crate) fn init_prometheus() -> Registry { + default_registry().clone() +} diff --git a/catalyst-gateway/bin/src/service/poem_service.rs b/catalyst-gateway/bin/src/service/poem_service.rs index ebc795f3917..f9ef7776860 100644 --- a/catalyst-gateway/bin/src/service/poem_service.rs +++ b/catalyst-gateway/bin/src/service/poem_service.rs @@ -11,12 +11,13 @@ use poem::{ }; use crate::{ + metrics::init_prometheus, service::{ api::mk_api, docs::{docs, favicon}, utilities::{ catch_panic::{set_panic_hook, ServicePanicHandler}, - middleware::tracing_mw::{init_prometheus, Tracing}, + middleware::tracing_mw::Tracing, }, }, settings::Settings, diff --git a/catalyst-gateway/bin/src/service/utilities/middleware/tracing_mw.rs b/catalyst-gateway/bin/src/service/utilities/middleware/tracing_mw.rs index 385d247840c..0584e5bd83c 100644 --- a/catalyst-gateway/bin/src/service/utilities/middleware/tracing_mw.rs +++ b/catalyst-gateway/bin/src/service/utilities/middleware/tracing_mw.rs @@ -1,5 +1,6 @@ //! Full Tracing and metrics middleware. -use std::{sync::LazyLock, time::Instant}; + +use std::time::Instant; use cpu_time::ProcessTime; // ThreadTime doesn't work. use poem::{ @@ -8,74 +9,17 @@ use poem::{ Endpoint, Error, FromRequest, IntoResponse, Middleware, PathPattern, Request, Response, Result, }; use poem_openapi::OperationId; -use prometheus::{ - default_registry, register_histogram_vec, register_int_counter_vec, HistogramVec, - IntCounterVec, Registry, -}; use tracing::{error, field, Instrument, Level, Span}; use ulid::Ulid; use uuid::Uuid; -use crate::{settings::Settings, utils::blake2b_hash::generate_uuid_string_from_data}; - -/// Labels for the metrics -const METRIC_LABELS: [&str; 3] = ["endpoint", "method", "status_code"]; -/// Labels for the client metrics -const CLIENT_METRIC_LABELS: [&str; 2] = ["client", "status_code"]; - -// Prometheus Metrics maintained by the service - -/// HTTP Request duration histogram. -static HTTP_REQ_DURATION_MS: LazyLock = LazyLock::new(|| { - #[allow(clippy::ignored_unit_patterns)] - register_histogram_vec!( - "http_request_duration_ms", - "Duration of HTTP requests in milliseconds", - &METRIC_LABELS - ) - .unwrap() -}); - -/// HTTP Request CPU Time histogram. -static HTTP_REQ_CPU_TIME_MS: LazyLock = LazyLock::new(|| { - #[allow(clippy::ignored_unit_patterns)] - register_histogram_vec!( - "http_request_cpu_time_ms", - "CPU Time of HTTP requests in milliseconds", - &METRIC_LABELS - ) - .unwrap() -}); - -// No Tacho implemented to enable this. -// static ref HTTP_REQUEST_RATE: GaugeVec = register_gauge_vec!( -// "http_request_rate", -// "Rate of HTTP requests per second", -// &METRIC_LABELS -// ) -// .unwrap(); - -/// HTTP Request count histogram. -static HTTP_REQUEST_COUNT: LazyLock = LazyLock::new(|| { - #[allow(clippy::ignored_unit_patterns)] - register_int_counter_vec!( - "http_request_count", - "Number of HTTP requests", - &METRIC_LABELS - ) - .unwrap() -}); - -/// Client Request Count histogram. -static CLIENT_REQUEST_COUNT: LazyLock = LazyLock::new(|| { - #[allow(clippy::ignored_unit_patterns)] - register_int_counter_vec!( - "client_request_count", - "Number of HTTP requests per client", - &CLIENT_METRIC_LABELS - ) - .unwrap() -}); +use crate::{ + metrics::endpoint::{ + CLIENT_REQUEST_COUNT, HTTP_REQUEST_COUNT, HTTP_REQ_CPU_TIME_MS, HTTP_REQ_DURATION_MS, + }, + settings::Settings, + utils::blake2b_hash::generate_uuid_string_from_data, +}; // Currently no way to get these values. TODO. // Panic Request Count histogram. @@ -389,13 +333,3 @@ impl Endpoint for TracingEndpoint { response } } - -/// Initialize Prometheus metrics. -/// -/// ## Returns -/// -/// Returns the default prometheus registry. -#[must_use] -pub(crate) fn init_prometheus() -> Registry { - default_registry().clone() -} From f9eca949ca1b21e89ab5eca27ca2925687405cfa Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Wed, 15 Jan 2025 16:31:39 +0700 Subject: [PATCH 3/4] feat(cat-gateway): Add document endpoints (#1470) * feat(cat-gateway): Add document get endpoint * feat(cat-gateway): Add generic cbor payload type * fix(docs): spelling * fix(cat-gateway): code format * feat(cat-gateway): Add document put endpoint * fix(cat-gateway): fix documentation for the error responses. * feat(general): Bump cat-ci to v3.2.31 to get documentation updates * feat(docs): Add alternative OpenAPI viewers because Elements has issues * feat(cat-gateway): Add document index endpoint (request side only) * feat(cat-gateway): Add response type for document index endpoint * fix(cat-gateway): code format * fix api lint errors * fix(cat-gateway): Response Documentation consistency for document endpoints * feat: increase time schemathesis waits for schema --------- Co-authored-by: kukkok3 <93382903+kukkok3@users.noreply.github.com> --- .config/dictionaries/project.dic | 2 + Earthfile | 6 +- catalyst-gateway/Earthfile | 2 +- catalyst-gateway/bin/Cargo.toml | 6 +- .../src/service/api/documents/get_document.rs | 34 +++ .../bin/src/service/api/documents/mod.rs | 113 +++++++++ .../post_document_index_query/mod.rs | 52 ++++ .../post_document_index_query/query_filter.rs | 154 ++++++++++++ .../post_document_index_query/response.rs | 168 +++++++++++++ .../documents/put_document/bad_put_request.rs | 28 +++ .../service/api/documents/put_document/mod.rs | 50 ++++ catalyst-gateway/bin/src/service/api/mod.rs | 6 +- .../service/common/objects/document/mod.rs | 1 + .../bin/src/service/common/objects/mod.rs | 1 + .../bin/src/service/common/tags.rs | 2 + .../service/common/types/document/doc_ref.rs | 195 +++++++++++++++ .../service/common/types/document/doc_type.rs | 102 ++++++++ .../src/service/common/types/document/id.rs | 224 +++++++++++++++++ .../src/service/common/types/document/mod.rs | 6 + .../src/service/common/types/document/ver.rs | 235 ++++++++++++++++++ .../src/service/common/types/generic/mod.rs | 2 + .../service/common/types/generic/uuidv4.rs | 94 +++++++ .../service/common/types/generic/uuidv7.rs | 94 +++++++ .../bin/src/service/common/types/mod.rs | 2 + .../src/service/common/types/payload/cbor.rs | 164 ++++++++++++ .../src/service/common/types/payload/mod.rs | 6 + catalyst-gateway/event-db/Earthfile | 2 +- catalyst-gateway/tests/Earthfile | 2 +- catalyst-gateway/tests/api_tests/Earthfile | 2 +- .../tests/schemathesis_tests/Earthfile | 2 +- catalyst_voices/Earthfile | 2 +- .../wallet-automation/Earthfile | 4 +- .../libs/catalyst_key_derivation/Earthfile | 4 +- .../catalyst_key_derivation/rust/Earthfile | 2 +- .../utilities/uikit_example/Earthfile | 2 +- docs/Earthfile | 2 +- docs/src/api/cat-gateway/.pages | 8 +- docs/src/api/cat-gateway/index.md | 7 + docs/src/api/cat-gateway/openapi-explorer.md | 12 + docs/src/api/cat-gateway/openapi-scalar.md | 12 + docs/src/api/cat-gateway/openapi-spec.md | 11 + .../cat-gateway/openapi-stoplight-elements.md | 11 + .../{openapi.md => openapi-swagger.md} | 5 +- .../templates/openapi_explorer.html | 17 ++ .../src/api/cat-gateway/templates/scalar.html | 25 ++ .../stoplight_elements.html} | 13 +- .../api/cat-gateway/templates/swagger.html | 31 +++ utilities/docs-preview/Earthfile | 2 +- 48 files changed, 1891 insertions(+), 36 deletions(-) create mode 100644 catalyst-gateway/bin/src/service/api/documents/get_document.rs create mode 100644 catalyst-gateway/bin/src/service/api/documents/mod.rs 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 create mode 100644 catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs create mode 100644 catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs create mode 100644 catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs create mode 100644 catalyst-gateway/bin/src/service/common/objects/document/mod.rs 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 create mode 100644 catalyst-gateway/bin/src/service/common/types/generic/uuidv7.rs create mode 100644 catalyst-gateway/bin/src/service/common/types/payload/cbor.rs create mode 100644 catalyst-gateway/bin/src/service/common/types/payload/mod.rs create mode 100644 docs/src/api/cat-gateway/openapi-explorer.md create mode 100644 docs/src/api/cat-gateway/openapi-scalar.md create mode 100644 docs/src/api/cat-gateway/openapi-spec.md create mode 100644 docs/src/api/cat-gateway/openapi-stoplight-elements.md rename docs/src/api/cat-gateway/{openapi.md => openapi-swagger.md} (65%) create mode 100644 docs/src/api/cat-gateway/templates/openapi_explorer.html create mode 100644 docs/src/api/cat-gateway/templates/scalar.html rename docs/src/api/cat-gateway/{stoplight_template.html => templates/stoplight_elements.html} (58%) create mode 100644 docs/src/api/cat-gateway/templates/swagger.html diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 52e4d0d526f..6645f1d4466 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -8,6 +8,7 @@ adminer afinet androidx anypolicy +apirequest appbar appspot Arbritrary @@ -319,6 +320,7 @@ Unstaked upskilling UTXO Utxos +uuidv varint Vespr vite diff --git a/Earthfile b/Earthfile index 5112c3bafca..4384c051130 100644 --- a/Earthfile +++ b/Earthfile @@ -1,8 +1,8 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/mdlint:v3.2.27 AS mdlint-ci -IMPORT github.com/input-output-hk/catalyst-ci/earthly/cspell:v3.2.27 AS cspell-ci -IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.27 AS postgresql-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/mdlint:v3.2.31 AS mdlint-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/cspell:v3.2.31 AS cspell-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.31 AS postgresql-ci FROM debian:stable-slim diff --git a/catalyst-gateway/Earthfile b/catalyst-gateway/Earthfile index 5ccd7eae33d..2b5c945fd03 100644 --- a/catalyst-gateway/Earthfile +++ b/catalyst-gateway/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.28 AS rust-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.31 AS rust-ci #cspell: words rustfmt toolsets USERARCH stdcfgs diff --git a/catalyst-gateway/bin/Cargo.toml b/catalyst-gateway/bin/Cargo.toml index 136add10bd5..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", @@ -96,6 +96,8 @@ bech32 = "0.11.0" const_format = "0.2.33" regex = "1.11.1" minijinja = "2.5.0" +bytes = "1.9.0" +mime = "0.3.17" [dev-dependencies] proptest = "1.5.0" diff --git a/catalyst-gateway/bin/src/service/api/documents/get_document.rs b/catalyst-gateway/bin/src/service/api/documents/get_document.rs new file mode 100644 index 00000000000..00c5ba4b9ab --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/get_document.rs @@ -0,0 +1,34 @@ +//! Implementation of the GET `/document` endpoint + +use poem::Body; +use poem_openapi::ApiResponse; + +use crate::service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor}; + +/// Endpoint responses. +#[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, +} + +/// All responses. +pub(crate) type AllResponses = WithErrorResponses; + +/// # GET `/document` +#[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] +pub(crate) async fn endpoint(document_id: uuid::Uuid, version: Option) -> AllResponses { + let _doc = document_id; + let _ver = version; + + Responses::NotFound.into() +} diff --git a/catalyst-gateway/bin/src/service/api/documents/mod.rs b/catalyst-gateway/bin/src/service/api/documents/mod.rs new file mode 100644 index 00000000000..5714232ac31 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/mod.rs @@ -0,0 +1,113 @@ +//! Signed Documents API endpoints + +use anyhow::anyhow; +use poem::{error::ReadBodyError, Body}; +use poem_openapi::{ + param::{Path, Query}, + payload::Json, + OpenApi, +}; +use post_document_index_query::query_filter::DocumentIndexQueryFilterBody; +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}, + }, + utilities::middleware::schema_validation::schema_version_validation, +}; + +mod get_document; +mod post_document_index_query; +mod put_document; + +/// Cardano Follower API Endpoints +pub(crate) struct DocumentApi; + +#[OpenApi(tag = "ApiTags::Documents")] +impl DocumentApi { + /// Get A Signed Document. + /// + /// This endpoint returns either a specific or latest version of a registered signed + /// document. + #[oai( + path = "/draft/document/:document_id", + method = "get", + operation_id = "getDocument", + transform = "schema_version_validation" + )] + async fn get_document( + &self, /// UUIDv7 Document ID to retrieve + document_id: Path, + /// UUIDv7 Version of the Document to retrieve, if omitted, returns the latest + /// version. + version: Query>, + /// No Authorization required, but Token permitted. + _auth: NoneOrRBAC, + ) -> get_document::AllResponses { + let Ok(doc_id) = document_id.0.try_into() else { + let err = anyhow!("Invalid UUIDv7"); // Should not happen as UUIDv7 is validating. + return get_document::AllResponses::internal_error(&err); + }; + let Ok(ver_id) = version.0.map(std::convert::TryInto::try_into).transpose() else { + let err = anyhow!("Invalid UUIDv7"); // Should not happen as UUIDv7 is validating. + return get_document::AllResponses::internal_error(&err); + }; + get_document::endpoint(doc_id, ver_id).await + } + + /// Put A Signed Document. + /// + /// This endpoint returns OK if the document is valid, able to be put by the + /// submitter, and if it already exists, is identical to the existing document. + #[oai( + path = "/draft/document", + method = "put", + operation_id = "putDocument", + transform = "schema_version_validation" + )] + async fn put_document( + &self, /// The document to PUT + document: Cbor, + /// Authorization required. + _auth: CatalystRBACSecurityScheme, + ) -> put_document::AllResponses { + 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() + }, + } + } + + /// 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, + page: Query>, + limit: Query>, + /// Authorization required. + _auth: CatalystRBACSecurityScheme, + ) -> post_document_index_query::AllResponses { + 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 new file mode 100644 index 00000000000..26ca8c004ac --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/mod.rs @@ -0,0 +1,52 @@ +//! Document Index Query + +use poem_openapi::{payload::Json, ApiResponse, Object}; +use query_filter::DocumentIndexQueryFilter; +use response::DocumentIndexListDocumented; + +use super::common; +use crate::service::common::responses::WithErrorResponses; + +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(Json), + /// ## 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, + 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/query_filter.rs b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs new file mode 100644 index 00000000000..7c82ae2d859 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/query_filter.rs @@ -0,0 +1,154 @@ +//! 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 super::common::types::document::ver::EqOrRangedVerDocumented; +use crate::service::common::types::document::{ + doc_ref::IdAndVerRefDocumented, doc_type::DocumentType, id::EqOrRangedIdDocumented, +}; + +/// 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(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() + } + } +} + +// 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/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..fdd46f0f6ba --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/post_document_index_query/response.rs @@ -0,0 +1,168 @@ +//! Cip36 Registration Query Endpoint Response +use poem_openapi::{types::Example, NewType, Object}; + +use self::common::types::document::{ + doc_ref::DocumentReference, doc_type::DocumentType, id::DocumentId, ver::DocumentVer, +}; +use crate::service::common; + +/// 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/api/documents/put_document/bad_put_request.rs b/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs new file mode 100644 index 00000000000..5d3bad1903a --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/put_document/bad_put_request.rs @@ -0,0 +1,28 @@ +//! Bad Document PUT request. + +use poem_openapi::{types::Example, Object}; + +/// Configuration Data Validation Error. +#[derive(Object, Default)] +#[oai(example = true)] +pub(crate) struct PutDocumentBadRequest { + /// Error messages. + #[oai(validator(max_length = "100", pattern = "^[0-9a-zA-Z].*$"))] + error: String, + // TODO: Add optional verbose error fields for documents that fail validation. +} + +impl PutDocumentBadRequest { + /// Create a new instance of `ConfigBadRequest`. + pub(crate) fn new(error: &str) -> Self { + Self { + error: error.to_owned(), + } + } +} + +impl Example for PutDocumentBadRequest { + fn example() -> Self { + PutDocumentBadRequest::new("Missing Document in request body") + } +} 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 new file mode 100644 index 00000000000..ddef4f7fa65 --- /dev/null +++ b/catalyst-gateway/bin/src/service/api/documents/put_document/mod.rs @@ -0,0 +1,50 @@ +//! Implementation of the PUT `/document` endpoint + +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; + +/// Maximum size of a Signed Document (1MB) +pub(crate) const MAXIMUM_DOCUMENT_SIZE: usize = 1_048_576; + +/// Endpoint responses. +#[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, + /// ## Bad Request + /// + /// Error Response. The document submitted is invalid. + #[oai(status = 400)] + BadRequest(Json), + /// ## Content Too Large + /// + /// Payload Too Large. The document exceeds the maximum size of a legitimate single + /// document. + #[oai(status = 413)] + PayloadTooLarge, +} + +/// All responses. +pub(crate) type AllResponses = WithErrorResponses; + +/// # PUT `/document` +#[allow(clippy::unused_async, clippy::no_effect_underscore_binding)] +pub(crate) async fn endpoint(document: Bytes) -> AllResponses { + let _doc = document; + + Responses::BadRequest(Json(PutDocumentBadRequest::new("unimplemented"))).into() +} diff --git a/catalyst-gateway/bin/src/service/api/mod.rs b/catalyst-gateway/bin/src/service/api/mod.rs index 0df680295f7..44a96af2d4b 100644 --- a/catalyst-gateway/bin/src/service/api/mod.rs +++ b/catalyst-gateway/bin/src/service/api/mod.rs @@ -5,6 +5,7 @@ use std::net::IpAddr; use config::ConfigApi; +use documents::DocumentApi; use gethostname::gethostname; use health::HealthApi; use legacy::LegacyApi; @@ -16,6 +17,7 @@ use crate::settings::Settings; pub(crate) mod cardano; mod config; +mod documents; mod health; mod legacy; @@ -50,12 +52,14 @@ const TERMS_OF_SERVICE: &str = "https://github.com/input-output-hk/catalyst-voices/blob/main/CODE_OF_CONDUCT.md"; /// Create the `OpenAPI` definition -pub(crate) fn mk_api() -> OpenApiService<(HealthApi, CardanoApi, ConfigApi, LegacyApi), ()> { +pub(crate) fn mk_api( +) -> OpenApiService<(HealthApi, CardanoApi, ConfigApi, DocumentApi, LegacyApi), ()> { let mut service = OpenApiService::new( ( HealthApi, (cardano::Api, cardano::staking::Api, cardano::cip36::Api), ConfigApi, + DocumentApi, (legacy::RegistrationApi, legacy::V0Api, legacy::V1Api), ), API_TITLE, diff --git a/catalyst-gateway/bin/src/service/common/objects/document/mod.rs b/catalyst-gateway/bin/src/service/common/objects/document/mod.rs new file mode 100644 index 00000000000..683f91959c2 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/objects/document/mod.rs @@ -0,0 +1 @@ +//! Signed Document Objects diff --git a/catalyst-gateway/bin/src/service/common/objects/mod.rs b/catalyst-gateway/bin/src/service/common/objects/mod.rs index c5e863052f3..b6a2272967e 100644 --- a/catalyst-gateway/bin/src/service/common/objects/mod.rs +++ b/catalyst-gateway/bin/src/service/common/objects/mod.rs @@ -2,5 +2,6 @@ pub(crate) mod cardano; pub(crate) mod config; +pub(crate) mod document; pub(crate) mod generic; pub(crate) mod legacy; diff --git a/catalyst-gateway/bin/src/service/common/tags.rs b/catalyst-gateway/bin/src/service/common/tags.rs index d65618e6c9b..82d8a7ae98e 100644 --- a/catalyst-gateway/bin/src/service/common/tags.rs +++ b/catalyst-gateway/bin/src/service/common/tags.rs @@ -12,6 +12,8 @@ pub(crate) enum ApiTags { // Registration, /// Service Configuration and Status. Config, + /// Signed Document endpoints + Documents, /// Legacy Mobile App Support. Legacy, } 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..f9310bbe1a8 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/doc_ref.rs @@ -0,0 +1,195 @@ +//! 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::{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: 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, + example = true +)] +/// 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(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, + /// Document Version, or Range of Document Versions + ver: EqOrRangedVerDocumented, +} + +impl Example for VerRefWithOptionalId { + fn example() -> Self { + Self { + id: None, + 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(), + } + } +} + +// 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(pub(crate) VerRefWithOptionalId); + +impl Example for VerRefWithOptionalIdDocumented { + fn example() -> Self { + Self(VerRefWithOptionalId::example()) + } +} + +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)] +/// Either a Single Document ID, or a Range of Document IDs +pub(crate) enum IdAndVerRef { + /// Document ID Reference ONLY + IdRefOnly(IdRefOnlyDocumented), + /// Version Reference with Optional Document ID Reference + IdAndVerRef(VerRefWithOptionalIdDocumented), +} + +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)] +#[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 IdAndVerRefDocumented(pub(crate) IdAndVerRef); + +impl Example for IdAndVerRefDocumented { + fn example() -> Self { + 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()) + } +} + +#[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()), + } + } +} 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..561fbf93b16 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/doc_type.rs @@ -0,0 +1,102 @@ +//! 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 self::generic::uuidv4; +use crate::service::common::types::{generic, string_types::impl_string_types}; + +/// 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..c47c96d43b0 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/id.rs @@ -0,0 +1,224 @@ +//! Signed Document ID +//! +//! `UUIDv7` Encoded Document ID. + +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}, + NewType, Object, Union, +}; +use serde_json::Value; + +use self::generic::uuidv7; +use crate::service::common::types::{generic, string_types::impl_string_types}; + +/// 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"; +/// 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"; +/// 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 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; + + 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)] +#[oai(example = true)] +/// A range of Document IDs. +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, + 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 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 EqOrRangedId { + /// This exact Document ID + Eq(IdEqDocumented), + /// Document IDs in this range + Range(IdRangeDocumented), +} + +impl Example for EqOrRangedId { + fn example() -> Self { + Self::Eq(IdEqDocumented::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 EqOrRangedIdDocumented(pub(crate) EqOrRangedId); + +impl Example for EqOrRangedIdDocumented { + fn example() -> Self { + Self(EqOrRangedId::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..3a1263a6fe6 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/mod.rs @@ -0,0 +1,6 @@ +//! Signed Document Types + +pub(crate) mod doc_ref; +pub(crate) mod doc_type; +pub(crate) mod id; +pub(crate) mod ver; 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..12a7aba76d0 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/document/ver.rs @@ -0,0 +1,235 @@ +//! Signed Document Version +//! +//! `UUIDv7` Encoded Document Version. + +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}, + NewType, Object, Union, +}; +use serde_json::Value; + +use self::generic::uuidv7; +use crate::service::common::types::{generic, string_types::impl_string_types}; + +/// 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 - Ranged max +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 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; + + 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 +/// +/// 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 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 { + 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)] +#[oai( + from_multipart = false, + from_parameter = false, + to_header = false, + example = true +)] +/// Version Range +/// +/// A range of [Document Versions](). +pub(crate) struct VerRangeDocumented(VerRange); + +impl Example for VerRangeDocumented { + fn example() -> Self { + Self(VerRange::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 EqOrRangedVer { + /// This exact Document ID + Eq(VerEqDocumented), + /// Document Versions in this range + Range(VerRangeDocumented), +} + +impl Example for EqOrRangedVer { + fn example() -> Self { + Self::Range(VerRangeDocumented::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 EqOrRangedVerDocumented(EqOrRangedVer); + +impl Example for EqOrRangedVerDocumented { + fn example() -> Self { + Self(EqOrRangedVer::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 a04a6ed0a59..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,3 +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..5853594a0f3 --- /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 new file mode 100644 index 00000000000..a464e08fdc1 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/generic/uuidv7.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 = "UUIDv7"; +/// Description. +const DESCRIPTION: &str = "128 Bit UUID Version 7 - Timestamp + Random"; +/// Example. +const EXAMPLE: &str = "01943a32-9f35-7a14-b364-36ad693465e6"; +/// 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}-7[0-9a-fA-F]{3}-[89abAB][0-9a-fA0F]{3}-[0-9a-fA-F]{12}$"; +/// Format +pub(crate) const FORMAT: &str = "uuidv7"; + +/// 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(uuidv7: &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(uuidv7) { + uuid.get_version() == Some(uuid::Version::SortRand) + } else { + false + } +} + +impl_string_types!(UUIDv7, "string", FORMAT, Some(SCHEMA.clone()), is_valid); + +impl Example for UUIDv7 { + /// An example `UUIDv7` + fn example() -> Self { + Self(EXAMPLE.to_owned()) + } +} + +impl TryFrom<&str> for UUIDv7 { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + value.to_string().try_into() + } +} + +impl TryFrom for UUIDv7 { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + if !is_valid(&value) { + bail!("Invalid UUIDv7") + } + Ok(Self(value)) + } +} + +impl TryInto for UUIDv7 { + 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/mod.rs b/catalyst-gateway/bin/src/service/common/types/mod.rs index d92199ed124..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,8 @@ //! or integer. pub(crate) mod cardano; +pub(crate) mod document; pub(crate) mod generic; pub(crate) mod headers; +pub(crate) mod payload; pub(crate) mod string_types; diff --git a/catalyst-gateway/bin/src/service/common/types/payload/cbor.rs b/catalyst-gateway/bin/src/service/common/types/payload/cbor.rs new file mode 100644 index 00000000000..67453a71e28 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/payload/cbor.rs @@ -0,0 +1,164 @@ +use std::ops::{Deref, DerefMut}; + +use bytes::Bytes; +use poem::{Body, FromRequest, IntoResponse, Request, RequestBody, Response, Result}; +use poem_openapi::{ + impl_apirequest_for_payload, + payload::{ParsePayload, Payload}, + registry::{MetaMediaType, MetaResponse, MetaResponses, MetaSchema, MetaSchemaRef, Registry}, + ApiResponse, +}; + +// use crate::{ +// payload::{ParsePayload, Payload}, +// registry::{MetaMediaType, MetaResponse, MetaResponses, MetaSchema, MetaSchemaRef, +// Registry}, ApiResponse, +//}; + +/// A cbor binary payload. +/// +/// # Examples +/// +/// ```rust +/// use poem::{ +/// error::BadRequest, +/// http::{Method, StatusCode, Uri}, +/// test::TestClient, +/// Body, IntoEndpoint, Request, Result, +/// }; +/// use poem_openapi::{ +/// payload::{Cbor, Json}, +/// OpenApi, OpenApiService, +/// }; +/// use tokio::io::AsyncReadExt; +/// +/// struct MyApi; +/// +/// #[OpenApi] +/// impl MyApi { +/// #[oai(path = "/upload", method = "post")] +/// async fn upload_binary(&self, data: Cbor>) -> Json { +/// Json(data.len()) +/// } +/// +/// #[oai(path = "/upload_stream", method = "post")] +/// async fn upload_binary_stream(&self, data: Cbor) -> Result> { +/// let mut reader = data.0.into_async_read(); +/// let mut bytes = Vec::new(); +/// reader.read_to_end(&mut bytes).await.map_err(BadRequest)?; +/// Ok(Json(bytes.len())) +/// } +/// } +/// +/// let api = OpenApiService::new(MyApi, "Demo", "0.1.0"); +/// let cli = TestClient::new(api); +/// +/// # tokio::runtime::Runtime::new().unwrap().block_on(async { +/// let resp = cli +/// .post("/upload") +/// .content_type("application/octet-stream") +/// .body("abcdef") +/// .send() +/// .await; +/// resp.assert_status_is_ok(); +/// resp.assert_text("6").await; +/// +/// let resp = cli +/// .post("/upload_stream") +/// .content_type("application/octet-stream") +/// .body("abcdef") +/// .send() +/// .await; +/// resp.assert_status_is_ok(); +/// resp.assert_text("6").await; +/// # }); +/// ``` +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Cbor(pub T); + +impl Deref for Cbor { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Cbor { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Payload for Cbor { + const CONTENT_TYPE: &'static str = "application/cbor"; + + fn check_content_type(content_type: &str) -> bool { + matches!(content_type.parse::(), Ok(content_type) if content_type.type_() == "application" + && (content_type.subtype() == "cbor" + || content_type + .suffix() + .is_some_and(|v| v == "cbor"))) + } + + fn schema_ref() -> MetaSchemaRef { + MetaSchemaRef::Inline(Box::new(MetaSchema { + format: Some("binary"), + ..MetaSchema::new("string") + })) + } +} + +impl ParsePayload for Cbor> { + const IS_REQUIRED: bool = true; + + async fn from_request(request: &Request, body: &mut RequestBody) -> Result { + Ok(Self(>::from_request(request, body).await?)) + } +} + +impl ParsePayload for Cbor { + const IS_REQUIRED: bool = true; + + async fn from_request(request: &Request, body: &mut RequestBody) -> Result { + Ok(Self(Bytes::from_request(request, body).await?)) + } +} + +impl ParsePayload for Cbor { + const IS_REQUIRED: bool = true; + + async fn from_request(request: &Request, body: &mut RequestBody) -> Result { + Ok(Self(Body::from_request(request, body).await?)) + } +} + +impl + Send> IntoResponse for Cbor { + fn into_response(self) -> Response { + Response::builder() + .content_type(Self::CONTENT_TYPE) + .body(self.0.into()) + } +} + +impl + Send> ApiResponse for Cbor { + fn meta() -> MetaResponses { + MetaResponses { + responses: vec![MetaResponse { + description: "", + status: Some(200), + content: vec![MetaMediaType { + content_type: Self::CONTENT_TYPE, + schema: Self::schema_ref(), + }], + headers: vec![], + }], + } + } + + fn register(_registry: &mut Registry) {} +} + +impl_apirequest_for_payload!(Cbor>); +impl_apirequest_for_payload!(Cbor); +impl_apirequest_for_payload!(Cbor); diff --git a/catalyst-gateway/bin/src/service/common/types/payload/mod.rs b/catalyst-gateway/bin/src/service/common/types/payload/mod.rs new file mode 100644 index 00000000000..08831155ec2 --- /dev/null +++ b/catalyst-gateway/bin/src/service/common/types/payload/mod.rs @@ -0,0 +1,6 @@ +//! Payload Types +//! +//! These are types used to define various non-json payloads. + +/// Generic CBOR Payload +pub(crate) mod cbor; diff --git a/catalyst-gateway/event-db/Earthfile b/catalyst-gateway/event-db/Earthfile index aed0287dab8..27f6799f8a6 100644 --- a/catalyst-gateway/event-db/Earthfile +++ b/catalyst-gateway/event-db/Earthfile @@ -3,7 +3,7 @@ # the database and its associated software. VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.27 AS postgresql-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/postgresql:v3.2.31 AS postgresql-ci # cspell: words diff --git a/catalyst-gateway/tests/Earthfile b/catalyst-gateway/tests/Earthfile index e16a77ec043..fd24409a0e0 100644 --- a/catalyst-gateway/tests/Earthfile +++ b/catalyst-gateway/tests/Earthfile @@ -1,5 +1,5 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/spectral:v3.2.27 AS spectral-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/spectral:v3.2.31 AS spectral-ci IMPORT .. AS gateway # INSTALL_SCYLLA - Installs scylla for bookworm-slim/debian diff --git a/catalyst-gateway/tests/api_tests/Earthfile b/catalyst-gateway/tests/api_tests/Earthfile index 94545c5c634..e803c48a85b 100644 --- a/catalyst-gateway/tests/api_tests/Earthfile +++ b/catalyst-gateway/tests/api_tests/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/python:v3.2.27 AS python-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/python:v3.2.31 AS python-ci builder: FROM python-ci+python-base diff --git a/catalyst-gateway/tests/schemathesis_tests/Earthfile b/catalyst-gateway/tests/schemathesis_tests/Earthfile index 3255d70db3b..ac66d900f4f 100644 --- a/catalyst-gateway/tests/schemathesis_tests/Earthfile +++ b/catalyst-gateway/tests/schemathesis_tests/Earthfile @@ -17,7 +17,7 @@ package-schemathesis: --exclude-path '/api/v1/health/inspection' \ #excluding since this is a internal debug endpoint $openapi_spec \ --workers=2 \ - --wait-for-schema=120 \ + --wait-for-schema=500 \ --max-response-time=5000 \ --hypothesis-max-examples=1000 \ --data-generation-method=all \ diff --git a/catalyst_voices/Earthfile b/catalyst_voices/Earthfile index 8dd8053bba2..4b666623342 100644 --- a/catalyst_voices/Earthfile +++ b/catalyst_voices/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 IMPORT ../catalyst-gateway AS catalyst-gateway -IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.27 AS flutter-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.31 AS flutter-ci # repo-catalyst-voices - Creates artifacts of all configuration files, # packages and folders related to catalyst_voices frontend. diff --git a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile index 7aaf72302a3..cb5b3d1132f 100644 --- a/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile +++ b/catalyst_voices/packages/libs/catalyst_cardano/catalyst_cardano/wallet-automation/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.27 AS flutter-ci -IMPORT github.com/input-output-hk/catalyst-ci/earthly/playwright:v3.2.27 AS playwright-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.31 AS flutter-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/playwright:v3.2.31 AS playwright-ci deps: DO playwright-ci+SETUP --workdir=/wallet-automation diff --git a/catalyst_voices/packages/libs/catalyst_key_derivation/Earthfile b/catalyst_voices/packages/libs/catalyst_key_derivation/Earthfile index 4563ebd6cb9..827f5b6b58e 100644 --- a/catalyst_voices/packages/libs/catalyst_key_derivation/Earthfile +++ b/catalyst_voices/packages/libs/catalyst_key_derivation/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.27 AS flutter-ci -IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter_rust_bridge:v3.2.27 AS flutter_rust_bridge +IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.31 AS flutter-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter_rust_bridge:v3.2.31 AS flutter_rust_bridge builder: FROM flutter_rust_bridge+builder diff --git a/catalyst_voices/packages/libs/catalyst_key_derivation/rust/Earthfile b/catalyst_voices/packages/libs/catalyst_key_derivation/rust/Earthfile index 992c7a6c0e6..1e75c2a00ed 100644 --- a/catalyst_voices/packages/libs/catalyst_key_derivation/rust/Earthfile +++ b/catalyst_voices/packages/libs/catalyst_key_derivation/rust/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.28 AS rust-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.31 AS rust-ci IMPORT ../ AS flutter-rust-bridge # builder : Setup the builder diff --git a/catalyst_voices/utilities/uikit_example/Earthfile b/catalyst_voices/utilities/uikit_example/Earthfile index 65cf84c4db4..8ddc602d953 100644 --- a/catalyst_voices/utilities/uikit_example/Earthfile +++ b/catalyst_voices/utilities/uikit_example/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 IMPORT ../../ AS catalyst-voices -IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.27 AS flutter-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/flutter:v3.2.31 AS flutter-ci # local-build-web - build web version of UIKit example. # Prefixed by "local" to make sure it's not auto triggered, the target was diff --git a/docs/Earthfile b/docs/Earthfile index 0e82903ad50..3106cf05c89 100644 --- a/docs/Earthfile +++ b/docs/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/docs:v3.2.27 AS docs-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/docs:v3.2.31 AS docs-ci IMPORT .. AS repo IMPORT ../catalyst-gateway AS catalyst-gateway diff --git a/docs/src/api/cat-gateway/.pages b/docs/src/api/cat-gateway/.pages index fcb4544c60c..df2a76508e0 100644 --- a/docs/src/api/cat-gateway/.pages +++ b/docs/src/api/cat-gateway/.pages @@ -1,4 +1,8 @@ nav: - index.md - - 'Rust docs': rust.md - - 'OpenAPI docs': openapi.md \ No newline at end of file + - "Catalyst Gateway Service - Rust Internal API Documentation": rust.md + - "OpenAPI - Specification": openapi-spec.md + - "OpenAPI - SwaggerUI Viewer": openapi-swagger.md + - "OpenAPI - OpenAPI Explorer Viewer": openapi-explorer.md + - "OpenAPI - Scalar Viewer": openapi-scalar.md + - "OpenAPI - Stoplight Elements Viewer": openapi-stoplight-elements.md diff --git a/docs/src/api/cat-gateway/index.md b/docs/src/api/cat-gateway/index.md index 65bf358322a..d80df495115 100644 --- a/docs/src/api/cat-gateway/index.md +++ b/docs/src/api/cat-gateway/index.md @@ -3,3 +3,10 @@ icon: material/gate --- # Catalyst Gateway API + +This section documents the internal RUST API of the catalyst-gateway service. + +It also documents the Open API 3.0 specification exposed by the service. + +Multiple viewers are available, but if there is any discrepancy between them, +the Swagger viewer is the source of truth. diff --git a/docs/src/api/cat-gateway/openapi-explorer.md b/docs/src/api/cat-gateway/openapi-explorer.md new file mode 100644 index 00000000000..174ad8b2929 --- /dev/null +++ b/docs/src/api/cat-gateway/openapi-explorer.md @@ -0,0 +1,12 @@ +--- +icon: material/server +--- + + +# OpenAPI + + +