Skip to main content

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}