//! Store metadata — reads title.txt, description.txt, whats-new.txt, etc. //! //! Directory structure (mirrors Fastlane's metadata format): //! ```text //! store/ //! en-US/ //! title.txt //! description.txt //! whats-new.txt //! keywords.txt //! promotional-text.txt //! de-DE/ //! title.txt //! ... //! ``` use crate::{PublishError, PublishResult}; use std::collections::HashMap; use std::path::Path; /// Metadata for a single locale. #[derive(Debug, Clone, Default)] pub struct LocaleMetadata { pub title: String, pub description: String, pub whats_new: String, pub keywords: Vec, pub promotional_text: String, } impl LocaleMetadata { pub fn new(title: impl Into, description: impl Into) -> Self { Self { title: title.into(), description: description.into(), ..Default::default() } } pub fn with_whats_new(mut self, whats_new: impl Into) -> Self { self.whats_new = whats_new.into(); self } } /// Complete store metadata across all locales. #[derive(Debug, Clone, Default)] pub struct StoreMetadata { pub locales: HashMap, } impl StoreMetadata { pub fn new() -> Self { Self::default() } pub fn add_locale(mut self, locale: impl Into, metadata: LocaleMetadata) -> Self { self.locales.insert(locale.into(), metadata); self } /// Get metadata for a specific locale, falling back to `en-US`. pub fn for_locale(&self, locale: &str) -> Option<&LocaleMetadata> { self.locales.get(locale).or_else(|| self.locales.get("en-US")) } /// Load metadata from the directory structure. /// /// Falls back gracefully: if the metadata directory doesn't exist, /// returns empty metadata (don't fail the publish for missing store copy). pub fn load_from_dir(dir: &str) -> PublishResult { let path = Path::new(dir); if !path.exists() { // Warn but don't fail — metadata is optional for internal/alpha builds. return Ok(Self::new()); } let mut metadata = Self::new(); let read_dir = std::fs::read_dir(path).map_err(|e| { PublishError::Metadata(format!("failed to read metadata dir {}: {}", dir, e)) })?; for entry in read_dir.flatten() { let locale_path = entry.path(); if !locale_path.is_dir() { continue; } let locale = locale_path .file_name() .and_then(|n| n.to_str()) .unwrap_or("") .to_string(); if locale.is_empty() { continue; } let locale_meta = LocaleMetadata { title: read_file(&locale_path, "title.txt"), description: read_file(&locale_path, "description.txt"), whats_new: read_file(&locale_path, "whats-new.txt"), keywords: read_file(&locale_path, "keywords.txt") .split(',') .map(|k| k.trim().to_string()) .filter(|k| !k.is_empty()) .collect(), promotional_text: read_file(&locale_path, "promotional-text.txt"), }; metadata.locales.insert(locale, locale_meta); } Ok(metadata) } } fn read_file(dir: &Path, filename: &str) -> String { std::fs::read_to_string(dir.join(filename)) .unwrap_or_default() .trim() .to_string() }