Skip to content

Commit 0c72366

Browse files
authored
Merge pull request #5 from hyperweb-io/feat/json-patch
add JSON Patch feature
2 parents 368c9d8 + f6411a2 commit 0c72366

File tree

9 files changed

+1246
-877
lines changed

9 files changed

+1246
-877
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
JSON Patch Effect on IntOrString Type
2+
======================================
3+
4+
The following diff shows how the JSON patch transforms the IntOrString type
5+
from a simple string type to a proper union type (string | number).
6+
7+
- Without JSON Patch - 1
8+
+ With JSON Patch + 1
9+
10+
@@ -23,6 +23,6 @@
11+
 selector?: LabelSelector;
12+
 [key: string]: unknown;
13+
 };
14+
- export type IntOrString = string;
15+
+ export type IntOrString = string | number;
16+
 export interface Info {
17+
 buildDate: string;
18+
19+
Key Changes:
20+
- Original: export type IntOrString = string;
21+
- Patched: export type IntOrString = string | number;
22+
23+
This affects all properties that use IntOrString, making them accept both
24+
string and number values as originally intended by the Kubernetes API.

packages/schema-sdk/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,54 @@ const code = generateOpenApiClient({
9090
writeFileSync(__dirname + '/output/swagger-client.ts', code);
9191
```
9292

93+
### Using JSON Patch to Modify OpenAPI Schemas
94+
95+
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.
96+
97+
```ts
98+
import schema from 'path-to-your/swagger.json';
99+
import { generateOpenApiClient, getDefaultSchemaSDKOptions } from 'schema-sdk';
100+
import type { Operation } from 'fast-json-patch';
101+
102+
// Example: Fix IntOrString type to be a proper union type
103+
const jsonPatchOperations: Operation[] = [
104+
{
105+
op: 'remove',
106+
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/type'
107+
},
108+
{
109+
op: 'remove',
110+
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/format'
111+
},
112+
{
113+
op: 'add',
114+
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/oneOf',
115+
value: [
116+
{ type: 'string' },
117+
{ type: 'integer', format: 'int32' }
118+
]
119+
}
120+
];
121+
122+
const options = getDefaultSchemaSDKOptions({
123+
clientName: 'KubernetesClient',
124+
jsonpatch: jsonPatchOperations,
125+
// ... other options
126+
});
127+
128+
const code = generateOpenApiClient(options, schema);
129+
```
130+
131+
The JSON Patch operations support all standard operations:
132+
- `add`: Add a new value
133+
- `remove`: Remove a value
134+
- `replace`: Replace an existing value
135+
- `move`: Move a value from one location to another
136+
- `copy`: Copy a value from one location to another
137+
- `test`: Test that a value equals a specified value
138+
139+
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).
140+
93141
## Contributing 🤝
94142

95143
Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { readFileSync } from 'fs';
2+
import type { Operation } from 'fast-json-patch';
3+
import * as jsonpatch from 'fast-json-patch';
4+
5+
import schema from '../../../__fixtures__/openapi/swagger.json';
6+
7+
describe('IntOrString JSON Patch fix', () => {
8+
it('should verify the original IntOrString is type string', () => {
9+
const originalDef = (schema as any).definitions['io.k8s.apimachinery.pkg.util.intstr.IntOrString'];
10+
expect(originalDef.type).toBe('string');
11+
expect(originalDef.format).toBe('int-or-string');
12+
expect(originalDef.oneOf).toBeUndefined();
13+
});
14+
15+
it('should patch IntOrString to use oneOf', () => {
16+
const jsonPatchOperations: Operation[] = [
17+
{
18+
op: 'remove',
19+
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/type'
20+
},
21+
{
22+
op: 'remove',
23+
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/format'
24+
},
25+
{
26+
op: 'add',
27+
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/oneOf',
28+
value: [
29+
{ type: 'string' },
30+
{ type: 'integer', format: 'int32' }
31+
]
32+
}
33+
];
34+
35+
// Apply patches
36+
const schemaCopy = JSON.parse(JSON.stringify(schema));
37+
const result = jsonpatch.applyPatch(schemaCopy, jsonPatchOperations);
38+
39+
const patchedDef = result.newDocument.definitions['io.k8s.apimachinery.pkg.util.intstr.IntOrString'];
40+
41+
// Verify the patch worked
42+
expect(patchedDef.type).toBeUndefined();
43+
expect(patchedDef.format).toBeUndefined();
44+
expect(patchedDef.oneOf).toEqual([
45+
{ type: 'string' },
46+
{ type: 'integer', format: 'int32' }
47+
]);
48+
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.');
49+
});
50+
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { writeFileSync } from 'fs';
2+
import type { Operation } from 'fast-json-patch';
3+
import { diff } from 'jest-diff';
4+
5+
import schema from '../../../__fixtures__/openapi/swagger.json';
6+
import { generateOpenApiClient } from '../src/openapi';
7+
import { getDefaultSchemaSDKOptions } from '../src/types';
8+
9+
describe('JSON Patch functionality', () => {
10+
it('should patch IntOrString type from string to oneOf', () => {
11+
// Define the patch to change IntOrString from type: string to oneOf: [string, integer]
12+
const jsonPatchOperations: Operation[] = [
13+
{
14+
op: 'remove',
15+
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/type'
16+
},
17+
{
18+
op: 'remove',
19+
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/format'
20+
},
21+
{
22+
op: 'add',
23+
path: '/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString/oneOf',
24+
value: [
25+
{ type: 'string' },
26+
{ type: 'integer', format: 'int32' }
27+
]
28+
}
29+
];
30+
31+
const baseOptions = {
32+
clientName: 'KubernetesClient',
33+
exclude: [
34+
'*.v1beta1.*',
35+
'*.v2beta1.*',
36+
'io.k8s.api.events.v1.EventSeries',
37+
'io.k8s.api.events.v1.Event',
38+
'io.k8s.api.flowcontrol*',
39+
],
40+
};
41+
42+
// Generate code without patch
43+
const optionsWithoutPatch = getDefaultSchemaSDKOptions(baseOptions);
44+
const codeWithoutPatch = generateOpenApiClient(optionsWithoutPatch, schema as any);
45+
46+
// Generate code with patch
47+
const optionsWithPatch = getDefaultSchemaSDKOptions({
48+
...baseOptions,
49+
jsonpatch: jsonPatchOperations,
50+
});
51+
const codeWithPatch = generateOpenApiClient(optionsWithPatch, schema as any);
52+
53+
// The generated code should contain the IntOrString type as a union
54+
expect(codeWithPatch).toContain('IntOrString');
55+
56+
// Extract just the IntOrString-related lines for a focused diff
57+
const extractIntOrStringContext = (code: string) => {
58+
const lines = code.split('\n');
59+
const relevantLines: string[] = [];
60+
61+
lines.forEach((line, index) => {
62+
if (line.includes('IntOrString')) {
63+
// Get context: 2 lines before and after
64+
for (let i = Math.max(0, index - 2); i <= Math.min(lines.length - 1, index + 2); i++) {
65+
if (!relevantLines.includes(lines[i])) {
66+
relevantLines.push(lines[i]);
67+
}
68+
}
69+
}
70+
});
71+
72+
return relevantLines.join('\n');
73+
};
74+
75+
const contextWithoutPatch = extractIntOrStringContext(codeWithoutPatch);
76+
const contextWithPatch = extractIntOrStringContext(codeWithPatch);
77+
78+
// Generate diff
79+
const diffOutput = diff(contextWithoutPatch, contextWithPatch, {
80+
aAnnotation: 'Without JSON Patch',
81+
bAnnotation: 'With JSON Patch',
82+
includeChangeCounts: true,
83+
contextLines: 3,
84+
expand: false,
85+
});
86+
87+
// Write the diff for inspection
88+
writeFileSync(
89+
__dirname + '/../../../__fixtures__/output/swagger.jsonpatch.diff',
90+
`JSON Patch Effect on IntOrString Type
91+
======================================
92+
93+
The following diff shows how the JSON patch transforms the IntOrString type
94+
from a simple string type to a proper union type (string | number).
95+
96+
${diffOutput}
97+
98+
Key Changes:
99+
- Original: export type IntOrString = string;
100+
- Patched: export type IntOrString = string | number;
101+
102+
This affects all properties that use IntOrString, making them accept both
103+
string and number values as originally intended by the Kubernetes API.
104+
`
105+
);
106+
107+
// Verify the type definition changed
108+
expect(codeWithoutPatch).toMatch(/export type IntOrString = string;/);
109+
expect(codeWithPatch).toMatch(/export type IntOrString = string \| number;/);
110+
});
111+
112+
it('should handle empty jsonpatch array', () => {
113+
const options = getDefaultSchemaSDKOptions({
114+
clientName: 'KubernetesClient',
115+
jsonpatch: [],
116+
exclude: ['*.v1beta1.*', '*.v2beta1.*'],
117+
});
118+
119+
const code = generateOpenApiClient(options, schema as any);
120+
expect(code).toBeTruthy();
121+
});
122+
123+
it('should handle undefined jsonpatch', () => {
124+
const options = getDefaultSchemaSDKOptions({
125+
clientName: 'KubernetesClient',
126+
exclude: ['*.v1beta1.*', '*.v2beta1.*'],
127+
});
128+
129+
const code = generateOpenApiClient(options, schema as any);
130+
expect(code).toBeTruthy();
131+
});
132+
});

packages/schema-sdk/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@interweb-utils/casing": "^0.2.0",
3535
"@interweb/fetch-api-client": "^0.6.1",
3636
"deepmerge": "^4.3.1",
37+
"fast-json-patch": "^3.1.1",
3738
"schema-typescript": "^0.12.1"
3839
},
3940
"keywords": [
@@ -42,5 +43,8 @@
4243
"typescript",
4344
"swagger",
4445
"openapi"
45-
]
46+
],
47+
"devDependencies": {
48+
"jest-diff": "^30.0.4"
49+
}
4650
}

packages/schema-sdk/src/openapi.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
Response,
1313
} from './openapi.types';
1414
import { OpenAPIOptions } from './types';
15-
import { createPathTemplateLiteral } from './utils';
15+
import { createPathTemplateLiteral, applyJsonPatch } from './utils';
1616

1717
/**
1818
includes: {
@@ -575,11 +575,14 @@ export function generateOpenApiClient(
575575
options: OpenAPIOptions,
576576
schema: OpenAPISpec
577577
): string {
578+
// Apply JSON patches if configured
579+
const patchedSchema = applyJsonPatch(schema, options);
580+
578581
const methods = [];
579582
if (options.includeSwaggerUrl) {
580583
methods.push(getSwaggerJSONMethod());
581584
}
582-
methods.push(...generateMethods(options, schema));
585+
methods.push(...generateMethods(options, patchedSchema));
583586

584587
const classBody = t.classBody([
585588
t.classMethod(
@@ -607,14 +610,14 @@ export function generateOpenApiClient(
607610
//// INTERFACES
608611
const apiSchema = {
609612
title: options.clientName,
610-
definitions: schema.definitions,
613+
definitions: patchedSchema.definitions,
611614
};
612615

613616
const types = generateTypeScriptTypes(apiSchema, {
614617
...(options as any),
615618
exclude: [options.clientName, ...(options.exclude ?? [])],
616619
});
617-
const openApiTypes = generateOpenApiTypes(options, schema);
620+
const openApiTypes = generateOpenApiTypes(options, patchedSchema);
618621

619622
return generate(
620623
t.file(
@@ -921,7 +924,10 @@ export function generateReactQueryHooks(
921924
options: OpenAPIOptions,
922925
schema: OpenAPISpec
923926
): string {
924-
const components = collectReactQueryHookComponents(options, schema);
927+
// Apply JSON patches if configured
928+
const patchedSchema = applyJsonPatch(schema, options);
929+
930+
const components = collectReactQueryHookComponents(options, patchedSchema);
925931
if (!components.length) return ''
926932
// Group imports
927933
const importMap = new Map<string, Set<string>>();

packages/schema-sdk/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import deepmerge from 'deepmerge';
22
import type { DeepPartial } from 'schema-typescript';
33
import { defaultSchemaTSOptions, SchemaTSOptions } from 'schema-typescript';
4+
import type { Operation } from 'fast-json-patch';
45

56
export interface OpenAPIOptions extends SchemaTSOptions {
67
clientName: string;
@@ -41,6 +42,12 @@ export interface OpenAPIOptions extends SchemaTSOptions {
4142

4243
typesImportPath: string; // kubernetesjs, ./swagger-client, etc.
4344
};
45+
/**
46+
* JSON Patch operations to apply to the OpenAPI spec before processing
47+
* Uses RFC 6902 JSON Patch format
48+
* @see https://www.npmjs.com/package/fast-json-patch
49+
*/
50+
jsonpatch?: Operation[];
4451
}
4552

4653
export const defaultSchemaSDKOptions: DeepPartial<OpenAPIOptions> = {
@@ -66,6 +73,7 @@ export const defaultSchemaSDKOptions: DeepPartial<OpenAPIOptions> = {
6673
typesImportPath: './client',
6774
contextHookName: './context'
6875
},
76+
jsonpatch: [],
6977
};
7078

7179
export const getDefaultSchemaSDKOptions = (

0 commit comments

Comments
 (0)