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 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
55pub 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 #[must_use]
135 pub fn sever_source(mut self) -> Self {
136 self.records.retained_source = None;
137 self
138 }
139
140 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 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 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 #[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 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 pub fn take_source(&mut self) -> Option<Source> {
377 self.records.retained_source.take()
378 }
379
380 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 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 #[doc(hidden)]
412 #[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
437fn 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}