ocel_transform/recipe.rs
1//! The recipe format: a named, ordered list of transformation steps.
2//!
3//! JSON uses externally tagged steps, so a recipe reads as data:
4//!
5//! ```json
6//! {
7//! "name": "clean",
8//! "steps": [
9//! { "dropEventTypes": ["subscribed", "mentioned"] },
10//! { "dropEventsWhere": { "eventType": "comment", "attr": "body",
11//! "matches": "(?i)^(thanks|thank you|lgtm)[!. ]*$" } },
12//! { "renameEventTypes": { "labeled": "triage", "unlabeled": "triage" } },
13//! { "timeWindow": { "from": "2024-01-01", "to": "2025-01-01" } },
14//! { "keepObjectTypes": ["issue", "user"] },
15//! "dropObjectsWithoutEvents"
16//! ]
17//! }
18//! ```
19
20use std::collections::BTreeMap;
21
22use serde::{Deserialize, Serialize};
23
24/// A named transformation pipeline.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct Recipe {
28 pub name: String,
29 #[serde(default)]
30 pub steps: Vec<Step>,
31}
32
33/// One transformation step. Applied in recipe order; each step's effect is
34/// reported (events/objects before and after).
35#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase")]
37pub enum Step {
38 /// Drop every event of these types. Objects stay (use
39 /// [`Step::DropObjectsWithoutEvents`] to clean up afterwards).
40 DropEventTypes(Vec<String>),
41 /// Keep only events of these types.
42 KeepEventTypes(Vec<String>),
43 /// Drop events matching the predicate (all set conditions must hold).
44 DropEventsWhere(EventPredicate),
45 /// Rename event types; several old names mapping to one new name merge.
46 RenameEventTypes(BTreeMap<String, String>),
47 /// Keep only events inside the half-open window `[from, to)`. Object
48 /// attribute observations are not trimmed.
49 TimeWindow(TimeWindow),
50 /// Keep objects of these types; events no longer related to any kept
51 /// object are dropped (their other E2O links are stripped).
52 KeepObjectTypes(Vec<String>),
53 /// Drop objects no remaining event references (O2O links to dropped
54 /// objects are stripped from survivors).
55 DropObjectsWithoutEvents,
56 /// Re-key objects through an alias table (identity resolution as data,
57 /// not code): ids map to their alias or stay as they are; several ids
58 /// mapping to one canonical id merge, and every E2O/O2O reference
59 /// follows.
60 MapObjectIds(AliasTable),
61}
62
63/// The alias table of [`Step::MapObjectIds`].
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66pub struct AliasTable {
67 /// old id → canonical id.
68 pub aliases: BTreeMap<String, String>,
69}
70
71impl Step {
72 /// Stable display label (matches the JSON tag).
73 #[must_use]
74 pub fn label(&self) -> &'static str {
75 match self {
76 Step::DropEventTypes(_) => "dropEventTypes",
77 Step::KeepEventTypes(_) => "keepEventTypes",
78 Step::DropEventsWhere(_) => "dropEventsWhere",
79 Step::RenameEventTypes(_) => "renameEventTypes",
80 Step::TimeWindow(_) => "timeWindow",
81 Step::KeepObjectTypes(_) => "keepObjectTypes",
82 Step::DropObjectsWithoutEvents => "dropObjectsWithoutEvents",
83 Step::MapObjectIds(_) => "mapObjectIds",
84 }
85 }
86}
87
88/// Conditions on one event; all set fields must hold (AND). At least one
89/// condition is required, and value conditions require `attr`. An event
90/// without the named attribute never matches.
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase", deny_unknown_fields)]
93pub struct EventPredicate {
94 /// Match only events of this type.
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub event_type: Option<String>,
97 /// Attribute the value conditions below apply to.
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub attr: Option<String>,
100 /// Value (as text) equals this exactly.
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub equals: Option<String>,
103 /// Value (as text) matches this regex.
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub matches: Option<String>,
106 /// Numeric value is at least this (non-numeric values never match).
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub min: Option<f64>,
109 /// Numeric value is at most this.
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub max: Option<f64>,
112}
113
114/// Half-open time window `[from, to)`. Bounds accept RFC 3339 or
115/// `YYYY-MM-DD`; a date-only `to` means "up to and including that day"
116/// (it parses to the following midnight).
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase", deny_unknown_fields)]
119pub struct TimeWindow {
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub from: Option<String>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub to: Option<String>,
124}