#![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};
#[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),
}
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, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuantityDimension {
Length,
Area,
Volume,
}
#[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),
}
#[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")?,
})
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Property {
pub property_set: String,
pub name: String,
pub value: PropertyValue,
pub evidence: Option<Evidence>,
}
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,
evidence: None,
})
}
pub fn with_evidence(mut self, evidence: Evidence) -> Self {
self.evidence = Some(evidence);
self
}
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,
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(),
properties: vec![],
classifications: vec![],
relationships: BTreeMap::new(),
}
}
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();
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, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Finding {
pub rule_id: RuleId,
pub object_id: ObjectId,
pub severity: Severity,
pub message: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub related: Vec<ObjectId>,
pub evidence: Vec<Evidence>,
}
impl Finding {
#[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();
self.related
.retain(|candidate| candidate != &self.object_id);
self
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NotEvaluatedReason {
MissingService,
BackendUnavailable,
IncompleteEvidence,
InvalidEvidence,
InvalidDeclaration,
ResourceLimit,
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NotEvaluated {
pub rule_id: RuleId,
pub object_id: Option<ObjectId>,
pub reason: NotEvaluatedReason,
pub message: String,
}
#[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>,
}
impl Report {
pub fn findings(&self) -> &[Finding] {
&self.findings
}
pub fn not_evaluated(&self) -> &[NotEvaluated] {
&self.not_evaluated
}
}