plana 0.2.2

PLANA protocol foundation: the canonical JSON-RPC 2.0 wire layer (unix-socket / HTTP / WS bindings, typed method routing) merged with the generic protocol core (handshake, identity, RBAC, regions, health descriptors) — the base every plana-rpc-* and domain-profile crate builds on.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use ts_rs::TS;

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, TS, JsonSchema, Default)]
#[ts(export, export_to = "region.ts")]
#[serde(rename_all = "snake_case")]
pub enum RegionPolicy {
    China {
        icp_number: Option<String>,
        #[serde(default = "default_true")]
        data_residency_required: bool,
        #[serde(default = "default_true")]
        payment_domestic_only: bool,
        #[serde(default = "default_true")]
        pipi_consent_required: bool,
    },
    EuropeanUnion {
        #[serde(default = "default_true")]
        gdpr_consent_required: bool,
        #[serde(default = "default_true")]
        right_to_deletion: bool,
        #[serde(default = "default_true")]
        right_to_export: bool,
        dpo_contact: Option<String>,
        #[serde(default = "default_true")]
        cookie_consent_required: bool,
    },
    Russia {
        #[serde(default = "default_true")]
        data_localization_required: bool,
        #[serde(default)]
        payment_mir_supported: bool,
    },
    #[default]
    FreeMarket,
}

fn default_true() -> bool {
    true
}

impl RegionPolicy {
    pub fn as_str(&self) -> &'static str {
        match self {
            RegionPolicy::China { .. } => "china",
            RegionPolicy::EuropeanUnion { .. } => "european_union",
            RegionPolicy::Russia { .. } => "russia",
            RegionPolicy::FreeMarket => "free_market",
        }
    }

    pub fn all() -> Vec<RegionPolicy> {
        vec![
            RegionPolicy::China {
                icp_number: None,
                data_residency_required: true,
                payment_domestic_only: true,
                pipi_consent_required: true,
            },
            RegionPolicy::EuropeanUnion {
                gdpr_consent_required: true,
                right_to_deletion: true,
                right_to_export: true,
                dpo_contact: None,
                cookie_consent_required: true,
            },
            RegionPolicy::Russia {
                data_localization_required: true,
                payment_mir_supported: false,
            },
            RegionPolicy::FreeMarket,
        ]
    }
}

impl fmt::Display for RegionPolicy {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl FromStr for RegionPolicy {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "china" => Ok(RegionPolicy::China {
                icp_number: None,
                data_residency_required: true,
                payment_domestic_only: true,
                pipi_consent_required: true,
            }),
            "european_union" => Ok(RegionPolicy::EuropeanUnion {
                gdpr_consent_required: true,
                right_to_deletion: true,
                right_to_export: true,
                dpo_contact: None,
                cookie_consent_required: true,
            }),
            "russia" => Ok(RegionPolicy::Russia {
                data_localization_required: true,
                payment_mir_supported: false,
            }),
            "free_market" => Ok(RegionPolicy::FreeMarket),
            _ => Err(format!("unknown region policy: {s}")),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn china_serde_round_trip() {
        let policy = RegionPolicy::China {
            icp_number: Some("ICP-12345".into()),
            data_residency_required: true,
            payment_domestic_only: true,
            pipi_consent_required: true,
        };
        let json = serde_json::to_string(&policy).unwrap();
        let back: RegionPolicy = serde_json::from_str(&json).unwrap();
        assert_eq!(back, policy);
    }

    #[test]
    fn european_union_serde_round_trip() {
        let policy = RegionPolicy::EuropeanUnion {
            gdpr_consent_required: true,
            right_to_deletion: true,
            right_to_export: false,
            dpo_contact: Some("dpo@example.com".into()),
            cookie_consent_required: true,
        };
        let json = serde_json::to_string(&policy).unwrap();
        let back: RegionPolicy = serde_json::from_str(&json).unwrap();
        assert_eq!(back, policy);
    }

    #[test]
    fn russia_serde_round_trip() {
        let policy = RegionPolicy::Russia {
            data_localization_required: true,
            payment_mir_supported: true,
        };
        let json = serde_json::to_string(&policy).unwrap();
        let back: RegionPolicy = serde_json::from_str(&json).unwrap();
        assert_eq!(back, policy);
    }

    #[test]
    fn free_market_serde_round_trip() {
        let policy = RegionPolicy::FreeMarket;
        let json = serde_json::to_string(&policy).unwrap();
        let back: RegionPolicy = serde_json::from_str(&json).unwrap();
        assert_eq!(back, policy);
    }

    #[test]
    fn default_is_free_market() {
        assert_eq!(RegionPolicy::default(), RegionPolicy::FreeMarket);
    }

    #[test]
    fn display_from_str_roundtrip() {
        for policy in RegionPolicy::all() {
            let s = policy.to_string();
            let back: RegionPolicy = s.parse().unwrap();
            assert_eq!(back, policy, "roundtrip failed for {s}");
        }
    }

