1use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use std::time::Duration;
7
8#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
10#[serde(deny_unknown_fields)]
11pub struct LineageConfig {
12 #[serde(rename = "type", default)]
14 pub kind: LineageKind,
15 pub namespace: String,
17 pub transport: Transport,
19 #[serde(default = "default_job_name")]
22 pub job_name: String,
23 #[serde(default)]
25 pub parent_job: Option<ParentJob>,
26 #[serde(default)]
28 pub include_column_lineage: bool,
29 #[serde(default)]
31 pub include_schema_facet: bool,
32 #[serde(default)]
35 pub include_source_code_facet: bool,
36 #[serde(default)]
38 pub emit_on: EmitOn,
39 #[serde(default = "default_sample")]
41 pub sample_records: usize,
42 #[serde(
44 with = "faucet_core::config::duration_secs",
45 default = "default_heartbeat"
46 )]
47 #[schemars(with = "u64")]
48 pub heartbeat_interval: Duration,
49}
50
51#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
52#[serde(rename_all = "snake_case")]
53pub enum LineageKind {
54 #[default]
55 Openlineage,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
65#[serde(tag = "type", content = "config", rename_all = "snake_case")]
66pub enum Transport {
67 Http {
69 url: String,
70 #[serde(
71 with = "faucet_core::config::duration_secs",
72 default = "default_http_timeout"
73 )]
74 #[schemars(with = "u64")]
75 timeout_secs: Duration,
76 #[serde(default)]
77 auth: Option<HttpAuth>,
78 },
79 File { path: PathBuf },
81 #[cfg(feature = "transport-kafka")]
83 Kafka { brokers: String, topic: String },
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
89#[serde(tag = "type", content = "config", rename_all = "snake_case")]
90pub enum HttpAuth {
91 Bearer { token: String },
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
96#[serde(deny_unknown_fields)]
97pub struct ParentJob {
98 pub namespace: String,
99 pub name: String,
100 #[serde(default)]
101 pub run_id: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
106#[serde(deny_unknown_fields)]
107pub struct EmitOn {
108 #[serde(default = "default_true")]
109 pub start: bool,
110 #[serde(default)]
111 pub running: bool,
112 #[serde(default = "default_true")]
113 pub complete: bool,
114 #[serde(default = "default_true")]
115 pub fail: bool,
116 #[serde(default = "default_true")]
117 pub abort: bool,
118}
119
120impl Default for EmitOn {
121 fn default() -> Self {
122 Self {
123 start: true,
124 running: false,
125 complete: true,
126 fail: true,
127 abort: true,
128 }
129 }
130}
131
132fn default_true() -> bool {
133 true
134}
135fn default_job_name() -> String {
136 "${name}::${row_id}".to_string()
137}
138fn default_sample() -> usize {
139 100
140}
141fn default_heartbeat() -> Duration {
142 Duration::from_secs(30)
143}
144fn default_http_timeout() -> Duration {
145 Duration::from_secs(10)
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn deserializes_http_transport_with_defaults() {
154 let json = serde_json::json!({
155 "type": "openlineage",
156 "namespace": "prod.warehouse",
157 "transport": { "type": "http", "config": { "url": "https://marquez/api/v1/lineage" } }
158 });
159 let cfg: LineageConfig = serde_json::from_value(json).unwrap();
160 assert_eq!(cfg.namespace, "prod.warehouse");
161 assert_eq!(cfg.job_name, "${name}::${row_id}");
162 assert!(cfg.emit_on.start && cfg.emit_on.complete && !cfg.emit_on.running);
163 assert_eq!(cfg.sample_records, 100);
164 assert!(matches!(cfg.transport, Transport::Http { .. }));
165 }
166
167 #[test]
168 fn deserializes_file_transport() {
169 let json = serde_json::json!({
170 "namespace": "n",
171 "transport": { "type": "file", "config": { "path": "/tmp/ol.jsonl" } }
172 });
173 let cfg: LineageConfig = serde_json::from_value(json).unwrap();
174 assert!(matches!(cfg.transport, Transport::File { .. }));
175 }
176
177 #[test]
178 fn deserializes_http_transport_with_bearer_auth() {
179 let json = serde_json::json!({
181 "namespace": "n",
182 "transport": {
183 "type": "http",
184 "config": {
185 "url": "https://marquez/api/v1/lineage",
186 "auth": { "type": "bearer", "config": { "token": "secret" } }
187 }
188 }
189 });
190 let cfg: LineageConfig = serde_json::from_value(json).unwrap();
191 match cfg.transport {
192 Transport::Http {
193 auth: Some(HttpAuth::Bearer { token }),
194 ..
195 } => {
196 assert_eq!(token, "secret");
197 }
198 _ => panic!("expected http transport with bearer auth"),
199 }
200 }
201
202 #[test]
203 fn rejects_unknown_field() {
204 let json = serde_json::json!({
205 "namespace": "n",
206 "transport": { "type": "file", "config": { "path": "/tmp/ol.jsonl" } },
207 "bogus": true
208 });
209 assert!(serde_json::from_value::<LineageConfig>(json).is_err());
210 }
211
212 #[test]
213 fn schema_generates() {
214 let _ = schemars::schema_for!(LineageConfig);
215 }
216}