Skip to main content

uqa_analysis/
descriptor.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Versioned resolved analyzer inputs, runtime profiles, and canonical JSON identity.
8
9use std::{collections::BTreeMap, sync::Arc};
10
11use serde::{Deserialize, Serialize};
12use serde_json::{value::RawValue, Value};
13
14use crate::{AnalysisError, AnalysisResult, Analyzer};
15
16mod config;
17#[cfg(feature = "kuromoji")]
18pub(crate) use config::check_config as check_configuration;
19mod hash;
20mod json;
21pub(crate) mod limits;
22mod profiles;
23
24pub use hash::AnalyzerFingerprint;
25pub use limits::AnalyzerLimits;
26use profiles::RuntimeProfiles;
27
28const FORMAT: &str = "uqa-analyzer";
29const FORMAT_VERSION: u32 = 1;
30const ALGORITHM_REVISION: u32 = 1;
31const SOURCE_MAPPING_REVISION: u32 = 1;
32
33/// The declared field-length policy contributes to analyzer revision identity.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum TokenLengthPolicy {
37    /// Count every emitted token, including stacked alternatives.
38    EmittedTokens,
39    /// Count only emitted tokens with a positive position increment.
40    DiscountOverlaps,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45struct DescriptorData {
46    format: String,
47    format_version: u32,
48    algorithm_revision: u32,
49    source_mapping_revision: u32,
50    length_policy: TokenLengthPolicy,
51    pipeline: Value,
52    runtime_profiles: RuntimeProfiles,
53}
54
55#[derive(Serialize, Deserialize)]
56#[serde(deny_unknown_fields)]
57struct Wire {
58    descriptor: DescriptorData,
59    fingerprint: AnalyzerFingerprint,
60}
61
62/// Immutable resolved JSON with a verified fingerprint and explicit compatibility revisions.
63#[derive(Debug)]
64pub struct AnalyzerDescriptor {
65    data: DescriptorData,
66    fingerprint: AnalyzerFingerprint,
67    wire: Box<RawValue>,
68}
69
70pub(crate) struct ResolvedDescriptor {
71    pub descriptor: Arc<AnalyzerDescriptor>,
72    #[cfg(feature = "nori")]
73    pub nori: crate::nori::pipeline::ResolvedNoriPipeline,
74    #[cfg(feature = "kuromoji")]
75    pub kuromoji: crate::kuromoji::pipeline::ResolvedKuromojiPipeline,
76}
77
78impl Serialize for AnalyzerDescriptor {
79    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
80        self.wire.serialize(serializer)
81    }
82}
83
84impl AnalyzerDescriptor {
85    pub fn resolve(
86        config: &Analyzer,
87        length_policy: TokenLengthPolicy,
88        limits: AnalyzerLimits,
89    ) -> AnalysisResult<Arc<Self>> {
90        Ok(Self::resolve_inputs(
91            config,
92            length_policy,
93            limits,
94            #[cfg(feature = "nori")]
95            &crate::nori::NoriResources::default(),
96            #[cfg(feature = "kuromoji")]
97            &crate::kuromoji::KuromojiResources::default(),
98        )?
99        .descriptor)
100    }
101
102    pub(crate) fn resolve_inputs(
103        config: &Analyzer,
104        length_policy: TokenLengthPolicy,
105        limits: AnalyzerLimits,
106        #[cfg(feature = "nori")] resources: &crate::nori::NoriResources,
107        #[cfg(feature = "kuromoji")] kuromoji_resources: &crate::kuromoji::KuromojiResources,
108    ) -> AnalysisResult<ResolvedDescriptor> {
109        config::check_config(config, limits)?;
110        let profiles = RuntimeProfiles::resolve(config)?;
111        #[cfg(any(feature = "nori", feature = "kuromoji"))]
112        let mut resolved_config = config.clone();
113        #[cfg(feature = "nori")]
114        let nori =
115            crate::nori::pipeline::ResolvedNoriPipeline::resolve(&mut resolved_config, resources)?;
116        #[cfg(feature = "kuromoji")]
117        let kuromoji = crate::kuromoji::pipeline::ResolvedKuromojiPipeline::resolve(
118            &mut resolved_config,
119            kuromoji_resources,
120            limits,
121        )?;
122        #[cfg(any(feature = "nori", feature = "kuromoji"))]
123        let config = &resolved_config;
124        let pipeline = config::snapshot(config, limits)?;
125        let descriptor = Self::finish(
126            DescriptorData {
127                format: FORMAT.into(),
128                format_version: FORMAT_VERSION,
129                algorithm_revision: ALGORITHM_REVISION,
130                source_mapping_revision: SOURCE_MAPPING_REVISION,
131                length_policy,
132                pipeline,
133                runtime_profiles: profiles,
134            },
135            limits,
136        )?;
137        Ok(ResolvedDescriptor {
138            descriptor,
139            #[cfg(feature = "nori")]
140            nori,
141            #[cfg(feature = "kuromoji")]
142            kuromoji,
143        })
144    }
145
146    /// Restore resolved inputs without opening any file or substituting current mutable definitions.
147    pub fn from_json(json: &str, limits: AnalyzerLimits) -> AnalysisResult<Arc<Self>> {
148        limits::check_limit(
149            "analyzer descriptor bytes",
150            json.len(),
151            limits.max_descriptor_bytes,
152        )?;
153        json::check_unique_keys(json)?;
154        let wire: Wire = serde_json::from_str(json)?;
155        let data_bytes = limits::encode(
156            &canonical(&serde_json::to_value(&wire.descriptor)?),
157            limits.max_descriptor_bytes,
158            true,
159        )?;
160        let fingerprint = AnalyzerFingerprint::digest(&data_bytes);
161        if wire.fingerprint != fingerprint {
162            return Err(AnalysisError::DescriptorFingerprint {
163                expected: wire.fingerprint,
164                actual: fingerprint,
165            });
166        }
167        Self::check_revision(&wire.descriptor)?;
168        let config = config::restore(&wire.descriptor.pipeline, limits)?;
169        if RuntimeProfiles::resolve(&config)? != wire.descriptor.runtime_profiles {
170            return Err(invalid(
171                "runtime Unicode or regular-expression profile differs",
172            ));
173        }
174        Self::finish(wire.descriptor, limits)
175    }
176
177    fn finish(data: DescriptorData, limits: AnalyzerLimits) -> AnalysisResult<Arc<Self>> {
178        let bytes = limits::encode(
179            &canonical(&serde_json::to_value(&data)?),
180            limits.max_descriptor_bytes,
181            true,
182        )?;
183        let fingerprint = AnalyzerFingerprint::digest(&bytes);
184        let wire = canonical(&serde_json::to_value(Wire {
185            descriptor: data.clone(),
186            fingerprint,
187        })?);
188        let bytes = limits::encode(&wire, limits.max_descriptor_bytes, true)?;
189        let json = String::from_utf8(bytes).expect("JSON serializer returns UTF-8");
190        Ok(Arc::new(Self {
191            data,
192            fingerprint,
193            wire: RawValue::from_string(json)?,
194        }))
195    }
196
197    fn check_revision(data: &DescriptorData) -> AnalysisResult<()> {
198        if data.format != FORMAT {
199            return Err(invalid("unknown descriptor format"));
200        }
201        for (component, expected, actual) in [
202            ("format", FORMAT_VERSION, data.format_version),
203            ("algorithm", ALGORITHM_REVISION, data.algorithm_revision),
204            (
205                "source mapping",
206                SOURCE_MAPPING_REVISION,
207                data.source_mapping_revision,
208            ),
209        ] {
210            if expected != actual {
211                return Err(AnalysisError::DescriptorRevision {
212                    component,
213                    expected,
214                    actual,
215                });
216            }
217        }
218        Ok(())
219    }
220
221    pub fn fingerprint(&self) -> AnalyzerFingerprint {
222        self.fingerprint
223    }
224    pub fn length_policy(&self) -> TokenLengthPolicy {
225        self.data.length_policy
226    }
227    pub fn canonical_json(&self) -> &str {
228        self.wire.get()
229    }
230
231    /// Return the resolved configuration for diagnostics or compatibility APIs. Execute a retained compiled handle when exact resource ownership is required.
232    pub fn configuration(&self) -> AnalysisResult<Analyzer> {
233        Ok(serde_json::from_value(self.data.pipeline.clone())?)
234    }
235
236    pub(crate) fn validate_limits(&self, limits: AnalyzerLimits) -> AnalysisResult<()> {
237        limits::check_limit(
238            "analyzer descriptor bytes",
239            self.canonical_json().len(),
240            limits.max_descriptor_bytes,
241        )?;
242        config::check_config(&self.configuration()?, limits)
243    }
244}
245
246fn invalid(reason: &'static str) -> AnalysisError {
247    AnalysisError::Descriptor(reason)
248}
249
250fn canonical(value: &Value) -> Value {
251    match value {
252        Value::Object(object) => Value::Object(
253            object
254                .iter()
255                .map(|(key, value)| (key.clone(), canonical(value)))
256                .collect::<BTreeMap<_, _>>()
257                .into_iter()
258                .collect(),
259        ),
260        Value::Array(array) => Value::Array(array.iter().map(canonical).collect()),
261        _ => value.clone(),
262    }
263}