triblespace_core/value/schemas/
r256.rs1use crate::id::ExclusiveId;
2use crate::id::Id;
3use crate::id_hex;
4use crate::macros::entity;
5use crate::metadata;
6use crate::metadata::ConstMetadata;
7use crate::repo::BlobStore;
8use crate::trible::TribleSet;
9use crate::value::schemas::hash::Blake3;
10use crate::value::FromValue;
11use crate::value::ToValue;
12use crate::value::TryFromValue;
13use crate::value::Value;
14use crate::value::ValueSchema;
15use std::convert::Infallible;
16
17use std::convert::TryInto;
18
19#[cfg(feature = "wasm")]
20use crate::blob::schemas::wasmcode::WasmCode;
21use num_rational::Ratio;
22
23pub struct R256LE;
32
33pub struct R256BE;
42
43pub type R256 = R256LE;
44
45impl ConstMetadata for R256LE {
46 fn id() -> Id {
47 id_hex!("0A9B43C5C2ECD45B257CDEFC16544358")
48 }
49
50 fn describe<B>(blobs: &mut B) -> Result<TribleSet, B::PutError>
51 where
52 B: BlobStore<Blake3>,
53 {
54 let id = Self::id();
55 let description = blobs.put(
56 "Exact ratio stored as two i128 values (numerator/denominator) in little-endian, normalized with a positive denominator. This keeps fractions canonical and comparable.\n\nUse for exact rates, proportions, or unit conversions where rounding is unacceptable. Prefer F64 or F256 when approximate floats are fine or when interfacing with floating-point APIs.\n\nDenominator zero is invalid; the schema expects canonicalized fractions. If you need intervals or ranges instead of ratios, use the range schemas.",
57 )?;
58 let tribles = entity! {
59 ExclusiveId::force_ref(&id) @
60 metadata::name: blobs.put("r256le".to_string())?,
61 metadata::description: description,
62 metadata::tag: metadata::KIND_VALUE_SCHEMA,
63 };
64
65 #[cfg(feature = "wasm")]
66 let tribles = {
67 let mut tribles = tribles;
68 tribles += entity! { ExclusiveId::force_ref(&id) @
69 metadata::value_formatter: blobs.put(wasm_formatter::R256_LE_WASM)?,
70 };
71 tribles
72 };
73 Ok(tribles)
74 }
75}
76impl ValueSchema for R256LE {
77 type ValidationError = Infallible;
78}
79impl ConstMetadata for R256BE {
80 fn id() -> Id {
81 id_hex!("CA5EAF567171772C1FFD776E9C7C02D1")
82 }
83
84 fn describe<B>(blobs: &mut B) -> Result<TribleSet, B::PutError>
85 where
86 B: BlobStore<Blake3>,
87 {
88 let id = Self::id();
89 let description = blobs.put(
90 "Exact ratio stored as two i128 values (numerator/denominator) in big-endian, normalized with a positive denominator. This is useful when bytewise ordering or protocol encoding matters.\n\nUse for exact fractions in ordered or interoperable formats. Prefer F64 or F256 when approximate floats are acceptable.\n\nAs with the little-endian variant, values are expected to be canonical and denominator must be non-zero.",
91 )?;
92 let tribles = entity! {
93 ExclusiveId::force_ref(&id) @
94 metadata::name: blobs.put("r256be".to_string())?,
95 metadata::description: description,
96 metadata::tag: metadata::KIND_VALUE_SCHEMA,
97 };
98
99 #[cfg(feature = "wasm")]
100 let tribles = {
101 let mut tribles = tribles;
102 tribles += entity! { ExclusiveId::force_ref(&id) @
103 metadata::value_formatter: blobs.put(wasm_formatter::R256_BE_WASM)?,
104 };
105 tribles
106 };
107 Ok(tribles)
108 }
109}
110
111#[cfg(feature = "wasm")]
112mod wasm_formatter {
113 use core::fmt::Write;
114
115 use triblespace_core_macros::value_formatter;
116
117 #[value_formatter]
118 pub(crate) fn r256_le(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
119 let mut buf = [0u8; 16];
120 buf.copy_from_slice(&raw[..16]);
121 let numer = i128::from_le_bytes(buf);
122 buf.copy_from_slice(&raw[16..]);
123 let denom = i128::from_le_bytes(buf);
124
125 if denom == 0 {
126 return Err(2);
127 }
128
129 if denom == 1 {
130 write!(out, "{numer}").map_err(|_| 1u32)?;
131 } else {
132 write!(out, "{numer}/{denom}").map_err(|_| 1u32)?;
133 }
134 Ok(())
135 }
136
137 #[value_formatter]
138 pub(crate) fn r256_be(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
139 let mut buf = [0u8; 16];
140 buf.copy_from_slice(&raw[..16]);
141 let numer = i128::from_be_bytes(buf);
142 buf.copy_from_slice(&raw[16..]);
143 let denom = i128::from_be_bytes(buf);
144
145 if denom == 0 {
146 return Err(2);
147 }
148
149 if denom == 1 {
150 write!(out, "{numer}").map_err(|_| 1u32)?;
151 } else {
152 write!(out, "{numer}/{denom}").map_err(|_| 1u32)?;
153 }
154 Ok(())
155 }
156}
157impl ValueSchema for R256BE {
158 type ValidationError = Infallible;
159}
160
161#[derive(Debug)]
166pub enum RatioError {
167 NonCanonical(i128, i128),
168 ZeroDenominator,
169}
170
171impl TryFromValue<'_, R256BE> for Ratio<i128> {
172 type Error = RatioError;
173
174 fn try_from_value(v: &Value<R256BE>) -> Result<Self, Self::Error> {
175 let n = i128::from_be_bytes(v.raw[0..16].try_into().unwrap());
176 let d = i128::from_be_bytes(v.raw[16..32].try_into().unwrap());
177
178 if d == 0 {
179 return Err(RatioError::ZeroDenominator);
180 }
181
182 let ratio = Ratio::new_raw(n, d);
183 let ratio = ratio.reduced();
184 let (reduced_n, reduced_d) = ratio.into_raw();
185
186 if reduced_n != n || reduced_d != d {
187 Err(RatioError::NonCanonical(n, d))
188 } else {
189 Ok(ratio)
190 }
191 }
192}
193
194impl FromValue<'_, R256BE> for Ratio<i128> {
195 fn from_value(v: &Value<R256BE>) -> Self {
196 match Ratio::try_from_value(v) {
197 Ok(ratio) => ratio,
198 Err(RatioError::NonCanonical(n, d)) => {
199 panic!("Non canonical ratio: {n}/{d}");
200 }
201 Err(RatioError::ZeroDenominator) => {
202 panic!("Zero denominator ratio");
203 }
204 }
205 }
206}
207
208impl ToValue<R256BE> for Ratio<i128> {
209 fn to_value(self) -> Value<R256BE> {
210 let ratio = self.reduced();
211
212 let mut bytes = [0; 32];
213 bytes[0..16].copy_from_slice(&ratio.numer().to_be_bytes());
214 bytes[16..32].copy_from_slice(&ratio.denom().to_be_bytes());
215
216 Value::new(bytes)
217 }
218}
219
220impl ToValue<R256BE> for i128 {
221 fn to_value(self) -> Value<R256BE> {
222 let mut bytes = [0; 32];
223 bytes[0..16].copy_from_slice(&self.to_be_bytes());
224 bytes[16..32].copy_from_slice(&1i128.to_be_bytes());
225
226 Value::new(bytes)
227 }
228}
229
230impl TryFromValue<'_, R256LE> for Ratio<i128> {
231 type Error = RatioError;
232
233 fn try_from_value(v: &Value<R256LE>) -> Result<Self, Self::Error> {
234 let n = i128::from_le_bytes(v.raw[0..16].try_into().unwrap());
235 let d = i128::from_le_bytes(v.raw[16..32].try_into().unwrap());
236
237 if d == 0 {
238 return Err(RatioError::ZeroDenominator);
239 }
240
241 let ratio = Ratio::new_raw(n, d);
242 let ratio = ratio.reduced();
243 let (reduced_n, reduced_d) = ratio.into_raw();
244
245 if reduced_n != n || reduced_d != d {
246 Err(RatioError::NonCanonical(n, d))
247 } else {
248 Ok(ratio)
249 }
250 }
251}
252
253impl FromValue<'_, R256LE> for Ratio<i128> {
254 fn from_value(v: &Value<R256LE>) -> Self {
255 match Ratio::try_from_value(v) {
256 Ok(ratio) => ratio,
257 Err(RatioError::NonCanonical(n, d)) => {
258 panic!("Non canonical ratio: {n}/{d}");
259 }
260 Err(RatioError::ZeroDenominator) => {
261 panic!("Zero denominator ratio");
262 }
263 }
264 }
265}
266
267impl ToValue<R256LE> for Ratio<i128> {
268 fn to_value(self) -> Value<R256LE> {
269 let mut bytes = [0; 32];
270 bytes[0..16].copy_from_slice(&self.numer().to_le_bytes());
271 bytes[16..32].copy_from_slice(&self.denom().to_le_bytes());
272
273 Value::new(bytes)
274 }
275}
276
277impl ToValue<R256LE> for i128 {
278 fn to_value(self) -> Value<R256LE> {
279 let mut bytes = [0; 32];
280 bytes[0..16].copy_from_slice(&self.to_le_bytes());
281 bytes[16..32].copy_from_slice(&1i128.to_le_bytes());
282
283 Value::new(bytes)
284 }
285}