cml_rs/
profile.rs

1//! CML Profile System
2//!
3//! JSON-based profile definitions for validating CML documents.
4//! Profiles define allowed elements, attributes, and type vocabularies.
5//!
6//! ## Directory Structure
7//!
8//! Each profile lives in its own directory with separate concerns:
9//! ```text
10//! schemas/0.2/profiles/
11//! ├── core/
12//! │   ├── core.json        # Element definitions
13//! │   └── constraints.json # Validation rules
14//! ├── standard/
15//! │   ├── standard.json
16//! │   ├── constraints.json
17//! │   └── dictionary.json  # BytePunch compression
18//! └── legal/
19//!     ├── legal.json
20//!     ├── constraints.json
21//!     └── dictionary.json
22//! ```
23//!
24//! ## Profile Hierarchy
25//!
26//! - **core**: Structural minimum (cml, header, body, footer, title)
27//! - **standard**: All v0.2 elements (extends core)
28//! - **legal**: Legal documents (extends standard with include whitelist)
29//! - **code**: API documentation (extends standard)
30//! - **bookstack**: Wiki-style documentation (extends standard)
31
32use crate::{CmlError, Result};
33use serde::{Deserialize, Serialize};
34use std::collections::{HashMap, HashSet};
35use std::path::Path;
36
37/// A CML profile definition
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Profile {
40    /// Profile name (e.g., "core", "standard", "legal")
41    pub name: String,
42
43    /// Profile version
44    pub version: String,
45
46    /// Parent profile to inherit from
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub extends: Option<String>,
49
50    /// Human-readable description
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub description: Option<String>,
53
54    /// Whitelist of elements to include from parent
55    #[serde(default, skip_serializing_if = "Vec::is_empty")]
56    pub include: Vec<String>,
57
58    /// Blacklist of elements to exclude from parent
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub exclude: Vec<String>,
61
62    /// Element definitions
63    #[serde(default)]
64    pub elements: HashMap<String, ElementDef>,
65
66    /// Global attribute definitions
67    #[serde(default)]
68    pub attributes: HashMap<String, AttributeDef>,
69
70    /// Type vocabularies (enums for type attributes)
71    #[serde(default)]
72    pub types: HashMap<String, Vec<String>>,
73}
74
75/// Element definition within a profile
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ElementDef {
78    /// Human-readable description
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub description: Option<String>,
81
82    /// Content model: "empty", "text", "inline", "block", "mixed"
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub content: Option<String>,
85
86    /// Attribute definitions for this element
87    #[serde(default)]
88    pub attributes: HashMap<String, AttributeDef>,
89
90    /// Allowed child elements
91    #[serde(default, skip_serializing_if = "Vec::is_empty")]
92    pub children: Vec<String>,
93
94    /// Valid parent elements
95    #[serde(default, skip_serializing_if = "Vec::is_empty")]
96    pub parents: Vec<String>,
97
98    /// Required child elements
99    #[serde(default, skip_serializing_if = "Vec::is_empty")]
100    pub required_children: Vec<String>,
101
102    /// Minimum occurrences
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub min_occurs: Option<u32>,
105
106    /// Maximum occurrences
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub max_occurs: Option<u32>,
109}
110
111/// Attribute definition
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct AttributeDef {
114    /// Human-readable description
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub description: Option<String>,
117
118    /// Attribute type: "string", "integer", "boolean", "date", "uri", "enum"
119    #[serde(rename = "type", default = "default_attr_type")]
120    pub attr_type: String,
121
122    /// Whether the attribute is required
123    #[serde(default)]
124    pub required: bool,
125
126    /// Default value
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub default: Option<String>,
129
130    /// Valid values if type is "enum"
131    #[serde(rename = "enum", default, skip_serializing_if = "Vec::is_empty")]
132    pub enum_values: Vec<String>,
133
134    /// Regex pattern for validation
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub pattern: Option<String>,
137}
138
139fn default_attr_type() -> String {
140    "string".to_string()
141}
142
143// =============================================================================
144// Constraint Types
145// =============================================================================
146
147/// Profile constraints definition
148#[derive(Debug, Clone, Default, Serialize, Deserialize)]
149pub struct ProfileConstraints {
150    /// Profile this constraint applies to
151    #[serde(default)]
152    pub profile: String,
153
154    /// Constraint version
155    #[serde(default)]
156    pub version: String,
157
158    /// Parent constraints to inherit from
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub extends: Option<String>,
161
162    /// Description
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub description: Option<String>,
165
166    /// Document-level constraints
167    #[serde(default)]
168    pub document: Option<DocumentConstraint>,
169
170    /// Element-specific constraints
171    #[serde(default)]
172    pub elements: HashMap<String, ElementConstraint>,
173
174    /// Attribute validation rules
175    #[serde(default)]
176    pub attributes: HashMap<String, AttributeConstraint>,
177
178    /// Parent-child relationship constraints
179    #[serde(default)]
180    pub hierarchy: HashMap<String, HierarchyConstraint>,
181
182    /// Nesting constraints
183    #[serde(default)]
184    pub nesting: Option<NestingConstraint>,
185
186    /// List-specific constraints
187    #[serde(default)]
188    pub list_constraints: Option<ListConstraints>,
189
190    /// Semantic rules
191    #[serde(default)]
192    pub semantic_rules: HashMap<String, SemanticRule>,
193}
194
195/// Document-level constraints
196#[derive(Debug, Clone, Default, Serialize, Deserialize)]
197pub struct DocumentConstraint {
198    /// Required attributes on root element
199    #[serde(default)]
200    pub required_attributes: Vec<String>,
201
202    /// Required child elements
203    #[serde(default)]
204    pub required_children: Vec<String>,
205
206    /// Required order of children
207    #[serde(default)]
208    pub child_order: Vec<String>,
209}
210
211/// Element-specific constraint
212#[derive(Debug, Clone, Default, Serialize, Deserialize)]
213pub struct ElementConstraint {
214    /// Minimum occurrences
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub min_occurs: Option<u32>,
217
218    /// Maximum occurrences
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub max_occurs: Option<u32>,
221
222    /// Minimum number of children
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub min_children: Option<u32>,
225
226    /// Maximum number of children
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub max_children: Option<u32>,
229
230    /// Minimum content length
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub min_content: Option<u32>,
233
234    /// Minimum text length
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub min_length: Option<u32>,
237
238    /// Required attributes
239    #[serde(default)]
240    pub required_attributes: Vec<String>,
241
242    /// Required children
243    #[serde(default)]
244    pub required_children: Vec<String>,
245
246    /// Required child types
247    #[serde(default)]
248    pub required_child_types: Vec<String>,
249
250    /// Preserve whitespace
251    #[serde(default)]
252    pub preserve_whitespace: bool,
253
254    /// Disallow nesting
255    #[serde(default)]
256    pub no_nesting: bool,
257
258    /// Allow self-nesting
259    #[serde(default)]
260    pub allow_nesting: bool,
261
262    /// Reserved for future use
263    #[serde(default)]
264    pub reserved: bool,
265
266    /// Warning message
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub warning: Option<String>,
269
270    /// Size constraints
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub size: Option<SizeConstraint>,
273}
274
275/// Size constraint for heading levels etc.
276#[derive(Debug, Clone, Default, Serialize, Deserialize)]
277pub struct SizeConstraint {
278    pub min: Option<u32>,
279    pub max: Option<u32>,
280}
281
282/// Attribute constraint
283#[derive(Debug, Clone, Default, Serialize, Deserialize)]
284pub struct AttributeConstraint {
285    /// Description
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub description: Option<String>,
288
289    /// Type (string, integer, boolean, etc.)
290    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
291    pub attr_type: Option<String>,
292
293    /// Format (iso8601, uri, etc.)
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub format: Option<String>,
296
297    /// Regex pattern
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub pattern: Option<String>,
300
301    /// Enum values
302    #[serde(rename = "enum", default)]
303    pub enum_values: Vec<String>,
304
305    /// Recommended values
306    #[serde(default)]
307    pub recommended: Vec<String>,
308
309    /// Must be unique across document
310    #[serde(default)]
311    pub unique: bool,
312
313    /// Minimum value
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub min: Option<i32>,
316
317    /// Maximum value
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub max: Option<i32>,
320
321    /// Default value
322    #[serde(skip_serializing_if = "Option::is_none")]
323    pub default: Option<serde_json::Value>,
324}
325
326/// Hierarchy constraint
327#[derive(Debug, Clone, Default, Serialize, Deserialize)]
328pub struct HierarchyConstraint {
329    /// Allowed parent elements
330    #[serde(default)]
331    pub allowed_parents: Vec<String>,
332
333    /// Allowed child elements
334    #[serde(default)]
335    pub allowed_children: Vec<String>,
336
337    /// Maximum occurrences within parent
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub max_occurs: Option<u32>,
340
341    /// Must be first child of parent
342    #[serde(default)]
343    pub must_be_first: bool,
344}
345
346/// Nesting constraints
347#[derive(Debug, Clone, Default, Serialize, Deserialize)]
348pub struct NestingConstraint {
349    /// Maximum section nesting depth
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub max_section_depth: Option<u32>,
352
353    /// Maximum inline nesting depth
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub max_inline_depth: Option<u32>,
356
357    /// Elements that cannot self-nest
358    #[serde(default)]
359    pub no_self_nesting: Vec<String>,
360}
361
362/// List-specific constraints
363#[derive(Debug, Clone, Default, Serialize, Deserialize)]
364pub struct ListConstraints {
365    /// Ordered list constraints
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub ordered: Option<OrderedListConstraint>,
368
369    /// Unordered list constraints
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub unordered: Option<UnorderedListConstraint>,
372}
373
374/// Ordered list constraint
375#[derive(Debug, Clone, Default, Serialize, Deserialize)]
376pub struct OrderedListConstraint {
377    /// Order enforcement: "alphanumeric", "numeric", "none"
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub enforce_order: Option<String>,
380
381    /// Description
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub description: Option<String>,
384}
385
386/// Unordered list constraint
387#[derive(Debug, Clone, Default, Serialize, Deserialize)]
388pub struct UnorderedListConstraint {
389    /// Description
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub description: Option<String>,
392}
393
394/// Semantic rule
395#[derive(Debug, Clone, Default, Serialize, Deserialize)]
396pub struct SemanticRule {
397    /// Description
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub description: Option<String>,
400
401    /// Preferred element
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub prefer: Option<String>,
404
405    /// Element to avoid
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub instead_of: Option<String>,
408}
409
410/// Resolved constraints with inheritance applied
411#[derive(Debug, Clone, Default)]
412pub struct ResolvedConstraints {
413    /// Profile name
414    pub profile: String,
415
416    /// Document constraints
417    pub document: Option<DocumentConstraint>,
418
419    /// Element constraints
420    pub elements: HashMap<String, ElementConstraint>,
421
422    /// Attribute constraints
423    pub attributes: HashMap<String, AttributeConstraint>,
424
425    /// Hierarchy constraints
426    pub hierarchy: HashMap<String, HierarchyConstraint>,
427
428    /// Nesting constraints
429    pub nesting: Option<NestingConstraint>,
430
431    /// List constraints
432    pub list_constraints: Option<ListConstraints>,
433
434    /// Semantic rules
435    pub semantic_rules: HashMap<String, SemanticRule>,
436}
437
438/// Resolved profile with inheritance applied
439#[derive(Debug, Clone)]
440pub struct ResolvedProfile {
441    /// Profile name
442    pub name: String,
443
444    /// Profile version
445    pub version: String,
446
447    /// All allowed elements (after inheritance + include/exclude)
448    pub elements: HashMap<String, ElementDef>,
449
450    /// All type vocabularies (merged from inheritance chain)
451    pub types: HashMap<String, Vec<String>>,
452}
453
454/// Profile registry for loading and resolving profiles
455pub struct ProfileRegistry {
456    /// Loaded profiles by name
457    profiles: HashMap<String, Profile>,
458
459    /// Resolved profiles (with inheritance applied)
460    resolved: HashMap<String, ResolvedProfile>,
461}
462
463impl ProfileRegistry {
464    /// Create a new empty registry
465    pub fn new() -> Self {
466        Self {
467            profiles: HashMap::new(),
468            resolved: HashMap::new(),
469        }
470    }
471
472    /// Create a registry with built-in profiles
473    pub fn with_builtins() -> Result<Self> {
474        let mut registry = Self::new();
475        registry.load_builtin_profiles()?;
476        Ok(registry)
477    }
478
479    /// Load built-in profiles (core, standard, legal, legal:constitution, code, code:api, wiki)
480    fn load_builtin_profiles(&mut self) -> Result<()> {
481        // Core profile
482        let core: Profile = serde_json::from_str(include_str!(
483            "../schemas/0.2/profiles/core/core.json"
484        ))
485        .map_err(|e| CmlError::ValidationError(format!("Failed to parse core profile: {}", e)))?;
486        self.profiles.insert("core".to_string(), core);
487
488        // Standard profile
489        let standard: Profile = serde_json::from_str(include_str!(
490            "../schemas/0.2/profiles/standard/standard.json"
491        ))
492        .map_err(|e| {
493            CmlError::ValidationError(format!("Failed to parse standard profile: {}", e))
494        })?;
495        self.profiles.insert("standard".to_string(), standard);
496
497        // Legal profile (base)
498        let legal: Profile = serde_json::from_str(include_str!(
499            "../schemas/0.2/profiles/legal/legal.json"
500        ))
501        .map_err(|e| CmlError::ValidationError(format!("Failed to parse legal profile: {}", e)))?;
502        self.profiles.insert("legal".to_string(), legal);
503
504        // Legal:constitution sub-profile
505        let legal_constitution: Profile = serde_json::from_str(include_str!(
506            "../schemas/0.2/profiles/legal/constitution/constitution.json"
507        ))
508        .map_err(|e| {
509            CmlError::ValidationError(format!("Failed to parse legal:constitution profile: {}", e))
510        })?;
511        self.profiles.insert("legal:constitution".to_string(), legal_constitution);
512
513        // Code profile (base)
514        let code: Profile = serde_json::from_str(include_str!(
515            "../schemas/0.2/profiles/code/code.json"
516        ))
517        .map_err(|e| CmlError::ValidationError(format!("Failed to parse code profile: {}", e)))?;
518        self.profiles.insert("code".to_string(), code);
519
520        // Code:api sub-profile
521        let code_api: Profile = serde_json::from_str(include_str!(
522            "../schemas/0.2/profiles/code/api/api.json"
523        ))
524        .map_err(|e| {
525            CmlError::ValidationError(format!("Failed to parse code:api profile: {}", e))
526        })?;
527        self.profiles.insert("code:api".to_string(), code_api);
528
529        // Wiki profile
530        let wiki: Profile = serde_json::from_str(include_str!(
531            "../schemas/0.2/profiles/wiki/wiki.json"
532        ))
533        .map_err(|e| {
534            CmlError::ValidationError(format!("Failed to parse wiki profile: {}", e))
535        })?;
536        self.profiles.insert("wiki".to_string(), wiki);
537
538        Ok(())
539    }
540
541    /// Load a profile from a JSON file
542    pub fn load_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
543        let content = std::fs::read_to_string(path.as_ref())?;
544        let profile: Profile = serde_json::from_str(&content).map_err(|e| {
545            CmlError::ValidationError(format!("Failed to parse profile: {}", e))
546        })?;
547        self.profiles.insert(profile.name.clone(), profile);
548        Ok(())
549    }
550
551    /// Load a profile from a JSON string
552    pub fn load_from_str(&mut self, json: &str) -> Result<()> {
553        let profile: Profile = serde_json::from_str(json)
554            .map_err(|e| CmlError::ValidationError(format!("Failed to parse profile: {}", e)))?;
555        self.profiles.insert(profile.name.clone(), profile);
556        Ok(())
557    }
558
559    /// Get a resolved profile by name
560    pub fn get(&mut self, name: &str) -> Result<&ResolvedProfile> {
561        // Check if already resolved
562        if self.resolved.contains_key(name) {
563            return Ok(self.resolved.get(name).unwrap());
564        }
565
566        // Resolve the profile
567        let resolved = self.resolve_profile(name)?;
568        self.resolved.insert(name.to_string(), resolved);
569        Ok(self.resolved.get(name).unwrap())
570    }
571
572    /// Resolve a profile with inheritance
573    fn resolve_profile(&self, name: &str) -> Result<ResolvedProfile> {
574        let profile = self.profiles.get(name).ok_or_else(|| {
575            CmlError::ValidationError(format!("Profile not found: {}", name))
576        })?;
577
578        // Start with parent profile if exists
579        let mut elements: HashMap<String, ElementDef> = HashMap::new();
580        let mut types: HashMap<String, Vec<String>> = HashMap::new();
581
582        if let Some(parent_name) = &profile.extends {
583            let parent = self.resolve_profile(parent_name)?;
584
585            // Apply include/exclude filtering
586            if !profile.include.is_empty() {
587                // Whitelist mode: only include specified elements
588                let include_set: HashSet<&str> = profile.include.iter().map(|s| s.as_str()).collect();
589                for (name, def) in parent.elements {
590                    if include_set.contains(name.as_str()) {
591                        elements.insert(name, def);
592                    }
593                }
594            } else if !profile.exclude.is_empty() {
595                // Blacklist mode: exclude specified elements
596                let exclude_set: HashSet<&str> = profile.exclude.iter().map(|s| s.as_str()).collect();
597                for (name, def) in parent.elements {
598                    if !exclude_set.contains(name.as_str()) {
599                        elements.insert(name, def);
600                    }
601                }
602            } else {
603                // No filtering: inherit all
604                elements = parent.elements;
605            }
606
607            // Inherit types
608            types = parent.types;
609        }
610
611        // Add/override with this profile's elements
612        for (name, def) in &profile.elements {
613            elements.insert(name.clone(), def.clone());
614        }
615
616        // Merge types (profile types override parent)
617        for (name, values) in &profile.types {
618            types.insert(name.clone(), values.clone());
619        }
620
621        Ok(ResolvedProfile {
622            name: profile.name.clone(),
623            version: profile.version.clone(),
624            elements,
625            types,
626        })
627    }
628
629    /// Check if an element is allowed in a profile
630    pub fn is_element_allowed(&mut self, profile: &str, element: &str) -> Result<bool> {
631        let resolved = self.get(profile)?;
632        Ok(resolved.elements.contains_key(element))
633    }
634
635    /// Get valid type values for an element
636    pub fn get_type_values(&mut self, profile: &str, type_name: &str) -> Result<Option<Vec<String>>> {
637        let resolved = self.get(profile)?;
638        Ok(resolved.types.get(type_name).cloned())
639    }
640
641    /// Validate a type value against the profile
642    pub fn validate_type_value(
643        &mut self,
644        profile: &str,
645        type_name: &str,
646        value: &str,
647    ) -> Result<bool> {
648        let resolved = self.get(profile)?;
649        match resolved.types.get(type_name) {
650            Some(values) => Ok(values.contains(&value.to_string())),
651            None => Ok(true), // No restriction if type not defined
652        }
653    }
654}
655
656impl Default for ProfileRegistry {
657    fn default() -> Self {
658        Self::new()
659    }
660}
661
662// =============================================================================
663// Constraint Registry
664// =============================================================================
665
666/// Constraint registry for loading and resolving profile constraints
667pub struct ConstraintRegistry {
668    /// Loaded constraints by profile name
669    constraints: HashMap<String, ProfileConstraints>,
670
671    /// Resolved constraints (with inheritance applied)
672    resolved: HashMap<String, ResolvedConstraints>,
673}
674
675impl ConstraintRegistry {
676    /// Create a new empty registry
677    pub fn new() -> Self {
678        Self {
679            constraints: HashMap::new(),
680            resolved: HashMap::new(),
681        }
682    }
683
684    /// Create a registry with built-in constraints
685    pub fn with_builtins() -> Result<Self> {
686        let mut registry = Self::new();
687        registry.load_builtin_constraints()?;
688        Ok(registry)
689    }
690
691    /// Load built-in constraints
692    fn load_builtin_constraints(&mut self) -> Result<()> {
693        // Core constraints
694        let core: ProfileConstraints = serde_json::from_str(include_str!(
695            "../schemas/0.2/profiles/core/constraints.json"
696        ))
697        .map_err(|e| {
698            CmlError::ValidationError(format!("Failed to parse core constraints: {}", e))
699        })?;
700        self.constraints.insert("core".to_string(), core);
701
702        // Standard constraints
703        let standard: ProfileConstraints = serde_json::from_str(include_str!(
704            "../schemas/0.2/profiles/standard/constraints.json"
705        ))
706        .map_err(|e| {
707            CmlError::ValidationError(format!("Failed to parse standard constraints: {}", e))
708        })?;
709        self.constraints.insert("standard".to_string(), standard);
710
711        // Legal constraints
712        let legal: ProfileConstraints = serde_json::from_str(include_str!(
713            "../schemas/0.2/profiles/legal/constraints.json"
714        ))
715        .map_err(|e| {
716            CmlError::ValidationError(format!("Failed to parse legal constraints: {}", e))
717        })?;
718        self.constraints.insert("legal".to_string(), legal);
719
720        // Code constraints
721        let code: ProfileConstraints = serde_json::from_str(include_str!(
722            "../schemas/0.2/profiles/code/constraints.json"
723        ))
724        .map_err(|e| {
725            CmlError::ValidationError(format!("Failed to parse code constraints: {}", e))
726        })?;
727        self.constraints.insert("code".to_string(), code);
728
729        // Wiki constraints
730        let wiki: ProfileConstraints = serde_json::from_str(include_str!(
731            "../schemas/0.2/profiles/wiki/constraints.json"
732        ))
733        .map_err(|e| {
734            CmlError::ValidationError(format!("Failed to parse wiki constraints: {}", e))
735        })?;
736        self.constraints.insert("wiki".to_string(), wiki);
737
738        Ok(())
739    }
740
741    /// Load constraints from a JSON string
742    pub fn load_from_str(&mut self, json: &str) -> Result<()> {
743        let constraints: ProfileConstraints = serde_json::from_str(json)
744            .map_err(|e| CmlError::ValidationError(format!("Failed to parse constraints: {}", e)))?;
745        self.constraints.insert(constraints.profile.clone(), constraints);
746        Ok(())
747    }
748
749    /// Get resolved constraints by profile name
750    pub fn get(&mut self, name: &str) -> Result<&ResolvedConstraints> {
751        if self.resolved.contains_key(name) {
752            return Ok(self.resolved.get(name).unwrap());
753        }
754
755        let resolved = self.resolve_constraints(name)?;
756        self.resolved.insert(name.to_string(), resolved);
757        Ok(self.resolved.get(name).unwrap())
758    }
759
760    /// Resolve constraints with inheritance
761    fn resolve_constraints(&self, name: &str) -> Result<ResolvedConstraints> {
762        let constraints = self.constraints.get(name).ok_or_else(|| {
763            CmlError::ValidationError(format!("Constraints not found: {}", name))
764        })?;
765
766        let mut resolved = ResolvedConstraints {
767            profile: name.to_string(),
768            ..Default::default()
769        };
770
771        // Inherit from parent if exists
772        if let Some(parent_name) = &constraints.extends {
773            let parent = self.resolve_constraints(parent_name)?;
774            resolved.document = parent.document;
775            resolved.elements = parent.elements;
776            resolved.attributes = parent.attributes;
777            resolved.hierarchy = parent.hierarchy;
778            resolved.nesting = parent.nesting;
779            resolved.list_constraints = parent.list_constraints;
780            resolved.semantic_rules = parent.semantic_rules;
781        }
782
783        // Override with this profile's constraints
784        if constraints.document.is_some() {
785            resolved.document = constraints.document.clone();
786        }
787
788        for (name, constraint) in &constraints.elements {
789            resolved.elements.insert(name.clone(), constraint.clone());
790        }
791
792        for (name, constraint) in &constraints.attributes {
793            resolved.attributes.insert(name.clone(), constraint.clone());
794        }
795
796        for (name, constraint) in &constraints.hierarchy {
797            resolved.hierarchy.insert(name.clone(), constraint.clone());
798        }
799
800        if constraints.nesting.is_some() {
801            resolved.nesting = constraints.nesting.clone();
802        }
803
804        if constraints.list_constraints.is_some() {
805            resolved.list_constraints = constraints.list_constraints.clone();
806        }
807
808        for (name, rule) in &constraints.semantic_rules {
809            resolved.semantic_rules.insert(name.clone(), rule.clone());
810        }
811
812        Ok(resolved)
813    }
814}
815
816impl Default for ConstraintRegistry {
817    fn default() -> Self {
818        Self::new()
819    }
820}
821
822#[cfg(test)]
823mod tests {
824    use super::*;
825
826    #[test]
827    fn test_load_core_profile() {
828        let json = r#"{
829            "name": "test-core",
830            "version": "0.2",
831            "elements": {
832                "cml": { "content": "block" },
833                "header": { "content": "block" },
834                "body": { "content": "block" },
835                "footer": { "content": "block" }
836            }
837        }"#;
838
839        let profile: Profile = serde_json::from_str(json).unwrap();
840        assert_eq!(profile.name, "test-core");
841        assert_eq!(profile.elements.len(), 4);
842    }
843
844    #[test]
845    fn test_profile_inheritance() {
846        let mut registry = ProfileRegistry::new();
847
848        // Load parent
849        registry
850            .load_from_str(
851                r#"{
852                "name": "parent",
853                "version": "0.2",
854                "elements": {
855                    "a": { "content": "text" },
856                    "b": { "content": "text" },
857                    "c": { "content": "text" }
858                }
859            }"#,
860            )
861            .unwrap();
862
863        // Load child with include whitelist
864        registry
865            .load_from_str(
866                r#"{
867                "name": "child",
868                "version": "0.2",
869                "extends": "parent",
870                "include": ["a", "b"],
871                "elements": {
872                    "d": { "content": "text" }
873                }
874            }"#,
875            )
876            .unwrap();
877
878        let resolved = registry.get("child").unwrap();
879        assert!(resolved.elements.contains_key("a"));
880        assert!(resolved.elements.contains_key("b"));
881        assert!(!resolved.elements.contains_key("c")); // Excluded by whitelist
882        assert!(resolved.elements.contains_key("d")); // Added by child
883    }
884
885    #[test]
886    fn test_type_vocabularies() {
887        let mut registry = ProfileRegistry::new();
888
889        registry
890            .load_from_str(
891                r#"{
892                "name": "typed",
893                "version": "0.2",
894                "types": {
895                    "date": ["created", "updated", "published"],
896                    "section": ["intro", "body", "conclusion"]
897                }
898            }"#,
899            )
900            .unwrap();
901
902        assert!(registry.validate_type_value("typed", "date", "created").unwrap());
903        assert!(registry.validate_type_value("typed", "date", "published").unwrap());
904        assert!(!registry.validate_type_value("typed", "date", "invalid").unwrap());
905    }
906}