1#![forbid(unsafe_code)]
3#![allow(
4 missing_docs,
5 clippy::missing_errors_doc,
6 clippy::return_self_not_must_use
7)]
8
9use std::{collections::BTreeMap, fmt};
10
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14pub mod contract;
16pub use contract::{DefinitionPackage, RuleSetPackage};
17
18#[derive(Debug, Error, PartialEq, Eq)]
20pub enum IrError {
21 #[error("{kind} must not be blank")]
23 Blank { kind: &'static str },
24 #[error("duplicate object id: {0}")]
26 DuplicateObject(ObjectId),
27}
28
29fn required(value: impl Into<String>, kind: &'static str) -> Result<String, IrError> {
30 let value = value.into();
31 if value.trim().is_empty() {
32 Err(IrError::Blank { kind })
33 } else {
34 Ok(value)
35 }
36}
37
38#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct SourceId {
42 pub system: String,
43 pub document: String,
44}
45impl SourceId {
46 pub fn new(system: impl Into<String>, document: impl Into<String>) -> Result<Self, IrError> {
48 Ok(Self {
49 system: required(system, "source system")?,
50 document: required(document, "source document")?,
51 })
52 }
53}
54impl fmt::Display for SourceId {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{}:{}", self.system, self.document)
57 }
58}
59
60#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct ObjectId {
64 pub source: SourceId,
65 pub local_id: String,
66}
67impl ObjectId {
68 pub fn new(source: SourceId, local_id: impl Into<String>) -> Result<Self, IrError> {
70 Ok(Self {
71 source,
72 local_id: required(local_id, "object local id")?,
73 })
74 }
75}
76impl fmt::Display for ObjectId {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 write!(f, "{}/{}", self.source, self.local_id)
79 }
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum QuantityDimension {
86 Length,
87 Area,
88 Volume,
89}
90
91#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
93#[serde(tag = "type", content = "value", rename_all = "snake_case")]
94pub enum PropertyValue {
95 Null,
96 Boolean(bool),
97 Integer(i64),
98 Decimal(f64),
99 Quantity {
100 value: f64,
101 dimension: QuantityDimension,
102 },
103 String(String),
104}
105
106#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
108#[serde(deny_unknown_fields)]
109pub struct Evidence {
110 pub source: SourceId,
111 pub locator: String,
112 pub exact: bool,
113}
114impl Evidence {
115 pub fn exact(source: SourceId, locator: impl Into<String>) -> Self {
117 Self {
118 source,
119 locator: locator.into(),
120 exact: true,
121 }
122 }
123}
124
125#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct Classification {
129 pub system: String,
130 pub code: String,
131}
132impl Classification {
133 pub fn new(system: impl Into<String>, code: impl Into<String>) -> Result<Self, IrError> {
135 Ok(Self {
136 system: required(system, "classification system")?,
137 code: required(code, "classification code")?,
138 })
139 }
140}
141
142#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct Property {
146 pub property_set: String,
147 pub name: String,
148 pub value: PropertyValue,
149 pub evidence: Option<Evidence>,
150}
151impl Property {
152 pub fn new(
154 property_set: impl Into<String>,
155 name: impl Into<String>,
156 value: PropertyValue,
157 ) -> Result<Self, IrError> {
158 Ok(Self {
159 property_set: required(property_set, "property set")?,
160 name: required(name, "property name")?,
161 value,
162 evidence: None,
163 })
164 }
165 pub fn with_evidence(mut self, evidence: Evidence) -> Self {
167 self.evidence = Some(evidence);
168 self
169 }
170 pub fn value(&self) -> &PropertyValue {
172 &self.value
173 }
174}
175
176#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct Object {
180 pub id: ObjectId,
181 pub kind: String,
182 pub properties: Vec<Property>,
183 pub classifications: Vec<Classification>,
184 pub relationships: BTreeMap<String, Vec<ObjectId>>,
185}
186impl Object {
187 pub fn new(id: ObjectId, kind: impl Into<String>) -> Self {
189 Self {
190 id,
191 kind: kind.into(),
192 properties: vec![],
193 classifications: vec![],
194 relationships: BTreeMap::new(),
195 }
196 }
197 pub fn with_property(mut self, property: Property) -> Self {
199 self.properties.push(property);
200 self
201 }
202 pub fn with_classification(mut self, classification: Classification) -> Self {
204 self.classifications.push(classification);
205 self
206 }
207 pub fn kind(&self) -> &str {
209 &self.kind
210 }
211 pub fn property(&self, set: &str, name: &str) -> Option<&Property> {
213 self.properties
214 .iter()
215 .find(|p| p.property_set == set && p.name == name)
216 }
217}
218
219#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
221#[serde(deny_unknown_fields)]
222pub struct Project {
223 objects: BTreeMap<ObjectId, Object>,
224}
225impl Project {
226 pub fn new(objects: Vec<Object>) -> Result<Self, IrError> {
228 let mut result = Self::default();
229 for object in objects {
230 if result
231 .objects
232 .insert(object.id.clone(), object.clone())
233 .is_some()
234 {
235 return Err(IrError::DuplicateObject(object.id));
236 }
237 }
238 Ok(result)
239 }
240 pub fn object(&self, id: &ObjectId) -> Option<&Object> {
242 self.objects.get(id)
243 }
244 pub fn objects(&self) -> impl Iterator<Item = &Object> {
246 self.objects.values()
247 }
248}
249
250#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(deny_unknown_fields)]
253pub struct Selector {
254 pub kinds: Vec<String>,
255 pub classification: Option<Classification>,
256}
257impl Selector {
258 pub fn by_kind(kind: impl Into<String>) -> Self {
260 Self {
261 kinds: vec![kind.into()],
262 classification: None,
263 }
264 }
265 pub fn with_classification(
267 mut self,
268 system: impl Into<String>,
269 code: impl Into<String>,
270 ) -> Self {
271 self.classification = Classification::new(system, code).ok();
272 self
273 }
274 pub fn matches(&self, object: &Object) -> bool {
276 (self.kinds.is_empty() || self.kinds.iter().any(|k| k == &object.kind))
277 && self
278 .classification
279 .as_ref()
280 .is_none_or(|c| object.classifications.contains(c))
281 }
282}
283
284#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
286#[serde(transparent)]
287pub struct RuleId(String);
288impl RuleId {
289 pub fn new(value: impl Into<String>) -> Result<Self, IrError> {
291 Ok(Self(required(value, "rule id")?))
292 }
293}
294impl fmt::Display for RuleId {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 self.0.fmt(f)
297 }
298}
299
300#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
302#[serde(rename_all = "snake_case")]
303pub enum Severity {
304 Error,
305 Warning,
306 Info,
307}
308#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
310#[serde(deny_unknown_fields)]
311pub struct Finding {
312 pub rule_id: RuleId,
313 pub object_id: ObjectId,
314 pub severity: Severity,
315 pub message: String,
316 pub evidence: Vec<Evidence>,
317}
318#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
320#[serde(rename_all = "snake_case")]
321pub enum NotEvaluatedReason {
322 MissingService,
323 BackendUnavailable,
324 IncompleteEvidence,
325 InvalidEvidence,
326 InvalidDeclaration,
327 ResourceLimit,
328}
329#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
331#[serde(deny_unknown_fields)]
332pub struct NotEvaluated {
333 pub rule_id: RuleId,
334 pub object_id: Option<ObjectId>,
335 pub reason: NotEvaluatedReason,
336 pub message: String,
337}
338#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
340#[serde(deny_unknown_fields)]
341pub struct Report {
342 pub findings: Vec<Finding>,
343 #[serde(default)]
344 pub not_evaluated: Vec<NotEvaluated>,
345}
346impl Report {
347 pub fn findings(&self) -> &[Finding] {
349 &self.findings
350 }
351 pub fn not_evaluated(&self) -> &[NotEvaluated] {
353 &self.not_evaluated
354 }
355}