Skip to main content

triblespace_core/value/schemas/
range.rs

1use crate::id::ExclusiveId;
2use crate::id::Id;
3use crate::id_hex;
4use crate::macros::entity;
5use crate::metadata;
6use crate::metadata::{ConstDescribe, ConstId};
7use crate::repo::BlobStore;
8use crate::trible::Fragment;
9use crate::value::schemas::hash::Blake3;
10use crate::value::RawValue;
11use crate::value::ToValue;
12use crate::value::TryFromValue;
13use crate::value::TryToValue;
14use crate::value::Value;
15use crate::value::ValueSchema;
16use std::convert::Infallible;
17use std::ops::{Range, RangeInclusive};
18
19/// A value schema for representing a pair of `u128` values.
20///
21/// [`RangeU128`] encodes the pair as a half-open interval while
22/// [`RangeInclusiveU128`] represents an inclusive range. Both schemas encode the
23/// endpoints by packing the line into the high 64 bits and the column into the
24/// low 64 bits of the `u128`.
25#[derive(Debug, Clone, Copy)]
26pub struct RangeU128;
27
28/// Inclusive range of two `u128` values (`start..=end`), big-endian encoded.
29#[derive(Debug, Clone, Copy)]
30pub struct RangeInclusiveU128;
31
32impl ConstId for RangeU128 {
33    const ID: Id = id_hex!("A4E25E3B92364FA5AB519C6A77D7CB3A");
34}
35
36impl ConstId for RangeInclusiveU128 {
37    const ID: Id = id_hex!("1D0D82CA84424CD0A2F98DB37039E152");
38}
39
40impl ConstDescribe for RangeU128 {
41    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
42    where
43        B: BlobStore<Blake3>,
44    {
45        let id = Self::ID;
46        let description = blobs.put(
47            "Half-open range encoded as two big-endian u128 values (start..end). This mirrors common slice semantics where the end is exclusive.\n\nUse for offsets, byte ranges, and spans where length matters and empty ranges are valid. Use RangeInclusiveU128 when both endpoints should be included.\n\nNo normalization is enforced; callers should ensure start <= end and interpret units consistently.",
48        )?;
49        let tribles = entity! {
50            ExclusiveId::force_ref(&id) @
51                metadata::name: blobs.put("range_u128")?,
52                metadata::description: description,
53                metadata::tag: metadata::KIND_VALUE_SCHEMA,
54        };
55
56        #[cfg(feature = "wasm")]
57        let tribles = {
58            let mut tribles = tribles;
59            tribles += entity! { ExclusiveId::force_ref(&id) @
60                metadata::value_formatter: blobs.put(wasm_formatters::RANGE_U128_WASM)?,
61            };
62            tribles
63        };
64        Ok(tribles)
65    }
66}
67
68impl ValueSchema for RangeU128 {
69    type ValidationError = Infallible;
70}
71
72impl ConstDescribe for RangeInclusiveU128 {
73    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
74    where
75        B: BlobStore<Blake3>,
76    {
77        let id = Self::ID;
78        let description = blobs.put(
79            "Inclusive range encoded as two big-endian u128 values (start..=end). This is convenient when both endpoints are meaningful.\n\nUse for closed intervals such as line/column ranges or inclusive numeric bounds. Prefer RangeU128 for half-open intervals and length-based calculations.\n\nCallers should decide how to handle empty or reversed ranges; the schema only defines the byte layout.",
80        )?;
81        let tribles = entity! {
82            ExclusiveId::force_ref(&id) @
83                metadata::name: blobs.put("range_u128_inc")?,
84                metadata::description: description,
85                metadata::tag: metadata::KIND_VALUE_SCHEMA,
86        };
87
88        #[cfg(feature = "wasm")]
89        let tribles = {
90            let mut tribles = tribles;
91            tribles += entity! { ExclusiveId::force_ref(&id) @
92                metadata::value_formatter: blobs.put(wasm_formatters::RANGE_INCLUSIVE_U128_WASM)?,
93            };
94            tribles
95        };
96        Ok(tribles)
97    }
98}
99
100#[cfg(feature = "wasm")]
101mod wasm_formatters {
102    use core::fmt::Write;
103
104    use triblespace_core_macros::value_formatter;
105
106    #[value_formatter]
107    pub(crate) fn range_u128(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
108        let mut buf = [0u8; 16];
109        buf.copy_from_slice(&raw[..16]);
110        let start = u128::from_be_bytes(buf);
111        buf.copy_from_slice(&raw[16..]);
112        let end = u128::from_be_bytes(buf);
113        write!(out, "{start}..{end}").map_err(|_| 1u32)?;
114        Ok(())
115    }
116
117    #[value_formatter]
118    pub(crate) fn range_inclusive_u128(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
119        let mut buf = [0u8; 16];
120        buf.copy_from_slice(&raw[..16]);
121        let start = u128::from_be_bytes(buf);
122        buf.copy_from_slice(&raw[16..]);
123        let end = u128::from_be_bytes(buf);
124        write!(out, "{start}..={end}").map_err(|_| 1u32)?;
125        Ok(())
126    }
127}
128
129impl ValueSchema for RangeInclusiveU128 {
130    type ValidationError = Infallible;
131}
132
133fn encode_pair(range: (u128, u128)) -> RawValue {
134    let mut raw = [0u8; 32];
135    raw[..16].copy_from_slice(&range.0.to_be_bytes());
136    raw[16..].copy_from_slice(&range.1.to_be_bytes());
137    raw
138}
139
140fn decode_pair(raw: &RawValue) -> (u128, u128) {
141    let mut first = [0u8; 16];
142    let mut second = [0u8; 16];
143    first.copy_from_slice(&raw[..16]);
144    second.copy_from_slice(&raw[16..]);
145    (u128::from_be_bytes(first), u128::from_be_bytes(second))
146}
147
148fn encode_range_value<S: ValueSchema>(range: (u128, u128)) -> Value<S> {
149    Value::new(encode_pair(range))
150}
151
152fn decode_range_value<S: ValueSchema>(value: &Value<S>) -> (u128, u128) {
153    decode_pair(&value.raw)
154}
155
156impl ToValue<RangeU128> for (u128, u128) {
157    fn to_value(self) -> Value<RangeU128> {
158        encode_range_value(self)
159    }
160}
161
162impl TryFromValue<'_, RangeU128> for (u128, u128) {
163    type Error = Infallible;
164    fn try_from_value(v: &Value<RangeU128>) -> Result<Self, Infallible> {
165        Ok(decode_range_value(v))
166    }
167}
168
169impl ToValue<RangeInclusiveU128> for (u128, u128) {
170    fn to_value(self) -> Value<RangeInclusiveU128> {
171        encode_range_value(self)
172    }
173}
174
175impl TryFromValue<'_, RangeInclusiveU128> for (u128, u128) {
176    type Error = Infallible;
177    fn try_from_value(v: &Value<RangeInclusiveU128>) -> Result<Self, Infallible> {
178        Ok(decode_range_value(v))
179    }
180}
181
182impl TryToValue<RangeU128> for Range<u128> {
183    type Error = Infallible;
184
185    fn try_to_value(self) -> Result<Value<RangeU128>, Self::Error> {
186        Ok(encode_range_value((self.start, self.end)))
187    }
188}
189
190impl TryFromValue<'_, RangeU128> for Range<u128> {
191    type Error = Infallible;
192
193    fn try_from_value(v: &Value<RangeU128>) -> Result<Self, Self::Error> {
194        let (start, end) = decode_range_value(v);
195        Ok(start..end)
196    }
197}
198
199impl TryToValue<RangeInclusiveU128> for RangeInclusive<u128> {
200    type Error = Infallible;
201
202    fn try_to_value(self) -> Result<Value<RangeInclusiveU128>, Self::Error> {
203        let (start, end) = self.into_inner();
204        Ok(encode_range_value((start, end)))
205    }
206}
207
208impl TryFromValue<'_, RangeInclusiveU128> for RangeInclusive<u128> {
209    type Error = Infallible;
210
211    fn try_from_value(v: &Value<RangeInclusiveU128>) -> Result<Self, Self::Error> {
212        let (start, end) = decode_range_value(v);
213        Ok(start..=end)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    use crate::value::{ToValue, TryFromValue, TryToValue};
221    use proptest::prelude::*;
222
223    proptest! {
224        #[test]
225        fn range_u128_tuple_roundtrip(a: u128, b: u128) {
226            let input = (a, b);
227            let value: Value<RangeU128> = input.to_value();
228            let output: (u128, u128) = value.from_value();
229            prop_assert_eq!(input, output);
230        }
231
232        #[test]
233        fn range_u128_range_roundtrip(a: u128, b: u128) {
234            let input = a..b;
235            let value: Value<RangeU128> = input.clone().try_to_value().unwrap();
236            let output = Range::<u128>::try_from_value(&value).unwrap();
237            prop_assert_eq!(input, output);
238        }
239
240        #[test]
241        fn range_inclusive_tuple_roundtrip(a: u128, b: u128) {
242            let input = (a, b);
243            let value: Value<RangeInclusiveU128> = input.to_value();
244            let output: (u128, u128) = value.from_value();
245            prop_assert_eq!(input, output);
246        }
247
248        #[test]
249        fn range_inclusive_range_roundtrip(a: u128, b: u128) {
250            let input = a..=b;
251            let value: Value<RangeInclusiveU128> = input.clone().try_to_value().unwrap();
252            let output = RangeInclusive::<u128>::try_from_value(&value).unwrap();
253            prop_assert_eq!(input, output);
254        }
255
256        #[test]
257        fn range_u128_tuple_and_range_agree(a: u128, b: u128) {
258            let tuple_val: Value<RangeU128> = (a, b).to_value();
259            let range_val: Value<RangeU128> = (a..b).try_to_value().unwrap();
260            prop_assert_eq!(tuple_val.raw, range_val.raw);
261        }
262
263        #[test]
264        fn range_inclusive_tuple_and_range_agree(a: u128, b: u128) {
265            let tuple_val: Value<RangeInclusiveU128> = (a, b).to_value();
266            let range_val: Value<RangeInclusiveU128> = (a..=b).try_to_value().unwrap();
267            prop_assert_eq!(tuple_val.raw, range_val.raw);
268        }
269
270        #[test]
271        fn range_u128_validates(a: u128, b: u128) {
272            let value: Value<RangeU128> = (a, b).to_value();
273            prop_assert!(RangeU128::validate(value).is_ok());
274        }
275
276        #[test]
277        fn range_inclusive_validates(a: u128, b: u128) {
278            let value: Value<RangeInclusiveU128> = (a, b).to_value();
279            prop_assert!(RangeInclusiveU128::validate(value).is_ok());
280        }
281    }
282}