Skip to main content

openlineage_client/
naming.rs

1//! OpenLineage dataset naming-spec mapping.
2//!
3//! Conformance here is what lets lineage graphs join across tools. See
4//! <https://openlineage.io/docs/spec/naming>.
5
6use url::Url;
7
8/// An OpenLineage dataset name: a `namespace` plus a `name` unique within it.
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct DatasetName {
11    /// The dataset's namespace (e.g. a storage root or job namespace).
12    pub namespace: String,
13    /// The dataset's name, unique within its namespace.
14    pub name: String,
15}
16
17impl DatasetName {
18    /// Map a storage location URL to its OpenLineage `(namespace, name)`.
19    ///
20    /// - `s3://bucket/key...` -> namespace `s3://bucket`, name `key...`
21    /// - `gs://bucket/key...` -> namespace `gs://bucket`, name `key...`
22    /// - `file:///path`       -> namespace `file`, name `/path`
23    /// - anything else        -> namespace `{scheme}://{host}`, name `{path}`
24    pub fn from_location(url: &Url) -> Self {
25        let scheme = url.scheme();
26        match scheme {
27            "s3" | "s3a" | "gs" | "gcs" | "abfs" | "abfss" | "wasbs" => {
28                let bucket = url.host_str().unwrap_or_default();
29                let name = url.path().trim_start_matches('/').to_string();
30                DatasetName {
31                    namespace: format!("{scheme}://{bucket}"),
32                    name,
33                }
34            }
35            "file" => DatasetName {
36                namespace: "file".to_string(),
37                name: url.path().to_string(),
38            },
39            _ => {
40                let host = url.host_str().unwrap_or_default();
41                DatasetName {
42                    namespace: format!("{scheme}://{host}"),
43                    name: url.path().trim_start_matches('/').to_string(),
44                }
45            }
46        }
47    }
48
49    /// A fallback name for a table reference that has no resolvable location
50    /// (e.g. an in-memory or unqualified table). Uses the configured job
51    /// namespace so it still lands somewhere sensible.
52    pub fn from_table_ref(default_namespace: &str, table_ref: &str) -> Self {
53        DatasetName {
54            namespace: default_namespace.to_string(),
55            name: table_ref.to_string(),
56        }
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn s3_location() {
66        let url = Url::parse("s3://open-lakehouse/warehouse/t1").unwrap();
67        let n = DatasetName::from_location(&url);
68        assert_eq!(n.namespace, "s3://open-lakehouse");
69        assert_eq!(n.name, "warehouse/t1");
70    }
71
72    #[test]
73    fn file_location() {
74        let url = Url::parse("file:///tmp/data/t").unwrap();
75        let n = DatasetName::from_location(&url);
76        assert_eq!(n.namespace, "file");
77        assert_eq!(n.name, "/tmp/data/t");
78    }
79}