commit 3d132a9ae8b0ad07091a12e6c5b3e3d64e2c7644
parent fa2569572700d2430dc55f4ff3459fe5f58327a0
Author: triesap <tyson@radroots.org>
Date: Tue, 26 May 2026 20:06:18 +0000
cli: organize command parser modules
Diffstat:
31 files changed, 4339 insertions(+), 4337 deletions(-)
diff --git a/src/runtime_args.rs b/src/cli/global.rs
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
@@ -0,0 +1,1721 @@
+#![allow(dead_code)]
+
+pub mod global;
+
+use std::path::PathBuf;
+
+use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
+pub enum TargetOutputFormat {
+ Human,
+ Json,
+ Ndjson,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
+pub enum TargetPublishMode {
+ #[value(name = "nostr_relay")]
+ NostrRelay,
+ Radrootsd,
+}
+
+impl TargetPublishMode {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::NostrRelay => "nostr_relay",
+ Self::Radrootsd => "radrootsd",
+ }
+ }
+}
+
+#[derive(Debug, Parser, Clone)]
+#[command(
+ name = "radroots",
+ about = "Operate Radroots local-first trade workflows.",
+ long_about = "Operate Radroots local-first trade workflows.\n\nPublish modes:\n nostr_relay uses direct relay publish with local signer custody.\n radrootsd is reserved and fails closed for active buyer and seller writes.\n\nRelay mode never silently falls back to radrootsd.",
+ disable_help_subcommand = true
+)]
+pub struct TargetCliArgs {
+ #[arg(long = "format", global = true, value_enum, default_value = "human")]
+ pub format: TargetOutputFormat,
+ #[arg(long = "account-id", global = true)]
+ pub account_id: Option<String>,
+ #[arg(long = "relay", global = true)]
+ pub relay: Vec<String>,
+ #[arg(
+ long = "publish-mode",
+ global = true,
+ value_enum,
+ help = "Select nostr_relay direct relay publish or reserved radrootsd guardrail mode"
+ )]
+ pub publish_mode: Option<TargetPublishMode>,
+ #[arg(long = "offline", global = true, action = ArgAction::SetTrue, conflicts_with = "online")]
+ pub offline: bool,
+ #[arg(long = "online", global = true, action = ArgAction::SetTrue, conflicts_with = "offline")]
+ pub online: bool,
+ #[arg(long = "dry-run", global = true, action = ArgAction::SetTrue)]
+ pub dry_run: bool,
+ #[arg(long = "idempotency-key", global = true)]
+ pub idempotency_key: Option<String>,
+ #[arg(long = "correlation-id", global = true)]
+ pub correlation_id: Option<String>,
+ #[arg(long = "approval-token", global = true)]
+ pub approval_token: Option<String>,
+ #[arg(long = "no-input", global = true, action = ArgAction::SetTrue)]
+ pub no_input: bool,
+ #[arg(long = "quiet", global = true, action = ArgAction::SetTrue)]
+ pub quiet: bool,
+ #[arg(long = "verbose", global = true, action = ArgAction::SetTrue)]
+ pub verbose: bool,
+ #[arg(long = "trace", global = true, action = ArgAction::SetTrue)]
+ pub trace: bool,
+ #[arg(long = "no-color", global = true, action = ArgAction::SetTrue)]
+ pub no_color: bool,
+ #[command(subcommand)]
+ pub command: TargetCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum TargetCommand {
+ #[command(about = "Inspect and initialize workspace state.")]
+ Workspace(WorkspaceArgs),
+ #[command(about = "Inspect local readiness and mode-specific recovery steps.")]
+ Health(HealthArgs),
+ #[command(about = "Show effective configuration and publish-plane readiness.")]
+ Config(ConfigArgs),
+ #[command(about = "Manage local signer accounts and custody.")]
+ Account(AccountArgs),
+ #[command(about = "Inspect signer readiness for local relay writes.")]
+ Signer(SignerArgs),
+ #[command(about = "List configured relay targets for direct relay mode.")]
+ Relay(RelayArgs),
+ #[command(about = "Initialize and inspect the local replica store.")]
+ Store(StoreArgs),
+ #[command(about = "Read from relay events into the local replica.")]
+ Sync(SyncArgs),
+ #[command(about = "Create, inspect, and publish farm profile data.")]
+ Farm(FarmArgs),
+ #[command(about = "Create, inspect, and publish listing data.")]
+ Listing(ListingArgs),
+ #[command(about = "Refresh and query market data from the local replica.")]
+ Market(MarketArgs),
+ #[command(about = "Prepare baskets and quotes before order coordination.")]
+ Basket(BasketArgs),
+ #[command(about = "Coordinate order lifecycle events without payments.")]
+ Order(OrderArgs),
+ #[command(about = "Inspect validation receipts and proof state.")]
+ Validation(ValidationArgs),
+}
+
+impl TargetCommand {
+ pub fn operation_id(&self) -> &'static str {
+ match self {
+ Self::Workspace(args) => match args.command {
+ WorkspaceCommand::Init => "workspace.init",
+ WorkspaceCommand::Get => "workspace.get",
+ },
+ Self::Health(args) => match &args.command {
+ HealthCommand::Status(status) => match status.command {
+ HealthStatusCommand::Get => "health.status.get",
+ },
+ HealthCommand::Check(check) => match check.command {
+ HealthCheckCommand::Run => "health.check.run",
+ },
+ },
+ Self::Config(args) => match args.command {
+ ConfigCommand::Get => "config.get",
+ },
+ Self::Account(args) => match &args.command {
+ AccountCommand::Create => "account.create",
+ AccountCommand::Import(_) => "account.import",
+ AccountCommand::AttachSecret(_) => "account.attach_secret",
+ AccountCommand::Get(_) => "account.get",
+ AccountCommand::List => "account.list",
+ AccountCommand::Remove(_) => "account.remove",
+ AccountCommand::Selection(selection) => match &selection.command {
+ AccountSelectionCommand::Get => "account.selection.get",
+ AccountSelectionCommand::Update(_) => "account.selection.update",
+ AccountSelectionCommand::Clear => "account.selection.clear",
+ },
+ },
+ Self::Signer(args) => match &args.command {
+ SignerCommand::Status(status) => match status.command {
+ SignerStatusCommand::Get => "signer.status.get",
+ },
+ },
+ Self::Relay(args) => match args.command {
+ RelayCommand::List => "relay.list",
+ },
+ Self::Store(args) => match &args.command {
+ StoreCommand::Init => "store.init",
+ StoreCommand::Status(status) => match status.command {
+ StoreStatusCommand::Get => "store.status.get",
+ },
+ StoreCommand::Export => "store.export",
+ StoreCommand::Backup(backup) => match backup.command {
+ StoreBackupCommand::Create => "store.backup.create",
+ },
+ },
+ Self::Sync(args) => match &args.command {
+ SyncCommand::Status(status) => match status.command {
+ SyncStatusCommand::Get => "sync.status.get",
+ },
+ SyncCommand::Pull => "sync.pull",
+ SyncCommand::Push => "sync.push",
+ SyncCommand::Watch => "sync.watch",
+ },
+ Self::Farm(args) => match &args.command {
+ FarmCommand::Create(_) => "farm.create",
+ FarmCommand::Get => "farm.get",
+ FarmCommand::Rebind(_) => "farm.rebind",
+ FarmCommand::Profile(profile) => match profile.command {
+ FarmProfileCommand::Update(_) => "farm.profile.update",
+ },
+ FarmCommand::Location(location) => match location.command {
+ FarmLocationCommand::Update(_) => "farm.location.update",
+ },
+ FarmCommand::Fulfillment(fulfillment) => match fulfillment.command {
+ FarmFulfillmentCommand::Update(_) => "farm.fulfillment.update",
+ },
+ FarmCommand::Readiness(readiness) => match readiness.command {
+ FarmReadinessCommand::Check => "farm.readiness.check",
+ },
+ FarmCommand::Publish => "farm.publish",
+ },
+ Self::Listing(args) => match &args.command {
+ ListingCommand::Create(_) => "listing.create",
+ ListingCommand::Get(_) => "listing.get",
+ ListingCommand::List => "listing.list",
+ ListingCommand::App(app) => match &app.command {
+ ListingAppCommand::List => "listing.app.list",
+ ListingAppCommand::Export(_) => "listing.app.export",
+ },
+ ListingCommand::Update(_) => "listing.update",
+ ListingCommand::Validate(_) => "listing.validate",
+ ListingCommand::Rebind(_) => "listing.rebind",
+ ListingCommand::Publish(_) => "listing.publish",
+ ListingCommand::Archive(_) => "listing.archive",
+ },
+ Self::Market(args) => match &args.command {
+ MarketCommand::Refresh => "market.refresh",
+ MarketCommand::Product(product) => match &product.command {
+ MarketProductCommand::Search(_) => "market.product.search",
+ },
+ MarketCommand::Listing(listing) => match &listing.command {
+ MarketListingCommand::Get(_) => "market.listing.get",
+ },
+ },
+ Self::Basket(args) => match &args.command {
+ BasketCommand::Create(_) => "basket.create",
+ BasketCommand::Get(_) => "basket.get",
+ BasketCommand::List => "basket.list",
+ BasketCommand::Item(item) => match item.command {
+ BasketItemCommand::Add(_) => "basket.item.add",
+ BasketItemCommand::Update(_) => "basket.item.update",
+ BasketItemCommand::Remove(_) => "basket.item.remove",
+ },
+ BasketCommand::Adjustment(adjustment) => match &adjustment.command {
+ BasketAdjustmentCommand::Add(_) => "basket.adjustment.add",
+ BasketAdjustmentCommand::Remove(_) => "basket.adjustment.remove",
+ },
+ BasketCommand::Validate(_) => "basket.validate",
+ BasketCommand::Quote(quote) => match quote.command {
+ BasketQuoteCommand::Create(_) => "basket.quote.create",
+ },
+ },
+ Self::Order(args) => match &args.command {
+ OrderCommand::Submit(_) => "order.submit",
+ OrderCommand::Get(_) => "order.get",
+ OrderCommand::List => "order.list",
+ OrderCommand::App(app) => match &app.command {
+ OrderAppCommand::List => "order.app.list",
+ OrderAppCommand::Export(_) => "order.app.export",
+ },
+ OrderCommand::Rebind(_) => "order.rebind",
+ OrderCommand::Accept(_) => "order.accept",
+ OrderCommand::Decline(_) => "order.decline",
+ OrderCommand::Cancel(_) => "order.cancel",
+ OrderCommand::Revision(revision) => match &revision.command {
+ OrderRevisionCommand::Propose(_) => "order.revision.propose",
+ OrderRevisionCommand::Accept(_) => "order.revision.accept",
+ OrderRevisionCommand::Decline(_) => "order.revision.decline",
+ },
+ OrderCommand::Fulfillment(fulfillment) => match &fulfillment.command {
+ OrderFulfillmentCommand::Update(_) => "order.fulfillment.update",
+ },
+ OrderCommand::Receipt(receipt) => match &receipt.command {
+ OrderReceiptCommand::Record(_) => "order.receipt.record",
+ },
+ OrderCommand::Payment(payment) => match &payment.command {
+ OrderPaymentCommand::Record(_) => "order.payment.record",
+ },
+ OrderCommand::Settlement(settlement) => match &settlement.command {
+ OrderSettlementCommand::Accept(_) => "order.settlement.accept",
+ OrderSettlementCommand::Reject(_) => "order.settlement.reject",
+ },
+ OrderCommand::Status(status) => match &status.command {
+ OrderStatusCommand::Get(_) => "order.status.get",
+ },
+ OrderCommand::Event(event) => match &event.command {
+ OrderEventCommand::List(_) => "order.event.list",
+ OrderEventCommand::Watch(_) => "order.event.watch",
+ },
+ },
+ Self::Validation(args) => match &args.command {
+ ValidationCommand::Receipt(receipt) => match &receipt.command {
+ ValidationReceiptCommand::Get(_) => "validation.receipt.get",
+ ValidationReceiptCommand::List(_) => "validation.receipt.list",
+ ValidationReceiptCommand::Verify(_) => "validation.receipt.verify",
+ },
+ },
+ }
+ }
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct WorkspaceArgs {
+ #[command(subcommand)]
+ pub command: WorkspaceCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum WorkspaceCommand {
+ Init,
+ Get,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct HealthArgs {
+ #[command(subcommand)]
+ pub command: HealthCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum HealthCommand {
+ Status(HealthStatusArgs),
+ Check(HealthCheckArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct HealthStatusArgs {
+ #[command(subcommand)]
+ pub command: HealthStatusCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum HealthStatusCommand {
+ Get,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct HealthCheckArgs {
+ #[command(subcommand)]
+ pub command: HealthCheckCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum HealthCheckCommand {
+ Run,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ConfigArgs {
+ #[command(subcommand)]
+ pub command: ConfigCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum ConfigCommand {
+ Get,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct AccountArgs {
+ #[command(subcommand)]
+ pub command: AccountCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum AccountCommand {
+ Create,
+ Import(AccountImportArgs),
+ AttachSecret(AccountAttachSecretArgs),
+ Get(AccountGetArgs),
+ List,
+ Remove(AccountSelectorArgs),
+ Selection(AccountSelectionArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct AccountImportArgs {
+ pub path: Option<PathBuf>,
+ #[arg(long, action = clap::ArgAction::SetTrue)]
+ pub default: bool,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct AccountAttachSecretArgs {
+ pub selector: Option<String>,
+ pub path: Option<PathBuf>,
+ #[arg(long, action = clap::ArgAction::SetTrue)]
+ pub default: bool,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct AccountGetArgs {
+ pub selector: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct AccountSelectorArgs {
+ pub selector: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct AccountSelectionArgs {
+ #[command(subcommand)]
+ pub command: AccountSelectionCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum AccountSelectionCommand {
+ Get,
+ Update(AccountSelectorArgs),
+ Clear,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct SignerArgs {
+ #[command(subcommand)]
+ pub command: SignerCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum SignerCommand {
+ Status(SignerStatusArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct SignerStatusArgs {
+ #[command(subcommand)]
+ pub command: SignerStatusCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum SignerStatusCommand {
+ Get,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct RelayArgs {
+ #[command(subcommand)]
+ pub command: RelayCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum RelayCommand {
+ List,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct StoreArgs {
+ #[command(subcommand)]
+ pub command: StoreCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum StoreCommand {
+ Init,
+ Status(StoreStatusArgs),
+ Export,
+ Backup(StoreBackupArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct StoreStatusArgs {
+ #[command(subcommand)]
+ pub command: StoreStatusCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum StoreStatusCommand {
+ Get,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct StoreBackupArgs {
+ #[command(subcommand)]
+ pub command: StoreBackupCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum StoreBackupCommand {
+ Create,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct SyncArgs {
+ #[command(subcommand)]
+ pub command: SyncCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum SyncCommand {
+ Status(SyncStatusArgs),
+ Pull,
+ Push,
+ Watch,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct SyncStatusArgs {
+ #[command(subcommand)]
+ pub command: SyncStatusCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum SyncStatusCommand {
+ Get,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmArgs {
+ #[command(subcommand)]
+ pub command: FarmCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum FarmCommand {
+ Create(FarmCreateArgs),
+ Get,
+ Rebind(FarmRebindArgs),
+ Profile(FarmProfileArgs),
+ Location(FarmLocationArgs),
+ Fulfillment(FarmFulfillmentArgs),
+ Readiness(FarmReadinessArgs),
+ Publish,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmCreateArgs {
+ #[arg(long = "farm-d-tag")]
+ pub farm_d_tag: Option<String>,
+ #[arg(long)]
+ pub name: Option<String>,
+ #[arg(long = "display-name")]
+ pub display_name: Option<String>,
+ #[arg(long)]
+ pub about: Option<String>,
+ #[arg(long)]
+ pub website: Option<String>,
+ #[arg(long)]
+ pub picture: Option<String>,
+ #[arg(long)]
+ pub banner: Option<String>,
+ #[arg(long)]
+ pub location: Option<String>,
+ #[arg(long)]
+ pub city: Option<String>,
+ #[arg(long)]
+ pub region: Option<String>,
+ #[arg(long)]
+ pub country: Option<String>,
+ #[arg(long = "delivery-method")]
+ pub delivery_method: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmRebindArgs {
+ pub selector: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmProfileArgs {
+ #[command(subcommand)]
+ pub command: FarmProfileCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum FarmProfileCommand {
+ Update(FarmProfileUpdateArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmProfileUpdateArgs {
+ #[arg(long)]
+ pub field: Option<String>,
+ #[arg(long)]
+ pub value: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmLocationArgs {
+ #[command(subcommand)]
+ pub command: FarmLocationCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum FarmLocationCommand {
+ Update(FarmLocationUpdateArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmLocationUpdateArgs {
+ #[arg(long)]
+ pub field: Option<String>,
+ #[arg(long)]
+ pub value: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmFulfillmentArgs {
+ #[command(subcommand)]
+ pub command: FarmFulfillmentCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum FarmFulfillmentCommand {
+ Update(FarmFulfillmentUpdateArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmFulfillmentUpdateArgs {
+ #[arg(long)]
+ pub value: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FarmReadinessArgs {
+ #[command(subcommand)]
+ pub command: FarmReadinessCommand,
+}
+
+#[derive(Debug, Clone, Copy, Subcommand)]
+pub enum FarmReadinessCommand {
+ Check,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ListingArgs {
+ #[command(subcommand)]
+ pub command: ListingCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum ListingCommand {
+ Create(ListingCreateArgs),
+ Get(LookupArgs),
+ List,
+ App(ListingAppArgs),
+ Update(FileArgs),
+ Validate(FileArgs),
+ Rebind(ListingRebindArgs),
+ Publish(FileArgs),
+ Archive(FileArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ListingCreateArgs {
+ #[arg(long)]
+ pub output: Option<PathBuf>,
+ #[arg(long)]
+ pub key: Option<String>,
+ #[arg(long)]
+ pub title: Option<String>,
+ #[arg(long)]
+ pub category: Option<String>,
+ #[arg(long)]
+ pub summary: Option<String>,
+ #[arg(long = "bin-id")]
+ pub bin_id: Option<String>,
+ #[arg(long = "quantity-amount")]
+ pub quantity_amount: Option<String>,
+ #[arg(long = "quantity-unit")]
+ pub quantity_unit: Option<String>,
+ #[arg(long = "price-amount")]
+ pub price_amount: Option<String>,
+ #[arg(long = "price-currency")]
+ pub price_currency: Option<String>,
+ #[arg(long = "price-per-amount")]
+ pub price_per_amount: Option<String>,
+ #[arg(long = "price-per-unit")]
+ pub price_per_unit: Option<String>,
+ #[arg(long)]
+ pub available: Option<String>,
+ #[arg(long)]
+ pub label: Option<String>,
+ #[arg(long = "discount-id")]
+ pub discount_id: Option<String>,
+ #[arg(long = "discount-label")]
+ pub discount_label: Option<String>,
+ #[arg(long = "discount-kind")]
+ pub discount_kind: Option<String>,
+ #[arg(long = "discount-value")]
+ pub discount_value: Option<String>,
+ #[arg(long = "discount-amount")]
+ pub discount_amount: Option<String>,
+ #[arg(long = "discount-currency")]
+ pub discount_currency: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct FileArgs {
+ pub file: Option<PathBuf>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ListingAppArgs {
+ #[command(subcommand)]
+ pub command: ListingAppCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum ListingAppCommand {
+ List,
+ Export(ListingAppExportArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ListingAppExportArgs {
+ pub record_id: Option<String>,
+ #[arg(long)]
+ pub output: Option<PathBuf>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ListingRebindArgs {
+ pub file: Option<PathBuf>,
+ pub selector: Option<String>,
+ #[arg(long = "farm-d-tag")]
+ pub farm_d_tag: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct LookupArgs {
+ pub key: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct MarketArgs {
+ #[command(subcommand)]
+ pub command: MarketCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum MarketCommand {
+ Refresh,
+ Product(MarketProductArgs),
+ Listing(MarketListingArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct MarketProductArgs {
+ #[command(subcommand)]
+ pub command: MarketProductCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum MarketProductCommand {
+ Search(QueryArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct MarketListingArgs {
+ #[command(subcommand)]
+ pub command: MarketListingCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum MarketListingCommand {
+ Get(LookupArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct QueryArgs {
+ pub query: Vec<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketArgs {
+ #[command(subcommand)]
+ pub command: BasketCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum BasketCommand {
+ Create(BasketCreateArgs),
+ Get(BasketKeyArgs),
+ List,
+ Item(BasketItemArgs),
+ Adjustment(BasketAdjustmentArgs),
+ Validate(BasketKeyArgs),
+ Quote(BasketQuoteArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketCreateArgs {
+ pub basket_id: Option<String>,
+ #[arg(long)]
+ pub listing: Option<String>,
+ #[arg(long = "listing-addr")]
+ pub listing_addr: Option<String>,
+ #[arg(long = "bin-id")]
+ pub bin_id: Option<String>,
+ #[arg(long)]
+ pub quantity: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketKeyArgs {
+ pub basket_id: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketItemArgs {
+ #[command(subcommand)]
+ pub command: BasketItemCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum BasketItemCommand {
+ Add(BasketItemMutationArgs),
+ Update(BasketItemMutationArgs),
+ Remove(BasketItemRemoveArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketAdjustmentArgs {
+ #[command(subcommand)]
+ pub command: BasketAdjustmentCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum BasketAdjustmentCommand {
+ Add(BasketAdjustmentAddArgs),
+ Remove(BasketAdjustmentRemoveArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketAdjustmentAddArgs {
+ pub basket_id: Option<String>,
+ #[arg(long)]
+ pub id: Option<String>,
+ #[arg(long)]
+ pub effect: Option<String>,
+ #[arg(long)]
+ pub amount: Option<String>,
+ #[arg(long)]
+ pub currency: Option<String>,
+ #[arg(long)]
+ pub reason: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketAdjustmentRemoveArgs {
+ pub basket_id: Option<String>,
+ #[arg(long)]
+ pub id: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketItemMutationArgs {
+ pub basket_id: Option<String>,
+ #[arg(long = "item-id")]
+ pub item_id: Option<String>,
+ #[arg(long)]
+ pub listing: Option<String>,
+ #[arg(long = "listing-addr")]
+ pub listing_addr: Option<String>,
+ #[arg(long = "bin-id")]
+ pub bin_id: Option<String>,
+ #[arg(long)]
+ pub quantity: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketItemRemoveArgs {
+ pub basket_id: Option<String>,
+ pub item_id: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct BasketQuoteArgs {
+ #[command(subcommand)]
+ pub command: BasketQuoteCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum BasketQuoteCommand {
+ Create(BasketKeyArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderArgs {
+ #[command(subcommand)]
+ pub command: OrderCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderCommand {
+ Submit(OrderSubmitArgs),
+ Get(OrderKeyArgs),
+ List,
+ App(OrderAppArgs),
+ Rebind(OrderRebindArgs),
+ Accept(OrderKeyArgs),
+ Decline(OrderDeclineArgs),
+ Cancel(OrderCancelArgs),
+ Revision(OrderRevisionArgs),
+ Fulfillment(OrderFulfillmentArgs),
+ Receipt(OrderReceiptArgs),
+ Payment(OrderPaymentArgs),
+ Settlement(OrderSettlementArgs),
+ Status(OrderStatusArgs),
+ Event(OrderEventArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderSubmitArgs {
+ pub order_id: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderKeyArgs {
+ pub order_id: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderAppArgs {
+ #[command(subcommand)]
+ pub command: OrderAppCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderAppCommand {
+ List,
+ Export(OrderAppExportArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderAppExportArgs {
+ pub record_id: Option<String>,
+ #[arg(long)]
+ pub output: Option<PathBuf>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderRebindArgs {
+ pub order_id: Option<String>,
+ pub selector: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderDeclineArgs {
+ pub order_id: Option<String>,
+ #[arg(long)]
+ pub reason: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderCancelArgs {
+ pub order_id: Option<String>,
+ #[arg(long)]
+ pub reason: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderRevisionArgs {
+ #[command(subcommand)]
+ pub command: OrderRevisionCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderRevisionCommand {
+ Propose(OrderRevisionProposeArgs),
+ Accept(OrderRevisionDecisionArgs),
+ Decline(OrderRevisionDeclineArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderRevisionProposeArgs {
+ pub order_id: Option<String>,
+ #[arg(long)]
+ pub reason: Option<String>,
+ #[arg(long)]
+ pub bin_id: Option<String>,
+ #[arg(long)]
+ pub bin_count: Option<u32>,
+ #[arg(long)]
+ pub adjustment_id: Option<String>,
+ #[arg(long)]
+ pub adjustment_effect: Option<String>,
+ #[arg(long)]
+ pub adjustment_amount: Option<String>,
+ #[arg(long)]
+ pub adjustment_currency: Option<String>,
+ #[arg(long)]
+ pub adjustment_reason: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderRevisionDecisionArgs {
+ pub order_id: Option<String>,
+ #[arg(long)]
+ pub revision_id: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderRevisionDeclineArgs {
+ pub order_id: Option<String>,
+ #[arg(long)]
+ pub revision_id: Option<String>,
+ #[arg(long)]
+ pub reason: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderFulfillmentArgs {
+ #[command(subcommand)]
+ pub command: OrderFulfillmentCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderFulfillmentCommand {
+ Update(OrderFulfillmentUpdateArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderFulfillmentUpdateArgs {
+ pub order_id: Option<String>,
+ #[arg(long, value_enum)]
+ pub state: Option<OrderFulfillmentStateArg>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
+#[value(rename_all = "snake_case")]
+pub enum OrderFulfillmentStateArg {
+ Preparing,
+ ReadyForPickup,
+ OutForDelivery,
+ Delivered,
+ SellerCancelled,
+}
+
+impl OrderFulfillmentStateArg {
+ pub const fn as_protocol_state(self) -> &'static str {
+ match self {
+ Self::Preparing => "preparing",
+ Self::ReadyForPickup => "ready_for_pickup",
+ Self::OutForDelivery => "out_for_delivery",
+ Self::Delivered => "delivered",
+ Self::SellerCancelled => "seller_cancelled",
+ }
+ }
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderReceiptArgs {
+ #[command(subcommand)]
+ pub command: OrderReceiptCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderReceiptCommand {
+ Record(OrderReceiptRecordArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderReceiptRecordArgs {
+ pub order_id: Option<String>,
+ #[arg(long, action = ArgAction::SetTrue, conflicts_with = "issue")]
+ pub received: bool,
+ #[arg(long)]
+ pub issue: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderPaymentArgs {
+ #[command(subcommand)]
+ pub command: OrderPaymentCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderPaymentCommand {
+ Record(OrderPaymentRecordArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderPaymentRecordArgs {
+ pub order_id: Option<String>,
+ #[arg(long)]
+ pub amount: Option<String>,
+ #[arg(long)]
+ pub currency: Option<String>,
+ #[arg(long)]
+ pub method: Option<String>,
+ #[arg(long)]
+ pub reference: Option<String>,
+ #[arg(long)]
+ pub paid_at: Option<u64>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderSettlementArgs {
+ #[command(subcommand)]
+ pub command: OrderSettlementCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderSettlementCommand {
+ Accept(OrderSettlementAcceptArgs),
+ Reject(OrderSettlementRejectArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderSettlementAcceptArgs {
+ pub order_id: Option<String>,
+ #[arg(long)]
+ pub payment_event_id: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderSettlementRejectArgs {
+ pub order_id: Option<String>,
+ #[arg(long)]
+ pub payment_event_id: Option<String>,
+ #[arg(long)]
+ pub reason: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderStatusArgs {
+ #[command(subcommand)]
+ pub command: OrderStatusCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderStatusCommand {
+ Get(OrderKeyArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct OrderEventArgs {
+ #[command(subcommand)]
+ pub command: OrderEventCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum OrderEventCommand {
+ List(OrderKeyArgs),
+ Watch(OrderKeyArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ValidationArgs {
+ #[command(subcommand)]
+ pub command: ValidationCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum ValidationCommand {
+ Receipt(ValidationReceiptArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ValidationReceiptArgs {
+ #[command(subcommand)]
+ pub command: ValidationReceiptCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum ValidationReceiptCommand {
+ Get(ValidationReceiptEventArgs),
+ List(ValidationReceiptListArgs),
+ Verify(ValidationReceiptEventArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ValidationReceiptEventArgs {
+ pub receipt_event_id: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct ValidationReceiptListArgs {
+ #[arg(long)]
+ pub order_id: Option<String>,
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct PathOutputArgs {
+ #[arg(long)]
+ pub output: Option<PathBuf>,
+}
+
+#[cfg(test)]
+mod tests {
+ use std::collections::BTreeSet;
+
+ use clap::{CommandFactory, Parser};
+
+ use super::{
+ AccountCommand, FarmCommand, ListingCommand, OrderCommand, OrderFulfillmentCommand,
+ OrderFulfillmentStateArg, OrderPaymentCommand, OrderReceiptCommand, OrderRevisionCommand,
+ OrderSettlementCommand, TargetCliArgs, TargetOutputFormat, ValidationCommand,
+ ValidationReceiptCommand,
+ };
+ use crate::registry::OPERATION_REGISTRY;
+
+ #[test]
+ fn target_parser_accepts_every_target_registry_path() {
+ for operation in OPERATION_REGISTRY {
+ let parsed = TargetCliArgs::try_parse_from(operation.cli_path.split_whitespace())
+ .unwrap_or_else(|error| {
+ panic!("{} failed to parse: {error}", operation.cli_path);
+ });
+ assert_eq!(parsed.command.operation_id(), operation.operation_id);
+ }
+ }
+
+ #[test]
+ fn target_parser_exposes_only_target_top_level_namespaces() {
+ let actual = TargetCliArgs::command()
+ .get_subcommands()
+ .map(|command| command.get_name().to_owned())
+ .collect::<BTreeSet<_>>();
+ let expected = [
+ "workspace",
+ "health",
+ "config",
+ "account",
+ "signer",
+ "relay",
+ "store",
+ "sync",
+ "farm",
+ "listing",
+ "market",
+ "basket",
+ "order",
+ "validation",
+ ]
+ .into_iter()
+ .map(str::to_owned)
+ .collect::<BTreeSet<_>>();
+
+ assert_eq!(actual, expected);
+ }
+
+ #[test]
+ fn target_global_flags_parse() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "--format",
+ "ndjson",
+ "--account-id",
+ "acct_test",
+ "--relay",
+ "wss://relay.one",
+ "--relay",
+ "wss://relay.two",
+ "--offline",
+ "--dry-run",
+ "--idempotency-key",
+ "idem_test",
+ "--correlation-id",
+ "corr_test",
+ "--approval-token",
+ "approval_test",
+ "--no-input",
+ "--quiet",
+ "--no-color",
+ "workspace",
+ "get",
+ ])
+ .expect("target args parse");
+
+ assert_eq!(parsed.format, TargetOutputFormat::Ndjson);
+ assert_eq!(parsed.account_id.as_deref(), Some("acct_test"));
+ assert_eq!(
+ parsed.relay,
+ vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()]
+ );
+ assert!(parsed.offline);
+ assert!(parsed.dry_run);
+ assert_eq!(parsed.idempotency_key.as_deref(), Some("idem_test"));
+ assert_eq!(parsed.correlation_id.as_deref(), Some("corr_test"));
+ assert_eq!(parsed.approval_token.as_deref(), Some("approval_test"));
+ assert!(parsed.no_input);
+ assert!(parsed.quiet);
+ assert!(parsed.no_color);
+ assert_eq!(parsed.command.operation_id(), "workspace.get");
+ }
+
+ #[test]
+ fn target_parser_accepts_account_attach_secret_inputs() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "account",
+ "attach-secret",
+ "acct_test",
+ "identity.json",
+ "--default",
+ ])
+ .expect("target args parse");
+
+ assert_eq!(parsed.command.operation_id(), "account.attach_secret");
+ let crate::cli::TargetCommand::Account(account) = parsed.command else {
+ panic!("expected account command")
+ };
+ let AccountCommand::AttachSecret(args) = account.command else {
+ panic!("expected account attach-secret command")
+ };
+ assert_eq!(args.selector.as_deref(), Some("acct_test"));
+ assert_eq!(
+ args.path.as_ref().map(|path| path.as_os_str()),
+ Some(std::ffi::OsStr::new("identity.json"))
+ );
+ assert!(args.default);
+ }
+
+ #[test]
+ fn target_parser_accepts_farm_rebind_selector() {
+ let parsed = TargetCliArgs::try_parse_from(["radroots", "farm", "rebind", "acct_test"])
+ .expect("target args parse");
+
+ assert_eq!(parsed.command.operation_id(), "farm.rebind");
+ let crate::cli::TargetCommand::Farm(farm) = parsed.command else {
+ panic!("expected farm command")
+ };
+ let FarmCommand::Rebind(args) = farm.command else {
+ panic!("expected farm rebind command")
+ };
+ assert_eq!(args.selector.as_deref(), Some("acct_test"));
+ }
+
+ #[test]
+ fn target_parser_accepts_listing_rebind_inputs() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "listing",
+ "rebind",
+ "listing.toml",
+ "acct_test",
+ "--farm-d-tag",
+ "AAAAAAAAAAAAAAAAAAAAAw",
+ ])
+ .expect("target args parse");
+
+ assert_eq!(parsed.command.operation_id(), "listing.rebind");
+ let crate::cli::TargetCommand::Listing(listing) = parsed.command else {
+ panic!("expected listing command")
+ };
+ let ListingCommand::Rebind(args) = listing.command else {
+ panic!("expected listing rebind command")
+ };
+ assert_eq!(
+ args.file.as_ref().map(|path| path.as_os_str()),
+ Some(std::ffi::OsStr::new("listing.toml"))
+ );
+ assert_eq!(args.selector.as_deref(), Some("acct_test"));
+ assert_eq!(args.farm_d_tag.as_deref(), Some("AAAAAAAAAAAAAAAAAAAAAw"));
+ }
+
+ #[test]
+ fn target_parser_accepts_order_rebind_inputs() {
+ let parsed =
+ TargetCliArgs::try_parse_from(["radroots", "order", "rebind", "ord_test", "acct_test"])
+ .expect("target args parse");
+
+ assert_eq!(parsed.command.operation_id(), "order.rebind");
+ let crate::cli::TargetCommand::Order(order) = parsed.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Rebind(args) = order.command else {
+ panic!("expected order rebind command")
+ };
+ assert_eq!(args.order_id.as_deref(), Some("ord_test"));
+ assert_eq!(args.selector.as_deref(), Some("acct_test"));
+ }
+
+ #[test]
+ fn target_parser_accepts_order_fulfillment_update_state() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "fulfillment",
+ "update",
+ "ord_test",
+ "--state",
+ "ready_for_pickup",
+ ])
+ .expect("target args parse");
+
+ assert_eq!(parsed.command.operation_id(), "order.fulfillment.update");
+ let crate::cli::TargetCommand::Order(order) = parsed.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Fulfillment(fulfillment) = order.command else {
+ panic!("expected order fulfillment command")
+ };
+ let OrderFulfillmentCommand::Update(args) = fulfillment.command;
+ assert_eq!(args.order_id.as_deref(), Some("ord_test"));
+ assert_eq!(args.state, Some(OrderFulfillmentStateArg::ReadyForPickup));
+ }
+
+ #[test]
+ fn target_parser_accepts_order_cancel_reason() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "cancel",
+ "ord_test",
+ "--reason",
+ "changed plans",
+ ])
+ .expect("target args parse");
+
+ assert_eq!(parsed.command.operation_id(), "order.cancel");
+ let crate::cli::TargetCommand::Order(order) = parsed.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Cancel(args) = order.command else {
+ panic!("expected order cancel command")
+ };
+ assert_eq!(args.order_id.as_deref(), Some("ord_test"));
+ assert_eq!(args.reason.as_deref(), Some("changed plans"));
+ }
+
+ #[test]
+ fn target_parser_accepts_order_revision_propose_inputs() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "revision",
+ "propose",
+ "ord_test",
+ "--reason",
+ "update count",
+ "--bin-id",
+ "bin-1",
+ "--bin-count",
+ "3",
+ "--adjustment-id",
+ "adj_revision",
+ "--adjustment-effect",
+ "increase",
+ "--adjustment-amount",
+ "2",
+ "--adjustment-currency",
+ "USD",
+ "--adjustment-reason",
+ "packing change",
+ ])
+ .expect("target args parse");
+
+ assert_eq!(parsed.command.operation_id(), "order.revision.propose");
+ let crate::cli::TargetCommand::Order(order) = parsed.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Revision(revision) = order.command else {
+ panic!("expected order revision command")
+ };
+ let OrderRevisionCommand::Propose(args) = revision.command else {
+ panic!("expected order revision propose command")
+ };
+ assert_eq!(args.order_id.as_deref(), Some("ord_test"));
+ assert_eq!(args.reason.as_deref(), Some("update count"));
+ assert_eq!(args.bin_id.as_deref(), Some("bin-1"));
+ assert_eq!(args.bin_count, Some(3));
+ assert_eq!(args.adjustment_id.as_deref(), Some("adj_revision"));
+ assert_eq!(args.adjustment_effect.as_deref(), Some("increase"));
+ }
+
+ #[test]
+ fn target_parser_accepts_order_revision_decision_inputs() {
+ let accepted = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "revision",
+ "accept",
+ "ord_test",
+ "--revision-id",
+ "rev_test",
+ ])
+ .expect("target args parse");
+
+ assert_eq!(accepted.command.operation_id(), "order.revision.accept");
+ let crate::cli::TargetCommand::Order(order) = accepted.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Revision(revision) = order.command else {
+ panic!("expected order revision command")
+ };
+ let OrderRevisionCommand::Accept(args) = revision.command else {
+ panic!("expected order revision accept command")
+ };
+ assert_eq!(args.order_id.as_deref(), Some("ord_test"));
+ assert_eq!(args.revision_id.as_deref(), Some("rev_test"));
+
+ let declined = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "revision",
+ "decline",
+ "ord_test",
+ "--revision-id",
+ "rev_test",
+ "--reason",
+ "keep original order",
+ ])
+ .expect("target args parse");
+
+ assert_eq!(declined.command.operation_id(), "order.revision.decline");
+ let crate::cli::TargetCommand::Order(order) = declined.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Revision(revision) = order.command else {
+ panic!("expected order revision command")
+ };
+ let OrderRevisionCommand::Decline(args) = revision.command else {
+ panic!("expected order revision decline command")
+ };
+ assert_eq!(args.order_id.as_deref(), Some("ord_test"));
+ assert_eq!(args.revision_id.as_deref(), Some("rev_test"));
+ assert_eq!(args.reason.as_deref(), Some("keep original order"));
+ }
+
+ #[test]
+ fn target_parser_accepts_order_receipt_record_outcomes() {
+ let received = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "receipt",
+ "record",
+ "ord_test",
+ "--received",
+ ])
+ .expect("target args parse");
+ assert_eq!(received.command.operation_id(), "order.receipt.record");
+ let crate::cli::TargetCommand::Order(order) = received.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Receipt(receipt) = order.command else {
+ panic!("expected order receipt command")
+ };
+ let OrderReceiptCommand::Record(args) = receipt.command;
+ assert_eq!(args.order_id.as_deref(), Some("ord_test"));
+ assert!(args.received);
+ assert_eq!(args.issue, None);
+
+ let issue = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "receipt",
+ "record",
+ "ord_test",
+ "--issue",
+ "damaged items",
+ ])
+ .expect("target args parse");
+ assert_eq!(issue.command.operation_id(), "order.receipt.record");
+ }
+
+ #[test]
+ fn target_parser_accepts_validation_receipt_commands() {
+ let get = TargetCliArgs::try_parse_from([
+ "radroots",
+ "validation",
+ "receipt",
+ "get",
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ ])
+ .expect("target args parse");
+ assert_eq!(get.command.operation_id(), "validation.receipt.get");
+ let crate::cli::TargetCommand::Validation(validation) = get.command else {
+ panic!("expected validation command")
+ };
+ let ValidationCommand::Receipt(receipt) = validation.command;
+ let ValidationReceiptCommand::Get(args) = receipt.command else {
+ panic!("expected validation receipt get command")
+ };
+ assert_eq!(
+ args.receipt_event_id.as_deref(),
+ Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ );
+
+ let list = TargetCliArgs::try_parse_from([
+ "radroots",
+ "validation",
+ "receipt",
+ "list",
+ "--order-id",
+ "ord_1",
+ ])
+ .expect("target args parse");
+ assert_eq!(list.command.operation_id(), "validation.receipt.list");
+ let crate::cli::TargetCommand::Validation(validation) = list.command else {
+ panic!("expected validation command")
+ };
+ let ValidationCommand::Receipt(receipt) = validation.command;
+ let ValidationReceiptCommand::List(args) = receipt.command else {
+ panic!("expected validation receipt list command")
+ };
+ assert_eq!(args.order_id.as_deref(), Some("ord_1"));
+
+ let verify = TargetCliArgs::try_parse_from([
+ "radroots",
+ "validation",
+ "receipt",
+ "verify",
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ ])
+ .expect("target args parse");
+ assert_eq!(verify.command.operation_id(), "validation.receipt.verify");
+ }
+
+ #[test]
+ fn target_parser_accepts_order_payment_record_methods() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "payment",
+ "record",
+ "ord_test",
+ "--amount",
+ "12",
+ "--currency",
+ "USD",
+ "--method",
+ "manual_transfer",
+ "--reference",
+ "memo-1",
+ "--paid-at",
+ "1777666000",
+ ])
+ .expect("target args parse");
+ assert_eq!(parsed.command.operation_id(), "order.payment.record");
+ let crate::cli::TargetCommand::Order(order) = parsed.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Payment(payment) = order.command else {
+ panic!("expected order payment command")
+ };
+ let OrderPaymentCommand::Record(args) = payment.command;
+ assert_eq!(args.order_id.as_deref(), Some("ord_test"));
+ assert_eq!(args.amount.as_deref(), Some("12"));
+ assert_eq!(args.currency.as_deref(), Some("USD"));
+ assert_eq!(args.method.as_deref(), Some("manual_transfer"));
+ assert_eq!(args.reference.as_deref(), Some("memo-1"));
+ assert_eq!(args.paid_at, Some(1_777_666_000));
+
+ let future_method = TargetCliArgs::try_parse_from([
+ "radroots", "order", "payment", "record", "ord_test", "--method", "card",
+ ])
+ .expect("target args parse");
+ let crate::cli::TargetCommand::Order(order) = future_method.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Payment(payment) = order.command else {
+ panic!("expected order payment command")
+ };
+ let OrderPaymentCommand::Record(args) = payment.command;
+ assert_eq!(args.method.as_deref(), Some("card"));
+ }
+
+ #[test]
+ fn target_parser_accepts_order_settlement_decisions() {
+ let accept = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "settlement",
+ "accept",
+ "ord_test",
+ "--payment-event-id",
+ "pay_event",
+ ])
+ .expect("target args parse");
+ assert_eq!(accept.command.operation_id(), "order.settlement.accept");
+ let crate::cli::TargetCommand::Order(order) = accept.command else {
+ panic!("expected order command")
+ };
+ let OrderCommand::Settlement(settlement) = order.command else {
+ panic!("expected order settlement command")
+ };
+ let OrderSettlementCommand::Accept(args) = settlement.command else {
+ panic!("expected settlement accept command")
+ };
+ assert_eq!(args.order_id.as_deref(), Some("ord_test"));
+ assert_eq!(args.payment_event_id.as_deref(), Some("pay_event"));
+
+ let reject = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "settlement",
+ "reject",
+ "ord_test",
+ "--payment-event-id",
+ "pay_event",
+ "--reason",
+ "reference mismatch",
+ ])
+ .expect("target args parse");
+ assert_eq!(reject.command.operation_id(), "order.settlement.reject");
+ }
+
+ #[test]
+ fn target_parser_rejects_removed_global_flags() {
+ let rejected = [
+ vec!["radroots", "--output", "json", "config", "get"],
+ vec!["radroots", "--json", "config", "get"],
+ vec!["radroots", "--ndjson", "config", "get"],
+ vec!["radroots", "--yes", "config", "get"],
+ vec!["radroots", "--non-interactive", "config", "get"],
+ vec!["radroots", "--signer", "myc", "config", "get"],
+ vec!["radroots", "--farm-id", "farm_test", "config", "get"],
+ vec!["radroots", "--profile", "repo_local", "config", "get"],
+ vec![
+ "radroots",
+ "--signer-session-id",
+ "sess_test",
+ "config",
+ "get",
+ ],
+ ];
+
+ for args in rejected {
+ assert!(TargetCliArgs::try_parse_from(args).is_err());
+ }
+ }
+
+ #[test]
+ fn target_parser_rejects_removed_top_level_commands() {
+ for command in [
+ "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc",
+ ] {
+ assert!(TargetCliArgs::try_parse_from(["radroots", command]).is_err());
+ }
+ }
+
+ #[test]
+ fn target_parser_rejects_deferred_namespaces() {
+ for command in ["product", "message", "approval", "agent"] {
+ assert!(TargetCliArgs::try_parse_from(["radroots", command]).is_err());
+ }
+ }
+
+ #[test]
+ fn target_parser_rejects_online_offline_conflict() {
+ assert!(
+ TargetCliArgs::try_parse_from([
+ "radroots",
+ "--online",
+ "--offline",
+ "health",
+ "status",
+ "get"
+ ])
+ .is_err()
+ );
+ }
+}
diff --git a/src/main.rs b/src/main.rs
@@ -1,21 +1,20 @@
#![forbid(unsafe_code)]
+mod cli;
mod deferred_payment;
-mod domain;
-mod operation_adapter;
mod operation_basket;
mod operation_core;
mod operation_farm;
mod operation_listing;
mod operation_market;
mod operation_order;
-mod operation_registry;
mod operation_runtime;
mod operation_validation;
-mod output_contract;
+mod ops;
+mod out;
+mod registry;
mod runtime;
-mod runtime_args;
-mod target_cli;
+mod view;
use std::io::Write;
use std::process::ExitCode;
@@ -25,31 +24,31 @@ use std::time::{SystemTime, UNIX_EPOCH};
use clap::Parser;
use serde_json::{Value, json};
+use crate::cli::global::{RuntimeInvocationArgs, RuntimeOutputFormatArg};
+use crate::cli::{TargetCliArgs, TargetOutputFormat};
use crate::deferred_payment::{deferred_payment_message, is_deferred_payment_operation};
-use crate::operation_adapter::{
- OperationAdapter, OperationAdapterError, OperationNetworkMode, OperationOutputFormat,
- OperationRequest, OperationRequestPayload, OperationResultPayload, OperationService,
- TargetOperationRequest,
-};
use crate::operation_basket::BasketOperationService;
use crate::operation_core::CoreOperationService;
use crate::operation_farm::FarmOperationService;
use crate::operation_listing::ListingOperationService;
use crate::operation_market::MarketOperationService;
use crate::operation_order::OrderOperationService;
-use crate::operation_registry::{
+use crate::operation_runtime::RuntimeOperationService;
+use crate::operation_validation::ValidationOperationService;
+use crate::ops::{
+ OperationAdapter, OperationAdapterError, OperationNetworkMode, OperationOutputFormat,
+ OperationRequest, OperationRequestPayload, OperationResultPayload, OperationService,
+ TargetOperationRequest,
+};
+use crate::out::envelope::OutputEnvelope;
+use crate::registry::{
NetworkRequirement, network_requirement, requires_local_signer_mode,
requires_nostr_relay_publish_mode,
};
-use crate::operation_runtime::RuntimeOperationService;
-use crate::operation_validation::ValidationOperationService;
-use crate::output_contract::OutputEnvelope;
use crate::runtime::config::{
PublishMode, RADROOTSD_PUBLISH_DEFERRED_REASON, RuntimeConfig, SignerBackend,
};
use crate::runtime::logging::initialize_logging;
-use crate::runtime_args::{RuntimeInvocationArgs, RuntimeOutputFormatArg};
-use crate::target_cli::{TargetCliArgs, TargetOutputFormat};
static REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
@@ -64,8 +63,8 @@ fn main() -> ExitCode {
}
fn run() -> Result<ExitCode, runtime::RuntimeError> {
- debug_assert!(operation_registry::registry_linkage_is_valid());
- debug_assert!(operation_adapter::adapter_registry_linkage_is_valid());
+ debug_assert!(registry::registry_linkage_is_valid());
+ debug_assert!(ops::adapter_registry_linkage_is_valid());
let args = TargetCliArgs::parse();
let request =
TargetOperationRequest::from_target_args(&args).map_err(operation_config_error)?;
diff --git a/src/operation_adapter.rs b/src/operation_adapter.rs
@@ -1,2466 +0,0 @@
-#![allow(dead_code)]
-
-use std::fmt::Debug;
-use std::io::ErrorKind;
-
-use serde::Serialize;
-use serde_json::{Map, Value, json};
-
-use crate::domain::runtime::CommandDisposition;
-use crate::operation_registry::{OPERATION_REGISTRY, OperationSpec, get_operation};
-use crate::output_contract::{
- CliExitCode, EnvelopeActor, EnvelopeContext, NextAction, OutputEnvelope, OutputError,
- OutputFormat, OutputWarning, next_actions_from_result_value,
-};
-use crate::runtime::RuntimeError;
-use crate::runtime::accounts::AccountRuntimeFailure;
-use crate::target_cli::{TargetCliArgs, TargetOutputFormat};
-
-#[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(
- args.command.operation_id(),
- OperationContext::from_target_args(args),
- 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
- }
- }
-}
-
-fn target_operation_input(command: &crate::target_cli::TargetCommand) -> OperationData {
- use crate::target_cli::{
- AccountCommand, AccountSelectionCommand, BasketAdjustmentCommand, BasketCommand,
- BasketItemCommand, BasketQuoteCommand, FarmCommand, FarmFulfillmentCommand,
- FarmLocationCommand, FarmProfileCommand, ListingAppCommand, ListingCommand, MarketCommand,
- MarketListingCommand, MarketProductCommand, OrderAppCommand, OrderCommand,
- OrderEventCommand, OrderFulfillmentCommand, OrderPaymentCommand, OrderReceiptCommand,
- OrderRevisionCommand, OrderSettlementCommand, OrderStatusCommand, TargetCommand,
- ValidationCommand, ValidationReceiptCommand,
- };
-
- let mut input = OperationData::new();
- match command {
- TargetCommand::Account(args) => match &args.command {
- AccountCommand::Import(args) => {
- insert_path(&mut input, "path", &args.path);
- if args.default {
- input.insert("default".to_owned(), Value::Bool(true));
- }
- }
- AccountCommand::AttachSecret(args) => {
- insert_string(&mut input, "selector", &args.selector);
- insert_path(&mut input, "path", &args.path);
- if args.default {
- input.insert("default".to_owned(), Value::Bool(true));
- }
- }
- AccountCommand::Get(args) => insert_string(&mut input, "selector", &args.selector),
- AccountCommand::Remove(args) => insert_string(&mut input, "selector", &args.selector),
- AccountCommand::Selection(args) => match &args.command {
- AccountSelectionCommand::Update(args) => {
- insert_string(&mut input, "selector", &args.selector)
- }
- AccountSelectionCommand::Get | AccountSelectionCommand::Clear => {}
- },
- AccountCommand::Create | AccountCommand::List => {}
- },
- TargetCommand::Farm(args) => match &args.command {
- FarmCommand::Create(args) => {
- insert_string(&mut input, "farm_d_tag", &args.farm_d_tag);
- insert_string(&mut input, "name", &args.name);
- insert_string(&mut input, "display_name", &args.display_name);
- insert_string(&mut input, "about", &args.about);
- insert_string(&mut input, "website", &args.website);
- insert_string(&mut input, "picture", &args.picture);
- insert_string(&mut input, "banner", &args.banner);
- insert_string(&mut input, "location", &args.location);
- insert_string(&mut input, "city", &args.city);
- insert_string(&mut input, "region", &args.region);
- insert_string(&mut input, "country", &args.country);
- insert_string(&mut input, "delivery_method", &args.delivery_method);
- }
- FarmCommand::Rebind(args) => {
- insert_string(&mut input, "selector", &args.selector);
- }
- FarmCommand::Profile(args) => match &args.command {
- FarmProfileCommand::Update(args) => {
- insert_string(&mut input, "field", &args.field);
- insert_string(&mut input, "value", &args.value);
- }
- },
- FarmCommand::Location(args) => match &args.command {
- FarmLocationCommand::Update(args) => {
- insert_string(&mut input, "field", &args.field);
- insert_string(&mut input, "value", &args.value);
- }
- },
- FarmCommand::Fulfillment(args) => match &args.command {
- FarmFulfillmentCommand::Update(args) => {
- insert_string(&mut input, "value", &args.value);
- }
- },
- FarmCommand::Get | FarmCommand::Readiness(_) | FarmCommand::Publish => {}
- },
- TargetCommand::Listing(args) => match &args.command {
- ListingCommand::Create(args) => {
- insert_path(&mut input, "output", &args.output);
- insert_string(&mut input, "key", &args.key);
- insert_string(&mut input, "title", &args.title);
- insert_string(&mut input, "category", &args.category);
- insert_string(&mut input, "summary", &args.summary);
- insert_string(&mut input, "bin_id", &args.bin_id);
- insert_string(&mut input, "quantity_amount", &args.quantity_amount);
- insert_string(&mut input, "quantity_unit", &args.quantity_unit);
- insert_string(&mut input, "price_amount", &args.price_amount);
- insert_string(&mut input, "price_currency", &args.price_currency);
- insert_string(&mut input, "price_per_amount", &args.price_per_amount);
- insert_string(&mut input, "price_per_unit", &args.price_per_unit);
- insert_string(&mut input, "available", &args.available);
- insert_string(&mut input, "label", &args.label);
- insert_string(&mut input, "discount_id", &args.discount_id);
- insert_string(&mut input, "discount_label", &args.discount_label);
- insert_string(&mut input, "discount_kind", &args.discount_kind);
- insert_string(&mut input, "discount_value", &args.discount_value);
- insert_string(&mut input, "discount_amount", &args.discount_amount);
- insert_string(&mut input, "discount_currency", &args.discount_currency);
- }
- ListingCommand::Get(args) => insert_string(&mut input, "key", &args.key),
- ListingCommand::App(args) => match &args.command {
- ListingAppCommand::Export(args) => {
- insert_string(&mut input, "record_id", &args.record_id);
- insert_path(&mut input, "output", &args.output);
- }
- ListingAppCommand::List => {}
- },
- ListingCommand::Update(args)
- | ListingCommand::Validate(args)
- | ListingCommand::Publish(args)
- | ListingCommand::Archive(args) => insert_path(&mut input, "file", &args.file),
- ListingCommand::Rebind(args) => {
- insert_path(&mut input, "file", &args.file);
- insert_string(&mut input, "selector", &args.selector);
- insert_string(&mut input, "farm_d_tag", &args.farm_d_tag);
- }
- ListingCommand::List => {}
- },
- TargetCommand::Market(args) => match &args.command {
- MarketCommand::Product(product) => match &product.command {
- MarketProductCommand::Search(args) => {
- insert_string_array(&mut input, "query", args.query.as_slice())
- }
- },
- MarketCommand::Listing(listing) => match &listing.command {
- MarketListingCommand::Get(args) => insert_string(&mut input, "key", &args.key),
- },
- MarketCommand::Refresh => {}
- },
- TargetCommand::Basket(args) => match &args.command {
- BasketCommand::Create(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "listing", &args.listing);
- insert_string(&mut input, "listing_addr", &args.listing_addr);
- insert_string(&mut input, "bin_id", &args.bin_id);
- insert_string(&mut input, "quantity", &args.quantity);
- }
- BasketCommand::Get(args) | BasketCommand::Validate(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id)
- }
- BasketCommand::Item(item) => match &item.command {
- BasketItemCommand::Add(args) | BasketItemCommand::Update(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "item_id", &args.item_id);
- insert_string(&mut input, "listing", &args.listing);
- insert_string(&mut input, "listing_addr", &args.listing_addr);
- insert_string(&mut input, "bin_id", &args.bin_id);
- insert_string(&mut input, "quantity", &args.quantity);
- }
- BasketItemCommand::Remove(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "item_id", &args.item_id);
- }
- },
- BasketCommand::Adjustment(adjustment) => match &adjustment.command {
- BasketAdjustmentCommand::Add(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "id", &args.id);
- insert_string(&mut input, "effect", &args.effect);
- insert_string(&mut input, "amount", &args.amount);
- insert_string(&mut input, "currency", &args.currency);
- insert_string(&mut input, "reason", &args.reason);
- }
- BasketAdjustmentCommand::Remove(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id);
- insert_string(&mut input, "id", &args.id);
- }
- },
- BasketCommand::Quote(quote) => match "e.command {
- BasketQuoteCommand::Create(args) => {
- insert_string(&mut input, "basket_id", &args.basket_id)
- }
- },
- BasketCommand::List => {}
- },
- TargetCommand::Order(args) => match &args.command {
- OrderCommand::Submit(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- }
- OrderCommand::Get(args) => insert_string(&mut input, "order_id", &args.order_id),
- OrderCommand::App(args) => match &args.command {
- OrderAppCommand::Export(args) => {
- insert_string(&mut input, "record_id", &args.record_id);
- insert_path(&mut input, "output", &args.output);
- }
- OrderAppCommand::List => {}
- },
- OrderCommand::Rebind(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- insert_string(&mut input, "selector", &args.selector);
- }
- OrderCommand::Accept(args) => insert_string(&mut input, "order_id", &args.order_id),
- OrderCommand::Decline(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- insert_string(&mut input, "reason", &args.reason);
- }
- OrderCommand::Cancel(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- insert_string(&mut input, "reason", &args.reason);
- }
- OrderCommand::Revision(revision) => match &revision.command {
- OrderRevisionCommand::Propose(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- insert_string(&mut input, "reason", &args.reason);
- insert_string(&mut input, "bin_id", &args.bin_id);
- if let Some(bin_count) = args.bin_count {
- input.insert(
- "bin_count".to_owned(),
- Value::Number(serde_json::Number::from(bin_count)),
- );
- }
- insert_string(&mut input, "adjustment_id", &args.adjustment_id);
- insert_string(&mut input, "adjustment_effect", &args.adjustment_effect);
- insert_string(&mut input, "adjustment_amount", &args.adjustment_amount);
- insert_string(&mut input, "adjustment_currency", &args.adjustment_currency);
- insert_string(&mut input, "adjustment_reason", &args.adjustment_reason);
- }
- OrderRevisionCommand::Accept(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- insert_string(&mut input, "revision_id", &args.revision_id);
- }
- OrderRevisionCommand::Decline(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- insert_string(&mut input, "revision_id", &args.revision_id);
- insert_string(&mut input, "reason", &args.reason);
- }
- },
- OrderCommand::Fulfillment(fulfillment) => match &fulfillment.command {
- OrderFulfillmentCommand::Update(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- if let Some(state) = args.state {
- input.insert(
- "state".to_owned(),
- Value::String(state.as_protocol_state().to_owned()),
- );
- }
- }
- },
- OrderCommand::Receipt(receipt) => match &receipt.command {
- OrderReceiptCommand::Record(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- if args.received {
- input.insert("received".to_owned(), Value::Bool(true));
- }
- insert_string(&mut input, "issue", &args.issue);
- }
- },
- OrderCommand::Payment(payment) => match &payment.command {
- OrderPaymentCommand::Record(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- insert_string(&mut input, "amount", &args.amount);
- insert_string(&mut input, "currency", &args.currency);
- insert_string(&mut input, "method", &args.method);
- insert_string(&mut input, "reference", &args.reference);
- if let Some(paid_at) = args.paid_at {
- input.insert(
- "paid_at".to_owned(),
- Value::Number(serde_json::Number::from(paid_at)),
- );
- }
- }
- },
- OrderCommand::Settlement(settlement) => match &settlement.command {
- OrderSettlementCommand::Accept(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- insert_string(&mut input, "payment_event_id", &args.payment_event_id);
- }
- OrderSettlementCommand::Reject(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- insert_string(&mut input, "payment_event_id", &args.payment_event_id);
- insert_string(&mut input, "reason", &args.reason);
- }
- },
- OrderCommand::Status(status) => match &status.command {
- OrderStatusCommand::Get(args) => {
- insert_string(&mut input, "order_id", &args.order_id)
- }
- },
- OrderCommand::Event(event) => match &event.command {
- OrderEventCommand::List(args) | OrderEventCommand::Watch(args) => {
- insert_string(&mut input, "order_id", &args.order_id)
- }
- },
- OrderCommand::List => {}
- },
- TargetCommand::Validation(args) => match &args.command {
- ValidationCommand::Receipt(receipt) => match &receipt.command {
- ValidationReceiptCommand::Get(args) | ValidationReceiptCommand::Verify(args) => {
- insert_string(&mut input, "receipt_event_id", &args.receipt_event_id);
- }
- ValidationReceiptCommand::List(args) => {
- insert_string(&mut input, "order_id", &args.order_id);
- }
- },
- },
- _ => {}
- }
- input
-}
-
-fn insert_string(input: &mut OperationData, key: &str, value: &Option<String>) {
- if let Some(value) = value
- .as_deref()
- .map(str::trim)
- .filter(|value| !value.is_empty())
- {
- input.insert(key.to_owned(), Value::String(value.to_owned()));
- }
-}
-
-fn insert_string_array(input: &mut OperationData, key: &str, values: &[String]) {
- let values = values
- .iter()
- .map(String::as_str)
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(|value| Value::String(value.to_owned()))
- .collect::<Vec<_>>();
- if !values.is_empty() {
- input.insert(key.to_owned(), Value::Array(values));
- }
-}
-
-fn insert_path(input: &mut OperationData, key: &str, value: &Option<std::path::PathBuf>) {
- if let Some(value) = value {
- input.insert(
- key.to_owned(),
- Value::String(value.to_string_lossy().into_owned()),
- );
- }
-}
-
-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)
- })
-}
-
-#[cfg(test)]
-mod tests {
- use std::io;
-
- use clap::Parser;
- use serde_json::{Value, json};
-
- use super::{
- OperationAdapter, OperationAdapterError, OperationContext, OperationInputMode,
- OperationNetworkMode, OperationOutputFormat, OperationRequest, OperationResult,
- OperationService, TargetOperationRequest, WorkspaceGetRequest, WorkspaceGetResult,
- adapter_registry_linkage_is_valid,
- };
- use crate::operation_registry::OPERATION_REGISTRY;
- use crate::runtime::RuntimeError;
- use crate::runtime::accounts::AccountRuntimeFailure;
- use crate::target_cli::TargetCliArgs;
-
- #[test]
- fn adapter_binds_every_registry_entry() {
- assert!(adapter_registry_linkage_is_valid());
-
- for operation in OPERATION_REGISTRY {
- let parsed = TargetCliArgs::try_parse_from(operation.cli_path.split_whitespace())
- .unwrap_or_else(|error| {
- panic!("{} failed to parse: {error}", operation.cli_path);
- });
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
-
- assert_eq!(request.operation_id(), operation.operation_id);
- assert_eq!(request.spec().mcp_tool, operation.mcp_tool);
- assert_eq!(request.request_type_name(), operation.rust_request);
- assert_eq!(
- TargetOperationRequest::request_type_for_operation(operation.operation_id),
- Some(operation.rust_request)
- );
- }
- }
-
- #[test]
- fn adapter_context_carries_target_global_scope() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "--format",
- "json",
- "--account-id",
- "acct_test",
- "--relay",
- "wss://relay.one",
- "--online",
- "--dry-run",
- "--idempotency-key",
- "idem_test",
- "--correlation-id",
- "corr_test",
- "--approval-token",
- "approval_test",
- "--no-input",
- "--quiet",
- "--verbose",
- "--trace",
- "--no-color",
- "workspace",
- "get",
- ])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let context = request.context();
-
- assert_eq!(context.output_format, OperationOutputFormat::Json);
- assert_eq!(context.account_id.as_deref(), Some("acct_test"));
- assert_eq!(context.relays, vec!["wss://relay.one".to_owned()]);
- assert_eq!(context.network_mode, OperationNetworkMode::Online);
- assert!(context.dry_run);
- assert_eq!(context.idempotency_key.as_deref(), Some("idem_test"));
- assert_eq!(context.correlation_id.as_deref(), Some("corr_test"));
- assert_eq!(context.approval_token.as_deref(), Some("approval_test"));
- assert_eq!(context.input_mode, OperationInputMode::NoInput);
- assert!(context.quiet);
- assert!(context.verbose);
- assert!(context.trace);
- assert!(!context.color);
-
- let envelope_context = context.envelope_context("req_test");
- let actor = envelope_context.actor.expect("account actor");
- assert_eq!(actor.account_id, "acct_test");
- assert_eq!(actor.role, "account");
- }
-
- #[test]
- fn adapter_maps_account_attach_secret_input() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "account",
- "attach-secret",
- "acct_test",
- "identity.json",
- "--default",
- ])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::AccountAttachSecret(request) = request else {
- panic!("expected account attach-secret request")
- };
-
- assert_eq!(request.operation_id(), "account.attach_secret");
- assert_eq!(
- request
- .payload
- .input
- .get("selector")
- .and_then(Value::as_str),
- Some("acct_test")
- );
- assert_eq!(
- request.payload.input.get("path").and_then(Value::as_str),
- Some("identity.json")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("default")
- .and_then(Value::as_bool),
- Some(true)
- );
- }
-
- #[test]
- fn adapter_maps_farm_rebind_selector() {
- let parsed = TargetCliArgs::try_parse_from(["radroots", "farm", "rebind", "acct_test"])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::FarmRebind(request) = request else {
- panic!("expected farm rebind request")
- };
-
- assert_eq!(request.operation_id(), "farm.rebind");
- assert_eq!(
- request
- .payload
- .input
- .get("selector")
- .and_then(Value::as_str),
- Some("acct_test")
- );
- }
-
- #[test]
- fn adapter_maps_listing_rebind_inputs() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "listing",
- "rebind",
- "listing.toml",
- "acct_test",
- "--farm-d-tag",
- "AAAAAAAAAAAAAAAAAAAAAw",
- ])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::ListingRebind(request) = request else {
- panic!("expected listing rebind request")
- };
-
- assert_eq!(request.operation_id(), "listing.rebind");
- assert_eq!(
- request.payload.input.get("file").and_then(Value::as_str),
- Some("listing.toml")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("selector")
- .and_then(Value::as_str),
- Some("acct_test")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("farm_d_tag")
- .and_then(Value::as_str),
- Some("AAAAAAAAAAAAAAAAAAAAAw")
- );
- }
-
- #[test]
- fn adapter_maps_order_rebind_inputs() {
- let parsed =
- TargetCliArgs::try_parse_from(["radroots", "order", "rebind", "ord_test", "acct_test"])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::OrderRebind(request) = request else {
- panic!("expected order rebind request")
- };
-
- assert_eq!(request.operation_id(), "order.rebind");
- assert_eq!(
- request
- .payload
- .input
- .get("order_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("selector")
- .and_then(Value::as_str),
- Some("acct_test")
- );
- }
-
- #[test]
- fn adapter_maps_order_fulfillment_update_input() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "fulfillment",
- "update",
- "ord_test",
- "--state",
- "seller_cancelled",
- ])
- .expect("target args parse");
-
- let request = TargetOperationRequest::from_target_args(&parsed)
- .expect("operation request from target args");
- let TargetOperationRequest::OrderFulfillmentUpdate(request) = request else {
- panic!("expected order fulfillment update request")
- };
-
- assert_eq!(request.operation_id(), "order.fulfillment.update");
- assert_eq!(
- request
- .payload
- .input
- .get("order_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request.payload.input.get("state").and_then(Value::as_str),
- Some("seller_cancelled")
- );
- }
-
- #[test]
- fn adapter_maps_order_lifecycle_inputs() {
- let revision = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "revision",
- "propose",
- "ord_test",
- "--reason",
- "update count",
- "--bin-id",
- "bin-1",
- "--bin-count",
- "3",
- "--adjustment-id",
- "adj-weather",
- "--adjustment-effect",
- "increase",
- "--adjustment-amount",
- "1.25",
- "--adjustment-currency",
- "USD",
- "--adjustment-reason",
- "weather delay",
- ])
- .expect("target args parse");
- let request =
- TargetOperationRequest::from_target_args(&revision).expect("operation request");
- let TargetOperationRequest::OrderRevisionPropose(request) = request else {
- panic!("expected order revision propose request")
- };
- assert_eq!(request.operation_id(), "order.revision.propose");
- assert_eq!(
- request
- .payload
- .input
- .get("order_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request.payload.input.get("reason").and_then(Value::as_str),
- Some("update count")
- );
- assert_eq!(
- request.payload.input.get("bin_id").and_then(Value::as_str),
- Some("bin-1")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("bin_count")
- .and_then(Value::as_u64),
- Some(3)
- );
- assert_eq!(
- request
- .payload
- .input
- .get("adjustment_id")
- .and_then(Value::as_str),
- Some("adj-weather")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("adjustment_effect")
- .and_then(Value::as_str),
- Some("increase")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("adjustment_amount")
- .and_then(Value::as_str),
- Some("1.25")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("adjustment_currency")
- .and_then(Value::as_str),
- Some("USD")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("adjustment_reason")
- .and_then(Value::as_str),
- Some("weather delay")
- );
-
- let revision_accept = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "revision",
- "accept",
- "ord_test",
- "--revision-id",
- "rev_test",
- ])
- .expect("target args parse");
- let request =
- TargetOperationRequest::from_target_args(&revision_accept).expect("operation request");
- let TargetOperationRequest::OrderRevisionAccept(request) = request else {
- panic!("expected order revision accept request")
- };
- assert_eq!(request.operation_id(), "order.revision.accept");
- assert_eq!(
- request
- .payload
- .input
- .get("order_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("revision_id")
- .and_then(Value::as_str),
- Some("rev_test")
- );
-
- let revision_decline = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "revision",
- "decline",
- "ord_test",
- "--revision-id",
- "rev_test",
- "--reason",
- "keep original order",
- ])
- .expect("target args parse");
- let request =
- TargetOperationRequest::from_target_args(&revision_decline).expect("operation request");
- let TargetOperationRequest::OrderRevisionDecline(request) = request else {
- panic!("expected order revision decline request")
- };
- assert_eq!(request.operation_id(), "order.revision.decline");
- assert_eq!(
- request
- .payload
- .input
- .get("order_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("revision_id")
- .and_then(Value::as_str),
- Some("rev_test")
- );
- assert_eq!(
- request.payload.input.get("reason").and_then(Value::as_str),
- Some("keep original order")
- );
-
- let cancel = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "cancel",
- "ord_test",
- "--reason",
- "changed plans",
- ])
- .expect("target args parse");
- let request = TargetOperationRequest::from_target_args(&cancel).expect("operation request");
- let TargetOperationRequest::OrderCancel(request) = request else {
- panic!("expected order cancel request")
- };
- assert_eq!(request.operation_id(), "order.cancel");
- assert_eq!(
- request
- .payload
- .input
- .get("order_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request.payload.input.get("reason").and_then(Value::as_str),
- Some("changed plans")
- );
-
- let receipt = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "receipt",
- "record",
- "ord_test",
- "--issue",
- "damaged items",
- ])
- .expect("target args parse");
- let request =
- TargetOperationRequest::from_target_args(&receipt).expect("operation request");
- let TargetOperationRequest::OrderReceiptRecord(request) = request else {
- panic!("expected order receipt record request")
- };
- assert_eq!(request.operation_id(), "order.receipt.record");
- assert_eq!(
- request
- .payload
- .input
- .get("order_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request.payload.input.get("issue").and_then(Value::as_str),
- Some("damaged items")
- );
-
- let payment = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "payment",
- "record",
- "ord_test",
- "--amount",
- "12",
- "--currency",
- "USD",
- "--method",
- "manual_transfer",
- "--reference",
- "memo-1",
- "--paid-at",
- "1777666000",
- ])
- .expect("target args parse");
- let request =
- TargetOperationRequest::from_target_args(&payment).expect("operation request");
- let TargetOperationRequest::OrderPaymentRecord(request) = request else {
- panic!("expected order payment record request")
- };
- assert_eq!(request.operation_id(), "order.payment.record");
- assert_eq!(
- request
- .payload
- .input
- .get("order_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request.payload.input.get("amount").and_then(Value::as_str),
- Some("12")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("currency")
- .and_then(Value::as_str),
- Some("USD")
- );
- assert_eq!(
- request.payload.input.get("method").and_then(Value::as_str),
- Some("manual_transfer")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("reference")
- .and_then(Value::as_str),
- Some("memo-1")
- );
- assert_eq!(
- request.payload.input.get("paid_at").and_then(Value::as_u64),
- Some(1_777_666_000)
- );
-
- let settlement = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "settlement",
- "reject",
- "ord_test",
- "--payment-event-id",
- "pay_event",
- "--reason",
- "reference mismatch",
- ])
- .expect("target args parse");
- let request =
- TargetOperationRequest::from_target_args(&settlement).expect("operation request");
- let TargetOperationRequest::OrderSettlementReject(request) = request else {
- panic!("expected order settlement reject request")
- };
- assert_eq!(request.operation_id(), "order.settlement.reject");
- assert_eq!(
- request
- .payload
- .input
- .get("order_id")
- .and_then(Value::as_str),
- Some("ord_test")
- );
- assert_eq!(
- request
- .payload
- .input
- .get("payment_event_id")
- .and_then(Value::as_str),
- Some("pay_event")
- );
- assert_eq!(
- request.payload.input.get("reason").and_then(Value::as_str),
- Some("reference mismatch")
- );
- }
-
- #[test]
- fn typed_service_boundary_returns_enveloped_result() {
- struct WorkspaceService;
-
- impl OperationService<WorkspaceGetRequest> for WorkspaceService {
- type Result = WorkspaceGetResult;
-
- fn execute(
- &self,
- request: OperationRequest<WorkspaceGetRequest>,
- ) -> Result<OperationResult<Self::Result>, super::OperationAdapterError> {
- assert_eq!(request.operation_id(), "workspace.get");
- OperationResult::new(WorkspaceGetResult::default())
- }
- }
-
- let adapter = OperationAdapter::new(WorkspaceService);
- let context = OperationContext::default();
- let request = OperationRequest::new(context.clone(), WorkspaceGetRequest::default())
- .expect("typed request");
- let result = adapter.execute(request).expect("typed result");
- let envelope = result
- .to_envelope(context.envelope_context("req_test"))
- .expect("operation envelope");
-
- assert_eq!(envelope.operation_id, "workspace.get");
- assert_eq!(envelope.kind, "workspace.get");
- assert_eq!(envelope.request_id, "req_test");
- assert_eq!(envelope.result, json!({}));
- }
-
- #[test]
- fn approval_errors_map_to_structured_exit_code() {
- let error = OperationAdapterError::approval_required("order.submit");
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "approval_required");
- assert_eq!(output_error.exit_code, 6);
- assert!(output_error.message.contains("approval_token"));
- }
-
- #[test]
- fn not_implemented_errors_map_to_structured_exit_code() {
- let error = OperationAdapterError::not_implemented(
- "order.payment.record",
- "coming soon".to_owned(),
- );
- let output_error = error.to_output_error();
-
- assert_eq!(output_error.code, "not_implemented");
- assert_eq!(output_error.exit_code, 3);
- assert_eq!(
- output_error.detail.expect("detail")["operation_id"],
- "order.payment.record"
- );
- }
-
- #[test]
- fn runtime_failures_map_to_specific_machine_codes() {
- let cases = [
- (
- OperationAdapterError::unconfigured(
- "listing.publish",
- "no selected account for seller write".to_owned(),
- ),
- "account_unresolved",
- "account",
- 5,
- ),
- (
- OperationAdapterError::unconfigured(
- "listing.publish",
- "resolved account `a` is watch_only and cannot sign because it is not secret-backed"
- .to_owned(),
- ),
- "account_watch_only",
- "account",
- 7,
- ),
- (
- OperationAdapterError::unconfigured(
- "listing.publish",
- "account mismatch: resolved account pubkey `b` cannot sign listing seller_pubkey `a`"
- .to_owned(),
- ),
- "account_mismatch",
- "account",
- 5,
- ),
- (
- OperationAdapterError::unconfigured(
- "listing.publish",
- "signer.remote_nip46 binding is missing".to_owned(),
- ),
- "signer_unconfigured",
- "signer",
- 7,
- ),
- (
- OperationAdapterError::unavailable(
- "listing.publish",
- "radrootsd bridge is unavailable".to_owned(),
- ),
- "provider_unavailable",
- "provider",
- 3,
- ),
- (
- OperationAdapterError::SignerModeDeferred {
- operation_id: "signer.status.get".to_owned(),
- message: "signer mode `myc` is deferred".to_owned(),
- },
- "signer_mode_deferred",
- "signer",
- 7,
- ),
- (
- OperationAdapterError::unconfigured(
- "basket.quote.create",
- "quote engine not ready".to_owned(),
- ),
- "operation_unavailable",
- "operation",
- 3,
- ),
- (
- OperationAdapterError::runtime_failure(
- "listing.publish",
- RuntimeError::Io(io::Error::new(io::ErrorKind::NotFound, "missing draft")),
- ),
- "not_found",
- "resource",
- 4,
- ),
- (
- OperationAdapterError::runtime_failure(
- "listing.validate",
- RuntimeError::Config("invalid listing draft listing.toml".to_owned()),
- ),
- "validation_failed",
- "validation",
- 10,
- ),
- (
- OperationAdapterError::runtime_failure(
- "listing.archive",
- RuntimeError::Account(AccountRuntimeFailure::mismatch(
- "account mismatch: resolved account pubkey `b` cannot sign listing seller_pubkey `a`",
- )),
- ),
- "account_mismatch",
- "account",
- 5,
- ),
- (
- OperationAdapterError::runtime_failure(
- "farm.publish",
- RuntimeError::Network("direct relay connection failed".to_owned()),
- ),
- "network_unavailable",
- "network",
- 8,
- ),
- ];
-
- for (error, code, class, exit_code) in cases {
- let output = error.to_output_error();
- assert_eq!(output.code, code);
- assert_eq!(output.exit_code, exit_code);
- assert_eq!(
- output.detail.expect("detail")["class"],
- serde_json::Value::String(class.to_owned())
- );
- }
- }
-}
diff --git a/src/operation_basket.rs b/src/operation_basket.rs
@@ -10,8 +10,8 @@ use radroots_sql_core::SqliteExecutor;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
-use crate::domain::runtime::OrderNewView;
-use crate::operation_adapter::{
+use crate::cli::global::{OrderDraftAdjustmentArgs, OrderDraftCreateArgs};
+use crate::ops::{
BasketAdjustmentAddRequest, BasketAdjustmentAddResult, BasketAdjustmentRemoveRequest,
BasketAdjustmentRemoveResult, BasketCreateRequest, BasketCreateResult, BasketGetRequest,
BasketGetResult, BasketItemAddRequest, BasketItemAddResult, BasketItemRemoveRequest,
@@ -21,7 +21,7 @@ use crate::operation_adapter::{
OperationRequestPayload, OperationResult, OperationResultData, OperationService,
};
use crate::runtime::config::RuntimeConfig;
-use crate::runtime_args::{OrderDraftAdjustmentArgs, OrderDraftCreateArgs};
+use crate::view::runtime::OrderNewView;
const BASKET_KIND: &str = "basket_v1";
const BASKET_SOURCE: &str = "local baskets - local first";
@@ -1325,7 +1325,7 @@ mod tests {
use tempfile::tempdir;
use super::BasketOperationService;
- use crate::operation_adapter::{
+ use crate::ops::{
BasketAdjustmentAddRequest, BasketAdjustmentRemoveRequest, BasketCreateRequest,
BasketGetRequest, BasketItemAddRequest, BasketItemRemoveRequest, BasketItemUpdateRequest,
BasketListRequest, BasketQuoteCreateRequest, BasketValidateRequest, OperationAdapter,
@@ -1750,7 +1750,7 @@ mod tests {
fn add_listing_item(
service: &OperationAdapter<BasketOperationService<'_>>,
basket_id: &str,
- ) -> crate::output_contract::OutputEnvelope {
+ ) -> crate::out::envelope::OutputEnvelope {
let request = OperationRequest::new(
OperationContext::default(),
BasketItemAddRequest::from_data(data(&[
diff --git a/src/operation_core.rs b/src/operation_core.rs
@@ -3,11 +3,8 @@ use std::path::PathBuf;
use serde::Serialize;
use serde_json::{Value, json};
-use crate::domain::runtime::{
- CommandDisposition, LocalBackupView, PublishProviderRuntimeView, PublishRelayRuntimeView,
- PublishRuntimeView,
-};
-use crate::operation_adapter::{
+use crate::cli::global::LocalExportFormatArg;
+use crate::ops::{
AccountAttachSecretRequest, AccountAttachSecretResult, AccountCreateRequest,
AccountCreateResult, AccountGetRequest, AccountGetResult, AccountImportRequest,
AccountImportResult, AccountListRequest, AccountListResult, AccountRemoveRequest,
@@ -34,7 +31,10 @@ use crate::runtime::config::{
PublishMode, RADROOTSD_PUBLISH_DEFERRED_REASON, RuntimeConfig, SignerBackend,
};
use crate::runtime::logging::LoggingState;
-use crate::runtime_args::LocalExportFormatArg;
+use crate::view::runtime::{
+ CommandDisposition, LocalBackupView, PublishProviderRuntimeView, PublishRelayRuntimeView,
+ PublishRuntimeView,
+};
pub struct CoreOperationService<'a> {
config: &'a RuntimeConfig,
@@ -999,7 +999,7 @@ mod tests {
use tempfile::tempdir;
use super::CoreOperationService;
- use crate::operation_adapter::{
+ use crate::ops::{
AccountAttachSecretRequest, AccountCreateRequest, AccountImportRequest, AccountListRequest,
AccountRemoveRequest, OperationAdapter, OperationContext, OperationData, OperationRequest,
StoreStatusGetRequest, WorkspaceGetRequest,
diff --git a/src/operation_farm.rs b/src/operation_farm.rs
@@ -1,8 +1,11 @@
use serde::Serialize;
use serde_json::Value;
-use crate::domain::runtime::{CommandDisposition, FarmPublishView};
-use crate::operation_adapter::{
+use crate::cli::global::{
+ FarmCreateArgs, FarmFieldArg, FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs,
+ FarmUpdateArgs,
+};
+use crate::ops::{
FarmCreateRequest, FarmCreateResult, FarmFulfillmentUpdateRequest, FarmFulfillmentUpdateResult,
FarmGetRequest, FarmGetResult, FarmLocationUpdateRequest, FarmLocationUpdateResult,
FarmProfileUpdateRequest, FarmProfileUpdateResult, FarmPublishRequest, FarmPublishResult,
@@ -12,10 +15,7 @@ use crate::operation_adapter::{
};
use crate::runtime::RuntimeError;
use crate::runtime::config::{PublishMode, RuntimeConfig};
-use crate::runtime_args::{
- FarmCreateArgs, FarmFieldArg, FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs,
- FarmUpdateArgs,
-};
+use crate::view::runtime::{CommandDisposition, FarmPublishView};
pub struct FarmOperationService<'a> {
config: &'a RuntimeConfig,
@@ -383,7 +383,7 @@ mod tests {
use tempfile::tempdir;
use super::FarmOperationService;
- use crate::operation_adapter::{
+ use crate::ops::{
FarmCreateRequest, FarmGetRequest, FarmPublishRequest, FarmReadinessCheckRequest,
FarmRebindRequest, OperationAdapter, OperationContext, OperationData, OperationRequest,
};
diff --git a/src/operation_listing.rs b/src/operation_listing.rs
@@ -3,8 +3,11 @@ use std::path::PathBuf;
use serde::Serialize;
use serde_json::Value;
-use crate::domain::runtime::{CommandDisposition, ListingAppRecordExportView, ListingMutationView};
-use crate::operation_adapter::{
+use crate::cli::global::{
+ ListingAppRecordExportArgs, ListingCreateArgs, ListingFileArgs, ListingMutationArgs,
+ ListingRebindArgs, RecordLookupArgs,
+};
+use crate::ops::{
ListingAppExportRequest, ListingAppExportResult, ListingAppListRequest, ListingAppListResult,
ListingArchiveRequest, ListingArchiveResult, ListingCreateRequest, ListingCreateResult,
ListingGetRequest, ListingGetResult, ListingListRequest, ListingListResult,
@@ -15,10 +18,7 @@ use crate::operation_adapter::{
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
-use crate::runtime_args::{
- ListingAppRecordExportArgs, ListingCreateArgs, ListingFileArgs, ListingMutationArgs,
- ListingRebindArgs, RecordLookupArgs,
-};
+use crate::view::runtime::{CommandDisposition, ListingAppRecordExportView, ListingMutationView};
pub struct ListingOperationService<'a> {
config: &'a RuntimeConfig,
@@ -467,7 +467,7 @@ mod tests {
use tempfile::tempdir;
use super::ListingOperationService;
- use crate::operation_adapter::{
+ use crate::ops::{
ListingArchiveRequest, ListingCreateRequest, ListingListRequest, ListingPublishRequest,
OperationAdapter, OperationContext, OperationData, OperationRequest,
};
diff --git a/src/operation_market.rs b/src/operation_market.rs
@@ -1,8 +1,8 @@
use serde::Serialize;
use serde_json::Value;
-use crate::domain::runtime::{FindView, ListingGetView, SyncActionView};
-use crate::operation_adapter::{
+use crate::cli::global::{FindQueryArgs, RecordLookupArgs};
+use crate::ops::{
MarketListingGetRequest, MarketListingGetResult, MarketProductSearchRequest,
MarketProductSearchResult, MarketRefreshRequest, MarketRefreshResult, OperationAdapterError,
OperationRequest, OperationRequestData, OperationRequestPayload, OperationResult,
@@ -10,7 +10,7 @@ use crate::operation_adapter::{
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
-use crate::runtime_args::{FindQueryArgs, RecordLookupArgs};
+use crate::view::runtime::{FindView, ListingGetView, SyncActionView};
pub struct MarketOperationService<'a> {
config: &'a RuntimeConfig,
@@ -249,11 +249,7 @@ mod tests {
use tempfile::tempdir;
use super::{MarketOperationService, market_listing_get_view, market_product_search_view};
- use crate::domain::runtime::{
- FindPriceView, FindQuantityView, FindResultProvenanceView, FindResultView, FindView,
- ListingGetView, MarketReadinessView, SyncFreshnessView,
- };
- use crate::operation_adapter::{
+ use crate::ops::{
MarketListingGetRequest, MarketProductSearchRequest, MarketRefreshRequest,
OperationAdapter, OperationContext, OperationData, OperationRequest,
};
@@ -263,6 +259,10 @@ mod tests {
PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
};
+ use crate::view::runtime::{
+ FindPriceView, FindQuantityView, FindResultProvenanceView, FindResultView, FindView,
+ ListingGetView, MarketReadinessView, SyncFreshnessView,
+ };
const LISTING_ADDR: &str = "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
diff --git a/src/operation_order.rs b/src/operation_order.rs
@@ -3,13 +3,14 @@ use std::path::PathBuf;
use serde::Serialize;
use serde_json::{Value, json};
-use crate::deferred_payment::deferred_payment_message;
-use crate::domain::runtime::{
- CommandDisposition, OrderAppRecordExportView, OrderCancellationView, OrderDecisionView,
- OrderFulfillmentView, OrderRebindView, OrderReceiptView, OrderRevisionDecisionView,
- OrderRevisionProposalView, OrderStatusView, OrderSubmitView,
+use crate::cli::global::{
+ OrderAppRecordExportArgs, OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs,
+ OrderFulfillmentArgs, OrderRebindArgs, OrderReceiptArgs, OrderRevisionDecisionArg,
+ OrderRevisionDecisionArgs, OrderRevisionProposeArgs, OrderStatusArgs, OrderSubmitArgs,
+ RecordLookupArgs,
};
-use crate::operation_adapter::{
+use crate::deferred_payment::deferred_payment_message;
+use crate::ops::{
OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
OperationResult, OperationResultData, OperationService, OrderAcceptRequest, OrderAcceptResult,
OrderAppExportRequest, OrderAppExportResult, OrderAppListRequest, OrderAppListResult,
@@ -26,11 +27,10 @@ use crate::operation_adapter::{
};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
-use crate::runtime_args::{
- OrderAppRecordExportArgs, OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs,
- OrderFulfillmentArgs, OrderRebindArgs, OrderReceiptArgs, OrderRevisionDecisionArg,
- OrderRevisionDecisionArgs, OrderRevisionProposeArgs, OrderStatusArgs, OrderSubmitArgs,
- RecordLookupArgs,
+use crate::view::runtime::{
+ CommandDisposition, OrderAppRecordExportView, OrderCancellationView, OrderDecisionView,
+ OrderFulfillmentView, OrderRebindView, OrderReceiptView, OrderRevisionDecisionView,
+ OrderRevisionProposalView, OrderStatusView, OrderSubmitView,
};
const ORDER_EVENT_WATCH_DEFERRED_REASON: &str = "relay-backed order event watch is not implemented";
@@ -1420,7 +1420,7 @@ fn order_submit_error_detail(view: &OrderSubmitView) -> Value {
fn event_list_result<R>(
operation_id: &str,
- view: &crate::domain::runtime::OrderEventListView,
+ view: &crate::view::runtime::OrderEventListView,
) -> Result<OperationResult<R>, OperationAdapterError>
where
R: OperationResultData,
@@ -1455,7 +1455,7 @@ where
}
}
-fn order_event_list_error_detail(view: &crate::domain::runtime::OrderEventListView) -> Value {
+fn order_event_list_error_detail(view: &crate::view::runtime::OrderEventListView) -> Value {
json!({
"state": &view.state,
"seller_pubkey": &view.seller_pubkey,
@@ -1565,8 +1565,7 @@ mod tests {
use tempfile::tempdir;
use super::{OrderOperationService, decision_result};
- use crate::domain::runtime::OrderDecisionView;
- use crate::operation_adapter::{
+ use crate::ops::{
OperationAdapter, OperationContext, OperationData, OperationRequest, OrderAcceptRequest,
OrderAcceptResult, OrderCancelRequest, OrderDeclineRequest, OrderDeclineResult,
OrderEventListRequest, OrderEventWatchRequest, OrderGetRequest, OrderListRequest,
@@ -1580,6 +1579,7 @@ mod tests {
PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
};
+ use crate::view::runtime::OrderDecisionView;
#[test]
fn order_service_get_and_list_preserve_order_truth() {
@@ -1649,7 +1649,7 @@ mod tests {
assert_eq!(output_error.code, "not_found");
assert_eq!(output_error.exit_code, 4);
assert!(output_error.message.contains("ord_missing"));
- let envelope = crate::output_contract::OutputEnvelope::failure(
+ let envelope = crate::out::envelope::OutputEnvelope::failure(
"order.submit",
output_error,
context.envelope_context("req_order_submit"),
@@ -2016,7 +2016,7 @@ mod tests {
assert_eq!(detail["fetched_count"], 0);
assert_eq!(detail["decoded_count"], 0);
assert_eq!(detail["skipped_count"], 0);
- let envelope = crate::output_contract::OutputEnvelope::failure(
+ let envelope = crate::out::envelope::OutputEnvelope::failure(
"order.status.get",
output_error,
OperationContext::default().envelope_context("req_order_status"),
@@ -2039,7 +2039,7 @@ mod tests {
.execute(request)
.expect_err("order event list unconfigured")
.to_output_error();
- let envelope = crate::output_contract::OutputEnvelope::failure(
+ let envelope = crate::out::envelope::OutputEnvelope::failure(
"order.event.list",
output_error,
context.envelope_context("req_order_event_list"),
@@ -2071,7 +2071,7 @@ mod tests {
.execute(request)
.expect_err("order event list missing account")
.to_output_error();
- let envelope = crate::output_contract::OutputEnvelope::failure(
+ let envelope = crate::out::envelope::OutputEnvelope::failure(
"order.event.list",
output_error,
context.envelope_context("req_order_event_list"),
@@ -2106,7 +2106,7 @@ mod tests {
let error = service
.execute(request)
.expect_err("order event watch deferred");
- let envelope = crate::output_contract::OutputEnvelope::failure(
+ let envelope = crate::out::envelope::OutputEnvelope::failure(
"order.event.watch",
error.to_output_error(),
OperationContext::default().envelope_context("req_order_watch"),
diff --git a/src/operation_runtime.rs b/src/operation_runtime.rs
@@ -1,8 +1,8 @@
use serde::Serialize;
use serde_json::{Value, json};
-use crate::domain::runtime::{CommandDisposition, SyncActionView, SyncStatusView};
-use crate::operation_adapter::{
+use crate::cli::global::SyncWatchArgs;
+use crate::ops::{
OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
OperationResult, OperationResultData, OperationService, RelayListRequest, RelayListResult,
SignerStatusGetRequest, SignerStatusGetResult, SyncPullRequest, SyncPullResult,
@@ -11,7 +11,7 @@ use crate::operation_adapter::{
};
use crate::runtime::RuntimeError;
use crate::runtime::config::{PublishMode, RuntimeConfig};
-use crate::runtime_args::SyncWatchArgs;
+use crate::view::runtime::{CommandDisposition, SyncActionView, SyncStatusView};
pub struct RuntimeOperationService<'a> {
config: &'a RuntimeConfig,
@@ -241,7 +241,7 @@ mod tests {
use tempfile::tempdir;
use super::RuntimeOperationService;
- use crate::operation_adapter::{
+ use crate::ops::{
OperationAdapter, OperationContext, OperationRequest, RelayListRequest,
SignerStatusGetRequest, SyncPushRequest, SyncStatusGetRequest,
};
diff --git a/src/operation_validation.rs b/src/operation_validation.rs
@@ -1,8 +1,7 @@
use serde::Serialize;
use serde_json::{Value, json};
-use crate::domain::runtime::CommandDisposition;
-use crate::operation_adapter::{
+use crate::ops::{
OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
OperationResult, OperationResultData, OperationService, ValidationReceiptGetRequest,
ValidationReceiptGetResult, ValidationReceiptListRequest, ValidationReceiptListResult,
@@ -13,6 +12,7 @@ use crate::runtime::validation_receipt::{
ValidationReceiptEventArgs, ValidationReceiptInspectionView, ValidationReceiptListArgs,
ValidationReceiptListView,
};
+use crate::view::runtime::CommandDisposition;
pub struct ValidationOperationService<'a> {
config: &'a RuntimeConfig,
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
@@ -0,0 +1,2466 @@
+#![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(
+ args.command.operation_id(),
+ OperationContext::from_target_args(args),
+ 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
+ }
+ }
+}
+
+fn target_operation_input(command: &crate::cli::TargetCommand) -> OperationData {
+ use crate::cli::{
+ AccountCommand, AccountSelectionCommand, BasketAdjustmentCommand, BasketCommand,
+ BasketItemCommand, BasketQuoteCommand, FarmCommand, FarmFulfillmentCommand,
+ FarmLocationCommand, FarmProfileCommand, ListingAppCommand, ListingCommand, MarketCommand,
+ MarketListingCommand, MarketProductCommand, OrderAppCommand, OrderCommand,
+ OrderEventCommand, OrderFulfillmentCommand, OrderPaymentCommand, OrderReceiptCommand,
+ OrderRevisionCommand, OrderSettlementCommand, OrderStatusCommand, TargetCommand,
+ ValidationCommand, ValidationReceiptCommand,
+ };
+
+ let mut input = OperationData::new();
+ match command {
+ TargetCommand::Account(args) => match &args.command {
+ AccountCommand::Import(args) => {
+ insert_path(&mut input, "path", &args.path);
+ if args.default {
+ input.insert("default".to_owned(), Value::Bool(true));
+ }
+ }
+ AccountCommand::AttachSecret(args) => {
+ insert_string(&mut input, "selector", &args.selector);
+ insert_path(&mut input, "path", &args.path);
+ if args.default {
+ input.insert("default".to_owned(), Value::Bool(true));
+ }
+ }
+ AccountCommand::Get(args) => insert_string(&mut input, "selector", &args.selector),
+ AccountCommand::Remove(args) => insert_string(&mut input, "selector", &args.selector),
+ AccountCommand::Selection(args) => match &args.command {
+ AccountSelectionCommand::Update(args) => {
+ insert_string(&mut input, "selector", &args.selector)
+ }
+ AccountSelectionCommand::Get | AccountSelectionCommand::Clear => {}
+ },
+ AccountCommand::Create | AccountCommand::List => {}
+ },
+ TargetCommand::Farm(args) => match &args.command {
+ FarmCommand::Create(args) => {
+ insert_string(&mut input, "farm_d_tag", &args.farm_d_tag);
+ insert_string(&mut input, "name", &args.name);
+ insert_string(&mut input, "display_name", &args.display_name);
+ insert_string(&mut input, "about", &args.about);
+ insert_string(&mut input, "website", &args.website);
+ insert_string(&mut input, "picture", &args.picture);
+ insert_string(&mut input, "banner", &args.banner);
+ insert_string(&mut input, "location", &args.location);
+ insert_string(&mut input, "city", &args.city);
+ insert_string(&mut input, "region", &args.region);
+ insert_string(&mut input, "country", &args.country);
+ insert_string(&mut input, "delivery_method", &args.delivery_method);
+ }
+ FarmCommand::Rebind(args) => {
+ insert_string(&mut input, "selector", &args.selector);
+ }
+ FarmCommand::Profile(args) => match &args.command {
+ FarmProfileCommand::Update(args) => {
+ insert_string(&mut input, "field", &args.field);
+ insert_string(&mut input, "value", &args.value);
+ }
+ },
+ FarmCommand::Location(args) => match &args.command {
+ FarmLocationCommand::Update(args) => {
+ insert_string(&mut input, "field", &args.field);
+ insert_string(&mut input, "value", &args.value);
+ }
+ },
+ FarmCommand::Fulfillment(args) => match &args.command {
+ FarmFulfillmentCommand::Update(args) => {
+ insert_string(&mut input, "value", &args.value);
+ }
+ },
+ FarmCommand::Get | FarmCommand::Readiness(_) | FarmCommand::Publish => {}
+ },
+ TargetCommand::Listing(args) => match &args.command {
+ ListingCommand::Create(args) => {
+ insert_path(&mut input, "output", &args.output);
+ insert_string(&mut input, "key", &args.key);
+ insert_string(&mut input, "title", &args.title);
+ insert_string(&mut input, "category", &args.category);
+ insert_string(&mut input, "summary", &args.summary);
+ insert_string(&mut input, "bin_id", &args.bin_id);
+ insert_string(&mut input, "quantity_amount", &args.quantity_amount);
+ insert_string(&mut input, "quantity_unit", &args.quantity_unit);
+ insert_string(&mut input, "price_amount", &args.price_amount);
+ insert_string(&mut input, "price_currency", &args.price_currency);
+ insert_string(&mut input, "price_per_amount", &args.price_per_amount);
+ insert_string(&mut input, "price_per_unit", &args.price_per_unit);
+ insert_string(&mut input, "available", &args.available);
+ insert_string(&mut input, "label", &args.label);
+ insert_string(&mut input, "discount_id", &args.discount_id);
+ insert_string(&mut input, "discount_label", &args.discount_label);
+ insert_string(&mut input, "discount_kind", &args.discount_kind);
+ insert_string(&mut input, "discount_value", &args.discount_value);
+ insert_string(&mut input, "discount_amount", &args.discount_amount);
+ insert_string(&mut input, "discount_currency", &args.discount_currency);
+ }
+ ListingCommand::Get(args) => insert_string(&mut input, "key", &args.key),
+ ListingCommand::App(args) => match &args.command {
+ ListingAppCommand::Export(args) => {
+ insert_string(&mut input, "record_id", &args.record_id);
+ insert_path(&mut input, "output", &args.output);
+ }
+ ListingAppCommand::List => {}
+ },
+ ListingCommand::Update(args)
+ | ListingCommand::Validate(args)
+ | ListingCommand::Publish(args)
+ | ListingCommand::Archive(args) => insert_path(&mut input, "file", &args.file),
+ ListingCommand::Rebind(args) => {
+ insert_path(&mut input, "file", &args.file);
+ insert_string(&mut input, "selector", &args.selector);
+ insert_string(&mut input, "farm_d_tag", &args.farm_d_tag);
+ }
+ ListingCommand::List => {}
+ },
+ TargetCommand::Market(args) => match &args.command {
+ MarketCommand::Product(product) => match &product.command {
+ MarketProductCommand::Search(args) => {
+ insert_string_array(&mut input, "query", args.query.as_slice())
+ }
+ },
+ MarketCommand::Listing(listing) => match &listing.command {
+ MarketListingCommand::Get(args) => insert_string(&mut input, "key", &args.key),
+ },
+ MarketCommand::Refresh => {}
+ },
+ TargetCommand::Basket(args) => match &args.command {
+ BasketCommand::Create(args) => {
+ insert_string(&mut input, "basket_id", &args.basket_id);
+ insert_string(&mut input, "listing", &args.listing);
+ insert_string(&mut input, "listing_addr", &args.listing_addr);
+ insert_string(&mut input, "bin_id", &args.bin_id);
+ insert_string(&mut input, "quantity", &args.quantity);
+ }
+ BasketCommand::Get(args) | BasketCommand::Validate(args) => {
+ insert_string(&mut input, "basket_id", &args.basket_id)
+ }
+ BasketCommand::Item(item) => match &item.command {
+ BasketItemCommand::Add(args) | BasketItemCommand::Update(args) => {
+ insert_string(&mut input, "basket_id", &args.basket_id);
+ insert_string(&mut input, "item_id", &args.item_id);
+ insert_string(&mut input, "listing", &args.listing);
+ insert_string(&mut input, "listing_addr", &args.listing_addr);
+ insert_string(&mut input, "bin_id", &args.bin_id);
+ insert_string(&mut input, "quantity", &args.quantity);
+ }
+ BasketItemCommand::Remove(args) => {
+ insert_string(&mut input, "basket_id", &args.basket_id);
+ insert_string(&mut input, "item_id", &args.item_id);
+ }
+ },
+ BasketCommand::Adjustment(adjustment) => match &adjustment.command {
+ BasketAdjustmentCommand::Add(args) => {
+ insert_string(&mut input, "basket_id", &args.basket_id);
+ insert_string(&mut input, "id", &args.id);
+ insert_string(&mut input, "effect", &args.effect);
+ insert_string(&mut input, "amount", &args.amount);
+ insert_string(&mut input, "currency", &args.currency);
+ insert_string(&mut input, "reason", &args.reason);
+ }
+ BasketAdjustmentCommand::Remove(args) => {
+ insert_string(&mut input, "basket_id", &args.basket_id);
+ insert_string(&mut input, "id", &args.id);
+ }
+ },
+ BasketCommand::Quote(quote) => match "e.command {
+ BasketQuoteCommand::Create(args) => {
+ insert_string(&mut input, "basket_id", &args.basket_id)
+ }
+ },
+ BasketCommand::List => {}
+ },
+ TargetCommand::Order(args) => match &args.command {
+ OrderCommand::Submit(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ }
+ OrderCommand::Get(args) => insert_string(&mut input, "order_id", &args.order_id),
+ OrderCommand::App(args) => match &args.command {
+ OrderAppCommand::Export(args) => {
+ insert_string(&mut input, "record_id", &args.record_id);
+ insert_path(&mut input, "output", &args.output);
+ }
+ OrderAppCommand::List => {}
+ },
+ OrderCommand::Rebind(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ insert_string(&mut input, "selector", &args.selector);
+ }
+ OrderCommand::Accept(args) => insert_string(&mut input, "order_id", &args.order_id),
+ OrderCommand::Decline(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ insert_string(&mut input, "reason", &args.reason);
+ }
+ OrderCommand::Cancel(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ insert_string(&mut input, "reason", &args.reason);
+ }
+ OrderCommand::Revision(revision) => match &revision.command {
+ OrderRevisionCommand::Propose(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ insert_string(&mut input, "reason", &args.reason);
+ insert_string(&mut input, "bin_id", &args.bin_id);
+ if let Some(bin_count) = args.bin_count {
+ input.insert(
+ "bin_count".to_owned(),
+ Value::Number(serde_json::Number::from(bin_count)),
+ );
+ }
+ insert_string(&mut input, "adjustment_id", &args.adjustment_id);
+ insert_string(&mut input, "adjustment_effect", &args.adjustment_effect);
+ insert_string(&mut input, "adjustment_amount", &args.adjustment_amount);
+ insert_string(&mut input, "adjustment_currency", &args.adjustment_currency);
+ insert_string(&mut input, "adjustment_reason", &args.adjustment_reason);
+ }
+ OrderRevisionCommand::Accept(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ insert_string(&mut input, "revision_id", &args.revision_id);
+ }
+ OrderRevisionCommand::Decline(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ insert_string(&mut input, "revision_id", &args.revision_id);
+ insert_string(&mut input, "reason", &args.reason);
+ }
+ },
+ OrderCommand::Fulfillment(fulfillment) => match &fulfillment.command {
+ OrderFulfillmentCommand::Update(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ if let Some(state) = args.state {
+ input.insert(
+ "state".to_owned(),
+ Value::String(state.as_protocol_state().to_owned()),
+ );
+ }
+ }
+ },
+ OrderCommand::Receipt(receipt) => match &receipt.command {
+ OrderReceiptCommand::Record(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ if args.received {
+ input.insert("received".to_owned(), Value::Bool(true));
+ }
+ insert_string(&mut input, "issue", &args.issue);
+ }
+ },
+ OrderCommand::Payment(payment) => match &payment.command {
+ OrderPaymentCommand::Record(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ insert_string(&mut input, "amount", &args.amount);
+ insert_string(&mut input, "currency", &args.currency);
+ insert_string(&mut input, "method", &args.method);
+ insert_string(&mut input, "reference", &args.reference);
+ if let Some(paid_at) = args.paid_at {
+ input.insert(
+ "paid_at".to_owned(),
+ Value::Number(serde_json::Number::from(paid_at)),
+ );
+ }
+ }
+ },
+ OrderCommand::Settlement(settlement) => match &settlement.command {
+ OrderSettlementCommand::Accept(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ insert_string(&mut input, "payment_event_id", &args.payment_event_id);
+ }
+ OrderSettlementCommand::Reject(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ insert_string(&mut input, "payment_event_id", &args.payment_event_id);
+ insert_string(&mut input, "reason", &args.reason);
+ }
+ },
+ OrderCommand::Status(status) => match &status.command {
+ OrderStatusCommand::Get(args) => {
+ insert_string(&mut input, "order_id", &args.order_id)
+ }
+ },
+ OrderCommand::Event(event) => match &event.command {
+ OrderEventCommand::List(args) | OrderEventCommand::Watch(args) => {
+ insert_string(&mut input, "order_id", &args.order_id)
+ }
+ },
+ OrderCommand::List => {}
+ },
+ TargetCommand::Validation(args) => match &args.command {
+ ValidationCommand::Receipt(receipt) => match &receipt.command {
+ ValidationReceiptCommand::Get(args) | ValidationReceiptCommand::Verify(args) => {
+ insert_string(&mut input, "receipt_event_id", &args.receipt_event_id);
+ }
+ ValidationReceiptCommand::List(args) => {
+ insert_string(&mut input, "order_id", &args.order_id);
+ }
+ },
+ },
+ _ => {}
+ }
+ input
+}
+
+fn insert_string(input: &mut OperationData, key: &str, value: &Option<String>) {
+ if let Some(value) = value
+ .as_deref()
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ {
+ input.insert(key.to_owned(), Value::String(value.to_owned()));
+ }
+}
+
+fn insert_string_array(input: &mut OperationData, key: &str, values: &[String]) {
+ let values = values
+ .iter()
+ .map(String::as_str)
+ .map(str::trim)
+ .filter(|value| !value.is_empty())
+ .map(|value| Value::String(value.to_owned()))
+ .collect::<Vec<_>>();
+ if !values.is_empty() {
+ input.insert(key.to_owned(), Value::Array(values));
+ }
+}
+
+fn insert_path(input: &mut OperationData, key: &str, value: &Option<std::path::PathBuf>) {
+ if let Some(value) = value {
+ input.insert(
+ key.to_owned(),
+ Value::String(value.to_string_lossy().into_owned()),
+ );
+ }
+}
+
+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)
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use std::io;
+
+ use clap::Parser;
+ use serde_json::{Value, json};
+
+ use super::{
+ OperationAdapter, OperationAdapterError, OperationContext, OperationInputMode,
+ OperationNetworkMode, OperationOutputFormat, OperationRequest, OperationResult,
+ OperationService, TargetOperationRequest, WorkspaceGetRequest, WorkspaceGetResult,
+ adapter_registry_linkage_is_valid,
+ };
+ use crate::cli::TargetCliArgs;
+ use crate::registry::OPERATION_REGISTRY;
+ use crate::runtime::RuntimeError;
+ use crate::runtime::accounts::AccountRuntimeFailure;
+
+ #[test]
+ fn adapter_binds_every_registry_entry() {
+ assert!(adapter_registry_linkage_is_valid());
+
+ for operation in OPERATION_REGISTRY {
+ let parsed = TargetCliArgs::try_parse_from(operation.cli_path.split_whitespace())
+ .unwrap_or_else(|error| {
+ panic!("{} failed to parse: {error}", operation.cli_path);
+ });
+ let request = TargetOperationRequest::from_target_args(&parsed)
+ .expect("operation request from target args");
+
+ assert_eq!(request.operation_id(), operation.operation_id);
+ assert_eq!(request.spec().mcp_tool, operation.mcp_tool);
+ assert_eq!(request.request_type_name(), operation.rust_request);
+ assert_eq!(
+ TargetOperationRequest::request_type_for_operation(operation.operation_id),
+ Some(operation.rust_request)
+ );
+ }
+ }
+
+ #[test]
+ fn adapter_context_carries_target_global_scope() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "--format",
+ "json",
+ "--account-id",
+ "acct_test",
+ "--relay",
+ "wss://relay.one",
+ "--online",
+ "--dry-run",
+ "--idempotency-key",
+ "idem_test",
+ "--correlation-id",
+ "corr_test",
+ "--approval-token",
+ "approval_test",
+ "--no-input",
+ "--quiet",
+ "--verbose",
+ "--trace",
+ "--no-color",
+ "workspace",
+ "get",
+ ])
+ .expect("target args parse");
+
+ let request = TargetOperationRequest::from_target_args(&parsed)
+ .expect("operation request from target args");
+ let context = request.context();
+
+ assert_eq!(context.output_format, OperationOutputFormat::Json);
+ assert_eq!(context.account_id.as_deref(), Some("acct_test"));
+ assert_eq!(context.relays, vec!["wss://relay.one".to_owned()]);
+ assert_eq!(context.network_mode, OperationNetworkMode::Online);
+ assert!(context.dry_run);
+ assert_eq!(context.idempotency_key.as_deref(), Some("idem_test"));
+ assert_eq!(context.correlation_id.as_deref(), Some("corr_test"));
+ assert_eq!(context.approval_token.as_deref(), Some("approval_test"));
+ assert_eq!(context.input_mode, OperationInputMode::NoInput);
+ assert!(context.quiet);
+ assert!(context.verbose);
+ assert!(context.trace);
+ assert!(!context.color);
+
+ let envelope_context = context.envelope_context("req_test");
+ let actor = envelope_context.actor.expect("account actor");
+ assert_eq!(actor.account_id, "acct_test");
+ assert_eq!(actor.role, "account");
+ }
+
+ #[test]
+ fn adapter_maps_account_attach_secret_input() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "account",
+ "attach-secret",
+ "acct_test",
+ "identity.json",
+ "--default",
+ ])
+ .expect("target args parse");
+
+ let request = TargetOperationRequest::from_target_args(&parsed)
+ .expect("operation request from target args");
+ let TargetOperationRequest::AccountAttachSecret(request) = request else {
+ panic!("expected account attach-secret request")
+ };
+
+ assert_eq!(request.operation_id(), "account.attach_secret");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("selector")
+ .and_then(Value::as_str),
+ Some("acct_test")
+ );
+ assert_eq!(
+ request.payload.input.get("path").and_then(Value::as_str),
+ Some("identity.json")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("default")
+ .and_then(Value::as_bool),
+ Some(true)
+ );
+ }
+
+ #[test]
+ fn adapter_maps_farm_rebind_selector() {
+ let parsed = TargetCliArgs::try_parse_from(["radroots", "farm", "rebind", "acct_test"])
+ .expect("target args parse");
+
+ let request = TargetOperationRequest::from_target_args(&parsed)
+ .expect("operation request from target args");
+ let TargetOperationRequest::FarmRebind(request) = request else {
+ panic!("expected farm rebind request")
+ };
+
+ assert_eq!(request.operation_id(), "farm.rebind");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("selector")
+ .and_then(Value::as_str),
+ Some("acct_test")
+ );
+ }
+
+ #[test]
+ fn adapter_maps_listing_rebind_inputs() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "listing",
+ "rebind",
+ "listing.toml",
+ "acct_test",
+ "--farm-d-tag",
+ "AAAAAAAAAAAAAAAAAAAAAw",
+ ])
+ .expect("target args parse");
+
+ let request = TargetOperationRequest::from_target_args(&parsed)
+ .expect("operation request from target args");
+ let TargetOperationRequest::ListingRebind(request) = request else {
+ panic!("expected listing rebind request")
+ };
+
+ assert_eq!(request.operation_id(), "listing.rebind");
+ assert_eq!(
+ request.payload.input.get("file").and_then(Value::as_str),
+ Some("listing.toml")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("selector")
+ .and_then(Value::as_str),
+ Some("acct_test")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("farm_d_tag")
+ .and_then(Value::as_str),
+ Some("AAAAAAAAAAAAAAAAAAAAAw")
+ );
+ }
+
+ #[test]
+ fn adapter_maps_order_rebind_inputs() {
+ let parsed =
+ TargetCliArgs::try_parse_from(["radroots", "order", "rebind", "ord_test", "acct_test"])
+ .expect("target args parse");
+
+ let request = TargetOperationRequest::from_target_args(&parsed)
+ .expect("operation request from target args");
+ let TargetOperationRequest::OrderRebind(request) = request else {
+ panic!("expected order rebind request")
+ };
+
+ assert_eq!(request.operation_id(), "order.rebind");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("order_id")
+ .and_then(Value::as_str),
+ Some("ord_test")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("selector")
+ .and_then(Value::as_str),
+ Some("acct_test")
+ );
+ }
+
+ #[test]
+ fn adapter_maps_order_fulfillment_update_input() {
+ let parsed = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "fulfillment",
+ "update",
+ "ord_test",
+ "--state",
+ "seller_cancelled",
+ ])
+ .expect("target args parse");
+
+ let request = TargetOperationRequest::from_target_args(&parsed)
+ .expect("operation request from target args");
+ let TargetOperationRequest::OrderFulfillmentUpdate(request) = request else {
+ panic!("expected order fulfillment update request")
+ };
+
+ assert_eq!(request.operation_id(), "order.fulfillment.update");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("order_id")
+ .and_then(Value::as_str),
+ Some("ord_test")
+ );
+ assert_eq!(
+ request.payload.input.get("state").and_then(Value::as_str),
+ Some("seller_cancelled")
+ );
+ }
+
+ #[test]
+ fn adapter_maps_order_lifecycle_inputs() {
+ let revision = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "revision",
+ "propose",
+ "ord_test",
+ "--reason",
+ "update count",
+ "--bin-id",
+ "bin-1",
+ "--bin-count",
+ "3",
+ "--adjustment-id",
+ "adj-weather",
+ "--adjustment-effect",
+ "increase",
+ "--adjustment-amount",
+ "1.25",
+ "--adjustment-currency",
+ "USD",
+ "--adjustment-reason",
+ "weather delay",
+ ])
+ .expect("target args parse");
+ let request =
+ TargetOperationRequest::from_target_args(&revision).expect("operation request");
+ let TargetOperationRequest::OrderRevisionPropose(request) = request else {
+ panic!("expected order revision propose request")
+ };
+ assert_eq!(request.operation_id(), "order.revision.propose");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("order_id")
+ .and_then(Value::as_str),
+ Some("ord_test")
+ );
+ assert_eq!(
+ request.payload.input.get("reason").and_then(Value::as_str),
+ Some("update count")
+ );
+ assert_eq!(
+ request.payload.input.get("bin_id").and_then(Value::as_str),
+ Some("bin-1")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("bin_count")
+ .and_then(Value::as_u64),
+ Some(3)
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("adjustment_id")
+ .and_then(Value::as_str),
+ Some("adj-weather")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("adjustment_effect")
+ .and_then(Value::as_str),
+ Some("increase")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("adjustment_amount")
+ .and_then(Value::as_str),
+ Some("1.25")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("adjustment_currency")
+ .and_then(Value::as_str),
+ Some("USD")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("adjustment_reason")
+ .and_then(Value::as_str),
+ Some("weather delay")
+ );
+
+ let revision_accept = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "revision",
+ "accept",
+ "ord_test",
+ "--revision-id",
+ "rev_test",
+ ])
+ .expect("target args parse");
+ let request =
+ TargetOperationRequest::from_target_args(&revision_accept).expect("operation request");
+ let TargetOperationRequest::OrderRevisionAccept(request) = request else {
+ panic!("expected order revision accept request")
+ };
+ assert_eq!(request.operation_id(), "order.revision.accept");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("order_id")
+ .and_then(Value::as_str),
+ Some("ord_test")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("revision_id")
+ .and_then(Value::as_str),
+ Some("rev_test")
+ );
+
+ let revision_decline = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "revision",
+ "decline",
+ "ord_test",
+ "--revision-id",
+ "rev_test",
+ "--reason",
+ "keep original order",
+ ])
+ .expect("target args parse");
+ let request =
+ TargetOperationRequest::from_target_args(&revision_decline).expect("operation request");
+ let TargetOperationRequest::OrderRevisionDecline(request) = request else {
+ panic!("expected order revision decline request")
+ };
+ assert_eq!(request.operation_id(), "order.revision.decline");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("order_id")
+ .and_then(Value::as_str),
+ Some("ord_test")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("revision_id")
+ .and_then(Value::as_str),
+ Some("rev_test")
+ );
+ assert_eq!(
+ request.payload.input.get("reason").and_then(Value::as_str),
+ Some("keep original order")
+ );
+
+ let cancel = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "cancel",
+ "ord_test",
+ "--reason",
+ "changed plans",
+ ])
+ .expect("target args parse");
+ let request = TargetOperationRequest::from_target_args(&cancel).expect("operation request");
+ let TargetOperationRequest::OrderCancel(request) = request else {
+ panic!("expected order cancel request")
+ };
+ assert_eq!(request.operation_id(), "order.cancel");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("order_id")
+ .and_then(Value::as_str),
+ Some("ord_test")
+ );
+ assert_eq!(
+ request.payload.input.get("reason").and_then(Value::as_str),
+ Some("changed plans")
+ );
+
+ let receipt = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "receipt",
+ "record",
+ "ord_test",
+ "--issue",
+ "damaged items",
+ ])
+ .expect("target args parse");
+ let request =
+ TargetOperationRequest::from_target_args(&receipt).expect("operation request");
+ let TargetOperationRequest::OrderReceiptRecord(request) = request else {
+ panic!("expected order receipt record request")
+ };
+ assert_eq!(request.operation_id(), "order.receipt.record");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("order_id")
+ .and_then(Value::as_str),
+ Some("ord_test")
+ );
+ assert_eq!(
+ request.payload.input.get("issue").and_then(Value::as_str),
+ Some("damaged items")
+ );
+
+ let payment = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "payment",
+ "record",
+ "ord_test",
+ "--amount",
+ "12",
+ "--currency",
+ "USD",
+ "--method",
+ "manual_transfer",
+ "--reference",
+ "memo-1",
+ "--paid-at",
+ "1777666000",
+ ])
+ .expect("target args parse");
+ let request =
+ TargetOperationRequest::from_target_args(&payment).expect("operation request");
+ let TargetOperationRequest::OrderPaymentRecord(request) = request else {
+ panic!("expected order payment record request")
+ };
+ assert_eq!(request.operation_id(), "order.payment.record");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("order_id")
+ .and_then(Value::as_str),
+ Some("ord_test")
+ );
+ assert_eq!(
+ request.payload.input.get("amount").and_then(Value::as_str),
+ Some("12")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("currency")
+ .and_then(Value::as_str),
+ Some("USD")
+ );
+ assert_eq!(
+ request.payload.input.get("method").and_then(Value::as_str),
+ Some("manual_transfer")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("reference")
+ .and_then(Value::as_str),
+ Some("memo-1")
+ );
+ assert_eq!(
+ request.payload.input.get("paid_at").and_then(Value::as_u64),
+ Some(1_777_666_000)
+ );
+
+ let settlement = TargetCliArgs::try_parse_from([
+ "radroots",
+ "order",
+ "settlement",
+ "reject",
+ "ord_test",
+ "--payment-event-id",
+ "pay_event",
+ "--reason",
+ "reference mismatch",
+ ])
+ .expect("target args parse");
+ let request =
+ TargetOperationRequest::from_target_args(&settlement).expect("operation request");
+ let TargetOperationRequest::OrderSettlementReject(request) = request else {
+ panic!("expected order settlement reject request")
+ };
+ assert_eq!(request.operation_id(), "order.settlement.reject");
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("order_id")
+ .and_then(Value::as_str),
+ Some("ord_test")
+ );
+ assert_eq!(
+ request
+ .payload
+ .input
+ .get("payment_event_id")
+ .and_then(Value::as_str),
+ Some("pay_event")
+ );
+ assert_eq!(
+ request.payload.input.get("reason").and_then(Value::as_str),
+ Some("reference mismatch")
+ );
+ }
+
+ #[test]
+ fn typed_service_boundary_returns_enveloped_result() {
+ struct WorkspaceService;
+
+ impl OperationService<WorkspaceGetRequest> for WorkspaceService {
+ type Result = WorkspaceGetResult;
+
+ fn execute(
+ &self,
+ request: OperationRequest<WorkspaceGetRequest>,
+ ) -> Result<OperationResult<Self::Result>, super::OperationAdapterError> {
+ assert_eq!(request.operation_id(), "workspace.get");
+ OperationResult::new(WorkspaceGetResult::default())
+ }
+ }
+
+ let adapter = OperationAdapter::new(WorkspaceService);
+ let context = OperationContext::default();
+ let request = OperationRequest::new(context.clone(), WorkspaceGetRequest::default())
+ .expect("typed request");
+ let result = adapter.execute(request).expect("typed result");
+ let envelope = result
+ .to_envelope(context.envelope_context("req_test"))
+ .expect("operation envelope");
+
+ assert_eq!(envelope.operation_id, "workspace.get");
+ assert_eq!(envelope.kind, "workspace.get");
+ assert_eq!(envelope.request_id, "req_test");
+ assert_eq!(envelope.result, json!({}));
+ }
+
+ #[test]
+ fn approval_errors_map_to_structured_exit_code() {
+ let error = OperationAdapterError::approval_required("order.submit");
+ let output_error = error.to_output_error();
+
+ assert_eq!(output_error.code, "approval_required");
+ assert_eq!(output_error.exit_code, 6);
+ assert!(output_error.message.contains("approval_token"));
+ }
+
+ #[test]
+ fn not_implemented_errors_map_to_structured_exit_code() {
+ let error = OperationAdapterError::not_implemented(
+ "order.payment.record",
+ "coming soon".to_owned(),
+ );
+ let output_error = error.to_output_error();
+
+ assert_eq!(output_error.code, "not_implemented");
+ assert_eq!(output_error.exit_code, 3);
+ assert_eq!(
+ output_error.detail.expect("detail")["operation_id"],
+ "order.payment.record"
+ );
+ }
+
+ #[test]
+ fn runtime_failures_map_to_specific_machine_codes() {
+ let cases = [
+ (
+ OperationAdapterError::unconfigured(
+ "listing.publish",
+ "no selected account for seller write".to_owned(),
+ ),
+ "account_unresolved",
+ "account",
+ 5,
+ ),
+ (
+ OperationAdapterError::unconfigured(
+ "listing.publish",
+ "resolved account `a` is watch_only and cannot sign because it is not secret-backed"
+ .to_owned(),
+ ),
+ "account_watch_only",
+ "account",
+ 7,
+ ),
+ (
+ OperationAdapterError::unconfigured(
+ "listing.publish",
+ "account mismatch: resolved account pubkey `b` cannot sign listing seller_pubkey `a`"
+ .to_owned(),
+ ),
+ "account_mismatch",
+ "account",
+ 5,
+ ),
+ (
+ OperationAdapterError::unconfigured(
+ "listing.publish",
+ "signer.remote_nip46 binding is missing".to_owned(),
+ ),
+ "signer_unconfigured",
+ "signer",
+ 7,
+ ),
+ (
+ OperationAdapterError::unavailable(
+ "listing.publish",
+ "radrootsd bridge is unavailable".to_owned(),
+ ),
+ "provider_unavailable",
+ "provider",
+ 3,
+ ),
+ (
+ OperationAdapterError::SignerModeDeferred {
+ operation_id: "signer.status.get".to_owned(),
+ message: "signer mode `myc` is deferred".to_owned(),
+ },
+ "signer_mode_deferred",
+ "signer",
+ 7,
+ ),
+ (
+ OperationAdapterError::unconfigured(
+ "basket.quote.create",
+ "quote engine not ready".to_owned(),
+ ),
+ "operation_unavailable",
+ "operation",
+ 3,
+ ),
+ (
+ OperationAdapterError::runtime_failure(
+ "listing.publish",
+ RuntimeError::Io(io::Error::new(io::ErrorKind::NotFound, "missing draft")),
+ ),
+ "not_found",
+ "resource",
+ 4,
+ ),
+ (
+ OperationAdapterError::runtime_failure(
+ "listing.validate",
+ RuntimeError::Config("invalid listing draft listing.toml".to_owned()),
+ ),
+ "validation_failed",
+ "validation",
+ 10,
+ ),
+ (
+ OperationAdapterError::runtime_failure(
+ "listing.archive",
+ RuntimeError::Account(AccountRuntimeFailure::mismatch(
+ "account mismatch: resolved account pubkey `b` cannot sign listing seller_pubkey `a`",
+ )),
+ ),
+ "account_mismatch",
+ "account",
+ 5,
+ ),
+ (
+ OperationAdapterError::runtime_failure(
+ "farm.publish",
+ RuntimeError::Network("direct relay connection failed".to_owned()),
+ ),
+ "network_unavailable",
+ "network",
+ 8,
+ ),
+ ];
+
+ for (error, code, class, exit_code) in cases {
+ let output = error.to_output_error();
+ assert_eq!(output.code, code);
+ assert_eq!(output.exit_code, exit_code);
+ assert_eq!(
+ output.detail.expect("detail")["class"],
+ serde_json::Value::String(class.to_owned())
+ );
+ }
+ }
+}
diff --git a/src/output_contract.rs b/src/out/envelope.rs
diff --git a/src/out/mod.rs b/src/out/mod.rs
@@ -0,0 +1 @@
+pub mod envelope;
diff --git a/src/operation_registry.rs b/src/registry/mod.rs
diff --git a/src/runtime/accounts.rs b/src/runtime/accounts.rs
@@ -13,9 +13,9 @@ use radroots_secret_vault::{
RadrootsSecretVaultError, RadrootsSecretVaultOsKeyring,
};
-use crate::domain::runtime::{AccountResolutionView, AccountSummaryView};
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
+use crate::view::runtime::{AccountResolutionView, AccountSummaryView};
const HOST_VAULT_AVAILABILITY_OVERRIDE_ENV: &str = "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE";
const HOST_VAULT_SERVICE_NAME: &str = "org.radroots.cli.local-account";
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -13,10 +13,10 @@ use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend};
use serde::Deserialize;
use url::Url;
+use crate::cli::global::RuntimeInvocationArgs;
use crate::runtime::RuntimeError;
pub use crate::runtime::paths::PathsConfig;
use crate::runtime::paths::{ENV_CLI_PATHS_PROFILE, ENV_CLI_PATHS_REPO_LOCAL_ROOT, resolve_paths};
-use crate::runtime_args::RuntimeInvocationArgs;
const DEFAULT_LOG_FILTER: &str = "info";
const DEFAULT_ENV_PATH: &str = ".env";
@@ -1689,7 +1689,7 @@ mod tests {
PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfigSource,
RelayPublishPolicy, RuntimeConfig, SignerBackend, Verbosity, parse_env_file_values,
};
- use crate::runtime_args::{RuntimeInvocationArgs, RuntimeOutputFormatArg};
+ use crate::cli::global::{RuntimeInvocationArgs, RuntimeOutputFormatArg};
use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform};
use radroots_secret_vault::{RadrootsHostVaultPolicy, RadrootsSecretBackend};
use std::collections::BTreeMap;
diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs
@@ -15,11 +15,9 @@ use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_inges
use radroots_sql_core::SqliteExecutor;
use serde_json::json;
-use crate::domain::runtime::{
- FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView,
- FarmPublicationView, FarmPublishComponentView, FarmPublishEventView,
- FarmPublishLocalReplicaView, FarmPublishView, FarmRebindView, FarmSelectionView, FarmSetView,
- FarmSetupView, FarmStatusView, RelayFailureView,
+use crate::cli::global::{
+ FarmCreateArgs, FarmFieldArg, FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs,
+ FarmUpdateArgs,
};
use crate::runtime::RuntimeError;
use crate::runtime::accounts::{self, AccountRecordView};
@@ -39,9 +37,11 @@ use crate::runtime::local_events::{
mark_signed_event_failed_for_publish_error,
};
use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority};
-use crate::runtime_args::{
- FarmCreateArgs, FarmFieldArg, FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs,
- FarmUpdateArgs,
+use crate::view::runtime::{
+ FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView,
+ FarmPublicationView, FarmPublishComponentView, FarmPublishEventView,
+ FarmPublishLocalReplicaView, FarmPublishView, FarmRebindView, FarmSelectionView, FarmSetView,
+ FarmSetupView, FarmStatusView, RelayFailureView,
};
const FARM_CONFIG_SOURCE: &str = "farm config · local first";
diff --git a/src/runtime/find.rs b/src/runtime/find.rs
@@ -1,10 +1,7 @@
use radroots_replica_db::ReplicaSql;
use radroots_sql_core::SqliteExecutor;
-use crate::domain::runtime::{
- FindHyfView, FindPriceView, FindQuantityView, FindResultHyfView, FindResultProvenanceView,
- FindResultView, FindView, MarketReadinessView,
-};
+use crate::cli::global::FindQueryArgs;
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
use crate::runtime::hyf::{self, HyfQueryRewriteRequest, HyfRequestContext};
@@ -12,7 +9,10 @@ use crate::runtime::sync::{
RelayIngestScope, freshness_for_scope_from_executor, freshness_requires_refresh,
market_refresh, missing_freshness,
};
-use crate::runtime_args::FindQueryArgs;
+use crate::view::runtime::{
+ FindHyfView, FindPriceView, FindQuantityView, FindResultHyfView, FindResultProvenanceView,
+ FindResultView, FindView, MarketReadinessView,
+};
const FIND_SOURCE: &str = "local replica · local first";
const FIND_HYF_SOURCE: &str = "hyf query_rewrite · local first";
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -31,12 +31,9 @@ use radroots_trade::listing::validation::validate_listing_event;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
-use crate::domain::runtime::{
- FindPriceView, FindQuantityView, FindResultProvenanceView, ListingAppRecordExportView,
- ListingAppRecordListView, ListingAppRecordSummaryView, ListingGetView, ListingListView,
- ListingMutationEventView, ListingMutationLocalReplicaView, ListingMutationView, ListingNewView,
- ListingRebindView, ListingSummaryView, ListingValidateView, ListingValidationIssueView,
- MarketReadinessView, RelayFailureView,
+use crate::cli::global::{
+ ListingAppRecordExportArgs, ListingCreateArgs, ListingFileArgs, ListingMutationArgs,
+ ListingRebindArgs, RecordLookupArgs,
};
use crate::runtime::RuntimeError;
use crate::runtime::accounts;
@@ -57,9 +54,12 @@ use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authori
use crate::runtime::sync::{
RelayIngestScope, freshness_for_scope_from_executor, market_refresh, missing_freshness,
};
-use crate::runtime_args::{
- ListingAppRecordExportArgs, ListingCreateArgs, ListingFileArgs, ListingMutationArgs,
- ListingRebindArgs, RecordLookupArgs,
+use crate::view::runtime::{
+ FindPriceView, FindQuantityView, FindResultProvenanceView, ListingAppRecordExportView,
+ ListingAppRecordListView, ListingAppRecordSummaryView, ListingGetView, ListingListView,
+ ListingMutationEventView, ListingMutationLocalReplicaView, ListingMutationView, ListingNewView,
+ ListingRebindView, ListingSummaryView, ListingValidateView, ListingValidationIssueView,
+ MarketReadinessView, RelayFailureView,
};
const DRAFT_KIND: &str = "listing_draft_v1";
diff --git a/src/runtime/local.rs b/src/runtime/local.rs
@@ -8,14 +8,14 @@ use radroots_replica_sync::radroots_replica_sync_status;
use radroots_sql_core::SqliteExecutor;
use serde_json::json;
-use crate::domain::runtime::{
- LocalBackupView, LocalExportView, LocalInitView, LocalReplicaCountsView, LocalReplicaSyncView,
- LocalStatusView,
-};
+use crate::cli::global::LocalExportFormatArg;
use crate::runtime::RuntimeError;
use crate::runtime::config::RuntimeConfig;
use crate::runtime::sync::ensure_sync_run_table;
-use crate::runtime_args::LocalExportFormatArg;
+use crate::view::runtime::{
+ LocalBackupView, LocalExportView, LocalInitView, LocalReplicaCountsView, LocalReplicaSyncView,
+ LocalStatusView,
+};
const LOCAL_SOURCE: &str = "local replica · local first";
diff --git a/src/runtime/network.rs b/src/runtime/network.rs
@@ -1,5 +1,5 @@
-use crate::domain::runtime::{RelayEntryView, RelayListView};
use crate::runtime::config::RuntimeConfig;
+use crate::view::runtime::{RelayEntryView, RelayListView};
pub fn relay_list(config: &RuntimeConfig) -> RelayListView {
let relays = config
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -86,15 +86,12 @@ use radroots_trade::order::{
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
-use crate::domain::runtime::{
- OrderAppRecordExportView, OrderAppRecordListView, OrderAppRecordSummaryView,
- OrderCancellationView, OrderDecisionView, OrderDraftItemView, OrderEventListEntryView,
- OrderEventListView, OrderFulfillmentView, OrderGetView, OrderInventoryBinView,
- OrderInventoryView, OrderIssueView, OrderListView, OrderNewView, OrderPaymentView,
- OrderRebindView, OrderReceiptView, OrderRevisionDecisionView, OrderRevisionProposalView,
- OrderSettlementView, OrderStatusFulfillmentView, OrderStatusLifecycleCancellationView,
- OrderStatusLifecycleReceiptView, OrderStatusLifecycleView, OrderStatusPaymentView,
- OrderStatusRevisionView, OrderStatusView, OrderSubmitView, OrderSummaryView, RelayFailureView,
+use crate::cli::global::{
+ OrderAppRecordExportArgs, OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs,
+ OrderDraftCreateArgs, OrderFulfillmentArgs, OrderPaymentArgs, OrderRebindArgs,
+ OrderReceiptArgs, OrderRevisionDecisionArg, OrderRevisionDecisionArgs,
+ OrderRevisionProposeArgs, OrderSettlementArgs, OrderSettlementDecisionArg, OrderStatusArgs,
+ OrderSubmitArgs, RecordLookupArgs,
};
use crate::runtime::RuntimeError;
use crate::runtime::accounts;
@@ -112,12 +109,15 @@ use crate::runtime::sync::{
RelayIngestScope, freshness_for_scope, freshness_requires_refresh, market_refresh,
relay_provenance_relays_for_scope,
};
-use crate::runtime_args::{
- OrderAppRecordExportArgs, OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs,
- OrderDraftCreateArgs, OrderFulfillmentArgs, OrderPaymentArgs, OrderRebindArgs,
- OrderReceiptArgs, OrderRevisionDecisionArg, OrderRevisionDecisionArgs,
- OrderRevisionProposeArgs, OrderSettlementArgs, OrderSettlementDecisionArg, OrderStatusArgs,
- OrderSubmitArgs, RecordLookupArgs,
+use crate::view::runtime::{
+ OrderAppRecordExportView, OrderAppRecordListView, OrderAppRecordSummaryView,
+ OrderCancellationView, OrderDecisionView, OrderDraftItemView, OrderEventListEntryView,
+ OrderEventListView, OrderFulfillmentView, OrderGetView, OrderInventoryBinView,
+ OrderInventoryView, OrderIssueView, OrderListView, OrderNewView, OrderPaymentView,
+ OrderRebindView, OrderReceiptView, OrderRevisionDecisionView, OrderRevisionProposalView,
+ OrderSettlementView, OrderStatusFulfillmentView, OrderStatusLifecycleCancellationView,
+ OrderStatusLifecycleReceiptView, OrderStatusLifecycleView, OrderStatusPaymentView,
+ OrderStatusRevisionView, OrderStatusView, OrderSubmitView, OrderSummaryView, RelayFailureView,
};
const ORDER_DRAFT_KIND: &str = "order_draft_v1";
@@ -9104,7 +9104,7 @@ fn order_economics_from_resolved_listing(
order_id: &str,
resolved_listing: Option<&ResolvedOrderListing>,
items: &[OrderDraftItem],
- adjustments: &[crate::runtime_args::OrderDraftAdjustmentArgs],
+ adjustments: &[crate::cli::global::OrderDraftAdjustmentArgs],
) -> Result<Option<RadrootsTradeOrderEconomics>, RuntimeError> {
let Some(listing) = resolved_listing else {
return Ok(None);
@@ -9293,7 +9293,7 @@ fn listing_discount_amount(
}
fn basket_adjustment_lines(
- adjustments: &[crate::runtime_args::OrderDraftAdjustmentArgs],
+ adjustments: &[crate::cli::global::OrderDraftAdjustmentArgs],
) -> Result<Vec<RadrootsTradeOrderEconomicLine>, RuntimeError> {
adjustments
.iter()
@@ -12625,6 +12625,12 @@ mod tests {
resolve_local_order_fulfillment_signing_identity,
seller_order_request_resolution_from_receipt,
};
+ use crate::cli::global::{
+ OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs, OrderDraftAdjustmentArgs,
+ OrderFulfillmentArgs, OrderPaymentArgs, OrderReceiptArgs, OrderRevisionDecisionArg,
+ OrderRevisionDecisionArgs, OrderRevisionProposeArgs, OrderSettlementArgs,
+ OrderSettlementDecisionArg, OrderSubmitArgs,
+ };
use crate::runtime::accounts;
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
@@ -12633,12 +12639,6 @@ mod tests {
RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
};
use crate::runtime::direct_relay::DirectRelayFetchReceipt;
- use crate::runtime_args::{
- OrderCancelArgs, OrderDecisionArg, OrderDecisionArgs, OrderDraftAdjustmentArgs,
- OrderFulfillmentArgs, OrderPaymentArgs, OrderReceiptArgs, OrderRevisionDecisionArg,
- OrderRevisionDecisionArgs, OrderRevisionProposeArgs, OrderSettlementArgs,
- OrderSettlementDecisionArg, OrderSubmitArgs,
- };
#[test]
fn generated_order_id_uses_stable_prefix() {
@@ -13576,7 +13576,7 @@ mod tests {
assert_eq!(view.state, "declined");
assert_eq!(
view.disposition(),
- crate::domain::runtime::CommandDisposition::ValidationFailed
+ crate::view::runtime::CommandDisposition::ValidationFailed
);
assert_eq!(
view.decision_event_id.as_deref(),
@@ -17335,7 +17335,7 @@ mod tests {
assert_eq!(view.state, "terminal");
assert_eq!(
view.disposition(),
- crate::domain::runtime::CommandDisposition::ValidationFailed
+ crate::view::runtime::CommandDisposition::ValidationFailed
);
assert_eq!(
view.prev_event_id.as_deref(),
@@ -17733,7 +17733,7 @@ mod tests {
assert_eq!(view.state, "terminal");
assert_eq!(
view.disposition(),
- crate::domain::runtime::CommandDisposition::ValidationFailed
+ crate::view::runtime::CommandDisposition::ValidationFailed
);
assert_eq!(view.event_id.as_deref(), Some(decision_event_id.as_str()));
assert_eq!(view.event_kind, Some(KIND_TRADE_ORDER_DECISION));
diff --git a/src/runtime/provider.rs b/src/runtime/provider.rs
@@ -1,4 +1,3 @@
-use crate::domain::runtime::PublishRuntimeView;
#[cfg(test)]
use crate::runtime::config::{
CapabilityBindingInspection, CapabilityBindingInspectionState, INFERENCE_HYF_STDIO_CAPABILITY,
@@ -6,6 +5,7 @@ use crate::runtime::config::{
use crate::runtime::config::{PublishMode, RuntimeConfig};
#[cfg(test)]
use crate::runtime::hyf;
+use crate::view::runtime::PublishRuntimeView;
#[cfg(test)]
const WRITE_PLANE_TARGET_DETAIL: &str =
@@ -264,9 +264,6 @@ mod tests {
ProviderProvenance, resolve_actor_write_plane_target, resolve_capability_providers,
resolve_hyf_provider, resolve_write_plane_provider,
};
- use crate::domain::runtime::{
- PublishProviderRuntimeView, PublishRelayRuntimeView, PublishRuntimeView,
- };
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, CapabilityBindingConfig,
CapabilityBindingSource, CapabilityBindingTargetKind, HyfConfig, IdentityConfig,
@@ -275,6 +272,9 @@ mod tests {
RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
SignerConfig, Verbosity,
};
+ use crate::view::runtime::{
+ PublishProviderRuntimeView, PublishRelayRuntimeView, PublishRuntimeView,
+ };
fn sample_config(bindings: Vec<CapabilityBindingConfig>, hyf_enabled: bool) -> RuntimeConfig {
RuntimeConfig {
diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs
@@ -1,11 +1,11 @@
-use crate::domain::runtime::{
- IdentityPublicView, LocalSignerStatusView, SignerBindingStatusView, SignerStatusView,
- SignerWriteKindReadinessView,
-};
use crate::runtime::RuntimeError;
use crate::runtime::accounts::AccountRuntimeFailure;
use crate::runtime::accounts::{SHARED_ACCOUNT_STORE_SOURCE, empty_account_resolution_view};
use crate::runtime::config::{RuntimeConfig, SIGNER_REMOTE_NIP46_CAPABILITY, SignerBackend};
+use crate::view::runtime::{
+ IdentityPublicView, LocalSignerStatusView, SignerBindingStatusView, SignerStatusView,
+ SignerWriteKindReadinessView,
+};
use radroots_events::kinds::{
KIND_FARM, KIND_LISTING, KIND_PROFILE, KIND_TRADE_CANCEL, KIND_TRADE_FULFILLMENT_UPDATE,
KIND_TRADE_ORDER_DECISION, KIND_TRADE_ORDER_REQUEST, KIND_TRADE_ORDER_REVISION,
diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs
@@ -24,11 +24,7 @@ use radroots_sql_core::{SqlExecutor, SqliteExecutor};
use serde::Deserialize;
use serde_json::json;
-use crate::domain::runtime::{
- RelayFailureView, SyncActionView, SyncFreshnessView, SyncPublishPlanAuthorView,
- SyncPublishPlanKindView, SyncPublishPlanView, SyncQueueView, SyncRunFreshnessView,
- SyncStatusView, SyncWatchFrameView, SyncWatchView,
-};
+use crate::cli::global::SyncWatchArgs;
use crate::runtime::RuntimeError;
use crate::runtime::accounts;
use crate::runtime::config::{PublishMode, RuntimeConfig};
@@ -36,7 +32,11 @@ use crate::runtime::direct_relay::{
DirectRelayFailure, DirectRelayFetchError, DirectRelayFetchReceipt, DirectRelayPublishError,
DirectRelayPublishReceipt, fetch_events_from_relays, publish_parts_with_identity,
};
-use crate::runtime_args::SyncWatchArgs;
+use crate::view::runtime::{
+ RelayFailureView, SyncActionView, SyncFreshnessView, SyncPublishPlanAuthorView,
+ SyncPublishPlanKindView, SyncPublishPlanView, SyncQueueView, SyncRunFreshnessView,
+ SyncStatusView, SyncWatchFrameView, SyncWatchView,
+};
const SYNC_SOURCE: &str = "local replica · local first";
const RELAY_PULL_SETUP_ACTION: &str = "radroots --relay wss://relay.example.com sync pull";
@@ -1615,13 +1615,13 @@ mod tests {
DirectRelayPublishReceipt, RelayIngestScope, market_refresh_with_fetcher,
pull_with_fetcher, push_with_publisher, relay_provenance_relays_for_scope, status,
};
+ use crate::cli::global::{FindQueryArgs, RecordLookupArgs};
use crate::runtime::config::{
AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
PathsConfig, PublishConfig, PublishMode, PublishModeSource, RelayConfig, RelayConfigSource,
RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity,
};
- use crate::runtime_args::{FindQueryArgs, RecordLookupArgs};
const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
const PLOT_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ";
diff --git a/src/runtime/validation_receipt.rs b/src/runtime/validation_receipt.rs
@@ -20,11 +20,11 @@ use radroots_trade::validation_receipt::{
};
use serde::{Deserialize, Serialize};
-use crate::domain::runtime::{CommandDisposition, RelayFailureView};
use crate::runtime::config::RuntimeConfig;
use crate::runtime::direct_relay::{
DirectRelayFailure, DirectRelayFetchError, DirectRelayFetchReceipt, fetch_events_from_relays,
};
+use crate::view::runtime::{CommandDisposition, RelayFailureView};
#[derive(Debug, Clone)]
pub struct ValidationReceiptEventArgs {
diff --git a/src/target_cli.rs b/src/target_cli.rs
@@ -1,1719 +0,0 @@
-#![allow(dead_code)]
-
-use std::path::PathBuf;
-
-use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
-pub enum TargetOutputFormat {
- Human,
- Json,
- Ndjson,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
-pub enum TargetPublishMode {
- #[value(name = "nostr_relay")]
- NostrRelay,
- Radrootsd,
-}
-
-impl TargetPublishMode {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::NostrRelay => "nostr_relay",
- Self::Radrootsd => "radrootsd",
- }
- }
-}
-
-#[derive(Debug, Parser, Clone)]
-#[command(
- name = "radroots",
- about = "Operate Radroots local-first trade workflows.",
- long_about = "Operate Radroots local-first trade workflows.\n\nPublish modes:\n nostr_relay uses direct relay publish with local signer custody.\n radrootsd is reserved and fails closed for active buyer and seller writes.\n\nRelay mode never silently falls back to radrootsd.",
- disable_help_subcommand = true
-)]
-pub struct TargetCliArgs {
- #[arg(long = "format", global = true, value_enum, default_value = "human")]
- pub format: TargetOutputFormat,
- #[arg(long = "account-id", global = true)]
- pub account_id: Option<String>,
- #[arg(long = "relay", global = true)]
- pub relay: Vec<String>,
- #[arg(
- long = "publish-mode",
- global = true,
- value_enum,
- help = "Select nostr_relay direct relay publish or reserved radrootsd guardrail mode"
- )]
- pub publish_mode: Option<TargetPublishMode>,
- #[arg(long = "offline", global = true, action = ArgAction::SetTrue, conflicts_with = "online")]
- pub offline: bool,
- #[arg(long = "online", global = true, action = ArgAction::SetTrue, conflicts_with = "offline")]
- pub online: bool,
- #[arg(long = "dry-run", global = true, action = ArgAction::SetTrue)]
- pub dry_run: bool,
- #[arg(long = "idempotency-key", global = true)]
- pub idempotency_key: Option<String>,
- #[arg(long = "correlation-id", global = true)]
- pub correlation_id: Option<String>,
- #[arg(long = "approval-token", global = true)]
- pub approval_token: Option<String>,
- #[arg(long = "no-input", global = true, action = ArgAction::SetTrue)]
- pub no_input: bool,
- #[arg(long = "quiet", global = true, action = ArgAction::SetTrue)]
- pub quiet: bool,
- #[arg(long = "verbose", global = true, action = ArgAction::SetTrue)]
- pub verbose: bool,
- #[arg(long = "trace", global = true, action = ArgAction::SetTrue)]
- pub trace: bool,
- #[arg(long = "no-color", global = true, action = ArgAction::SetTrue)]
- pub no_color: bool,
- #[command(subcommand)]
- pub command: TargetCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum TargetCommand {
- #[command(about = "Inspect and initialize workspace state.")]
- Workspace(WorkspaceArgs),
- #[command(about = "Inspect local readiness and mode-specific recovery steps.")]
- Health(HealthArgs),
- #[command(about = "Show effective configuration and publish-plane readiness.")]
- Config(ConfigArgs),
- #[command(about = "Manage local signer accounts and custody.")]
- Account(AccountArgs),
- #[command(about = "Inspect signer readiness for local relay writes.")]
- Signer(SignerArgs),
- #[command(about = "List configured relay targets for direct relay mode.")]
- Relay(RelayArgs),
- #[command(about = "Initialize and inspect the local replica store.")]
- Store(StoreArgs),
- #[command(about = "Read from relay events into the local replica.")]
- Sync(SyncArgs),
- #[command(about = "Create, inspect, and publish farm profile data.")]
- Farm(FarmArgs),
- #[command(about = "Create, inspect, and publish listing data.")]
- Listing(ListingArgs),
- #[command(about = "Refresh and query market data from the local replica.")]
- Market(MarketArgs),
- #[command(about = "Prepare baskets and quotes before order coordination.")]
- Basket(BasketArgs),
- #[command(about = "Coordinate order lifecycle events without payments.")]
- Order(OrderArgs),
- #[command(about = "Inspect validation receipts and proof state.")]
- Validation(ValidationArgs),
-}
-
-impl TargetCommand {
- pub fn operation_id(&self) -> &'static str {
- match self {
- Self::Workspace(args) => match args.command {
- WorkspaceCommand::Init => "workspace.init",
- WorkspaceCommand::Get => "workspace.get",
- },
- Self::Health(args) => match &args.command {
- HealthCommand::Status(status) => match status.command {
- HealthStatusCommand::Get => "health.status.get",
- },
- HealthCommand::Check(check) => match check.command {
- HealthCheckCommand::Run => "health.check.run",
- },
- },
- Self::Config(args) => match args.command {
- ConfigCommand::Get => "config.get",
- },
- Self::Account(args) => match &args.command {
- AccountCommand::Create => "account.create",
- AccountCommand::Import(_) => "account.import",
- AccountCommand::AttachSecret(_) => "account.attach_secret",
- AccountCommand::Get(_) => "account.get",
- AccountCommand::List => "account.list",
- AccountCommand::Remove(_) => "account.remove",
- AccountCommand::Selection(selection) => match &selection.command {
- AccountSelectionCommand::Get => "account.selection.get",
- AccountSelectionCommand::Update(_) => "account.selection.update",
- AccountSelectionCommand::Clear => "account.selection.clear",
- },
- },
- Self::Signer(args) => match &args.command {
- SignerCommand::Status(status) => match status.command {
- SignerStatusCommand::Get => "signer.status.get",
- },
- },
- Self::Relay(args) => match args.command {
- RelayCommand::List => "relay.list",
- },
- Self::Store(args) => match &args.command {
- StoreCommand::Init => "store.init",
- StoreCommand::Status(status) => match status.command {
- StoreStatusCommand::Get => "store.status.get",
- },
- StoreCommand::Export => "store.export",
- StoreCommand::Backup(backup) => match backup.command {
- StoreBackupCommand::Create => "store.backup.create",
- },
- },
- Self::Sync(args) => match &args.command {
- SyncCommand::Status(status) => match status.command {
- SyncStatusCommand::Get => "sync.status.get",
- },
- SyncCommand::Pull => "sync.pull",
- SyncCommand::Push => "sync.push",
- SyncCommand::Watch => "sync.watch",
- },
- Self::Farm(args) => match &args.command {
- FarmCommand::Create(_) => "farm.create",
- FarmCommand::Get => "farm.get",
- FarmCommand::Rebind(_) => "farm.rebind",
- FarmCommand::Profile(profile) => match profile.command {
- FarmProfileCommand::Update(_) => "farm.profile.update",
- },
- FarmCommand::Location(location) => match location.command {
- FarmLocationCommand::Update(_) => "farm.location.update",
- },
- FarmCommand::Fulfillment(fulfillment) => match fulfillment.command {
- FarmFulfillmentCommand::Update(_) => "farm.fulfillment.update",
- },
- FarmCommand::Readiness(readiness) => match readiness.command {
- FarmReadinessCommand::Check => "farm.readiness.check",
- },
- FarmCommand::Publish => "farm.publish",
- },
- Self::Listing(args) => match &args.command {
- ListingCommand::Create(_) => "listing.create",
- ListingCommand::Get(_) => "listing.get",
- ListingCommand::List => "listing.list",
- ListingCommand::App(app) => match &app.command {
- ListingAppCommand::List => "listing.app.list",
- ListingAppCommand::Export(_) => "listing.app.export",
- },
- ListingCommand::Update(_) => "listing.update",
- ListingCommand::Validate(_) => "listing.validate",
- ListingCommand::Rebind(_) => "listing.rebind",
- ListingCommand::Publish(_) => "listing.publish",
- ListingCommand::Archive(_) => "listing.archive",
- },
- Self::Market(args) => match &args.command {
- MarketCommand::Refresh => "market.refresh",
- MarketCommand::Product(product) => match &product.command {
- MarketProductCommand::Search(_) => "market.product.search",
- },
- MarketCommand::Listing(listing) => match &listing.command {
- MarketListingCommand::Get(_) => "market.listing.get",
- },
- },
- Self::Basket(args) => match &args.command {
- BasketCommand::Create(_) => "basket.create",
- BasketCommand::Get(_) => "basket.get",
- BasketCommand::List => "basket.list",
- BasketCommand::Item(item) => match item.command {
- BasketItemCommand::Add(_) => "basket.item.add",
- BasketItemCommand::Update(_) => "basket.item.update",
- BasketItemCommand::Remove(_) => "basket.item.remove",
- },
- BasketCommand::Adjustment(adjustment) => match &adjustment.command {
- BasketAdjustmentCommand::Add(_) => "basket.adjustment.add",
- BasketAdjustmentCommand::Remove(_) => "basket.adjustment.remove",
- },
- BasketCommand::Validate(_) => "basket.validate",
- BasketCommand::Quote(quote) => match quote.command {
- BasketQuoteCommand::Create(_) => "basket.quote.create",
- },
- },
- Self::Order(args) => match &args.command {
- OrderCommand::Submit(_) => "order.submit",
- OrderCommand::Get(_) => "order.get",
- OrderCommand::List => "order.list",
- OrderCommand::App(app) => match &app.command {
- OrderAppCommand::List => "order.app.list",
- OrderAppCommand::Export(_) => "order.app.export",
- },
- OrderCommand::Rebind(_) => "order.rebind",
- OrderCommand::Accept(_) => "order.accept",
- OrderCommand::Decline(_) => "order.decline",
- OrderCommand::Cancel(_) => "order.cancel",
- OrderCommand::Revision(revision) => match &revision.command {
- OrderRevisionCommand::Propose(_) => "order.revision.propose",
- OrderRevisionCommand::Accept(_) => "order.revision.accept",
- OrderRevisionCommand::Decline(_) => "order.revision.decline",
- },
- OrderCommand::Fulfillment(fulfillment) => match &fulfillment.command {
- OrderFulfillmentCommand::Update(_) => "order.fulfillment.update",
- },
- OrderCommand::Receipt(receipt) => match &receipt.command {
- OrderReceiptCommand::Record(_) => "order.receipt.record",
- },
- OrderCommand::Payment(payment) => match &payment.command {
- OrderPaymentCommand::Record(_) => "order.payment.record",
- },
- OrderCommand::Settlement(settlement) => match &settlement.command {
- OrderSettlementCommand::Accept(_) => "order.settlement.accept",
- OrderSettlementCommand::Reject(_) => "order.settlement.reject",
- },
- OrderCommand::Status(status) => match &status.command {
- OrderStatusCommand::Get(_) => "order.status.get",
- },
- OrderCommand::Event(event) => match &event.command {
- OrderEventCommand::List(_) => "order.event.list",
- OrderEventCommand::Watch(_) => "order.event.watch",
- },
- },
- Self::Validation(args) => match &args.command {
- ValidationCommand::Receipt(receipt) => match &receipt.command {
- ValidationReceiptCommand::Get(_) => "validation.receipt.get",
- ValidationReceiptCommand::List(_) => "validation.receipt.list",
- ValidationReceiptCommand::Verify(_) => "validation.receipt.verify",
- },
- },
- }
- }
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct WorkspaceArgs {
- #[command(subcommand)]
- pub command: WorkspaceCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum WorkspaceCommand {
- Init,
- Get,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct HealthArgs {
- #[command(subcommand)]
- pub command: HealthCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum HealthCommand {
- Status(HealthStatusArgs),
- Check(HealthCheckArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct HealthStatusArgs {
- #[command(subcommand)]
- pub command: HealthStatusCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum HealthStatusCommand {
- Get,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct HealthCheckArgs {
- #[command(subcommand)]
- pub command: HealthCheckCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum HealthCheckCommand {
- Run,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ConfigArgs {
- #[command(subcommand)]
- pub command: ConfigCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum ConfigCommand {
- Get,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct AccountArgs {
- #[command(subcommand)]
- pub command: AccountCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum AccountCommand {
- Create,
- Import(AccountImportArgs),
- AttachSecret(AccountAttachSecretArgs),
- Get(AccountGetArgs),
- List,
- Remove(AccountSelectorArgs),
- Selection(AccountSelectionArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct AccountImportArgs {
- pub path: Option<PathBuf>,
- #[arg(long, action = clap::ArgAction::SetTrue)]
- pub default: bool,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct AccountAttachSecretArgs {
- pub selector: Option<String>,
- pub path: Option<PathBuf>,
- #[arg(long, action = clap::ArgAction::SetTrue)]
- pub default: bool,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct AccountGetArgs {
- pub selector: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct AccountSelectorArgs {
- pub selector: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct AccountSelectionArgs {
- #[command(subcommand)]
- pub command: AccountSelectionCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum AccountSelectionCommand {
- Get,
- Update(AccountSelectorArgs),
- Clear,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct SignerArgs {
- #[command(subcommand)]
- pub command: SignerCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum SignerCommand {
- Status(SignerStatusArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct SignerStatusArgs {
- #[command(subcommand)]
- pub command: SignerStatusCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum SignerStatusCommand {
- Get,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct RelayArgs {
- #[command(subcommand)]
- pub command: RelayCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum RelayCommand {
- List,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct StoreArgs {
- #[command(subcommand)]
- pub command: StoreCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum StoreCommand {
- Init,
- Status(StoreStatusArgs),
- Export,
- Backup(StoreBackupArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct StoreStatusArgs {
- #[command(subcommand)]
- pub command: StoreStatusCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum StoreStatusCommand {
- Get,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct StoreBackupArgs {
- #[command(subcommand)]
- pub command: StoreBackupCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum StoreBackupCommand {
- Create,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct SyncArgs {
- #[command(subcommand)]
- pub command: SyncCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum SyncCommand {
- Status(SyncStatusArgs),
- Pull,
- Push,
- Watch,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct SyncStatusArgs {
- #[command(subcommand)]
- pub command: SyncStatusCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum SyncStatusCommand {
- Get,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmArgs {
- #[command(subcommand)]
- pub command: FarmCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum FarmCommand {
- Create(FarmCreateArgs),
- Get,
- Rebind(FarmRebindArgs),
- Profile(FarmProfileArgs),
- Location(FarmLocationArgs),
- Fulfillment(FarmFulfillmentArgs),
- Readiness(FarmReadinessArgs),
- Publish,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmCreateArgs {
- #[arg(long = "farm-d-tag")]
- pub farm_d_tag: Option<String>,
- #[arg(long)]
- pub name: Option<String>,
- #[arg(long = "display-name")]
- pub display_name: Option<String>,
- #[arg(long)]
- pub about: Option<String>,
- #[arg(long)]
- pub website: Option<String>,
- #[arg(long)]
- pub picture: Option<String>,
- #[arg(long)]
- pub banner: Option<String>,
- #[arg(long)]
- pub location: Option<String>,
- #[arg(long)]
- pub city: Option<String>,
- #[arg(long)]
- pub region: Option<String>,
- #[arg(long)]
- pub country: Option<String>,
- #[arg(long = "delivery-method")]
- pub delivery_method: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmRebindArgs {
- pub selector: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmProfileArgs {
- #[command(subcommand)]
- pub command: FarmProfileCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum FarmProfileCommand {
- Update(FarmProfileUpdateArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmProfileUpdateArgs {
- #[arg(long)]
- pub field: Option<String>,
- #[arg(long)]
- pub value: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmLocationArgs {
- #[command(subcommand)]
- pub command: FarmLocationCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum FarmLocationCommand {
- Update(FarmLocationUpdateArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmLocationUpdateArgs {
- #[arg(long)]
- pub field: Option<String>,
- #[arg(long)]
- pub value: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmFulfillmentArgs {
- #[command(subcommand)]
- pub command: FarmFulfillmentCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum FarmFulfillmentCommand {
- Update(FarmFulfillmentUpdateArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmFulfillmentUpdateArgs {
- #[arg(long)]
- pub value: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FarmReadinessArgs {
- #[command(subcommand)]
- pub command: FarmReadinessCommand,
-}
-
-#[derive(Debug, Clone, Copy, Subcommand)]
-pub enum FarmReadinessCommand {
- Check,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ListingArgs {
- #[command(subcommand)]
- pub command: ListingCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum ListingCommand {
- Create(ListingCreateArgs),
- Get(LookupArgs),
- List,
- App(ListingAppArgs),
- Update(FileArgs),
- Validate(FileArgs),
- Rebind(ListingRebindArgs),
- Publish(FileArgs),
- Archive(FileArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ListingCreateArgs {
- #[arg(long)]
- pub output: Option<PathBuf>,
- #[arg(long)]
- pub key: Option<String>,
- #[arg(long)]
- pub title: Option<String>,
- #[arg(long)]
- pub category: Option<String>,
- #[arg(long)]
- pub summary: Option<String>,
- #[arg(long = "bin-id")]
- pub bin_id: Option<String>,
- #[arg(long = "quantity-amount")]
- pub quantity_amount: Option<String>,
- #[arg(long = "quantity-unit")]
- pub quantity_unit: Option<String>,
- #[arg(long = "price-amount")]
- pub price_amount: Option<String>,
- #[arg(long = "price-currency")]
- pub price_currency: Option<String>,
- #[arg(long = "price-per-amount")]
- pub price_per_amount: Option<String>,
- #[arg(long = "price-per-unit")]
- pub price_per_unit: Option<String>,
- #[arg(long)]
- pub available: Option<String>,
- #[arg(long)]
- pub label: Option<String>,
- #[arg(long = "discount-id")]
- pub discount_id: Option<String>,
- #[arg(long = "discount-label")]
- pub discount_label: Option<String>,
- #[arg(long = "discount-kind")]
- pub discount_kind: Option<String>,
- #[arg(long = "discount-value")]
- pub discount_value: Option<String>,
- #[arg(long = "discount-amount")]
- pub discount_amount: Option<String>,
- #[arg(long = "discount-currency")]
- pub discount_currency: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct FileArgs {
- pub file: Option<PathBuf>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ListingAppArgs {
- #[command(subcommand)]
- pub command: ListingAppCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum ListingAppCommand {
- List,
- Export(ListingAppExportArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ListingAppExportArgs {
- pub record_id: Option<String>,
- #[arg(long)]
- pub output: Option<PathBuf>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ListingRebindArgs {
- pub file: Option<PathBuf>,
- pub selector: Option<String>,
- #[arg(long = "farm-d-tag")]
- pub farm_d_tag: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct LookupArgs {
- pub key: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct MarketArgs {
- #[command(subcommand)]
- pub command: MarketCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum MarketCommand {
- Refresh,
- Product(MarketProductArgs),
- Listing(MarketListingArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct MarketProductArgs {
- #[command(subcommand)]
- pub command: MarketProductCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum MarketProductCommand {
- Search(QueryArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct MarketListingArgs {
- #[command(subcommand)]
- pub command: MarketListingCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum MarketListingCommand {
- Get(LookupArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct QueryArgs {
- pub query: Vec<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketArgs {
- #[command(subcommand)]
- pub command: BasketCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum BasketCommand {
- Create(BasketCreateArgs),
- Get(BasketKeyArgs),
- List,
- Item(BasketItemArgs),
- Adjustment(BasketAdjustmentArgs),
- Validate(BasketKeyArgs),
- Quote(BasketQuoteArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketCreateArgs {
- pub basket_id: Option<String>,
- #[arg(long)]
- pub listing: Option<String>,
- #[arg(long = "listing-addr")]
- pub listing_addr: Option<String>,
- #[arg(long = "bin-id")]
- pub bin_id: Option<String>,
- #[arg(long)]
- pub quantity: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketKeyArgs {
- pub basket_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketItemArgs {
- #[command(subcommand)]
- pub command: BasketItemCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum BasketItemCommand {
- Add(BasketItemMutationArgs),
- Update(BasketItemMutationArgs),
- Remove(BasketItemRemoveArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketAdjustmentArgs {
- #[command(subcommand)]
- pub command: BasketAdjustmentCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum BasketAdjustmentCommand {
- Add(BasketAdjustmentAddArgs),
- Remove(BasketAdjustmentRemoveArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketAdjustmentAddArgs {
- pub basket_id: Option<String>,
- #[arg(long)]
- pub id: Option<String>,
- #[arg(long)]
- pub effect: Option<String>,
- #[arg(long)]
- pub amount: Option<String>,
- #[arg(long)]
- pub currency: Option<String>,
- #[arg(long)]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketAdjustmentRemoveArgs {
- pub basket_id: Option<String>,
- #[arg(long)]
- pub id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketItemMutationArgs {
- pub basket_id: Option<String>,
- #[arg(long = "item-id")]
- pub item_id: Option<String>,
- #[arg(long)]
- pub listing: Option<String>,
- #[arg(long = "listing-addr")]
- pub listing_addr: Option<String>,
- #[arg(long = "bin-id")]
- pub bin_id: Option<String>,
- #[arg(long)]
- pub quantity: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketItemRemoveArgs {
- pub basket_id: Option<String>,
- pub item_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct BasketQuoteArgs {
- #[command(subcommand)]
- pub command: BasketQuoteCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum BasketQuoteCommand {
- Create(BasketKeyArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderArgs {
- #[command(subcommand)]
- pub command: OrderCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum OrderCommand {
- Submit(OrderSubmitArgs),
- Get(OrderKeyArgs),
- List,
- App(OrderAppArgs),
- Rebind(OrderRebindArgs),
- Accept(OrderKeyArgs),
- Decline(OrderDeclineArgs),
- Cancel(OrderCancelArgs),
- Revision(OrderRevisionArgs),
- Fulfillment(OrderFulfillmentArgs),
- Receipt(OrderReceiptArgs),
- Payment(OrderPaymentArgs),
- Settlement(OrderSettlementArgs),
- Status(OrderStatusArgs),
- Event(OrderEventArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderSubmitArgs {
- pub order_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderKeyArgs {
- pub order_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderAppArgs {
- #[command(subcommand)]
- pub command: OrderAppCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum OrderAppCommand {
- List,
- Export(OrderAppExportArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderAppExportArgs {
- pub record_id: Option<String>,
- #[arg(long)]
- pub output: Option<PathBuf>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderRebindArgs {
- pub order_id: Option<String>,
- pub selector: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderDeclineArgs {
- pub order_id: Option<String>,
- #[arg(long)]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderCancelArgs {
- pub order_id: Option<String>,
- #[arg(long)]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderRevisionArgs {
- #[command(subcommand)]
- pub command: OrderRevisionCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum OrderRevisionCommand {
- Propose(OrderRevisionProposeArgs),
- Accept(OrderRevisionDecisionArgs),
- Decline(OrderRevisionDeclineArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderRevisionProposeArgs {
- pub order_id: Option<String>,
- #[arg(long)]
- pub reason: Option<String>,
- #[arg(long)]
- pub bin_id: Option<String>,
- #[arg(long)]
- pub bin_count: Option<u32>,
- #[arg(long)]
- pub adjustment_id: Option<String>,
- #[arg(long)]
- pub adjustment_effect: Option<String>,
- #[arg(long)]
- pub adjustment_amount: Option<String>,
- #[arg(long)]
- pub adjustment_currency: Option<String>,
- #[arg(long)]
- pub adjustment_reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderRevisionDecisionArgs {
- pub order_id: Option<String>,
- #[arg(long)]
- pub revision_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderRevisionDeclineArgs {
- pub order_id: Option<String>,
- #[arg(long)]
- pub revision_id: Option<String>,
- #[arg(long)]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderFulfillmentArgs {
- #[command(subcommand)]
- pub command: OrderFulfillmentCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum OrderFulfillmentCommand {
- Update(OrderFulfillmentUpdateArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderFulfillmentUpdateArgs {
- pub order_id: Option<String>,
- #[arg(long, value_enum)]
- pub state: Option<OrderFulfillmentStateArg>,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
-#[value(rename_all = "snake_case")]
-pub enum OrderFulfillmentStateArg {
- Preparing,
- ReadyForPickup,
- OutForDelivery,
- Delivered,
- SellerCancelled,
-}
-
-impl OrderFulfillmentStateArg {
- pub const fn as_protocol_state(self) -> &'static str {
- match self {
- Self::Preparing => "preparing",
- Self::ReadyForPickup => "ready_for_pickup",
- Self::OutForDelivery => "out_for_delivery",
- Self::Delivered => "delivered",
- Self::SellerCancelled => "seller_cancelled",
- }
- }
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderReceiptArgs {
- #[command(subcommand)]
- pub command: OrderReceiptCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum OrderReceiptCommand {
- Record(OrderReceiptRecordArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderReceiptRecordArgs {
- pub order_id: Option<String>,
- #[arg(long, action = ArgAction::SetTrue, conflicts_with = "issue")]
- pub received: bool,
- #[arg(long)]
- pub issue: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderPaymentArgs {
- #[command(subcommand)]
- pub command: OrderPaymentCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum OrderPaymentCommand {
- Record(OrderPaymentRecordArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderPaymentRecordArgs {
- pub order_id: Option<String>,
- #[arg(long)]
- pub amount: Option<String>,
- #[arg(long)]
- pub currency: Option<String>,
- #[arg(long)]
- pub method: Option<String>,
- #[arg(long)]
- pub reference: Option<String>,
- #[arg(long)]
- pub paid_at: Option<u64>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderSettlementArgs {
- #[command(subcommand)]
- pub command: OrderSettlementCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum OrderSettlementCommand {
- Accept(OrderSettlementAcceptArgs),
- Reject(OrderSettlementRejectArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderSettlementAcceptArgs {
- pub order_id: Option<String>,
- #[arg(long)]
- pub payment_event_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderSettlementRejectArgs {
- pub order_id: Option<String>,
- #[arg(long)]
- pub payment_event_id: Option<String>,
- #[arg(long)]
- pub reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderStatusArgs {
- #[command(subcommand)]
- pub command: OrderStatusCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum OrderStatusCommand {
- Get(OrderKeyArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct OrderEventArgs {
- #[command(subcommand)]
- pub command: OrderEventCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum OrderEventCommand {
- List(OrderKeyArgs),
- Watch(OrderKeyArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ValidationArgs {
- #[command(subcommand)]
- pub command: ValidationCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum ValidationCommand {
- Receipt(ValidationReceiptArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ValidationReceiptArgs {
- #[command(subcommand)]
- pub command: ValidationReceiptCommand,
-}
-
-#[derive(Debug, Clone, Subcommand)]
-pub enum ValidationReceiptCommand {
- Get(ValidationReceiptEventArgs),
- List(ValidationReceiptListArgs),
- Verify(ValidationReceiptEventArgs),
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ValidationReceiptEventArgs {
- pub receipt_event_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct ValidationReceiptListArgs {
- #[arg(long)]
- pub order_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Args)]
-pub struct PathOutputArgs {
- #[arg(long)]
- pub output: Option<PathBuf>,
-}
-
-#[cfg(test)]
-mod tests {
- use std::collections::BTreeSet;
-
- use clap::{CommandFactory, Parser};
-
- use super::{
- AccountCommand, FarmCommand, ListingCommand, OrderCommand, OrderFulfillmentCommand,
- OrderFulfillmentStateArg, OrderPaymentCommand, OrderReceiptCommand, OrderRevisionCommand,
- OrderSettlementCommand, TargetCliArgs, TargetOutputFormat, ValidationCommand,
- ValidationReceiptCommand,
- };
- use crate::operation_registry::OPERATION_REGISTRY;
-
- #[test]
- fn target_parser_accepts_every_target_registry_path() {
- for operation in OPERATION_REGISTRY {
- let parsed = TargetCliArgs::try_parse_from(operation.cli_path.split_whitespace())
- .unwrap_or_else(|error| {
- panic!("{} failed to parse: {error}", operation.cli_path);
- });
- assert_eq!(parsed.command.operation_id(), operation.operation_id);
- }
- }
-
- #[test]
- fn target_parser_exposes_only_target_top_level_namespaces() {
- let actual = TargetCliArgs::command()
- .get_subcommands()
- .map(|command| command.get_name().to_owned())
- .collect::<BTreeSet<_>>();
- let expected = [
- "workspace",
- "health",
- "config",
- "account",
- "signer",
- "relay",
- "store",
- "sync",
- "farm",
- "listing",
- "market",
- "basket",
- "order",
- "validation",
- ]
- .into_iter()
- .map(str::to_owned)
- .collect::<BTreeSet<_>>();
-
- assert_eq!(actual, expected);
- }
-
- #[test]
- fn target_global_flags_parse() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "--format",
- "ndjson",
- "--account-id",
- "acct_test",
- "--relay",
- "wss://relay.one",
- "--relay",
- "wss://relay.two",
- "--offline",
- "--dry-run",
- "--idempotency-key",
- "idem_test",
- "--correlation-id",
- "corr_test",
- "--approval-token",
- "approval_test",
- "--no-input",
- "--quiet",
- "--no-color",
- "workspace",
- "get",
- ])
- .expect("target args parse");
-
- assert_eq!(parsed.format, TargetOutputFormat::Ndjson);
- assert_eq!(parsed.account_id.as_deref(), Some("acct_test"));
- assert_eq!(
- parsed.relay,
- vec!["wss://relay.one".to_owned(), "wss://relay.two".to_owned()]
- );
- assert!(parsed.offline);
- assert!(parsed.dry_run);
- assert_eq!(parsed.idempotency_key.as_deref(), Some("idem_test"));
- assert_eq!(parsed.correlation_id.as_deref(), Some("corr_test"));
- assert_eq!(parsed.approval_token.as_deref(), Some("approval_test"));
- assert!(parsed.no_input);
- assert!(parsed.quiet);
- assert!(parsed.no_color);
- assert_eq!(parsed.command.operation_id(), "workspace.get");
- }
-
- #[test]
- fn target_parser_accepts_account_attach_secret_inputs() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "account",
- "attach-secret",
- "acct_test",
- "identity.json",
- "--default",
- ])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "account.attach_secret");
- let crate::target_cli::TargetCommand::Account(account) = parsed.command else {
- panic!("expected account command")
- };
- let AccountCommand::AttachSecret(args) = account.command else {
- panic!("expected account attach-secret command")
- };
- assert_eq!(args.selector.as_deref(), Some("acct_test"));
- assert_eq!(
- args.path.as_ref().map(|path| path.as_os_str()),
- Some(std::ffi::OsStr::new("identity.json"))
- );
- assert!(args.default);
- }
-
- #[test]
- fn target_parser_accepts_farm_rebind_selector() {
- let parsed = TargetCliArgs::try_parse_from(["radroots", "farm", "rebind", "acct_test"])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "farm.rebind");
- let crate::target_cli::TargetCommand::Farm(farm) = parsed.command else {
- panic!("expected farm command")
- };
- let FarmCommand::Rebind(args) = farm.command else {
- panic!("expected farm rebind command")
- };
- assert_eq!(args.selector.as_deref(), Some("acct_test"));
- }
-
- #[test]
- fn target_parser_accepts_listing_rebind_inputs() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "listing",
- "rebind",
- "listing.toml",
- "acct_test",
- "--farm-d-tag",
- "AAAAAAAAAAAAAAAAAAAAAw",
- ])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "listing.rebind");
- let crate::target_cli::TargetCommand::Listing(listing) = parsed.command else {
- panic!("expected listing command")
- };
- let ListingCommand::Rebind(args) = listing.command else {
- panic!("expected listing rebind command")
- };
- assert_eq!(
- args.file.as_ref().map(|path| path.as_os_str()),
- Some(std::ffi::OsStr::new("listing.toml"))
- );
- assert_eq!(args.selector.as_deref(), Some("acct_test"));
- assert_eq!(args.farm_d_tag.as_deref(), Some("AAAAAAAAAAAAAAAAAAAAAw"));
- }
-
- #[test]
- fn target_parser_accepts_order_rebind_inputs() {
- let parsed =
- TargetCliArgs::try_parse_from(["radroots", "order", "rebind", "ord_test", "acct_test"])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "order.rebind");
- let crate::target_cli::TargetCommand::Order(order) = parsed.command else {
- panic!("expected order command")
- };
- let OrderCommand::Rebind(args) = order.command else {
- panic!("expected order rebind command")
- };
- assert_eq!(args.order_id.as_deref(), Some("ord_test"));
- assert_eq!(args.selector.as_deref(), Some("acct_test"));
- }
-
- #[test]
- fn target_parser_accepts_order_fulfillment_update_state() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "fulfillment",
- "update",
- "ord_test",
- "--state",
- "ready_for_pickup",
- ])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "order.fulfillment.update");
- let crate::target_cli::TargetCommand::Order(order) = parsed.command else {
- panic!("expected order command")
- };
- let OrderCommand::Fulfillment(fulfillment) = order.command else {
- panic!("expected order fulfillment command")
- };
- let OrderFulfillmentCommand::Update(args) = fulfillment.command;
- assert_eq!(args.order_id.as_deref(), Some("ord_test"));
- assert_eq!(args.state, Some(OrderFulfillmentStateArg::ReadyForPickup));
- }
-
- #[test]
- fn target_parser_accepts_order_cancel_reason() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "cancel",
- "ord_test",
- "--reason",
- "changed plans",
- ])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "order.cancel");
- let crate::target_cli::TargetCommand::Order(order) = parsed.command else {
- panic!("expected order command")
- };
- let OrderCommand::Cancel(args) = order.command else {
- panic!("expected order cancel command")
- };
- assert_eq!(args.order_id.as_deref(), Some("ord_test"));
- assert_eq!(args.reason.as_deref(), Some("changed plans"));
- }
-
- #[test]
- fn target_parser_accepts_order_revision_propose_inputs() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "revision",
- "propose",
- "ord_test",
- "--reason",
- "update count",
- "--bin-id",
- "bin-1",
- "--bin-count",
- "3",
- "--adjustment-id",
- "adj_revision",
- "--adjustment-effect",
- "increase",
- "--adjustment-amount",
- "2",
- "--adjustment-currency",
- "USD",
- "--adjustment-reason",
- "packing change",
- ])
- .expect("target args parse");
-
- assert_eq!(parsed.command.operation_id(), "order.revision.propose");
- let crate::target_cli::TargetCommand::Order(order) = parsed.command else {
- panic!("expected order command")
- };
- let OrderCommand::Revision(revision) = order.command else {
- panic!("expected order revision command")
- };
- let OrderRevisionCommand::Propose(args) = revision.command else {
- panic!("expected order revision propose command")
- };
- assert_eq!(args.order_id.as_deref(), Some("ord_test"));
- assert_eq!(args.reason.as_deref(), Some("update count"));
- assert_eq!(args.bin_id.as_deref(), Some("bin-1"));
- assert_eq!(args.bin_count, Some(3));
- assert_eq!(args.adjustment_id.as_deref(), Some("adj_revision"));
- assert_eq!(args.adjustment_effect.as_deref(), Some("increase"));
- }
-
- #[test]
- fn target_parser_accepts_order_revision_decision_inputs() {
- let accepted = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "revision",
- "accept",
- "ord_test",
- "--revision-id",
- "rev_test",
- ])
- .expect("target args parse");
-
- assert_eq!(accepted.command.operation_id(), "order.revision.accept");
- let crate::target_cli::TargetCommand::Order(order) = accepted.command else {
- panic!("expected order command")
- };
- let OrderCommand::Revision(revision) = order.command else {
- panic!("expected order revision command")
- };
- let OrderRevisionCommand::Accept(args) = revision.command else {
- panic!("expected order revision accept command")
- };
- assert_eq!(args.order_id.as_deref(), Some("ord_test"));
- assert_eq!(args.revision_id.as_deref(), Some("rev_test"));
-
- let declined = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "revision",
- "decline",
- "ord_test",
- "--revision-id",
- "rev_test",
- "--reason",
- "keep original order",
- ])
- .expect("target args parse");
-
- assert_eq!(declined.command.operation_id(), "order.revision.decline");
- let crate::target_cli::TargetCommand::Order(order) = declined.command else {
- panic!("expected order command")
- };
- let OrderCommand::Revision(revision) = order.command else {
- panic!("expected order revision command")
- };
- let OrderRevisionCommand::Decline(args) = revision.command else {
- panic!("expected order revision decline command")
- };
- assert_eq!(args.order_id.as_deref(), Some("ord_test"));
- assert_eq!(args.revision_id.as_deref(), Some("rev_test"));
- assert_eq!(args.reason.as_deref(), Some("keep original order"));
- }
-
- #[test]
- fn target_parser_accepts_order_receipt_record_outcomes() {
- let received = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "receipt",
- "record",
- "ord_test",
- "--received",
- ])
- .expect("target args parse");
- assert_eq!(received.command.operation_id(), "order.receipt.record");
- let crate::target_cli::TargetCommand::Order(order) = received.command else {
- panic!("expected order command")
- };
- let OrderCommand::Receipt(receipt) = order.command else {
- panic!("expected order receipt command")
- };
- let OrderReceiptCommand::Record(args) = receipt.command;
- assert_eq!(args.order_id.as_deref(), Some("ord_test"));
- assert!(args.received);
- assert_eq!(args.issue, None);
-
- let issue = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "receipt",
- "record",
- "ord_test",
- "--issue",
- "damaged items",
- ])
- .expect("target args parse");
- assert_eq!(issue.command.operation_id(), "order.receipt.record");
- }
-
- #[test]
- fn target_parser_accepts_validation_receipt_commands() {
- let get = TargetCliArgs::try_parse_from([
- "radroots",
- "validation",
- "receipt",
- "get",
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- ])
- .expect("target args parse");
- assert_eq!(get.command.operation_id(), "validation.receipt.get");
- let crate::target_cli::TargetCommand::Validation(validation) = get.command else {
- panic!("expected validation command")
- };
- let ValidationCommand::Receipt(receipt) = validation.command;
- let ValidationReceiptCommand::Get(args) = receipt.command else {
- panic!("expected validation receipt get command")
- };
- assert_eq!(
- args.receipt_event_id.as_deref(),
- Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
- );
-
- let list = TargetCliArgs::try_parse_from([
- "radroots",
- "validation",
- "receipt",
- "list",
- "--order-id",
- "ord_1",
- ])
- .expect("target args parse");
- assert_eq!(list.command.operation_id(), "validation.receipt.list");
- let crate::target_cli::TargetCommand::Validation(validation) = list.command else {
- panic!("expected validation command")
- };
- let ValidationCommand::Receipt(receipt) = validation.command;
- let ValidationReceiptCommand::List(args) = receipt.command else {
- panic!("expected validation receipt list command")
- };
- assert_eq!(args.order_id.as_deref(), Some("ord_1"));
-
- let verify = TargetCliArgs::try_parse_from([
- "radroots",
- "validation",
- "receipt",
- "verify",
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
- ])
- .expect("target args parse");
- assert_eq!(verify.command.operation_id(), "validation.receipt.verify");
- }
-
- #[test]
- fn target_parser_accepts_order_payment_record_methods() {
- let parsed = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "payment",
- "record",
- "ord_test",
- "--amount",
- "12",
- "--currency",
- "USD",
- "--method",
- "manual_transfer",
- "--reference",
- "memo-1",
- "--paid-at",
- "1777666000",
- ])
- .expect("target args parse");
- assert_eq!(parsed.command.operation_id(), "order.payment.record");
- let crate::target_cli::TargetCommand::Order(order) = parsed.command else {
- panic!("expected order command")
- };
- let OrderCommand::Payment(payment) = order.command else {
- panic!("expected order payment command")
- };
- let OrderPaymentCommand::Record(args) = payment.command;
- assert_eq!(args.order_id.as_deref(), Some("ord_test"));
- assert_eq!(args.amount.as_deref(), Some("12"));
- assert_eq!(args.currency.as_deref(), Some("USD"));
- assert_eq!(args.method.as_deref(), Some("manual_transfer"));
- assert_eq!(args.reference.as_deref(), Some("memo-1"));
- assert_eq!(args.paid_at, Some(1_777_666_000));
-
- let future_method = TargetCliArgs::try_parse_from([
- "radroots", "order", "payment", "record", "ord_test", "--method", "card",
- ])
- .expect("target args parse");
- let crate::target_cli::TargetCommand::Order(order) = future_method.command else {
- panic!("expected order command")
- };
- let OrderCommand::Payment(payment) = order.command else {
- panic!("expected order payment command")
- };
- let OrderPaymentCommand::Record(args) = payment.command;
- assert_eq!(args.method.as_deref(), Some("card"));
- }
-
- #[test]
- fn target_parser_accepts_order_settlement_decisions() {
- let accept = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "settlement",
- "accept",
- "ord_test",
- "--payment-event-id",
- "pay_event",
- ])
- .expect("target args parse");
- assert_eq!(accept.command.operation_id(), "order.settlement.accept");
- let crate::target_cli::TargetCommand::Order(order) = accept.command else {
- panic!("expected order command")
- };
- let OrderCommand::Settlement(settlement) = order.command else {
- panic!("expected order settlement command")
- };
- let OrderSettlementCommand::Accept(args) = settlement.command else {
- panic!("expected settlement accept command")
- };
- assert_eq!(args.order_id.as_deref(), Some("ord_test"));
- assert_eq!(args.payment_event_id.as_deref(), Some("pay_event"));
-
- let reject = TargetCliArgs::try_parse_from([
- "radroots",
- "order",
- "settlement",
- "reject",
- "ord_test",
- "--payment-event-id",
- "pay_event",
- "--reason",
- "reference mismatch",
- ])
- .expect("target args parse");
- assert_eq!(reject.command.operation_id(), "order.settlement.reject");
- }
-
- #[test]
- fn target_parser_rejects_removed_global_flags() {
- let rejected = [
- vec!["radroots", "--output", "json", "config", "get"],
- vec!["radroots", "--json", "config", "get"],
- vec!["radroots", "--ndjson", "config", "get"],
- vec!["radroots", "--yes", "config", "get"],
- vec!["radroots", "--non-interactive", "config", "get"],
- vec!["radroots", "--signer", "myc", "config", "get"],
- vec!["radroots", "--farm-id", "farm_test", "config", "get"],
- vec!["radroots", "--profile", "repo_local", "config", "get"],
- vec![
- "radroots",
- "--signer-session-id",
- "sess_test",
- "config",
- "get",
- ],
- ];
-
- for args in rejected {
- assert!(TargetCliArgs::try_parse_from(args).is_err());
- }
- }
-
- #[test]
- fn target_parser_rejects_removed_top_level_commands() {
- for command in [
- "setup", "status", "doctor", "sell", "find", "local", "net", "myc", "rpc",
- ] {
- assert!(TargetCliArgs::try_parse_from(["radroots", command]).is_err());
- }
- }
-
- #[test]
- fn target_parser_rejects_deferred_namespaces() {
- for command in ["product", "message", "approval", "agent"] {
- assert!(TargetCliArgs::try_parse_from(["radroots", command]).is_err());
- }
- }
-
- #[test]
- fn target_parser_rejects_online_offline_conflict() {
- assert!(
- TargetCliArgs::try_parse_from([
- "radroots",
- "--online",
- "--offline",
- "health",
- "status",
- "get"
- ])
- .is_err()
- );
- }
-}
diff --git a/src/domain/mod.rs b/src/view/mod.rs
diff --git a/src/domain/runtime.rs b/src/view/runtime.rs