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}