    #[test]
    fn from_str_rejects_unknown() {
        assert!(RegionPolicy::from_str("unknown").is_err());
        assert!(RegionPolicy::from_str("").is_err());
    }

    #[test]
    fn all_returns_four_unique_variants() {
        let all = RegionPolicy::all();
        assert_eq!(
            all.len(),
            4,
            "RegionPolicy::all() must return exactly 4 variants"
        );
        let mut seen = std::collections::HashSet::new();
        for p in &all {
            let s = p.as_str();
            assert!(seen.insert(s), "duplicate region policy: {s}");
        }
    }

    #[test]
    fn china_default_flags() {
        let policy = RegionPolicy::China {
            icp_number: None,
            data_residency_required: true,
            payment_domestic_only: true,
            pipi_consent_required: true,
        };
        match policy {
            RegionPolicy::China {
                data_residency_required,
                payment_domestic_only,
                pipi_consent_required,
                icp_number,
            } => {
                assert!(data_residency_required);
                assert!(payment_domestic_only);
                assert!(pipi_consent_required);
                assert!(icp_number.is_none());
            }
            _ => unreachable!(),
        }
    }

    #[test]
    fn eu_default_flags() {
        let policy = RegionPolicy::EuropeanUnion {
            gdpr_consent_required: true,
            right_to_deletion: true,
            right_to_export: true,
            dpo_contact: None,
            cookie_consent_required: true,
        };
        match policy {
            RegionPolicy::EuropeanUnion {
                gdpr_consent_required,
                right_to_deletion,
                right_to_export,
                cookie_consent_required,
                dpo_contact,
            } => {
                assert!(gdpr_consent_required);
                assert!(right_to_deletion);
                assert!(right_to_export);
                assert!(cookie_consent_required);
                assert!(dpo_contact.is_none());
            }
            _ => unreachable!(),
        }
    }

    #[test]
    fn russia_default_flags() {
        let policy = RegionPolicy::Russia {
            data_localization_required: true,
            payment_mir_supported: false,
        };
        match policy {
            RegionPolicy::Russia {
                data_localization_required,
                payment_mir_supported,
            } => {
                assert!(data_localization_required);
                assert!(!payment_mir_supported);
            }
            _ => unreachable!(),
        }
    }

    #[test]
    fn china_deser_fills_defaults() {
        let json = r#"{"china": {"icp_number": null}}"#;
        let policy: RegionPolicy = serde_json::from_str(json).unwrap();
        match policy {
            RegionPolicy::China {
                data_residency_required,
                payment_domestic_only,
                pipi_consent_required,
                ..
            } => {
                assert!(data_residency_required);
                assert!(payment_domestic_only);
                assert!(pipi_consent_required);
            }
            _ => panic!("expected China"),
        }
    }

    #[test]
    fn eu_deser_fills_defaults() {
        let json = r#"{"european_union": {}}"#;
        let policy: RegionPolicy = serde_json::from_str(json).unwrap();
        match policy {
            RegionPolicy::EuropeanUnion {
                gdpr_consent_required,
                right_to_deletion,
                right_to_export,
                cookie_consent_required,
                ..
            } => {
                assert!(gdpr_consent_required);
                assert!(right_to_deletion);
                assert!(right_to_export);
                assert!(cookie_consent_required);
            }
            _ => panic!("expected EuropeanUnion"),
        }
    }

    #[test]
    fn russia_deser_fills_defaults() {
        let json = r#"{"russia": {}}"#;
        let policy: RegionPolicy = serde_json::from_str(json).unwrap();
        match policy {
            RegionPolicy::Russia {
                data_localization_required,
                payment_mir_supported,
            } => {
                assert!(data_localization_required);
                assert!(!payment_mir_supported);
            }
            _ => panic!("expected Russia"),
        }
    }

    #[test]
    fn as_str_all_variants() {
        assert_eq!(
            RegionPolicy::China {
                icp_number: None,
                data_residency_required: true,
                payment_domestic_only: true,
                pipi_consent_required: true
            }
            .as_str(),
            "china"
        );
        assert_eq!(
            RegionPolicy::EuropeanUnion {
                gdpr_consent_required: true,
                right_to_deletion: true,
                right_to_export: true,
                dpo_contact: None,
                cookie_consent_required: true
            }
            .as_str(),
            "european_union"
        );
        assert_eq!(
            RegionPolicy::Russia {
                data_localization_required: true,
                payment_mir_supported: false
            }
            .as_str(),
            "russia"
        );
        assert_eq!(RegionPolicy::FreeMarket.as_str(), "free_market");
    }

    #[test]
    fn free_market_serializes_as_snake_case_string() {
        let json = serde_json::to_string(&RegionPolicy::FreeMarket).unwrap();
        assert_eq!(json, r#""free_market""#);
    }
}