market.rs (26628B)
1 use serde::Serialize; 2 use serde_json::Value; 3 4 use crate::cli::global::{FindQueryArgs, RecordLookupArgs}; 5 use crate::ops::{ 6 MarketListingGetRequest, MarketListingGetResult, MarketProductSearchRequest, 7 MarketProductSearchResult, MarketRefreshRequest, MarketRefreshResult, OperationAdapterError, 8 OperationRequest, OperationRequestData, OperationRequestPayload, OperationResult, 9 OperationResultData, OperationService, 10 }; 11 use crate::runtime::RuntimeError; 12 use crate::runtime::config::RuntimeConfig; 13 use crate::view::runtime::{FindView, ListingGetView, SyncActionView}; 14 15 pub struct MarketOperationService<'a> { 16 config: &'a RuntimeConfig, 17 } 18 19 impl<'a> MarketOperationService<'a> { 20 pub fn new(config: &'a RuntimeConfig) -> Self { 21 Self { config } 22 } 23 } 24 25 impl OperationService<MarketRefreshRequest> for MarketOperationService<'_> { 26 type Result = MarketRefreshResult; 27 28 fn execute( 29 &self, 30 _request: OperationRequest<MarketRefreshRequest>, 31 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 32 let view = market_refresh_view(map_runtime(crate::runtime::sync::market_refresh( 33 self.config, 34 ))?); 35 serialized_operation_result::<MarketRefreshResult, _>(&view) 36 } 37 } 38 39 impl OperationService<MarketProductSearchRequest> for MarketOperationService<'_> { 40 type Result = MarketProductSearchResult; 41 42 fn execute( 43 &self, 44 request: OperationRequest<MarketProductSearchRequest>, 45 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 46 let args = FindQueryArgs { 47 query: required_query_terms(&request)?, 48 }; 49 let view = market_product_search_view(map_runtime(crate::runtime::find::search( 50 self.config, 51 &args, 52 ))?); 53 serialized_operation_result::<MarketProductSearchResult, _>(&view) 54 } 55 } 56 57 impl OperationService<MarketListingGetRequest> for MarketOperationService<'_> { 58 type Result = MarketListingGetResult; 59 60 fn execute( 61 &self, 62 request: OperationRequest<MarketListingGetRequest>, 63 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 64 let args = RecordLookupArgs { 65 key: required_lookup(&request)?, 66 }; 67 let view = market_listing_get_view(map_runtime(crate::runtime::listing::get( 68 self.config, 69 &args, 70 ))?); 71 serialized_operation_result::<MarketListingGetResult, _>(&view) 72 } 73 } 74 75 fn market_refresh_view(mut view: SyncActionView) -> SyncActionView { 76 view.actions = match view.state.as_str() { 77 "ready" => vec!["radroots market product search tomatoes".to_owned()], 78 "unavailable" => vec!["radroots sync status get".to_owned()], 79 "unconfigured" => { 80 let mut actions = Vec::new(); 81 if view.replica_db == "missing" { 82 actions.push("radroots store init".to_owned()); 83 } 84 if view.relay_count == 0 { 85 actions.push("radroots --relay wss://relay.example.com market refresh".to_owned()); 86 } 87 if actions.is_empty() { 88 actions.extend(std::mem::take(&mut view.actions)); 89 } 90 actions 91 } 92 _ => std::mem::take(&mut view.actions), 93 }; 94 view 95 } 96 97 fn market_product_search_view(mut view: FindView) -> FindView { 98 view.actions = match view.state.as_str() { 99 "ready" => view 100 .results 101 .first() 102 .map(|result| { 103 let mut actions = vec![format!( 104 "radroots market listing get {}", 105 result.product_key 106 )]; 107 if result.readiness.order_request_enabled { 108 actions.push("radroots basket create".to_owned()); 109 } 110 actions 111 }) 112 .unwrap_or_default(), 113 "empty" => vec![ 114 "radroots market refresh".to_owned(), 115 "radroots market product search eggs".to_owned(), 116 ], 117 "unconfigured" => vec![ 118 "radroots store init".to_owned(), 119 "radroots market refresh".to_owned(), 120 ], 121 _ => std::mem::take(&mut view.actions), 122 }; 123 view 124 } 125 126 fn market_listing_get_view(mut view: ListingGetView) -> ListingGetView { 127 view.actions = match view.state.as_str() { 128 "ready" => { 129 if view.readiness.order_request_enabled { 130 vec!["radroots basket create".to_owned()] 131 } else { 132 Vec::new() 133 } 134 } 135 "missing" => vec![ 136 "radroots market product search tomatoes".to_owned(), 137 "radroots market refresh".to_owned(), 138 ], 139 "unconfigured" => vec![ 140 "radroots store init".to_owned(), 141 "radroots market refresh".to_owned(), 142 ], 143 _ => std::mem::take(&mut view.actions), 144 }; 145 view 146 } 147 148 fn required_query_terms<P>( 149 request: &OperationRequest<P>, 150 ) -> Result<Vec<String>, OperationAdapterError> 151 where 152 P: OperationRequestPayload + OperationRequestData, 153 { 154 let input = request.payload.input(); 155 let Some(value) = input.get("query").or_else(|| input.get("terms")) else { 156 return Err(invalid_input( 157 request.operation_id(), 158 "missing required `query` input".to_owned(), 159 )); 160 }; 161 let terms = match value { 162 Value::String(value) => value 163 .split_whitespace() 164 .map(str::trim) 165 .filter(|term| !term.is_empty()) 166 .map(str::to_owned) 167 .collect::<Vec<_>>(), 168 Value::Array(values) => values 169 .iter() 170 .map(|value| { 171 value.as_str().map(str::to_owned).ok_or_else(|| { 172 invalid_input( 173 request.operation_id(), 174 "`query` array entries must be strings".to_owned(), 175 ) 176 }) 177 }) 178 .collect::<Result<Vec<_>, _>>()?, 179 _ => { 180 return Err(invalid_input( 181 request.operation_id(), 182 "`query` input must be a string or string array".to_owned(), 183 )); 184 } 185 }; 186 187 if terms.is_empty() { 188 return Err(invalid_input( 189 request.operation_id(), 190 "`query` input must not be empty".to_owned(), 191 )); 192 } 193 Ok(terms) 194 } 195 196 fn required_lookup<P>(request: &OperationRequest<P>) -> Result<String, OperationAdapterError> 197 where 198 P: OperationRequestPayload + OperationRequestData, 199 { 200 string_input(request, "key") 201 .or_else(|| string_input(request, "listing_id")) 202 .or_else(|| string_input(request, "listing")) 203 .ok_or_else(|| { 204 invalid_input( 205 request.operation_id(), 206 "missing required `key` input".to_owned(), 207 ) 208 }) 209 } 210 211 fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError> 212 where 213 R: OperationResultData, 214 T: Serialize, 215 { 216 OperationResult::new(R::from_serializable(value)?) 217 } 218 219 fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapterError> { 220 result.map_err(|error| OperationAdapterError::Runtime(error.to_string())) 221 } 222 223 fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String> 224 where 225 P: OperationRequestPayload + OperationRequestData, 226 { 227 request 228 .payload 229 .input() 230 .get(key) 231 .and_then(Value::as_str) 232 .map(str::to_owned) 233 } 234 235 fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError { 236 OperationAdapterError::InvalidInput { 237 operation_id: operation_id.to_owned(), 238 message, 239 } 240 } 241 242 #[cfg(test)] 243 mod tests { 244 use std::path::{Path, PathBuf}; 245 246 use radroots_runtime_paths::RadrootsMigrationReport; 247 use radroots_secret_vault::RadrootsSecretBackend; 248 use serde_json::{Map, Value}; 249 use tempfile::tempdir; 250 251 use super::{MarketOperationService, market_listing_get_view, market_product_search_view}; 252 use crate::ops::{ 253 MarketListingGetRequest, MarketProductSearchRequest, MarketRefreshRequest, 254 OperationAdapter, OperationContext, OperationData, OperationRequest, 255 }; 256 use crate::runtime::config::{ 257 AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig, 258 LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat, 259 PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig, 260 RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, 261 SignerConfig, Verbosity, 262 }; 263 use crate::view::runtime::{ 264 FindPriceView, FindQuantityView, FindResultProvenanceView, FindResultView, FindView, 265 ListingGetView, MarketReadinessView, SyncFreshnessView, 266 }; 267 268 const LISTING_ADDR: &str = "30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg"; 269 270 #[test] 271 fn market_refresh_preserves_unconfigured_ingest_truth() { 272 let dir = tempdir().expect("tempdir"); 273 let config = sample_config(dir.path()); 274 let service = OperationAdapter::new(MarketOperationService::new(&config)); 275 let request = 276 OperationRequest::new(OperationContext::default(), MarketRefreshRequest::default()) 277 .expect("market refresh request"); 278 let envelope = service 279 .execute(request) 280 .expect("market refresh result") 281 .to_envelope(OperationContext::default().envelope_context("req_market_refresh")) 282 .expect("market refresh envelope"); 283 284 assert_eq!(envelope.operation_id, "market.refresh"); 285 assert_eq!(envelope.result["state"], "unconfigured"); 286 assert_eq!(envelope.result["direction"], "pull"); 287 assert_eq!(envelope.result["actions"][0], "radroots store init"); 288 } 289 290 #[test] 291 fn market_refresh_supports_dry_run() { 292 let dir = tempdir().expect("tempdir"); 293 let config = sample_config(dir.path()); 294 let service = OperationAdapter::new(MarketOperationService::new(&config)); 295 let mut context = OperationContext::default(); 296 context.dry_run = true; 297 let request = OperationRequest::new(context.clone(), MarketRefreshRequest::default()) 298 .expect("market refresh request"); 299 let envelope = service 300 .execute(request) 301 .expect("market refresh dry run") 302 .to_envelope(context.envelope_context("req_market_refresh")) 303 .expect("market refresh envelope"); 304 305 assert_eq!(envelope.operation_id, "market.refresh"); 306 assert_eq!(envelope.dry_run, true); 307 assert_eq!(envelope.result["state"], "unconfigured"); 308 assert_eq!(envelope.result["replica_db"], "missing"); 309 assert_eq!(envelope.result["direction"], "pull"); 310 } 311 312 #[test] 313 fn market_refresh_dry_run_skips_relay_fetch_when_store_is_ready() { 314 let dir = tempdir().expect("tempdir"); 315 let mut config = sample_config(dir.path()); 316 config.output.dry_run = true; 317 config.relay.urls = vec!["wss://relay.example.com".to_owned()]; 318 crate::runtime::store::init(&config).expect("store init"); 319 320 let service = OperationAdapter::new(MarketOperationService::new(&config)); 321 let mut context = OperationContext::default(); 322 context.dry_run = true; 323 let request = OperationRequest::new(context.clone(), MarketRefreshRequest::default()) 324 .expect("market refresh request"); 325 let envelope = service 326 .execute(request) 327 .expect("market refresh dry run") 328 .to_envelope(context.envelope_context("req_market_refresh")) 329 .expect("market refresh envelope"); 330 331 assert_eq!(envelope.operation_id, "market.refresh"); 332 assert_eq!(envelope.result["state"], "ready"); 333 assert_eq!( 334 envelope.result["target_relays"][0], 335 "wss://relay.example.com" 336 ); 337 assert_eq!(envelope.result["fetched_count"], 0); 338 assert_eq!(envelope.result["ingested_count"], 0); 339 assert_eq!(envelope.result["skipped_count"], 0); 340 assert_eq!(envelope.result["unsupported_count"], 0); 341 assert!( 342 envelope.result["reason"] 343 .as_str() 344 .expect("reason") 345 .contains("dry run") 346 ); 347 } 348 349 #[test] 350 fn market_refresh_no_relay_action_is_actionable() { 351 let dir = tempdir().expect("tempdir"); 352 let config = sample_config(dir.path()); 353 crate::runtime::store::init(&config).expect("store init"); 354 let service = OperationAdapter::new(MarketOperationService::new(&config)); 355 let request = 356 OperationRequest::new(OperationContext::default(), MarketRefreshRequest::default()) 357 .expect("market refresh request"); 358 let envelope = service 359 .execute(request) 360 .expect("market refresh result") 361 .to_envelope(OperationContext::default().envelope_context("req_market_refresh")) 362 .expect("market refresh envelope"); 363 364 assert_eq!(envelope.result["state"], "unconfigured"); 365 assert_eq!( 366 envelope.result["actions"][0], 367 "radroots --relay wss://relay.example.com market refresh" 368 ); 369 } 370 371 #[test] 372 fn market_product_search_uses_find_runtime_without_top_level_find() { 373 let dir = tempdir().expect("tempdir"); 374 let config = sample_config(dir.path()); 375 let service = OperationAdapter::new(MarketOperationService::new(&config)); 376 let request = OperationRequest::new( 377 OperationContext::default(), 378 MarketProductSearchRequest::from_data(data(&[("query", "eggs")])), 379 ) 380 .expect("market product search request"); 381 let envelope = service 382 .execute(request) 383 .expect("market product search result") 384 .to_envelope(OperationContext::default().envelope_context("req_market_search")) 385 .expect("market product search envelope"); 386 387 assert_eq!(envelope.operation_id, "market.product.search"); 388 assert_eq!(envelope.result["state"], "unconfigured"); 389 assert_eq!(envelope.result["query"], "eggs"); 390 assert_eq!(envelope.result["actions"][0], "radroots store init"); 391 } 392 393 #[test] 394 fn market_listing_get_requires_lookup_key() { 395 let dir = tempdir().expect("tempdir"); 396 let config = sample_config(dir.path()); 397 let service = OperationAdapter::new(MarketOperationService::new(&config)); 398 let request = OperationRequest::new( 399 OperationContext::default(), 400 MarketListingGetRequest::default(), 401 ) 402 .expect("market listing get request"); 403 let error = service.execute(request).expect_err("key required"); 404 405 assert!(format!("{error}").contains("`key`")); 406 } 407 408 #[test] 409 fn market_listing_get_wraps_listing_runtime_with_target_actions() { 410 let dir = tempdir().expect("tempdir"); 411 let config = sample_config(dir.path()); 412 let service = OperationAdapter::new(MarketOperationService::new(&config)); 413 let request = OperationRequest::new( 414 OperationContext::default(), 415 MarketListingGetRequest::from_data(data(&[("key", "eggs")])), 416 ) 417 .expect("market listing get request"); 418 let envelope = service 419 .execute(request) 420 .expect("market listing get result") 421 .to_envelope(OperationContext::default().envelope_context("req_market_listing")) 422 .expect("market listing get envelope"); 423 424 assert_eq!(envelope.operation_id, "market.listing.get"); 425 assert_eq!(envelope.result["state"], "unconfigured"); 426 assert_eq!(envelope.result["actions"][0], "radroots store init"); 427 } 428 429 #[test] 430 fn market_ready_actions_do_not_emit_incomplete_basket_item_add_commands() { 431 let search = market_product_search_view(FindView { 432 state: "ready".to_owned(), 433 source: "test".to_owned(), 434 query: "eggs".to_owned(), 435 count: 1, 436 relay_count: 1, 437 replica_db: "ready".to_owned(), 438 freshness: freshness(), 439 results: vec![FindResultView { 440 id: "listing_eggs".to_owned(), 441 product_key: "eggs".to_owned(), 442 readiness: readiness_enabled(), 443 listing_addr: Some(LISTING_ADDR.to_owned()), 444 primary_bin_id: Some("bin-1".to_owned()), 445 title: "Eggs".to_owned(), 446 category: "eggs".to_owned(), 447 summary: None, 448 location_primary: None, 449 available: quantity(), 450 price: price(), 451 provenance: provenance(), 452 hyf: None, 453 }], 454 hyf: None, 455 reason: None, 456 actions: Vec::new(), 457 }); 458 459 assert_eq!( 460 search.actions, 461 vec![ 462 "radroots market listing get eggs".to_owned(), 463 "radroots basket create".to_owned() 464 ] 465 ); 466 467 let listing = market_listing_get_view(ListingGetView { 468 state: "ready".to_owned(), 469 source: "test".to_owned(), 470 lookup: "eggs".to_owned(), 471 readiness: readiness_enabled(), 472 listing_id: Some("listing_eggs".to_owned()), 473 product_key: Some("eggs".to_owned()), 474 listing_addr: Some(LISTING_ADDR.to_owned()), 475 primary_bin_id: Some("bin-1".to_owned()), 476 title: Some("Eggs".to_owned()), 477 category: Some("eggs".to_owned()), 478 description: None, 479 location_primary: None, 480 available: Some(quantity()), 481 price: Some(price()), 482 provenance: provenance(), 483 reason: None, 484 actions: Vec::new(), 485 }); 486 487 assert_eq!(listing.actions, vec!["radroots basket create".to_owned()]); 488 assert!( 489 search 490 .actions 491 .iter() 492 .chain(listing.actions.iter()) 493 .all(|action| !action.starts_with("radroots basket item add ")) 494 ); 495 } 496 497 #[test] 498 fn market_ready_actions_require_order_request_enabled() { 499 let disabled_search = market_product_search_view(FindView { 500 state: "ready".to_owned(), 501 source: "test".to_owned(), 502 query: "eggs".to_owned(), 503 count: 1, 504 relay_count: 1, 505 replica_db: "ready".to_owned(), 506 freshness: freshness(), 507 results: vec![FindResultView { 508 id: "listing_eggs".to_owned(), 509 product_key: "eggs".to_owned(), 510 readiness: readiness_disabled(), 511 listing_addr: Some(LISTING_ADDR.to_owned()), 512 primary_bin_id: Some("bin-1".to_owned()), 513 title: "Eggs".to_owned(), 514 category: "eggs".to_owned(), 515 summary: None, 516 location_primary: None, 517 available: quantity(), 518 price: price(), 519 provenance: provenance(), 520 hyf: None, 521 }], 522 hyf: None, 523 reason: None, 524 actions: Vec::new(), 525 }); 526 527 assert_eq!( 528 disabled_search.actions, 529 vec!["radroots market listing get eggs".to_owned()] 530 ); 531 532 let disabled_listing = market_listing_get_view(ListingGetView { 533 state: "ready".to_owned(), 534 source: "test".to_owned(), 535 lookup: "eggs".to_owned(), 536 readiness: readiness_disabled(), 537 listing_id: Some("listing_eggs".to_owned()), 538 product_key: Some("eggs".to_owned()), 539 listing_addr: Some(LISTING_ADDR.to_owned()), 540 primary_bin_id: Some("bin-1".to_owned()), 541 title: Some("Eggs".to_owned()), 542 category: Some("eggs".to_owned()), 543 description: None, 544 location_primary: None, 545 available: Some(quantity()), 546 price: Some(price()), 547 provenance: provenance(), 548 reason: None, 549 actions: Vec::new(), 550 }); 551 552 assert!(disabled_listing.actions.is_empty()); 553 } 554 555 fn freshness() -> SyncFreshnessView { 556 SyncFreshnessView { 557 state: "fresh".to_owned(), 558 display: "fresh".to_owned(), 559 age_seconds: Some(0), 560 last_event_at: Some(0), 561 run: None, 562 } 563 } 564 565 fn provenance() -> FindResultProvenanceView { 566 FindResultProvenanceView { 567 origin: "fixture".to_owned(), 568 freshness: "fresh".to_owned(), 569 relay_count: 1, 570 } 571 } 572 573 fn quantity() -> FindQuantityView { 574 FindQuantityView { 575 total_amount: 1.0, 576 total_unit: "each".to_owned(), 577 label: None, 578 available_amount: Some(1), 579 } 580 } 581 582 fn price() -> FindPriceView { 583 FindPriceView { 584 amount: 6.0, 585 currency: "USD".to_owned(), 586 per_amount: 1.0, 587 per_unit: "each".to_owned(), 588 } 589 } 590 591 fn readiness_enabled() -> MarketReadinessView { 592 MarketReadinessView { 593 protocol_valid: true, 594 marketplace_eligible: true, 595 order_request_enabled: true, 596 primary_bin_verified: true, 597 reason_codes: Vec::new(), 598 } 599 } 600 601 fn readiness_disabled() -> MarketReadinessView { 602 MarketReadinessView { 603 protocol_valid: true, 604 marketplace_eligible: true, 605 order_request_enabled: false, 606 primary_bin_verified: true, 607 reason_codes: vec![ 608 "listing_order_request_disabled".to_owned(), 609 "listing_inventory_unavailable".to_owned(), 610 ], 611 } 612 } 613 614 fn sample_config(root: &Path) -> RuntimeConfig { 615 let data = root.join("data"); 616 let logs = root.join("logs"); 617 let secrets = root.join("secrets"); 618 RuntimeConfig { 619 output: OutputConfig { 620 format: OutputFormat::Human, 621 verbosity: Verbosity::Normal, 622 color: true, 623 dry_run: false, 624 }, 625 interaction: InteractionConfig { 626 input_enabled: true, 627 assume_yes: false, 628 stdin_tty: false, 629 stdout_tty: false, 630 prompts_allowed: false, 631 confirmations_allowed: false, 632 }, 633 paths: PathsConfig { 634 profile: "interactive_user".into(), 635 profile_source: "test".into(), 636 allowed_profiles: vec!["interactive_user".into(), "repo_local".into()], 637 root_source: "test".into(), 638 repo_local_root: None, 639 repo_local_root_source: None, 640 subordinate_path_override_source: "runtime_config".into(), 641 app_namespace: "apps/cli".into(), 642 shared_accounts_namespace: "shared/accounts".into(), 643 shared_identities_namespace: "shared/identities".into(), 644 app_config_path: root.join("config/apps/cli/config.toml"), 645 workspace_config_path: None, 646 app_data_root: data.join("apps/cli"), 647 app_logs_root: logs.join("apps/cli"), 648 shared_accounts_data_root: data.join("shared/accounts"), 649 shared_accounts_secrets_root: secrets.join("shared/accounts"), 650 default_identity_path: secrets.join("shared/identities/default.json"), 651 }, 652 migration: MigrationConfig { 653 report: RadrootsMigrationReport::empty(), 654 }, 655 logging: LoggingConfig { 656 filter: "info".into(), 657 directory: None, 658 stdout: false, 659 }, 660 account: AccountConfig { 661 selector: None, 662 store_path: data.join("shared/accounts/store.json"), 663 secrets_dir: secrets.join("shared/accounts"), 664 secret_backend: RadrootsSecretBackend::EncryptedFile, 665 secret_fallback: None, 666 }, 667 account_secret_contract: AccountSecretContractConfig { 668 default_backend: "host_vault".into(), 669 default_fallback: Some("encrypted_file".into()), 670 allowed_backends: vec!["host_vault".into(), "encrypted_file".into()], 671 host_vault_policy: Some("desktop".into()), 672 uses_protected_store: true, 673 }, 674 identity: IdentityConfig { 675 path: secrets.join("shared/identities/default.json"), 676 }, 677 signer: SignerConfig { 678 backend: SignerBackend::Local, 679 }, 680 publish: PublishConfig { 681 transport: PublishTransport::DirectNostrRelay, 682 source: PublishTransportSource::Defaults, 683 radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(), 684 }, 685 relay: RelayConfig { 686 urls: Vec::new(), 687 publish_policy: RelayPublishPolicy::Any, 688 source: RelayConfigSource::Defaults, 689 }, 690 local: LocalConfig { 691 root: data.join("apps/cli/replica"), 692 replica_db_path: data.join("apps/cli/replica/replica.sqlite"), 693 backups_dir: data.join("apps/cli/replica/backups"), 694 exports_dir: data.join("apps/cli/replica/exports"), 695 }, 696 myc: MycConfig { 697 executable: PathBuf::from("myc"), 698 status_timeout_ms: 2_000, 699 }, 700 hyf: HyfConfig { 701 enabled: false, 702 executable: PathBuf::from("hyfd"), 703 }, 704 rpc: RpcConfig { 705 url: "http://127.0.0.1:7070".into(), 706 }, 707 rhi: crate::runtime::config::RhiConfig { 708 trusted_worker_pubkeys: Vec::new(), 709 }, 710 capability_bindings: Vec::new(), 711 } 712 } 713 714 fn data(entries: &[(&str, &str)]) -> OperationData { 715 entries 716 .iter() 717 .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned()))) 718 .collect::<Map<String, Value>>() 719 } 720 }