Skip to main content

triblespace_core/value/schemas/
boolean.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::ToValue;
11use crate::value::TryFromValue;
12use crate::value::TryToValue;
13use crate::value::Value;
14use crate::value::ValueSchema;
15use crate::value::VALUE_LEN;
16
17use std::convert::Infallible;
18
19/// Error raised when a value does not match the [`Boolean`] encoding.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct InvalidBoolean;
22
23/// Value schema that stores boolean flags as either all-zero or all-one bit patterns.
24///
25/// Storing `false` as `0x00` and `true` as `0xFF` in every byte makes it trivial to
26/// distinguish the two cases while leaving room for future SIMD optimisations when
27/// scanning large collections of flags.
28pub struct Boolean;
29
30impl ConstId for Boolean {
31    const ID: Id = id_hex!("73B414A3E25B0C0F9E4D6B0694DC33C5");
32}
33
34impl Boolean {
35    fn encode(flag: bool) -> Value<Self> {
36        if flag {
37            Value::new([u8::MAX; VALUE_LEN])
38        } else {
39            Value::new([0u8; VALUE_LEN])
40        }
41    }
42
43    fn decode(value: &Value<Self>) -> Result<bool, InvalidBoolean> {
44        if value.raw.iter().all(|&b| b == 0) {
45            Ok(false)
46        } else if value.raw.iter().all(|&b| b == u8::MAX) {
47            Ok(true)
48        } else {
49            Err(InvalidBoolean)
50        }
51    }
52}
53
54impl ConstDescribe for Boolean {
55    fn describe<B>(blobs: &mut B) -> Result<Fragment, B::PutError>
56    where
57        B: BlobStore<Blake3>,
58    {
59        let id = Self::ID;
60        let description = blobs.put(
61            "Boolean stored as all-zero bytes for false and all-0xFF bytes for true. The encoding uses the full 32-byte value, making the two states obvious and cheap to test.\n\nUse for simple flags and binary states. Represent unknown or missing data by omitting the trible rather than inventing a third sentinel value.\n\nMixed patterns are invalid and will fail validation. If you need tri-state or richer states, model it explicitly (for example with ShortString or a dedicated entity).",
62        )?;
63        let name = blobs.put("boolean")?;
64        let tribles = entity! {
65            ExclusiveId::force_ref(&id) @
66                metadata::name: name,
67                metadata::description: description,
68                metadata::tag: metadata::KIND_VALUE_SCHEMA,
69        };
70
71        #[cfg(feature = "wasm")]
72        let tribles = {
73            let mut tribles = tribles;
74            tribles += entity! { ExclusiveId::force_ref(&id) @
75                metadata::value_formatter: blobs.put(wasm_formatter::BOOLEAN_WASM)?,
76            };
77            tribles
78        };
79        Ok(tribles)
80    }
81}
82
83#[cfg(feature = "wasm")]
84mod wasm_formatter {
85    use core::fmt::Write;
86
87    use triblespace_core_macros::value_formatter;
88
89    #[value_formatter]
90    pub(crate) fn boolean(raw: &[u8; 32], out: &mut impl Write) -> Result<(), u32> {
91        let all_zero = raw.iter().all(|&b| b == 0);
92        let all_ones = raw.iter().all(|&b| b == u8::MAX);
93
94        let text = if all_zero {
95            "false"
96        } else if all_ones {
97            "true"
98        } else {
99            return Err(2);
100        };
101
102        out.write_str(text).map_err(|_| 1u32)?;
103        Ok(())
104    }
105}
106
107impl ValueSchema for Boolean {
108    type ValidationError = InvalidBoolean;
109
110    fn validate(value: Value<Self>) -> Result<Value<Self>, Self::ValidationError> {
111        Self::decode(&value)?;
112        Ok(value)
113    }
114}
115
116impl<'a> TryFromValue<'a, Boolean> for bool {
117    type Error = InvalidBoolean;
118
119    fn try_from_value(v: &'a Value<Boolean>) -> Result<Self, Self::Error> {
120        Boolean::decode(v)
121    }
122}
123
124impl TryToValue<Boolean> for bool {
125    type Error = Infallible;
126
127    fn try_to_value(self) -> Result<Value<Boolean>, Self::Error> {
128        Ok(Boolean::encode(self))
129    }
130}
131
132impl TryToValue<Boolean> for &bool {
133    type Error = Infallible;
134
135    fn try_to_value(self) -> Result<Value<Boolean>, Self::Error> {
136        Ok(Boolean::encode(*self))
137    }
138}
139
140impl ToValue<Boolean> for bool {
141    fn to_value(self) -> Value<Boolean> {
142        Boolean::encode(self)
143    }
144}
145
146impl ToValue<Boolean> for &bool {
147    fn to_value(self) -> Value<Boolean> {
148        Boolean::encode(*self)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::Boolean;
155    use super::InvalidBoolean;
156    use crate::value::Value;
157    use crate::value::ValueSchema;
158
159    #[test]
160    fn encodes_false_as_zero_bytes() {
161        let value = Boolean::value_from(false);
162        assert!(value.raw.iter().all(|&b| b == 0));
163        assert_eq!(Boolean::validate(value), Ok(Boolean::value_from(false)));
164    }
165
166    #[test]
167    fn encodes_true_as_all_ones() {
168        let value = Boolean::value_from(true);
169        assert!(value.raw.iter().all(|&b| b == u8::MAX));
170        assert_eq!(Boolean::validate(value), Ok(Boolean::value_from(true)));
171    }
172
173    #[test]
174    fn rejects_mixed_bit_patterns() {
175        let mut mixed = [0u8; crate::value::VALUE_LEN];
176        mixed[0] = 1;
177        let value = Value::<Boolean>::new(mixed);
178        assert_eq!(Boolean::validate(value), Err(InvalidBoolean));
179    }
180}