Skip to main content

powerio_core/
module.rs

1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, TryReserveError};
2use std::fmt;
3
4use serde_json::Value;
5
6use crate::validation::valid_nonempty_text;
7use crate::{
8    Diagnostic, DiagnosticId, Error, HistoryEntry, HistoryId, Producer, Source, SourceDescriptor,
9    SourceId, SourceMapEntry, SourceSpan,
10};
11
12#[derive(Debug)]
13struct ModuleRecords {
14    producer: Producer,
15    sources: Vec<SourceDescriptor>,
16    source_map: Vec<SourceMapEntry>,
17    diagnostics: Vec<Diagnostic>,
18    history: Vec<HistoryEntry>,
19    extensions: BTreeMap<String, Value>,
20    retained_source: Option<Source>,
21    /// Identity indexes maintained by the `add_*` methods, the only mutation
22    /// paths. Duplicate detection and span source resolution consult these
23    /// instead of scanning previously inserted records, so populating a module
24    /// with N records costs O(N) expected rather than O(N^2).
25    source_positions: HashMap<SourceId, usize>,
26    diagnostic_ids: HashSet<DiagnosticId>,
27    history_ids: HashSet<HistoryId>,
28}
29
30impl Default for ModuleRecords {
31    fn default() -> Self {
32        Self {
33            producer: Producer::powerio(),
34            sources: Vec::new(),
35            source_map: Vec::new(),
36            diagnostics: Vec::new(),
37            history: Vec::new(),
38            extensions: BTreeMap::new(),
39            retained_source: None,
40            source_positions: HashMap::new(),
41            diagnostic_ids: HashSet::new(),
42            history_ids: HashSet::new(),
43        }
44    }
45}
46
47fn allocation_refused(cause: TryReserveError) -> Error {
48    Error::new(
49        &crate::codes::REQUEST_RECORD_ALLOCATION_REFUSED,
50        "cannot reserve the record identity index",
51    )
52    .with_cause(cause)
53}
54
55/// One typed PowerIO compiler unit.
56///
57/// `T` has no PowerIO marker bound. Dynamic parsing and stored JSON register a
58/// finite set elsewhere, while Rust applications can use any value here.
59pub struct PioModule<T> {
60    value: T,
61    records: ModuleRecords,
62}
63
64impl<T> PioModule<T> {
65    #[must_use]
66    pub fn new(value: T) -> Self {
67        Self {
68            value,
69            records: ModuleRecords::default(),
70        }
71    }
72
73    #[must_use]
74    pub const fn value(&self) -> &T {
75        &self.value
76    }
77
78    #[must_use]
79    pub fn into_value(self) -> T {
80        self.value
81    }
82
83    #[must_use]
84    pub const fn producer(&self) -> &Producer {
85        &self.records.producer
86    }
87
88    #[must_use]
89    pub fn sources(&self) -> &[SourceDescriptor] {
90        &self.records.sources
91    }
92
93    #[must_use]
94    pub fn source_map(&self) -> &[SourceMapEntry] {
95        &self.records.source_map
96    }
97
98    #[must_use]
99    pub fn diagnostics(&self) -> &[Diagnostic] {
100        &self.records.diagnostics
101    }
102
103    #[must_use]
104    pub fn history(&self) -> &[HistoryEntry] {
105        &self.records.history
106    }
107
108    #[must_use]
109    pub const fn extensions(&self) -> &BTreeMap<String, Value> {
110        &self.records.extensions
111    }
112
113    #[must_use]
114    pub const fn source(&self) -> Option<&Source> {
115        self.records.retained_source.as_ref()
116    }
117
118    #[must_use]
119    pub fn with_producer(mut self, producer: Producer) -> Self {
120        self.records.producer = producer;
121        self
122    }
123
124    #[must_use]
125    pub fn with_source(mut self, source: Source) -> Self {
126        self.records.retained_source = Some(source);
127        self
128    }
129
130    /// Drop the retained source owner: the operation that calls this changed
131    /// the value, so a same format write must serialize the value rather than
132    /// echo bytes the value no longer matches. Descriptors, diagnostics, and
133    /// history stay.
134    #[must_use]
135    pub fn sever_source(mut self) -> Self {
136        self.records.retained_source = None;
137        self
138    }
139
140    /// Append a finding, applying the same duplicate identity and span
141    /// reference checks as [`PioModule::add_diagnostic`]. There is no unchecked
142    /// path onto a module's records.
143    pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Result<Self, Error> {
144        self.add_diagnostic(diagnostic)?;
145        Ok(self)
146    }
147
148    pub fn add_source_descriptor(&mut self, source: SourceDescriptor) -> Result<(), Error> {
149        if self.records.sources.len() >= crate::validation::MAX_MODULE_SOURCES {
150            return Err(record_cap("sources", crate::validation::MAX_MODULE_SOURCES));
151        }
152        if self.records.source_positions.contains_key(source.id()) {
153            return Err(Error::new(
154                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
155                format!("duplicate source ID `{}`", source.id()),
156            ));
157        }
158        self.records
159            .source_positions
160            .try_reserve(1)
161            .map_err(allocation_refused)?;
162        self.records
163            .source_positions
164            .insert(source.id().clone(), self.records.sources.len());
165        self.records.sources.push(source);
166        Ok(())
167    }
168
169    pub fn add_source_map_entry(&mut self, entry: SourceMapEntry) -> Result<(), Error> {
170        if self.records.source_map.len() >= crate::validation::MAX_MODULE_SOURCE_MAP_ENTRIES {
171            return Err(record_cap(
172                "source map entries",
173                crate::validation::MAX_MODULE_SOURCE_MAP_ENTRIES,
174            ));
175        }
176        for span in entry.spans() {
177            validate_span(span, &self.records.sources, &self.records.source_positions)?;
178        }
179        self.records.source_map.push(entry);
180        Ok(())
181    }
182
183    pub fn add_diagnostic(&mut self, diagnostic: Diagnostic) -> Result<(), Error> {
184        if self.records.diagnostics.len() >= crate::validation::MAX_MODULE_DIAGNOSTICS {
185            return Err(record_cap(
186                "diagnostics",
187                crate::validation::MAX_MODULE_DIAGNOSTICS,
188            ));
189        }
190        if let Some(id) = diagnostic.id()
191            && self.records.diagnostic_ids.contains(id)
192        {
193            return Err(Error::new(
194                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
195                format!("duplicate diagnostic ID `{id}`"),
196            ));
197        }
198        for span in diagnostic.spans() {
199            validate_span(span, &self.records.sources, &self.records.source_positions)?;
200        }
201        if let Some(id) = diagnostic.id() {
202            self.records
203                .diagnostic_ids
204                .try_reserve(1)
205                .map_err(allocation_refused)?;
206            self.records.diagnostic_ids.insert(id.clone());
207        }
208        self.records.diagnostics.push(diagnostic);
209        Ok(())
210    }
211
212    pub fn add_history_entry(&mut self, entry: HistoryEntry) -> Result<(), Error> {
213        if self.records.history.len() >= crate::validation::MAX_MODULE_HISTORY_ENTRIES {
214            return Err(record_cap(
215                "history entries",
216                crate::validation::MAX_MODULE_HISTORY_ENTRIES,
217            ));
218        }
219        if self.records.history_ids.contains(entry.id()) {
220            return Err(Error::new(
221                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
222                format!("duplicate history ID `{}`", entry.id()),
223            ));
224        }
225        self.records
226            .history_ids
227            .try_reserve(1)
228            .map_err(allocation_refused)?;
229        self.records.history_ids.insert(entry.id().clone());
230        self.records.history.push(entry);
231        Ok(())
232    }
233
234    pub fn insert_extension(
235        &mut self,
236        namespace: impl Into<String>,
237        value: Value,
238    ) -> Result<Option<Value>, Error> {
239        let namespace = namespace.into();
240        if !valid_extension_namespace(&namespace) {
241            return Err(Error::new(
242                &crate::codes::REQUEST_RECORD_INVALID_EXTENSION,
243                "extension keys must be bounded namespaced strings",
244            ));
245        }
246        if self.records.extensions.len() >= crate::validation::MAX_MODULE_EXTENSION_KEYS
247            && !self.records.extensions.contains_key(&namespace)
248        {
249            return Err(Error::new(
250                &crate::codes::REQUEST_RECORD_TOO_LARGE,
251                format!(
252                    "a module carries at most {} extension keys",
253                    crate::validation::MAX_MODULE_EXTENSION_KEYS
254                ),
255            ));
256        }
257        Ok(self.records.extensions.insert(namespace, value))
258    }
259
260    /// Verify cross-record references that cannot be checked by constructors.
261    pub fn verify_records(&self) -> Result<(), Error> {
262        let source_ids: BTreeSet<_> = self
263            .records
264            .sources
265            .iter()
266            .map(SourceDescriptor::id)
267            .collect();
268        if source_ids.len() != self.records.sources.len() {
269            return Err(Error::new(
270                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
271                "module contains duplicate source IDs",
272            ));
273        }
274        for entry in &self.records.source_map {
275            for span in entry.spans() {
276                validate_span(span, &self.records.sources, &self.records.source_positions)?;
277            }
278        }
279
280        let diagnostic_ids: BTreeSet<&DiagnosticId> = self
281            .records
282            .diagnostics
283            .iter()
284            .filter_map(Diagnostic::id)
285            .collect();
286        if diagnostic_ids.len()
287            != self
288                .records
289                .diagnostics
290                .iter()
291                .filter(|diagnostic| diagnostic.id().is_some())
292                .count()
293        {
294            return Err(Error::new(
295                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
296                "module contains duplicate diagnostic IDs",
297            ));
298        }
299        for diagnostic in &self.records.diagnostics {
300            for span in diagnostic.spans() {
301                validate_span(span, &self.records.sources, &self.records.source_positions)?;
302            }
303            for related in diagnostic.related() {
304                if !diagnostic_ids.contains(related) {
305                    return Err(Error::new(
306                        &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
307                        format!("diagnostic refers to unknown diagnostic `{related}`"),
308                    ));
309                }
310            }
311        }
312
313        let history_ids: BTreeSet<_> = self.records.history.iter().map(HistoryEntry::id).collect();
314        if history_ids.len() != self.records.history.len() {
315            return Err(Error::new(
316                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
317                "module contains duplicate history IDs",
318            ));
319        }
320        if self
321            .records
322            .extensions
323            .keys()
324            .any(|namespace| !valid_extension_namespace(namespace))
325        {
326            return Err(Error::new(
327                &crate::codes::REQUEST_RECORD_INVALID_EXTENSION,
328                "module contains an extension key that is not namespaced",
329            ));
330        }
331        Ok(())
332    }
333
334    /// Drop records that address the old value: the operation that calls this
335    /// replaced the value with one of a different kind, so RFC 6901 targets
336    /// into the old value no longer identify anything. Every diagnostic
337    /// keeps its code, message, severity, and spans but loses its target,
338    /// and the source map (whose entries are keyed by such targets) is
339    /// cleared. Pair this with [`PioModule::map_value`] in a kind changing
340    /// transform so the module still serializes.
341    pub fn sever_value_targets(&mut self) {
342        for diagnostic in &mut self.records.diagnostics {
343            diagnostic.clear_target();
344        }
345        self.records.source_map.clear();
346    }
347
348    /// Move the value and every module record into another typed module.
349    #[must_use]
350    pub fn map_value<U>(self, convert: impl FnOnce(T) -> U) -> PioModule<U> {
351        PioModule {
352            value: convert(self.value),
353            records: self.records,
354        }
355    }
356
357    /// Move the value through a fallible conversion, keeping every module
358    /// record on success. On failure the conversion's error is returned and
359    /// the records are dropped with the consumed value; a caller that must
360    /// keep the source or findings on the failure route takes them off the
361    /// module first ([`PioModule::take_source`], [`PioModule::diagnostics`]).
362    pub fn try_map_value<U, E>(
363        self,
364        convert: impl FnOnce(T) -> Result<U, E>,
365    ) -> Result<PioModule<U>, E> {
366        let Self { value, records } = self;
367        Ok(PioModule {
368            value: convert(value)?,
369            records,
370        })
371    }
372
373    /// Take the retained source owner off the module, leaving descriptors,
374    /// diagnostics, and history in place. The module then reads as
375    /// constructed in memory until a source is reattached.
376    pub fn take_source(&mut self) -> Option<Source> {
377        self.records.retained_source.take()
378    }
379
380    /// Assemble the module a parser returns: the typed value, one descriptor
381    /// per acquired buffer of the retained source, and the reader's findings.
382    ///
383    /// # Errors
384    /// A duplicate acquired buffer identity, an invalid buffer name, or a
385    /// finding that fails the record checks of [`PioModule::add_diagnostic`].
386    pub fn parsed(value: T, source: Source, diagnostics: Vec<Diagnostic>) -> Result<Self, Error> {
387        let mut module = Self::new(value);
388        for buffer in source.acquired_buffers() {
389            // The stored descriptor names the file, never the local path,
390            // and carries the resolved format so a same format write can
391            // default to it.
392            let name = std::path::Path::new(buffer.name())
393                .file_name()
394                .and_then(|n| n.to_str())
395                .unwrap_or_else(|| buffer.name());
396            let mut descriptor =
397                SourceDescriptor::new(buffer.id().clone(), name, buffer.bytes().len() as u64)?;
398            if let Some(format) = source.format() {
399                descriptor = descriptor.with_format(format.clone());
400            }
401            module.add_source_descriptor(descriptor)?;
402        }
403        let mut module = module.with_source(source);
404        for record in diagnostics {
405            module.add_diagnostic(record)?;
406        }
407        Ok(module)
408    }
409
410    /// Internal cross-crate support for recoverable consuming narrowing.
411    #[doc(hidden)]
412    // Boxing the failure would allocate and violate the recoverable no-copy
413    // narrowing rule. The caller gets the original module by value.
414    #[allow(clippy::result_large_err)]
415    pub fn __try_map_value<U>(
416        self,
417        convert: impl FnOnce(T) -> Result<U, T>,
418    ) -> Result<PioModule<U>, PioModule<T>> {
419        let Self { value, records } = self;
420        match convert(value) {
421            Ok(value) => Ok(PioModule { value, records }),
422            Err(value) => Err(PioModule { value, records }),
423        }
424    }
425}
426
427impl<T: fmt::Debug> fmt::Debug for PioModule<T> {
428    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
429        formatter
430            .debug_struct("PioModule")
431            .field("value", &self.value)
432            .field("records", &self.records)
433            .finish()
434    }
435}
436
437/// The uniform record count refusal every adder applies at its maximum.
438fn record_cap(what: &str, max: usize) -> Error {
439    Error::new(
440        &crate::codes::REQUEST_RECORD_TOO_LARGE,
441        format!("the module already holds the maximum {max} {what}"),
442    )
443}
444
445fn validate_span(
446    span: &SourceSpan,
447    sources: &[SourceDescriptor],
448    positions: &HashMap<SourceId, usize>,
449) -> Result<(), Error> {
450    let Some(source) = positions
451        .get(span.source())
452        .and_then(|position| sources.get(*position))
453    else {
454        return Err(Error::new(
455            &crate::codes::REQUEST_RECORD_INVALID_SPAN,
456            format!("source span refers to unknown source `{}`", span.source()),
457        ));
458    };
459    if span.byte_end() > source.byte_length() {
460        return Err(Error::new(
461            &crate::codes::REQUEST_RECORD_INVALID_SPAN,
462            format!(
463                "source span end {} exceeds source `{}` length {}",
464                span.byte_end(),
465                span.source(),
466                source.byte_length()
467            ),
468        ));
469    }
470    Ok(())
471}
472
473fn valid_extension_namespace(namespace: &str) -> bool {
474    valid_nonempty_text(namespace)
475        && !namespace.starts_with('.')
476        && !namespace.ends_with('.')
477        && namespace.contains('.')
478        && namespace.split('.').all(|segment| !segment.is_empty())
479}
480
481#[cfg(test)]
482mod tests {
483    use std::rc::Rc;
484    use std::sync::Arc;
485
486    use super::*;
487    use crate::{DiagnosticSeverity, HistoryKind, SourceRelation};
488
489    #[test]
490    fn modules_accept_unregistered_application_values() {
491        struct ApplicationValue(Rc<()>);
492        let module = PioModule::new(ApplicationValue(Rc::new(())));
493        assert_eq!(Rc::strong_count(&module.value().0), 1);
494    }
495
496    #[test]
497    fn map_value_moves_records_and_retained_source_without_allocation() {
498        let bytes: Arc<[u8]> = b"source".as_slice().into();
499        let source = Source::from_bytes("case.m", Arc::clone(&bytes)).unwrap();
500        let diagnostic = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "kept");
501        let module = PioModule::new(String::from("value"))
502            .with_source(source)
503            .with_diagnostic(diagnostic)
504            .unwrap();
505        let diagnostics_pointer = module.diagnostics().as_ptr();
506        let source_pointer = module
507            .source()
508            .unwrap()
509            .primary_buffer()
510            .unwrap()
511            .bytes()
512            .as_ptr();
513        let mapped = module.map_value(String::into_bytes);
514        assert_eq!(mapped.value(), b"value");
515        assert_eq!(mapped.diagnostics().as_ptr(), diagnostics_pointer);
516        assert_eq!(
517            mapped
518                .source()
519                .unwrap()
520                .primary_buffer()
521                .unwrap()
522                .bytes()
523                .as_ptr(),
524            source_pointer
525        );
526    }
527
528    #[test]
529    fn failed_try_map_returns_the_original_module_and_records() {
530        let module = PioModule::new(String::from("value"))
531            .with_diagnostic(Diagnostic::of(
532                &crate::codes::VALIDATE_TIME_SERIES_SHAPE,
533                "kept",
534            ))
535            .unwrap();
536        let diagnostics_pointer = module.diagnostics().as_ptr();
537        let recovered = module
538            .__try_map_value::<usize>(Err)
539            .expect_err("conversion fails");
540        assert_eq!(recovered.value(), "value");
541        assert_eq!(recovered.diagnostics().as_ptr(), diagnostics_pointer);
542    }
543
544    #[test]
545    fn record_references_and_namespaces_are_checked() {
546        let source_id = SourceId::new("input").unwrap();
547        let mut module = PioModule::new(1_u8);
548        module
549            .add_source_descriptor(SourceDescriptor::new(source_id.clone(), "case.m", 4).unwrap())
550            .unwrap();
551        let span = SourceSpan::new(source_id, 0, 4).unwrap();
552        module
553            .add_source_map_entry(
554                SourceMapEntry::new("/value", SourceRelation::Exact, vec![span.clone()]).unwrap(),
555            )
556            .unwrap();
557        module
558            .add_diagnostic(
559                Diagnostic::new(
560                    crate::DiagnosticCode::new("PARTNER.TEST.FINDING").unwrap(),
561                    DiagnosticSeverity::Note,
562                    "note",
563                )
564                .with_id(DiagnosticId::new("d1").unwrap())
565                .with_span(span)
566                .unwrap(),
567            )
568            .unwrap();
569        module
570            .add_history_entry(
571                HistoryEntry::new(HistoryId::new("h1").unwrap(), HistoryKind::Parse, "parse")
572                    .unwrap(),
573            )
574            .unwrap();
575        module
576            .insert_extension("org.example", Value::Bool(true))
577            .unwrap();
578        assert!(module.verify_records().is_ok());
579        assert!(
580            module
581                .insert_extension("not-namespaced", Value::Null)
582                .is_err()
583        );
584
585        let invalid = SourceSpan::new(SourceId::new("input").unwrap(), 0, 5).unwrap();
586        assert!(
587            module
588                .add_source_map_entry(
589                    SourceMapEntry::new("", SourceRelation::Exact, vec![invalid]).unwrap()
590                )
591                .is_err()
592        );
593    }
594}