use crate::blob::schemas::longstring::LongString;
use crate::blob::ToBlob;
use crate::id::ExclusiveId;
use crate::id::RawId;
use crate::macros::entity;
use crate::metadata::{self, Describe};
use crate::trible::Fragment;
use crate::trible::TribleSet;
use crate::value::schemas::genid::GenId;
use crate::value::schemas::hash::Blake3;
use crate::value::ValueSchema;
use blake3::Hasher;
use core::marker::PhantomData;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AttributeUsage {
pub name: &'static str,
pub description: Option<&'static str>,
pub source: Option<AttributeUsageSource>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AttributeUsageSource {
pub module_path: &'static str,
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl AttributeUsageSource {}
impl AttributeUsage {
const USAGE_DOMAIN: &'static [u8] = b"triblespace.attribute_usage";
pub const fn named(name: &'static str) -> Self {
Self {
name,
description: None,
source: None,
}
}
pub const fn description(mut self, description: &'static str) -> Self {
self.description = Some(description);
self
}
pub const fn source(mut self, source: AttributeUsageSource) -> Self {
self.source = Some(source);
self
}
fn usage_id(&self, attribute_id: crate::id::Id) -> crate::id::Id {
let mut hasher = Hasher::new();
hasher.update(Self::USAGE_DOMAIN);
hasher.update(attribute_id.as_ref());
if let Some(source) = self.source {
hasher.update(source.module_path.as_bytes());
}
let digest = hasher.finalize();
let mut raw = [0u8; crate::id::ID_LEN];
let lower_half = &digest.as_bytes()[digest.as_bytes().len() - crate::id::ID_LEN..];
raw.copy_from_slice(lower_half);
crate::id::Id::new(raw).expect("usage id must be non-nil")
}
fn describe<B>(
&self,
blobs: &mut B,
attribute_id: crate::id::Id,
) -> Result<Fragment, B::PutError>
where
B: crate::repo::BlobStore<Blake3>,
{
let mut tribles = TribleSet::new();
let usage_id = self.usage_id(attribute_id);
let usage_entity = ExclusiveId::force_ref(&usage_id);
tribles += entity! { &usage_entity @ metadata::name: blobs.put(self.name)? };
if let Some(description) = self.description {
let description_handle = blobs.put(description)?;
tribles += entity! { &usage_entity @ metadata::description: description_handle };
}
if let Some(source) = self.source {
let module_handle = blobs.put(source.module_path)?;
tribles += entity! { &usage_entity @ metadata::source_module: module_handle };
}
tribles += entity! { &usage_entity @
metadata::attribute: GenId::value_from(attribute_id),
metadata::tag: metadata::KIND_ATTRIBUTE_USAGE,
};
Ok(Fragment::rooted(usage_id, tribles))
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Attribute<S: ValueSchema> {
raw: RawId,
handle: Option<crate::value::Value<crate::value::schemas::hash::Handle<Blake3, LongString>>>,
usage: Option<AttributeUsage>,
_schema: PhantomData<S>,
}
impl<S: ValueSchema> Clone for Attribute<S> {
fn clone(&self) -> Self {
Self {
raw: self.raw,
handle: self.handle,
usage: self.usage,
_schema: PhantomData,
}
}
}
impl<S: ValueSchema> Attribute<S> {
pub const fn from_id_with_usage(raw: RawId, usage: AttributeUsage) -> Self {
Self {
raw,
handle: None,
usage: Some(usage),
_schema: PhantomData,
}
}
pub const fn from_id(raw: RawId) -> Self {
Self {
raw,
handle: None,
usage: None,
_schema: PhantomData,
}
}
pub const fn raw(&self) -> RawId {
self.raw
}
pub fn id(&self) -> crate::id::Id {
crate::id::Id::new(self.raw).unwrap()
}
pub fn value_from<T: crate::value::ToValue<S>>(&self, v: T) -> crate::value::Value<S> {
crate::value::ToValue::to_value(v)
}
pub fn as_variable(&self, v: crate::query::Variable<S>) -> crate::query::Variable<S> {
v
}
pub fn name(&self) -> Option<&str> {
self.usage.map(|usage| usage.name)
}
pub const fn with_usage(mut self, usage: AttributeUsage) -> Self {
self.usage = Some(usage);
self
}
pub fn from_name(name: &str) -> Self {
let field_handle = String::from(name).to_blob().get_handle::<Blake3>();
let mut hasher = Hasher::new();
hasher.update(&field_handle.raw);
hasher.update(&<S as crate::metadata::ConstId>::ID.raw());
let digest = hasher.finalize();
let mut raw = [0u8; crate::id::ID_LEN];
let lower_half = &digest.as_bytes()[digest.as_bytes().len() - crate::id::ID_LEN..];
raw.copy_from_slice(lower_half);
Self {
raw,
handle: Some(field_handle),
usage: None,
_schema: PhantomData,
}
}
}
impl<S> Describe for Attribute<S>
where
S: ValueSchema + crate::metadata::ConstDescribe,
{
fn describe<B>(&self, blobs: &mut B) -> Result<Fragment, B::PutError>
where
B: crate::repo::BlobStore<Blake3>,
{
let mut tribles = TribleSet::new();
let id = self.id();
if let Some(handle) = self.handle {
tribles += entity! { ExclusiveId::force_ref(&id) @ metadata::name: handle };
}
tribles += entity! { ExclusiveId::force_ref(&id) @ metadata::value_schema: GenId::value_from(<S as crate::metadata::ConstId>::ID) };
if let Some(usage) = self.usage {
tribles += usage.describe(blobs, id)?;
}
tribles += <S as crate::metadata::ConstDescribe>::describe(blobs)?.into_facts();
Ok(Fragment::rooted(id, tribles))
}
}
pub use crate::id::RawId as RawIdAlias;
#[cfg(test)]
mod tests {
use super::*;
use crate::blob::schemas::longstring::LongString;
use crate::value::schemas::hash::{Blake3, Handle};
use crate::value::schemas::shortstring::ShortString;
#[test]
fn dynamic_field_is_deterministic() {
let a1 = Attribute::<ShortString>::from_name("title");
let a2 = Attribute::<ShortString>::from_name("title");
assert_eq!(a1.raw(), a2.raw());
assert_ne!(a1.raw(), [0; crate::id::ID_LEN]);
}
#[test]
fn dynamic_field_changes_with_name() {
let title = Attribute::<ShortString>::from_name("title");
let author = Attribute::<ShortString>::from_name("author");
assert_ne!(title.raw(), author.raw());
}
#[test]
fn dynamic_field_changes_with_schema() {
let short = Attribute::<ShortString>::from_name("title");
let handle = Attribute::<Handle<Blake3, LongString>>::from_name("title");
assert_ne!(short.raw(), handle.raw());
}
}