Skip to main content

knowledge_base_extension_framework/
contracts.rs

1use crate::bindings::ResolvedBindings;
2use crate::error::{FrameworkError, IdentifierError};
3use knowledge_base_models::{Cardinality, PropertyUsage, ValueType};
4use knowledge_base_validation::KnowledgeBaseValidator;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use std::borrow::Cow;
7use std::collections::BTreeSet;
8use std::fmt;
9use std::str::FromStr;
10use std::sync::Arc;
11
12/// An exact integer version of an extension contract.
13#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct ContractVersion(u32);
15
16impl ContractVersion {
17    pub const fn new(value: u32) -> Self {
18        Self(value)
19    }
20    pub const fn get(self) -> u32 {
21        self.0
22    }
23}
24
25impl fmt::Display for ContractVersion {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        self.0.fmt(formatter)
28    }
29}
30
31impl Serialize for ContractVersion {
32    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
33    where
34        S: Serializer,
35    {
36        serializer.serialize_u32(self.0)
37    }
38}
39
40impl<'de> Deserialize<'de> for ContractVersion {
41    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
42    where
43        D: Deserializer<'de>,
44    {
45        u32::deserialize(deserializer).map(Self)
46    }
47}
48
49/// A unique lowercase kebab-case extension identifier.
50#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub struct ExtensionId(Cow<'static, str>);
52
53impl ExtensionId {
54    /// Constructs a canonical extension identifier from a static literal.
55    ///
56    /// Invalid literals cause compilation to fail when used in a constant.
57    pub const fn from_static(value: &'static str) -> Self {
58        assert!(canonical_segments(value, b'-'), "extension identifier must be lowercase kebab-case");
59        Self(Cow::Borrowed(value))
60    }
61
62    pub fn as_str(&self) -> &str {
63        &self.0
64    }
65}
66
67impl fmt::Display for ExtensionId {
68    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
69        formatter.write_str(&self.0)
70    }
71}
72
73impl FromStr for ExtensionId {
74    type Err = IdentifierError;
75
76    fn from_str(value: &str) -> Result<Self, Self::Err> {
77        canonical_segments(value, b'-')
78            .then(|| Self(Cow::Owned(value.to_owned())))
79            .ok_or_else(|| IdentifierError::new(value, "lowercase kebab-case"))
80    }
81}
82
83impl Serialize for ExtensionId {
84    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85    where
86        S: Serializer,
87    {
88        serializer.serialize_str(&self.0)
89    }
90}
91
92impl<'de> Deserialize<'de> for ExtensionId {
93    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
94    where
95        D: Deserializer<'de>,
96    {
97        String::deserialize(deserializer)?.parse().map_err(serde::de::Error::custom)
98    }
99}
100
101/// A lowercase snake_case semantic-binding key.
102#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
103pub struct BindingKey(Cow<'static, str>);
104
105impl BindingKey {
106    /// Constructs a canonical semantic-binding key from a static literal.
107    ///
108    /// Invalid literals cause compilation to fail when used in a constant.
109    pub const fn from_static(value: &'static str) -> Self {
110        assert!(canonical_segments(value, b'_'), "binding key must be lowercase snake_case");
111        Self(Cow::Borrowed(value))
112    }
113
114    pub fn as_str(&self) -> &str {
115        &self.0
116    }
117}
118
119impl fmt::Display for BindingKey {
120    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121        formatter.write_str(&self.0)
122    }
123}
124
125impl FromStr for BindingKey {
126    type Err = IdentifierError;
127
128    fn from_str(value: &str) -> Result<Self, Self::Err> {
129        canonical_segments(value, b'_')
130            .then(|| Self(Cow::Owned(value.to_owned())))
131            .ok_or_else(|| IdentifierError::new(value, "lowercase snake_case"))
132    }
133}
134
135impl Serialize for BindingKey {
136    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
137    where
138        S: Serializer,
139    {
140        serializer.serialize_str(&self.0)
141    }
142}
143
144impl<'de> Deserialize<'de> for BindingKey {
145    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
146    where
147        D: Deserializer<'de>,
148    {
149        String::deserialize(deserializer)?.parse().map_err(serde::de::Error::custom)
150    }
151}
152
153/// Validates names whose segments start with a lowercase letter and then contain
154/// lowercase ASCII letters or digits. The separator joins, but never empties, segments.
155const fn canonical_segments(value: &str, separator: u8) -> bool {
156    let bytes = value.as_bytes();
157    if bytes.is_empty() {
158        return false;
159    }
160
161    let mut index = 0;
162    let mut starts_segment = true;
163    while index < bytes.len() {
164        let byte = bytes[index];
165        if byte == separator {
166            if starts_segment {
167                return false;
168            }
169            starts_segment = true;
170        } else if starts_segment {
171            if !(byte >= b'a' && byte <= b'z') {
172                return false;
173            }
174            starts_segment = false;
175        } else if !((byte >= b'a' && byte <= b'z') || (byte >= b'0' && byte <= b'9')) {
176            return false;
177        }
178        index += 1;
179    }
180    !starts_segment
181}
182
183/// The ontology identifier kind expected by a semantic binding.
184#[derive(Clone, Copy, Debug, Eq, PartialEq)]
185pub enum BindingKind {
186    EntityType,
187    Property,
188}
189
190/// A fully qualified semantic binding reference, such as `wikidata:item_id_property`.
191#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
192pub struct BindingReference {
193    extension_id: ExtensionId,
194    key: BindingKey,
195}
196
197impl BindingReference {
198    pub fn new(extension_id: ExtensionId, key: BindingKey) -> Self {
199        Self { extension_id, key }
200    }
201    pub fn extension_id(&self) -> &ExtensionId {
202        &self.extension_id
203    }
204    pub fn key(&self) -> &BindingKey {
205        &self.key
206    }
207
208    /// Constructs a canonical binding reference from static literals.
209    ///
210    /// Invalid literals cause compilation to fail when used in a constant.
211    pub const fn from_static(extension_id: &'static str, key: &'static str) -> Self {
212        Self {
213            extension_id: ExtensionId::from_static(extension_id),
214            key: BindingKey::from_static(key),
215        }
216    }
217}
218
219impl fmt::Display for BindingReference {
220    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
221        write!(formatter, "{}:{}", self.extension_id, self.key)
222    }
223}
224
225impl FromStr for BindingReference {
226    type Err = IdentifierError;
227
228    fn from_str(value: &str) -> Result<Self, Self::Err> {
229        let Some((extension, key)) = value.split_once(':') else {
230            return Err(IdentifierError::new(value, "<extension-id>:<binding_key>"));
231        };
232        if key.contains(':') {
233            return Err(IdentifierError::new(value, "<extension-id>:<binding_key>"));
234        }
235        Ok(Self::new(extension.parse()?, key.parse()?))
236    }
237}
238
239/// A dependency that must be available at one exact contract version.
240#[derive(Clone, Debug, Eq, PartialEq)]
241pub struct ExtensionDependency {
242    pub id: ExtensionId,
243    pub contract: ContractVersion,
244}
245
246/// A semantic binding declared by an extension.
247#[derive(Clone, Debug, Eq, PartialEq)]
248pub struct BindingDeclaration {
249    pub key: BindingKey,
250    pub kind: BindingKind,
251}
252
253/// A partial requirement for an entity-type binding.
254#[derive(Clone, Debug, Eq, PartialEq)]
255pub struct EntityTypeRequirement {
256    pub binding: BindingReference,
257}
258
259/// A partial requirement for a property binding. `None` scalar fields are unconstrained.
260#[derive(Clone, Debug, Eq, PartialEq)]
261pub struct PropertyRequirement {
262    pub binding: BindingReference,
263    pub value_type: Option<ValueType>,
264    pub usage: Option<PropertyUsage>,
265    pub cardinality: Option<Cardinality>,
266    pub subject_types: BTreeSet<BindingReference>,
267    pub target_types: Option<BTreeSet<BindingReference>>,
268    pub allowed_qualifiers: BTreeSet<BindingReference>,
269}
270
271/// Partial ontology requirements that an extension declares for its bindings.
272#[derive(Clone, Debug, Default, Eq, PartialEq)]
273pub struct OntologyRequirements {
274    pub entity_types: Vec<EntityTypeRequirement>,
275    pub properties: Vec<PropertyRequirement>,
276}
277
278/// Static metadata that defines one extension contract.
279#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct ExtensionMetadata {
281    pub id: ExtensionId,
282    pub contract: ContractVersion,
283    pub dependencies: Vec<ExtensionDependency>,
284    pub bindings: Vec<BindingDeclaration>,
285    pub ontology_requirements: OntologyRequirements,
286}
287
288/// A concrete extension implementation without CLI coupling.
289pub trait KnowledgeBaseExtension: Send + Sync {
290    fn metadata(&self) -> &ExtensionMetadata;
291    fn validators(&self, _: &ResolvedBindings) -> Result<Vec<Arc<dyn KnowledgeBaseValidator>>, FrameworkError> {
292        Ok(Vec::new())
293    }
294}