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}
57
58impl Step {
59 /// Stable display label (matches the JSON tag).
60 #[must_use]
61 pub fn label(&self) -> &'static str {
62 match self {
63 Step::DropEventTypes(_) => "dropEventTypes",
64 Step::KeepEventTypes(_) => "keepEventTypes",
65 Step::DropEventsWhere(_) => "dropEventsWhere",
66 Step::RenameEventTypes(_) => "renameEventTypes",
67 Step::TimeWindow(_) => "timeWindow",
68 Step::KeepObjectTypes(_) => "keepObjectTypes",
69 Step::DropObjectsWithoutEvents => "dropObjectsWithoutEvents",
70 }
71 }
72}
73
74/// Conditions on one event; all set fields must hold (AND). At least one
75/// condition is required, and value conditions require `attr`. An event
76/// without the named attribute never matches.
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase", deny_unknown_fields)]
79pub struct EventPredicate {
80 /// Match only events of this type.
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub event_type: Option<String>,
83 /// Attribute the value conditions below apply to.
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub attr: Option<String>,
86 /// Value (as text) equals this exactly.
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub equals: Option<String>,
89 /// Value (as text) matches this regex.
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub matches: Option<String>,
92 /// Numeric value is at least this (non-numeric values never match).
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub min: Option<f64>,
95 /// Numeric value is at most this.
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub max: Option<f64>,
98}
99
100/// Half-open time window `[from, to)`. Bounds accept RFC 3339 or
101/// `YYYY-MM-DD`; a date-only `to` means "up to and including that day"
102/// (it parses to the following midnight).
103#[derive(Debug, Clone, Default, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase", deny_unknown_fields)]
105pub struct TimeWindow {
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub from: Option<String>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub to: Option<String>,
110}