use std::collections::BTreeMap;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct LoanPhonology {
#[serde(default = "default_repair")]
pub repair: String,
#[serde(default)]
pub epenthetic_vowel: String,
#[serde(default)]
pub substitutions: BTreeMap<String, String>,
}
fn default_repair() -> String {
"epenthesis".to_string()
}
impl Default for LoanPhonology {
fn default() -> Self {
Self {
repair: default_repair(),
epenthetic_vowel: String::new(),
substitutions: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Contact {
#[serde(default)]
pub region: String,
#[serde(default)]
pub with: Vec<String>,
#[serde(default)]
pub areal_features: BTreeMap<String, String>,
}
impl Contact {
fn is_empty(&self) -> bool {
self.region.trim().is_empty() && self.with.is_empty() && self.areal_features.is_empty()
}
pub fn from_hjson(body: &str) -> Result<Option<Self>, String> {
if body.trim().is_empty() {
return Ok(None);
}
let block = crate::language_entry::extract_hjson_block(body).unwrap_or(body);
match serde_hjson::from_str::<ContactWrap>(block) {
Ok(w) => Ok(w.contact.filter(|c| !c.is_empty())),
Err(e) => Err(format!("contact HJSON parse failed: {e}")),
}
}
}
#[derive(Deserialize)]
struct ContactWrap {
#[serde(default)]
contact: Option<Contact>,
}
#[derive(Deserialize)]
struct LoanWrap {
#[serde(default)]
loan_phonology: Option<LoanPhonology>,
}
impl LoanPhonology {
pub fn from_hjson(body: &str) -> Result<Option<Self>, String> {
if body.trim().is_empty() {
return Ok(None);
}
let block = crate::language_entry::extract_hjson_block(body).unwrap_or(body);
match serde_hjson::from_str::<LoanWrap>(block) {
Ok(w) => Ok(w.loan_phonology),
Err(e) => Err(format!("loan_phonology HJSON parse failed: {e}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_loan_phonology() {
let body = r#"{ loan_phonology: {
repair: "epenthesis", epenthetic_vowel: "u",
substitutions: { "θ": "t", "r": "l" }
} }"#;
let lp = LoanPhonology::from_hjson(body).unwrap().unwrap();
assert_eq!(lp.repair, "epenthesis");
assert_eq!(lp.epenthetic_vowel, "u");
assert_eq!(lp.substitutions.get("θ").unwrap(), "t");
}
#[test]
fn no_block_is_none() {
assert!(LoanPhonology::from_hjson("{ phonemes: [] }").unwrap().is_none());
}
#[test]
fn parses_contact() {
let body = r#"{ contact: {
region: "the Inner Sea", with: ["Sindar", "Khuz"],
areal_features: { word_order: "sov", alignment: "ergative_absolutive" }
} }"#;
let c = Contact::from_hjson(body).unwrap().unwrap();
assert_eq!(c.region, "the Inner Sea");
assert_eq!(c.with, vec!["Sindar".to_string(), "Khuz".to_string()]);
assert_eq!(c.areal_features.get("word_order").unwrap(), "sov");
assert!(Contact::from_hjson("{ contact: {} }").unwrap().is_none());
}
}