cli

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

commit 3d132a9ae8b0ad07091a12e6c5b3e3d64e2c7644
parent fa2569572700d2430dc55f4ff3459fe5f58327a0
Author: triesap <tyson@radroots.org>
Date:   Tue, 26 May 2026 20:06:18 +0000

cli: organize command parser modules

Diffstat:
Rsrc/runtime_args.rs -> src/cli/global.rs | 0
Asrc/cli/mod.rs | 1721+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.rs | 37++++++++++++++++++-------------------
Dsrc/operation_adapter.rs | 2466-------------------------------------------------------------------------------
Msrc/operation_basket.rs | 10+++++-----
Msrc/operation_core.rs | 14+++++++-------
Msrc/operation_farm.rs | 14+++++++-------
Msrc/operation_listing.rs | 14+++++++-------
Msrc/operation_market.rs | 16++++++++--------
Msrc/operation_order.rs | 40++++++++++++++++++++--------------------
Msrc/operation_runtime.rs | 8++++----
Msrc/operation_validation.rs | 4++--
Asrc/ops/mod.rs | 2466+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/output_contract.rs -> src/out/envelope.rs | 0
Asrc/out/mod.rs | 1+
Rsrc/operation_registry.rs -> src/registry/mod.rs | 0
Msrc/runtime/accounts.rs | 2+-
Msrc/runtime/config.rs | 4++--
Msrc/runtime/farm.rs | 16++++++++--------
Msrc/runtime/find.rs | 10+++++-----
Msrc/runtime/listing.rs | 18+++++++++---------
Msrc/runtime/local.rs | 10+++++-----
Msrc/runtime/network.rs | 2+-
Msrc/runtime/order.rs | 52++++++++++++++++++++++++++--------------------------
Msrc/runtime/provider.rs | 8++++----
Msrc/runtime/signer.rs | 8++++----
Msrc/runtime/sync.rs | 14+++++++-------
Msrc/runtime/validation_receipt.rs | 2+-
Dsrc/target_cli.rs | 1719-------------------------------------------------------------------------------
Rsrc/domain/mod.rs -> src/view/mod.rs | 0
Rsrc/domain/runtime.rs -> src/view/runtime.rs | 0
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 &quote.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 &quote.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