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    /// 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}