knowledge_base_extension_framework/
contracts.rs1use 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#[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
51pub struct ExtensionId(Cow<'static, str>);
52
53impl ExtensionId {
54 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
103pub struct BindingKey(Cow<'static, str>);
104
105impl BindingKey {
106 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
153const 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
185pub enum BindingKind {
186 EntityType,
187 Property,
188}
189
190#[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 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#[derive(Clone, Debug, Eq, PartialEq)]
241pub struct ExtensionDependency {
242 pub id: ExtensionId,
243 pub contract: ContractVersion,
244}
245
246#[derive(Clone, Debug, Eq, PartialEq)]
248pub struct BindingDeclaration {
249 pub key: BindingKey,
250 pub kind: BindingKind,
251}
252
253#[derive(Clone, Debug, Eq, PartialEq)]
255pub struct EntityTypeRequirement {
256 pub binding: BindingReference,
257}
258
259#[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#[derive(Clone, Debug, Default, Eq, PartialEq)]
273pub struct OntologyRequirements {
274 pub entity_types: Vec<EntityTypeRequirement>,
275 pub properties: Vec<PropertyRequirement>,
276}
277
278#[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
288pub 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}