triblespace_core/
attribute.rs1use crate::blob::schemas::longstring::LongString;
9use crate::blob::ToBlob;
10use crate::id::ExclusiveId;
11use crate::id::RawId;
12use crate::macros::entity;
13use crate::metadata::{self, Describe};
14use crate::trible::Fragment;
15use crate::trible::TribleSet;
16use crate::value::schemas::genid::GenId;
17use crate::value::schemas::hash::Blake3;
18use crate::value::ValueSchema;
19use blake3::Hasher;
20use core::marker::PhantomData;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct AttributeUsage {
25 pub name: &'static str,
27 pub description: Option<&'static str>,
29 pub source: Option<AttributeUsageSource>,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct AttributeUsageSource {
36 pub module_path: &'static str,
38 pub file: &'static str,
40 pub line: u32,
42 pub column: u32,
44}
45
46impl AttributeUsageSource {}
47
48impl AttributeUsage {
49 const USAGE_DOMAIN: &'static [u8] = b"triblespace.attribute_usage";
50 pub const fn named(name: &'static str) -> Self {
52 Self {
53 name,
54 description: None,
55 source: None,
56 }
57 }
58
59 pub const fn description(mut self, description: &'static str) -> Self {
61 self.description = Some(description);
62 self
63 }
64
65 pub const fn source(mut self, source: AttributeUsageSource) -> Self {
67 self.source = Some(source);
68 self
69 }
70
71 fn usage_id(&self, attribute_id: crate::id::Id) -> crate::id::Id {
72 let mut hasher = Hasher::new();
73 hasher.update(Self::USAGE_DOMAIN);
74 hasher.update(attribute_id.as_ref());
75 if let Some(source) = self.source {
76 hasher.update(source.module_path.as_bytes());
77 }
78 let digest = hasher.finalize();
79 let mut raw = [0u8; crate::id::ID_LEN];
80 let lower_half = &digest.as_bytes()[digest.as_bytes().len() - crate::id::ID_LEN..];
81 raw.copy_from_slice(lower_half);
82 crate::id::Id::new(raw).expect("usage id must be non-nil")
83 }
84
85 fn describe<B>(
86 &self,
87 blobs: &mut B,
88 attribute_id: crate::id::Id,
89 ) -> Result<Fragment, B::PutError>
90 where
91 B: crate::repo::BlobStore<Blake3>,
92 {
93 let mut tribles = TribleSet::new();
94 let usage_id = self.usage_id(attribute_id);
95 let usage_entity = ExclusiveId::force_ref(&usage_id);
96
97 tribles += entity! { &usage_entity @ metadata::name: blobs.put(self.name)? };
98
99 if let Some(description) = self.description {
100 let description_handle = blobs.put(description)?;
101 tribles += entity! { &usage_entity @ metadata::description: description_handle };
102 }
103
104 if let Some(source) = self.source {
105 let module_handle = blobs.put(source.module_path)?;
106 tribles += entity! { &usage_entity @ metadata::source_module: module_handle };
107 }
108
109 tribles += entity! { &usage_entity @
110 metadata::attribute: GenId::value_from(attribute_id),
111 metadata::tag: metadata::KIND_ATTRIBUTE_USAGE,
112 };
113
114 Ok(Fragment::rooted(usage_id, tribles))
115 }
116}
117#[derive(Debug, PartialEq, Eq, Hash)]
119pub struct Attribute<S: ValueSchema> {
120 raw: RawId,
121 handle: Option<crate::value::Value<crate::value::schemas::hash::Handle<Blake3, LongString>>>,
122 usage: Option<AttributeUsage>,
123 _schema: PhantomData<S>,
124}
125
126impl<S: ValueSchema> Clone for Attribute<S> {
127 fn clone(&self) -> Self {
128 Self {
129 raw: self.raw,
130 handle: self.handle,
131 usage: self.usage,
132 _schema: PhantomData,
133 }
134 }
135}
136
137impl<S: ValueSchema> Attribute<S> {
138 pub const fn from_id_with_usage(raw: RawId, usage: AttributeUsage) -> Self {
140 Self {
141 raw,
142 handle: None,
143 usage: Some(usage),
144 _schema: PhantomData,
145 }
146 }
147
148 pub const fn from_id(raw: RawId) -> Self {
151 Self {
152 raw,
153 handle: None,
154 usage: None,
155 _schema: PhantomData,
156 }
157 }
158
159 pub const fn raw(&self) -> RawId {
161 self.raw
162 }
163
164 pub fn id(&self) -> crate::id::Id {
167 crate::id::Id::new(self.raw).unwrap()
168 }
169
170 pub fn value_from<T: crate::value::ToValue<S>>(&self, v: T) -> crate::value::Value<S> {
175 crate::value::ToValue::to_value(v)
176 }
177
178 pub fn as_variable(&self, v: crate::query::Variable<S>) -> crate::query::Variable<S> {
187 v
188 }
189
190 pub fn name(&self) -> Option<&str> {
192 self.usage.map(|usage| usage.name)
193 }
194
195 pub const fn with_usage(mut self, usage: AttributeUsage) -> Self {
197 self.usage = Some(usage);
198 self
199 }
200
201 pub fn from_name(name: &str) -> Self {
209 let field_handle = String::from(name).to_blob().get_handle::<Blake3>();
210 let mut hasher = Hasher::new();
211 hasher.update(&field_handle.raw);
212 hasher.update(&<S as crate::metadata::ConstId>::ID.raw());
213
214 let digest = hasher.finalize();
215 let mut raw = [0u8; crate::id::ID_LEN];
216 let lower_half = &digest.as_bytes()[digest.as_bytes().len() - crate::id::ID_LEN..];
217 raw.copy_from_slice(lower_half);
218
219 Self {
220 raw,
221 handle: Some(field_handle),
222 usage: None,
223 _schema: PhantomData,
224 }
225 }
226}
227
228impl<S> Describe for Attribute<S>
229where
230 S: ValueSchema + crate::metadata::ConstDescribe,
231{
232 fn describe<B>(&self, blobs: &mut B) -> Result<Fragment, B::PutError>
233 where
234 B: crate::repo::BlobStore<Blake3>,
235 {
236 let mut tribles = TribleSet::new();
237 let id = self.id();
238
239 if let Some(handle) = self.handle {
240 tribles += entity! { ExclusiveId::force_ref(&id) @ metadata::name: handle };
241 }
242
243 tribles += entity! { ExclusiveId::force_ref(&id) @ metadata::value_schema: GenId::value_from(<S as crate::metadata::ConstId>::ID) };
244
245 if let Some(usage) = self.usage {
246 tribles += usage.describe(blobs, id)?;
247 }
248
249 tribles += <S as crate::metadata::ConstDescribe>::describe(blobs)?.into_facts();
250
251 Ok(Fragment::rooted(id, tribles))
252 }
253}
254
255pub use crate::id::RawId as RawIdAlias;
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::blob::schemas::longstring::LongString;
262 use crate::value::schemas::hash::{Blake3, Handle};
263 use crate::value::schemas::shortstring::ShortString;
264
265 #[test]
266 fn dynamic_field_is_deterministic() {
267 let a1 = Attribute::<ShortString>::from_name("title");
268 let a2 = Attribute::<ShortString>::from_name("title");
269
270 assert_eq!(a1.raw(), a2.raw());
271 assert_ne!(a1.raw(), [0; crate::id::ID_LEN]);
272 }
273
274 #[test]
275 fn dynamic_field_changes_with_name() {
276 let title = Attribute::<ShortString>::from_name("title");
277 let author = Attribute::<ShortString>::from_name("author");
278
279 assert_ne!(title.raw(), author.raw());
280 }
281
282 #[test]
283 fn dynamic_field_changes_with_schema() {
284 let short = Attribute::<ShortString>::from_name("title");
285 let handle = Attribute::<Handle<Blake3, LongString>>::from_name("title");
286
287 assert_ne!(short.raw(), handle.raw());
288 }
289}