commit c7063fc3fc3440938bb80c58b34ce8eedaa7b7ee
parent 6922ee248c652334bdd2ec43fe8cc1f2438cfc1f
Author: triesap <tyson@radroots.org>
Date: Tue, 26 May 2026 20:24:12 +0000
ops: split operation execution modules
Diffstat:
| A | src/ops/context.rs | | | 123 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/ops/contract.rs | | | 391 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | src/ops/error.rs | | | 831 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/ops/mod.rs | | | 1378 | +------------------------------------------------------------------------------ |
| A | src/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)
+ }
+}