Skip to main content

ant_types/
ids.rs

1//! Identifier newtypes: tenant, project, vertex, edge, namespace,
2//! type name. Each is a thin wrapper so ids of different planes can't
3//! be mixed up silently.
4
5use serde::{Deserialize, Serialize};
6
7/// Tenant identifier. Tenants are the top-level isolation boundary.
8#[derive(
9    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
10)]
11#[serde(transparent)]
12pub struct TenantId(pub u64);
13
14// `#[serde(transparent)]` newtypes serialize as their inner value, so
15// the schema has to say `integer` and not `object` — the derive would
16// otherwise disagree with the wire.
17#[cfg(feature = "utoipa")]
18impl<'s> utoipa::ToSchema<'s> for TenantId {
19    fn schema() -> (&'s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
20        ("TenantId", transparent_u64_schema("Tenant identifier."))
21    }
22}
23
24/// Project identifier. A project lives under one tenant and has its own
25/// schema namespace.
26#[derive(
27    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
28)]
29#[serde(transparent)]
30pub struct ProjectId(pub u64);
31
32#[cfg(feature = "utoipa")]
33impl<'s> utoipa::ToSchema<'s> for ProjectId {
34    fn schema() -> (&'s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
35        ("ProjectId", transparent_u64_schema("Project identifier."))
36    }
37}
38
39#[cfg(feature = "utoipa")]
40fn transparent_u64_schema(description: &str) -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
41    use utoipa::openapi::schema::{ObjectBuilder, SchemaFormat, SchemaType};
42    ObjectBuilder::new()
43        .schema_type(SchemaType::Integer)
44        .format(Some(SchemaFormat::KnownFormat(
45            utoipa::openapi::KnownFormat::Int64,
46        )))
47        .minimum(Some(0.0))
48        .description(Some(description))
49        .into()
50}
51
52/// Project namespace (e.g. "Antares"). Prepended to all type names at the
53/// SPG layer (`Antares.Deal`).
54#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
55#[serde(transparent)]
56pub struct Namespace(pub String);
57
58/// Namespace-qualified SPG type name, e.g. `Antares.Deal` or `Antares.Chunk`.
59#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
60#[serde(transparent)]
61pub struct TypeName(pub String);
62
63impl TypeName {
64    /// Build a namespace-qualified type name from an unqualified label.
65    pub fn qualified(ns: &Namespace, label: &str) -> Self {
66        if label.contains('.') {
67            // Already qualified.
68            Self(label.to_owned())
69        } else {
70            Self(format!("{}.{}", ns.0, label))
71        }
72    }
73}
74
75/// Vertex business id (the `id` field on the wire, e.g. "deal_hooli_001").
76#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
77#[serde(transparent)]
78pub struct VertexId(pub String);
79
80/// Edge id (assigned by the caller in `writerGraph`, e.g. "e1").
81#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
82#[serde(transparent)]
83pub struct EdgeId(pub String);
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn qualified_name_prepends_namespace() {
91        let ns = Namespace("Antares".into());
92        assert_eq!(TypeName::qualified(&ns, "Deal").0, "Antares.Deal");
93    }
94
95    #[test]
96    fn qualified_name_is_idempotent() {
97        let ns = Namespace("Antares".into());
98        assert_eq!(TypeName::qualified(&ns, "Antares.Deal").0, "Antares.Deal");
99    }
100}