app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 66bf69b8e7b8635d18e2309d80edb584a3dc74fb
parent 881c00414cf060c4c4c51980bc6da2bbb70faaf9
Author: triesap <tyson@radroots.org>
Date:   Fri,  6 Feb 2026 16:06:20 +0000

app: add config flow draft and validation

- add config flow steps and draft fields for role data
- validate profile and role-specific required inputs
- add flow step helpers and unit tests
- export config flow api from app crate

Diffstat:
Aapp/src/config_flow.rs | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/src/lib.rs | 10++++++++++
2 files changed, 189 insertions(+), 0 deletions(-)

diff --git a/app/src/config_flow.rs b/app/src/config_flow.rs @@ -0,0 +1,179 @@ +#![forbid(unsafe_code)] + +use crate::RadrootsAppRole; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsAppConfigStep { + Profile, + Role, + Preferences, +} + +pub const fn app_config_step_default() -> RadrootsAppConfigStep { + RadrootsAppConfigStep::Profile +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsAppConfigFlowDraft { + pub step: RadrootsAppConfigStep, + pub profile_name: String, + pub profile_location: String, + pub role: Option<RadrootsAppRole>, + pub farmer_farm_name: String, + pub farmer_location: String, + pub farmer_products: Vec<String>, + pub individual_name: String, + pub individual_location: String, + pub individual_products: Vec<String>, + pub business_name: String, + pub business_location: String, + pub business_operations: String, + pub notifications_orders: bool, + pub notifications_messages: bool, + pub payment_method: String, +} + +impl Default for RadrootsAppConfigFlowDraft { + fn default() -> Self { + Self { + step: app_config_step_default(), + profile_name: String::new(), + profile_location: String::new(), + role: None, + farmer_farm_name: String::new(), + farmer_location: String::new(), + farmer_products: Vec::new(), + individual_name: String::new(), + individual_location: String::new(), + individual_products: Vec::new(), + business_name: String::new(), + business_location: String::new(), + business_operations: String::new(), + notifications_orders: true, + notifications_messages: true, + payment_method: String::new(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsAppConfigFlowValidation { + pub can_continue: bool, + pub can_back: bool, + pub next_step: RadrootsAppConfigStep, + pub prev_step: RadrootsAppConfigStep, +} + +pub fn app_config_flow_next_step(draft: &RadrootsAppConfigFlowDraft) -> RadrootsAppConfigStep { + match draft.step { + RadrootsAppConfigStep::Profile => RadrootsAppConfigStep::Role, + RadrootsAppConfigStep::Role => RadrootsAppConfigStep::Preferences, + RadrootsAppConfigStep::Preferences => RadrootsAppConfigStep::Preferences, + } +} + +pub fn app_config_flow_prev_step(draft: &RadrootsAppConfigFlowDraft) -> RadrootsAppConfigStep { + match draft.step { + RadrootsAppConfigStep::Profile => RadrootsAppConfigStep::Profile, + RadrootsAppConfigStep::Role => RadrootsAppConfigStep::Profile, + RadrootsAppConfigStep::Preferences => RadrootsAppConfigStep::Role, + } +} + +fn has_text(value: &str) -> bool { + !value.trim().is_empty() +} + +fn has_items(values: &[String]) -> bool { + values.iter().any(|value| !value.trim().is_empty()) +} + +fn role_step_valid(draft: &RadrootsAppConfigFlowDraft) -> bool { + match draft.role { + Some(RadrootsAppRole::Farm) => { + has_text(&draft.farmer_farm_name) + && has_text(&draft.farmer_location) + && has_items(&draft.farmer_products) + } + Some(RadrootsAppRole::Individual) => { + has_text(&draft.individual_name) + && has_text(&draft.individual_location) + && has_items(&draft.individual_products) + } + Some(RadrootsAppRole::Business) => { + has_text(&draft.business_name) + && has_text(&draft.business_location) + && has_text(&draft.business_operations) + } + None => false, + } +} + +pub fn app_config_flow_validate(draft: &RadrootsAppConfigFlowDraft) -> RadrootsAppConfigFlowValidation { + let can_continue = match draft.step { + RadrootsAppConfigStep::Profile => { + has_text(&draft.profile_name) && has_text(&draft.profile_location) + } + RadrootsAppConfigStep::Role => role_step_valid(draft), + RadrootsAppConfigStep::Preferences => true, + }; + let can_back = !matches!(draft.step, RadrootsAppConfigStep::Profile); + RadrootsAppConfigFlowValidation { + can_continue, + can_back, + next_step: app_config_flow_next_step(draft), + prev_step: app_config_flow_prev_step(draft), + } +} + +#[cfg(test)] +mod tests { + use super::{ + app_config_flow_next_step, + app_config_flow_prev_step, + app_config_flow_validate, + RadrootsAppConfigFlowDraft, + RadrootsAppConfigStep, + }; + use crate::RadrootsAppRole; + + #[test] + fn flow_defaults_to_profile() { + let draft = RadrootsAppConfigFlowDraft::default(); + assert_eq!(draft.step, RadrootsAppConfigStep::Profile); + assert!(draft.notifications_orders); + assert!(draft.notifications_messages); + } + + #[test] + fn flow_step_transitions() { + let mut draft = RadrootsAppConfigFlowDraft::default(); + assert_eq!(app_config_flow_next_step(&draft), RadrootsAppConfigStep::Role); + draft.step = RadrootsAppConfigStep::Role; + assert_eq!(app_config_flow_next_step(&draft), RadrootsAppConfigStep::Preferences); + draft.step = RadrootsAppConfigStep::Preferences; + assert_eq!(app_config_flow_next_step(&draft), RadrootsAppConfigStep::Preferences); + assert_eq!(app_config_flow_prev_step(&draft), RadrootsAppConfigStep::Role); + } + + #[test] + fn flow_validation_requires_profile_fields() { + let draft = RadrootsAppConfigFlowDraft::default(); + let validation = app_config_flow_validate(&draft); + assert!(!validation.can_continue); + } + + #[test] + fn flow_validation_requires_role_fields() { + let mut draft = RadrootsAppConfigFlowDraft::default(); + draft.step = RadrootsAppConfigStep::Role; + draft.role = Some(RadrootsAppRole::Farm); + draft.farmer_farm_name = String::from("Radroots Farm"); + draft.farmer_location = String::from("Valley"); + let validation = app_config_flow_validate(&draft); + assert!(!validation.can_continue); + draft.farmer_products = vec![String::from("tomatoes")]; + let validation = app_config_flow_validate(&draft); + assert!(validation.can_continue); + } +} diff --git a/app/src/lib.rs b/app/src/lib.rs @@ -4,6 +4,7 @@ mod app; mod bootstrap; mod context; mod config; +mod config_flow; mod configuration; mod data; mod health; @@ -252,6 +253,15 @@ pub use config::{ APP_KEYSTORE_KEY_NOSTR_DEFAULT, app_datastore_key_setup_lock, }; +pub use config_flow::{ + app_config_flow_next_step, + app_config_flow_prev_step, + app_config_flow_validate, + app_config_step_default, + RadrootsAppConfigFlowDraft, + RadrootsAppConfigFlowValidation, + RadrootsAppConfigStep, +}; pub use init::{ app_init_assets, app_init_backends,