#![forbid(unsafe_code)]
#![allow(
missing_docs,
clippy::missing_errors_doc,
clippy::return_self_not_must_use
)]
use std::{collections::BTreeMap, fmt};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub mod contract;
pub use contract::{DefinitionPackage, RuleSetPackage};
pub mod temporal;
pub use temporal::{Date, DateTime, TemporalError, TemporalPrecision};
pub mod identity;
pub use identity::{FindingId, IdentityError, finding_ids, not_evaluated_ids};
pub mod decision;
pub use decision::{
ChangedFacet, Decision, DecisionBasis, DecisionChange, DecisionError, DecisionStatus,
Decisions, EvidenceCheck, FindingDecision,
};
pub mod table;
pub use table::{
ReportColumn, ReportColumnKind, ReportRow, ReportTable, ReportTableError, ReportValue,
};
#[derive(Debug, Error, PartialEq, Eq)]
pub enum IrError {
#[error("{kind} must not be blank")]
Blank { kind: &'static str },
#[error("duplicate object id: {0}")]
DuplicateObject(ObjectId),
#[error("object {object} has more than one `{scheme}` identity")]
ConflictingExternalId { object: ObjectId, scheme: String },
#[error("external id {} is claimed by both {} and {}", .0.id, .0.first, .0.second)]
DuplicateExternalId(Box<ExternalIdClash>),
#[error(
"invalid discipline `{0}`: use 1 to 64 lowercase ASCII letters, digits, `-` or `_`, starting with a letter or digit"
)]
InvalidDiscipline(String),
#[error("column types describe a table value only")]
ColumnTypesWithoutTable,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ExternalIdClash {
pub id: ExternalId,
pub first: ObjectId,
pub second: ObjectId,
}
fn required(value: impl Into<String>, kind: &'static str) -> Result<String, IrError> {
let value = value.into();
if value.trim().is_empty() {
Err(IrError::Blank { kind })
} else {
Ok(value)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SourceId {
pub system: String,
pub document: String,
}
impl SourceId {
pub fn new(system: impl Into<String>, document: impl Into<String>) -> Result<Self, IrError> {
Ok(Self {
system: required(system, "source system")?,
document: required(document, "source document")?,
})
}
}
impl fmt::Display for SourceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.system, self.document)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ObjectId {
pub source: SourceId,
pub local_id: String,
}
impl ObjectId {
pub fn new(source: SourceId, local_id: impl Into<String>) -> Result<Self, IrError> {
Ok(Self {
source,
local_id: required(local_id, "object local id")?,
})
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.source, self.local_id)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExternalId {
pub scheme: String,
pub value: String,
}
impl ExternalId {
pub fn new(scheme: impl Into<String>, value: impl Into<String>) -> Result<Self, IrError> {
Ok(Self {
scheme: required(scheme, "external id scheme")?,
value: required(value, "external id value")?,
})
}
}
impl fmt::Display for ExternalId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.scheme, self.value)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct Discipline(String);
impl Discipline {
pub const MAX_LEN: usize = 64;
pub fn new(name: impl Into<String>) -> Result<Self, IrError> {
let name = name.into();
let mut bytes = name.bytes();
let valid = name.len() <= Self::MAX_LEN
&& bytes
.next()
.is_some_and(|first| first.is_ascii_lowercase() || first.is_ascii_digit())
&& bytes.all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-' || byte == b'_'
});
if valid {
Ok(Self(name))
} else {
Err(IrError::InvalidDiscipline(name))
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for Discipline {
type Error = IrError;
fn try_from(name: String) -> Result<Self, IrError> {
Self::new(name)
}
}
impl From<Discipline> for String {
fn from(discipline: Discipline) -> Self {
discipline.0
}
}
impl fmt::Display for Discipline {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuantityDimension {
Length,
Area,
Volume,
PlaneAngle,
Other {
exponents: [i8; 7],
},
}
impl QuantityDimension {
#[must_use]
pub fn from_exponents(exponents: [i8; 7]) -> Option<Self> {
Some(match exponents {
[0, 0, 0, 0, 0, 0, 0] => return None,
[1, 0, 0, 0, 0, 0, 0] => Self::Length,
[2, 0, 0, 0, 0, 0, 0] => Self::Area,
[3, 0, 0, 0, 0, 0, 0] => Self::Volume,
exponents => Self::Other { exponents },
})
}
#[must_use]
pub fn unit_symbol(self) -> String {
const BASE: [&str; 7] = ["m", "kg", "s", "A", "K", "mol", "cd"];
let exponents = match self {
Self::Length => return "m".into(),
Self::Area => return "m²".into(),
Self::Volume => return "m³".into(),
Self::PlaneAngle => return "rad".into(),
Self::Other { exponents } => exponents,
};
let superscript = |digit: char| match digit {
'-' => '⁻',
'1' => '¹',
'2' => '²',
'3' => '³',
'4' => '⁴',
'5' => '⁵',
'6' => '⁶',
'7' => '⁷',
'8' => '⁸',
'9' => '⁹',
_ => '⁰',
};
BASE.iter()
.zip(exponents)
.filter(|(_, exponent)| *exponent != 0)
.map(|(base, exponent)| {
if exponent == 1 {
(*base).to_owned()
} else {
format!(
"{base}{}",
exponent
.to_string()
.chars()
.map(superscript)
.collect::<String>()
)
}
})
.collect::<Vec<_>>()
.join("·")
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum PropertyValue {
Null,
Boolean(bool),
Integer(i64),
Decimal(f64),
Quantity {
value: f64,
dimension: QuantityDimension,
},
String(String),
Date(Date),
#[serde(rename = "dateTime")]
DateTime(DateTime),
List(Vec<PropertyValue>),
Bounded {
#[serde(default, skip_serializing_if = "Option::is_none")]
lower: Option<Box<PropertyValue>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
upper: Option<Box<PropertyValue>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
set_point: Option<Box<PropertyValue>>,
},
Table(Vec<PropertyTableRow>),
Reference(ObjectId),
Measured {
lower: f64,
upper: f64,
dimension: QuantityDimension,
},
Complex,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PropertyTableRow {
pub defining: PropertyValue,
pub defined: PropertyValue,
}
impl PropertyValue {
#[must_use]
pub fn is_scalar(&self) -> bool {
!matches!(
self,
Self::Null | Self::List(_) | Self::Bounded { .. } | Self::Table(_) | Self::Complex
)
}
#[must_use]
pub fn stated_values(&self) -> Option<Vec<&PropertyValue>> {
match self {
Self::List(elements) => Some(elements.iter().collect()),
Self::Bounded {
lower,
upper,
set_point,
} => Some(
[lower, upper, set_point]
.into_iter()
.flatten()
.map(AsRef::as_ref)
.collect(),
),
Self::Table(rows) => Some(
rows.iter()
.flat_map(|row| [&row.defining, &row.defined])
.collect(),
),
_ => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Evidence {
pub source: SourceId,
pub locator: String,
pub exact: bool,
}
impl Evidence {
pub fn exact(source: SourceId, locator: impl Into<String>) -> Self {
Self {
source,
locator: locator.into(),
exact: true,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Classification {
pub system: String,
pub code: String,
}
impl Classification {
pub fn new(system: impl Into<String>, code: impl Into<String>) -> Result<Self, IrError> {
Ok(Self {
system: required(system, "classification system")?,
code: required(code, "classification code")?,
})
}
}
pub const ATTRIBUTE_SET: &str = "axioval:attributes";
pub const TYPE_ATTRIBUTE_SET: &str = "axioval:type-attributes";
pub const PRESENTATION_SET: &str = "axioval:presentation";
pub const PRESENTATION_LAYER: &str = "Layer";
pub const PRESENTATION_TRANSPARENCY: &str = "Transparency";
pub const MATERIAL_SET: &str = "axioval:material";
pub const MATERIAL_KIND: &str = "Kind";
pub const MATERIAL_NAME: &str = "Name";
pub const MATERIAL_CATEGORY: &str = "Category";
pub const MATERIAL_TOTAL_THICKNESS: &str = "TotalThickness";
pub const MATERIAL_COUNT: &str = "Count";
pub const MATERIAL_NAMES: &str = "Names";
pub const MATERIAL_KIND_SINGLE: &str = "material";
pub const MATERIAL_KIND_LAYER_SET: &str = "layer-set";
pub const MATERIAL_KIND_CONSTITUENT_SET: &str = "constituent-set";
pub const MATERIAL_KIND_PROFILE_SET: &str = "profile-set";
pub const MATERIAL_KIND_LIST: &str = "list";
pub const BODY_SET: &str = "axioval:body";
pub const BODY_COUNT: &str = "Count";
pub const BODY_KINDS: &str = "Kinds";
pub const BODY_KIND: &str = "Kind";
pub const BODY_MAPPED: &str = "Mapped";
pub const BODY_PROFILE_TYPE: &str = "Profile.Type";
pub const BODY_PROFILE_NAME: &str = "Profile.Name";
pub const BODY_PROFILE_OUTLINE_X: &str = "Profile.OutlineX";
pub const BODY_PROFILE_OUTLINE_Y: &str = "Profile.OutlineY";
pub const BODY_KIND_EXTRUSION: &str = "extrusion";
pub const CLASSIFICATION_SET: &str = "axioval:classification";
#[must_use]
pub fn is_reserved_set(set: &str) -> bool {
set == ATTRIBUTE_SET
|| set == TYPE_ATTRIBUTE_SET
|| set == PRESENTATION_SET
|| set == MATERIAL_SET
|| set == BODY_SET
|| is_derived_set(set)
}
pub const MEASURED_SET: &str = "axioval:measured";
pub const MEASURED_EXTENT_X: &str = "extent_x";
pub const MEASURED_EXTENT_Y: &str = "extent_y";
pub const MEASURED_EXTENT_Z: &str = "extent_z";
pub const MEASURED_BOTTOM: &str = "bottom";
pub const MEASURED_TOP: &str = "top";
pub const MEASURED_AREA: &str = "area";
pub const MEASURED_VOLUME: &str = "volume";
pub const MEASURED_X: &str = "x";
pub const MEASURED_Y: &str = "y";
pub const MEASURED_Z: &str = "z";
pub const MEASURED_BOTTOM_ABOVE_LEVEL: &str = "bottom_above_level";
pub const MEASURED_BOUNDARY_AREA: &str = "boundary_area";
pub const MEASURED_LEVEL_HEIGHT: &str = "level_height";
pub const MEASURED_NAMES: [&str; 11] = [
MEASURED_EXTENT_X,
MEASURED_EXTENT_Y,
MEASURED_EXTENT_Z,
MEASURED_BOTTOM,
MEASURED_TOP,
MEASURED_AREA,
MEASURED_VOLUME,
MEASURED_X,
MEASURED_Y,
MEASURED_Z,
MEASURED_LEVEL_HEIGHT,
];
#[must_use]
pub fn is_derived_set(set: &str) -> bool {
set == CLASSIFICATION_SET || set == MEASURED_SET
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Property {
pub property_set: String,
pub name: String,
pub value: PropertyValue,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub column_types: Option<PropertyColumnTypes>,
pub evidence: Option<Evidence>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PropertyColumnTypes {
pub defining: String,
pub defined: String,
}
impl Property {
pub fn new(
property_set: impl Into<String>,
name: impl Into<String>,
value: PropertyValue,
) -> Result<Self, IrError> {
Ok(Self {
property_set: required(property_set, "property set")?,
name: required(name, "property name")?,
value,
data_type: None,
column_types: None,
evidence: None,
})
}
pub fn with_data_type(mut self, data_type: impl Into<String>) -> Result<Self, IrError> {
self.data_type = Some(required(data_type, "property data type")?);
Ok(self)
}
pub fn with_column_types(
mut self,
defining: impl Into<String>,
defined: impl Into<String>,
) -> Result<Self, IrError> {
if !matches!(self.value, PropertyValue::Table(_)) {
return Err(IrError::ColumnTypesWithoutTable);
}
self.column_types = Some(PropertyColumnTypes {
defining: required(defining, "property column type")?,
defined: required(defined, "property column type")?,
});
Ok(self)
}
pub fn with_evidence(mut self, evidence: Evidence) -> Self {
self.evidence = Some(evidence);
self
}
pub fn column_types(&self) -> Option<&PropertyColumnTypes> {
self.column_types.as_ref()
}
pub fn data_type(&self) -> Option<&str> {
self.data_type.as_deref()
}
pub fn value(&self) -> &PropertyValue {
&self.value
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Object {
pub id: ObjectId,
pub kind: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub external_ids: Vec<ExternalId>,
pub properties: Vec<Property>,
pub classifications: Vec<Classification>,
pub relationships: BTreeMap<String, Vec<ObjectId>>,
}
impl Object {
pub fn new(id: ObjectId, kind: impl Into<String>) -> Self {
Self {
id,
kind: kind.into(),
external_ids: vec![],
properties: vec![],
classifications: vec![],
relationships: BTreeMap::new(),
}
}
pub fn with_external_id(mut self, id: ExternalId) -> Self {
let at = self.external_ids.partition_point(|held| held <= &id);
self.external_ids.insert(at, id);
self
}
pub fn external_id(&self, scheme: &str) -> Option<&str> {
self.external_ids
.iter()
.find(|id| id.scheme == scheme)
.map(|id| id.value.as_str())
}
pub fn with_property(mut self, property: Property) -> Self {
self.properties.push(property);
self
}
pub fn with_classification(mut self, classification: Classification) -> Self {
self.classifications.push(classification);
self
}
pub fn kind(&self) -> &str {
&self.kind
}
pub fn property(&self, set: &str, name: &str) -> Option<&Property> {
self.properties
.iter()
.find(|p| p.property_set == set && p.name == name)
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Project {
objects: BTreeMap<ObjectId, Object>,
}
impl Project {
pub fn new(objects: Vec<Object>) -> Result<Self, IrError> {
let mut result = Self::default();
let mut claimed: BTreeMap<(&SourceId, &ExternalId), &ObjectId> = BTreeMap::new();
for object in &objects {
let mut schemes = std::collections::BTreeSet::new();
if let Some(id) = object
.external_ids
.iter()
.find(|id| !schemes.insert(id.scheme.as_str()))
{
return Err(IrError::ConflictingExternalId {
object: object.id.clone(),
scheme: id.scheme.clone(),
});
}
for id in &object.external_ids {
if let Some(first) = claimed.insert((&object.id.source, id), &object.id) {
let (first, second) = if first <= &object.id {
(first, &object.id)
} else {
(&object.id, first)
};
return Err(IrError::DuplicateExternalId(Box::new(ExternalIdClash {
id: id.clone(),
first: first.clone(),
second: second.clone(),
})));
}
}
}
for object in objects {
if result
.objects
.insert(object.id.clone(), object.clone())
.is_some()
{
return Err(IrError::DuplicateObject(object.id));
}
}
Ok(result)
}
pub fn object(&self, id: &ObjectId) -> Option<&Object> {
self.objects.get(id)
}
pub fn objects(&self) -> impl Iterator<Item = &Object> {
self.objects.values()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Selector {
pub kinds: Vec<String>,
pub classification: Option<Classification>,
}
impl Selector {
pub fn by_kind(kind: impl Into<String>) -> Self {
Self {
kinds: vec![kind.into()],
classification: None,
}
}
pub fn with_classification(
mut self,
system: impl Into<String>,
code: impl Into<String>,
) -> Self {
self.classification = Classification::new(system, code).ok();
self
}
pub fn matches(&self, object: &Object) -> bool {
(self.kinds.is_empty() || self.kinds.iter().any(|k| k == &object.kind))
&& self
.classification
.as_ref()
.is_none_or(|c| object.classifications.contains(c))
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RuleId(String);
impl RuleId {
pub fn new(value: impl Into<String>) -> Result<Self, IrError> {
Ok(Self(required(value, "rule id")?))
}
}
impl fmt::Display for RuleId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Severity {
Error,
Warning,
Info,
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Scope {
Project,
Source(SourceId),
Object(ObjectId),
}
impl Scope {
#[must_use]
pub fn object(&self) -> Option<&ObjectId> {
match self {
Self::Object(object) => Some(object),
Self::Project | Self::Source(_) => None,
}
}
#[must_use]
pub fn source(&self) -> Option<&SourceId> {
match self {
Self::Project => None,
Self::Source(source) => Some(source),
Self::Object(object) => Some(&object.source),
}
}
fn into_wire(self) -> (Option<ObjectId>, Option<SourceId>) {
match self {
Self::Project => (None, None),
Self::Source(source) => (None, Some(source)),
Self::Object(object) => (Some(object), None),
}
}
fn from_wire(object_id: Option<ObjectId>, source: Option<SourceId>) -> Result<Self, String> {
match (object_id, source) {
(None, None) => Ok(Self::Project),
(None, Some(source)) => Ok(Self::Source(source)),
(Some(object), None) => Ok(Self::Object(object)),
(Some(object), Some(source)) => Err(format!(
"outcome names both object {object} and source {source}; an object already names its source"
)),
}
}
}
impl From<ObjectId> for Scope {
fn from(object: ObjectId) -> Self {
Self::Object(object)
}
}
impl From<SourceId> for Scope {
fn from(source: SourceId) -> Self {
Self::Source(source)
}
}
impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Project => f.write_str("project"),
Self::Source(source) => write!(f, "source {source}"),
Self::Object(object) => object.fmt(f),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Place {
pub id: ObjectId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Location {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub storeys: Vec<Place>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub spaces: Vec<Place>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unresolved: Option<String>,
}
impl Location {
pub fn places(&self) -> impl Iterator<Item = &Place> {
self.storeys.iter().chain(&self.spaces)
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "FindingWire", into = "FindingWire")]
pub struct Finding {
pub id: Option<FindingId>,
pub rule_id: RuleId,
pub scope: Scope,
pub severity: Severity,
pub message: String,
pub related: Vec<ObjectId>,
pub evidence: Vec<Evidence>,
pub location: Option<Location>,
pub categories: Vec<String>,
pub decision: Option<FindingDecision>,
}
impl Finding {
#[must_use]
pub fn new(
rule_id: RuleId,
scope: impl Into<Scope>,
severity: Severity,
message: impl Into<String>,
) -> Self {
Self {
id: None,
rule_id,
scope: scope.into(),
severity,
message: message.into(),
related: Vec::new(),
evidence: Vec::new(),
location: None,
categories: Vec::new(),
decision: None,
}
}
#[must_use]
pub fn object_id(&self) -> Option<&ObjectId> {
self.scope.object()
}
#[must_use]
pub fn with_evidence(mut self, evidence: impl IntoIterator<Item = Evidence>) -> Self {
self.evidence = evidence.into_iter().collect();
self.evidence
.sort_by(|a, b| (&a.source, &a.locator).cmp(&(&b.source, &b.locator)));
self.evidence.dedup();
self
}
#[must_use]
pub fn with_related(mut self, related: impl IntoIterator<Item = ObjectId>) -> Self {
self.related = related.into_iter().collect();
self.related.sort();
self.related.dedup();
if let Scope::Object(subject) = &self.scope {
let subject = subject.clone();
self.related.retain(|candidate| candidate != &subject);
}
self
}
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct FindingWire {
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<FindingId>,
rule_id: RuleId,
#[serde(default, skip_serializing_if = "Option::is_none")]
object_id: Option<ObjectId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
source: Option<SourceId>,
severity: Severity,
message: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
related: Vec<ObjectId>,
evidence: Vec<Evidence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
location: Option<Location>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
categories: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
decision: Option<FindingDecision>,
}
impl From<Finding> for FindingWire {
fn from(finding: Finding) -> Self {
let (object_id, source) = finding.scope.into_wire();
Self {
id: finding.id,
rule_id: finding.rule_id,
object_id,
source,
severity: finding.severity,
message: finding.message,
related: finding.related,
evidence: finding.evidence,
location: finding.location,
categories: finding.categories,
decision: finding.decision,
}
}
}
impl TryFrom<FindingWire> for Finding {
type Error = String;
fn try_from(wire: FindingWire) -> Result<Self, String> {
Ok(Self {
id: wire.id,
rule_id: wire.rule_id,
scope: Scope::from_wire(wire.object_id, wire.source)?,
severity: wire.severity,
message: wire.message,
related: wire.related,
evidence: wire.evidence,
location: wire.location,
categories: wire.categories,
decision: wire.decision,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NotEvaluatedReason {
MissingService,
BackendUnavailable,
IncompleteEvidence,
InvalidEvidence,
InvalidDeclaration,
ResourceLimit,
UnboundConcept,
NotRecorded,
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(try_from = "NotEvaluatedWire", into = "NotEvaluatedWire")]
pub struct NotEvaluated {
pub rule_id: RuleId,
pub scope: Scope,
pub reason: NotEvaluatedReason,
pub message: String,
pub location: Option<Location>,
}
impl NotEvaluated {
#[must_use]
pub fn object_id(&self) -> Option<&ObjectId> {
self.scope.object()
}
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct NotEvaluatedWire {
rule_id: RuleId,
#[serde(default)]
object_id: Option<ObjectId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
source: Option<SourceId>,
reason: NotEvaluatedReason,
message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
location: Option<Location>,
}
impl From<NotEvaluated> for NotEvaluatedWire {
fn from(outcome: NotEvaluated) -> Self {
let (object_id, source) = outcome.scope.into_wire();
Self {
rule_id: outcome.rule_id,
object_id,
source,
reason: outcome.reason,
message: outcome.message,
location: outcome.location,
}
}
}
impl TryFrom<NotEvaluatedWire> for NotEvaluated {
type Error = String;
fn try_from(wire: NotEvaluatedWire) -> Result<Self, String> {
Ok(Self {
rule_id: wire.rule_id,
scope: Scope::from_wire(wire.object_id, wire.source)?,
reason: wire.reason,
message: wire.message,
location: wire.location,
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuleStatus {
Passed,
Failed,
NotEvaluated,
NothingSelected,
Skipped,
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RuleSummary {
pub rule_id: RuleId,
pub checked: usize,
pub failed: usize,
pub not_evaluated: usize,
pub status: RuleStatus,
}
impl RuleSummary {
#[must_use]
pub fn skipped(rule_id: RuleId) -> Self {
Self {
rule_id,
checked: 0,
failed: 0,
not_evaluated: 0,
status: RuleStatus::Skipped,
}
}
#[must_use]
pub fn new(
rule_id: RuleId,
checked: usize,
(failed, found): (usize, bool),
(not_evaluated, open): (usize, bool),
) -> Self {
let status = if found {
RuleStatus::Failed
} else if open {
RuleStatus::NotEvaluated
} else if checked == 0 {
RuleStatus::NothingSelected
} else {
RuleStatus::Passed
};
Self {
rule_id,
checked,
failed,
not_evaluated,
status,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Report {
pub findings: Vec<Finding>,
#[serde(default)]
pub not_evaluated: Vec<NotEvaluated>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tables: Vec<ReportTable>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub rules: Vec<RuleSummary>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub stale_decisions: Vec<Decision>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub resources: Vec<Object>,
}
impl Report {
pub fn findings(&self) -> &[Finding] {
&self.findings
}
pub fn not_evaluated(&self) -> &[NotEvaluated] {
&self.not_evaluated
}
pub fn tables(&self) -> &[ReportTable] {
&self.tables
}
pub fn rules(&self) -> &[RuleSummary] {
&self.rules
}
pub fn stale_decisions(&self) -> &[Decision] {
&self.stale_decisions
}
pub fn identify_findings(
&mut self,
project: &Project,
stable_scheme: &str,
) -> Result<(), IdentityError> {
let ids = finding_ids(self, project, stable_scheme)?;
for (finding, id) in self.findings.iter_mut().zip(ids) {
finding.id = Some(id);
}
Ok(())
}
pub fn resource(&self, id: &ObjectId) -> Option<&Object> {
self.resources
.binary_search_by(|resource| resource.id.cmp(id))
.ok()
.map(|index| &self.resources[index])
}
pub fn object<'a>(&'a self, project: &'a Project, id: &ObjectId) -> Option<&'a Object> {
project.object(id).or_else(|| self.resource(id))
}
pub fn table(&self, rule_id: &RuleId, name: &str) -> Option<&ReportTable> {
self.tables
.iter()
.find(|table| table.rule_id() == rule_id && table.name() == name)
}
}