1use std::collections::BTreeMap;
2use std::fmt;
3
4use serde::Deserialize;
5use serde_json::Value;
6
7use crate::validation::{valid_nonempty_text, valid_rfc6901_pointer};
8use crate::{Error, FormatId};
9
10macro_rules! record_id {
11 ($name:ident, $label:literal) => {
12 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
13 pub struct $name(Box<str>);
14
15 impl $name {
16 pub fn new(value: impl Into<String>) -> Result<Self, Error> {
17 let value = value.into();
18 if !valid_nonempty_text(&value) {
19 return Err(Error::new(
20 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
21 concat!($label, " must be nonempty and bounded"),
22 ));
23 }
24 Ok(Self(value.into_boxed_str()))
25 }
26
27 #[must_use]
28 pub fn as_str(&self) -> &str {
29 &self.0
30 }
31 }
32
33 impl fmt::Display for $name {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 formatter.write_str(&self.0)
36 }
37 }
38
39 impl serde::Serialize for $name {
40 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
41 serializer.serialize_str(&self.0)
42 }
43 }
44
45 impl<'de> serde::Deserialize<'de> for $name {
49 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
50 use serde::de::DeserializeSeed;
51 let value = crate::bounded::BoundedStr {
52 what: $label,
53 max_bytes: crate::validation::MAX_IDENTIFIER_BYTES,
54 }
55 .deserialize(deserializer)?;
56 Self::new(value).map_err(serde::de::Error::custom)
57 }
58 }
59 };
60}
61
62record_id!(SourceId, "source ID");
63record_id!(DiagnosticId, "diagnostic ID");
64record_id!(HistoryId, "history ID");
65
66#[derive(Clone, Debug, PartialEq, Eq)]
68pub struct Producer {
69 name: Box<str>,
70 version: Box<str>,
71}
72
73impl Producer {
74 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Result<Self, Error> {
75 let name = name.into();
76 let version = version.into();
77 if !valid_nonempty_text(&name) || !valid_nonempty_text(&version) {
78 return Err(Error::new(
79 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
80 "producer name and version must be nonempty and bounded",
81 ));
82 }
83 Ok(Self {
84 name: name.into_boxed_str(),
85 version: version.into_boxed_str(),
86 })
87 }
88
89 pub(crate) fn powerio() -> Self {
90 Self {
91 name: "powerio".into(),
92 version: env!("CARGO_PKG_VERSION").into(),
93 }
94 }
95
96 #[must_use]
97 pub fn name(&self) -> &str {
98 &self.name
99 }
100
101 #[must_use]
102 pub fn version(&self) -> &str {
103 &self.version
104 }
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
108#[non_exhaustive]
109pub enum DigestAlgorithm {
110 Sha256,
111}
112
113impl DigestAlgorithm {
114 #[must_use]
115 pub const fn as_str(self) -> &'static str {
116 match self {
117 Self::Sha256 => "sha256",
118 }
119 }
120}
121
122#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct Digest {
125 algorithm: DigestAlgorithm,
126 value: Box<str>,
127}
128
129impl Digest {
130 pub fn sha256(value: impl Into<String>) -> Result<Self, Error> {
131 let value = value.into();
132 if value.len() != 64
133 || !value
134 .bytes()
135 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
136 {
137 return Err(Error::new(
138 &crate::codes::REQUEST_RECORD_INVALID_DIGEST,
139 "a SHA-256 digest must contain 64 lowercase hexadecimal characters",
140 ));
141 }
142 Ok(Self {
143 algorithm: DigestAlgorithm::Sha256,
144 value: value.into_boxed_str(),
145 })
146 }
147
148 #[must_use]
149 pub const fn algorithm(&self) -> DigestAlgorithm {
150 self.algorithm
151 }
152
153 #[must_use]
154 pub fn value(&self) -> &str {
155 &self.value
156 }
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct SourceDescriptor {
162 id: SourceId,
163 name: Box<str>,
164 byte_length: u64,
165 format: Option<FormatId>,
166 digest: Option<Digest>,
167}
168
169impl SourceDescriptor {
170 pub fn new(id: SourceId, name: impl Into<String>, byte_length: u64) -> Result<Self, Error> {
171 let name = name.into();
172 if !valid_nonempty_text(&name) {
173 return Err(Error::new(
174 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
175 "source name must be nonempty and bounded",
176 ));
177 }
178 Ok(Self {
179 id,
180 name: name.into_boxed_str(),
181 byte_length,
182 format: None,
183 digest: None,
184 })
185 }
186
187 #[must_use]
188 pub fn id(&self) -> &SourceId {
189 &self.id
190 }
191
192 #[must_use]
193 pub fn name(&self) -> &str {
194 &self.name
195 }
196
197 #[must_use]
198 pub const fn byte_length(&self) -> u64 {
199 self.byte_length
200 }
201
202 #[must_use]
203 pub const fn format(&self) -> Option<&FormatId> {
204 self.format.as_ref()
205 }
206
207 #[must_use]
208 pub const fn digest(&self) -> Option<&Digest> {
209 self.digest.as_ref()
210 }
211
212 #[must_use]
213 pub fn with_format(mut self, format: FormatId) -> Self {
214 self.format = Some(format);
215 self
216 }
217
218 #[must_use]
219 pub fn with_digest(mut self, digest: Digest) -> Self {
220 self.digest = Some(digest);
221 self
222 }
223}
224
225#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
227pub struct SourceSpan {
228 source: SourceId,
229 byte_start: u64,
230 byte_end: u64,
231}
232
233impl SourceSpan {
234 pub fn new(source: SourceId, byte_start: u64, byte_end: u64) -> Result<Self, Error> {
235 if byte_start > byte_end {
236 return Err(Error::new(
237 &crate::codes::REQUEST_RECORD_INVALID_SPAN,
238 format!("source span {byte_start}..{byte_end} is reversed"),
239 ));
240 }
241 Ok(Self {
242 source,
243 byte_start,
244 byte_end,
245 })
246 }
247
248 #[must_use]
249 pub fn source(&self) -> &SourceId {
250 &self.source
251 }
252
253 #[must_use]
254 pub const fn byte_start(&self) -> u64 {
255 self.byte_start
256 }
257
258 #[must_use]
259 pub const fn byte_end(&self) -> u64 {
260 self.byte_end
261 }
262}
263
264#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
265#[non_exhaustive]
266pub enum SourceRelation {
267 Exact,
268 Defaulted,
269 Inferred,
270 ConvertedUnits,
271 Aggregated,
272 Split,
273 Synthetic,
274 Transformed,
275 RetainedExtra,
276}
277
278impl SourceRelation {
279 #[must_use]
280 pub const fn allows_empty_spans(self) -> bool {
281 matches!(self, Self::Defaulted | Self::Synthetic | Self::Transformed)
282 }
283}
284
285#[derive(Clone, Debug, PartialEq, Eq)]
287pub struct SourceMapEntry {
288 target: Box<str>,
289 relation: SourceRelation,
290 spans: Vec<SourceSpan>,
291}
292
293impl SourceMapEntry {
294 pub fn new(
295 target: impl Into<String>,
296 relation: SourceRelation,
297 spans: Vec<SourceSpan>,
298 ) -> Result<Self, Error> {
299 let target = target.into();
300 if !valid_rfc6901_pointer(&target) {
301 return Err(Error::new(
302 &crate::codes::REQUEST_RECORD_INVALID_POINTER,
303 "a source map target must be an RFC 6901 pointer",
304 ));
305 }
306 if spans.is_empty() && !relation.allows_empty_spans() {
307 return Err(Error::new(
308 &crate::codes::REQUEST_RECORD_INVALID_SPAN,
309 "this source relation requires at least one byte span",
310 ));
311 }
312 if spans.len() > crate::validation::MAX_SOURCE_MAP_SPANS {
313 return Err(Error::new(
314 &crate::codes::REQUEST_RECORD_TOO_LARGE,
315 format!(
316 "a source map entry carries more than {} byte spans",
317 crate::validation::MAX_SOURCE_MAP_SPANS
318 ),
319 ));
320 }
321 Ok(Self {
322 target: target.into_boxed_str(),
323 relation,
324 spans,
325 })
326 }
327
328 #[must_use]
329 pub fn target(&self) -> &str {
330 &self.target
331 }
332
333 #[must_use]
334 pub const fn relation(&self) -> SourceRelation {
335 self.relation
336 }
337
338 #[must_use]
339 pub fn spans(&self) -> &[SourceSpan] {
340 &self.spans
341 }
342}
343
344#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
345#[non_exhaustive]
346pub enum HistoryKind {
347 Parse,
348 Upgrade,
349 Transform,
350 Edit,
351 Repair,
352}
353
354#[derive(Clone, Debug, PartialEq)]
356pub struct HistoryEntry {
357 id: HistoryId,
358 kind: HistoryKind,
359 name: Box<str>,
360 input_kind: Option<Box<str>>,
361 output_kind: Option<Box<str>>,
362 parameters: BTreeMap<String, Value>,
363 assumptions: Vec<String>,
364 losses: Vec<String>,
365}
366
367impl HistoryEntry {
368 pub fn new(id: HistoryId, kind: HistoryKind, name: impl Into<String>) -> Result<Self, Error> {
369 let name = name.into();
370 if !valid_nonempty_text(&name) {
371 return Err(Error::new(
372 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
373 "history operation name must be nonempty and bounded",
374 ));
375 }
376 Ok(Self {
377 id,
378 kind,
379 name: name.into_boxed_str(),
380 input_kind: None,
381 output_kind: None,
382 parameters: BTreeMap::new(),
383 assumptions: Vec::new(),
384 losses: Vec::new(),
385 })
386 }
387
388 #[must_use]
389 pub fn id(&self) -> &HistoryId {
390 &self.id
391 }
392
393 #[must_use]
394 pub const fn kind(&self) -> HistoryKind {
395 self.kind
396 }
397
398 #[must_use]
399 pub fn name(&self) -> &str {
400 &self.name
401 }
402
403 #[must_use]
404 pub fn input_kind(&self) -> Option<&str> {
405 self.input_kind.as_deref()
406 }
407
408 #[must_use]
409 pub fn output_kind(&self) -> Option<&str> {
410 self.output_kind.as_deref()
411 }
412
413 #[must_use]
414 pub const fn parameters(&self) -> &BTreeMap<String, Value> {
415 &self.parameters
416 }
417
418 #[must_use]
419 pub fn assumptions(&self) -> &[String] {
420 &self.assumptions
421 }
422
423 #[must_use]
424 pub fn losses(&self) -> &[String] {
425 &self.losses
426 }
427
428 pub fn with_input_kind(mut self, kind: impl Into<String>) -> Result<Self, Error> {
429 self.input_kind = Some(validated_kind(kind.into())?);
430 Ok(self)
431 }
432
433 pub fn with_output_kind(mut self, kind: impl Into<String>) -> Result<Self, Error> {
434 self.output_kind = Some(validated_kind(kind.into())?);
435 Ok(self)
436 }
437
438 pub fn with_parameters(mut self, parameters: BTreeMap<String, Value>) -> Result<Self, Error> {
439 if parameters.len() > crate::validation::MAX_HISTORY_PARAMETERS {
440 return Err(history_too_large(
441 "parameters",
442 crate::validation::MAX_HISTORY_PARAMETERS,
443 ));
444 }
445 if parameters.keys().any(|key| !valid_nonempty_text(key)) {
446 return Err(Error::new(
447 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
448 "a history parameter key must be nonempty and bounded",
449 ));
450 }
451 self.parameters = parameters;
452 Ok(self)
453 }
454
455 pub fn with_assumption(mut self, assumption: impl Into<String>) -> Result<Self, Error> {
456 self.assumptions = push_history_note(self.assumptions, assumption.into(), "assumptions")?;
457 Ok(self)
458 }
459
460 pub fn with_loss(mut self, loss: impl Into<String>) -> Result<Self, Error> {
461 self.losses = push_history_note(self.losses, loss.into(), "losses")?;
462 Ok(self)
463 }
464}
465
466fn history_too_large(what: &str, limit: usize) -> Error {
467 Error::new(
468 &crate::codes::REQUEST_RECORD_TOO_LARGE,
469 format!("a history entry carries more than {limit} {what}"),
470 )
471}
472
473fn push_history_note(
474 mut notes: Vec<String>,
475 note: String,
476 what: &'static str,
477) -> Result<Vec<String>, Error> {
478 if notes.len() >= crate::validation::MAX_HISTORY_NOTES {
479 return Err(history_too_large(
480 what,
481 crate::validation::MAX_HISTORY_NOTES,
482 ));
483 }
484 if !valid_nonempty_text(¬e) {
485 return Err(Error::new(
486 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
487 format!("a history {what} note must be nonempty and bounded"),
488 ));
489 }
490 notes.push(note);
491 Ok(notes)
492}
493
494fn validated_kind(kind: String) -> Result<Box<str>, Error> {
495 if !valid_nonempty_text(&kind) {
496 return Err(Error::new(
497 &crate::codes::REQUEST_RECORD_INVALID_IDENTIFIER,
498 "a history value kind must be nonempty and bounded",
499 ));
500 }
501 Ok(kind.into_boxed_str())
502}
503
504impl<'de> serde::Deserialize<'de> for SourceSpan {
505 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
506 #[derive(Deserialize)]
507 struct Wire {
508 source: SourceId,
509 byte_start: u64,
510 byte_end: u64,
511 }
512 let wire = Wire::deserialize(deserializer)?;
513 Self::new(wire.source, wire.byte_start, wire.byte_end).map_err(serde::de::Error::custom)
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 use super::*;
520
521 #[test]
522 fn identifiers_and_digests_are_strict() {
523 assert!(SourceId::new("").is_err());
524 assert!(SourceId::new("x\0y").is_err());
525 assert!(SourceId::new("x".repeat(65_537)).is_err());
526 assert!(SourceId::new("Case A").is_ok());
527 assert!(Digest::sha256("a".repeat(64)).is_ok());
528 assert!(Digest::sha256("A".repeat(64)).is_err());
529 assert!(Digest::sha256("a".repeat(63)).is_err());
530 }
531
532 #[test]
533 fn source_map_spans_obey_relation_rules() {
534 let id = SourceId::new("input").unwrap();
535 assert!(SourceSpan::new(id.clone(), 2, 1).is_err());
536 assert!(SourceMapEntry::new("/bus/0", SourceRelation::Exact, Vec::new()).is_err());
537 assert!(SourceMapEntry::new("/bus/0", SourceRelation::Defaulted, Vec::new()).is_ok());
538 assert!(SourceMapEntry::new("bad", SourceRelation::Synthetic, Vec::new()).is_err());
539 }
540}