Skip to main content

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 =
499            serde_json::from_str(include_str!("../schemas/0.2/profiles/legal/legal.json"))
500                .map_err(|e| {
501                    CmlError::ValidationError(format!("Failed to parse legal profile: {}", e))
502                })?;
503        self.profiles.insert("legal".to_string(), legal);
504
505        // Legal:constitution sub-profile
506        let legal_constitution: Profile = serde_json::from_str(include_str!(
507            "../schemas/0.2/profiles/legal/constitution/constitution.json"
508        ))
509        .map_err(|e| {
510            CmlError::ValidationError(format!("Failed to parse legal:constitution profile: {}", e))
511        })?;
512        self.profiles
513            .insert("legal:constitution".to_string(), legal_constitution);
514
515        // Code profile (base)
516        let code: Profile = serde_json::from_str(include_str!(
517            "../schemas/0.2/profiles/code/code.json"
518        ))
519        .map_err(|e| CmlError::ValidationError(format!("Failed to parse code profile: {}", e)))?;
520        self.profiles.insert("code".to_string(), code);
521
522        // Code:api sub-profile
523        let code_api: Profile =
524            serde_json::from_str(include_str!("../schemas/0.2/profiles/code/api/api.json"))
525                .map_err(|e| {
526                    CmlError::ValidationError(format!("Failed to parse code:api profile: {}", e))
527                })?;
528        self.profiles.insert("code:api".to_string(), code_api);
529
530        // Wiki profile
531        let wiki: Profile = serde_json::from_str(include_str!(
532            "../schemas/0.2/profiles/wiki/wiki.json"
533        ))
534        .map_err(|e| CmlError::ValidationError(format!("Failed to parse wiki profile: {}", e)))?;
535        self.profiles.insert("wiki".to_string(), wiki);
536
537        Ok(())
538    }
539
540    /// Load a profile from a JSON file
541    pub fn load_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
542        let content = std::fs::read_to_string(path.as_ref())?;
543        let profile: Profile = serde_json::from_str(&content)
544            .map_err(|e| CmlError::ValidationError(format!("Failed to parse profile: {}", e)))?;
545        self.profiles.insert(profile.name.clone(), profile);
546        Ok(())
547    }
548
549    /// Load a profile from a JSON string
550    pub fn load_from_str(&mut self, json: &str) -> Result<()> {
551        let profile: Profile = serde_json::from_str(json)
552            .map_err(|e| CmlError::ValidationError(format!("Failed to parse profile: {}", e)))?;
553        self.profiles.insert(profile.name.clone(), profile);
554        Ok(())
555    }
556
557    /// Get a resolved profile by name
558    pub fn get(&mut self, name: &str) -> Result<&ResolvedProfile> {
559        // Check if already resolved
560        if self.resolved.contains_key(name) {
561            return Ok(self.resolved.get(name).unwrap());
562        }
563
564        // Resolve the profile
565        let resolved = self.resolve_profile(name)?;
566        self.resolved.insert(name.to_string(), resolved);
567        Ok(self.resolved.get(name).unwrap())
568    }
569
570    /// Resolve a profile with inheritance
571    fn resolve_profile(&self, name: &str) -> Result<ResolvedProfile> {
572        let profile = self
573            .profiles
574            .get(name)
575            .ok_or_else(|| CmlError::ValidationError(format!("Profile not found: {}", name)))?;
576
577        // Start with parent profile if exists
578        let mut elements: HashMap<String, ElementDef> = HashMap::new();
579        let mut types: HashMap<String, Vec<String>> = HashMap::new();
580
581        if let Some(parent_name) = &profile.extends {
582            let parent = self.resolve_profile(parent_name)?;
583
584            // Apply include/exclude filtering
585            if !profile.include.is_empty() {
586                // Whitelist mode: only include specified elements
587                let include_set: HashSet<&str> =
588                    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> =
597                    profile.exclude.iter().map(|s| s.as_str()).collect();
598                for (name, def) in parent.elements {
599                    if !exclude_set.contains(name.as_str()) {
600                        elements.insert(name, def);
601                    }
602                }
603            } else {
604                // No filtering: inherit all
605                elements = parent.elements;
606            }
607
608            // Inherit types
609            types = parent.types;
610        }
611
612        // Add/override with this profile's elements
613        for (name, def) in &profile.elements {
614            elements.insert(name.clone(), def.clone());
615        }
616
617        // Merge types (profile types override parent)
618        for (name, values) in &profile.types {
619            types.insert(name.clone(), values.clone());
620        }
621
622        Ok(ResolvedProfile {
623            name: profile.name.clone(),
624            version: profile.version.clone(),
625            elements,
626            types,
627        })
628    }
629
630    /// Check if an element is allowed in a profile
631    pub fn is_element_allowed(&mut self, profile: &str, element: &str) -> Result<bool> {
632        let resolved = self.get(profile)?;
633        Ok(resolved.elements.contains_key(element))
634    }
635
636    /// Get valid type values for an element
637    pub fn get_type_values(
638        &mut self,
639        profile: &str,
640        type_name: &str,
641    ) -> Result<Option<Vec<String>>> {
642        let resolved = self.get(profile)?;
643        Ok(resolved.types.get(type_name).cloned())
644    }
645
646    /// Validate a type value against the profile
647    pub fn validate_type_value(
648        &mut self,
649        profile: &str,
650        type_name: &str,
651        value: &str,
652    ) -> Result<bool> {
653        let resolved = self.get(profile)?;
654        match resolved.types.get(type_name) {
655            Some(values) => Ok(values.contains(&value.to_string())),
656            None => Ok(true), // No restriction if type not defined
657        }
658    }
659}
660
661impl Default for ProfileRegistry {
662    fn default() -> Self {
663        Self::new()
664    }
665}
666
667// =============================================================================
668// Constraint Registry
669// =============================================================================
670
671/// Constraint registry for loading and resolving profile constraints
672pub struct ConstraintRegistry {
673    /// Loaded constraints by profile name
674    constraints: HashMap<String, ProfileConstraints>,
675
676    /// Resolved constraints (with inheritance applied)
677    resolved: HashMap<String, ResolvedConstraints>,
678}
679
680impl ConstraintRegistry {
681    /// Create a new empty registry
682    pub fn new() -> Self {
683        Self {
684            constraints: HashMap::new(),
685            resolved: HashMap::new(),
686        }
687    }
688
689    /// Create a registry with built-in constraints
690    pub fn with_builtins() -> Result<Self> {
691        let mut registry = Self::new();
692        registry.load_builtin_constraints()?;
693        Ok(registry)
694    }
695
696    /// Load built-in constraints
697    fn load_builtin_constraints(&mut self) -> Result<()> {
698        // Core constraints
699        let core: ProfileConstraints = serde_json::from_str(include_str!(
700            "../schemas/0.2/profiles/core/constraints.json"
701        ))
702        .map_err(|e| {
703            CmlError::ValidationError(format!("Failed to parse core constraints: {}", e))
704        })?;
705        self.constraints.insert("core".to_string(), core);
706
707        // Standard constraints
708        let standard: ProfileConstraints = serde_json::from_str(include_str!(
709            "../schemas/0.2/profiles/standard/constraints.json"
710        ))
711        .map_err(|e| {
712            CmlError::ValidationError(format!("Failed to parse standard constraints: {}", e))
713        })?;
714        self.constraints.insert("standard".to_string(), standard);
715
716        // Legal constraints
717        let legal: ProfileConstraints = serde_json::from_str(include_str!(
718            "../schemas/0.2/profiles/legal/constraints.json"
719        ))
720        .map_err(|e| {
721            CmlError::ValidationError(format!("Failed to parse legal constraints: {}", e))
722        })?;
723        self.constraints.insert("legal".to_string(), legal);
724
725        // Code constraints
726        let code: ProfileConstraints = serde_json::from_str(include_str!(
727            "../schemas/0.2/profiles/code/constraints.json"
728        ))
729        .map_err(|e| {
730            CmlError::ValidationError(format!("Failed to parse code constraints: {}", e))
731        })?;
732        self.constraints.insert("code".to_string(), code);
733
734        // Wiki constraints
735        let wiki: ProfileConstraints = serde_json::from_str(include_str!(
736            "../schemas/0.2/profiles/wiki/constraints.json"
737        ))
738        .map_err(|e| {
739            CmlError::ValidationError(format!("Failed to parse wiki constraints: {}", e))
740        })?;
741        self.constraints.insert("wiki".to_string(), wiki);
742
743        Ok(())
744    }
745
746    /// Load constraints from a JSON string
747    pub fn load_from_str(&mut self, json: &str) -> Result<()> {
748        let constraints: ProfileConstraints = serde_json::from_str(json).map_err(|e| {
749            CmlError::ValidationError(format!("Failed to parse constraints: {}", e))
750        })?;
751        self.constraints
752            .insert(constraints.profile.clone(), constraints);
753        Ok(())
754    }
755
756    /// Get resolved constraints by profile name
757    pub fn get(&mut self, name: &str) -> Result<&ResolvedConstraints> {
758        if self.resolved.contains_key(name) {
759            return Ok(self.resolved.get(name).unwrap());
760        }
761
762        let resolved = self.resolve_constraints(name)?;
763        self.resolved.insert(name.to_string(), resolved);
764        Ok(self.resolved.get(name).unwrap())
765    }
766
767    /// Resolve constraints with inheritance
768    fn resolve_constraints(&self, name: &str) -> Result<ResolvedConstraints> {
769        let constraints = self
770            .constraints
771            .get(name)
772            .ok_or_else(|| CmlError::ValidationError(format!("Constraints not found: {}", name)))?;
773
774        let mut resolved = ResolvedConstraints {
775            profile: name.to_string(),
776            ..Default::default()
777        };
778
779        // Inherit from parent if exists
780        if let Some(parent_name) = &constraints.extends {
781            let parent = self.resolve_constraints(parent_name)?;
782            resolved.document = parent.document;
783            resolved.elements = parent.elements;
784            resolved.attributes = parent.attributes;
785            resolved.hierarchy = parent.hierarchy;
786            resolved.nesting = parent.nesting;
787            resolved.list_constraints = parent.list_constraints;
788            resolved.semantic_rules = parent.semantic_rules;
789        }
790
791        // Override with this profile's constraints
792        if constraints.document.is_some() {
793            resolved.document = constraints.document.clone();
794        }
795
796        for (name, constraint) in &constraints.elements {
797            resolved.elements.insert(name.clone(), constraint.clone());
798        }
799
800        for (name, constraint) in &constraints.attributes {
801            resolved.attributes.insert(name.clone(), constraint.clone());
802        }
803
804        for (name, constraint) in &constraints.hierarchy {
805            resolved.hierarchy.insert(name.clone(), constraint.clone());
806        }
807
808        if constraints.nesting.is_some() {
809            resolved.nesting = constraints.nesting.clone();
810        }
811
812        if constraints.list_constraints.is_some() {
813            resolved.list_constraints = constraints.list_constraints.clone();
814        }
815
816        for (name, rule) in &constraints.semantic_rules {
817            resolved.semantic_rules.insert(name.clone(), rule.clone());
818        }
819
820        Ok(resolved)
821    }
822}
823
824impl Default for ConstraintRegistry {
825    fn default() -> Self {
826        Self::new()
827    }
828}
829
830#[cfg(test)]
831mod tests {
832    use super::*;
833
834    #[test]
835    fn test_load_core_profile() {
836        let json = r#"{
837            "name": "test-core",
838            "version": "0.2",
839            "elements": {
840                "cml": { "content": "block" },
841                "header": { "content": "block" },
842                "body": { "content": "block" },
843                "footer": { "content": "block" }
844            }
845        }"#;
846
847        let profile: Profile = serde_json::from_str(json).unwrap();
848        assert_eq!(profile.name, "test-core");
849        assert_eq!(profile.elements.len(), 4);
850    }
851
852    #[test]
853    fn test_profile_inheritance() {
854        let mut registry = ProfileRegistry::new();
855
856        // Load parent
857        registry
858            .load_from_str(
859                r#"{
860                "name": "parent",
861                "version": "0.2",
862                "elements": {
863                    "a": { "content": "text" },
864                    "b": { "content": "text" },
865                    "c": { "content": "text" }
866                }
867            }"#,
868            )
869            .unwrap();
870
871        // Load child with include whitelist
872        registry
873            .load_from_str(
874                r#"{
875                "name": "child",
876                "version": "0.2",
877                "extends": "parent",
878                "include": ["a", "b"],
879                "elements": {
880                    "d": { "content": "text" }
881                }
882            }"#,
883            )
884            .unwrap();
885
886        let resolved = registry.get("child").unwrap();
887        assert!(resolved.elements.contains_key("a"));
888        assert!(resolved.elements.contains_key("b"));
889        assert!(!resolved.elements.contains_key("c")); // Excluded by whitelist
890        assert!(resolved.elements.contains_key("d")); // Added by child
891    }
892
893    #[test]
894    fn test_type_vocabularies() {
895        let mut registry = ProfileRegistry::new();
896
897        registry
898            .load_from_str(
899                r#"{
900                "name": "typed",
901                "version": "0.2",
902                "types": {
903                    "date": ["created", "updated", "published"],
904                    "section": ["intro", "body", "conclusion"]
905                }
906            }"#,
907            )
908            .unwrap();
909
910        assert!(registry
911            .validate_type_value("typed", "date", "created")
912            .unwrap());
913        assert!(registry
914            .validate_type_value("typed", "date", "published")
915            .unwrap());
916        assert!(!registry
917            .validate_type_value("typed", "date", "invalid")
918            .unwrap());
919    }
920}