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:
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,
}
}