cli

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

runtime.rs (14856B)


      1 use serde::Serialize;
      2 use serde_json::Value;
      3 
      4 use crate::cli::global::SyncWatchArgs;
      5 use crate::ops::{
      6     OperationAdapterError, OperationRequest, OperationRequestData, OperationRequestPayload,
      7     OperationResult, OperationResultData, OperationService, RelayListRequest, RelayListResult,
      8     SignerStatusGetRequest, SignerStatusGetResult, SyncPullRequest, SyncPullResult,
      9     SyncPushRequest, SyncPushResult, SyncStatusGetRequest, SyncStatusGetResult, SyncWatchRequest,
     10     SyncWatchResult,
     11 };
     12 use crate::runtime::RuntimeError;
     13 use crate::runtime::config::RuntimeConfig;
     14 use crate::view::runtime::{CommandDisposition, SyncActionView, SyncStatusView};
     15 
     16 pub struct RuntimeOperationService<'a> {
     17     config: &'a RuntimeConfig,
     18 }
     19 
     20 impl<'a> RuntimeOperationService<'a> {
     21     pub fn new(config: &'a RuntimeConfig) -> Self {
     22         Self { config }
     23     }
     24 }
     25 
     26 impl OperationService<SignerStatusGetRequest> for RuntimeOperationService<'_> {
     27     type Result = SignerStatusGetResult;
     28 
     29     fn execute(
     30         &self,
     31         _request: OperationRequest<SignerStatusGetRequest>,
     32     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     33         let view = crate::runtime::signer::resolve_signer_status(self.config);
     34         serialized_operation_result::<SignerStatusGetResult, _>(&view)
     35     }
     36 }
     37 
     38 impl OperationService<RelayListRequest> for RuntimeOperationService<'_> {
     39     type Result = RelayListResult;
     40 
     41     fn execute(
     42         &self,
     43         _request: OperationRequest<RelayListRequest>,
     44     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     45         let view = crate::runtime::network::relay_list(self.config);
     46         serialized_operation_result::<RelayListResult, _>(&view)
     47     }
     48 }
     49 
     50 impl OperationService<SyncStatusGetRequest> for RuntimeOperationService<'_> {
     51     type Result = SyncStatusGetResult;
     52 
     53     fn execute(
     54         &self,
     55         _request: OperationRequest<SyncStatusGetRequest>,
     56     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     57         let view = crate::runtime::sync::status(self.config).map_err(|error| {
     58             OperationAdapterError::sdk_adapter_failure("sync.status.get", error)
     59         })?;
     60         sync_status_result(&view)
     61     }
     62 }
     63 
     64 impl OperationService<SyncPullRequest> for RuntimeOperationService<'_> {
     65     type Result = SyncPullResult;
     66 
     67     fn execute(
     68         &self,
     69         _request: OperationRequest<SyncPullRequest>,
     70     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     71         let view = map_runtime("sync.pull", crate::runtime::sync::pull(self.config))?;
     72         sync_action_result::<SyncPullResult>("sync.pull", &view)
     73     }
     74 }
     75 
     76 impl OperationService<SyncPushRequest> for RuntimeOperationService<'_> {
     77     type Result = SyncPushResult;
     78 
     79     fn execute(
     80         &self,
     81         request: OperationRequest<SyncPushRequest>,
     82     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     83         if request.context.requires_approval_token() {
     84             return Err(OperationAdapterError::approval_required("sync.push"));
     85         }
     86         let view = crate::runtime::sync::push(self.config)
     87             .map_err(|error| OperationAdapterError::sdk_adapter_failure("sync.push", error))?;
     88         sync_action_result::<SyncPushResult>("sync.push", &view)
     89     }
     90 }
     91 
     92 impl OperationService<SyncWatchRequest> for RuntimeOperationService<'_> {
     93     type Result = SyncWatchResult;
     94 
     95     fn execute(
     96         &self,
     97         request: OperationRequest<SyncWatchRequest>,
     98     ) -> Result<OperationResult<Self::Result>, OperationAdapterError> {
     99         let args = SyncWatchArgs {
    100             frames: usize_input(&request, "frames").unwrap_or(1),
    101             interval_ms: u64_input(&request, "interval_ms").unwrap_or(1_000),
    102         };
    103         let view = map_runtime(
    104             "sync.watch",
    105             crate::runtime::sync::watch(self.config, &args),
    106         )?;
    107         serialized_operation_result::<SyncWatchResult, _>(&view)
    108     }
    109 }
    110 
    111 fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError>
    112 where
    113     R: OperationResultData,
    114     T: Serialize,
    115 {
    116     OperationResult::new(R::from_serializable(value)?)
    117 }
    118 
    119 fn sync_status_result(
    120     view: &SyncStatusView,
    121 ) -> Result<OperationResult<SyncStatusGetResult>, OperationAdapterError> {
    122     match view.disposition() {
    123         CommandDisposition::Success => serialized_operation_result::<SyncStatusGetResult, _>(view),
    124         disposition => Err(sync_view_error(
    125             "sync.status.get",
    126             disposition,
    127             view,
    128             view.reason.as_deref(),
    129         )),
    130     }
    131 }
    132 
    133 fn sync_action_result<R>(
    134     operation_id: &str,
    135     view: &SyncActionView,
    136 ) -> Result<OperationResult<R>, OperationAdapterError>
    137 where
    138     R: OperationResultData,
    139 {
    140     match view.disposition() {
    141         CommandDisposition::Success => serialized_operation_result::<R, _>(view),
    142         disposition => Err(sync_view_error(
    143             operation_id,
    144             disposition,
    145             view,
    146             view.reason.as_deref(),
    147         )),
    148     }
    149 }
    150 
    151 fn sync_view_error<T>(
    152     operation_id: &str,
    153     disposition: CommandDisposition,
    154     view: &T,
    155     reason: Option<&str>,
    156 ) -> OperationAdapterError
    157 where
    158     T: Serialize,
    159 {
    160     let detail = serde_json::to_value(view).unwrap_or_else(|_| Value::Object(Default::default()));
    161     let message = reason
    162         .map(str::to_owned)
    163         .unwrap_or_else(|| format!("`{operation_id}` is not ready"));
    164     match disposition {
    165         CommandDisposition::Unconfigured => {
    166             OperationAdapterError::operation_unavailable_with_detail(operation_id, message, detail)
    167         }
    168         CommandDisposition::ExternalUnavailable => {
    169             OperationAdapterError::network_unavailable_with_detail(operation_id, message, detail)
    170         }
    171         CommandDisposition::Unsupported => OperationAdapterError::InvalidInput {
    172             operation_id: operation_id.to_owned(),
    173             message,
    174         },
    175         CommandDisposition::ValidationFailed => OperationAdapterError::ValidationFailed {
    176             operation_id: operation_id.to_owned(),
    177             message,
    178         },
    179         CommandDisposition::NotFound => OperationAdapterError::NotFound {
    180             operation_id: operation_id.to_owned(),
    181             message,
    182         },
    183         CommandDisposition::InternalError | CommandDisposition::Success => {
    184             OperationAdapterError::Runtime(message)
    185         }
    186     }
    187 }
    188 
    189 fn map_runtime<T>(
    190     operation_id: &str,
    191     result: Result<T, RuntimeError>,
    192 ) -> Result<T, OperationAdapterError> {
    193     result.map_err(|error| OperationAdapterError::runtime_failure(operation_id, error))
    194 }
    195 
    196 fn usize_input<P>(request: &OperationRequest<P>, key: &str) -> Option<usize>
    197 where
    198     P: OperationRequestPayload + OperationRequestData,
    199 {
    200     request
    201         .payload
    202         .input()
    203         .get(key)
    204         .and_then(Value::as_u64)
    205         .and_then(|value| usize::try_from(value).ok())
    206 }
    207 
    208 fn u64_input<P>(request: &OperationRequest<P>, key: &str) -> Option<u64>
    209 where
    210     P: OperationRequestPayload + OperationRequestData,
    211 {
    212     request.payload.input().get(key).and_then(Value::as_u64)
    213 }
    214 
    215 #[cfg(test)]
    216 mod tests {
    217     use std::path::{Path, PathBuf};
    218 
    219     use radroots_runtime_paths::RadrootsMigrationReport;
    220     use radroots_secret_vault::RadrootsSecretBackend;
    221     use tempfile::tempdir;
    222 
    223     use super::RuntimeOperationService;
    224     use crate::ops::{
    225         OperationAdapter, OperationContext, OperationRequest, RelayListRequest,
    226         SignerStatusGetRequest, SyncStatusGetRequest,
    227     };
    228     use crate::runtime::config::{
    229         AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig,
    230         LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat,
    231         PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig,
    232         RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend,
    233         SignerConfig, Verbosity,
    234     };
    235 
    236     #[test]
    237     fn runtime_service_backs_signer_and_relay_status() {
    238         let dir = tempdir().expect("tempdir");
    239         let config = sample_config(dir.path(), vec!["wss://relay.test".into()]);
    240         let service = OperationAdapter::new(RuntimeOperationService::new(&config));
    241 
    242         let signer = OperationRequest::new(
    243             OperationContext::default(),
    244             SignerStatusGetRequest::default(),
    245         )
    246         .expect("signer status request");
    247         let signer_envelope = service
    248             .execute(signer)
    249             .expect("signer status result")
    250             .to_envelope(OperationContext::default().envelope_context("req_signer"))
    251             .expect("signer envelope");
    252         assert_eq!(signer_envelope.operation_id, "signer.status.get");
    253         assert_eq!(signer_envelope.result["state"], "unconfigured");
    254 
    255         let relay = OperationRequest::new(OperationContext::default(), RelayListRequest::default())
    256             .expect("relay list request");
    257         let relay_envelope = service
    258             .execute(relay)
    259             .expect("relay list result")
    260             .to_envelope(OperationContext::default().envelope_context("req_relay"))
    261             .expect("relay envelope");
    262         assert_eq!(relay_envelope.operation_id, "relay.list");
    263         assert_eq!(relay_envelope.result["state"], "configured");
    264         assert_eq!(relay_envelope.result["count"], 1);
    265     }
    266 
    267     #[test]
    268     fn runtime_service_backs_sync_status() {
    269         let dir = tempdir().expect("tempdir");
    270         let config = sample_config(dir.path(), Vec::new());
    271         let service = OperationAdapter::new(RuntimeOperationService::new(&config));
    272 
    273         let sync =
    274             OperationRequest::new(OperationContext::default(), SyncStatusGetRequest::default())
    275                 .expect("sync status request");
    276         let envelope = service
    277             .execute(sync)
    278             .expect("sync status result")
    279             .to_envelope(OperationContext::default().envelope_context("req_sync_status"))
    280             .expect("sync status envelope");
    281 
    282         assert_eq!(envelope.operation_id, "sync.status.get");
    283         assert_eq!(envelope.result["state"], "ready");
    284         assert_eq!(
    285             envelope.result["source"],
    286             "SDK canonical event store and outbox"
    287         );
    288         assert_eq!(envelope.result["replica_db"], "legacy_derived_not_checked");
    289         assert_eq!(envelope.result["queue"]["pending_count"], 0);
    290         assert_eq!(envelope.result["queue"]["total_count"], 0);
    291         assert_eq!(envelope.result["actions"][0], "radroots sync pull");
    292     }
    293 
    294     fn sample_config(root: &Path, relays: Vec<String>) -> RuntimeConfig {
    295         let data = root.join("data");
    296         let logs = root.join("logs");
    297         let secrets = root.join("secrets");
    298         RuntimeConfig {
    299             output: OutputConfig {
    300                 format: OutputFormat::Human,
    301                 verbosity: Verbosity::Normal,
    302                 color: true,
    303                 dry_run: false,
    304             },
    305             interaction: InteractionConfig {
    306                 input_enabled: true,
    307                 assume_yes: false,
    308                 stdin_tty: false,
    309                 stdout_tty: false,
    310                 prompts_allowed: false,
    311                 confirmations_allowed: false,
    312             },
    313             paths: PathsConfig {
    314                 profile: "interactive_user".into(),
    315                 profile_source: "test".into(),
    316                 allowed_profiles: vec!["interactive_user".into(), "repo_local".into()],
    317                 root_source: "test".into(),
    318                 repo_local_root: None,
    319                 repo_local_root_source: None,
    320                 subordinate_path_override_source: "runtime_config".into(),
    321                 app_namespace: "apps/cli".into(),
    322                 shared_accounts_namespace: "shared/accounts".into(),
    323                 shared_identities_namespace: "shared/identities".into(),
    324                 app_config_path: root.join("config/apps/cli/config.toml"),
    325                 workspace_config_path: None,
    326                 app_data_root: data.join("apps/cli"),
    327                 app_logs_root: logs.join("apps/cli"),
    328                 shared_accounts_data_root: data.join("shared/accounts"),
    329                 shared_accounts_secrets_root: secrets.join("shared/accounts"),
    330                 default_identity_path: secrets.join("shared/identities/default.json"),
    331             },
    332             migration: MigrationConfig {
    333                 report: RadrootsMigrationReport::empty(),
    334             },
    335             logging: LoggingConfig {
    336                 filter: "info".into(),
    337                 directory: None,
    338                 stdout: false,
    339             },
    340             account: AccountConfig {
    341                 selector: None,
    342                 store_path: data.join("shared/accounts/store.json"),
    343                 secrets_dir: secrets.join("shared/accounts"),
    344                 secret_backend: RadrootsSecretBackend::EncryptedFile,
    345                 secret_fallback: None,
    346             },
    347             account_secret_contract: AccountSecretContractConfig {
    348                 default_backend: "host_vault".into(),
    349                 default_fallback: Some("encrypted_file".into()),
    350                 allowed_backends: vec!["host_vault".into(), "encrypted_file".into()],
    351                 host_vault_policy: Some("desktop".into()),
    352                 uses_protected_store: true,
    353             },
    354             identity: IdentityConfig {
    355                 path: secrets.join("shared/identities/default.json"),
    356             },
    357             signer: SignerConfig {
    358                 backend: SignerBackend::Local,
    359             },
    360             publish: PublishConfig {
    361                 transport: PublishTransport::DirectNostrRelay,
    362                 source: PublishTransportSource::Defaults,
    363                 radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(),
    364             },
    365             relay: RelayConfig {
    366                 urls: relays,
    367                 publish_policy: RelayPublishPolicy::Any,
    368                 source: RelayConfigSource::Defaults,
    369             },
    370             local: LocalConfig {
    371                 root: data.join("apps/cli/replica"),
    372                 replica_db_path: data.join("apps/cli/replica/replica.sqlite"),
    373                 backups_dir: data.join("apps/cli/replica/backups"),
    374                 exports_dir: data.join("apps/cli/replica/exports"),
    375             },
    376             myc: MycConfig {
    377                 executable: PathBuf::from("myc"),
    378                 status_timeout_ms: 2_000,
    379             },
    380             hyf: HyfConfig {
    381                 enabled: false,
    382                 executable: PathBuf::from("hyfd"),
    383             },
    384             rpc: RpcConfig {
    385                 url: "http://127.0.0.1:7070".into(),
    386             },
    387             rhi: crate::runtime::config::RhiConfig {
    388                 trusted_worker_pubkeys: Vec::new(),
    389             },
    390             capability_bindings: Vec::new(),
    391         }
    392     }
    393 }