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 }