Skip to content

feat: add model registry to object serializer #2433

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 146 additions & 21 deletions src/serializer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { ObjectSerializer as InternalSerializer, V1ObjectMeta } from './gen/models/ObjectSerializer.js';

type KubernetesObjectHeader = {
apiVersion: string;
kind: string;
};

const isKubernetesObject = (data: unknown): data is KubernetesObjectHeader =>
!!data && typeof data === 'object' && 'apiVersion' in data && 'kind' in data;

type AttributeType = {
name: string;
baseName: string;
Expand Down Expand Up @@ -38,30 +46,38 @@ class KubernetesObject {
format: '',
},
];
}

const isKubernetesObject = (data: unknown): boolean =>
!!data && typeof data === 'object' && 'apiVersion' in data && 'kind' in data;

/**
* Wraps the ObjectSerializer to support custom resources and generic Kubernetes objects.
*/
export class ObjectSerializer extends InternalSerializer {
public static serialize(data: any, type: string, format: string = ''): any {
const obj = InternalSerializer.serialize(data, type, format);
if (obj !== data) {
return obj;
public serialize(): any {
const instance: Record<string, any> = {};
for (const attributeType of KubernetesObject.attributeTypeMap) {
const value = this[attributeType.baseName];
if (value !== undefined) {
instance[attributeType.name] = InternalSerializer.serialize(
this[attributeType.baseName],
attributeType.type,
attributeType.format,
);
}
}
// add all unknown properties as is.
for (const [key, value] of Object.entries(this)) {
if (KubernetesObject.attributeTypeMap.find((t) => t.name === key)) {
continue;
}
instance[key] = value;
}
return instance;
}

public static fromUnknown(data: unknown): KubernetesObject {
if (!isKubernetesObject(data)) {
return obj;
throw new Error(`Unable to deseriliaze non-Kubernetes object ${data}.`);
}

const instance: Record<string, any> = {};
const instance = new KubernetesObject();
for (const attributeType of KubernetesObject.attributeTypeMap) {
const value = data[attributeType.baseName];
if (value !== undefined) {
instance[attributeType.name] = InternalSerializer.serialize(
instance[attributeType.name] = InternalSerializer.deserialize(
data[attributeType.baseName],
attributeType.type,
attributeType.format,
Expand All @@ -77,23 +93,114 @@ export class ObjectSerializer extends InternalSerializer {
}
return instance;
}
}

public static deserialize(data: any, type: string, format: string = ''): any {
const obj = InternalSerializer.deserialize(data, type, format);
export interface Serializer {
serialize(data: any, type: string, format?: string): any;
deserialize(data: any, type: string, format?): any;
}

export type GroupVersionKind = {
group: string;
version: string;
kind: string;
};

type ModelRegistry = {
[gv: string]: {
[kind: string]: Serializer;
};
};

const gvString = ({ group, version }: GroupVersionKind): string => [group, version].join('/');

const gvkFromObject = (obj: KubernetesObjectHeader): GroupVersionKind => {
const [g, v] = obj.apiVersion.split('/');
return {
kind: obj.kind,
group: v ? g : '',
version: v ? v : g,
};
};

/**
* Default serializer that uses the KubernetesObject to serialize and deserialize
* any object using only the minimum required attributes.
*/
export const defaultSerializer: Serializer = {
serialize: (data: any, type: string, format?: string): any => {
if (data instanceof KubernetesObject) {
return data.serialize();
}
return KubernetesObject.fromUnknown(data).serialize();
},
deserialize: (data: any, type: string, format?): any => {
return KubernetesObject.fromUnknown(data);
},
};

/**
* Wraps the ObjectSerializer to support custom resources and generic Kubernetes objects.
*
* CustomResources that are unknown to the ObjectSerializer can be registered
* by using ObjectSerializer.registerModel().
*/
export class ObjectSerializer extends InternalSerializer {
private static modelRegistry: ModelRegistry = {};

/**
* Adds a dedicated seriliazer for a Kubernetes resource.
* Every resource is uniquly identified using its group, version and kind.
* @param gvk
* @param serializer
*/
public static registerModel(gvk: GroupVersionKind, serializer: Serializer) {
const gv = gvString(gvk);
const kinds = (this.modelRegistry[gv] ??= {});
if (kinds[gvk.kind]) {
throw new Error(`Kind ${gvk.kind} of ${gv} is already defined`);
}
kinds[gvk.kind] = serializer;
}

/**
* Removes all registered models from the registry.
*/
public static clearModelRegistry(): void {
this.modelRegistry = {};
}

private static getSerializerForObject(obj: unknown): undefined | Serializer {
if (!isKubernetesObject(obj)) {
return undefined;
}
const gvk = gvkFromObject(obj);
return ObjectSerializer.modelRegistry[gvString(gvk)]?.[gvk.kind];
}

public static serialize(data: any, type: string, format: string = ''): any {
const serializer = ObjectSerializer.getSerializerForObject(data);
if (serializer) {
return serializer.serialize(data, type, format);
}
if (data instanceof KubernetesObject) {
return data.serialize();
}

const obj = InternalSerializer.serialize(data, type, format);
if (obj !== data) {
// the serializer knows the type and already deserialized it.
return obj;
}

if (!isKubernetesObject(data)) {
return obj;
}

const instance = new KubernetesObject();
const instance: Record<string, any> = {};
for (const attributeType of KubernetesObject.attributeTypeMap) {
const value = data[attributeType.baseName];
if (value !== undefined) {
instance[attributeType.name] = InternalSerializer.deserialize(
instance[attributeType.name] = InternalSerializer.serialize(
data[attributeType.baseName],
attributeType.type,
attributeType.format,
Expand All @@ -109,4 +216,22 @@ export class ObjectSerializer extends InternalSerializer {
}
return instance;
}

public static deserialize(data: any, type: string, format: string = ''): any {
const serializer = ObjectSerializer.getSerializerForObject(data);
if (serializer) {
return serializer.deserialize(data, type, format);
}
const obj = InternalSerializer.deserialize(data, type, format);
if (obj !== data) {
// the serializer knows the type and already deserialized it.
return obj;
}

if (!isKubernetesObject(data)) {
return obj;
}

return KubernetesObject.fromUnknown(data);
}
}
Loading