Skip to main content

uqa_analysis/
term.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Lossless term identity for Unicode strings and unpaired UTF-16 units.
8
9use std::borrow::Cow;
10#[cfg(test)]
11use std::ops::Range;
12
13use serde::{Deserialize, Serialize, Serializer};
14
15use crate::{AnalysisError, AnalysisResult};
16
17mod allocation;
18pub(crate) use allocation::{TermBoundary, TermBuffer};
19
20#[derive(Clone)]
21enum Characters<'a> {
22    Unicode(std::str::Chars<'a>),
23    UTF16(std::char::DecodeUtf16<std::iter::Copied<std::slice::Iter<'a, u16>>>),
24}
25
26impl Iterator for Characters<'_> {
27    type Item = Result<char, u16>;
28
29    fn next(&mut self) -> Option<Self::Item> {
30        match self {
31            Self::Unicode(characters) => characters.next().map(Ok),
32            Self::UTF16(characters) => characters
33                .next()
34                .map(|value| value.map_err(|error| error.unpaired_surrogate())),
35        }
36    }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40enum Representation {
41    Unicode(String),
42    UTF16(Vec<u16>),
43}
44
45#[derive(Clone)]
46enum UTF16Units<'a> {
47    Unicode(std::str::EncodeUtf16<'a>),
48    Raw(std::iter::Copied<std::slice::Iter<'a, u16>>),
49}
50
51impl Iterator for UTF16Units<'_> {
52    type Item = u16;
53    fn next(&mut self) -> Option<u16> {
54        match self {
55            Self::Unicode(units) => units.next(),
56            Self::Raw(units) => units.next(),
57        }
58    }
59    fn size_hint(&self) -> (usize, Option<usize>) {
60        match self {
61            Self::Unicode(units) => units.size_hint(),
62            Self::Raw(units) => units.size_hint(),
63        }
64    }
65}
66
67/// Canonical term text. Valid UTF-16 is stored as a string; unpaired units remain exact.
68///
69/// Serialization uses a JSON string for scalar text or an explicit `{"utf16":[...]}` object for unpaired units. Scalar and raw-unit construction give identical identity for the same valid text.
70///
71/// ```
72/// use uqa_analysis::TokenTerm;
73/// let term = TokenTerm::from_utf16(vec![0xd83d]);
74/// assert!(term.as_str().is_none());
75/// assert_eq!(term.utf16().as_ref(), [0xd83d]);
76/// assert!(term.into_string().is_err());
77/// ```
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub struct TokenTerm(Representation);
80
81impl TokenTerm {
82    pub(crate) fn characters(&self) -> impl Iterator<Item = Result<char, u16>> + Clone + '_ {
83        match &self.0 {
84            Representation::Unicode(text) => Characters::Unicode(text.chars()),
85            Representation::UTF16(units) => {
86                Characters::UTF16(char::decode_utf16(units.iter().copied()))
87            }
88        }
89    }
90
91    pub fn from_utf16(units: Vec<u16>) -> Self {
92        match String::from_utf16(&units) {
93            Ok(text) => Self::from(text),
94            Err(_) => Self(Representation::UTF16(units)),
95        }
96    }
97
98    pub fn as_str(&self) -> Option<&str> {
99        match &self.0 {
100            Representation::Unicode(text) => Some(text),
101            Representation::UTF16(_) => None,
102        }
103    }
104
105    pub fn utf16(&self) -> Cow<'_, [u16]> {
106        match &self.0 {
107            Representation::Unicode(_) => Cow::Owned(self.utf16_units().collect()),
108            Representation::UTF16(units) => Cow::Borrowed(units),
109        }
110    }
111
112    pub(crate) fn utf16_units(&self) -> impl Iterator<Item = u16> + Clone + '_ {
113        match &self.0 {
114            Representation::Unicode(text) => UTF16Units::Unicode(text.encode_utf16()),
115            Representation::UTF16(units) => UTF16Units::Raw(units.iter().copied()),
116        }
117    }
118
119    pub fn into_utf16(self) -> Vec<u16> {
120        match self.0 {
121            Representation::Unicode(text) => text.encode_utf16().collect(),
122            Representation::UTF16(units) => units,
123        }
124    }
125
126    /// Project to a Unicode string, returning an error instead of replacing unpaired units.
127    pub fn into_string(self) -> AnalysisResult<String> {
128        match self.0 {
129            Representation::Unicode(text) => Ok(text),
130            Representation::UTF16(units) => {
131                let unit = char::decode_utf16(units)
132                    .find_map(Result::err)
133                    .expect("non-scalar representation")
134                    .unpaired_surrogate();
135                Err(AnalysisError::UnpairedTokenSurrogate { unit })
136            }
137        }
138    }
139
140    /// Count each scalar or isolated surrogate as one character.
141    pub fn character_count(&self) -> usize {
142        match &self.0 {
143            Representation::Unicode(text) => text.chars().count(),
144            Representation::UTF16(units) => char::decode_utf16(units.iter().copied()).count(),
145        }
146    }
147
148    pub fn utf16_len(&self) -> usize {
149        match &self.0 {
150            Representation::Unicode(text) => text.encode_utf16().count(),
151            Representation::UTF16(units) => units.len(),
152        }
153    }
154
155    #[cfg(test)]
156    pub(crate) fn map_unicode(&self, transform: impl Fn(&str) -> String) -> Self {
157        if let Some(text) = self.as_str() {
158            return Self::from(transform(text));
159        }
160        let mut output = Vec::new();
161        let mut text = String::new();
162        for character in char::decode_utf16(self.utf16().iter().copied()) {
163            match character {
164                Ok(character) => text.push(character),
165                Err(error) => {
166                    output.extend(transform(&text).encode_utf16());
167                    text.clear();
168                    output.push(error.unpaired_surrogate());
169                }
170            }
171        }
172        output.extend(transform(&text).encode_utf16());
173        Self::from_utf16(output)
174    }
175
176    #[cfg(test)]
177    pub(crate) fn boundaries(&self) -> Vec<usize> {
178        match &self.0 {
179            Representation::Unicode(text) => text
180                .char_indices()
181                .map(|(offset, _)| offset)
182                .chain(std::iter::once(text.len()))
183                .collect(),
184            Representation::UTF16(units) => {
185                let mut boundaries = vec![0];
186                let mut offset = 0;
187                for character in char::decode_utf16(units.iter().copied()) {
188                    offset += character.map_or(1, char::len_utf16);
189                    boundaries.push(offset);
190                }
191                boundaries
192            }
193        }
194    }
195
196    #[cfg(test)]
197    pub(crate) fn substring(&self, range: Range<usize>) -> Self {
198        match &self.0 {
199            Representation::Unicode(text) => Self::from(text[range].to_owned()),
200            Representation::UTF16(units) => Self::from_utf16(units[range].to_vec()),
201        }
202    }
203}
204
205impl From<String> for TokenTerm {
206    fn from(value: String) -> Self {
207        Self(Representation::Unicode(value))
208    }
209}
210
211impl From<&str> for TokenTerm {
212    fn from(value: &str) -> Self {
213        Self::from(value.to_owned())
214    }
215}
216
217impl PartialEq<str> for TokenTerm {
218    fn eq(&self, other: &str) -> bool {
219        self.as_str() == Some(other)
220    }
221}
222
223impl PartialEq<&str> for TokenTerm {
224    fn eq(&self, other: &&str) -> bool {
225        self == *other
226    }
227}
228
229#[derive(Serialize, Deserialize)]
230#[serde(deny_unknown_fields)]
231struct RawUnits {
232    utf16: Vec<u16>,
233}
234
235impl Serialize for TokenTerm {
236    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
237        match &self.0 {
238            Representation::Unicode(text) => text.serialize(serializer),
239            Representation::UTF16(units) => {
240                use serde::ser::SerializeStruct;
241                let mut value = serializer.serialize_struct("RawUnits", 1)?;
242                value.serialize_field("utf16", units)?;
243                value.end()
244            }
245        }
246    }
247}
248
249impl<'de> Deserialize<'de> for TokenTerm {
250    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
251        #[derive(Deserialize)]
252        #[serde(untagged)]
253        enum Value {
254            Unicode(String),
255            UTF16(RawUnits),
256        }
257        Ok(match Value::deserialize(deserializer)? {
258            Value::Unicode(text) => Self::from(text),
259            Value::UTF16(raw) => Self::from_utf16(raw.utf16),
260        })
261    }
262}