use serde_json::{Map, Value};
use crate::json_object::JsonMapStrExt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct K8sWireIdentity {
pub api_version: &'static str,
pub kind: &'static str,
}
impl K8sWireIdentity {
pub const fn new(api_version: &'static str, kind: &'static str) -> Self {
Self { api_version, kind }
}
pub fn as_json(self) -> Value {
let mut m = Map::with_capacity(2);
self.insert_identity_slots(&mut m);
Value::Object(m)
}
pub fn resource_json(self, extras: Value) -> Value {
let mut m = match extras {
Value::Object(m) => m,
_ => Map::new(),
};
self.insert_identity_slots(&mut m);
Value::Object(m)
}
#[inline]
fn insert_identity_slots(self, m: &mut Map<String, Value>) {
m.insert_str("apiVersion", self.api_version);
m.insert_str("kind", self.kind);
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn new_pairs_api_version_and_kind_by_position() {
let id = K8sWireIdentity::new("group.io/v1", "Widget");
assert_eq!(id.api_version, "group.io/v1");
assert_eq!(id.kind, "Widget");
}
#[test]
fn new_is_const_reachable() {
const ID: K8sWireIdentity = K8sWireIdentity::new("g/v", "K");
assert_eq!(ID.api_version, "g/v");
assert_eq!(ID.kind, "K");
}
#[test]
fn as_json_emits_two_slot_object() {
let id = K8sWireIdentity::new("group.io/v1", "Widget");
let v = id.as_json();
let obj = v.as_object().expect("as_json emits an Object");
assert_eq!(obj.len(), 2);
assert_eq!(obj["apiVersion"], "group.io/v1");
assert_eq!(obj["kind"], "Widget");
}
#[test]
fn resource_json_merges_identity_with_extras() {
let id = K8sWireIdentity::new("group.io/v1", "Widget");
let out = id.resource_json(json!({
"metadata": {"name": "foo"},
"spec": {"replicas": 3},
}));
let obj = out.as_object().expect("resource_json emits an Object");
assert_eq!(obj["apiVersion"], "group.io/v1");
assert_eq!(obj["kind"], "Widget");
assert_eq!(obj["metadata"]["name"], "foo");
assert_eq!(obj["spec"]["replicas"], 3);
}
#[test]
fn resource_json_identity_slots_win_over_extras_collision() {
let id = K8sWireIdentity::new("group.io/v1", "Widget");
let out = id.resource_json(json!({
"apiVersion": "wrong.io/v0",
"kind": "WrongKind",
"metadata": {"name": "foo"},
}));
let obj = out.as_object().unwrap();
assert_eq!(obj["apiVersion"], "group.io/v1");
assert_eq!(obj["kind"], "Widget");
assert_eq!(obj["metadata"]["name"], "foo");
}
#[test]
fn resource_json_non_object_extras_returns_pure_identity() {
let id = K8sWireIdentity::new("group.io/v1", "Widget");
let out = id.resource_json(Value::Null);
let obj = out.as_object().unwrap();
assert_eq!(obj.len(), 2);
assert_eq!(obj["apiVersion"], "group.io/v1");
assert_eq!(obj["kind"], "Widget");
let out2 = id.resource_json(Value::String("scalar-extras".into()));
let obj2 = out2.as_object().unwrap();
assert_eq!(obj2.len(), 2);
assert_eq!(obj2["apiVersion"], "group.io/v1");
}
#[test]
fn resource_json_empty_object_extras_returns_pure_identity() {
let id = K8sWireIdentity::new("group.io/v1", "Widget");
let out = id.resource_json(json!({}));
let obj = out.as_object().unwrap();
assert_eq!(obj.len(), 2);
assert_eq!(obj["apiVersion"], "group.io/v1");
assert_eq!(obj["kind"], "Widget");
}
#[test]
fn as_json_and_resource_json_agree_on_identity_slots() {
let id = K8sWireIdentity::new("group.io/v1", "Widget");
let pure = id.as_json();
let composed = id.resource_json(json!({"metadata": {"name": "foo"}}));
assert_eq!(pure["apiVersion"], composed["apiVersion"]);
assert_eq!(pure["kind"], composed["kind"]);
}
#[test]
fn struct_equality_pins_the_pair_axis() {
let a = K8sWireIdentity::new("g/v1", "K");
let b = K8sWireIdentity::new("g/v1", "K");
let c = K8sWireIdentity::new("g/v2", "K");
let d = K8sWireIdentity::new("g/v1", "L");
assert_eq!(a, b);
assert_ne!(a, c);
assert_ne!(a, d);
}
#[test]
fn insert_identity_slots_stamps_exactly_api_version_and_kind() {
let id = K8sWireIdentity::new("group.io/v1", "Widget");
let mut m = Map::new();
id.insert_identity_slots(&mut m);
assert_eq!(m.len(), 2, "helper must stamp exactly 2 slots");
assert_eq!(
m.get("apiVersion"),
Some(&Value::String("group.io/v1".to_string())),
"apiVersion slot must carry the identity's api_version verbatim as Value::String",
);
assert_eq!(
m.get("kind"),
Some(&Value::String("Widget".to_string())),
"kind slot must carry the identity's kind verbatim as Value::String",
);
}
#[test]
fn insert_identity_slots_overwrites_pre_existing_apiversion_and_kind_keys() {
let id = K8sWireIdentity::new("group.io/v1", "Widget");
let mut m = Map::new();
m.insert(
"apiVersion".to_string(),
Value::String("wrong.io/v0".to_string()),
);
m.insert("kind".to_string(), Value::String("WrongKind".to_string()));
m.insert("extra".to_string(), Value::String("preserved".to_string()));
id.insert_identity_slots(&mut m);
assert_eq!(m["apiVersion"], "group.io/v1");
assert_eq!(m["kind"], "Widget");
assert_eq!(
m["extra"], "preserved",
"helper must NOT touch non-identity keys the caller pre-seeded",
);
}
#[test]
fn as_json_delegates_through_insert_identity_slots_bytewise() {
for id in [
K8sWireIdentity::new("group.io/v1", "Widget"),
K8sWireIdentity::new("kustomize.toolkit.fluxcd.io/v1", "Kustomization"),
K8sWireIdentity::new("networking.k8s.io/v1", "Ingress"),
K8sWireIdentity::new("v1", "ConfigMap"),
] {
let via_as_json = id.as_json();
let via_helper = {
let mut m = Map::new();
id.insert_identity_slots(&mut m);
Value::Object(m)
};
assert_eq!(
via_as_json, via_helper,
"as_json must delegate through insert_identity_slots for {id:?}",
);
}
}
#[test]
fn resource_json_delegates_through_insert_identity_slots_bytewise_on_null_extras() {
for id in [
K8sWireIdentity::new("group.io/v1", "Widget"),
K8sWireIdentity::new("helm.toolkit.fluxcd.io/v2", "HelmRelease"),
K8sWireIdentity::new("externaldns.k8s.io/v1alpha1", "DNSEndpoint"),
] {
let via_resource_json = id.resource_json(Value::Null);
let via_helper = {
let mut m = Map::new();
id.insert_identity_slots(&mut m);
Value::Object(m)
};
assert_eq!(
via_resource_json, via_helper,
"resource_json(Null) must delegate through insert_identity_slots for {id:?}",
);
}
}
#[test]
fn as_json_and_resource_json_share_body_by_construction() {
for id in [
K8sWireIdentity::new("group.io/v1", "Widget"),
K8sWireIdentity::new("kustomize.toolkit.fluxcd.io/v1", "Kustomization"),
K8sWireIdentity::new("helm.toolkit.fluxcd.io/v2", "HelmRelease"),
K8sWireIdentity::new("source.toolkit.fluxcd.io/v1beta2", "OCIRepository"),
K8sWireIdentity::new("networking.k8s.io/v1", "Ingress"),
K8sWireIdentity::new("externaldns.k8s.io/v1alpha1", "DNSEndpoint"),
K8sWireIdentity::new("batch/v1", "Job"),
K8sWireIdentity::new("v1", "ConfigMap"),
] {
let pure = id.as_json();
let composed = id.resource_json(Value::Null);
assert_eq!(
pure["apiVersion"], composed["apiVersion"],
"apiVersion slot must agree across both peers for {id:?}",
);
assert_eq!(
pure["kind"], composed["kind"],
"kind slot must agree across both peers for {id:?}",
);
assert_eq!(pure, composed, "full emit must agree for {id:?}");
}
}
#[test]
fn insert_identity_slots_composes_through_json_map_str_ext_insert_str_bytewise() {
for id in [
K8sWireIdentity::new("group.io/v1", "Widget"),
K8sWireIdentity::new("kustomize.toolkit.fluxcd.io/v1", "Kustomization"),
K8sWireIdentity::new("helm.toolkit.fluxcd.io/v2", "HelmRelease"),
K8sWireIdentity::new("source.toolkit.fluxcd.io/v1beta2", "OCIRepository"),
K8sWireIdentity::new("networking.k8s.io/v1", "Ingress"),
K8sWireIdentity::new("externaldns.k8s.io/v1alpha1", "DNSEndpoint"),
K8sWireIdentity::new("batch/v1", "Job"),
K8sWireIdentity::new("v1", "ConfigMap"),
] {
let via_primitive = id.as_json();
let mut hand = Map::with_capacity(2);
hand.insert_str("apiVersion", id.api_version);
hand.insert_str("kind", id.kind);
let via_hand = Value::Object(hand);
assert_eq!(
via_primitive, via_hand,
"insert_identity_slots must compose byte-identically to an \
`insert_str`-authored (apiVersion, kind) pair for {id:?}",
);
}
}
}