diff --git a/README.md b/README.md index 2746c62..023cee1 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,7 @@ You can use type-safe Kubernetes models from the `kubernetes-models` package: ```typescript import { Deployment } from "kubernetes-models/apps/v1"; import { Pod } from "kubernetes-models/v1"; +import { fromModel } from "@crossplane-org/function-sdk-typescript"; // Create a type-safe Pod const pod = new Pod({ @@ -200,7 +201,10 @@ const pod = new Pod({ // Validate the pod pod.validate(); -// Convert to Resource +// Convert to Resource using the simplified helper +dcds["my-pod"] = fromModel(pod); + +// Alternative: Using the lower-level API dcds["my-pod"] = Resource.fromJSON({ resource: pod.toJSON() }); diff --git a/src/index.ts b/src/index.ts index 1e22898..1b008da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export { asObject, asStruct, fromObject, + fromModel, toObject, newDesiredComposed, mustStructObject, diff --git a/src/resource/resource.test.ts b/src/resource/resource.test.ts new file mode 100644 index 0000000..0932b03 --- /dev/null +++ b/src/resource/resource.test.ts @@ -0,0 +1,130 @@ +import { describe, it, expect } from 'vitest'; +import { fromModel, fromObject, toObject } from './resource.js'; +import { Resource, Ready } from '../proto/run_function.js'; + +describe('fromModel', () => { + it('should convert a kubernetes-models-like object with toJSON() method', () => { + // Simulate a kubernetes-models object + const mockModel = { + metadata: { + name: 'my-pod', + namespace: 'default', + }, + spec: { + containers: [{ name: 'app', image: 'nginx:latest' }], + }, + toJSON() { + return { + apiVersion: 'v1', + kind: 'Pod', + metadata: this.metadata, + spec: this.spec, + }; + }, + }; + + const resource = fromModel(mockModel); + + expect(resource.resource).toEqual({ + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'my-pod', namespace: 'default' }, + spec: { containers: [{ name: 'app', image: 'nginx:latest' }] }, + }); + expect(resource.ready).toBe(Ready.READY_UNSPECIFIED); + expect(resource.connectionDetails).toEqual({}); + }); + + it('should accept connection details and ready status', () => { + const mockModel = { + metadata: { + name: 'my-secret', + }, + toJSON() { + return { + apiVersion: 'v1', + kind: 'Secret', + metadata: this.metadata, + }; + }, + }; + + const connectionDetails = { password: Buffer.from('secret') }; + const ready = Ready.READY_TRUE; + + const resource = fromModel(mockModel, connectionDetails, ready); + + expect(resource.resource).toEqual({ + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: 'my-secret' }, + }); + expect(resource.ready).toBe(Ready.READY_TRUE); + expect(resource.connectionDetails).toEqual(connectionDetails); + }); +}); + +describe('fromObject', () => { + it('should create a Resource from a plain object', () => { + const obj = { + apiVersion: 'v1', + kind: 'Service', + metadata: { name: 'my-service' }, + }; + + const resource = fromObject(obj); + + expect(resource.resource).toEqual(obj); + expect(resource.ready).toBe(Ready.READY_UNSPECIFIED); + expect(resource.connectionDetails).toEqual({}); + }); + + it('should accept connection details and ready status', () => { + const obj = { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { name: 'my-config' }, + data: { key: 'value' }, + }; + + const connectionDetails = { apiKey: Buffer.from('secret') }; + const ready = Ready.READY_TRUE; + + const resource = fromObject(obj, connectionDetails, ready); + + expect(resource.resource).toEqual(obj); + expect(resource.ready).toBe(Ready.READY_TRUE); + expect(resource.connectionDetails).toEqual(connectionDetails); + }); +}); + +describe('toObject', () => { + it('should extract the resource object from a Resource', () => { + const obj = { + apiVersion: 'v1', + kind: 'Deployment', + metadata: { name: 'my-deployment' }, + }; + + const resource = Resource.fromJSON({ + resource: obj, + connectionDetails: {}, + ready: Ready.READY_TRUE, + }); + + const extracted = toObject(resource); + + expect(extracted).toEqual(obj); + }); + + it('should return undefined if resource is not present', () => { + const resource = Resource.fromJSON({ + connectionDetails: {}, + ready: Ready.READY_TRUE, + }); + + const extracted = toObject(resource); + + expect(extracted).toBeUndefined(); + }); +}); diff --git a/src/resource/resource.ts b/src/resource/resource.ts index bd10619..82a2394 100644 --- a/src/resource/resource.ts +++ b/src/resource/resource.ts @@ -1,5 +1,5 @@ // Resource utilities for working with Kubernetes resources and protobuf conversion -import { Resource, Ready } from '../proto/run_function.js'; +import { Ready, Resource } from '../proto/run_function.js'; // Type aliases for better readability export type ConnectionDetails = { [key: string]: Buffer }; @@ -51,7 +51,9 @@ export function newDesiredComposed(): DesiredComposed { * @param struct - The protobuf Struct to convert * @returns A plain JavaScript object representing the Kubernetes resource */ -export function asObject(struct: Record | undefined): Record { +export function asObject( + struct: Record | undefined, +): Record { if (!struct) { return {}; } @@ -73,7 +75,9 @@ export function asObject(struct: Record | undefined): Record): Record { +export function asStruct( + obj: Record, +): Record { // In our TypeScript implementation, this is essentially a pass-through // The actual conversion happens in the protobuf serialization layer return obj; @@ -92,12 +96,16 @@ export function asStruct(obj: Record): Record * @returns A Struct representation * @throws Error if conversion fails */ -export function mustStructObject(obj: Record): Record { +export function mustStructObject( + obj: Record, +): Record { try { return asStruct(obj); } catch (error) { throw new Error( - `Failed to convert object to struct: ${error instanceof Error ? error.message : String(error)}` + `Failed to convert object to struct: ${ + error instanceof Error ? error.message : String(error) + }`, ); } } @@ -121,7 +129,9 @@ export function mustStructJSON(json: string): Record { return asStruct(obj); } catch (error) { throw new Error( - `Failed to parse JSON to struct: ${error instanceof Error ? error.message : String(error)}` + `Failed to parse JSON to struct: ${ + error instanceof Error ? error.message : String(error) + }`, ); } } @@ -138,7 +148,7 @@ export function mustStructJSON(json: string): Record { export function fromObject( obj: Record, connectionDetails?: ConnectionDetails, - ready?: Ready + ready?: Ready, ): Resource { return Resource.fromJSON({ resource: obj, @@ -154,6 +164,45 @@ export function fromObject( * @param resource - The Resource to extract from * @returns The plain JavaScript object, or undefined if not present */ -export function toObject(resource: Resource): Record | undefined { +export function toObject( + resource: Resource, +): Record | undefined { return resource.resource; } + +/** + * Create a Resource from a kubernetes-models object + * This is a convenience function for objects with a toJSON() method + * (like kubernetes-models objects) + * + * For plain JavaScript objects, use fromObject() instead. + * + * @param obj - A kubernetes-models object with a toJSON() method + * @param connectionDetails - Optional connection details + * @param ready - Optional ready status + * @returns A Resource + * + * @example + * ```typescript + * import { Pod } from "kubernetes-models/v1"; + * + * const pod = new Pod({ + * metadata: { name: "my-pod" }, + * spec: { containers: [{ name: "app", image: "nginx" }] } + * }); + * + * // Automatically calls toJSON() on the model object + * const resource = fromModel(pod); + * ``` + */ +export function fromModel>( + obj: { toJSON: () => T }, + connectionDetails?: ConnectionDetails, + ready?: Ready, +): Resource { + return fromObject( + obj.toJSON() as Record, + connectionDetails, + ready, + ); +}