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, SourceRelation, SourceSpan,
10};
11
12#[derive(Clone, Debug)]
13struct ModuleRecords {
14    producer: Producer,
15    sources: Vec<SourceDescriptor>,
16    source_map: Vec<SourceMapEntry>,
17    history: Vec<HistoryEntry>,
18    extensions: BTreeMap<String, Value>,
19    retained_source: Option<Source>,
20    /// Identity indexes maintained by the `add_*` methods, the only mutation
21    /// paths. Duplicate detection and span source resolution consult these
22    /// instead of scanning previously inserted records, so populating a module
23    /// with N records costs O(N) expected rather than O(N^2).
24    source_positions: HashMap<SourceId, usize>,
25    diagnostic_ids: HashSet<DiagnosticId>,
26    history_ids: HashSet<HistoryId>,
27}
28
29impl Default for ModuleRecords {
30    fn default() -> Self {
31        Self {
32            producer: Producer::powerio(),
33            sources: Vec::new(),
34            source_map: Vec::new(),
35            history: Vec::new(),
36            extensions: BTreeMap::new(),
37            retained_source: None,
38            source_positions: HashMap::new(),
39            diagnostic_ids: HashSet::new(),
40            history_ids: HashSet::new(),
41        }
42    }
43}
44
45fn allocation_refused(cause: TryReserveError) -> Error {
46    Error::new(
47        &crate::codes::REQUEST_RECORD_ALLOCATION_REFUSED,
48        "cannot reserve the record identity index",
49    )
50    .with_cause(cause)
51}
52
53/// One typed PowerIO compiler unit.
54///
55/// `T` has no PowerIO marker bound. Dynamic parsing and stored JSON register a
56/// finite set elsewhere, while Rust applications can use any value here.
57pub struct PioModule<T> {
58    value: T,
59    diagnostics: Vec<Diagnostic>,
60    records: ModuleRecords,
61}
62
63impl<T: Clone> Clone for PioModule<T> {
64    fn clone(&self) -> Self {
65        Self {
66            value: self.value.clone(),
67            diagnostics: self.diagnostics.clone(),
68            records: self.records.clone(),
69        }
70    }
71}
72
73impl<T> PioModule<T> {
74    #[must_use]
75    pub fn new(value: T) -> Self {
76        Self {
77            value,
78            diagnostics: Vec::new(),
79            records: ModuleRecords::default(),
80        }
81    }
82
83    #[must_use]
84    pub fn into_value(self) -> T {
85        self.value
86    }
87
88    /// The typed value carried by this module.
89    #[must_use]
90    pub const fn value(&self) -> &T {
91        &self.value
92    }
93
94    /// The typed value, for an edit in place.
95    ///
96    /// Taking the reference drops the retained source owner and the source
97    /// map: bytes and RFC 6901 targets that describe the pre-edit value no
98    /// longer describe what the module carries, so a same format write
99    /// serializes the value instead of emitting them. Descriptors,
100    /// diagnostics, and history stay. [`PioModule::stage_edit`] is the atomic
101    /// form, which leaves the module untouched when an edit does not complete.
102    pub fn value_mut(&mut self) -> &mut T {
103        self.records.retained_source = None;
104        self.sever_value_targets();
105        &mut self.value
106    }
107
108    /// Diagnostics produced while acquiring, validating, or deriving the
109    /// value, in the order they were recorded.
110    #[must_use]
111    pub fn diagnostics(&self) -> &[Diagnostic] {
112        &self.diagnostics
113    }
114
115    #[must_use]
116    pub const fn producer(&self) -> &Producer {
117        &self.records.producer
118    }
119
120    #[must_use]
121    pub fn sources(&self) -> &[SourceDescriptor] {
122        &self.records.sources
123    }
124
125    #[must_use]
126    pub fn source_map(&self) -> &[SourceMapEntry] {
127        &self.records.source_map
128    }
129
130    #[must_use]
131    pub fn history(&self) -> &[HistoryEntry] {
132        &self.records.history
133    }
134
135    #[must_use]
136    pub const fn extensions(&self) -> &BTreeMap<String, Value> {
137        &self.records.extensions
138    }
139
140    #[must_use]
141    pub const fn source(&self) -> Option<&Source> {
142        self.records.retained_source.as_ref()
143    }
144
145    #[must_use]
146    pub fn with_producer(mut self, producer: Producer) -> Self {
147        self.records.producer = producer;
148        self
149    }
150
151    #[must_use]
152    pub fn with_source(mut self, source: Source) -> Self {
153        self.records.retained_source = Some(source);
154        self
155    }
156
157    /// Drop the retained source owner: the operation that calls this changed
158    /// the value, so a same format write must serialize the value rather than
159    /// echo bytes the value no longer matches. Descriptors, diagnostics, and
160    /// history stay.
161    #[must_use]
162    pub fn sever_source(mut self) -> Self {
163        self.records.retained_source = None;
164        self
165    }
166
167    /// Append a finding, applying the same duplicate identity and span
168    /// reference checks as [`PioModule::add_diagnostic`]. There is no unchecked
169    /// path onto a module's records.
170    pub fn with_diagnostic(mut self, diagnostic: Diagnostic) -> Result<Self, Error> {
171        self.add_diagnostic(diagnostic)?;
172        Ok(self)
173    }
174
175    pub fn add_source_descriptor(&mut self, source: SourceDescriptor) -> Result<(), Error> {
176        if self.records.sources.len() >= crate::validation::MAX_MODULE_SOURCES {
177            return Err(record_cap("sources", crate::validation::MAX_MODULE_SOURCES));
178        }
179        if self.records.source_positions.contains_key(source.id()) {
180            return Err(Error::new(
181                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
182                format!("duplicate source ID `{}`", source.id()),
183            ));
184        }
185        self.records
186            .source_positions
187            .try_reserve(1)
188            .map_err(allocation_refused)?;
189        self.records
190            .source_positions
191            .insert(source.id().clone(), self.records.sources.len());
192        self.records.sources.push(source);
193        Ok(())
194    }
195
196    pub fn add_source_map_entry(&mut self, entry: SourceMapEntry) -> Result<(), Error> {
197        if self.records.source_map.len() >= crate::validation::MAX_MODULE_SOURCE_MAP_ENTRIES {
198            return Err(record_cap(
199                "source map entries",
200                crate::validation::MAX_MODULE_SOURCE_MAP_ENTRIES,
201            ));
202        }
203        for span in entry.spans() {
204            validate_span(span, &self.records.sources, &self.records.source_positions)?;
205        }
206        self.records.source_map.push(entry);
207        Ok(())
208    }
209
210    pub fn add_diagnostic(&mut self, diagnostic: Diagnostic) -> Result<(), Error> {
211        if self.diagnostics.len() >= crate::validation::MAX_MODULE_DIAGNOSTICS {
212            return Err(record_cap(
213                "diagnostics",
214                crate::validation::MAX_MODULE_DIAGNOSTICS,
215            ));
216        }
217        if let Some(id) = diagnostic.id()
218            && self.records.diagnostic_ids.contains(id)
219        {
220            return Err(Error::new(
221                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
222                format!("duplicate diagnostic ID `{id}`"),
223            ));
224        }
225        for span in diagnostic.spans() {
226            validate_span(span, &self.records.sources, &self.records.source_positions)?;
227        }
228        if let Some(id) = diagnostic.id() {
229            self.records
230                .diagnostic_ids
231                .try_reserve(1)
232                .map_err(allocation_refused)?;
233            self.records.diagnostic_ids.insert(id.clone());
234        }
235        self.diagnostics.push(diagnostic);
236        Ok(())
237    }
238
239    pub fn add_history_entry(&mut self, entry: HistoryEntry) -> Result<(), Error> {
240        if self.records.history.len() >= crate::validation::MAX_MODULE_HISTORY_ENTRIES {
241            return Err(record_cap(
242                "history entries",
243                crate::validation::MAX_MODULE_HISTORY_ENTRIES,
244            ));
245        }
246        if self.records.history_ids.contains(entry.id()) {
247            return Err(Error::new(
248                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
249                format!("duplicate history ID `{}`", entry.id()),
250            ));
251        }
252        self.records
253            .history_ids
254            .try_reserve(1)
255            .map_err(allocation_refused)?;
256        self.records.history_ids.insert(entry.id().clone());
257        self.records.history.push(entry);
258        Ok(())
259    }
260
261    pub fn insert_extension(
262        &mut self,
263        namespace: impl Into<String>,
264        value: Value,
265    ) -> Result<Option<Value>, Error> {
266        let namespace = namespace.into();
267        if !valid_extension_namespace(&namespace) {
268            return Err(Error::new(
269                &crate::codes::REQUEST_RECORD_INVALID_EXTENSION,
270                "extension keys must be bounded namespaced strings",
271            ));
272        }
273        if self.records.extensions.len() >= crate::validation::MAX_MODULE_EXTENSION_KEYS
274            && !self.records.extensions.contains_key(&namespace)
275        {
276            return Err(Error::new(
277                &crate::codes::REQUEST_RECORD_TOO_LARGE,
278                format!(
279                    "a module carries at most {} extension keys",
280                    crate::validation::MAX_MODULE_EXTENSION_KEYS
281                ),
282            ));
283        }
284        Ok(self.records.extensions.insert(namespace, value))
285    }
286
287    /// Verify cross-record references that cannot be checked by constructors.
288    pub fn verify_records(&self) -> Result<(), Error> {
289        let source_ids: BTreeSet<_> = self
290            .records
291            .sources
292            .iter()
293            .map(SourceDescriptor::id)
294            .collect();
295        if source_ids.len() != self.records.sources.len() {
296            return Err(Error::new(
297                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
298                "module contains duplicate source IDs",
299            ));
300        }
301        for entry in &self.records.source_map {
302            for span in entry.spans() {
303                validate_span(span, &self.records.sources, &self.records.source_positions)?;
304            }
305        }
306
307        let diagnostic_ids: BTreeSet<&DiagnosticId> =
308            self.diagnostics.iter().filter_map(Diagnostic::id).collect();
309        if diagnostic_ids.len()
310            != self
311                .diagnostics
312                .iter()
313                .filter(|diagnostic| diagnostic.id().is_some())
314                .count()
315        {
316            return Err(Error::new(
317                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
318                "module contains duplicate diagnostic IDs",
319            ));
320        }
321        for diagnostic in &self.diagnostics {
322            for span in diagnostic.spans() {
323                validate_span(span, &self.records.sources, &self.records.source_positions)?;
324            }
325            for related in diagnostic.related() {
326                if !diagnostic_ids.contains(related) {
327                    return Err(Error::new(
328                        &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
329                        format!("diagnostic refers to unknown diagnostic `{related}`"),
330                    ));
331                }
332            }
333        }
334
335        let history_ids: BTreeSet<_> = self.records.history.iter().map(HistoryEntry::id).collect();
336        if history_ids.len() != self.records.history.len() {
337            return Err(Error::new(
338                &crate::codes::REQUEST_RECORD_DUPLICATE_ID,
339                "module contains duplicate history IDs",
340            ));
341        }
342        if self
343            .records
344            .extensions
345            .keys()
346            .any(|namespace| !valid_extension_namespace(namespace))
347        {
348            return Err(Error::new(
349                &crate::codes::REQUEST_RECORD_INVALID_EXTENSION,
350                "module contains an extension key that is not namespaced",
351            ));
352        }
353        Ok(())
354    }
355
356    /// Drop records that address the old value: the operation that calls this
357    /// replaced the value with one of a different kind, so RFC 6901 targets
358    /// into the old value no longer identify anything. Every diagnostic
359    /// keeps its code, message, severity, and spans but loses its target,
360    /// and the source map (whose entries are keyed by such targets) is
361    /// cleared. Pair this with [`PioModule::map_value`] in a kind changing
362    /// transform so the module still serializes.
363    pub fn sever_value_targets(&mut self) {
364        for diagnostic in &mut self.diagnostics {
365            diagnostic.clear_target();
366        }
367        self.records.source_map.clear();
368    }
369
370    /// Derive a semantically new value and record the operation that produced
371    /// it. Source descriptors, diagnostics, prior history, and extensions stay
372    /// with the module. Locators into the replaced value and retained bytes
373    /// that no longer encode the result are invalidated.
374    pub fn derive_value<U>(
375        self,
376        producer: Producer,
377        history: HistoryEntry,
378        derive: impl FnOnce(T) -> U,
379    ) -> Result<PioModule<U>, Error> {
380        self.try_derive_value(producer, history, |value| Ok(derive(value)))
381    }
382
383    /// Fallible form of [`PioModule::derive_value`].
384    pub fn try_derive_value<U>(
385        self,
386        producer: Producer,
387        history: HistoryEntry,
388        derive: impl FnOnce(T) -> Result<U, Error>,
389    ) -> Result<PioModule<U>, Error> {
390        let Self {
391            value,
392            mut diagnostics,
393            mut records,
394        } = self;
395        let value = derive(value)?;
396        records.producer = producer;
397        records.retained_source = None;
398        records.source_map.clear();
399        for diagnostic in &mut diagnostics {
400            diagnostic.clear_target();
401        }
402
403        let mut derived = PioModule {
404            value,
405            diagnostics,
406            records,
407        };
408        derived.add_history_entry(history)?;
409        derived.verify_records()?;
410        Ok(derived)
411    }
412
413    /// Parse a value that depends on this module and one additional source.
414    ///
415    /// Records that describe the input value are retargeted below
416    /// `input_target`. The additional source receives distinct source IDs,
417    /// becomes the retained source, and is mapped to the new value's root.
418    /// This is the shared module operation for readers whose document is not
419    /// self contained, such as a solution file that must be interpreted
420    /// against its calculation instance.
421    ///
422    /// # Errors
423    /// The derived value cannot be built, a target is not an RFC 6901
424    /// pointer, or the combined records exceed a module bound.
425    pub fn try_derive_from_source<U>(
426        self,
427        input_target: &str,
428        source: Source,
429        producer: Producer,
430        history: HistoryEntry,
431        derive: impl FnOnce(T, &Source) -> Result<(U, Vec<Diagnostic>), Error>,
432    ) -> Result<PioModule<U>, Error> {
433        // Validate the prefix before consuming the input value.
434        SourceMapEntry::new(input_target, SourceRelation::Transformed, Vec::new())?;
435
436        let Self {
437            value,
438            mut diagnostics,
439            mut records,
440        } = self;
441        let (value, mut source_diagnostics) = derive(value, &source)?;
442
443        for diagnostic in &mut diagnostics {
444            diagnostic.prefix_target(input_target)?;
445        }
446        let old_source_map = std::mem::take(&mut records.source_map);
447        for entry in old_source_map {
448            records.source_map.push(SourceMapEntry::new(
449                format!("{input_target}{}", entry.target()),
450                entry.relation(),
451                entry.spans().to_vec(),
452            )?);
453        }
454
455        let mut source_ids = HashMap::new();
456        let mut root_spans = Vec::new();
457        for (index, buffer) in source.acquired_buffers().into_iter().enumerate() {
458            let mut suffix = index + 1;
459            let id = loop {
460                let candidate = SourceId::new(format!("solution-source-{suffix}"))?;
461                if !records.source_positions.contains_key(&candidate)
462                    && !source_ids.values().any(|existing| existing == &candidate)
463                {
464                    break candidate;
465                }
466                suffix += 1;
467            };
468            source_ids.insert(buffer.id().clone(), id.clone());
469
470            let name = std::path::Path::new(buffer.name())
471                .file_name()
472                .and_then(|name| name.to_str())
473                .unwrap_or_else(|| buffer.name());
474            let mut descriptor =
475                SourceDescriptor::new(id.clone(), name, buffer.bytes().len() as u64)?;
476            if let Some(format) = source.format() {
477                descriptor = descriptor.with_format(format.clone());
478            }
479            if records.sources.len() >= crate::validation::MAX_MODULE_SOURCES {
480                return Err(record_cap("sources", crate::validation::MAX_MODULE_SOURCES));
481            }
482            records
483                .source_positions
484                .try_reserve(1)
485                .map_err(allocation_refused)?;
486            records
487                .source_positions
488                .insert(id.clone(), records.sources.len());
489            records.sources.push(descriptor);
490            root_spans.push(SourceSpan::new(id, 0, buffer.bytes().len() as u64)?);
491        }
492
493        for diagnostic in &mut source_diagnostics {
494            diagnostic.remap_span_sources(|id| {
495                source_ids.get(id).cloned().unwrap_or_else(|| id.clone())
496            })?;
497        }
498        records.producer = producer;
499        records.retained_source = Some(source);
500
501        let mut derived = PioModule {
502            value,
503            diagnostics,
504            records,
505        };
506        for spans in root_spans.chunks(crate::validation::MAX_SOURCE_MAP_SPANS) {
507            derived.add_source_map_entry(SourceMapEntry::new(
508                "",
509                SourceRelation::Aggregated,
510                spans.to_vec(),
511            )?)?;
512        }
513        for diagnostic in source_diagnostics {
514            derived.add_diagnostic(diagnostic)?;
515        }
516        derived.add_history_entry(history)?;
517        derived.verify_records()?;
518        Ok(derived)
519    }
520
521    /// Move the value and every module record into another typed module.
522    #[must_use]
523    pub fn map_value<U>(self, convert: impl FnOnce(T) -> U) -> PioModule<U> {
524        PioModule {
525            value: convert(self.value),
526            diagnostics: self.diagnostics,
527            records: self.records,
528        }
529    }
530
531    /// Move the value through a fallible conversion, keeping every module
532    /// record on success. On failure the conversion's error is returned and
533    /// the records are dropped with the consumed value; a caller that must
534    /// keep the source or findings on the failure route takes them off the
535    /// module first with [`PioModule::take_source`] and the public
536    /// `diagnostics` field.
537    pub fn try_map_value<U, E>(
538        self,
539        convert: impl FnOnce(T) -> Result<U, E>,
540    ) -> Result<PioModule<U>, E> {
541        let Self {
542            value,
543            diagnostics,
544            records,
545        } = self;
546        Ok(PioModule {
547            value: convert(value)?,
548            diagnostics,
549            records,
550        })
551    }
552
553    /// Take the retained source owner off the module, leaving descriptors,
554    /// diagnostics, and history in place. The module then reads as
555    /// constructed in memory until a source is reattached.
556    pub fn take_source(&mut self) -> Option<Source> {
557        self.records.retained_source.take()
558    }
559
560    /// Assemble the module a parser returns: the typed value, one descriptor
561    /// per acquired buffer of the retained source, a coarse root source map,
562    /// and the reader's findings.
563    ///
564    /// # Errors
565    /// A duplicate acquired buffer identity, an invalid buffer name or span,
566    /// or a finding that fails the record checks of
567    /// [`PioModule::add_diagnostic`].
568    pub fn parsed(value: T, source: Source, diagnostics: Vec<Diagnostic>) -> Result<Self, Error> {
569        let mut module = Self::new(value);
570        let mut root_spans = Vec::new();
571        for buffer in source.acquired_buffers() {
572            // The stored descriptor names the file, never the local path,
573            // and carries the resolved format so a same format write can
574            // default to it.
575            let name = std::path::Path::new(buffer.name())
576                .file_name()
577                .and_then(|n| n.to_str())
578                .unwrap_or_else(|| buffer.name());
579            let mut descriptor =
580                SourceDescriptor::new(buffer.id().clone(), name, buffer.bytes().len() as u64)?;
581            if let Some(format) = source.format() {
582                descriptor = descriptor.with_format(format.clone());
583            }
584            module.add_source_descriptor(descriptor)?;
585            root_spans.push(SourceSpan::new(
586                buffer.id().clone(),
587                0,
588                buffer.bytes().len() as u64,
589            )?);
590        }
591        // Parsers can add field precise mappings later. The root mapping is
592        // the minimum durable connection between the typed value and every
593        // buffer that produced it, so a module handed to another process does
594        // not retain source descriptors with no corresponding value target.
595        // Chunk it because one source map entry has a bounded span list while
596        // a directory source can acquire more files than that bound.
597        for spans in root_spans.chunks(crate::validation::MAX_SOURCE_MAP_SPANS) {
598            module.add_source_map_entry(SourceMapEntry::new(
599                "",
600                SourceRelation::Aggregated,
601                spans.to_vec(),
602            )?)?;
603        }
604        let mut module = module.with_source(source);
605        for record in diagnostics {
606            module.add_diagnostic(record)?;
607        }
608        Ok(module)
609    }
610
611    /// Internal cross-crate support for recoverable consuming narrowing.
612    #[doc(hidden)]
613    // Boxing the failure would allocate and violate the recoverable no-copy
614    // narrowing rule. The caller gets the original module by value.
615    #[allow(clippy::result_large_err)]
616    pub fn __try_map_value<U>(
617        self,
618        convert: impl FnOnce(T) -> Result<U, T>,
619    ) -> Result<PioModule<U>, PioModule<T>> {
620        let Self {
621            value,
622            diagnostics,
623            records,
624        } = self;
625        match convert(value) {
626            Ok(value) => Ok(PioModule {
627                value,
628                diagnostics,
629                records,
630            }),
631            Err(value) => Err(PioModule {
632                value,
633                diagnostics,
634                records,
635            }),
636        }
637    }
638}
639
640impl<T: Clone> PioModule<T> {
641    /// Begin an atomic edit of this module's value and findings.
642    ///
643    /// The candidate uses `T::clone`; values backed by shared tables retain
644    /// those tables until [`StagedEdit::value_mut`] performs their own copy on
645    /// write detachment. Dropping the staged edit, or any failed operation on
646    /// it, leaves this module untouched.
647    pub fn stage_edit(&mut self) -> StagedEdit<'_, T> {
648        let candidate = PioModule {
649            value: self.value.clone(),
650            diagnostics: self.diagnostics.clone(),
651            records: self.records.clone(),
652        };
653        StagedEdit {
654            module: self,
655            candidate,
656        }
657    }
658}
659
660/// An isolated candidate for one atomic module edit.
661pub struct StagedEdit<'a, T: Clone> {
662    module: &'a mut PioModule<T>,
663    candidate: PioModule<T>,
664}
665
666impl<T: Clone> StagedEdit<'_, T> {
667    #[must_use]
668    pub const fn value(&self) -> &T {
669        &self.candidate.value
670    }
671
672    pub const fn value_mut(&mut self) -> &mut T {
673        &mut self.candidate.value
674    }
675
676    #[must_use]
677    pub fn diagnostics(&self) -> &[Diagnostic] {
678        &self.candidate.diagnostics
679    }
680
681    /// Add a finding to the candidate under the normal module record checks.
682    pub fn add_diagnostic(&mut self, diagnostic: Diagnostic) -> Result<(), Error> {
683        self.candidate.add_diagnostic(diagnostic)
684    }
685
686    /// Validate and atomically replace the original module with this
687    /// candidate. A committed edit keeps source descriptors and prior records,
688    /// but invalidates retained bytes and source mappings that describe the
689    /// pre-edit value.
690    pub fn commit(self, producer: Producer, history: HistoryEntry) -> Result<(), Error> {
691        let Self {
692            module,
693            mut candidate,
694        } = self;
695        candidate.records.producer = producer;
696        candidate.records.retained_source = None;
697        candidate.records.source_map.clear();
698        candidate.add_history_entry(history)?;
699        candidate.verify_records()?;
700        *module = candidate;
701        Ok(())
702    }
703}
704
705impl<T: fmt::Debug> fmt::Debug for PioModule<T> {
706    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
707        formatter
708            .debug_struct("PioModule")
709            .field("value", &self.value)
710            .field("diagnostics", &self.diagnostics)
711            .field("records", &self.records)
712            .finish()
713    }
714}
715
716/// The uniform record count refusal every adder applies at its maximum.
717fn record_cap(what: &str, max: usize) -> Error {
718    Error::new(
719        &crate::codes::REQUEST_RECORD_TOO_LARGE,
720        format!("the module already holds the maximum {max} {what}"),
721    )
722}
723
724fn validate_span(
725    span: &SourceSpan,
726    sources: &[SourceDescriptor],
727    positions: &HashMap<SourceId, usize>,
728) -> Result<(), Error> {
729    let Some(source) = positions
730        .get(span.source())
731        .and_then(|position| sources.get(*position))
732    else {
733        return Err(Error::new(
734            &crate::codes::REQUEST_RECORD_INVALID_SPAN,
735            format!("source span refers to unknown source `{}`", span.source()),
736        ));
737    };
738    if span.byte_end() > source.byte_length() {
739        return Err(Error::new(
740            &crate::codes::REQUEST_RECORD_INVALID_SPAN,
741            format!(
742                "source span end {} exceeds source `{}` length {}",
743                span.byte_end(),
744                span.source(),
745                source.byte_length()
746            ),
747        ));
748    }
749    Ok(())
750}
751
752fn valid_extension_namespace(namespace: &str) -> bool {
753    valid_nonempty_text(namespace)
754        && !namespace.starts_with('.')
755        && !namespace.ends_with('.')
756        && namespace.contains('.')
757        && namespace.split('.').all(|segment| !segment.is_empty())
758}
759
760#[cfg(test)]
761mod tests {
762    use std::rc::Rc;
763    use std::sync::Arc;
764
765    use super::*;
766    use crate::{DiagnosticSeverity, HistoryKind, SourceRelation};
767
768    #[test]
769    fn modules_accept_unregistered_application_values() {
770        struct ApplicationValue(Rc<()>);
771        let module = PioModule::new(ApplicationValue(Rc::new(())));
772        assert_eq!(Rc::strong_count(&module.value.0), 1);
773    }
774
775    #[test]
776    fn map_value_moves_records_and_retained_source_without_allocation() {
777        let bytes: Arc<[u8]> = b"source".as_slice().into();
778        let source = Source::from_memory("case.m", Arc::clone(&bytes)).unwrap();
779        let diagnostic = Diagnostic::of(&crate::codes::VALIDATE_TIME_SERIES_SHAPE, "kept");
780        let module = PioModule::new(String::from("value"))
781            .with_source(source)
782            .with_diagnostic(diagnostic)
783            .unwrap();
784        let diagnostics_pointer = module.diagnostics().as_ptr();
785        let source_pointer = module
786            .source()
787            .unwrap()
788            .primary_buffer()
789            .unwrap()
790            .bytes()
791            .as_ptr();
792        let mapped = module.map_value(String::into_bytes);
793        assert_eq!(mapped.value, b"value");
794        assert_eq!(mapped.diagnostics().as_ptr(), diagnostics_pointer);
795        assert_eq!(
796            mapped
797                .source()
798                .unwrap()
799                .primary_buffer()
800                .unwrap()
801                .bytes()
802                .as_ptr(),
803            source_pointer
804        );
805    }
806
807    #[test]
808    fn failed_try_map_returns_the_original_module_and_records() {
809        let module = PioModule::new(String::from("value"))
810            .with_diagnostic(Diagnostic::of(
811                &crate::codes::VALIDATE_TIME_SERIES_SHAPE,
812                "kept",
813            ))
814            .unwrap();
815        let diagnostics_pointer = module.diagnostics().as_ptr();
816        let recovered = module
817            .__try_map_value::<usize>(Err)
818            .expect_err("conversion fails");
819        assert_eq!(recovered.value, "value");
820        assert_eq!(recovered.diagnostics().as_ptr(), diagnostics_pointer);
821    }
822
823    #[test]
824    fn parsed_module_maps_the_value_to_its_source() {
825        let source = Source::from_memory("case.m", b"source".as_slice()).unwrap();
826        let module = PioModule::parsed(1_u8, source, Vec::new()).unwrap();
827        assert_eq!(module.source_map().len(), 1);
828        let entry = &module.source_map()[0];
829        assert_eq!(entry.target(), "");
830        assert_eq!(entry.relation(), SourceRelation::Aggregated);
831        assert_eq!(entry.spans().len(), 1);
832        assert_eq!(entry.spans()[0].byte_start(), 0);
833        assert_eq!(entry.spans()[0].byte_end(), 6);
834    }
835
836    #[test]
837    fn record_references_and_namespaces_are_checked() {
838        let source_id = SourceId::new("input").unwrap();
839        let mut module = PioModule::new(1_u8);
840        module
841            .add_source_descriptor(SourceDescriptor::new(source_id.clone(), "case.m", 4).unwrap())
842            .unwrap();
843        let span = SourceSpan::new(source_id, 0, 4).unwrap();
844        module
845            .add_source_map_entry(
846                SourceMapEntry::new("/value", SourceRelation::Exact, vec![span.clone()]).unwrap(),
847            )
848            .unwrap();
849        module
850            .add_diagnostic(
851                Diagnostic::new(
852                    crate::DiagnosticCode::new("PARTNER.TEST.FINDING").unwrap(),
853                    DiagnosticSeverity::Note,
854                    "note",
855                )
856                .with_id(DiagnosticId::new("d1").unwrap())
857                .with_span(span)
858                .unwrap(),
859            )
860            .unwrap();
861        module
862            .add_history_entry(
863                HistoryEntry::new(HistoryId::new("h1").unwrap(), HistoryKind::Parse, "parse")
864                    .unwrap(),
865            )
866            .unwrap();
867        module
868            .insert_extension("org.example", Value::Bool(true))
869            .unwrap();
870        assert!(module.verify_records().is_ok());
871        assert!(
872            module
873                .insert_extension("not-namespaced", Value::Null)
874                .is_err()
875        );
876
877        let invalid = SourceSpan::new(SourceId::new("input").unwrap(), 0, 5).unwrap();
878        assert!(
879            module
880                .add_source_map_entry(
881                    SourceMapEntry::new("", SourceRelation::Exact, vec![invalid]).unwrap()
882                )
883                .is_err()
884        );
885    }
886
887    #[test]
888    fn semantic_derivation_records_provenance_and_invalidates_old_value_locators() {
889        let source = Source::from_memory("case.m", b"source".as_slice()).unwrap();
890        let span = SourceSpan::new(
891            source.primary_buffer().unwrap().id().clone(),
892            0,
893            b"source".len() as u64,
894        )
895        .unwrap();
896        let diagnostic = Diagnostic::new(
897            crate::DiagnosticCode::new("PARTNER.TEST.DERIVE").unwrap(),
898            DiagnosticSeverity::Note,
899            "kept",
900        )
901        .with_id(DiagnosticId::new("d1").unwrap())
902        .with_target("/old/value")
903        .unwrap()
904        .with_span(span)
905        .unwrap();
906        let mut module = PioModule::parsed(3_u8, source, vec![diagnostic]).unwrap();
907        module
908            .add_history_entry(
909                HistoryEntry::new(HistoryId::new("h1").unwrap(), HistoryKind::Parse, "parse")
910                    .unwrap(),
911            )
912            .unwrap();
913        module
914            .insert_extension("org.example", Value::Bool(true))
915            .unwrap();
916
917        let derived = module
918            .derive_value(
919                Producer::new("tellegen", "1.0.0").unwrap(),
920                HistoryEntry::new(
921                    HistoryId::new("h2").unwrap(),
922                    HistoryKind::Transform,
923                    "build instance",
924                )
925                .unwrap(),
926                |value| usize::from(value) * 2,
927            )
928            .unwrap();
929
930        assert_eq!(derived.value, 6);
931        assert_eq!(derived.producer().name(), "tellegen");
932        assert_eq!(derived.sources().len(), 1);
933        assert!(derived.source().is_none());
934        assert_eq!(derived.source_map(), []);
935        assert_eq!(derived.diagnostics().len(), 1);
936        assert!(derived.diagnostics()[0].target().is_none());
937        assert_eq!(derived.diagnostics()[0].spans().len(), 1);
938        assert_eq!(derived.history().len(), 2);
939        assert_eq!(
940            derived.extensions().get("org.example"),
941            Some(&Value::Bool(true))
942        );
943    }
944
945    #[test]
946    fn staged_edits_commit_atomically_or_leave_the_module_untouched() {
947        let source = Source::from_memory("case.m", b"source".as_slice()).unwrap();
948        let diagnostic = Diagnostic::new(
949            crate::DiagnosticCode::new("PARTNER.TEST.EDIT").unwrap(),
950            DiagnosticSeverity::Note,
951            "existing",
952        )
953        .with_id(DiagnosticId::new("d1").unwrap())
954        .with_target("/0")
955        .unwrap();
956        let mut module = PioModule::parsed([1_u8, 2], source, vec![diagnostic]).unwrap();
957        module
958            .add_history_entry(
959                HistoryEntry::new(HistoryId::new("h1").unwrap(), HistoryKind::Parse, "parse")
960                    .unwrap(),
961            )
962            .unwrap();
963
964        {
965            let mut staged = module.stage_edit();
966            staged.value_mut()[0] = 8;
967        }
968        assert_eq!(module.value, [1, 2]);
969        assert!(module.source().is_some());
970
971        let mut staged = module.stage_edit();
972        staged.value_mut()[0] = 9;
973        let error = staged
974            .commit(
975                Producer::new("editor", "1.0.0").unwrap(),
976                HistoryEntry::new(HistoryId::new("h1").unwrap(), HistoryKind::Edit, "edit")
977                    .unwrap(),
978            )
979            .expect_err("duplicate history makes the staged commit fail");
980        assert_eq!(error.category(), crate::ErrorCategory::Request);
981        assert_eq!(module.value, [1, 2]);
982        assert!(module.source().is_some());
983        assert_eq!(module.history().len(), 1);
984
985        let mut staged = module.stage_edit();
986        staged.value_mut()[0] = 7;
987        staged
988            .add_diagnostic(
989                Diagnostic::new(
990                    crate::DiagnosticCode::new("PARTNER.TEST.EDITED").unwrap(),
991                    DiagnosticSeverity::Note,
992                    "changed",
993                )
994                .with_id(DiagnosticId::new("d2").unwrap())
995                .with_target("/0")
996                .unwrap(),
997            )
998            .unwrap();
999        staged
1000            .commit(
1001                Producer::new("editor", "1.0.0").unwrap(),
1002                HistoryEntry::new(HistoryId::new("h2").unwrap(), HistoryKind::Edit, "edit")
1003                    .unwrap(),
1004            )
1005            .unwrap();
1006
1007        assert_eq!(module.value, [7, 2]);
1008        assert_eq!(module.producer().name(), "editor");
1009        assert!(module.source().is_none());
1010        assert_eq!(module.source_map(), []);
1011        assert_eq!(module.sources().len(), 1);
1012        assert_eq!(module.diagnostics().len(), 2);
1013        assert_eq!(module.diagnostics()[0].target(), Some("/0"));
1014        assert_eq!(module.history().len(), 2);
1015    }
1016}