cli

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

commit ab5f64368cfb63adbc53c6578d925b4bab2465d3
parent 73a4cfc6dc523046d53d585fb188eb04c44ced8d
Author: triesap <tyson@radroots.org>
Date:   Fri, 26 Jun 2026 23:43:44 +0000

cli: add geonames farm location execution

Wire farm location set through the SDK forward-location result model, add typed operation inputs for exact, city, query, and GeoNames id modes, and surface no-match/ambiguous lookup failures with structured detail.

Validate parser, adapter, and farm-location result handling with focused cargo extbuild test lanes.

Diffstat:
Msrc/cli/global.rs | 20+++++++++++++++++---
Msrc/cli/input.rs | 4++--
Msrc/ops/exec/farm.rs | 353+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/ops/mod.rs | 39+++++++++++++++++++++++++++++++++++++++
Msrc/runtime/farm.rs | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/view/runtime.rs | 19+++++++++++++++++++
6 files changed, 548 insertions(+), 48 deletions(-)

diff --git a/src/cli/global.rs b/src/cli/global.rs @@ -135,9 +135,23 @@ pub struct FarmUpdateArgs { #[derive(Debug, Clone)] pub struct FarmPrivateLocationSetArgs { pub farm_d_tag: Option<String>, - pub latitude: f64, - pub longitude: f64, - pub lookup: String, + pub input: FarmPrivateLocationSetInput, + pub label: Option<String>, +} + +#[derive(Debug, Clone)] +pub enum FarmPrivateLocationSetInput { + Exact { + latitude: f64, + longitude: f64, + }, + City { + city: String, + region: Option<String>, + country: Option<String>, + }, + Query(String), + GeonamesId(i64), } #[derive(Debug, Clone, Default)] diff --git a/src/cli/input.rs b/src/cli/input.rs @@ -105,10 +105,10 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData { FarmCommand::Location(args) => match &args.command { FarmLocationCommand::Set(args) => { if let Some(latitude) = args.lat { - insert_number(&mut input, "latitude", latitude); + insert_number(&mut input, "lat", latitude); } if let Some(longitude) = args.lng { - insert_number(&mut input, "longitude", longitude); + insert_number(&mut input, "lng", longitude); } insert_string(&mut input, "farm_d_tag", &args.farm_d_tag); insert_string(&mut input, "city", &args.city); diff --git a/src/ops/exec/farm.rs b/src/ops/exec/farm.rs @@ -3,7 +3,8 @@ use serde_json::Value; use crate::cli::global::{ FarmCreateArgs, FarmFieldArg, FarmPrivateLocationKeyArgs, FarmPrivateLocationSetArgs, - FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs, FarmUpdateArgs, + FarmPrivateLocationSetInput, FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs, + FarmUpdateArgs, }; use crate::ops::{ FarmCreateRequest, FarmCreateResult, FarmFulfillmentUpdateRequest, FarmFulfillmentUpdateResult, @@ -130,24 +131,16 @@ impl OperationService<FarmLocationSetRequest> for FarmOperationService<'_> { &self, request: OperationRequest<FarmLocationSetRequest>, ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { - let lookup = string_input(&request, "lookup").unwrap_or_else(|| "geonames".to_owned()); - if lookup != "geonames" { - return Err(invalid_input( - request.operation_id(), - format!("farm location lookup `{lookup}` is not supported"), - )); - } let args = FarmPrivateLocationSetArgs { farm_d_tag: string_input(&request, "farm_d_tag"), - latitude: required_f64(&request, "latitude")?, - longitude: required_f64(&request, "longitude")?, - lookup, + input: farm_private_location_input(&request)?, + label: string_input(&request, "label"), }; let view = crate::runtime::farm::private_location_set(self.config, &args).map_err(|error| { OperationAdapterError::sdk_adapter_failure(request.operation_id(), error) })?; - serialized_operation_result::<FarmLocationSetResult, _>(&view) + farm_private_location_set_result(request.operation_id(), &view) } } @@ -348,6 +341,29 @@ fn farm_publish_result( } } +fn farm_private_location_set_result( + operation_id: &str, + view: &crate::view::runtime::FarmPrivateLocationView, +) -> Result<OperationResult<FarmLocationSetResult>, OperationAdapterError> { + match view.state.as_str() { + "no_match" => Err(OperationAdapterError::not_found_with_detail( + operation_id, + view.reason + .clone() + .unwrap_or_else(|| "GeoNames lookup returned no matching locality".to_owned()), + serde_json::to_value(view).unwrap_or(Value::Null), + )), + "ambiguous" => Err(OperationAdapterError::validation_failed_with_detail( + operation_id, + view.reason + .clone() + .unwrap_or_else(|| "GeoNames lookup matched multiple localities".to_owned()), + serde_json::to_value(view).unwrap_or(Value::Null), + )), + _ => serialized_operation_result::<FarmLocationSetResult, _>(view), + } +} + fn farm_publish_relay_unavailable(view: &FarmPublishView) -> bool { view.state == "partial" || !view.profile.failed_relays.is_empty() @@ -422,7 +438,89 @@ where .map(str::to_owned) } -fn required_f64<P>(request: &OperationRequest<P>, key: &str) -> Result<f64, OperationAdapterError> +fn farm_private_location_input<P>( + request: &OperationRequest<P>, +) -> Result<FarmPrivateLocationSetInput, OperationAdapterError> +where + P: OperationRequestPayload + OperationRequestData, +{ + if request.payload.input().contains_key("lookup") { + return Err(invalid_input( + request.operation_id(), + "`lookup` is not a supported farm location input".to_owned(), + )); + } + + let lat = optional_f64(request, "lat")?; + let lng = optional_f64(request, "lng")?; + let city = trimmed_string_input(request, "city")?; + let region = trimmed_string_input(request, "region")?; + let country = trimmed_string_input(request, "country")?; + let query = trimmed_string_input(request, "query")?; + let geonames_id = optional_i64(request, "geonames_id")?; + + if (lat.is_some() || lng.is_some()) && !(lat.is_some() && lng.is_some()) { + return Err(invalid_input( + request.operation_id(), + "`lat` and `lng` must be provided together".to_owned(), + )); + } + if (region.is_some() || country.is_some()) && city.is_none() { + return Err(invalid_input( + request.operation_id(), + "`region` and `country` require `city`".to_owned(), + )); + } + + let mode_count = usize::from(lat.is_some()) + + usize::from(city.is_some()) + + usize::from(query.is_some()) + + usize::from(geonames_id.is_some()); + + if mode_count != 1 { + return Err(invalid_input( + request.operation_id(), + "farm location requires exactly one of `lat`/`lng`, `city`, `query`, or `geonames_id`" + .to_owned(), + )); + } + + if let (Some(latitude), Some(longitude)) = (lat, lng) { + return Ok(FarmPrivateLocationSetInput::Exact { + latitude, + longitude, + }); + } + if let Some(city) = city { + return Ok(FarmPrivateLocationSetInput::City { + city, + region, + country, + }); + } + if let Some(query) = query { + return Ok(FarmPrivateLocationSetInput::Query(query)); + } + if let Some(geonames_id) = geonames_id { + if geonames_id <= 0 { + return Err(invalid_input( + request.operation_id(), + "`geonames_id` must be a positive integer".to_owned(), + )); + } + return Ok(FarmPrivateLocationSetInput::GeonamesId(geonames_id)); + } + + Err(invalid_input( + request.operation_id(), + "farm location input could not be resolved".to_owned(), + )) +} + +fn optional_f64<P>( + request: &OperationRequest<P>, + key: &str, +) -> Result<Option<f64>, OperationAdapterError> where P: OperationRequestPayload + OperationRequestData, { @@ -430,14 +528,62 @@ where .payload .input() .get(key) - .and_then(Value::as_f64) - .filter(|value| value.is_finite()) - .ok_or_else(|| { - invalid_input( - request.operation_id(), - format!("missing required finite `{key}` input"), - ) + .map(|value| { + value + .as_f64() + .filter(|value| value.is_finite()) + .ok_or_else(|| { + invalid_input( + request.operation_id(), + format!("`{key}` must be a finite number"), + ) + }) }) + .transpose() +} + +fn optional_i64<P>( + request: &OperationRequest<P>, + key: &str, +) -> Result<Option<i64>, OperationAdapterError> +where + P: OperationRequestPayload + OperationRequestData, +{ + request + .payload + .input() + .get(key) + .map(|value| { + value.as_i64().ok_or_else(|| { + invalid_input( + request.operation_id(), + format!("`{key}` must be an integer"), + ) + }) + }) + .transpose() +} + +fn trimmed_string_input<P>( + request: &OperationRequest<P>, + key: &str, +) -> Result<Option<String>, OperationAdapterError> +where + P: OperationRequestPayload + OperationRequestData, +{ + string_input(request, key) + .map(|value| { + let trimmed = value.trim(); + if trimmed.is_empty() { + Err(invalid_input( + request.operation_id(), + format!("`{key}` must not be empty"), + )) + } else { + Ok(trimmed.to_owned()) + } + }) + .transpose() } fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool> @@ -460,13 +606,14 @@ mod tests { use radroots_runtime_paths::RadrootsMigrationReport; use radroots_secret_vault::RadrootsSecretBackend; - use serde_json::{Map, Value}; + use serde_json::{Map, Value, json}; use tempfile::tempdir; use super::FarmOperationService; use crate::ops::{ - FarmCreateRequest, FarmGetRequest, FarmPublishRequest, FarmReadinessCheckRequest, - FarmRebindRequest, OperationAdapter, OperationContext, OperationData, OperationRequest, + FarmCreateRequest, FarmGetRequest, FarmLocationSetRequest, FarmPublishRequest, + FarmReadinessCheckRequest, FarmRebindRequest, OperationAdapter, OperationContext, + OperationData, OperationRequest, }; use crate::runtime::config::{ AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig, @@ -475,6 +622,9 @@ mod tests { RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, SignerConfig, Verbosity, }; + use crate::view::runtime::{ + FarmPrivateExactLocationView, FarmPrivateLocationCandidateView, FarmPrivateLocationView, + }; #[test] fn farm_service_reports_missing_farm_config() { @@ -560,6 +710,132 @@ mod tests { assert_eq!(error.to_output_error().exit_code, 6); } + #[test] + fn farm_service_accepts_canonical_location_set_modes() { + let dir = tempdir().expect("tempdir"); + let config = sample_config(dir.path()); + let service = OperationAdapter::new(FarmOperationService::new(&config)); + let cases = [ + value_data(&[ + ("lat", json!(48.429456)), + ("lng", json!(-123.349786)), + ("label", json!("farm gate")), + ]), + value_data(&[ + ("city", json!("Victoria")), + ("region", json!("BC")), + ("country", json!("CA")), + ]), + value_data(&[("query", json!("Victoria, BC"))]), + value_data(&[("geonames_id", json!(6174041))]), + ]; + + for input in cases { + let request = OperationRequest::new( + OperationContext::default(), + FarmLocationSetRequest::from_data(input), + ) + .expect("farm location request"); + let envelope = service + .execute(request) + .expect("farm location set result") + .to_envelope(OperationContext::default().envelope_context("req_farm_location")) + .expect("farm location envelope"); + + assert_eq!(envelope.operation_id, "farm.location.set"); + assert_eq!(envelope.result["state"], "unconfigured"); + } + } + + #[test] + fn farm_service_rejects_invalid_location_set_modes() { + let dir = tempdir().expect("tempdir"); + let config = sample_config(dir.path()); + let service = OperationAdapter::new(FarmOperationService::new(&config)); + let cases = [ + ( + value_data(&[("lookup", json!("Victoria, BC"))]), + "`lookup` is not a supported farm location input", + ), + ( + value_data(&[("lat", json!(48.429456))]), + "`lat` and `lng` must be provided together", + ), + ( + value_data(&[ + ("lat", json!(48.429456)), + ("lng", json!(-123.349786)), + ("city", json!("Victoria")), + ]), + "requires exactly one", + ), + ( + value_data(&[("query", json!("Victoria")), ("country", json!("CA"))]), + "`region` and `country` require `city`", + ), + ( + value_data(&[("geonames_id", json!(0))]), + "`geonames_id` must be a positive integer", + ), + ( + value_data(&[("city", json!(" "))]), + "`city` must not be empty", + ), + ]; + + for (input, expected) in cases { + let request = OperationRequest::new( + OperationContext::default(), + FarmLocationSetRequest::from_data(input), + ) + .expect("farm location request"); + let error = service + .execute(request) + .expect_err("invalid location input"); + + assert!(format!("{error}").contains(expected)); + } + } + + #[test] + fn farm_location_set_maps_lookup_failures_to_output_errors() { + let no_match = location_lookup_view("no_match", Vec::new()); + let no_match_error = + super::farm_private_location_set_result("farm.location.set", &no_match) + .expect_err("no match error") + .to_output_error(); + assert_eq!(no_match_error.code, "not_found"); + assert_eq!( + no_match_error.detail.as_ref().expect("no match detail")["state"], + "no_match" + ); + + let ambiguous = location_lookup_view( + "ambiguous", + vec![FarmPrivateLocationCandidateView { + geonames_feature_id: 3002, + geonames_country_id: "CA".to_owned(), + name: "Shared Market".to_owned(), + display_name: "Shared Market, British Columbia, Canada".to_owned(), + exact_location: FarmPrivateExactLocationView { + lat: 48.7, + lng: -123.2, + }, + region: Some("British Columbia".to_owned()), + country: Some("Canada".to_owned()), + }], + ); + let ambiguous_error = + super::farm_private_location_set_result("farm.location.set", &ambiguous) + .expect_err("ambiguous error") + .to_output_error(); + assert_eq!(ambiguous_error.code, "validation_failed"); + assert_eq!( + ambiguous_error.detail.as_ref().expect("ambiguous detail")["candidates"][0]["geonames_feature_id"], + 3002 + ); + } + fn sample_config(root: &Path) -> RuntimeConfig { let data = root.join("data"); let cache = root.join("cache"); @@ -668,4 +944,35 @@ mod tests { .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned()))) .collect::<Map<String, Value>>() } + + fn value_data(entries: &[(&str, Value)]) -> OperationData { + entries + .iter() + .map(|(key, value)| ((*key).to_owned(), value.clone())) + .collect::<Map<String, Value>>() + } + + fn location_lookup_view( + state: &str, + candidates: Vec<FarmPrivateLocationCandidateView>, + ) -> FarmPrivateLocationView { + FarmPrivateLocationView { + state: state.to_owned(), + source: "test".to_owned(), + farm_addr: Some("30401:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAA".to_owned()), + farm_d_tag: Some("AAAAAAAAAAAAAAAAAAAAAA".to_owned()), + seller_account_id: Some("acct_test".to_owned()), + seller_pubkey: Some("1111111111111111111111111111111111111111111111111111111111111111".to_owned()), + label: None, + exact_location: None, + public_locality: None, + geonames_feature_id: None, + geonames_country_id: None, + geonames_database_path: Some("cache/shared/geonames/geonames-1.0.db".to_owned()), + cleared: None, + candidates, + reason: Some(format!("{state} reason")), + actions: Vec::new(), + } + } } diff --git a/src/ops/mod.rs b/src/ops/mod.rs @@ -171,6 +171,45 @@ mod tests { } #[test] + fn adapter_maps_farm_location_set_inputs() { + let parsed = TargetCliArgs::try_parse_from([ + "radroots", + "farm", + "location", + "set", + "--lat", + "48.429456", + "--lng", + "-123.349786", + "--label", + "farm gate", + ]) + .expect("target args parse"); + + let request = TargetOperationRequest::from_target_args(&parsed) + .expect("operation request from target args"); + let TargetOperationRequest::FarmLocationSet(request) = request else { + panic!("expected farm location set request") + }; + + assert_eq!(request.operation_id(), "farm.location.set"); + assert_eq!( + request.payload.input.get("lat").and_then(Value::as_f64), + Some(48.429456) + ); + assert_eq!( + request.payload.input.get("lng").and_then(Value::as_f64), + Some(-123.349786) + ); + assert_eq!( + request.payload.input.get("label").and_then(Value::as_str), + Some("farm gate") + ); + assert!(!request.payload.input.contains_key("latitude")); + assert!(!request.payload.input.contains_key("longitude")); + } + + #[test] fn adapter_maps_listing_rebind_inputs() { let parsed = TargetCliArgs::try_parse_from([ "radroots", diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -12,15 +12,18 @@ use radroots_events_codec::d_tag::is_d_tag_base64url; use radroots_events_codec::profile::encode::to_wire_parts_with_profile_type; use radroots_sdk::{ FarmEnqueuePublishRequest, FarmEnqueueReceipt, FarmPreparePublishRequest, - FarmPrivateLocationClearRequest, FarmPrivateLocationReceipt, FarmPrivateLocationUpsertRequest, - FarmPublishPlan, PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, - PushOutboxRelayOutcomeKind, PushOutboxRequest, SdkExactLocation, SdkMutationState, + FarmPrivateLocationClearRequest, FarmPrivateLocationInput, FarmPrivateLocationLookupCandidate, + FarmPrivateLocationReceipt, FarmPrivateLocationSetRequest, FarmPrivateLocationSetResult, + FarmPublishPlan, GeocoderLocalityQuery, PushOutboxEventReceipt, PushOutboxEventState, + PushOutboxReceipt, PushOutboxRelayOutcomeKind, PushOutboxRequest, SdkExactLocation, + SdkMutationState, }; use serde_json::json; use crate::cli::global::{ FarmCreateArgs, FarmFieldArg, FarmPrivateLocationKeyArgs, FarmPrivateLocationSetArgs, - FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs, FarmUpdateArgs, + FarmPrivateLocationSetInput, FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs, + FarmUpdateArgs, }; use crate::runtime::RuntimeError; use crate::runtime::account::{self, AccountRecordView}; @@ -37,10 +40,10 @@ use crate::runtime::sdk::{ use crate::runtime::signer::ActorWriteBindingError; use crate::view::runtime::{ FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView, - FarmPrivateExactLocationView, FarmPrivateLocationView, FarmPrivatePublicLocalityView, - FarmPublicationView, FarmPublishComponentView, FarmPublishEventView, FarmPublishView, - FarmRebindView, FarmSelectionView, FarmSetView, FarmSetupView, FarmStatusView, - RelayFailureView, + FarmPrivateExactLocationView, FarmPrivateLocationCandidateView, FarmPrivateLocationView, + FarmPrivatePublicLocalityView, FarmPublicationView, FarmPublishComponentView, + FarmPublishEventView, FarmPublishView, FarmRebindView, FarmSelectionView, FarmSetView, + FarmSetupView, FarmStatusView, RelayFailureView, }; const FARM_CONFIG_SOURCE: &str = "farm config ยท local first"; @@ -352,19 +355,41 @@ pub fn private_location_set( }; let session = CliSdkSession::connect(config)?; let geonames_status = session.sdk().geonames().ensure()?; - let request = FarmPrivateLocationUpsertRequest::new( + let mut request = FarmPrivateLocationSetRequest::new( target.actor.clone(), target.farm_d_tag.clone(), - SdkExactLocation::new(args.latitude, args.longitude), + sdk_private_location_input(&args.input), ); - let receipt = session.block_on(session.sdk().farms().upsert_private_location(request))?; - Ok(private_location_view_from_receipt( - "set", - target, - Some(receipt), - Some(geonames_status.path.display().to_string()), - None, - )) + if let Some(label) = args.label.clone() { + request = request.with_label(label); + } + let geonames_database_path = Some(geonames_status.path.display().to_string()); + match session.block_on(session.sdk().farms().set_private_location(request))? { + FarmPrivateLocationSetResult::Stored(receipt) => Ok(private_location_view_from_receipt( + "set", + target, + Some(receipt), + geonames_database_path, + None, + )), + FarmPrivateLocationSetResult::NoMatch(lookup) => Ok(private_location_view_from_lookup( + "no_match", + target, + lookup.candidates, + geonames_database_path, + Some("GeoNames lookup returned no matching locality".to_owned()), + )), + FarmPrivateLocationSetResult::Ambiguous(lookup) => Ok(private_location_view_from_lookup( + "ambiguous", + target, + lookup.candidates, + geonames_database_path, + Some( + "GeoNames lookup matched multiple localities; retry with --region, --country, or --geonames-id" + .to_owned(), + ), + )), + } } pub fn private_location_get( @@ -414,18 +439,23 @@ pub fn private_location_clear( farm_d_tag: Some(target.farm_d_tag), seller_account_id: Some(target.seller_account_id), seller_pubkey: Some(target.seller_pubkey), + label: None, exact_location: None, public_locality: None, geonames_feature_id: None, geonames_country_id: None, geonames_database_path: None, cleared: Some(receipt.cleared), + candidates: Vec::new(), reason: (!receipt.cleared) .then(|| "no private exact farm location was stored for this farm".to_owned()), actions: if receipt.cleared { Vec::new() } else { - vec!["radroots farm location set --lat <lat> --lng <lng>".to_owned()] + vec![ + "radroots farm location set --city <city> --region <region> --country <country>" + .to_owned(), + ] }, }) } @@ -1398,12 +1428,14 @@ fn private_location_unconfigured_view() -> FarmPrivateLocationView { farm_d_tag: None, seller_account_id: None, seller_pubkey: None, + label: None, exact_location: None, public_locality: None, geonames_feature_id: None, geonames_country_id: None, geonames_database_path: None, cleared: None, + candidates: Vec::new(), reason: Some("create and bind a farm before storing private location".to_owned()), actions: vec![ "radroots account create".to_owned(), @@ -1427,6 +1459,7 @@ fn private_location_view_from_receipt( farm_d_tag: Some(target.farm_d_tag), seller_account_id: Some(target.seller_account_id), seller_pubkey: Some(target.seller_pubkey), + label: receipt_ref.and_then(|receipt| receipt.label.clone()), exact_location: receipt_ref.map(|receipt| FarmPrivateExactLocationView { lat: receipt.exact_location.latitude, lng: receipt.exact_location.longitude, @@ -1442,15 +1475,103 @@ fn private_location_view_from_receipt( geonames_country_id: receipt_ref.and_then(|receipt| receipt.geonames_country_id.clone()), geonames_database_path, cleared: None, + candidates: Vec::new(), reason, actions: if receipt.is_some() { Vec::new() } else { - vec!["radroots farm location set --lat <lat> --lng <lng>".to_owned()] + vec![ + "radroots farm location set --city <city> --region <region> --country <country>" + .to_owned(), + ] }, } } +fn private_location_view_from_lookup( + state: &str, + target: PrivateFarmLocationTarget, + candidates: Vec<FarmPrivateLocationLookupCandidate>, + geonames_database_path: Option<String>, + reason: Option<String>, +) -> FarmPrivateLocationView { + let actions = if state == "ambiguous" { + vec![ + "radroots farm location set --geonames-id <id>".to_owned(), + "radroots farm location set --city <city> --region <region> --country <country>" + .to_owned(), + ] + } else { + vec![ + "radroots farm location set --query \"<city>, <region>, <country>\"".to_owned(), + "radroots farm location set --lat <lat> --lng <lng>".to_owned(), + ] + }; + FarmPrivateLocationView { + state: state.to_owned(), + source: SDK_FARM_PRIVATE_LOCATION_SOURCE.to_owned(), + farm_addr: Some(target.farm_addr.to_string()), + farm_d_tag: Some(target.farm_d_tag), + seller_account_id: Some(target.seller_account_id), + seller_pubkey: Some(target.seller_pubkey), + label: None, + exact_location: None, + public_locality: None, + geonames_feature_id: None, + geonames_country_id: None, + geonames_database_path, + cleared: None, + candidates: private_location_candidate_views(candidates), + reason, + actions, + } +} + +fn sdk_private_location_input(input: &FarmPrivateLocationSetInput) -> FarmPrivateLocationInput { + match input { + FarmPrivateLocationSetInput::Exact { + latitude, + longitude, + } => FarmPrivateLocationInput::exact(SdkExactLocation::new(*latitude, *longitude)), + FarmPrivateLocationSetInput::City { + city, + region, + country, + } => { + let mut query = GeocoderLocalityQuery::structured(city.clone()); + if let Some(region) = region { + query = query.with_region(region.clone()); + } + if let Some(country) = country { + query = query.with_country(country.clone()); + } + FarmPrivateLocationInput::Locality(query) + } + FarmPrivateLocationSetInput::Query(query) => FarmPrivateLocationInput::query(query.clone()), + FarmPrivateLocationSetInput::GeonamesId(id) => FarmPrivateLocationInput::geonames_id(*id), + } +} + +fn private_location_candidate_views( + candidates: Vec<FarmPrivateLocationLookupCandidate>, +) -> Vec<FarmPrivateLocationCandidateView> { + candidates + .into_iter() + .map(|candidate| FarmPrivateLocationCandidateView { + geonames_feature_id: candidate.geonames_feature_id, + geonames_country_id: candidate.geonames_country_id, + name: candidate.name, + display_name: candidate.display_name, + exact_location: FarmPrivateExactLocationView { + lat: candidate.exact_location.latitude, + lng: candidate.exact_location.longitude, + }, + region: candidate.region, + country: candidate.country, + }) + .collect() +} + fn init_document( scope: FarmConfigScope, account: &AccountRecordView, diff --git a/src/view/runtime.rs b/src/view/runtime.rs @@ -820,6 +820,8 @@ pub struct FarmPrivateLocationView { pub seller_account_id: Option<String>, pub seller_pubkey: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] pub exact_location: Option<FarmPrivateExactLocationView>, #[serde(skip_serializing_if = "Option::is_none")] pub public_locality: Option<FarmPrivatePublicLocalityView>, @@ -831,6 +833,8 @@ pub struct FarmPrivateLocationView { pub geonames_database_path: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub cleared: Option<bool>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub candidates: Vec<FarmPrivateLocationCandidateView>, #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] @@ -855,11 +859,26 @@ pub struct FarmPrivatePublicLocalityView { pub geohash5: String, } +#[derive(Debug, Clone, Serialize)] +pub struct FarmPrivateLocationCandidateView { + pub geonames_feature_id: i64, + pub geonames_country_id: String, + pub name: String, + pub display_name: String, + pub exact_location: FarmPrivateExactLocationView, + #[serde(skip_serializing_if = "Option::is_none")] + pub region: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub country: Option<String>, +} + impl FarmPrivateLocationView { pub fn disposition(&self) -> CommandDisposition { match self.state.as_str() { "unconfigured" => CommandDisposition::Unconfigured, "missing" => CommandDisposition::NotFound, + "no_match" => CommandDisposition::NotFound, + "ambiguous" => CommandDisposition::ValidationFailed, _ => CommandDisposition::Success, } }