use crate::{CmlError, Result};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Profile {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub extends: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub include: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exclude: Vec<String>,
#[serde(default)]
pub elements: HashMap<String, ElementDef>,
#[serde(default)]
pub attributes: HashMap<String, AttributeDef>,
#[serde(default)]
pub types: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ElementDef {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(default)]
pub attributes: HashMap<String, AttributeDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parents: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_children: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_occurs: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_occurs: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttributeDef {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "type", default = "default_attr_type")]
pub attr_type: String,
#[serde(default)]
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
#[serde(rename = "enum", default, skip_serializing_if = "Vec::is_empty")]
pub enum_values: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
}
fn default_attr_type() -> String {
"string".to_string()
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProfileConstraints {
#[serde(default)]
pub profile: String,
#[serde(default)]
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub extends: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub document: Option<DocumentConstraint>,
#[serde(default)]
pub elements: HashMap<String, ElementConstraint>,
#[serde(default)]
pub attributes: HashMap<String, AttributeConstraint>,
#[serde(default)]
pub hierarchy: HashMap<String, HierarchyConstraint>,
#[serde(default)]
pub nesting: Option<NestingConstraint>,
#[serde(default)]
pub list_constraints: Option<ListConstraints>,
#[serde(default)]
pub semantic_rules: HashMap<String, SemanticRule>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DocumentConstraint {
#[serde(default)]
pub required_attributes: Vec<String>,
#[serde(default)]
pub required_children: Vec<String>,
#[serde(default)]
pub child_order: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ElementConstraint {
#[serde(skip_serializing_if = "Option::is_none")]
pub min_occurs: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_occurs: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_children: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_children: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_content: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_length: Option<u32>,
#[serde(default)]
pub required_attributes: Vec<String>,
#[serde(default)]
pub required_children: Vec<String>,
#[serde(default)]
pub required_child_types: Vec<String>,
#[serde(default)]
pub preserve_whitespace: bool,
#[serde(default)]
pub no_nesting: bool,
#[serde(default)]
pub allow_nesting: bool,
#[serde(default)]
pub reserved: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub warning: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<SizeConstraint>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SizeConstraint {
pub min: Option<u32>,
pub max: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AttributeConstraint {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub attr_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(rename = "enum", default)]
pub enum_values: Vec<String>,
#[serde(default)]
pub recommended: Vec<String>,
#[serde(default)]
pub unique: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HierarchyConstraint {
#[serde(default)]
pub allowed_parents: Vec<String>,
#[serde(default)]
pub allowed_children: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_occurs: Option<u32>,
#[serde(default)]
pub must_be_first: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NestingConstraint {
#[serde(skip_serializing_if = "Option::is_none")]
pub max_section_depth: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_inline_depth: Option<u32>,
#[serde(default)]
pub no_self_nesting: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListConstraints {
#[serde(skip_serializing_if = "Option::is_none")]
pub ordered: Option<OrderedListConstraint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unordered: Option<UnorderedListConstraint>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OrderedListConstraint {
#[serde(skip_serializing_if = "Option::is_none")]
pub enforce_order: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct UnorderedListConstraint {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SemanticRule {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instead_of: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedConstraints {
pub profile: String,
pub document: Option<DocumentConstraint>,
pub elements: HashMap<String, ElementConstraint>,
pub attributes: HashMap<String, AttributeConstraint>,
pub hierarchy: HashMap<String, HierarchyConstraint>,
pub nesting: Option<NestingConstraint>,
pub list_constraints: Option<ListConstraints>,
pub semantic_rules: HashMap<String, SemanticRule>,
}
#[derive(Debug, Clone)]
pub struct ResolvedProfile {
pub name: String,
pub version: String,
pub elements: HashMap<String, ElementDef>,
pub types: HashMap<String, Vec<String>>,
}
pub struct ProfileRegistry {
profiles: HashMap<String, Profile>,
resolved: HashMap<String, ResolvedProfile>,
}
impl ProfileRegistry {
pub fn new() -> Self {
Self {
profiles: HashMap::new(),
resolved: HashMap::new(),
}
}
pub fn with_builtins() -> Result<Self> {
let mut registry = Self::new();
registry.load_builtin_profiles()?;
Ok(registry)
}
fn load_builtin_profiles(&mut self) -> Result<()> {
let core: Profile = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/core/core.json"
))
.map_err(|e| CmlError::ValidationError(format!("Failed to parse core profile: {}", e)))?;
self.profiles.insert("core".to_string(), core);
let standard: Profile = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/standard/standard.json"
))
.map_err(|e| {
CmlError::ValidationError(format!("Failed to parse standard profile: {}", e))
})?;
self.profiles.insert("standard".to_string(), standard);
let legal: Profile =
serde_json::from_str(include_str!("../schemas/0.2/profiles/legal/legal.json"))
.map_err(|e| {
CmlError::ValidationError(format!("Failed to parse legal profile: {}", e))
})?;
self.profiles.insert("legal".to_string(), legal);
let legal_constitution: Profile = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/legal/constitution/constitution.json"
))
.map_err(|e| {
CmlError::ValidationError(format!("Failed to parse legal:constitution profile: {}", e))
})?;
self.profiles
.insert("legal:constitution".to_string(), legal_constitution);
let code: Profile = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/code/code.json"
))
.map_err(|e| CmlError::ValidationError(format!("Failed to parse code profile: {}", e)))?;
self.profiles.insert("code".to_string(), code);
let code_api: Profile =
serde_json::from_str(include_str!("../schemas/0.2/profiles/code/api/api.json"))
.map_err(|e| {
CmlError::ValidationError(format!("Failed to parse code:api profile: {}", e))
})?;
self.profiles.insert("code:api".to_string(), code_api);
let wiki: Profile = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/wiki/wiki.json"
))
.map_err(|e| CmlError::ValidationError(format!("Failed to parse wiki profile: {}", e)))?;
self.profiles.insert("wiki".to_string(), wiki);
Ok(())
}
pub fn load_from_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let content = std::fs::read_to_string(path.as_ref())?;
let profile: Profile = serde_json::from_str(&content)
.map_err(|e| CmlError::ValidationError(format!("Failed to parse profile: {}", e)))?;
self.profiles.insert(profile.name.clone(), profile);
Ok(())
}
pub fn load_from_str(&mut self, json: &str) -> Result<()> {
let profile: Profile = serde_json::from_str(json)
.map_err(|e| CmlError::ValidationError(format!("Failed to parse profile: {}", e)))?;
self.profiles.insert(profile.name.clone(), profile);
Ok(())
}
pub fn get(&mut self, name: &str) -> Result<&ResolvedProfile> {
if self.resolved.contains_key(name) {
return Ok(self.resolved.get(name).unwrap());
}
let resolved = self.resolve_profile(name)?;
self.resolved.insert(name.to_string(), resolved);
Ok(self.resolved.get(name).unwrap())
}
fn resolve_profile(&self, name: &str) -> Result<ResolvedProfile> {
let profile = self
.profiles
.get(name)
.ok_or_else(|| CmlError::ValidationError(format!("Profile not found: {}", name)))?;
let mut elements: HashMap<String, ElementDef> = HashMap::new();
let mut types: HashMap<String, Vec<String>> = HashMap::new();
if let Some(parent_name) = &profile.extends {
let parent = self.resolve_profile(parent_name)?;
if !profile.include.is_empty() {
let include_set: HashSet<&str> =
profile.include.iter().map(|s| s.as_str()).collect();
for (name, def) in parent.elements {
if include_set.contains(name.as_str()) {
elements.insert(name, def);
}
}
} else if !profile.exclude.is_empty() {
let exclude_set: HashSet<&str> =
profile.exclude.iter().map(|s| s.as_str()).collect();
for (name, def) in parent.elements {
if !exclude_set.contains(name.as_str()) {
elements.insert(name, def);
}
}
} else {
elements = parent.elements;
}
types = parent.types;
}
for (name, def) in &profile.elements {
elements.insert(name.clone(), def.clone());
}
for (name, values) in &profile.types {
types.insert(name.clone(), values.clone());
}
Ok(ResolvedProfile {
name: profile.name.clone(),
version: profile.version.clone(),
elements,
types,
})
}
pub fn is_element_allowed(&mut self, profile: &str, element: &str) -> Result<bool> {
let resolved = self.get(profile)?;
Ok(resolved.elements.contains_key(element))
}
pub fn get_type_values(
&mut self,
profile: &str,
type_name: &str,
) -> Result<Option<Vec<String>>> {
let resolved = self.get(profile)?;
Ok(resolved.types.get(type_name).cloned())
}
pub fn validate_type_value(
&mut self,
profile: &str,
type_name: &str,
value: &str,
) -> Result<bool> {
let resolved = self.get(profile)?;
match resolved.types.get(type_name) {
Some(values) => Ok(values.contains(&value.to_string())),
None => Ok(true), }
}
}
impl Default for ProfileRegistry {
fn default() -> Self {
Self::new()
}
}
pub struct ConstraintRegistry {
constraints: HashMap<String, ProfileConstraints>,
resolved: HashMap<String, ResolvedConstraints>,
}
impl ConstraintRegistry {
pub fn new() -> Self {
Self {
constraints: HashMap::new(),
resolved: HashMap::new(),
}
}
pub fn with_builtins() -> Result<Self> {
let mut registry = Self::new();
registry.load_builtin_constraints()?;
Ok(registry)
}
fn load_builtin_constraints(&mut self) -> Result<()> {
let core: ProfileConstraints = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/core/constraints.json"
))
.map_err(|e| {
CmlError::ValidationError(format!("Failed to parse core constraints: {}", e))
})?;
self.constraints.insert("core".to_string(), core);
let standard: ProfileConstraints = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/standard/constraints.json"
))
.map_err(|e| {
CmlError::ValidationError(format!("Failed to parse standard constraints: {}", e))
})?;
self.constraints.insert("standard".to_string(), standard);
let legal: ProfileConstraints = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/legal/constraints.json"
))
.map_err(|e| {
CmlError::ValidationError(format!("Failed to parse legal constraints: {}", e))
})?;
self.constraints.insert("legal".to_string(), legal);
let code: ProfileConstraints = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/code/constraints.json"
))
.map_err(|e| {
CmlError::ValidationError(format!("Failed to parse code constraints: {}", e))
})?;
self.constraints.insert("code".to_string(), code);
let wiki: ProfileConstraints = serde_json::from_str(include_str!(
"../schemas/0.2/profiles/wiki/constraints.json"
))
.map_err(|e| {
CmlError::ValidationError(format!("Failed to parse wiki constraints: {}", e))
})?;
self.constraints.insert("wiki".to_string(), wiki);
Ok(())
}
pub fn load_from_str(&mut self, json: &str) -> Result<()> {
let constraints: ProfileConstraints = serde_json::from_str(json).map_err(|e| {
CmlError::ValidationError(format!("Failed to parse constraints: {}", e))
})?;
self.constraints
.insert(constraints.profile.clone(), constraints);
Ok(())
}
pub fn get(&mut self, name: &str) -> Result<&ResolvedConstraints> {
if self.resolved.contains_key(name) {
return Ok(self.resolved.get(name).unwrap());
}
let resolved = self.resolve_constraints(name)?;
self.resolved.insert(name.to_string(), resolved);
Ok(self.resolved.get(name).unwrap())
}
fn resolve_constraints(&self, name: &str) -> Result<ResolvedConstraints> {
let constraints = self
.constraints
.get(name)
.ok_or_else(|| CmlError::ValidationError(format!("Constraints not found: {}", name)))?;
let mut resolved = ResolvedConstraints {
profile: name.to_string(),
..Default::default()
};
if let Some(parent_name) = &constraints.extends {
let parent = self.resolve_constraints(parent_name)?;
resolved.document = parent.document;
resolved.elements = parent.elements;
resolved.attributes = parent.attributes;
resolved.hierarchy = parent.hierarchy;
resolved.nesting = parent.nesting;
resolved.list_constraints = parent.list_constraints;
resolved.semantic_rules = parent.semantic_rules;
}
if constraints.document.is_some() {
resolved.document = constraints.document.clone();
}
for (name, constraint) in &constraints.elements {
resolved.elements.insert(name.clone(), constraint.clone());
}
for (name, constraint) in &constraints.attributes {
resolved.attributes.insert(name.clone(), constraint.clone());
}
for (name, constraint) in &constraints.hierarchy {
resolved.hierarchy.insert(name.clone(), constraint.clone());
}
if constraints.nesting.is_some() {
resolved.nesting = constraints.nesting.clone();
}
if constraints.list_constraints.is_some() {
resolved.list_constraints = constraints.list_constraints.clone();
}
for (name, rule) in &constraints.semantic_rules {
resolved.semantic_rules.insert(name.clone(), rule.clone());
}
Ok(resolved)
}
}
impl Default for ConstraintRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_core_profile() {
let json = r#"{
"name": "test-core",
"version": "0.2",
"elements": {
"cml": { "content": "block" },
"header": { "content": "block" },
"body": { "content": "block" },
"footer": { "content": "block" }
}
}"#;
let profile: Profile = serde_json::from_str(json).unwrap();
assert_eq!(profile.name, "test-core");
assert_eq!(profile.elements.len(), 4);
}
#[test]
fn test_profile_inheritance() {
let mut registry = ProfileRegistry::new();
registry
.load_from_str(
r#"{
"name": "parent",
"version": "0.2",
"elements": {
"a": { "content": "text" },
"b": { "content": "text" },
"c": { "content": "text" }
}
}"#,
)
.unwrap();
registry
.load_from_str(
r#"{
"name": "child",
"version": "0.2",
"extends": "parent",
"include": ["a", "b"],
"elements": {
"d": { "content": "text" }
}
}"#,
)
.unwrap();
let resolved = registry.get("child").unwrap();
assert!(resolved.elements.contains_key("a"));
assert!(resolved.elements.contains_key("b"));
assert!(!resolved.elements.contains_key("c")); assert!(resolved.elements.contains_key("d")); }
#[test]
fn test_type_vocabularies() {
let mut registry = ProfileRegistry::new();
registry
.load_from_str(
r#"{
"name": "typed",
"version": "0.2",
"types": {
"date": ["created", "updated", "published"],
"section": ["intro", "body", "conclusion"]
}
}"#,
)
.unwrap();
assert!(registry
.validate_type_value("typed", "date", "created")
.unwrap());
assert!(registry
.validate_type_value("typed", "date", "published")
.unwrap());
assert!(!registry
.validate_type_value("typed", "date", "invalid")
.unwrap());
}
}