Skip to content

add JSON Patch feature #5

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 3 commits into from
Jul 9, 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
24 changes: 24 additions & 0 deletions __fixtures__/output/swagger.jsonpatch.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
JSON Patch Effect on IntOrString Type
======================================

The following diff shows how the JSON patch transforms the IntOrString type
from a simple string type to a proper union type (string | number).

- Without JSON Patch - 1
+ With JSON Patch + 1

@@ -23,6 +23,6 @@
 selector?: LabelSelector;
 [key: string]: unknown;
 };
- export type IntOrString = string;
+ export type IntOrString = string | number;
 export interface Info {
 buildDate: string;

Key Changes:
- Original: export type IntOrString = string;
- Patched: export type IntOrString = string | number;

This affects all properties that use IntOrString, making them accept both
string and number values as originally intended by the Kubernetes API.
48 changes: 48 additions & 0 deletions packages/schema-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,54 @@ const code = generateOpenApiClient({
writeFileSync(__dirname + '/output/swagger-client.ts', code);
```

### Using JSON Patch to Modify OpenAPI Schemas

The `jsonpatch` option allows you to apply RFC 6902 JSON Patch operations to the OpenAPI specification before processing. This is useful for fixing schema issues or making adjustments without modifying the source file.

```ts
import schema from 'path-to-your/swagger.json';
import { generateOpenApiClient, getDefaultSchemaSDKOptions } from 'schema-sdk';
import type { Operation } from 'fast-json-patch';

// Example: Fix IntOrString type to be a proper union type
const jsonPatchOperations: Operation[] = [
{
op: 'remove',
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/type'
},
{
op: 'remove',
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/format'
},
{
op: 'add',
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/oneOf',
value: [
{ type: 'string' },
{ type: 'integer', format: 'int32' }
]
}
];

const options = getDefaultSchemaSDKOptions({
clientName: 'KubernetesClient',
jsonpatch: jsonPatchOperations,
// ... other options
});

const code = generateOpenApiClient(options, schema);
```

The JSON Patch operations support all standard operations:
- `add`: Add a new value
- `remove`: Remove a value
- `replace`: Replace an existing value
- `move`: Move a value from one location to another
- `copy`: Copy a value from one location to another
- `test`: Test that a value equals a specified value

For more information about JSON Patch format, see [RFC 6902](https://tools.ietf.org/html/rfc6902) and the [fast-json-patch documentation](https://www.npmjs.com/package/fast-json-patch).

## Contributing 🤝

Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
Expand Down
50 changes: 50 additions & 0 deletions packages/schema-sdk/__tests__/jsonpatch.intorstring.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { readFileSync } from 'fs';
import type { Operation } from 'fast-json-patch';
import * as jsonpatch from 'fast-json-patch';

import schema from '../../../__fixtures__/openapi/swagger.json';

describe('IntOrString JSON Patch fix', () => {
it('should verify the original IntOrString is type string', () => {
const originalDef = (schema as any).definitions['io.k8s.apimachinery.pkg.util.intstr.IntOrString'];
expect(originalDef.type).toBe('string');
expect(originalDef.format).toBe('int-or-string');
expect(originalDef.oneOf).toBeUndefined();
});

it('should patch IntOrString to use oneOf', () => {
const jsonPatchOperations: Operation[] = [
{
op: 'remove',
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/type'
},
{
op: 'remove',
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/format'
},
{
op: 'add',
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/oneOf',
value: [
{ type: 'string' },
{ type: 'integer', format: 'int32' }
]
}
];

// Apply patches
const schemaCopy = JSON.parse(JSON.stringify(schema));
const result = jsonpatch.applyPatch(schemaCopy, jsonPatchOperations);

const patchedDef = result.newDocument.definitions['io.k8s.apimachinery.pkg.util.intstr.IntOrString'];

// Verify the patch worked
expect(patchedDef.type).toBeUndefined();
expect(patchedDef.format).toBeUndefined();
expect(patchedDef.oneOf).toEqual([
{ type: 'string' },
{ type: 'integer', format: 'int32' }
]);
expect(patchedDef.description).toBe('IntOrString is a type that can hold an int32 or a string. When used in JSON or YAML marshalling and unmarshalling, it produces or consumes the inner type. This allows you to have, for example, a JSON field that can accept a name or number.');
});
});
132 changes: 132 additions & 0 deletions packages/schema-sdk/__tests__/jsonpatch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { writeFileSync } from 'fs';
import type { Operation } from 'fast-json-patch';
import { diff } from 'jest-diff';

import schema from '../../../__fixtures__/openapi/swagger.json';
import { generateOpenApiClient } from '../src/openapi';
import { getDefaultSchemaSDKOptions } from '../src/types';

describe('JSON Patch functionality', () => {
it('should patch IntOrString type from string to oneOf', () => {
// Define the patch to change IntOrString from type: string to oneOf: [string, integer]
const jsonPatchOperations: Operation[] = [
{
op: 'remove',
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/type'
},
{
op: 'remove',
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/format'
},
{
op: 'add',
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/oneOf',
value: [
{ type: 'string' },
{ type: 'integer', format: 'int32' }
]
}
];

const baseOptions = {
clientName: 'KubernetesClient',
exclude: [
'*.v1beta1.*',
'*.v2beta1.*',
'io.k8s.api.events.v1.EventSeries',
'io.k8s.api.events.v1.Event',
'io.k8s.api.flowcontrol*',
],
};

// Generate code without patch
const optionsWithoutPatch = getDefaultSchemaSDKOptions(baseOptions);
const codeWithoutPatch = generateOpenApiClient(optionsWithoutPatch, schema as any);

// Generate code with patch
const optionsWithPatch = getDefaultSchemaSDKOptions({
...baseOptions,
jsonpatch: jsonPatchOperations,
});
const codeWithPatch = generateOpenApiClient(optionsWithPatch, schema as any);

// The generated code should contain the IntOrString type as a union
expect(codeWithPatch).toContain('IntOrString');

// Extract just the IntOrString-related lines for a focused diff
const extractIntOrStringContext = (code: string) => {
const lines = code.split('\n');
const relevantLines: string[] = [];

lines.forEach((line, index) => {
if (line.includes('IntOrString')) {
// Get context: 2 lines before and after
for (let i = Math.max(0, index - 2); i <= Math.min(lines.length - 1, index + 2); i++) {
if (!relevantLines.includes(lines[i])) {
relevantLines.push(lines[i]);
}
}
}
});

return relevantLines.join('\n');
};

const contextWithoutPatch = extractIntOrStringContext(codeWithoutPatch);
const contextWithPatch = extractIntOrStringContext(codeWithPatch);

// Generate diff
const diffOutput = diff(contextWithoutPatch, contextWithPatch, {
aAnnotation: 'Without JSON Patch',
bAnnotation: 'With JSON Patch',
includeChangeCounts: true,
contextLines: 3,
expand: false,
});

// Write the diff for inspection
writeFileSync(
__dirname + '/../../../__fixtures__/output/swagger.jsonpatch.diff',
`JSON Patch Effect on IntOrString Type
======================================

The following diff shows how the JSON patch transforms the IntOrString type
from a simple string type to a proper union type (string | number).

${diffOutput}

Key Changes:
- Original: export type IntOrString = string;
- Patched: export type IntOrString = string | number;

This affects all properties that use IntOrString, making them accept both
string and number values as originally intended by the Kubernetes API.
`
);

// Verify the type definition changed
expect(codeWithoutPatch).toMatch(/export type IntOrString = string;/);
expect(codeWithPatch).toMatch(/export type IntOrString = string \| number;/);
});

it('should handle empty jsonpatch array', () => {
const options = getDefaultSchemaSDKOptions({
clientName: 'KubernetesClient',
jsonpatch: [],
exclude: ['*.v1beta1.*', '*.v2beta1.*'],
});

const code = generateOpenApiClient(options, schema as any);
expect(code).toBeTruthy();
});

it('should handle undefined jsonpatch', () => {
const options = getDefaultSchemaSDKOptions({
clientName: 'KubernetesClient',
exclude: ['*.v1beta1.*', '*.v2beta1.*'],
});

const code = generateOpenApiClient(options, schema as any);
expect(code).toBeTruthy();
});
});
6 changes: 5 additions & 1 deletion packages/schema-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@interweb-utils/casing": "^0.2.0",
"@interweb/fetch-api-client": "^0.6.1",
"deepmerge": "^4.3.1",
"fast-json-patch": "^3.1.1",
"schema-typescript": "^0.12.1"
},
"keywords": [
Expand All @@ -42,5 +43,8 @@
"typescript",
"swagger",
"openapi"
]
],
"devDependencies": {
"jest-diff": "^30.0.4"
}
}
16 changes: 11 additions & 5 deletions packages/schema-sdk/src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Response,
} from './openapi.types';
import { OpenAPIOptions } from './types';
import { createPathTemplateLiteral } from './utils';
import { createPathTemplateLiteral, applyJsonPatch } from './utils';

/**
includes: {
Expand Down Expand Up @@ -575,11 +575,14 @@ export function generateOpenApiClient(
options: OpenAPIOptions,
schema: OpenAPISpec
): string {
// Apply JSON patches if configured
const patchedSchema = applyJsonPatch(schema, options);

const methods = [];
if (options.includeSwaggerUrl) {
methods.push(getSwaggerJSONMethod());
}
methods.push(...generateMethods(options, schema));
methods.push(...generateMethods(options, patchedSchema));

const classBody = t.classBody([
t.classMethod(
Expand Down Expand Up @@ -607,14 +610,14 @@ export function generateOpenApiClient(
//// INTERFACES
const apiSchema = {
title: options.clientName,
definitions: schema.definitions,
definitions: patchedSchema.definitions,
};

const types = generateTypeScriptTypes(apiSchema, {
...(options as any),
exclude: [options.clientName, ...(options.exclude ?? [])],
});
const openApiTypes = generateOpenApiTypes(options, schema);
const openApiTypes = generateOpenApiTypes(options, patchedSchema);

return generate(
t.file(
Expand Down Expand Up @@ -921,7 +924,10 @@ export function generateReactQueryHooks(
options: OpenAPIOptions,
schema: OpenAPISpec
): string {
const components = collectReactQueryHookComponents(options, schema);
// Apply JSON patches if configured
const patchedSchema = applyJsonPatch(schema, options);

const components = collectReactQueryHookComponents(options, patchedSchema);
if (!components.length) return ''
// Group imports
const importMap = new Map<string, Set<string>>();
Expand Down
8 changes: 8 additions & 0 deletions packages/schema-sdk/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import deepmerge from 'deepmerge';
import type { DeepPartial } from 'schema-typescript';
import { defaultSchemaTSOptions, SchemaTSOptions } from 'schema-typescript';
import type { Operation } from 'fast-json-patch';

export interface OpenAPIOptions extends SchemaTSOptions {
clientName: string;
Expand Down Expand Up @@ -41,6 +42,12 @@ export interface OpenAPIOptions extends SchemaTSOptions {

typesImportPath: string; // kubernetesjs, ./swagger-client, etc.
};
/**
* JSON Patch operations to apply to the OpenAPI spec before processing
* Uses RFC 6902 JSON Patch format
* @see https://www.npmjs.com/package/fast-json-patch
*/
jsonpatch?: Operation[];
}

export const defaultSchemaSDKOptions: DeepPartial<OpenAPIOptions> = {
Expand All @@ -66,6 +73,7 @@ export const defaultSchemaSDKOptions: DeepPartial<OpenAPIOptions> = {
typesImportPath: './client',
contextHookName: './context'
},
jsonpatch: [],
};

export const getDefaultSchemaSDKOptions = (
Expand Down
Loading