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