Skip to main content

dtcs/model/
registry.rs

1//! Registry model (SPEC Chapter 22).
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5
6/// A contract-level reference to an external registry catalog.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct RegistryRef {
9    /// Registry identifier.
10    pub id: String,
11    /// Registry URI or location.
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub uri: Option<String>,
14}
15
16/// Publication status for a registry document (Ch 22 §4).
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum RegistryPublicationStatus {
20    /// Work in progress.
21    Draft,
22    /// Published for experimental use.
23    Experimental,
24    /// Normative standard publication.
25    Standard,
26    /// Still valid but discouraged.
27    Deprecated,
28    /// No longer supported.
29    Obsolete,
30}
31
32impl RegistryPublicationStatus {
33    /// Returns the serialized status name.
34    #[must_use]
35    pub fn as_str(self) -> &'static str {
36        match self {
37            Self::Draft => "draft",
38            Self::Experimental => "experimental",
39            Self::Standard => "standard",
40            Self::Deprecated => "deprecated",
41            Self::Obsolete => "obsolete",
42        }
43    }
44}
45
46/// Status of an individual registry entry (Ch 22 §10).
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "lowercase")]
49pub enum RegistryEntryStatus {
50    /// Work in progress.
51    Draft,
52    /// Published for experimental use.
53    Experimental,
54    /// Candidate entry awaiting independent conformance evidence.
55    Candidate,
56    /// Normative standard entry.
57    Standard,
58    /// Still valid but discouraged.
59    Deprecated,
60    /// No longer supported; identifier remains reserved.
61    Obsolete,
62}
63
64impl RegistryEntryStatus {
65    /// Returns the serialized status name.
66    #[must_use]
67    pub fn as_str(self) -> &'static str {
68        match self {
69            Self::Draft => "draft",
70            Self::Experimental => "experimental",
71            Self::Candidate => "candidate",
72            Self::Standard => "standard",
73            Self::Deprecated => "deprecated",
74            Self::Obsolete => "obsolete",
75        }
76    }
77}
78
79/// Category of a registry entry (Ch 22 §3).
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub enum RegistryCategory {
83    /// Semantic action identifier.
84    SemanticAction,
85    /// Function identifier.
86    Function,
87    /// Operator identifier (`dtcs:eq`, …).
88    Operator,
89    /// Rule identifier.
90    Rule,
91    /// Logical type identifier.
92    LogicalType,
93    /// Diagnostic code.
94    Diagnostic,
95    /// Extension namespace.
96    ExtensionNamespace,
97    /// Conformance profile.
98    Profile,
99    /// Engine capability.
100    Capability,
101}
102
103impl RegistryCategory {
104    /// Returns the serialized category name.
105    #[must_use]
106    pub fn as_str(self) -> &'static str {
107        match self {
108            Self::SemanticAction => "semanticAction",
109            Self::Function => "function",
110            Self::Operator => "operator",
111            Self::Rule => "rule",
112            Self::LogicalType => "logicalType",
113            Self::Diagnostic => "diagnostic",
114            Self::ExtensionNamespace => "extensionNamespace",
115            Self::Profile => "profile",
116            Self::Capability => "capability",
117        }
118    }
119}
120
121/// Compatibility requirement for an extension entry (Ch 21 §7).
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "lowercase")]
124pub enum ExtensionCompatibility {
125    /// Unsupported mandatory extensions prevent successful processing.
126    Mandatory,
127    /// Optional extensions may be preserved without interpretation.
128    Optional,
129}
130
131impl ExtensionCompatibility {
132    /// Returns the serialized compatibility name.
133    #[must_use]
134    pub fn as_str(self) -> &'static str {
135        match self {
136            Self::Mandatory => "mandatory",
137            Self::Optional => "optional",
138        }
139    }
140}
141
142/// A single registry entry (Ch 22 §5).
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct RegistryEntry {
146    /// Stable identifier (for example `dtcs:lowercase`).
147    ///
148    /// When omitted in a map-keyed registry document, the map key is used.
149    #[serde(default)]
150    pub id: String,
151    /// Human-readable name.
152    pub name: String,
153    /// Entry category.
154    pub category: RegistryCategory,
155    /// Entry version.
156    pub version: String,
157    /// Publication status.
158    pub status: RegistryEntryStatus,
159    /// Compatibility requirement (primarily for extension namespaces).
160    #[serde(default, skip_serializing_if = "Option::is_none")]
161    pub compatibility: Option<ExtensionCompatibility>,
162    /// Semantic definition summary.
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub definition: Option<String>,
165    /// Normative references.
166    #[serde(default, skip_serializing_if = "Vec::is_empty")]
167    pub references: Vec<String>,
168    /// Whether this implementation supports the entry.
169    #[serde(default = "default_supported")]
170    pub supported: bool,
171}
172
173fn default_supported() -> bool {
174    true
175}
176
177/// An authoritative registry catalog document (Ch 22 §4).
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct RegistryDocument {
181    /// Registry identifier.
182    pub id: String,
183    /// Registry version.
184    pub version: String,
185    /// Governing specification version.
186    pub governing_specification: String,
187    /// Publication status of the registry document.
188    pub publication_status: RegistryPublicationStatus,
189    /// Catalog entries keyed by stable identifier.
190    #[serde(default)]
191    pub entries: IndexMap<String, RegistryEntry>,
192}
193
194impl RegistryDocument {
195    /// Returns the entry for `id`, if present.
196    #[must_use]
197    pub fn get(&self, id: &str) -> Option<&RegistryEntry> {
198        self.entries.get(id)
199    }
200
201    /// Returns an entry by identifier and registry category.
202    #[must_use]
203    pub fn get_category(&self, id: &str, category: RegistryCategory) -> Option<&RegistryEntry> {
204        self.entries
205            .values()
206            .find(|entry| entry.id == id && entry.category == category)
207    }
208
209    /// Inserts or replaces an entry, keyed by `entry.id`.
210    pub fn insert(&mut self, entry: RegistryEntry) {
211        self.entries.insert(entry.id.clone(), entry);
212    }
213
214    /// Merges `other` into this document.
215    ///
216    /// Non-`dtcs:` entries from `other` override existing entries. `dtcs:` entries
217    /// already present in this document are preserved (builtin authority). Novel
218    /// `dtcs:` keys from `other` are rejected.
219    pub fn merge(
220        &mut self,
221        other: &RegistryDocument,
222    ) -> Result<(), crate::diagnostics::DiagnosticReport> {
223        for (id, entry) in &other.entries {
224            if id.starts_with("dtcs:") {
225                if self.entries.contains_key(id) {
226                    continue;
227                }
228                let mut report = crate::diagnostics::DiagnosticReport::new();
229                report.push(
230                    crate::diagnostics::Diagnostic::new(
231                        crate::diagnostics::codes::INVALID_REGISTRY,
232                        crate::diagnostics::Severity::Error,
233                        crate::diagnostics::DiagnosticStage::Validation,
234                        crate::diagnostics::DiagnosticCategory::Structure,
235                        format!(
236                            "registry merge rejected novel standard entry '{id}'; vendor catalogs cannot extend the dtcs: namespace"
237                        ),
238                    )
239                    .with_object_ref(format!("entries.{id}"))
240                    .with_remediation(
241                        "Use vendor namespaces for custom identifiers; only builtin dtcs: entries are authoritative",
242                    ),
243                );
244                return Err(report);
245            }
246            self.entries.insert(id.clone(), entry.clone());
247        }
248        Ok(())
249    }
250
251    /// Merges `other` into this document without rejecting novel `dtcs:` keys.
252    ///
253    /// Reserved for trusted builtin catalog assembly.
254    pub(crate) fn merge_trusted(&mut self, other: &RegistryDocument) {
255        for (id, entry) in &other.entries {
256            if id.starts_with("dtcs:") && self.entries.contains_key(id) {
257                if self
258                    .entries
259                    .get(id)
260                    .is_some_and(|existing| existing.category != entry.category)
261                {
262                    self.entries
263                        .insert(format!("{}:{id}", entry.category.as_str()), entry.clone());
264                }
265                continue;
266            }
267            self.entries.insert(id.clone(), entry.clone());
268        }
269    }
270
271    /// Returns all entries in insertion order.
272    #[must_use]
273    pub fn list(&self) -> Vec<&RegistryEntry> {
274        self.entries.values().collect()
275    }
276}