Skip to main content

triblespace_core/
inline.rs

1//! `Inline<S>` (32-byte stored payload), `Encoded<V>` (the
2//! Inline-or-Blob sum that `entity!{}` builds), and the conversion
3//! traits between them. For a deeper look at portability goals,
4//! common formats, and schema design, refer to the "Portability &
5//! Common Formats" chapter in the project book.
6//!
7//! # Example
8//!
9//! ```
10//! use triblespace_core::inline::{Inline, InlineEncoding, IntoInline, TryFromInline};
11//! use triblespace_core::metadata::{self, MetaDescribe};
12//! use triblespace_core::trible::Fragment;
13//! use triblespace_core::id::{ExclusiveId, Id};
14//! use triblespace_core::macros::{id_hex, entity};
15//! use std::convert::{TryInto, Infallible};
16//!
17//! // Define a new schema type.
18//! // We're going to define an unsigned integer type that is stored as a little-endian 32-byte array.
19//! // Note that makes our example easier, as we don't have to worry about sign-extension or padding bytes.
20//! pub struct MyNumber;
21//!
22//! // The schema's identity hex lives inline in its describe body — that's
23//! // the only place it appears; callers reach the id via MyNumber::id().
24//! // `entity!{ &id @ ... }` returns a Fragment already rooted at `id` with
25//! // the listed facts; auto-puts any blob-source values into its local
26//! // store. The `metadata::tag` annotation lets schema registries discover
27//! // this entity as an inline encoding.
28//! impl MetaDescribe for MyNumber {
29//!    fn describe() -> Fragment {
30//!        let id: Id = id_hex!("345EAC0C5B5D7D034C87777280B88AE2");
31//!        entity! { ExclusiveId::force_ref(&id) @
32//!            metadata::name: "my_number",
33//!            metadata::tag:  metadata::KIND_INLINE_ENCODING,
34//!        }
35//!    }
36//! }
37//! impl InlineEncoding for MyNumber {
38//!    type ValidationError = ();
39//!    type Encoding = Self;
40//!    // Every bit pattern is valid for this schema.
41//! }
42//!
43//! // Implement conversion functions for the schema type.
44//! // Use `Error = Infallible` when the conversion cannot fail.
45//! impl TryFromInline<'_, MyNumber> for u32 {
46//!    type Error = Infallible;
47//!    fn try_from_inline(v: &Inline<MyNumber>) -> Result<Self, Infallible> {
48//!      Ok(u32::from_le_bytes(v.raw[0..4].try_into().unwrap()))
49//!    }
50//! }
51//!
52//! impl triblespace_core::inline::IntoEncoded<MyNumber> for u32 {
53//!   type Output = Inline<MyNumber>;
54//!   fn into_encoded(self) -> Inline<MyNumber> {
55//!      // Convert the Rust type to the schema type, i.e. a 32-byte array.
56//!      let mut bytes = [0; 32];
57//!      bytes[0..4].copy_from_slice(&self.to_le_bytes());
58//!      Inline::new(bytes)
59//!   }
60//! }
61//!
62//! // Use the schema type to store and retrieve a Rust type.
63//! let value: Inline<MyNumber> = MyNumber::inline_from(42u32);
64//! let i: u32 = value.from_inline();
65//! assert_eq!(i, 42);
66//!
67//! // You can also implement conversion functions for other Rust types.
68//! impl TryFromInline<'_, MyNumber> for u64 {
69//!   type Error = Infallible;
70//!   fn try_from_inline(v: &Inline<MyNumber>) -> Result<Self, Infallible> {
71//!    Ok(u64::from_le_bytes(v.raw[0..8].try_into().unwrap()))
72//!   }
73//! }
74//!
75//! impl triblespace_core::inline::IntoEncoded<MyNumber> for u64 {
76//!  type Output = Inline<MyNumber>;
77//!  fn into_encoded(self) -> Inline<MyNumber> {
78//!   let mut bytes = [0; 32];
79//!   bytes[0..8].copy_from_slice(&self.to_le_bytes());
80//!   Inline::new(bytes)
81//!   }
82//! }
83//!
84//! let value: Inline<MyNumber> = MyNumber::inline_from(42u64);
85//! let i: u64 = value.from_inline();
86//! assert_eq!(i, 42);
87//!
88//! // And use a value round-trip to convert between Rust types.
89//! let value: Inline<MyNumber> = MyNumber::inline_from(42u32);
90//! let i: u64 = value.from_inline();
91//! assert_eq!(i, 42);
92//! ```
93
94/// Built-in inline encoding types and their conversion implementations.
95pub mod encodings;
96
97use crate::metadata::MetaDescribe;
98
99use core::fmt;
100use std::borrow::Borrow;
101use std::cmp::Ordering;
102use std::fmt::Debug;
103use std::hash::Hash;
104use std::marker::PhantomData;
105
106use hex::ToHex;
107use zerocopy::Immutable;
108use zerocopy::IntoBytes;
109use zerocopy::KnownLayout;
110use zerocopy::TryFromBytes;
111use zerocopy::Unaligned;
112
113/// The length of a value in bytes.
114pub const INLINE_LEN: usize = 32;
115
116/// A raw value is simply a 32-byte array.
117pub type RawInline = [u8; INLINE_LEN];
118
119/// A value is a 32-byte array that can be (de)serialized as a Rust type.
120/// The schema type parameter is an abstract type that represents the meaning
121/// and valid bit patterns of the bytes.
122///
123/// # Example
124///
125/// ```
126/// use triblespace_core::prelude::*;
127/// use inlineencodings::R256;
128/// use num_rational::Ratio;
129///
130/// let ratio = Ratio::new(1, 2);
131/// let value: Inline<R256> = R256::inline_from(ratio);
132/// let ratio2: Ratio<i128> = value.try_from_inline().unwrap();
133/// assert_eq!(ratio, ratio2);
134/// ```
135#[derive(TryFromBytes, IntoBytes, Unaligned, Immutable, KnownLayout)]
136#[repr(transparent)]
137pub struct Inline<T: InlineEncoding> {
138    /// The 32-byte representation of this value.
139    pub raw: RawInline,
140    _schema: PhantomData<T>,
141}
142
143impl<S: InlineEncoding> Inline<S> {
144    /// Create a new value from a 32-byte array.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use triblespace_core::inline::{Inline, InlineEncoding};
150    /// use triblespace_core::inline::encodings::UnknownInline;
151    ///
152    /// let bytes = [0; 32];
153    /// let value = Inline::<UnknownInline>::new(bytes);
154    /// ```
155    pub fn new(value: RawInline) -> Self {
156        Self {
157            raw: value,
158            _schema: PhantomData,
159        }
160    }
161
162    /// Validate this value using its schema.
163    pub fn validate(self) -> Result<Self, S::ValidationError> {
164        S::validate(self)
165    }
166
167    /// Check if this value conforms to its schema.
168    pub fn is_valid(&self) -> bool {
169        S::validate(*self).is_ok()
170    }
171
172    /// Transmute a value from one schema type to another.
173    /// This is a safe operation, as the bytes are not changed.
174    /// The schema type is only changed in the type system.
175    /// This is a zero-cost operation.
176    /// This is useful when you have a value with an abstract schema type,
177    /// but you know the concrete schema type.
178    pub fn transmute<O>(self) -> Inline<O>
179    where
180        O: InlineEncoding,
181    {
182        Inline::new(self.raw)
183    }
184
185    /// Transmute a value reference from one schema type to another.
186    /// This is a safe operation, as the bytes are not changed.
187    /// The schema type is only changed in the type system.
188    /// This is a zero-cost operation.
189    /// This is useful when you have a value reference with an abstract schema type,
190    /// but you know the concrete schema type.
191    pub fn as_transmute<O>(&self) -> &Inline<O>
192    where
193        O: InlineEncoding,
194    {
195        unsafe { std::mem::transmute(self) }
196    }
197
198    /// Transmute a raw value reference to a value reference.
199    ///
200    /// # Example
201    ///
202    /// ```
203    /// use triblespace_core::inline::{Inline, InlineEncoding};
204    /// use triblespace_core::inline::encodings::UnknownInline;
205    /// use std::borrow::Borrow;
206    ///
207    /// let bytes = [0; 32];
208    /// let value: Inline<UnknownInline> = Inline::new(bytes);
209    /// let value_ref: &Inline<UnknownInline> = &value;
210    /// let raw_value_ref: &[u8; 32] = value_ref.borrow();
211    /// let value_ref2: &Inline<UnknownInline> = Inline::as_transmute_raw(raw_value_ref);
212    /// assert_eq!(&value, value_ref2);
213    /// ```
214    pub fn as_transmute_raw(value: &RawInline) -> &Self {
215        unsafe { std::mem::transmute(value) }
216    }
217
218    /// Deserialize a value with an abstract schema type to a concrete Rust type.
219    ///
220    /// This method only works for infallible conversions (where `Error = Infallible`).
221    /// For fallible conversions, use the [Inline::try_from_inline] method.
222    ///
223    /// # Example
224    ///
225    /// ```
226    /// use triblespace_core::prelude::*;
227    /// use inlineencodings::F64;
228    ///
229    /// let value: Inline<F64> = (3.14f64).to_inline();
230    /// let concrete: f64 = value.from_inline();
231    /// ```
232    pub fn from_inline<'a, T>(&'a self) -> T
233    where
234        T: TryFromInline<'a, S, Error = std::convert::Infallible>,
235    {
236        match <T as TryFromInline<'a, S>>::try_from_inline(self) {
237            Ok(v) => v,
238            Err(e) => match e {},
239        }
240    }
241
242    /// Deserialize a value with an abstract schema type to a concrete Rust type.
243    ///
244    /// This method returns an error if the conversion is not possible.
245    /// This might happen if the bytes are not valid for the schema type or if the
246    /// rust type can't represent the specific value of the schema type,
247    /// e.g. if the schema type is a fractional number and the rust type is an integer.
248    ///
249    /// For infallible conversions, use the [Inline::from_inline] method.
250    ///
251    /// # Example
252    ///
253    /// ```
254    /// use triblespace_core::prelude::*;
255    /// use inlineencodings::R256;
256    /// use num_rational::Ratio;
257    ///
258    /// let value: Inline<R256> = R256::inline_from(Ratio::new(1, 2));
259    /// let concrete: Result<Ratio<i128>, _> = value.try_from_inline();
260    /// ```
261    ///
262    pub fn try_from_inline<'a, T>(&'a self) -> Result<T, <T as TryFromInline<'a, S>>::Error>
263    where
264        T: TryFromInline<'a, S>,
265    {
266        <T as TryFromInline<'a, S>>::try_from_inline(self)
267    }
268}
269
270impl<T: InlineEncoding> Copy for Inline<T> {}
271
272impl<T: InlineEncoding> Clone for Inline<T> {
273    fn clone(&self) -> Self {
274        *self
275    }
276}
277
278impl<T: InlineEncoding> PartialEq for Inline<T> {
279    fn eq(&self, other: &Self) -> bool {
280        self.raw == other.raw
281    }
282}
283
284impl<T: InlineEncoding> Eq for Inline<T> {}
285
286impl<T: InlineEncoding> Hash for Inline<T> {
287    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
288        self.raw.hash(state);
289    }
290}
291
292impl<T: InlineEncoding> Ord for Inline<T> {
293    fn cmp(&self, other: &Self) -> Ordering {
294        self.raw.cmp(&other.raw)
295    }
296}
297
298impl<T: InlineEncoding> PartialOrd for Inline<T> {
299    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
300        Some(self.cmp(other))
301    }
302}
303
304impl<S: InlineEncoding> Borrow<RawInline> for Inline<S> {
305    fn borrow(&self) -> &RawInline {
306        &self.raw
307    }
308}
309
310impl<T: InlineEncoding> Debug for Inline<T> {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        write!(
313            f,
314            "Inline<{}>({})",
315            std::any::type_name::<T>(),
316            ToHex::encode_hex::<String>(&self.raw)
317        )
318    }
319}
320
321/// A trait that represents an abstract schema type that can be (de)serialized as a [Inline].
322///
323/// This trait is usually implemented on a type-level empty struct,
324/// but may contain additional information about the schema type as associated constants or types.
325/// The [Handle](crate::inline::encodings::hash::Handle) type for example contains type information about the hash algorithm,
326/// and the schema of the referenced blob.
327///
328/// See the [value](crate::value) module for more information.
329/// See the [BlobEncoding](crate::blob::BlobEncoding) trait for the counterpart trait for blobs.
330pub trait InlineEncoding: MetaDescribe + Sized + 'static {
331    /// The error type returned by [`validate`](InlineEncoding::validate).
332    /// Use `()` or [`Infallible`](std::convert::Infallible) when every bit pattern is valid.
333    type ValidationError;
334
335    /// The trait parameter to dispatch via for `entity!{}` field
336    /// conversion. For *inline* schemas (32-byte data lives in the
337    /// trible), set `Encoding = Self` — sources convert via
338    /// `IntoEncoded<Self> { Output = Inline<Self> }`. For
339    /// [`Handle<T>`](crate::inline::encodings::hash::Handle), set
340    /// `Encoding = T` — sources convert via `IntoEncoded<T> { Output =
341    /// Blob<T> }`. The BlobEncoding `T` sitting directly at trait
342    /// position 0 is what lets downstream impl `IntoEncoded<MyBlob>
343    /// for MyType` without bumping into the orphan rule.
344    type Encoding;
345
346    /// Check if the given value conforms to this schema.
347    fn validate(value: Inline<Self>) -> Result<Inline<Self>, Self::ValidationError> {
348        Ok(value)
349    }
350
351    /// Create a new value from a concrete Rust type via [`IntoInline`].
352    /// Panics if the underlying conversion panics.
353    fn inline_from<T: IntoInline<Self>>(t: T) -> Inline<Self> {
354        t.to_inline()
355    }
356
357    /// Create a new value from a concrete Rust type via [`TryToInline`].
358    /// Returns an error if the conversion fails.
359    fn inline_try_from<T: TryToInline<Self>>(
360        t: T,
361    ) -> Result<Inline<Self>, <T as TryToInline<Self>>::Error> {
362        t.try_to_inline()
363    }
364
365    /// Lift an already-encoded `Inline<Self>` into the [`Encoded`] sum
366    /// `entity!{}` consumes — yields `Encoded::Inline(form)`, no
367    /// side-blob.
368    ///
369    /// Overridable if a schema has unusual storage semantics. The
370    /// blob-path counterpart lives on
371    /// [`BlobEncoding::to_encoded`](crate::blob::BlobEncoding::to_encoded).
372    fn to_encoded(form: Inline<Self>) -> Encoded<Self> {
373        Encoded::Inline(form)
374    }
375}
376
377/// Fallible variant of value conversion — `T → Result<Inline<S>, Error>`.
378///
379/// Kept as a standalone trait (not folded into [`IntoEncoded`])
380/// because the error type is part of the per-source/per-target contract.
381/// Used for parses that can fail (e.g. `&str → Hash<Blake3>` via
382/// hex-decoding).
383pub trait TryToInline<S: InlineEncoding> {
384    /// The error type returned when the conversion fails.
385    type Error;
386    /// Convert the Rust type to a [Inline] with a specific schema type.
387    fn try_to_inline(self) -> Result<Inline<S>, Self::Error>;
388}
389
390/// User-implemented schema-side encoding trait, in the `From`
391/// direction: **the schema is the impl target**, the source is the
392/// trait parameter.
393///
394/// ```ignore
395/// impl Encodes<&str> for LongString {
396///     type Output = Blob<LongString>;
397///     fn encode(s: &str) -> Blob<LongString> { Blob::new(s.into()) }
398/// }
399/// ```
400///
401/// This is the canonical orphan-rule shape (mirroring `From<T>` in
402/// std): downstream that defines a local `MyBlobEncoding` writes
403/// `impl Encodes<ForeignType> for MyBlobEncoding` — the local encoding
404/// sits at the impl-target position so Rust's orphan checker
405/// trivially accepts the impl, no matter how foreign the source
406/// type is.
407///
408/// The user-facing source-side ergonomic — `source.into_encoded()` /
409/// `source.to_inline()` / `source.to_blob()` — is blanket-derived
410/// from this trait via [`IntoEncoded`].
411pub trait Encodes<Source> {
412    /// The concrete form this source produces when encoded for this
413    /// schema. `Inline<Self>` for inline encodings, `Blob<Self>` for
414    /// blob encodings, or `Inline<Handle<Self>>` for the
415    /// precomputed-handle case where `Self: BlobEncoding`.
416    type Output;
417    /// Run the encoding.
418    fn encode(source: Source) -> Self::Output;
419}
420
421/// Source-side ergonomic counterpart of [`Encodes`], in the `Into`
422/// direction: methods like `42u32.to_inline()` resolve here.
423///
424/// Blanket-derived from every `Encodes` impl — users never implement
425/// `IntoEncoded` directly. The split mirrors std's `From`/`Into`:
426/// implement `From`, get `Into` for free.
427pub trait IntoEncoded<S> {
428    /// The concrete form this source produces.
429    type Output;
430    /// Run the conversion.
431    fn into_encoded(self) -> Self::Output;
432}
433
434impl<S, T> IntoEncoded<S> for T
435where
436    S: Encodes<T>,
437{
438    type Output = <S as Encodes<T>>::Output;
439    fn into_encoded(self) -> Self::Output {
440        <S as Encodes<T>>::encode(self)
441    }
442}
443
444/// Shorthand bound for `IntoEncoded<S, Output = Inline<S>>` — "this
445/// source produces a directly-encoded `Inline<S>`, no side-blob."
446///
447/// `IntoInline` is a supertrait alias over [`IntoEncoded`]: any type
448/// that implements `IntoEncoded<S>` with `Output = Inline<S>`
449/// automatically becomes `IntoInline<S>`, and gains the
450/// `to_inline(self) -> Inline<S>` convenience method.
451pub trait IntoInline<S: InlineEncoding>: IntoEncoded<S, Output = Inline<S>> {
452    /// Convert directly to `Inline<S>`.
453    fn to_inline(self) -> Inline<S>
454    where
455        Self: Sized,
456    {
457        self.into_encoded()
458    }
459}
460impl<S, T> IntoInline<S> for T
461where
462    S: InlineEncoding,
463    T: IntoEncoded<S, Output = Inline<S>>,
464{
465}
466
467/// The two-shape sum an attribute's value can take when an
468/// `entity!{}` field is encoded: either a 32-byte [`Inline<V>`]
469/// payload that lives directly in the trible, or a [`Blob`] holding
470/// the heavy content with a derivable handle.
471///
472/// Replaces the older `(Inline<V>, Option<Blob>)` pair that carried
473/// an implicit "Option is Some iff V is a Handle schema" invariant.
474/// Encoding the split as a sum makes the invariant structural — a
475/// `Encoded::Inline` never has a stored blob; a `Encoded::Blob` always
476/// does — and drops the redundant handle that used to be carried
477/// alongside its own blob.
478#[derive(Debug, Clone)]
479pub enum Encoded<V: InlineEncoding> {
480    /// 32-byte payload stored directly in the trible.
481    Inline(Inline<V>),
482    /// Bytes resolvable via a content-addressed handle. The handle
483    /// is `blob.get_handle().transmute::<V>()` — the same 32 bytes,
484    /// just re-phantomed back to the attribute's schema.
485    Blob(crate::blob::Blob<crate::blob::encodings::UnknownBlob>),
486}
487
488impl<V: InlineEncoding> Encoded<V> {
489    /// The 32-byte form that goes into the trible. For
490    /// [`Encoded::Blob`], this rederives the handle from the cached
491    /// hash in the blob (no rehash) and recasts the phantom.
492    pub fn inline(&self) -> Inline<V> {
493        match self {
494            Encoded::Inline(i) => *i,
495            Encoded::Blob(b) => b.get_handle().transmute(),
496        }
497    }
498
499    /// Yield the inline form alongside the side-blob (if any). This
500    /// is the macro consumer's destructuring entry point — it gets
501    /// both pieces in one call without losing the structural
502    /// guarantee from [`Encoded`].
503    pub fn into_parts(
504        self,
505    ) -> (
506        Inline<V>,
507        Option<crate::blob::Blob<crate::blob::encodings::UnknownBlob>>,
508    ) {
509        match self {
510            Encoded::Inline(i) => (i, None),
511            Encoded::Blob(b) => {
512                let h = b.get_handle().transmute();
513                (h, Some(b))
514            }
515        }
516    }
517}
518
519/// Lift an [`IntoEncoded::Output`] into the [`Encoded`] sum the
520/// `entity!{}` macro folds into a Fragment.
521///
522/// `V` is the *attribute's* inline encoding. Two impls cover everything:
523/// - `Inline<V>` delegates to [`InlineEncoding::to_encoded`] — inline
524///   path, yields `Encoded::Inline(form)`.
525/// - `Blob<T>` targeting `Handle<T>` delegates to
526///   [`BlobEncoding::to_encoded`](crate::blob::BlobEncoding::to_encoded) —
527///   handle path, yields `Encoded::Blob(form.transmute())`.
528///
529/// This trait is the **dispatch shim** for the macro layer; the
530/// actual logic lives on the schema traits so users (and overriding
531/// schemas) can call it directly without going through the trait.
532/// `to_encoded` matches the `to_inline`/`to_blob` style of the
533/// supertrait aliases.
534pub trait ToEncoded<V: InlineEncoding> {
535    /// Produce the [`Encoded`] the macro absorbs.
536    fn to_encoded(self) -> Encoded<V>;
537}
538
539impl<V: InlineEncoding> ToEncoded<V> for Inline<V> {
540    fn to_encoded(self) -> Encoded<V> {
541        <V as InlineEncoding>::to_encoded(self)
542    }
543}
544
545/// A trait for converting a [Inline] with a specific schema type to a Rust type.
546/// This trait is implemented on the concrete Rust type.
547///
548/// Values are 32-byte arrays that represent data at a deserialization boundary.
549/// Conversions may fail depending on the schema and target type. Use
550/// `Error = Infallible` for conversions that genuinely cannot fail (e.g.
551/// `ethnum::U256` from `U256BE`), and a real error type for narrowing
552/// conversions (e.g. `u64` from `U256BE`).
553///
554/// This is the counterpart to the [TryToInline] trait.
555///
556/// See [TryFromBlob](crate::blob::TryFromBlob) for the counterpart trait for blobs.
557pub trait TryFromInline<'a, S: InlineEncoding>: Sized {
558    /// The error type returned when the conversion fails.
559    type Error;
560    /// Convert the [Inline] with a specific schema type to the Rust type.
561    fn try_from_inline(v: &'a Inline<S>) -> Result<Self, Self::Error>;
562}
563
564impl<S: InlineEncoding> Encodes<Inline<S>> for S
565{
566    type Output = Inline<S>;
567    fn encode(source: Inline<S>) -> Inline<S> {
568        source
569    }
570}
571
572impl<S: InlineEncoding> Encodes<&Inline<S>> for S
573{
574    type Output = Inline<S>;
575    fn encode(source: &Inline<S>) -> Inline<S> {
576        *source
577    }
578}
579
580impl<'a, S: InlineEncoding> TryFromInline<'a, S> for Inline<S> {
581    type Error = std::convert::Infallible;
582    fn try_from_inline(v: &'a Inline<S>) -> Result<Self, std::convert::Infallible> {
583        Ok(*v)
584    }
585}
586
587impl<'a, S: InlineEncoding> TryFromInline<'a, S> for () {
588    type Error = std::convert::Infallible;
589    fn try_from_inline(_v: &'a Inline<S>) -> Result<Self, std::convert::Infallible> {
590        Ok(())
591    }
592}