cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit c7063fc3fc3440938bb80c58b34ce8eedaa7b7ee
parent 6922ee248c652334bdd2ec43fe8cc1f2438cfc1f
Author: triesap <tyson@radroots.org>
Date:   Tue, 26 May 2026 20:24:12 +0000

ops: split operation execution modules

Diffstat:
Asrc/ops/context.rs | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ops/contract.rs | 391+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ops/error.rs | 831+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/ops/mod.rs | 1378+------------------------------------------------------------------------------
Asrc/ops/service.rs | 35+++++++++++++++++++++++++++++++++++
5 files changed, 1389 insertions(+), 1369 deletions(-)

diff --git a/src/ops/context.rs b/src/ops/context.rs @@ -0,0 +1,123 @@ +use crate::cli::{TargetCliArgs, TargetOutputFormat}; +use crate::out::envelope::{EnvelopeActor, EnvelopeContext, OutputFormat}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OperationOutputFormat { + Human, + Json, + Ndjson, +} + +impl Default for OperationOutputFormat { + fn default() -> Self { + Self::Human + } +} + +impl From<TargetOutputFormat> for OperationOutputFormat { + fn from(format: TargetOutputFormat) -> Self { + match format { + TargetOutputFormat::Human => Self::Human, + TargetOutputFormat::Json => Self::Json, + TargetOutputFormat::Ndjson => Self::Ndjson, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OperationNetworkMode { + Default, + Offline, + Online, +} + +impl Default for OperationNetworkMode { + fn default() -> Self { + Self::Default + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OperationInputMode { + PromptingAllowed, + NoInput, +} + +impl Default for OperationInputMode { + fn default() -> Self { + Self::PromptingAllowed + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct OperationContext { + pub output_format: OperationOutputFormat, + pub account_id: Option<String>, + pub relays: Vec<String>, + pub network_mode: OperationNetworkMode, + pub dry_run: bool, + pub idempotency_key: Option<String>, + pub correlation_id: Option<String>, + pub approval_token: Option<String>, + pub input_mode: OperationInputMode, + pub quiet: bool, + pub verbose: bool, + pub trace: bool, + pub color: bool, +} + +impl OperationContext { + pub fn from_target_args(args: &TargetCliArgs) -> Self { + Self { + output_format: OperationOutputFormat::from(args.format), + account_id: args.account_id.clone(), + relays: args.relay.clone(), + network_mode: if args.offline { + OperationNetworkMode::Offline + } else if args.online { + OperationNetworkMode::Online + } else { + OperationNetworkMode::Default + }, + dry_run: args.dry_run, + idempotency_key: args.idempotency_key.clone(), + correlation_id: args.correlation_id.clone(), + approval_token: args.approval_token.clone(), + input_mode: if args.no_input { + OperationInputMode::NoInput + } else { + OperationInputMode::PromptingAllowed + }, + quiet: args.quiet, + verbose: args.verbose, + trace: args.trace, + color: !args.no_color, + } + } + + pub fn envelope_context(&self, request_id: impl Into<String>) -> EnvelopeContext { + let mut context = EnvelopeContext::new(request_id, self.dry_run); + context.output_format = match self.output_format { + OperationOutputFormat::Human => OutputFormat::Human, + OperationOutputFormat::Json => OutputFormat::Json, + OperationOutputFormat::Ndjson => OutputFormat::Ndjson, + }; + context.correlation_id = self.correlation_id.clone(); + context.idempotency_key = self.idempotency_key.clone(); + context.actor = self.account_id.as_ref().map(|account_id| EnvelopeActor { + account_id: account_id.clone(), + role: "account".to_owned(), + }); + context + } + + pub fn requires_approval_token(&self) -> bool { + !self.dry_run && !self.has_approval_token() + } + + pub fn has_approval_token(&self) -> bool { + self.approval_token + .as_deref() + .is_some_and(|token| !token.trim().is_empty()) + } +} diff --git a/src/ops/contract.rs b/src/ops/contract.rs @@ -0,0 +1,391 @@ +use std::fmt::Debug; + +use serde::Serialize; +use serde_json::{Map, Value}; + +use super::context::OperationContext; +use super::error::OperationAdapterError; +use crate::cli::TargetCliArgs; +use crate::out::envelope::{ + EnvelopeContext, NextAction, OutputEnvelope, OutputWarning, next_actions_from_result_value, +}; +use crate::registry::{OPERATION_REGISTRY, OperationSpec, get_operation}; + +pub type OperationData = Map<String, Value>; + +pub trait OperationRequestPayload: Debug + Clone + PartialEq + 'static { + const OPERATION_ID: &'static str; + const REQUEST_TYPE: &'static str; +} + +pub trait OperationRequestData: OperationRequestPayload { + fn input(&self) -> &OperationData; +} + +pub trait OperationResultPayload: Debug + Clone + PartialEq + Serialize + 'static { + const OPERATION_ID: &'static str; + const RESULT_TYPE: &'static str; +} + +pub trait OperationResultData: OperationResultPayload + Sized { + fn from_data(data: OperationData) -> Self; + + fn from_value(value: Value) -> Self { + Self::from_data(value_to_data(value)) + } + + fn from_serializable<T: Serialize>(value: &T) -> Result<Self, OperationAdapterError> { + Ok(Self::from_value(serde_json::to_value(value).map_err( + |error| OperationAdapterError::Serialization(error.to_string()), + )?)) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct OperationRequest<P: OperationRequestPayload> { + pub spec: &'static OperationSpec, + pub context: OperationContext, + pub payload: P, +} + +impl<P: OperationRequestPayload> OperationRequest<P> { + pub fn new(context: OperationContext, payload: P) -> Result<Self, OperationAdapterError> { + let spec = get_operation(P::OPERATION_ID) + .ok_or_else(|| OperationAdapterError::UnknownOperation(P::OPERATION_ID.to_owned()))?; + if spec.rust_request != P::REQUEST_TYPE { + return Err(OperationAdapterError::RequestTypeMismatch { + operation_id: P::OPERATION_ID.to_owned(), + registry_request: spec.rust_request.to_owned(), + adapter_request: P::REQUEST_TYPE.to_owned(), + }); + } + Ok(Self { + spec, + context, + payload, + }) + } + + pub fn operation_id(&self) -> &'static str { + P::OPERATION_ID + } + + pub fn request_type_name(&self) -> &'static str { + P::REQUEST_TYPE + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct OperationResult<P: OperationResultPayload> { + pub spec: &'static OperationSpec, + pub payload: P, + pub warnings: Vec<OutputWarning>, + pub next_actions: Vec<NextAction>, +} + +impl<P: OperationResultPayload> OperationResult<P> { + pub fn new(payload: P) -> Result<Self, OperationAdapterError> { + let spec = get_operation(P::OPERATION_ID) + .ok_or_else(|| OperationAdapterError::UnknownOperation(P::OPERATION_ID.to_owned()))?; + if spec.rust_result != P::RESULT_TYPE { + return Err(OperationAdapterError::ResultTypeMismatch { + operation_id: P::OPERATION_ID.to_owned(), + registry_result: spec.rust_result.to_owned(), + adapter_result: P::RESULT_TYPE.to_owned(), + }); + } + Ok(Self { + spec, + payload, + warnings: Vec::new(), + next_actions: Vec::new(), + }) + } + + pub fn operation_id(&self) -> &'static str { + P::OPERATION_ID + } + + pub fn result_type_name(&self) -> &'static str { + P::RESULT_TYPE + } + + pub fn to_envelope( + &self, + context: EnvelopeContext, + ) -> Result<OutputEnvelope, OperationAdapterError> { + let result = serde_json::to_value(&self.payload) + .map_err(|error| OperationAdapterError::Serialization(error.to_string()))?; + let next_actions = if self.next_actions.is_empty() { + next_actions_from_result(&result) + } else { + self.next_actions.clone() + }; + let mut envelope = OutputEnvelope::success(self.operation_id(), result, context); + envelope.warnings = self.warnings.clone(); + envelope.next_actions = next_actions; + Ok(envelope) + } +} + +fn next_actions_from_result(result: &Value) -> Vec<NextAction> { + next_actions_from_result_value(result) +} + +macro_rules! target_operation_contracts { + ($( $variant:ident => ($request:ident, $result:ident, $operation_id:literal) ),+ $(,)?) => { + #[derive(Debug, Clone, PartialEq)] + pub enum TargetOperationRequest { + $( $variant(OperationRequest<$request>), )+ + } + + impl TargetOperationRequest { + pub fn from_target_args(args: &TargetCliArgs) -> Result<Self, OperationAdapterError> { + Self::from_operation_id_with_input( + crate::cli::input::operation_id_from_target(args), + OperationContext::from_target_args(args), + crate::cli::input::target_operation_input(&args.command), + ) + } + + pub fn from_operation_id( + operation_id: &'static str, + context: OperationContext, + ) -> Result<Self, OperationAdapterError> { + Self::from_operation_id_with_input(operation_id, context, OperationData::new()) + } + + fn from_operation_id_with_input( + operation_id: &'static str, + context: OperationContext, + input: OperationData, + ) -> Result<Self, OperationAdapterError> { + match operation_id { + $( $operation_id => Ok(Self::$variant(OperationRequest::new(context, $request::from_data(input))?)), )+ + _ => Err(OperationAdapterError::UnknownOperation(operation_id.to_owned())), + } + } + + pub fn operation_id(&self) -> &'static str { + match self { + $( Self::$variant(request) => request.operation_id(), )+ + } + } + + pub fn spec(&self) -> &'static OperationSpec { + match self { + $( Self::$variant(request) => request.spec, )+ + } + } + + pub fn context(&self) -> &OperationContext { + match self { + $( Self::$variant(request) => &request.context, )+ + } + } + + pub fn request_type_name(&self) -> &'static str { + match self { + $( Self::$variant(request) => request.request_type_name(), )+ + } + } + + pub fn request_type_for_operation(operation_id: &str) -> Option<&'static str> { + match operation_id { + $( $operation_id => Some(stringify!($request)), )+ + _ => None, + } + } + } + + #[derive(Debug, Clone, PartialEq)] + pub enum TargetOperationResult { + $( $variant(OperationResult<$result>), )+ + } + + impl TargetOperationResult { + pub fn operation_id(&self) -> &'static str { + match self { + $( Self::$variant(result) => result.operation_id(), )+ + } + } + + pub fn result_type_name(&self) -> &'static str { + match self { + $( Self::$variant(result) => result.result_type_name(), )+ + } + } + + pub fn result_type_for_operation(operation_id: &str) -> Option<&'static str> { + match operation_id { + $( $operation_id => Some(stringify!($result)), )+ + _ => None, + } + } + } + + $( + #[derive(Debug, Default, Clone, PartialEq, Serialize)] + pub struct $request { + #[serde(flatten)] + pub input: OperationData, + } + + impl $request { + pub fn from_data(input: OperationData) -> Self { + Self { input } + } + } + + impl OperationRequestPayload for $request { + const OPERATION_ID: &'static str = $operation_id; + const REQUEST_TYPE: &'static str = stringify!($request); + } + + impl OperationRequestData for $request { + fn input(&self) -> &OperationData { + &self.input + } + } + + #[derive(Debug, Default, Clone, PartialEq, Serialize)] + pub struct $result { + #[serde(flatten)] + pub data: OperationData, + } + + impl $result { + pub fn from_data(data: OperationData) -> Self { + Self { data } + } + + pub fn from_value(value: Value) -> Self { + Self { + data: value_to_data(value), + } + } + + pub fn from_serializable<T: Serialize>( + value: &T, + ) -> Result<Self, OperationAdapterError> { + Ok(Self::from_value( + serde_json::to_value(value) + .map_err(|error| OperationAdapterError::Serialization(error.to_string()))?, + )) + } + } + + impl OperationResultPayload for $result { + const OPERATION_ID: &'static str = $operation_id; + const RESULT_TYPE: &'static str = stringify!($result); + } + + impl OperationResultData for $result { + fn from_data(data: OperationData) -> Self { + Self { data } + } + } + )+ + }; +} + +fn value_to_data(value: Value) -> OperationData { + match value { + Value::Object(map) => map, + other => { + let mut map = OperationData::new(); + map.insert("value".to_owned(), other); + map + } + } +} + +target_operation_contracts! { + WorkspaceInit => (WorkspaceInitRequest, WorkspaceInitResult, "workspace.init"), + WorkspaceGet => (WorkspaceGetRequest, WorkspaceGetResult, "workspace.get"), + HealthStatusGet => (HealthStatusGetRequest, HealthStatusGetResult, "health.status.get"), + HealthCheckRun => (HealthCheckRunRequest, HealthCheckRunResult, "health.check.run"), + ConfigGet => (ConfigGetRequest, ConfigGetResult, "config.get"), + AccountCreate => (AccountCreateRequest, AccountCreateResult, "account.create"), + AccountImport => (AccountImportRequest, AccountImportResult, "account.import"), + AccountAttachSecret => (AccountAttachSecretRequest, AccountAttachSecretResult, "account.attach_secret"), + AccountGet => (AccountGetRequest, AccountGetResult, "account.get"), + AccountList => (AccountListRequest, AccountListResult, "account.list"), + AccountRemove => (AccountRemoveRequest, AccountRemoveResult, "account.remove"), + AccountSelectionGet => (AccountSelectionGetRequest, AccountSelectionGetResult, "account.selection.get"), + AccountSelectionUpdate => (AccountSelectionUpdateRequest, AccountSelectionUpdateResult, "account.selection.update"), + AccountSelectionClear => (AccountSelectionClearRequest, AccountSelectionClearResult, "account.selection.clear"), + SignerStatusGet => (SignerStatusGetRequest, SignerStatusGetResult, "signer.status.get"), + RelayList => (RelayListRequest, RelayListResult, "relay.list"), + StoreInit => (StoreInitRequest, StoreInitResult, "store.init"), + StoreStatusGet => (StoreStatusGetRequest, StoreStatusGetResult, "store.status.get"), + StoreExport => (StoreExportRequest, StoreExportResult, "store.export"), + StoreBackupCreate => (StoreBackupCreateRequest, StoreBackupCreateResult, "store.backup.create"), + SyncStatusGet => (SyncStatusGetRequest, SyncStatusGetResult, "sync.status.get"), + SyncPull => (SyncPullRequest, SyncPullResult, "sync.pull"), + SyncPush => (SyncPushRequest, SyncPushResult, "sync.push"), + SyncWatch => (SyncWatchRequest, SyncWatchResult, "sync.watch"), + FarmCreate => (FarmCreateRequest, FarmCreateResult, "farm.create"), + FarmGet => (FarmGetRequest, FarmGetResult, "farm.get"), + FarmRebind => (FarmRebindRequest, FarmRebindResult, "farm.rebind"), + FarmProfileUpdate => (FarmProfileUpdateRequest, FarmProfileUpdateResult, "farm.profile.update"), + FarmLocationUpdate => (FarmLocationUpdateRequest, FarmLocationUpdateResult, "farm.location.update"), + FarmFulfillmentUpdate => (FarmFulfillmentUpdateRequest, FarmFulfillmentUpdateResult, "farm.fulfillment.update"), + FarmReadinessCheck => (FarmReadinessCheckRequest, FarmReadinessCheckResult, "farm.readiness.check"), + FarmPublish => (FarmPublishRequest, FarmPublishResult, "farm.publish"), + ListingCreate => (ListingCreateRequest, ListingCreateResult, "listing.create"), + ListingGet => (ListingGetRequest, ListingGetResult, "listing.get"), + ListingList => (ListingListRequest, ListingListResult, "listing.list"), + ListingAppList => (ListingAppListRequest, ListingAppListResult, "listing.app.list"), + ListingAppExport => (ListingAppExportRequest, ListingAppExportResult, "listing.app.export"), + ListingUpdate => (ListingUpdateRequest, ListingUpdateResult, "listing.update"), + ListingValidate => (ListingValidateRequest, ListingValidateResult, "listing.validate"), + ListingRebind => (ListingRebindRequest, ListingRebindResult, "listing.rebind"), + ListingPublish => (ListingPublishRequest, ListingPublishResult, "listing.publish"), + ListingArchive => (ListingArchiveRequest, ListingArchiveResult, "listing.archive"), + MarketRefresh => (MarketRefreshRequest, MarketRefreshResult, "market.refresh"), + MarketProductSearch => (MarketProductSearchRequest, MarketProductSearchResult, "market.product.search"), + MarketListingGet => (MarketListingGetRequest, MarketListingGetResult, "market.listing.get"), + BasketCreate => (BasketCreateRequest, BasketCreateResult, "basket.create"), + BasketGet => (BasketGetRequest, BasketGetResult, "basket.get"), + BasketList => (BasketListRequest, BasketListResult, "basket.list"), + BasketItemAdd => (BasketItemAddRequest, BasketItemAddResult, "basket.item.add"), + BasketItemUpdate => (BasketItemUpdateRequest, BasketItemUpdateResult, "basket.item.update"), + BasketItemRemove => (BasketItemRemoveRequest, BasketItemRemoveResult, "basket.item.remove"), + BasketAdjustmentAdd => (BasketAdjustmentAddRequest, BasketAdjustmentAddResult, "basket.adjustment.add"), + BasketAdjustmentRemove => (BasketAdjustmentRemoveRequest, BasketAdjustmentRemoveResult, "basket.adjustment.remove"), + BasketValidate => (BasketValidateRequest, BasketValidateResult, "basket.validate"), + BasketQuoteCreate => (BasketQuoteCreateRequest, BasketQuoteCreateResult, "basket.quote.create"), + OrderSubmit => (OrderSubmitRequest, OrderSubmitResult, "order.submit"), + OrderGet => (OrderGetRequest, OrderGetResult, "order.get"), + OrderList => (OrderListRequest, OrderListResult, "order.list"), + OrderAppList => (OrderAppListRequest, OrderAppListResult, "order.app.list"), + OrderAppExport => (OrderAppExportRequest, OrderAppExportResult, "order.app.export"), + OrderRebind => (OrderRebindRequest, OrderRebindResult, "order.rebind"), + OrderAccept => (OrderAcceptRequest, OrderAcceptResult, "order.accept"), + OrderDecline => (OrderDeclineRequest, OrderDeclineResult, "order.decline"), + OrderCancel => (OrderCancelRequest, OrderCancelResult, "order.cancel"), + OrderRevisionPropose => (OrderRevisionProposeRequest, OrderRevisionProposeResult, "order.revision.propose"), + OrderRevisionAccept => (OrderRevisionAcceptRequest, OrderRevisionAcceptResult, "order.revision.accept"), + OrderRevisionDecline => (OrderRevisionDeclineRequest, OrderRevisionDeclineResult, "order.revision.decline"), + OrderFulfillmentUpdate => (OrderFulfillmentUpdateRequest, OrderFulfillmentUpdateResult, "order.fulfillment.update"), + OrderReceiptRecord => (OrderReceiptRecordRequest, OrderReceiptRecordResult, "order.receipt.record"), + OrderPaymentRecord => (OrderPaymentRecordRequest, OrderPaymentRecordResult, "order.payment.record"), + OrderSettlementAccept => (OrderSettlementAcceptRequest, OrderSettlementAcceptResult, "order.settlement.accept"), + OrderSettlementReject => (OrderSettlementRejectRequest, OrderSettlementRejectResult, "order.settlement.reject"), + OrderStatusGet => (OrderStatusGetRequest, OrderStatusGetResult, "order.status.get"), + OrderEventList => (OrderEventListRequest, OrderEventListResult, "order.event.list"), + OrderEventWatch => (OrderEventWatchRequest, OrderEventWatchResult, "order.event.watch"), + ValidationReceiptGet => (ValidationReceiptGetRequest, ValidationReceiptGetResult, "validation.receipt.get"), + ValidationReceiptList => (ValidationReceiptListRequest, ValidationReceiptListResult, "validation.receipt.list"), + ValidationReceiptVerify => (ValidationReceiptVerifyRequest, ValidationReceiptVerifyResult, "validation.receipt.verify"), +} + +pub fn adapter_registry_linkage_is_valid() -> bool { + OPERATION_REGISTRY.iter().all(|operation| { + TargetOperationRequest::request_type_for_operation(operation.operation_id) + == Some(operation.rust_request) + && TargetOperationResult::result_type_for_operation(operation.operation_id) + == Some(operation.rust_result) + }) +} diff --git a/src/ops/error.rs b/src/ops/error.rs @@ -0,0 +1,831 @@ +use std::io::ErrorKind; + +use serde_json::{Map, Value, json}; + +use crate::out::envelope::{CliExitCode, OutputError}; +use crate::runtime::RuntimeError; +use crate::runtime::accounts::AccountRuntimeFailure; +use crate::view::runtime::CommandDisposition; + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum OperationAdapterError { + #[error("unknown operation `{0}`")] + UnknownOperation(String), + #[error( + "operation `{operation_id}` registry request `{registry_request}` does not match adapter request `{adapter_request}`" + )] + RequestTypeMismatch { + operation_id: String, + registry_request: String, + adapter_request: String, + }, + #[error( + "operation `{operation_id}` registry result `{registry_result}` does not match adapter result `{adapter_result}`" + )] + ResultTypeMismatch { + operation_id: String, + registry_result: String, + adapter_result: String, + }, + #[error("failed to serialize operation result: {0}")] + Serialization(String), + #[error("invalid operation input for `{operation_id}`: {message}")] + InvalidInput { + operation_id: String, + message: String, + }, + #[error("resource not found for `{operation_id}`: {message}")] + NotFound { + operation_id: String, + message: String, + }, + #[error("validation failed for `{operation_id}`: {message}")] + ValidationFailed { + operation_id: String, + message: String, + }, + #[error("approval required for `{operation_id}`: {message}")] + ApprovalRequired { + operation_id: String, + message: String, + }, + #[error("operation `{operation_id}` is forbidden while offline: {message}")] + OfflineForbidden { + operation_id: String, + message: String, + }, + #[error("operation `{operation_id}` cannot run online: {message}")] + NetworkUnavailable { + operation_id: String, + message: String, + }, + #[error("account unresolved for `{operation_id}`: {message}")] + AccountUnresolved { + operation_id: String, + message: String, + }, + #[error("account is watch-only for `{operation_id}`: {message}")] + AccountWatchOnly { + operation_id: String, + message: String, + }, + #[error("account mismatch for `{operation_id}`: {message}")] + AccountMismatch { + operation_id: String, + message: String, + }, + #[error("signer unconfigured for `{operation_id}`: {message}")] + SignerUnconfigured { + operation_id: String, + message: String, + }, + #[error("signer unavailable for `{operation_id}`: {message}")] + SignerUnavailable { + operation_id: String, + message: String, + }, + #[error("signer mode deferred for `{operation_id}`: {message}")] + SignerModeDeferred { + operation_id: String, + message: String, + }, + #[error("provider unconfigured for `{operation_id}`: {message}")] + ProviderUnconfigured { + operation_id: String, + message: String, + }, + #[error("provider unavailable for `{operation_id}`: {message}")] + ProviderUnavailable { + operation_id: String, + message: String, + }, + #[error("operation `{operation_id}` is unavailable: {message}")] + OperationUnavailable { + operation_id: String, + message: String, + }, + #[error("operation `{operation_id}` is not implemented: {message}")] + NotImplemented { + operation_id: String, + message: String, + }, + #[error("operation `{operation_id}` failed: {message}")] + DetailedFailure { + operation_id: String, + code: String, + class: String, + message: String, + exit_code: CliExitCode, + detail_json: String, + }, + #[error("operation runtime error: {0}")] + Runtime(String), +} + +impl OperationAdapterError { + pub fn approval_required(operation_id: &str) -> Self { + Self::ApprovalRequired { + operation_id: operation_id.to_owned(), + message: "missing required `approval_token` input".to_owned(), + } + } + + pub fn from_command_disposition( + operation_id: &str, + disposition: CommandDisposition, + message: String, + ) -> Self { + match disposition { + CommandDisposition::Success => Self::Runtime(message), + CommandDisposition::NotFound => Self::NotFound { + operation_id: operation_id.to_owned(), + message, + }, + CommandDisposition::ValidationFailed => Self::ValidationFailed { + operation_id: operation_id.to_owned(), + message, + }, + CommandDisposition::Unconfigured => Self::unconfigured(operation_id, message), + CommandDisposition::ExternalUnavailable => Self::unavailable(operation_id, message), + CommandDisposition::Unsupported => Self::InvalidInput { + operation_id: operation_id.to_owned(), + message, + }, + CommandDisposition::InternalError => Self::Runtime(message), + } + } + + pub fn unconfigured(operation_id: &str, message: String) -> Self { + classify_runtime_failure( + operation_id, + message, + RuntimeFailureAvailability::Unconfigured, + ) + } + + pub fn operation_unavailable_with_detail( + operation_id: &str, + message: String, + detail: Value, + ) -> Self { + Self::DetailedFailure { + operation_id: operation_id.to_owned(), + code: "operation_unavailable".to_owned(), + class: "operation".to_owned(), + message, + exit_code: CliExitCode::RuntimeUnavailable, + detail_json: detail.to_string(), + } + } + + pub fn not_found_with_detail(operation_id: &str, message: String, detail: Value) -> Self { + Self::DetailedFailure { + operation_id: operation_id.to_owned(), + code: "not_found".to_owned(), + class: "resource".to_owned(), + message, + exit_code: CliExitCode::NotFound, + detail_json: detail.to_string(), + } + } + + pub fn not_implemented(operation_id: &str, message: String) -> Self { + Self::NotImplemented { + operation_id: operation_id.to_owned(), + message, + } + } + + pub fn not_implemented_with_detail(operation_id: &str, message: String, detail: Value) -> Self { + Self::DetailedFailure { + operation_id: operation_id.to_owned(), + code: "not_implemented".to_owned(), + class: "operation".to_owned(), + message, + exit_code: CliExitCode::RuntimeUnavailable, + detail_json: detail.to_string(), + } + } + + pub fn network_unavailable_with_detail( + operation_id: &str, + message: String, + detail: Value, + ) -> Self { + Self::DetailedFailure { + operation_id: operation_id.to_owned(), + code: "network_unavailable".to_owned(), + class: "network".to_owned(), + message, + exit_code: CliExitCode::SyncOrNetworkFailure, + detail_json: detail.to_string(), + } + } + + pub fn validation_failed_with_detail( + operation_id: &str, + message: String, + detail: Value, + ) -> Self { + Self::DetailedFailure { + operation_id: operation_id.to_owned(), + code: "validation_failed".to_owned(), + class: "validation".to_owned(), + message, + exit_code: CliExitCode::ValidationFailed, + detail_json: detail.to_string(), + } + } + + pub fn unavailable(operation_id: &str, message: String) -> Self { + classify_runtime_failure( + operation_id, + message, + RuntimeFailureAvailability::Unavailable, + ) + } + + pub fn runtime_failure(operation_id: &str, error: RuntimeError) -> Self { + let message = error.to_string(); + let lowered = message.to_ascii_lowercase(); + match &error { + RuntimeError::Io(io_error) if io_error.kind() == ErrorKind::NotFound => { + Self::NotFound { + operation_id: operation_id.to_owned(), + message, + } + } + RuntimeError::Config(_) if looks_like_not_found(&lowered) => Self::NotFound { + operation_id: operation_id.to_owned(), + message, + }, + RuntimeError::Account(failure) => account_runtime_failure(operation_id, failure), + RuntimeError::Config(_) + if contains_any( + &lowered, + &[ + "no local account", + "account selector", + "account selection", + "account mismatch", + "did not match any local account", + "unresolved account", + ], + ) => + { + classify_runtime_failure( + operation_id, + message, + RuntimeFailureAvailability::Unconfigured, + ) + } + RuntimeError::Config(_) if looks_like_validation_failure(&lowered) => { + Self::ValidationFailed { + operation_id: operation_id.to_owned(), + message, + } + } + RuntimeError::Network(_) if looks_like_auth_failure(&lowered) => { + auth_runtime_failure(operation_id, message, &lowered) + } + RuntimeError::Network(_) if looks_like_signer_failure(&lowered) => { + Self::SignerUnavailable { + operation_id: operation_id.to_owned(), + message, + } + } + RuntimeError::Network(_) if looks_like_provider_failure(&lowered) => { + Self::ProviderUnavailable { + operation_id: operation_id.to_owned(), + message, + } + } + RuntimeError::Network(_) if looks_like_operation_failure(&lowered) => { + Self::OperationUnavailable { + operation_id: operation_id.to_owned(), + message, + } + } + RuntimeError::Network(_) => Self::NetworkUnavailable { + operation_id: operation_id.to_owned(), + message, + }, + RuntimeError::Accounts(_) => classify_runtime_failure( + operation_id, + message, + RuntimeFailureAvailability::Unavailable, + ), + _ => Self::Runtime(message), + } + } + + pub fn to_output_error(&self) -> OutputError { + match self { + Self::ApprovalRequired { message, .. } => OutputError::new( + "approval_required", + message.clone(), + CliExitCode::ApprovalRequiredOrDenied, + ), + Self::InvalidInput { message, .. } => { + OutputError::new("invalid_input", message.clone(), CliExitCode::InvalidInput) + } + Self::NotFound { + operation_id, + message, + } => runtime_output_error( + "not_found", + operation_id, + "resource", + message, + CliExitCode::NotFound, + ), + Self::ValidationFailed { + operation_id, + message, + } => runtime_output_error( + "validation_failed", + operation_id, + "validation", + message, + CliExitCode::ValidationFailed, + ), + Self::OfflineForbidden { + operation_id, + message, + } => runtime_output_error( + "offline_forbidden", + operation_id, + "network", + message, + CliExitCode::SyncOrNetworkFailure, + ), + Self::NetworkUnavailable { + operation_id, + message, + } => runtime_output_error( + "network_unavailable", + operation_id, + "network", + message, + CliExitCode::SyncOrNetworkFailure, + ), + Self::AccountUnresolved { + operation_id, + message, + } => runtime_output_error( + "account_unresolved", + operation_id, + "account", + message, + CliExitCode::AuthorizationFailed, + ), + Self::AccountWatchOnly { + operation_id, + message, + } => runtime_output_error( + "account_watch_only", + operation_id, + "account", + message, + CliExitCode::SignerUnavailable, + ), + Self::AccountMismatch { + operation_id, + message, + } => runtime_output_error( + "account_mismatch", + operation_id, + "account", + message, + CliExitCode::AuthorizationFailed, + ), + Self::SignerUnconfigured { + operation_id, + message, + } => runtime_output_error( + "signer_unconfigured", + operation_id, + "signer", + message, + CliExitCode::SignerUnavailable, + ), + Self::SignerUnavailable { + operation_id, + message, + } => runtime_output_error( + "signer_unavailable", + operation_id, + "signer", + message, + CliExitCode::SignerUnavailable, + ), + Self::SignerModeDeferred { + operation_id, + message, + } => runtime_output_error( + "signer_mode_deferred", + operation_id, + "signer", + message, + CliExitCode::SignerUnavailable, + ), + Self::ProviderUnconfigured { + operation_id, + message, + } => runtime_output_error( + "provider_unconfigured", + operation_id, + "provider", + message, + CliExitCode::RuntimeUnavailable, + ), + Self::ProviderUnavailable { + operation_id, + message, + } => runtime_output_error( + "provider_unavailable", + operation_id, + "provider", + message, + CliExitCode::RuntimeUnavailable, + ), + Self::OperationUnavailable { + operation_id, + message, + } => runtime_output_error( + "operation_unavailable", + operation_id, + "operation", + message, + CliExitCode::RuntimeUnavailable, + ), + Self::NotImplemented { + operation_id, + message, + } => runtime_output_error( + "not_implemented", + operation_id, + "operation", + message, + CliExitCode::RuntimeUnavailable, + ), + Self::DetailedFailure { + operation_id, + code, + class, + message, + exit_code, + detail_json, + } => runtime_output_error_with_detail( + code.as_str(), + operation_id, + class, + message, + *exit_code, + detail_json, + ), + Self::UnknownOperation(operation_id) => OutputError::new( + "unknown_operation", + format!("unknown operation `{operation_id}`"), + CliExitCode::InvalidInput, + ), + Self::RequestTypeMismatch { .. } | Self::ResultTypeMismatch { .. } => OutputError::new( + "contract_mismatch", + self.to_string(), + CliExitCode::InternalError, + ), + Self::Serialization(message) => OutputError::new( + "serialization_failed", + message.clone(), + CliExitCode::InternalError, + ), + Self::Runtime(message) => { + OutputError::new("runtime_error", message.clone(), CliExitCode::InternalError) + } + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RuntimeFailureAvailability { + Unconfigured, + Unavailable, +} + +fn account_runtime_failure( + operation_id: &str, + failure: &AccountRuntimeFailure, +) -> OperationAdapterError { + let message = failure.message().to_owned(); + match failure { + AccountRuntimeFailure::Unresolved(_) => account_failure_output( + operation_id, + "account_unresolved", + message, + CliExitCode::AuthorizationFailed, + failure.detail_json(), + || OperationAdapterError::AccountUnresolved { + operation_id: operation_id.to_owned(), + message: failure.message().to_owned(), + }, + ), + AccountRuntimeFailure::WatchOnly(_) => account_failure_output( + operation_id, + "account_watch_only", + message, + CliExitCode::SignerUnavailable, + failure.detail_json(), + || OperationAdapterError::AccountWatchOnly { + operation_id: operation_id.to_owned(), + message: failure.message().to_owned(), + }, + ), + AccountRuntimeFailure::Mismatch(_) => account_failure_output( + operation_id, + "account_mismatch", + message, + CliExitCode::AuthorizationFailed, + failure.detail_json(), + || OperationAdapterError::AccountMismatch { + operation_id: operation_id.to_owned(), + message: failure.message().to_owned(), + }, + ), + } +} + +fn account_failure_output( + operation_id: &str, + code: &str, + message: String, + exit_code: CliExitCode, + detail_json: Option<&str>, + fallback: impl FnOnce() -> OperationAdapterError, +) -> OperationAdapterError { + match detail_json { + Some(detail_json) => OperationAdapterError::DetailedFailure { + operation_id: operation_id.to_owned(), + code: code.to_owned(), + class: "account".to_owned(), + message, + exit_code, + detail_json: detail_json.to_owned(), + }, + None => fallback(), + } +} + +fn auth_runtime_failure( + operation_id: &str, + message: String, + lowered: &str, +) -> OperationAdapterError { + let unauthorized = contains_any( + lowered, + &[ + "unauthorized", + "forbidden", + "permission denied", + "invalid token", + "bearer token rejected", + "http 401", + "http 403", + "status 401", + "status 403", + ], + ); + OperationAdapterError::DetailedFailure { + operation_id: operation_id.to_owned(), + code: if unauthorized { + "auth_unauthorized".to_owned() + } else { + "auth_unavailable".to_owned() + }, + class: "auth".to_owned(), + message, + exit_code: CliExitCode::AuthorizationFailed, + detail_json: Value::Null.to_string(), + } +} + +fn classify_runtime_failure( + operation_id: &str, + message: String, + availability: RuntimeFailureAvailability, +) -> OperationAdapterError { + let lowered = message.to_ascii_lowercase(); + if contains_any(&lowered, &["watch_only", "watch-only", "watch only"]) { + return OperationAdapterError::AccountWatchOnly { + operation_id: operation_id.to_owned(), + message, + }; + } + if contains_any(&lowered, &["account mismatch"]) { + return OperationAdapterError::AccountMismatch { + operation_id: operation_id.to_owned(), + message, + }; + } + if contains_any( + &lowered, + &[ + "no account", + "no local account", + "account selector", + "account selection", + "did not match any local account", + "unresolved account", + "selected account", + ], + ) { + return OperationAdapterError::AccountUnresolved { + operation_id: operation_id.to_owned(), + message, + }; + } + if contains_any( + &lowered, + &[ + "signer", + "sign_event", + "remote_nip46", + "nip46", + "secret-backed", + "secret backed", + ], + ) { + return match availability { + RuntimeFailureAvailability::Unconfigured => OperationAdapterError::SignerUnconfigured { + operation_id: operation_id.to_owned(), + message, + }, + RuntimeFailureAvailability::Unavailable => OperationAdapterError::SignerUnavailable { + operation_id: operation_id.to_owned(), + message, + }, + }; + } + if contains_any( + &lowered, + &[ + "provider", + "write-plane", + "write plane", + "radrootsd", + "bridge", + "rpc", + "daemon", + ], + ) { + return match availability { + RuntimeFailureAvailability::Unconfigured => { + OperationAdapterError::ProviderUnconfigured { + operation_id: operation_id.to_owned(), + message, + } + } + RuntimeFailureAvailability::Unavailable => OperationAdapterError::ProviderUnavailable { + operation_id: operation_id.to_owned(), + message, + }, + }; + } + OperationAdapterError::OperationUnavailable { + operation_id: operation_id.to_owned(), + message, + } +} + +fn contains_any(value: &str, needles: &[&str]) -> bool { + needles.iter().any(|needle| value.contains(needle)) +} + +fn looks_like_auth_failure(value: &str) -> bool { + contains_any( + value, + &[ + "authentication", + "bridge auth", + "authorization", + "authorize", + "unauthorized", + "forbidden", + "bearer token", + "invalid token", + "permission denied", + "status 401", + "status 403", + "http 401", + "http 403", + ], + ) +} + +fn looks_like_signer_failure(value: &str) -> bool { + contains_any( + value, + &[ + "signer", + "sign_event", + "sign event", + "signer_session_id", + "signer session", + "nip46", + "nip-46", + "remote_nip46", + ], + ) +} + +fn looks_like_provider_failure(value: &str) -> bool { + contains_any( + value, + &[ + "provider unavailable", + "provider unconfigured", + "provider runtime", + "provider failed", + "radrootsd unavailable", + "daemon unavailable", + "bridge provider", + ], + ) +} + +fn looks_like_operation_failure(value: &str) -> bool { + contains_any( + value, + &[ + "method not found", + "unknown method", + "unsupported method", + "unsupported operation", + "operation unavailable", + "operation disabled", + "bridge disabled", + "bridge is disabled", + "bridge.listing.publish is disabled", + ], + ) +} + +fn looks_like_not_found(value: &str) -> bool { + contains_any( + value, + &[ + "not found", + "no such file or directory", + "path not found", + "missing file", + ], + ) +} + +fn looks_like_validation_failure(value: &str) -> bool { + contains_any( + value, + &[ + "invalid", + "parse ", + "parse:", + "must not", + "must be", + "validation", + "failed to import account", + ], + ) +} + +fn runtime_output_error( + code: &str, + operation_id: &str, + class: &str, + message: &str, + exit_code: CliExitCode, +) -> OutputError { + let mut error = OutputError::new(code, message.to_owned(), exit_code); + error.detail = Some(json!({ + "operation_id": operation_id, + "class": class, + })); + error +} + +fn runtime_output_error_with_detail( + code: &str, + operation_id: &str, + class: &str, + message: &str, + exit_code: CliExitCode, + detail_json: &str, +) -> OutputError { + let mut error = OutputError::new(code, message.to_owned(), exit_code); + let mut detail = serde_json::from_str::<Map<String, Value>>(detail_json).unwrap_or_default(); + detail.insert( + "operation_id".to_owned(), + Value::from(operation_id.to_owned()), + ); + detail.insert("class".to_owned(), Value::from(class.to_owned())); + error.detail = Some(Value::Object(detail)); + error +} diff --git a/src/ops/mod.rs b/src/ops/mod.rs @@ -1,1374 +1,14 @@ #![allow(dead_code)] -use std::fmt::Debug; -use std::io::ErrorKind; - -use serde::Serialize; -use serde_json::{Map, Value, json}; - -use crate::cli::{TargetCliArgs, TargetOutputFormat}; -use crate::out::envelope::{ - CliExitCode, EnvelopeActor, EnvelopeContext, NextAction, OutputEnvelope, OutputError, - OutputFormat, OutputWarning, next_actions_from_result_value, -}; -use crate::registry::{OPERATION_REGISTRY, OperationSpec, get_operation}; -use crate::runtime::RuntimeError; -use crate::runtime::accounts::AccountRuntimeFailure; -use crate::view::runtime::CommandDisposition; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OperationOutputFormat { - Human, - Json, - Ndjson, -} - -impl Default for OperationOutputFormat { - fn default() -> Self { - Self::Human - } -} - -impl From<TargetOutputFormat> for OperationOutputFormat { - fn from(format: TargetOutputFormat) -> Self { - match format { - TargetOutputFormat::Human => Self::Human, - TargetOutputFormat::Json => Self::Json, - TargetOutputFormat::Ndjson => Self::Ndjson, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OperationNetworkMode { - Default, - Offline, - Online, -} - -impl Default for OperationNetworkMode { - fn default() -> Self { - Self::Default - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OperationInputMode { - PromptingAllowed, - NoInput, -} - -impl Default for OperationInputMode { - fn default() -> Self { - Self::PromptingAllowed - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct OperationContext { - pub output_format: OperationOutputFormat, - pub account_id: Option<String>, - pub relays: Vec<String>, - pub network_mode: OperationNetworkMode, - pub dry_run: bool, - pub idempotency_key: Option<String>, - pub correlation_id: Option<String>, - pub approval_token: Option<String>, - pub input_mode: OperationInputMode, - pub quiet: bool, - pub verbose: bool, - pub trace: bool, - pub color: bool, -} - -impl OperationContext { - pub fn from_target_args(args: &TargetCliArgs) -> Self { - Self { - output_format: OperationOutputFormat::from(args.format), - account_id: args.account_id.clone(), - relays: args.relay.clone(), - network_mode: if args.offline { - OperationNetworkMode::Offline - } else if args.online { - OperationNetworkMode::Online - } else { - OperationNetworkMode::Default - }, - dry_run: args.dry_run, - idempotency_key: args.idempotency_key.clone(), - correlation_id: args.correlation_id.clone(), - approval_token: args.approval_token.clone(), - input_mode: if args.no_input { - OperationInputMode::NoInput - } else { - OperationInputMode::PromptingAllowed - }, - quiet: args.quiet, - verbose: args.verbose, - trace: args.trace, - color: !args.no_color, - } - } - - pub fn envelope_context(&self, request_id: impl Into<String>) -> EnvelopeContext { - let mut context = EnvelopeContext::new(request_id, self.dry_run); - context.output_format = match self.output_format { - OperationOutputFormat::Human => OutputFormat::Human, - OperationOutputFormat::Json => OutputFormat::Json, - OperationOutputFormat::Ndjson => OutputFormat::Ndjson, - }; - context.correlation_id = self.correlation_id.clone(); - context.idempotency_key = self.idempotency_key.clone(); - context.actor = self.account_id.as_ref().map(|account_id| EnvelopeActor { - account_id: account_id.clone(), - role: "account".to_owned(), - }); - context - } - - pub fn requires_approval_token(&self) -> bool { - !self.dry_run && !self.has_approval_token() - } - - pub fn has_approval_token(&self) -> bool { - self.approval_token - .as_deref() - .is_some_and(|token| !token.trim().is_empty()) - } -} - -pub type OperationData = Map<String, Value>; - -pub trait OperationRequestPayload: Debug + Clone + PartialEq + 'static { - const OPERATION_ID: &'static str; - const REQUEST_TYPE: &'static str; -} - -pub trait OperationRequestData: OperationRequestPayload { - fn input(&self) -> &OperationData; -} - -pub trait OperationResultPayload: Debug + Clone + PartialEq + Serialize + 'static { - const OPERATION_ID: &'static str; - const RESULT_TYPE: &'static str; -} - -pub trait OperationResultData: OperationResultPayload + Sized { - fn from_data(data: OperationData) -> Self; - - fn from_value(value: Value) -> Self { - Self::from_data(value_to_data(value)) - } - - fn from_serializable<T: Serialize>(value: &T) -> Result<Self, OperationAdapterError> { - Ok(Self::from_value(serde_json::to_value(value).map_err( - |error| OperationAdapterError::Serialization(error.to_string()), - )?)) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct OperationRequest<P: OperationRequestPayload> { - pub spec: &'static OperationSpec, - pub context: OperationContext, - pub payload: P, -} - -impl<P: OperationRequestPayload> OperationRequest<P> { - pub fn new(context: OperationContext, payload: P) -> Result<Self, OperationAdapterError> { - let spec = get_operation(P::OPERATION_ID) - .ok_or_else(|| OperationAdapterError::UnknownOperation(P::OPERATION_ID.to_owned()))?; - if spec.rust_request != P::REQUEST_TYPE { - return Err(OperationAdapterError::RequestTypeMismatch { - operation_id: P::OPERATION_ID.to_owned(), - registry_request: spec.rust_request.to_owned(), - adapter_request: P::REQUEST_TYPE.to_owned(), - }); - } - Ok(Self { - spec, - context, - payload, - }) - } - - pub fn operation_id(&self) -> &'static str { - P::OPERATION_ID - } - - pub fn request_type_name(&self) -> &'static str { - P::REQUEST_TYPE - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct OperationResult<P: OperationResultPayload> { - pub spec: &'static OperationSpec, - pub payload: P, - pub warnings: Vec<OutputWarning>, - pub next_actions: Vec<NextAction>, -} - -impl<P: OperationResultPayload> OperationResult<P> { - pub fn new(payload: P) -> Result<Self, OperationAdapterError> { - let spec = get_operation(P::OPERATION_ID) - .ok_or_else(|| OperationAdapterError::UnknownOperation(P::OPERATION_ID.to_owned()))?; - if spec.rust_result != P::RESULT_TYPE { - return Err(OperationAdapterError::ResultTypeMismatch { - operation_id: P::OPERATION_ID.to_owned(), - registry_result: spec.rust_result.to_owned(), - adapter_result: P::RESULT_TYPE.to_owned(), - }); - } - Ok(Self { - spec, - payload, - warnings: Vec::new(), - next_actions: Vec::new(), - }) - } - - pub fn operation_id(&self) -> &'static str { - P::OPERATION_ID - } - - pub fn result_type_name(&self) -> &'static str { - P::RESULT_TYPE - } - - pub fn to_envelope( - &self, - context: EnvelopeContext, - ) -> Result<OutputEnvelope, OperationAdapterError> { - let result = serde_json::to_value(&self.payload) - .map_err(|error| OperationAdapterError::Serialization(error.to_string()))?; - let next_actions = if self.next_actions.is_empty() { - next_actions_from_result(&result) - } else { - self.next_actions.clone() - }; - let mut envelope = OutputEnvelope::success(self.operation_id(), result, context); - envelope.warnings = self.warnings.clone(); - envelope.next_actions = next_actions; - Ok(envelope) - } -} - -fn next_actions_from_result(result: &Value) -> Vec<NextAction> { - next_actions_from_result_value(result) -} - -pub trait OperationService<P: OperationRequestPayload> { - type Result: OperationResultPayload; - - fn execute( - &self, - request: OperationRequest<P>, - ) -> Result<OperationResult<Self::Result>, OperationAdapterError>; -} - -#[derive(Debug, Clone)] -pub struct OperationAdapter<S> { - service: S, -} - -impl<S> OperationAdapter<S> { - pub fn new(service: S) -> Self { - Self { service } - } - - pub fn execute<P>( - &self, - request: OperationRequest<P>, - ) -> Result<OperationResult<<S as OperationService<P>>::Result>, OperationAdapterError> - where - P: OperationRequestPayload, - S: OperationService<P>, - { - self.service.execute(request) - } -} - -#[derive(Debug, thiserror::Error, PartialEq, Eq)] -pub enum OperationAdapterError { - #[error("unknown operation `{0}`")] - UnknownOperation(String), - #[error( - "operation `{operation_id}` registry request `{registry_request}` does not match adapter request `{adapter_request}`" - )] - RequestTypeMismatch { - operation_id: String, - registry_request: String, - adapter_request: String, - }, - #[error( - "operation `{operation_id}` registry result `{registry_result}` does not match adapter result `{adapter_result}`" - )] - ResultTypeMismatch { - operation_id: String, - registry_result: String, - adapter_result: String, - }, - #[error("failed to serialize operation result: {0}")] - Serialization(String), - #[error("invalid operation input for `{operation_id}`: {message}")] - InvalidInput { - operation_id: String, - message: String, - }, - #[error("resource not found for `{operation_id}`: {message}")] - NotFound { - operation_id: String, - message: String, - }, - #[error("validation failed for `{operation_id}`: {message}")] - ValidationFailed { - operation_id: String, - message: String, - }, - #[error("approval required for `{operation_id}`: {message}")] - ApprovalRequired { - operation_id: String, - message: String, - }, - #[error("operation `{operation_id}` is forbidden while offline: {message}")] - OfflineForbidden { - operation_id: String, - message: String, - }, - #[error("operation `{operation_id}` cannot run online: {message}")] - NetworkUnavailable { - operation_id: String, - message: String, - }, - #[error("account unresolved for `{operation_id}`: {message}")] - AccountUnresolved { - operation_id: String, - message: String, - }, - #[error("account is watch-only for `{operation_id}`: {message}")] - AccountWatchOnly { - operation_id: String, - message: String, - }, - #[error("account mismatch for `{operation_id}`: {message}")] - AccountMismatch { - operation_id: String, - message: String, - }, - #[error("signer unconfigured for `{operation_id}`: {message}")] - SignerUnconfigured { - operation_id: String, - message: String, - }, - #[error("signer unavailable for `{operation_id}`: {message}")] - SignerUnavailable { - operation_id: String, - message: String, - }, - #[error("signer mode deferred for `{operation_id}`: {message}")] - SignerModeDeferred { - operation_id: String, - message: String, - }, - #[error("provider unconfigured for `{operation_id}`: {message}")] - ProviderUnconfigured { - operation_id: String, - message: String, - }, - #[error("provider unavailable for `{operation_id}`: {message}")] - ProviderUnavailable { - operation_id: String, - message: String, - }, - #[error("operation `{operation_id}` is unavailable: {message}")] - OperationUnavailable { - operation_id: String, - message: String, - }, - #[error("operation `{operation_id}` is not implemented: {message}")] - NotImplemented { - operation_id: String, - message: String, - }, - #[error("operation `{operation_id}` failed: {message}")] - DetailedFailure { - operation_id: String, - code: String, - class: String, - message: String, - exit_code: CliExitCode, - detail_json: String, - }, - #[error("operation runtime error: {0}")] - Runtime(String), -} - -impl OperationAdapterError { - pub fn approval_required(operation_id: &str) -> Self { - Self::ApprovalRequired { - operation_id: operation_id.to_owned(), - message: "missing required `approval_token` input".to_owned(), - } - } - - pub fn from_command_disposition( - operation_id: &str, - disposition: CommandDisposition, - message: String, - ) -> Self { - match disposition { - CommandDisposition::Success => Self::Runtime(message), - CommandDisposition::NotFound => Self::NotFound { - operation_id: operation_id.to_owned(), - message, - }, - CommandDisposition::ValidationFailed => Self::ValidationFailed { - operation_id: operation_id.to_owned(), - message, - }, - CommandDisposition::Unconfigured => Self::unconfigured(operation_id, message), - CommandDisposition::ExternalUnavailable => Self::unavailable(operation_id, message), - CommandDisposition::Unsupported => Self::InvalidInput { - operation_id: operation_id.to_owned(), - message, - }, - CommandDisposition::InternalError => Self::Runtime(message), - } - } - - pub fn unconfigured(operation_id: &str, message: String) -> Self { - classify_runtime_failure( - operation_id, - message, - RuntimeFailureAvailability::Unconfigured, - ) - } - - pub fn operation_unavailable_with_detail( - operation_id: &str, - message: String, - detail: Value, - ) -> Self { - Self::DetailedFailure { - operation_id: operation_id.to_owned(), - code: "operation_unavailable".to_owned(), - class: "operation".to_owned(), - message, - exit_code: CliExitCode::RuntimeUnavailable, - detail_json: detail.to_string(), - } - } - - pub fn not_found_with_detail(operation_id: &str, message: String, detail: Value) -> Self { - Self::DetailedFailure { - operation_id: operation_id.to_owned(), - code: "not_found".to_owned(), - class: "resource".to_owned(), - message, - exit_code: CliExitCode::NotFound, - detail_json: detail.to_string(), - } - } - - pub fn not_implemented(operation_id: &str, message: String) -> Self { - Self::NotImplemented { - operation_id: operation_id.to_owned(), - message, - } - } - - pub fn not_implemented_with_detail(operation_id: &str, message: String, detail: Value) -> Self { - Self::DetailedFailure { - operation_id: operation_id.to_owned(), - code: "not_implemented".to_owned(), - class: "operation".to_owned(), - message, - exit_code: CliExitCode::RuntimeUnavailable, - detail_json: detail.to_string(), - } - } - - pub fn network_unavailable_with_detail( - operation_id: &str, - message: String, - detail: Value, - ) -> Self { - Self::DetailedFailure { - operation_id: operation_id.to_owned(), - code: "network_unavailable".to_owned(), - class: "network".to_owned(), - message, - exit_code: CliExitCode::SyncOrNetworkFailure, - detail_json: detail.to_string(), - } - } - - pub fn validation_failed_with_detail( - operation_id: &str, - message: String, - detail: Value, - ) -> Self { - Self::DetailedFailure { - operation_id: operation_id.to_owned(), - code: "validation_failed".to_owned(), - class: "validation".to_owned(), - message, - exit_code: CliExitCode::ValidationFailed, - detail_json: detail.to_string(), - } - } - - pub fn unavailable(operation_id: &str, message: String) -> Self { - classify_runtime_failure( - operation_id, - message, - RuntimeFailureAvailability::Unavailable, - ) - } - - pub fn runtime_failure(operation_id: &str, error: RuntimeError) -> Self { - let message = error.to_string(); - let lowered = message.to_ascii_lowercase(); - match &error { - RuntimeError::Io(io_error) if io_error.kind() == ErrorKind::NotFound => { - Self::NotFound { - operation_id: operation_id.to_owned(), - message, - } - } - RuntimeError::Config(_) if looks_like_not_found(&lowered) => Self::NotFound { - operation_id: operation_id.to_owned(), - message, - }, - RuntimeError::Account(failure) => account_runtime_failure(operation_id, failure), - RuntimeError::Config(_) - if contains_any( - &lowered, - &[ - "no local account", - "account selector", - "account selection", - "account mismatch", - "did not match any local account", - "unresolved account", - ], - ) => - { - classify_runtime_failure( - operation_id, - message, - RuntimeFailureAvailability::Unconfigured, - ) - } - RuntimeError::Config(_) if looks_like_validation_failure(&lowered) => { - Self::ValidationFailed { - operation_id: operation_id.to_owned(), - message, - } - } - RuntimeError::Network(_) if looks_like_auth_failure(&lowered) => { - auth_runtime_failure(operation_id, message, &lowered) - } - RuntimeError::Network(_) if looks_like_signer_failure(&lowered) => { - Self::SignerUnavailable { - operation_id: operation_id.to_owned(), - message, - } - } - RuntimeError::Network(_) if looks_like_provider_failure(&lowered) => { - Self::ProviderUnavailable { - operation_id: operation_id.to_owned(), - message, - } - } - RuntimeError::Network(_) if looks_like_operation_failure(&lowered) => { - Self::OperationUnavailable { - operation_id: operation_id.to_owned(), - message, - } - } - RuntimeError::Network(_) => Self::NetworkUnavailable { - operation_id: operation_id.to_owned(), - message, - }, - RuntimeError::Accounts(_) => classify_runtime_failure( - operation_id, - message, - RuntimeFailureAvailability::Unavailable, - ), - _ => Self::Runtime(message), - } - } - - pub fn to_output_error(&self) -> OutputError { - match self { - Self::ApprovalRequired { message, .. } => OutputError::new( - "approval_required", - message.clone(), - CliExitCode::ApprovalRequiredOrDenied, - ), - Self::InvalidInput { message, .. } => { - OutputError::new("invalid_input", message.clone(), CliExitCode::InvalidInput) - } - Self::NotFound { - operation_id, - message, - } => runtime_output_error( - "not_found", - operation_id, - "resource", - message, - CliExitCode::NotFound, - ), - Self::ValidationFailed { - operation_id, - message, - } => runtime_output_error( - "validation_failed", - operation_id, - "validation", - message, - CliExitCode::ValidationFailed, - ), - Self::OfflineForbidden { - operation_id, - message, - } => runtime_output_error( - "offline_forbidden", - operation_id, - "network", - message, - CliExitCode::SyncOrNetworkFailure, - ), - Self::NetworkUnavailable { - operation_id, - message, - } => runtime_output_error( - "network_unavailable", - operation_id, - "network", - message, - CliExitCode::SyncOrNetworkFailure, - ), - Self::AccountUnresolved { - operation_id, - message, - } => runtime_output_error( - "account_unresolved", - operation_id, - "account", - message, - CliExitCode::AuthorizationFailed, - ), - Self::AccountWatchOnly { - operation_id, - message, - } => runtime_output_error( - "account_watch_only", - operation_id, - "account", - message, - CliExitCode::SignerUnavailable, - ), - Self::AccountMismatch { - operation_id, - message, - } => runtime_output_error( - "account_mismatch", - operation_id, - "account", - message, - CliExitCode::AuthorizationFailed, - ), - Self::SignerUnconfigured { - operation_id, - message, - } => runtime_output_error( - "signer_unconfigured", - operation_id, - "signer", - message, - CliExitCode::SignerUnavailable, - ), - Self::SignerUnavailable { - operation_id, - message, - } => runtime_output_error( - "signer_unavailable", - operation_id, - "signer", - message, - CliExitCode::SignerUnavailable, - ), - Self::SignerModeDeferred { - operation_id, - message, - } => runtime_output_error( - "signer_mode_deferred", - operation_id, - "signer", - message, - CliExitCode::SignerUnavailable, - ), - Self::ProviderUnconfigured { - operation_id, - message, - } => runtime_output_error( - "provider_unconfigured", - operation_id, - "provider", - message, - CliExitCode::RuntimeUnavailable, - ), - Self::ProviderUnavailable { - operation_id, - message, - } => runtime_output_error( - "provider_unavailable", - operation_id, - "provider", - message, - CliExitCode::RuntimeUnavailable, - ), - Self::OperationUnavailable { - operation_id, - message, - } => runtime_output_error( - "operation_unavailable", - operation_id, - "operation", - message, - CliExitCode::RuntimeUnavailable, - ), - Self::NotImplemented { - operation_id, - message, - } => runtime_output_error( - "not_implemented", - operation_id, - "operation", - message, - CliExitCode::RuntimeUnavailable, - ), - Self::DetailedFailure { - operation_id, - code, - class, - message, - exit_code, - detail_json, - } => runtime_output_error_with_detail( - code.as_str(), - operation_id, - class, - message, - *exit_code, - detail_json, - ), - Self::UnknownOperation(operation_id) => OutputError::new( - "unknown_operation", - format!("unknown operation `{operation_id}`"), - CliExitCode::InvalidInput, - ), - Self::RequestTypeMismatch { .. } | Self::ResultTypeMismatch { .. } => OutputError::new( - "contract_mismatch", - self.to_string(), - CliExitCode::InternalError, - ), - Self::Serialization(message) => OutputError::new( - "serialization_failed", - message.clone(), - CliExitCode::InternalError, - ), - Self::Runtime(message) => { - OutputError::new("runtime_error", message.clone(), CliExitCode::InternalError) - } - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum RuntimeFailureAvailability { - Unconfigured, - Unavailable, -} - -fn account_runtime_failure( - operation_id: &str, - failure: &AccountRuntimeFailure, -) -> OperationAdapterError { - let message = failure.message().to_owned(); - match failure { - AccountRuntimeFailure::Unresolved(_) => account_failure_output( - operation_id, - "account_unresolved", - message, - CliExitCode::AuthorizationFailed, - failure.detail_json(), - || OperationAdapterError::AccountUnresolved { - operation_id: operation_id.to_owned(), - message: failure.message().to_owned(), - }, - ), - AccountRuntimeFailure::WatchOnly(_) => account_failure_output( - operation_id, - "account_watch_only", - message, - CliExitCode::SignerUnavailable, - failure.detail_json(), - || OperationAdapterError::AccountWatchOnly { - operation_id: operation_id.to_owned(), - message: failure.message().to_owned(), - }, - ), - AccountRuntimeFailure::Mismatch(_) => account_failure_output( - operation_id, - "account_mismatch", - message, - CliExitCode::AuthorizationFailed, - failure.detail_json(), - || OperationAdapterError::AccountMismatch { - operation_id: operation_id.to_owned(), - message: failure.message().to_owned(), - }, - ), - } -} - -fn account_failure_output( - operation_id: &str, - code: &str, - message: String, - exit_code: CliExitCode, - detail_json: Option<&str>, - fallback: impl FnOnce() -> OperationAdapterError, -) -> OperationAdapterError { - match detail_json { - Some(detail_json) => OperationAdapterError::DetailedFailure { - operation_id: operation_id.to_owned(), - code: code.to_owned(), - class: "account".to_owned(), - message, - exit_code, - detail_json: detail_json.to_owned(), - }, - None => fallback(), - } -} - -fn auth_runtime_failure( - operation_id: &str, - message: String, - lowered: &str, -) -> OperationAdapterError { - let unauthorized = contains_any( - lowered, - &[ - "unauthorized", - "forbidden", - "permission denied", - "invalid token", - "bearer token rejected", - "http 401", - "http 403", - "status 401", - "status 403", - ], - ); - OperationAdapterError::DetailedFailure { - operation_id: operation_id.to_owned(), - code: if unauthorized { - "auth_unauthorized".to_owned() - } else { - "auth_unavailable".to_owned() - }, - class: "auth".to_owned(), - message, - exit_code: CliExitCode::AuthorizationFailed, - detail_json: Value::Null.to_string(), - } -} - -fn classify_runtime_failure( - operation_id: &str, - message: String, - availability: RuntimeFailureAvailability, -) -> OperationAdapterError { - let lowered = message.to_ascii_lowercase(); - if contains_any(&lowered, &["watch_only", "watch-only", "watch only"]) { - return OperationAdapterError::AccountWatchOnly { - operation_id: operation_id.to_owned(), - message, - }; - } - if contains_any(&lowered, &["account mismatch"]) { - return OperationAdapterError::AccountMismatch { - operation_id: operation_id.to_owned(), - message, - }; - } - if contains_any( - &lowered, - &[ - "no account", - "no local account", - "account selector", - "account selection", - "did not match any local account", - "unresolved account", - "selected account", - ], - ) { - return OperationAdapterError::AccountUnresolved { - operation_id: operation_id.to_owned(), - message, - }; - } - if contains_any( - &lowered, - &[ - "signer", - "sign_event", - "remote_nip46", - "nip46", - "secret-backed", - "secret backed", - ], - ) { - return match availability { - RuntimeFailureAvailability::Unconfigured => OperationAdapterError::SignerUnconfigured { - operation_id: operation_id.to_owned(), - message, - }, - RuntimeFailureAvailability::Unavailable => OperationAdapterError::SignerUnavailable { - operation_id: operation_id.to_owned(), - message, - }, - }; - } - if contains_any( - &lowered, - &[ - "provider", - "write-plane", - "write plane", - "radrootsd", - "bridge", - "rpc", - "daemon", - ], - ) { - return match availability { - RuntimeFailureAvailability::Unconfigured => { - OperationAdapterError::ProviderUnconfigured { - operation_id: operation_id.to_owned(), - message, - } - } - RuntimeFailureAvailability::Unavailable => OperationAdapterError::ProviderUnavailable { - operation_id: operation_id.to_owned(), - message, - }, - }; - } - OperationAdapterError::OperationUnavailable { - operation_id: operation_id.to_owned(), - message, - } -} - -fn contains_any(value: &str, needles: &[&str]) -> bool { - needles.iter().any(|needle| value.contains(needle)) -} - -fn looks_like_auth_failure(value: &str) -> bool { - contains_any( - value, - &[ - "authentication", - "bridge auth", - "authorization", - "authorize", - "unauthorized", - "forbidden", - "bearer token", - "invalid token", - "permission denied", - "status 401", - "status 403", - "http 401", - "http 403", - ], - ) -} - -fn looks_like_signer_failure(value: &str) -> bool { - contains_any( - value, - &[ - "signer", - "sign_event", - "sign event", - "signer_session_id", - "signer session", - "nip46", - "nip-46", - "remote_nip46", - ], - ) -} - -fn looks_like_provider_failure(value: &str) -> bool { - contains_any( - value, - &[ - "provider unavailable", - "provider unconfigured", - "provider runtime", - "provider failed", - "radrootsd unavailable", - "daemon unavailable", - "bridge provider", - ], - ) -} - -fn looks_like_operation_failure(value: &str) -> bool { - contains_any( - value, - &[ - "method not found", - "unknown method", - "unsupported method", - "unsupported operation", - "operation unavailable", - "operation disabled", - "bridge disabled", - "bridge is disabled", - "bridge.listing.publish is disabled", - ], - ) -} - -fn looks_like_not_found(value: &str) -> bool { - contains_any( - value, - &[ - "not found", - "no such file or directory", - "path not found", - "missing file", - ], - ) -} - -fn looks_like_validation_failure(value: &str) -> bool { - contains_any( - value, - &[ - "invalid", - "parse ", - "parse:", - "must not", - "must be", - "validation", - "failed to import account", - ], - ) -} - -fn runtime_output_error( - code: &str, - operation_id: &str, - class: &str, - message: &str, - exit_code: CliExitCode, -) -> OutputError { - let mut error = OutputError::new(code, message.to_owned(), exit_code); - error.detail = Some(json!({ - "operation_id": operation_id, - "class": class, - })); - error -} - -fn runtime_output_error_with_detail( - code: &str, - operation_id: &str, - class: &str, - message: &str, - exit_code: CliExitCode, - detail_json: &str, -) -> OutputError { - let mut error = OutputError::new(code, message.to_owned(), exit_code); - let mut detail = serde_json::from_str::<Map<String, Value>>(detail_json).unwrap_or_default(); - detail.insert( - "operation_id".to_owned(), - Value::from(operation_id.to_owned()), - ); - detail.insert("class".to_owned(), Value::from(class.to_owned())); - error.detail = Some(Value::Object(detail)); - error -} - -macro_rules! target_operation_contracts { - ($( $variant:ident => ($request:ident, $result:ident, $operation_id:literal) ),+ $(,)?) => { - #[derive(Debug, Clone, PartialEq)] - pub enum TargetOperationRequest { - $( $variant(OperationRequest<$request>), )+ - } - - impl TargetOperationRequest { - pub fn from_target_args(args: &TargetCliArgs) -> Result<Self, OperationAdapterError> { - Self::from_operation_id_with_input( - crate::cli::input::operation_id_from_target(args), - OperationContext::from_target_args(args), - crate::cli::input::target_operation_input(&args.command), - ) - } - - pub fn from_operation_id( - operation_id: &'static str, - context: OperationContext, - ) -> Result<Self, OperationAdapterError> { - Self::from_operation_id_with_input(operation_id, context, OperationData::new()) - } - - fn from_operation_id_with_input( - operation_id: &'static str, - context: OperationContext, - input: OperationData, - ) -> Result<Self, OperationAdapterError> { - match operation_id { - $( $operation_id => Ok(Self::$variant(OperationRequest::new(context, $request::from_data(input))?)), )+ - _ => Err(OperationAdapterError::UnknownOperation(operation_id.to_owned())), - } - } - - pub fn operation_id(&self) -> &'static str { - match self { - $( Self::$variant(request) => request.operation_id(), )+ - } - } - - pub fn spec(&self) -> &'static OperationSpec { - match self { - $( Self::$variant(request) => request.spec, )+ - } - } - - pub fn context(&self) -> &OperationContext { - match self { - $( Self::$variant(request) => &request.context, )+ - } - } - - pub fn request_type_name(&self) -> &'static str { - match self { - $( Self::$variant(request) => request.request_type_name(), )+ - } - } - - pub fn request_type_for_operation(operation_id: &str) -> Option<&'static str> { - match operation_id { - $( $operation_id => Some(stringify!($request)), )+ - _ => None, - } - } - } - - #[derive(Debug, Clone, PartialEq)] - pub enum TargetOperationResult { - $( $variant(OperationResult<$result>), )+ - } - - impl TargetOperationResult { - pub fn operation_id(&self) -> &'static str { - match self { - $( Self::$variant(result) => result.operation_id(), )+ - } - } - - pub fn result_type_name(&self) -> &'static str { - match self { - $( Self::$variant(result) => result.result_type_name(), )+ - } - } - - pub fn result_type_for_operation(operation_id: &str) -> Option<&'static str> { - match operation_id { - $( $operation_id => Some(stringify!($result)), )+ - _ => None, - } - } - } - - $( - #[derive(Debug, Default, Clone, PartialEq, Serialize)] - pub struct $request { - #[serde(flatten)] - pub input: OperationData, - } - - impl $request { - pub fn from_data(input: OperationData) -> Self { - Self { input } - } - } - - impl OperationRequestPayload for $request { - const OPERATION_ID: &'static str = $operation_id; - const REQUEST_TYPE: &'static str = stringify!($request); - } - - impl OperationRequestData for $request { - fn input(&self) -> &OperationData { - &self.input - } - } - - #[derive(Debug, Default, Clone, PartialEq, Serialize)] - pub struct $result { - #[serde(flatten)] - pub data: OperationData, - } - - impl $result { - pub fn from_data(data: OperationData) -> Self { - Self { data } - } - - pub fn from_value(value: Value) -> Self { - Self { - data: value_to_data(value), - } - } - - pub fn from_serializable<T: Serialize>( - value: &T, - ) -> Result<Self, OperationAdapterError> { - Ok(Self::from_value( - serde_json::to_value(value) - .map_err(|error| OperationAdapterError::Serialization(error.to_string()))?, - )) - } - } - - impl OperationResultPayload for $result { - const OPERATION_ID: &'static str = $operation_id; - const RESULT_TYPE: &'static str = stringify!($result); - } - - impl OperationResultData for $result { - fn from_data(data: OperationData) -> Self { - Self { data } - } - } - )+ - }; -} - -fn value_to_data(value: Value) -> OperationData { - match value { - Value::Object(map) => map, - other => { - let mut map = OperationData::new(); - map.insert("value".to_owned(), other); - map - } - } -} - -target_operation_contracts! { - WorkspaceInit => (WorkspaceInitRequest, WorkspaceInitResult, "workspace.init"), - WorkspaceGet => (WorkspaceGetRequest, WorkspaceGetResult, "workspace.get"), - HealthStatusGet => (HealthStatusGetRequest, HealthStatusGetResult, "health.status.get"), - HealthCheckRun => (HealthCheckRunRequest, HealthCheckRunResult, "health.check.run"), - ConfigGet => (ConfigGetRequest, ConfigGetResult, "config.get"), - AccountCreate => (AccountCreateRequest, AccountCreateResult, "account.create"), - AccountImport => (AccountImportRequest, AccountImportResult, "account.import"), - AccountAttachSecret => (AccountAttachSecretRequest, AccountAttachSecretResult, "account.attach_secret"), - AccountGet => (AccountGetRequest, AccountGetResult, "account.get"), - AccountList => (AccountListRequest, AccountListResult, "account.list"), - AccountRemove => (AccountRemoveRequest, AccountRemoveResult, "account.remove"), - AccountSelectionGet => (AccountSelectionGetRequest, AccountSelectionGetResult, "account.selection.get"), - AccountSelectionUpdate => (AccountSelectionUpdateRequest, AccountSelectionUpdateResult, "account.selection.update"), - AccountSelectionClear => (AccountSelectionClearRequest, AccountSelectionClearResult, "account.selection.clear"), - SignerStatusGet => (SignerStatusGetRequest, SignerStatusGetResult, "signer.status.get"), - RelayList => (RelayListRequest, RelayListResult, "relay.list"), - StoreInit => (StoreInitRequest, StoreInitResult, "store.init"), - StoreStatusGet => (StoreStatusGetRequest, StoreStatusGetResult, "store.status.get"), - StoreExport => (StoreExportRequest, StoreExportResult, "store.export"), - StoreBackupCreate => (StoreBackupCreateRequest, StoreBackupCreateResult, "store.backup.create"), - SyncStatusGet => (SyncStatusGetRequest, SyncStatusGetResult, "sync.status.get"), - SyncPull => (SyncPullRequest, SyncPullResult, "sync.pull"), - SyncPush => (SyncPushRequest, SyncPushResult, "sync.push"), - SyncWatch => (SyncWatchRequest, SyncWatchResult, "sync.watch"), - FarmCreate => (FarmCreateRequest, FarmCreateResult, "farm.create"), - FarmGet => (FarmGetRequest, FarmGetResult, "farm.get"), - FarmRebind => (FarmRebindRequest, FarmRebindResult, "farm.rebind"), - FarmProfileUpdate => (FarmProfileUpdateRequest, FarmProfileUpdateResult, "farm.profile.update"), - FarmLocationUpdate => (FarmLocationUpdateRequest, FarmLocationUpdateResult, "farm.location.update"), - FarmFulfillmentUpdate => (FarmFulfillmentUpdateRequest, FarmFulfillmentUpdateResult, "farm.fulfillment.update"), - FarmReadinessCheck => (FarmReadinessCheckRequest, FarmReadinessCheckResult, "farm.readiness.check"), - FarmPublish => (FarmPublishRequest, FarmPublishResult, "farm.publish"), - ListingCreate => (ListingCreateRequest, ListingCreateResult, "listing.create"), - ListingGet => (ListingGetRequest, ListingGetResult, "listing.get"), - ListingList => (ListingListRequest, ListingListResult, "listing.list"), - ListingAppList => (ListingAppListRequest, ListingAppListResult, "listing.app.list"), - ListingAppExport => (ListingAppExportRequest, ListingAppExportResult, "listing.app.export"), - ListingUpdate => (ListingUpdateRequest, ListingUpdateResult, "listing.update"), - ListingValidate => (ListingValidateRequest, ListingValidateResult, "listing.validate"), - ListingRebind => (ListingRebindRequest, ListingRebindResult, "listing.rebind"), - ListingPublish => (ListingPublishRequest, ListingPublishResult, "listing.publish"), - ListingArchive => (ListingArchiveRequest, ListingArchiveResult, "listing.archive"), - MarketRefresh => (MarketRefreshRequest, MarketRefreshResult, "market.refresh"), - MarketProductSearch => (MarketProductSearchRequest, MarketProductSearchResult, "market.product.search"), - MarketListingGet => (MarketListingGetRequest, MarketListingGetResult, "market.listing.get"), - BasketCreate => (BasketCreateRequest, BasketCreateResult, "basket.create"), - BasketGet => (BasketGetRequest, BasketGetResult, "basket.get"), - BasketList => (BasketListRequest, BasketListResult, "basket.list"), - BasketItemAdd => (BasketItemAddRequest, BasketItemAddResult, "basket.item.add"), - BasketItemUpdate => (BasketItemUpdateRequest, BasketItemUpdateResult, "basket.item.update"), - BasketItemRemove => (BasketItemRemoveRequest, BasketItemRemoveResult, "basket.item.remove"), - BasketAdjustmentAdd => (BasketAdjustmentAddRequest, BasketAdjustmentAddResult, "basket.adjustment.add"), - BasketAdjustmentRemove => (BasketAdjustmentRemoveRequest, BasketAdjustmentRemoveResult, "basket.adjustment.remove"), - BasketValidate => (BasketValidateRequest, BasketValidateResult, "basket.validate"), - BasketQuoteCreate => (BasketQuoteCreateRequest, BasketQuoteCreateResult, "basket.quote.create"), - OrderSubmit => (OrderSubmitRequest, OrderSubmitResult, "order.submit"), - OrderGet => (OrderGetRequest, OrderGetResult, "order.get"), - OrderList => (OrderListRequest, OrderListResult, "order.list"), - OrderAppList => (OrderAppListRequest, OrderAppListResult, "order.app.list"), - OrderAppExport => (OrderAppExportRequest, OrderAppExportResult, "order.app.export"), - OrderRebind => (OrderRebindRequest, OrderRebindResult, "order.rebind"), - OrderAccept => (OrderAcceptRequest, OrderAcceptResult, "order.accept"), - OrderDecline => (OrderDeclineRequest, OrderDeclineResult, "order.decline"), - OrderCancel => (OrderCancelRequest, OrderCancelResult, "order.cancel"), - OrderRevisionPropose => (OrderRevisionProposeRequest, OrderRevisionProposeResult, "order.revision.propose"), - OrderRevisionAccept => (OrderRevisionAcceptRequest, OrderRevisionAcceptResult, "order.revision.accept"), - OrderRevisionDecline => (OrderRevisionDeclineRequest, OrderRevisionDeclineResult, "order.revision.decline"), - OrderFulfillmentUpdate => (OrderFulfillmentUpdateRequest, OrderFulfillmentUpdateResult, "order.fulfillment.update"), - OrderReceiptRecord => (OrderReceiptRecordRequest, OrderReceiptRecordResult, "order.receipt.record"), - OrderPaymentRecord => (OrderPaymentRecordRequest, OrderPaymentRecordResult, "order.payment.record"), - OrderSettlementAccept => (OrderSettlementAcceptRequest, OrderSettlementAcceptResult, "order.settlement.accept"), - OrderSettlementReject => (OrderSettlementRejectRequest, OrderSettlementRejectResult, "order.settlement.reject"), - OrderStatusGet => (OrderStatusGetRequest, OrderStatusGetResult, "order.status.get"), - OrderEventList => (OrderEventListRequest, OrderEventListResult, "order.event.list"), - OrderEventWatch => (OrderEventWatchRequest, OrderEventWatchResult, "order.event.watch"), - ValidationReceiptGet => (ValidationReceiptGetRequest, ValidationReceiptGetResult, "validation.receipt.get"), - ValidationReceiptList => (ValidationReceiptListRequest, ValidationReceiptListResult, "validation.receipt.list"), - ValidationReceiptVerify => (ValidationReceiptVerifyRequest, ValidationReceiptVerifyResult, "validation.receipt.verify"), -} - -pub fn adapter_registry_linkage_is_valid() -> bool { - OPERATION_REGISTRY.iter().all(|operation| { - TargetOperationRequest::request_type_for_operation(operation.operation_id) - == Some(operation.rust_request) - && TargetOperationResult::result_type_for_operation(operation.operation_id) - == Some(operation.rust_result) - }) -} +mod context; +mod contract; +mod error; +mod service; + +pub use context::*; +pub use contract::*; +pub use error::OperationAdapterError; +pub use service::*; #[cfg(test)] mod tests { diff --git a/src/ops/service.rs b/src/ops/service.rs @@ -0,0 +1,35 @@ +use super::contract::{ + OperationRequest, OperationRequestPayload, OperationResult, OperationResultPayload, +}; +use super::error::OperationAdapterError; + +pub trait OperationService<P: OperationRequestPayload> { + type Result: OperationResultPayload; + + fn execute( + &self, + request: OperationRequest<P>, + ) -> Result<OperationResult<Self::Result>, OperationAdapterError>; +} + +#[derive(Debug, Clone)] +pub struct OperationAdapter<S> { + service: S, +} + +impl<S> OperationAdapter<S> { + pub fn new(service: S) -> Self { + Self { service } + } + + pub fn execute<P>( + &self, + request: OperationRequest<P>, + ) -> Result<OperationResult<<S as OperationService<P>>::Result>, OperationAdapterError> + where + P: OperationRequestPayload, + S: OperationService<P>, + { + self.service.execute(request) + } +}