use crate::id::Id;
use crate::id::RawId;
use crate::inline::InlineEncoding;
use crate::trible::Fragment;
use core::cell::Cell;
use core::marker::PhantomData;
use std::sync::OnceLock;
std::thread_local! {
static META_DEPTH: Cell<u32> = const { Cell::new(0) };
}
struct MetaDepthGuard;
impl MetaDepthGuard {
fn enter() -> Option<Self> {
META_DEPTH.with(|depth| {
if depth.get() > 0 {
None
} else {
depth.set(1);
Some(Self)
}
})
}
}
impl Drop for MetaDepthGuard {
fn drop(&mut self) {
META_DEPTH.with(|depth| depth.set(0));
}
}
pub struct AttributeMeta {
cell: OnceLock<Fragment>,
build: fn() -> Fragment,
}
impl AttributeMeta {
pub const fn new(build: fn() -> Fragment) -> Self {
Self {
cell: OnceLock::new(),
build,
}
}
pub fn get(&self) -> Option<&Fragment> {
if let Some(fragment) = self.cell.get() {
return Some(fragment);
}
let _guard = MetaDepthGuard::enter()?;
let fragment = (self.build)();
Some(self.cell.get_or_init(|| fragment))
}
}
impl core::fmt::Debug for AttributeMeta {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("AttributeMeta")
.field("built", &self.cell.get().is_some())
.finish()
}
}
pub struct Attribute<S: InlineEncoding> {
id: Id,
fragment: Fragment,
meta: Option<&'static AttributeMeta>,
_schema: PhantomData<S>,
}
impl<S: InlineEncoding> core::fmt::Debug for Attribute<S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Attribute")
.field("id", &self.id)
.field("fragment", &self.fragment)
.finish_non_exhaustive()
}
}
impl<S: InlineEncoding> PartialEq for Attribute<S> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id && self.fragment == other.fragment
}
}
impl<S: InlineEncoding> Eq for Attribute<S> {}
impl<S: InlineEncoding> Clone for Attribute<S> {
fn clone(&self) -> Self {
Self {
id: self.id,
fragment: self.fragment.clone(),
meta: self.meta,
_schema: PhantomData,
}
}
}
impl<S: InlineEncoding + crate::metadata::MetaDescribe> Attribute<S> {
pub fn anchored(anchor: Id) -> Self {
Self::from_fragment_unchecked(crate::macros::entity! {
crate::metadata::anchor:
crate::inline::encodings::genid::GenId::inline_from(anchor),
crate::metadata::value_encoding: <S as crate::metadata::MetaDescribe>::id(),
crate::metadata::tag: crate::metadata::KIND_ATTRIBUTE,
})
}
pub fn named(name: &str) -> Self {
Self::from_fragment_unchecked(crate::macros::entity! {
crate::metadata::name: name.to_owned(),
crate::metadata::value_encoding: <S as crate::metadata::MetaDescribe>::id(),
crate::metadata::tag: crate::metadata::KIND_ATTRIBUTE,
})
}
pub fn iri(iri: &str) -> Self {
Self::from_fragment_unchecked(crate::macros::entity! {
crate::metadata::iri: iri.to_owned(),
crate::metadata::value_encoding: <S as crate::metadata::MetaDescribe>::id(),
crate::metadata::tag: crate::metadata::KIND_ATTRIBUTE,
})
}
}
impl<S: InlineEncoding> Attribute<S> {
pub fn id(&self) -> Id {
self.id
}
pub fn raw(&self) -> RawId {
self.id().into()
}
pub fn fragment(&self) -> &Fragment {
&self.fragment
}
pub fn with_meta(mut self, meta: &'static AttributeMeta) -> Self {
self.meta = Some(meta);
self
}
pub fn meta(&self) -> &Fragment {
self.meta
.and_then(AttributeMeta::get)
.unwrap_or(&self.fragment)
}
pub fn inline_from<T: crate::inline::IntoInline<S>>(&self, v: T) -> crate::inline::Inline<S> {
crate::inline::IntoInline::to_inline(v)
}
pub fn encoded_from<V>(&self, v: V) -> crate::inline::Encoded<S>
where
V: crate::inline::IntoEncoded<<S as crate::inline::InlineEncoding>::Encoding>,
<V as crate::inline::IntoEncoded<<S as crate::inline::InlineEncoding>::Encoding>>::Output:
crate::inline::ToEncoded<S>,
{
use crate::inline::ToEncoded;
v.into_encoded().to_encoded()
}
pub fn as_variable(&self, v: crate::query::Variable<S>) -> crate::query::Variable<S> {
v
}
}
impl<S: InlineEncoding> Attribute<S> {
pub fn from_fragment_unchecked(fragment: Fragment) -> Self {
let id = fragment
.root()
.expect("Attribute::from_fragment_unchecked requires a rooted fragment");
Self {
id,
fragment,
meta: None,
_schema: PhantomData,
}
}
}
impl<S> crate::metadata::Describe for Attribute<S>
where
S: InlineEncoding,
{
fn describe(&self) -> Fragment {
self.fragment.clone()
}
}
pub use crate::id::RawId as RawIdAlias;
#[cfg(test)]
mod tests {
use super::*;
use crate::blob::encodings::utf8string::UTF8String;
use crate::blob::IntoBlob;
use crate::id::Id;
use crate::inline::encodings::hash::Handle;
use crate::inline::encodings::shortstring::ShortString;
use crate::inline::Inline;
use crate::macros::{entity, find, pattern};
use crate::metadata::{self, Describe, MetaDescribe};
crate::macros::attributes! {
"5F3C1A0E7B294D6685A0C1F2E3D40912" as anchored_probe: ShortString;
"5F3C1A0E7B294D6685A0C1F2E3D40912" as anchored_probe_other: Handle<UTF8String>;
"5F3C1A0E7B294D6685A0C1F2E3D40912" unsafe as pinned_probe: ShortString;
}
#[test]
fn anchored_arm_does_not_yield_the_literal() {
let lit = "5F3C1A0E7B294D6685A0C1F2E3D40912";
assert_eq!(
format!("{:X}", pinned_probe.id()),
lit,
"pinned must be verbatim"
);
assert_ne!(
format!("{:X}", anchored_probe.id()),
lit,
"anchored must derive, not pin — if this passes the arm is not deriving"
);
}
#[test]
fn anchored_arm_separates_schemas() {
assert_ne!(anchored_probe.raw(), anchored_probe_other.raw());
}
#[test]
fn anchored_arm_matches_the_constructor() {
let a = Id::from_hex("5F3C1A0E7B294D6685A0C1F2E3D40912").unwrap();
assert_eq!(
anchored_probe.raw(),
Attribute::<ShortString>::anchored(a).raw()
);
}
#[test]
fn declared_hex_ids_are_unchanged() {
assert_eq!(
format!("{:X}", crate::metadata::value_encoding.id()),
"213F89E3F49628A105B3830BD3A6612C"
);
assert_eq!(
format!("{:X}", crate::metadata::anchor.id()),
"E16A3F51AF63084FFE1079E8A0BA57AB"
);
}
#[test]
fn anchored_is_deterministic() {
let a = Id::from_hex("2ADC6462A7F70E230558C5D681E38768").unwrap();
assert_eq!(
Attribute::<ShortString>::anchored(a).raw(),
Attribute::<ShortString>::anchored(a).raw()
);
}
#[test]
fn anchored_changes_with_schema() {
let a = Id::from_hex("2ADC6462A7F70E230558C5D681E38768").unwrap();
let short = Attribute::<ShortString>::anchored(a);
let handle =
Attribute::<Handle<crate::blob::encodings::utf8string::UTF8String>>::anchored(a);
assert_ne!(short.raw(), handle.raw());
}
#[test]
fn anchored_changes_with_anchor() {
let a = Id::from_hex("2ADC6462A7F70E230558C5D681E38768").unwrap();
let b = Id::from_hex("23178058559C762BB4B1FEAA36B3566D").unwrap();
assert_ne!(
Attribute::<ShortString>::anchored(a).raw(),
Attribute::<ShortString>::anchored(b).raw()
);
}
#[test]
fn from_fragment_permits_a_lying_schema() {
let lying = Attribute::<ShortString>::from_fragment_unchecked(entity! {
metadata::value_encoding:
<Handle<crate::blob::encodings::utf8string::UTF8String> as MetaDescribe>::id(),
});
let honest = Attribute::<ShortString>::from_fragment_unchecked(entity! {
metadata::value_encoding: <ShortString as MetaDescribe>::id(),
});
assert_ne!(lying.raw(), honest.raw());
}
#[test]
fn pinned_form_ignores_the_schema() {
let a = Id::from_hex("2ADC6462A7F70E230558C5D681E38768").unwrap();
let short = Attribute::<ShortString>::from_fragment_unchecked(entity! {
crate::id::ExclusiveId::force_ref(&a) @
metadata::value_encoding: <ShortString as MetaDescribe>::id(),
});
let handle = Attribute::<Handle<crate::blob::encodings::utf8string::UTF8String>>::from_fragment_unchecked(
entity! {
crate::id::ExclusiveId::force_ref(&a) @
metadata::value_encoding:
<Handle<crate::blob::encodings::utf8string::UTF8String> as MetaDescribe>::id(),
},
);
assert_eq!(short.raw(), handle.raw());
assert_eq!(short.id(), a);
assert_ne!(
Attribute::<ShortString>::anchored(a).raw(),
Attribute::<Handle<crate::blob::encodings::utf8string::UTF8String>>::anchored(a).raw()
);
}
#[test]
fn dynamic_field_is_deterministic() {
let h1 = "title".to_blob().get_handle();
let h2 = "title".to_blob().get_handle();
let a1 = Attribute::<ShortString>::from_fragment_unchecked(entity! {
metadata::name: h1,
metadata::value_encoding: <ShortString as MetaDescribe>::id(),
});
let a2 = Attribute::<ShortString>::from_fragment_unchecked(entity! {
metadata::name: h2,
metadata::value_encoding: <ShortString as MetaDescribe>::id(),
});
assert_eq!(a1.raw(), a2.raw());
assert_ne!(a1.raw(), [0; crate::id::ID_LEN]);
}
#[test]
fn dynamic_field_changes_with_name() {
let h_title = "title".to_blob().get_handle();
let h_author = "author".to_blob().get_handle();
let title = Attribute::<ShortString>::from_fragment_unchecked(entity! {
metadata::name: h_title,
metadata::value_encoding: <ShortString as MetaDescribe>::id(),
});
let author = Attribute::<ShortString>::from_fragment_unchecked(entity! {
metadata::name: h_author,
metadata::value_encoding: <ShortString as MetaDescribe>::id(),
});
assert_ne!(title.raw(), author.raw());
}
#[test]
fn dynamic_field_changes_with_schema() {
let h = "title".to_blob().get_handle();
let short = Attribute::<ShortString>::from_fragment_unchecked(entity! {
metadata::name: h,
metadata::value_encoding: <ShortString as MetaDescribe>::id(),
});
let handle = Attribute::<Handle<UTF8String>>::from_fragment_unchecked(entity! {
metadata::name: h,
metadata::value_encoding: <Handle<UTF8String> as MetaDescribe>::id(),
});
assert_ne!(short.raw(), handle.raw());
}
#[test]
fn describe_preserves_identity_iri() {
let iri = "http://example.org/foo".to_string();
let iri_handle: Inline<Handle<UTF8String>> = iri.to_blob().get_handle();
let attr = Attribute::<ShortString>::from_fragment_unchecked(entity! {
metadata::iri: iri_handle,
metadata::value_encoding: <ShortString as crate::metadata::MetaDescribe>::id(),
});
let attr_id = attr.id();
let meta = attr.describe();
let hits: Vec<Id> = find!(
(a: Id),
pattern!(&meta, [{ ?a @ metadata::iri: iri_handle }])
)
.map(|(a,)| a)
.collect();
assert_eq!(hits, vec![attr_id]);
assert_eq!(meta.root(), Some(attr_id));
}
}