Skip to main content

forgedb_types/
lib.rs

1//! ForgeDB Types
2//!
3//! Core type definitions for ForgeDB schemas and generated code.
4//!
5//! # Overview
6//!
7//! This crate provides type definitions that match ForgeDB's schema language types,
8//! enabling type-safe serialization, validation, and storage operations. It is a
9//! foundational crate used by generated code and other ForgeDB runtime libraries.
10//!
11//! # Architecture
12//!
13//! The crate is designed around two key concepts:
14//!
15//! - **Primitive Types**: Direct mappings of ForgeDB schema types to Rust types
16//! - **Generic Value Enum**: Runtime type information for heterogeneous data
17//!
18//! All types are designed for zero or minimal overhead with `#[repr(transparent)]`
19//! for wrapper types and efficient serialization using Serde derive macros.
20//!
21//! # Supported Types
22//!
23//! | ForgeDB Type | Rust Type | Description |
24//! |--------------|-----------|-------------|
25//! | `u32` | `u32` | 32-bit unsigned integer |
26//! | `u64` | `u64` | 64-bit unsigned integer |
27//! | `i32` | `i32` | 32-bit signed integer |
28//! | `i64` | `i64` | 64-bit signed integer |
29//! | `f64` | `f64` | 64-bit floating point |
30//! | `bool` | `bool` | Boolean value |
31//! | `string` | `String` | UTF-8 encoded text |
32//! | `string(N)` / `string(N!)` | `String`, or [`InlineStr<N>`](InlineStr) as a **key** | Fixed-slot inline text (#238); a `Copy` key type when the field is an identity or a foreign key to one (#252) |
33//! | `uuid` | [`Uuid`] | Universally unique identifier |
34//! | `timestamp` | [`Timestamp`] | Instant, as **microseconds** since the Unix epoch (#254) |
35//!
36//! # Examples
37//!
38//! ## Basic Usage
39//!
40//! ```rust
41//! use forgedb_types::{Value, Timestamp, Uuid};
42//!
43//! // Create a timestamp from the current time
44//! let ts = Timestamp::now();
45//! println!("Current timestamp: {}", ts);   // RFC 3339
46//!
47//! // Work with generic values
48//! let values = vec![
49//!     Value::I32(42),
50//!     Value::String("hello".to_string()),
51//!     Value::Uuid(Uuid::new_v4()),
52//! ];
53//!
54//! // Serialize to JSON
55//! let json = serde_json::to_string(&values[0]).unwrap();
56//! ```
57//!
58//! ## Type Conversions
59//!
60//! ```rust
61//! use forgedb_types::Value;
62//!
63//! // Convenient From implementations
64//! let val: Value = 42_i32.into();
65//! let val: Value = "hello".into();
66//!
67//! // Type checking
68//! if val.is_numeric() {
69//!     println!("This is a numeric value");
70//! }
71//! ```
72//!
73//! # Public API
74//!
75//! ## Core Types
76//!
77//! - [`Timestamp`] - Wrapper around `i64` microseconds since the Unix epoch
78//! - [`InlineStr`] - A `Copy`, fixed-capacity string; the backing type of a
79//!   `string(N)` identity (#252), and of any foreign key pointing at one (#266)
80//! - [`InlineStrError`] - Why a `&str` did not fit an [`InlineStr`]
81//! - [`Value`] - Enum that can hold any ForgeDB primitive type
82//! - [`Uuid`] - Re-exported from the `uuid` crate
83//!
84//! ## Key Methods
85//!
86//! - `Timestamp::now()` - Get current timestamp
87//! - `Timestamp::from_micros(i64)` / `as_micros()` - The canonical unit
88//! - `Timestamp::to_rfc3339()` / `from_rfc3339(&str)` - The canonical wire form
89//! - `InlineStr::try_from(&str)` / `as_str()` - The key type's bound and its text
90//! - `Value::type_name()` - Get type name string
91//! - `Value::is_numeric()` - Check if numeric type
92//!
93//! # Related Crates
94//!
95//! - [`forgedb-storage`](../forgedb_storage) - Uses these types for columnar storage
96//! - [`forgedb-parser`](../forgedb_parser) - Parses schemas into these types
97//!
98//! # See Also
99//!
100//! - [README](./README.md) for detailed documentation and usage examples
101//! - [uuid crate documentation](https://docs.rs/uuid) for UUID operations
102
103use serde::{Deserialize, Serialize};
104use std::time::{SystemTime, UNIX_EPOCH};
105
106/// Re-export uuid for convenience
107pub use uuid::Uuid;
108
109/// Microseconds elapsed since the Unix epoch (January 1, 1970 00:00:00 UTC).
110///
111/// # The unit is microseconds (#254)
112///
113/// Before v0.4.0 this held whole **seconds**. It now holds **microseconds**, and
114/// microseconds are the one canonical on-disk unit regardless of a field's
115/// declared `timestamp(s|ms|us)` precision — a `timestamp(ms)` field simply
116/// stores millisecond-aligned microsecond values. Storing each field in its own
117/// unit would make a `Timestamp` value unit-ambiguous at runtime, which is only
118/// resolvable with a fatter value (a layout change) or a `Timestamp<const U>`
119/// (generated signature churn everywhere).
120///
121/// `i64` microseconds spans ±292,277 years, which is why microseconds rather
122/// than nanoseconds are the floor: nanoseconds would cap the type at 1678–2262,
123/// putting a birth date or a long-dated bond maturity out of range.
124///
125/// # The wire form is RFC 3339, not a number
126///
127/// [`Display`], [`FromStr`](std::str::FromStr) and serde all speak RFC 3339 —
128/// one rendering everywhere, so the create handler's `id.to_string()` and a REST
129/// path parameter's `parse()` agree with no generated codec. Output is fixed
130/// (**exactly 6 fractional digits, always `Z`**, including for a `timestamp(s)`
131/// field: `Display` is on the type and cannot see a field's precision); input is
132/// lenient (fewer or more fractional digits, lowercase `t`/`z`, any numeric
133/// offset).
134///
135/// A number would have been a *silent* break — a client reading seconds would
136/// start reading microseconds and display 1970 forever. A client expecting a
137/// number and receiving a string fails immediately and visibly.
138///
139/// # Range
140///
141/// The rendering has no failure mode, so the **type's** range is RFC 3339's year
142/// range 0000–9999 ([`is_rfc3339_representable`](Timestamp::is_rfc3339_representable)),
143/// enforced by the generated write path rather than here. Values outside it
144/// still render and re-parse losslessly (with an extended year), so no data is
145/// trapped; they are simply refused at the boundary.
146///
147/// # Examples
148///
149/// ```rust
150/// use forgedb_types::Timestamp;
151///
152/// let ts = Timestamp::from_micros(1_775_000_000_123_456);
153/// assert_eq!(ts.to_rfc3339(), "2026-03-31T23:33:20.123456Z");
154/// assert_eq!(ts.as_micros(), 1_775_000_000_123_456);
155/// assert_eq!("2026-03-31T23:33:20.123456Z".parse::<Timestamp>().unwrap(), ts);
156/// ```
157#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
158pub struct Timestamp(i64);
159
160/// Microseconds in one day — the quantum the civil-date split works in.
161const US_PER_DAY: i64 = 86_400_000_000;
162/// Microseconds in one second.
163const US_PER_SEC: i64 = 1_000_000;
164
165/// `0000-01-01T00:00:00Z`, the first instant RFC 3339 can render.
166const RFC3339_MIN_US: i64 = -62_167_219_200_000_000;
167/// `9999-12-31T23:59:59.999999Z`, the last one.
168const RFC3339_MAX_US: i64 = 253_402_300_799_999_999;
169
170impl Timestamp {
171    /// Creates a timestamp from microseconds since the Unix epoch.
172    ///
173    /// ```rust
174    /// use forgedb_types::Timestamp;
175    /// assert_eq!(Timestamp::from_micros(1_234_567_890).as_micros(), 1_234_567_890);
176    /// ```
177    #[must_use]
178    pub fn from_micros(micros: i64) -> Self {
179        Timestamp(micros)
180    }
181
182    /// Returns the timestamp as microseconds since the Unix epoch.
183    #[must_use]
184    pub fn as_micros(&self) -> i64 {
185        self.0
186    }
187
188    /// Returns the current timestamp, in microseconds.
189    ///
190    /// ```rust
191    /// use forgedb_types::Timestamp;
192    /// assert!(Timestamp::now().as_micros() > 1_577_836_800_000_000);
193    /// ```
194    #[must_use]
195    pub fn now() -> Self {
196        // Saturate rather than panic if the system clock is set before the Unix
197        // epoch: a library constructor should not crash on a misconfigured clock.
198        let micros = SystemTime::now()
199            .duration_since(UNIX_EPOCH)
200            .map(|d| i64::try_from(d.as_micros()).unwrap_or(i64::MAX))
201            .unwrap_or(0);
202        Timestamp(micros)
203    }
204
205    /// Renders the instant as RFC 3339: exactly 6 fractional digits, always `Z`.
206    ///
207    /// Total by construction — this is what [`Display`] calls, and `Display`
208    /// cannot fail. A value outside RFC 3339's year range renders with an
209    /// extended year (`+10000-…` / `-0001-…`) so the round-trip stays lossless;
210    /// such a value is refused at the write boundary instead, via
211    /// [`is_rfc3339_representable`](Self::is_rfc3339_representable).
212    #[must_use]
213    pub fn to_rfc3339(&self) -> String {
214        // Floor division, not truncation: for a pre-epoch value the day index
215        // must round *down* and the remainder stay non-negative, or the clock
216        // fields come out negative.
217        let days = self.0.div_euclid(US_PER_DAY);
218        let rem = self.0.rem_euclid(US_PER_DAY);
219        let (y, m, d) = civil_from_days(days);
220        let (h, min, s, us) = (
221            rem / 3_600_000_000,
222            (rem / 60_000_000) % 60,
223            (rem / US_PER_SEC) % 60,
224            rem % US_PER_SEC,
225        );
226        let year = if (0..=9999).contains(&y) {
227            format!("{y:04}")
228        } else if y < 0 {
229            format!("-{:04}", -y)
230        } else {
231            format!("+{y}")
232        };
233        format!("{year}-{m:02}-{d:02}T{h:02}:{min:02}:{s:02}.{us:06}Z")
234    }
235
236    /// Parses an RFC 3339 instant. Lenient about everything the rendering fixes:
237    /// any number of fractional digits (sub-microsecond digits are **truncated**,
238    /// since the stored unit is the finest thing the value can mean), a lowercase
239    /// `t`/`z` separator, and any numeric offset (`±HH:MM` / `±HHMM` / `±HH`).
240    ///
241    /// An offset is applied, not recorded: the stored value is an instant.
242    pub fn from_rfc3339(s: &str) -> std::result::Result<Self, TimestampParseError> {
243        parse_rfc3339(s).map(Timestamp).ok_or(TimestampParseError)
244    }
245
246    /// Floors the value to a multiple of `quantum_us` (res 5): a user-supplied
247    /// value finer than a field's declared precision is *recorded* coarsely
248    /// rather than refused, because rejecting a valid measurement for being too
249    /// precise only exports the flooring to every client.
250    ///
251    /// **Floor, never truncate toward zero.** Truncation rounds a pre-epoch value
252    /// *forward* in time (`-0.5s` → `0s`), which breaks monotonicity across 1970
253    /// — two instants an hour apart could quantize to the same value from
254    /// opposite sides, or swap order.
255    ///
256    /// A `quantum_us` of `0` or less is the identity (there is nothing to floor
257    /// to), so this never divides by zero on a caller's behalf.
258    #[must_use]
259    pub fn floor_to_micros(&self, quantum_us: i64) -> Self {
260        if quantum_us <= 1 {
261            return *self;
262        }
263        Timestamp(self.0.div_euclid(quantum_us) * quantum_us)
264    }
265
266    /// Whether the value renders inside RFC 3339's year range (0000–9999).
267    ///
268    /// `i64` microseconds spans ±292,277 years, so the wire form is narrower than
269    /// the storage type. Since [`Display`] cannot fail, the narrowing is enforced
270    /// as a validity constraint at the write boundary (a 422) rather than as a
271    /// rendering error — and this predicate is what the generated write path
272    /// checks.
273    #[must_use]
274    pub fn is_rfc3339_representable(&self) -> bool {
275        (RFC3339_MIN_US..=RFC3339_MAX_US).contains(&self.0)
276    }
277}
278
279/// Why an RFC 3339 string could not be read as a [`Timestamp`].
280///
281/// Deliberately opaque: this reaches users through a 400/422 on a REST path
282/// segment or query parameter, where the useful information is "that is not an
283/// RFC 3339 instant", not which of fourteen sub-fields was malformed.
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub struct TimestampParseError;
286
287impl std::fmt::Display for TimestampParseError {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        f.write_str("not an RFC 3339 timestamp (expected e.g. 2026-03-31T23:33:20.123456Z)")
290    }
291}
292
293impl std::error::Error for TimestampParseError {}
294
295impl std::fmt::Display for Timestamp {
296    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297        f.write_str(&self.to_rfc3339())
298    }
299}
300
301impl std::str::FromStr for Timestamp {
302    type Err = TimestampParseError;
303    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
304        Timestamp::from_rfc3339(s)
305    }
306}
307
308impl Serialize for Timestamp {
309    fn serialize<S: serde::Serializer>(&self, ser: S) -> std::result::Result<S::Ok, S::Error> {
310        ser.serialize_str(&self.to_rfc3339())
311    }
312}
313
314impl<'de> Deserialize<'de> for Timestamp {
315    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
316        let s = <std::borrow::Cow<'de, str>>::deserialize(de)?;
317        Timestamp::from_rfc3339(&s).map_err(serde::de::Error::custom)
318    }
319}
320
321// ---- RFC 3339 <-> micros, hand-rolled ---------------------------------------
322//
323// `forgedb-types` is linked by every generated project and must build for
324// `wasm32-unknown-unknown`, so its dependency list is deliberately two crates
325// long. Pulling `chrono` in for two functions would add a large transitive
326// surface and a wasm feature-flag question for no gain: the civil-date
327// conversion below is Howard Hinnant's, exact over the whole `i64` micros range,
328// and about sixty lines.
329
330/// Days since 1970-01-01 for a proleptic-Gregorian civil date.
331fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
332    let y = if m <= 2 { y - 1 } else { y };
333    let era = (if y >= 0 { y } else { y - 399 }) / 400;
334    let yoe = y - era * 400; // [0, 399]
335    let mp = if m > 2 { m - 3 } else { m + 9 }; // [0, 11]
336    let doy = (153 * mp + 2) / 5 + d - 1; // [0, 365]
337    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
338    era * 146097 + doe - 719468
339}
340
341/// The proleptic-Gregorian civil date for a day index since 1970-01-01.
342fn civil_from_days(z: i64) -> (i64, i64, i64) {
343    let z = z + 719468;
344    let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
345    let doe = z - era * 146097; // [0, 146096]
346    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
347    let y = yoe + era * 400;
348    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
349    let mp = (5 * doy + 2) / 153; // [0, 11]
350    let d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
351    let m = if mp < 10 { mp + 3 } else { mp - 9 }; // [1, 12]
352    (if m <= 2 { y + 1 } else { y }, m, d)
353}
354
355/// Days in `m` of year `y`, for the calendar-validity check. A parse that
356/// skipped this would silently accept `2026-02-30` and normalize it to March 2 —
357/// a wrong date accepted quietly, which is the failure class this issue is about.
358fn days_in_month(y: i64, m: i64) -> i64 {
359    match m {
360        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
361        4 | 6 | 9 | 11 => 30,
362        2 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 => 29,
363        2 => 28,
364        _ => 0,
365    }
366}
367
368/// `Some(micros)` for a well-formed RFC 3339 instant, `None` otherwise. Returns
369/// an `Option` rather than panicking anywhere: every caller is a REST boundary.
370fn parse_rfc3339(s: &str) -> Option<i64> {
371    let b = s.as_bytes();
372    let mut i = 0usize;
373
374    // Year: an optional sign then at least four digits (exactly four unless the
375    // year is signed — the extended form only ever comes back from our own
376    // rendering of an out-of-range value).
377    let neg_year = match b.first() {
378        Some(b'-') => {
379            i = 1;
380            true
381        }
382        Some(b'+') => {
383            i = 1;
384            false
385        }
386        _ => false,
387    };
388    let year_start = i;
389    while i < b.len() && b[i].is_ascii_digit() {
390        i += 1;
391    }
392    let year_digits = i - year_start;
393    if year_digits < 4 || (year_digits > 4 && year_start == 0) {
394        return None;
395    }
396    let year: i64 = s[year_start..i].parse().ok()?;
397    let year = if neg_year { -year } else { year };
398
399    let month = take_fixed(b, &mut i, b'-', 2)?;
400    let day = take_fixed(b, &mut i, b'-', 2)?;
401
402    // Date/time separator: `T`, `t`, or a space (RFC 3339 §5.6's NOTE).
403    if i >= b.len() || !matches!(b[i], b'T' | b't' | b' ') {
404        return None;
405    }
406    i += 1;
407
408    let hour = take_digits(b, &mut i, 2)?;
409    let minute = take_fixed(b, &mut i, b':', 2)?;
410    let second = take_fixed(b, &mut i, b':', 2)?;
411
412    // Calendar and clock validity. A leap second (`:60`) is refused rather than
413    // clamped: there is no microsecond value for it, and silently mapping it to
414    // `:59.999999` would move the instant.
415    if !(1..=12).contains(&month)
416        || day < 1
417        || day > days_in_month(year, month)
418        || hour > 23
419        || minute > 59
420        || second > 59
421    {
422        return None;
423    }
424
425    // Fraction: at least one digit if the `.` is present. Digits past the sixth
426    // are truncated (the stored unit is the finest meaning available).
427    let mut micros_of_second = 0i64;
428    if i < b.len() && b[i] == b'.' {
429        i += 1;
430        let start = i;
431        while i < b.len() && b[i].is_ascii_digit() {
432            i += 1;
433        }
434        if i == start {
435            return None;
436        }
437        let digits = &s[start..(start + 6).min(i)];
438        micros_of_second = digits.parse::<i64>().ok()? * 10i64.pow(6 - digits.len() as u32);
439    }
440
441    // Offset: `Z`/`z`, or a signed `HH`, `HHMM`, `HH:MM`. Required — a bare local
442    // time is not an instant, so it is not a Timestamp.
443    let offset_seconds = match b.get(i) {
444        Some(b'Z') | Some(b'z') => {
445            i += 1;
446            0
447        }
448        Some(c @ (b'+' | b'-')) => {
449            let sign = if *c == b'-' { -1 } else { 1 };
450            i += 1;
451            let oh = take_digits(b, &mut i, 2)?;
452            let om = if i < b.len() {
453                if b[i] == b':' {
454                    i += 1;
455                }
456                if i < b.len() && b[i].is_ascii_digit() {
457                    take_digits(b, &mut i, 2)?
458                } else {
459                    return None;
460                }
461            } else {
462                0
463            };
464            if oh > 23 || om > 59 {
465                return None;
466            }
467            sign * (oh * 3600 + om * 60)
468        }
469        _ => return None,
470    };
471    // Trailing anything (including a space) is a malformed instant, not a
472    // forgivable one — a lenient tail is how `"2026-…Z garbage"` gets accepted.
473    if i != b.len() {
474        return None;
475    }
476
477    let days = days_from_civil(year, month, day);
478    let secs_of_day = hour * 3600 + minute * 60 + second - offset_seconds;
479    days.checked_mul(US_PER_DAY)?
480        .checked_add(secs_of_day.checked_mul(US_PER_SEC)?)?
481        .checked_add(micros_of_second)
482}
483
484/// Consume exactly `n` ASCII digits at `i`, advancing it.
485fn take_digits(b: &[u8], i: &mut usize, n: usize) -> Option<i64> {
486    if *i + n > b.len() {
487        return None;
488    }
489    let mut v: i64 = 0;
490    for k in 0..n {
491        let c = b[*i + k];
492        if !c.is_ascii_digit() {
493            return None;
494        }
495        v = v * 10 + i64::from(c - b'0');
496    }
497    *i += n;
498    Some(v)
499}
500
501/// Consume a fixed separator then exactly `n` digits.
502fn take_fixed(b: &[u8], i: &mut usize, sep: u8, n: usize) -> Option<i64> {
503    if b.get(*i) != Some(&sep) {
504        return None;
505    }
506    *i += 1;
507    take_digits(b, i, n)
508}
509
510impl From<i64> for Timestamp {
511    /// Microseconds since the epoch, matching [`Timestamp::from_micros`].
512    fn from(micros: i64) -> Self {
513        Timestamp(micros)
514    }
515}
516
517impl From<Timestamp> for i64 {
518    fn from(ts: Timestamp) -> Self {
519        ts.0
520    }
521}
522
523/// A generic value type that can hold any ForgeDB primitive type
524///
525/// This enum represents all primitive types supported by ForgeDB schemas,
526/// providing a type-safe way to work with heterogeneous data.
527///
528/// # Examples
529///
530/// ```rust
531/// use forgedb_types::{Value, Uuid};
532///
533/// let int_val = Value::I32(42);
534/// let uint_val = Value::U64(1_000_000_000_u64);
535/// let str_val = Value::String("hello".to_string());
536/// let uuid_val = Value::Uuid(Uuid::new_v4());
537///
538/// // Serialize to JSON
539/// let json = serde_json::to_string(&int_val).unwrap();
540/// ```
541#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
542#[serde(tag = "type", content = "value")]
543pub enum Value {
544    /// 32-bit unsigned integer
545    U32(u32),
546    /// 64-bit unsigned integer — stored losslessly; `u64` values above `i64::MAX`
547    /// cannot be represented by the signed `I64` variant without truncation
548    U64(u64),
549    /// 32-bit signed integer
550    I32(i32),
551    /// 64-bit signed integer
552    I64(i64),
553    /// 64-bit floating point number
554    F64(f64),
555    /// Boolean value
556    Bool(bool),
557    /// UTF-8 encoded string
558    String(String),
559    /// Universally unique identifier
560    Uuid(Uuid),
561    /// An instant (microseconds since the Unix epoch); JSON form is RFC 3339
562    Timestamp(Timestamp),
563}
564
565impl Value {
566    /// Returns the type name of this value
567    ///
568    /// # Examples
569    ///
570    /// ```rust
571    /// use forgedb_types::Value;
572    ///
573    /// let val = Value::I32(42);
574    /// assert_eq!(val.type_name(), "i32");
575    ///
576    /// let val = Value::U64(u64::MAX);
577    /// assert_eq!(val.type_name(), "u64");
578    /// ```
579    #[must_use]
580    pub fn type_name(&self) -> &'static str {
581        match self {
582            Value::U32(_) => "u32",
583            Value::U64(_) => "u64",
584            Value::I32(_) => "i32",
585            Value::I64(_) => "i64",
586            Value::F64(_) => "f64",
587            Value::Bool(_) => "bool",
588            Value::String(_) => "string",
589            Value::Uuid(_) => "uuid",
590            Value::Timestamp(_) => "timestamp",
591        }
592    }
593
594    /// Returns true if this value is a numeric type (u32, u64, i32, i64, or f64)
595    ///
596    /// # Examples
597    ///
598    /// ```rust
599    /// use forgedb_types::Value;
600    ///
601    /// assert!(Value::U32(10).is_numeric());
602    /// assert!(Value::U64(u64::MAX).is_numeric());
603    /// assert!(Value::I32(42).is_numeric());
604    /// assert!(Value::F64(3.14).is_numeric());
605    /// assert!(!Value::String("hello".to_string()).is_numeric());
606    /// ```
607    #[must_use]
608    pub fn is_numeric(&self) -> bool {
609        matches!(
610            self,
611            Value::U32(_) | Value::U64(_) | Value::I32(_) | Value::I64(_) | Value::F64(_)
612        )
613    }
614
615    /// Returns true if this value is a string
616    ///
617    /// # Examples
618    ///
619    /// ```rust
620    /// use forgedb_types::Value;
621    ///
622    /// assert!(Value::String("hello".to_string()).is_string());
623    /// assert!(!Value::I32(42).is_string());
624    /// ```
625    #[must_use]
626    pub fn is_string(&self) -> bool {
627        matches!(self, Value::String(_))
628    }
629}
630
631// Implement From for convenient value construction
632impl From<u32> for Value {
633    fn from(v: u32) -> Self {
634        Value::U32(v)
635    }
636}
637
638impl From<u64> for Value {
639    fn from(v: u64) -> Self {
640        Value::U64(v)
641    }
642}
643
644impl From<i32> for Value {
645    fn from(v: i32) -> Self {
646        Value::I32(v)
647    }
648}
649
650impl From<i64> for Value {
651    fn from(v: i64) -> Self {
652        Value::I64(v)
653    }
654}
655
656impl From<f64> for Value {
657    fn from(v: f64) -> Self {
658        Value::F64(v)
659    }
660}
661
662impl From<bool> for Value {
663    fn from(v: bool) -> Self {
664        Value::Bool(v)
665    }
666}
667
668impl From<String> for Value {
669    fn from(v: String) -> Self {
670        Value::String(v)
671    }
672}
673
674impl From<&str> for Value {
675    fn from(v: &str) -> Self {
676        Value::String(v.to_string())
677    }
678}
679
680impl From<Uuid> for Value {
681    fn from(v: Uuid) -> Self {
682        Value::Uuid(v)
683    }
684}
685
686impl From<Timestamp> for Value {
687    fn from(v: Timestamp) -> Self {
688        Value::Timestamp(v)
689    }
690}
691
692
693// ---- InlineStr: the `Copy` fixed-capacity string key (#252) ------------------
694
695/// A `Copy`, fixed-capacity UTF-8 string holding at most `BYTES` bytes.
696///
697/// # Why it exists
698///
699/// The generated code passes a model's identity **by value**, repeatedly —
700/// `get(id)`, `delete(id)`, relation resolution, index-key construction, the
701/// live-query delta enum. That is sound for `Uuid`/`u32`/`u64`/[`Timestamp`]
702/// because they are `Copy`; `String` is not, so `id: string` produced 31
703/// move/borrow errors across a single model. Making the key type `Copy` is far
704/// smaller than making every id-consuming path stop copying, and it is what
705/// `string(N)` / `string(N!)` identities are built on (#252).
706///
707/// # Class-1 substrate
708///
709/// It knows nothing about schemas, identities or URLs. In particular the
710/// URL-path-segment alphabet a *string identity's value* must obey (#252 res 4)
711/// is **not** enforced here — that rule applies because a field is an identity,
712/// which is schema knowledge, so the check is emitted into the generated write
713/// path (res 7). What this type enforces is only the capacity bound.
714///
715/// # Parameterized by bytes, declared in characters
716///
717/// `BYTES` is a byte capacity, not a character count: the substrate cannot
718/// compute `[u8; CHARS * 4]` without the unstable `generic_const_exprs`, so the
719/// generator emits the constant. For an identity that mapping is the identity
720/// function — `string(26)` and `string(26!)` both give `InlineStr<26>`, because
721/// the column is one byte per character (#238) and `@utf8` on an identity is a
722/// validation error (#252 res 3). The `4N` form survives for non-identity
723/// columns, which is why the parameter stays in bytes.
724///
725/// # Nothing here is derived except `Clone, Copy`
726///
727/// | impl | why it is hand-written |
728/// |---|---|
729/// | `PartialEq`/`Eq`/`PartialOrd`/`Ord`/`Hash` | defined on [`Self::as_str`]; the bytes past `len` are not part of the value (#252 res 8) |
730/// | `Serialize`/`Deserialize` | a JSON **string**. serde's *derive* has per-length array impls that stop at 32 — the #243 defect (`char(N)` above 32 could not be indexed). Deriving here would compile for `InlineStr<26>` and fail for `InlineStr<64>` |
731/// | `Debug` | a derive would print `BYTES` integers into every log line and panic message |
732///
733/// # Examples
734///
735/// ```rust
736/// use forgedb_types::InlineStr;
737///
738/// let ulid: InlineStr<26> = "01JQZ8Y7X6W5V4T3S2R1Q0P9NM".try_into().unwrap();
739/// assert_eq!(ulid.len(), 26);
740/// assert_eq!(ulid.to_string(), "01JQZ8Y7X6W5V4T3S2R1Q0P9NM");
741///
742/// // The capacity is a bound, never a truncation.
743/// assert!(InlineStr::<4>::try_from("hello").is_err());
744/// ```
745#[derive(Clone, Copy)]
746pub struct InlineStr<const BYTES: usize> {
747    buf: [u8; BYTES],
748    /// Bytes of `buf` that are in use.
749    ///
750    /// `u16` rather than `u8` because `InlineStr` is not identity-only: #256 uses
751    /// it for bounded string *index keys*, and a non-identity `string(255) @utf8`
752    /// column reaches `BYTES = 1020`. For an identity `BYTES <= 255` always.
753    len: u16,
754}
755
756/// Why a `&str` did not fit an [`InlineStr`].
757///
758/// Carries both numbers so the generated 422 can name the bound without
759/// re-deriving it from the schema.
760#[derive(Debug, Clone, Copy, PartialEq, Eq)]
761pub struct InlineStrError {
762    /// The length of the offered value, in bytes.
763    pub got_bytes: usize,
764    /// The capacity of the target `InlineStr`, in bytes.
765    pub capacity: usize,
766}
767
768impl std::fmt::Display for InlineStrError {
769    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
770        write!(
771            f,
772            "value is {} bytes, but the column holds at most {}",
773            self.got_bytes, self.capacity
774        )
775    }
776}
777
778impl std::error::Error for InlineStrError {}
779
780impl<const BYTES: usize> InlineStr<BYTES> {
781    /// The byte capacity, as a constant.
782    pub const CAPACITY: usize = BYTES;
783
784    /// Tripped at monomorphization if a width could not record its own length.
785    /// A `u16` `len` caps the type at 65535 bytes, well above the 1020 that
786    /// `string(255) @utf8` reaches — this exists so the cap is a compile error
787    /// rather than a silent truncating `as u16`.
788    const _LEN_FITS: () = assert!(
789        BYTES <= u16::MAX as usize,
790        "InlineStr records its length in a u16, so BYTES must be at most 65535",
791    );
792
793    /// The text, borrowed.
794    ///
795    /// Infallible: UTF-8 is checked at construction, and construction is the only
796    /// writer of `buf`.
797    #[must_use]
798    pub fn as_str(&self) -> &str {
799        // SAFETY: `buf` is private and every constructor copies a whole `&str`
800        // into it — the over-capacity case is *rejected* rather than truncated,
801        // so a partial UTF-8 sequence can never be stored. `buf[..len]` is
802        // therefore always a complete, valid UTF-8 sequence.
803        unsafe { std::str::from_utf8_unchecked(&self.buf[..self.len as usize]) }
804    }
805
806    /// The used bytes, borrowed. Excludes the unused tail.
807    #[must_use]
808    pub fn as_bytes(&self) -> &[u8] {
809        &self.buf[..self.len as usize]
810    }
811
812    /// Length in **bytes** (matching `str::len`), not in characters.
813    #[must_use]
814    pub fn len(&self) -> usize {
815        self.len as usize
816    }
817
818    /// Whether the value is the empty string.
819    ///
820    /// The empty string is a legal `InlineStr` — it is the [`Default`], which the
821    /// generated id field's `#[serde(default)]` needs — and is rejected as a
822    /// *key* at write time (#252 res 5), not here.
823    #[must_use]
824    pub fn is_empty(&self) -> bool {
825        self.len == 0
826    }
827}
828
829impl<const BYTES: usize> Default for InlineStr<BYTES> {
830    /// The empty string.
831    ///
832    /// Every generated model struct carries `#[serde(default)]` on its id field
833    /// so a create body may omit it. That is safe against res 5 because the empty
834    /// key is rejected at *write*: the default exists for the
835    /// deserialize-then-overwrite path to land in.
836    fn default() -> Self {
837        Self {
838            buf: [0u8; BYTES],
839            len: 0,
840        }
841    }
842}
843
844impl<const BYTES: usize> TryFrom<&str> for InlineStr<BYTES> {
845    type Error = InlineStrError;
846
847    /// Copy `s` in whole, or fail.
848    ///
849    /// **Never truncates.** A silently shortened key writes a row under an id the
850    /// caller never asked for, and the caller's own copy of the id then resolves
851    /// to nothing.
852    ///
853    /// The unused tail is zeroed. Nothing depends on it — [`Self::as_str`] slices
854    /// to `len`, and comparison is defined on the text — but it makes two equal
855    /// values byte-identical, which is what lets an inline column file stay
856    /// byte-reproducible for the same logical content (#238).
857    fn try_from(s: &str) -> Result<Self, Self::Error> {
858        let () = Self::_LEN_FITS;
859        let b = s.as_bytes();
860        if b.len() > BYTES {
861            return Err(InlineStrError {
862                got_bytes: b.len(),
863                capacity: BYTES,
864            });
865        }
866        let mut buf = [0u8; BYTES];
867        buf[..b.len()].copy_from_slice(b);
868        Ok(Self {
869            buf,
870            len: b.len() as u16,
871        })
872    }
873}
874
875impl<const BYTES: usize> TryFrom<&String> for InlineStr<BYTES> {
876    type Error = InlineStrError;
877    fn try_from(s: &String) -> Result<Self, Self::Error> {
878        Self::try_from(s.as_str())
879    }
880}
881
882impl<const BYTES: usize> std::str::FromStr for InlineStr<BYTES> {
883    type Err = InlineStrError;
884
885    /// Identical to [`TryFrom<&str>`]. Present because axum's `Path<T>` extractor
886    /// resolves a route segment through `FromStr`, which is what makes
887    /// `/docs/{id}` work for a string-keyed model with nothing generated.
888    fn from_str(s: &str) -> Result<Self, Self::Err> {
889        Self::try_from(s)
890    }
891}
892
893impl<const BYTES: usize> std::ops::Deref for InlineStr<BYTES> {
894    type Target = str;
895
896    /// So the generated validation, filter and sort code keeps comparing against
897    /// `&str` unchanged.
898    fn deref(&self) -> &str {
899        self.as_str()
900    }
901}
902
903impl<const BYTES: usize> AsRef<str> for InlineStr<BYTES> {
904    fn as_ref(&self) -> &str {
905        self.as_str()
906    }
907}
908
909// Comparison and hashing are defined on the TEXT, never on the buffer (#252
910// res 8). The bytes past `len` are not part of the value: two values built from
911// the same `&str` through different paths must be equal and must hash equal, and
912// comparing `buf` wholesale would make that depend on what happened to be in the
913// tail. This is a `HashMap` key type — `id_to_row`, `id_versions`, and #266's
914// junction traversal indexes — so getting it wrong produces lookup misses rather
915// than compile errors. The zeroed tail in the constructors is defence in depth,
916// not the mechanism.
917impl<const BYTES: usize> PartialEq for InlineStr<BYTES> {
918    fn eq(&self, other: &Self) -> bool {
919        self.as_str() == other.as_str()
920    }
921}
922
923impl<const BYTES: usize> Eq for InlineStr<BYTES> {}
924
925impl<const BYTES: usize> std::hash::Hash for InlineStr<BYTES> {
926    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
927        self.as_str().hash(state);
928    }
929}
930
931impl<const BYTES: usize> Ord for InlineStr<BYTES> {
932    /// Lexicographic by bytes, exactly as `str` orders — so an index range scan
933    /// over a string key (#256) returns rows in the order the key text implies.
934    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
935        self.as_str().cmp(other.as_str())
936    }
937}
938
939impl<const BYTES: usize> PartialOrd for InlineStr<BYTES> {
940    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
941        Some(self.cmp(other))
942    }
943}
944
945// Cross-type comparison against the borrowed and owned string types, so a
946// generated filter can compare a key column against a parsed query parameter
947// without either side spelling a conversion.
948impl<const BYTES: usize> PartialEq<str> for InlineStr<BYTES> {
949    fn eq(&self, other: &str) -> bool {
950        self.as_str() == other
951    }
952}
953
954impl<const BYTES: usize> PartialEq<&str> for InlineStr<BYTES> {
955    fn eq(&self, other: &&str) -> bool {
956        self.as_str() == *other
957    }
958}
959
960impl<const BYTES: usize> PartialEq<String> for InlineStr<BYTES> {
961    fn eq(&self, other: &String) -> bool {
962        self.as_str() == other.as_str()
963    }
964}
965
966impl<const BYTES: usize> PartialEq<InlineStr<BYTES>> for str {
967    fn eq(&self, other: &InlineStr<BYTES>) -> bool {
968        self == other.as_str()
969    }
970}
971
972impl<const BYTES: usize> PartialEq<InlineStr<BYTES>> for String {
973    fn eq(&self, other: &InlineStr<BYTES>) -> bool {
974        self.as_str() == other.as_str()
975    }
976}
977
978impl<const BYTES: usize> std::fmt::Display for InlineStr<BYTES> {
979    /// The text. This is what the generated create handler's `id.to_string()`
980    /// and the index key's `write!` produce, and what makes `/docs/{id}`
981    /// resolvable.
982    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
983        f.write_str(self.as_str())
984    }
985}
986
987impl<const BYTES: usize> std::fmt::Debug for InlineStr<BYTES> {
988    /// Renders as a quoted string, like `str` — a derived `Debug` would print
989    /// `BYTES` integers into every log line and every panic message.
990    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
991        std::fmt::Debug::fmt(self.as_str(), f)
992    }
993}
994
995impl<const BYTES: usize> Serialize for InlineStr<BYTES> {
996    /// A JSON **string**, at every width.
997    ///
998    /// Do NOT replace this with `#[derive(Serialize)]`. serde's derive would
999    /// serialize the `[u8; BYTES]` field, and its array impls stop at 32 — so a
1000    /// derive compiles for `InlineStr<26>` and fails to compile for
1001    /// `InlineStr<64>`. That is the #243 defect (`char(N)` above 32 could not be
1002    /// indexed) designed out at the type level rather than re-hit per width.
1003    fn serialize<S: serde::Serializer>(&self, ser: S) -> std::result::Result<S::Ok, S::Error> {
1004        ser.serialize_str(self.as_str())
1005    }
1006}
1007
1008impl<'de, const BYTES: usize> Deserialize<'de> for InlineStr<BYTES> {
1009    /// From a JSON string, rejecting an over-capacity value rather than
1010    /// truncating it — see [`Serialize`] for why this is not a derive.
1011    fn deserialize<D: serde::Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
1012        let s = <std::borrow::Cow<'de, str>>::deserialize(de)?;
1013        Self::try_from(s.as_ref()).map_err(serde::de::Error::custom)
1014    }
1015}
1016
1017#[cfg(test)]
1018mod inline_str_internal_tests {
1019    use super::InlineStr;
1020    use std::collections::hash_map::DefaultHasher;
1021    use std::hash::{Hash, Hasher};
1022
1023    fn hash_of<T: Hash>(v: &T) -> u64 {
1024        let mut h = DefaultHasher::new();
1025        v.hash(&mut h);
1026        h.finish()
1027    }
1028
1029    /// Res 8's tail clause, which cannot be written as an integration test: the
1030    /// public API has no way to produce a dirty tail (every constructor copies a
1031    /// whole `&str` into a zeroed buffer), by design. So the guard reaches
1032    /// through the private fields and builds one directly.
1033    ///
1034    /// Without this, `#[derive(PartialEq, Hash)]` would pass every other
1035    /// scenario — with a zeroed tail a buffer-wise comparison agrees with a
1036    /// text-wise one — and res 8 would be an unguarded claim.
1037    #[test]
1038    fn inline_str_tail_is_not_part_of_the_value() {
1039        let clean = InlineStr::<16> {
1040            buf: *b"ab\0\0\0\0\0\0\0\0\0\0\0\0\0\0",
1041            len: 2,
1042        };
1043        let dirty = InlineStr::<16> {
1044            buf: *b"abZZZZZZZZZZZZZZ",
1045            len: 2,
1046        };
1047
1048        assert_eq!(clean.as_str(), "ab");
1049        assert_eq!(dirty.as_str(), "ab");
1050        assert_eq!(clean, dirty, "the tail must not participate in equality");
1051        assert_eq!(
1052            hash_of(&clean),
1053            hash_of(&dirty),
1054            "the tail must not participate in the hash"
1055        );
1056        assert_eq!(
1057            clean.cmp(&dirty),
1058            std::cmp::Ordering::Equal,
1059            "the tail must not participate in the ordering"
1060        );
1061    }
1062
1063    /// The same clause for `Ord` across two different values, where a buffer-wise
1064    /// comparison would produce the *wrong* answer rather than merely a
1065    /// different-looking one: `"ab"` with a `Z` tail sorts above `"abc"` on bytes,
1066    /// but below it on text.
1067    #[test]
1068    fn inline_str_ordering_ignores_a_dirty_tail() {
1069        let ab_dirty = InlineStr::<8> {
1070            buf: *b"abZZZZZZ",
1071            len: 2,
1072        };
1073        let abc = InlineStr::<8> {
1074            buf: *b"abc\0\0\0\0\0",
1075            len: 3,
1076        };
1077        assert!(ab_dirty < abc);
1078    }
1079}