Skip to main content

triblespace_core/
attribute.rs

1//! Field helper type used by the query macros.
2//!
3//! The `Field<S>` type is a small, const-friendly wrapper around a 16-byte
4//! attribute id (RawId) and a phantom type parameter `S` indicating the value
5//! schema for that attribute. We keep construction simple and const-friendly so
6//! fields can be declared as `pub const F: Field<ShortString> = Field::from(hex!("..."));`.
7
8use 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/// Describes a concrete usage of an attribute in source code.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub struct AttributeUsage {
25    /// Contextual name for this usage (may differ across codebases).
26    pub name: &'static str,
27    /// Optional human-facing description for this usage.
28    pub description: Option<&'static str>,
29    /// Optional source location to disambiguate multiple usages.
30    pub source: Option<AttributeUsageSource>,
31}
32
33/// Source location metadata for attribute usages.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct AttributeUsageSource {
36    /// Fully qualified Rust module path (e.g. `"crate::schema::core"`).
37    pub module_path: &'static str,
38    /// Source file path where the attribute is used.
39    pub file: &'static str,
40    /// Line number within the source file.
41    pub line: u32,
42    /// Column number within the source line.
43    pub column: u32,
44}
45
46impl AttributeUsageSource {}
47
48impl AttributeUsage {
49    const USAGE_DOMAIN: &'static [u8] = b"triblespace.attribute_usage";
50    /// Construct a minimal usage entry with a name.
51    pub const fn named(name: &'static str) -> Self {
52        Self {
53            name,
54            description: None,
55            source: None,
56        }
57    }
58
59    /// Set a human-facing description for this usage.
60    pub const fn description(mut self, description: &'static str) -> Self {
61        self.description = Some(description);
62        self
63    }
64
65    /// Set a source location for this usage.
66    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/// A typed reference to an attribute id together with its value schema.
118#[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    /// Construct a `Field` from a raw 16-byte id and a fully specified usage.
139    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    /// Construct a `Field` from a raw 16-byte id without attaching a static name.
149    /// Prefer [`Attribute::from_id_with_usage`] when a static usage is available.
150    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    /// Return the underlying raw id bytes.
160    pub const fn raw(&self) -> RawId {
161        self.raw
162    }
163
164    /// Convert to a runtime [`Id`](crate::id::Id) value. This performs the nil check and will
165    /// panic if the raw id is the nil id (all zeros).
166    pub fn id(&self) -> crate::id::Id {
167        crate::id::Id::new(self.raw).unwrap()
168    }
169
170    /// Convert a host value into a typed `Value<S>` using the Field's schema.
171    /// This is a small convenience wrapper around the `ToValue` trait and
172    /// simplifies macro expansion: `af.value_from(expr)` preserves the
173    /// schema `S` for type inference.
174    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    /// Coerce an existing variable of any schema into a variable typed with
179    /// this field's schema. This is a convenience for macros: they can
180    /// allocate an untyped/UnknownValue variable and then annotate it with the
181    /// field's schema using `af.as_variable(raw_var)`.
182    ///
183    /// The operation is a zero-cost conversion as variables are simply small
184    /// integer indexes; the implementation uses an unsafe transmute to change
185    /// the type parameter without moving the underlying data.
186    pub fn as_variable(&self, v: crate::query::Variable<S>) -> crate::query::Variable<S> {
187        v
188    }
189
190    /// Returns the declared name of this attribute, if any.
191    pub fn name(&self) -> Option<&str> {
192        self.usage.map(|usage| usage.name)
193    }
194
195    /// Attach usage metadata to an attribute.
196    pub const fn with_usage(mut self, usage: AttributeUsage) -> Self {
197        self.usage = Some(usage);
198        self
199    }
200
201    /// Derive an attribute id from a dynamic name and this schema's metadata.
202    ///
203    /// The identifier is computed by hashing the field name handle produced as a
204    /// `Handle<Blake3, crate::blob::schemas::longstring::LongString>` together with the
205    /// schema's [`crate::metadata::ConstId::ID`].
206    /// The resulting 32-byte Blake3 digest uses its rightmost 16 bytes to match the
207    /// `RawId` layout used by [`Attribute::from_id`].
208    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
255/// Re-export of [`RawId`](crate::id::RawId) used by generated macro code.
256pub 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}