Skip to content

Commit 5f4ef1a

Browse files
terencehonlesalan-agius4
authored andcommitted
fix(@angular/cli): handle oneOf when converting schema to yargs options
This change fixes JSONSchemas where `oneOf` is placed at the root of the schema rather than in an array's `items`. This allows an array to be passed via the command-line, but additional types to be represented via configuration.
1 parent 0a61f20 commit 5f4ef1a

File tree

2 files changed

+87
-8
lines changed

2 files changed

+87
-8
lines changed

packages/angular/cli/src/command-builder/utilities/json-schema.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,21 +130,22 @@ function isStringMap(node: json.JsonObject): boolean {
130130
);
131131
}
132132

133-
const SUPPORTED_PRIMITIVE_TYPES = new Set(['boolean', 'number', 'string']);
133+
const SUPPORTED_PRIMITIVE_TYPES = new Set(['boolean', 'number', 'string'] as const);
134+
type SupportedPrimitiveType = Parameters<typeof SUPPORTED_PRIMITIVE_TYPES.add>[0];
134135

135136
/**
136137
* Checks if a string is a supported primitive type.
137138
* @param value The string to check.
138139
* @returns `true` if the string is a supported primitive type, otherwise `false`.
139140
*/
140-
function isSupportedPrimitiveType(value: string): boolean {
141-
return SUPPORTED_PRIMITIVE_TYPES.has(value);
141+
function isSupportedPrimitiveType(value: string): value is SupportedPrimitiveType {
142+
return SUPPORTED_PRIMITIVE_TYPES.has(value as any);
142143
}
143144

144145
/**
145146
* Recursively checks if a JSON schema for an array's items is a supported primitive type.
146147
* It supports `oneOf` and `anyOf` keywords.
147-
* @param schema The JSON schema for the array's items.
148+
* @param schema The JSON schema to check.
148149
* @returns `true` if the schema is a supported primitive type, otherwise `false`.
149150
*/
150151
function isSupportedArrayItemSchema(schema: json.JsonObject): boolean {
@@ -156,6 +157,10 @@ function isSupportedArrayItemSchema(schema: json.JsonObject): boolean {
156157
return true;
157158
}
158159

160+
if (isJsonObject(schema.items)) {
161+
return isSupportedArrayItemSchema(schema.items);
162+
}
163+
159164
if (json.isJsonArray(schema.items)) {
160165
return schema.items.some((item) => isJsonObject(item) && isSupportedArrayItemSchema(item));
161166
}
@@ -177,6 +182,40 @@ function isSupportedArrayItemSchema(schema: json.JsonObject): boolean {
177182
return false;
178183
}
179184

185+
/**
186+
* Recursively finds the first supported array primitive type for the given JSON schema.
187+
* It supports `oneOf` and `anyOf` keywords.
188+
* @param schema The JSON schema to inspect.
189+
* @returns The supported primitive type or 'string' if none is found.
190+
*/
191+
function getSupportedArrayType(schema: json.JsonObject): SupportedPrimitiveType {
192+
if (typeof schema.type === 'string' && isSupportedPrimitiveType(schema.type)) {
193+
return schema.type;
194+
}
195+
196+
if (json.isJsonArray(schema.enum)) {
197+
return 'string';
198+
}
199+
200+
if (isJsonObject(schema.items)) {
201+
const result = getSupportedArrayType(schema.items);
202+
if (result) return result;
203+
}
204+
205+
for (const key of ['items', 'oneOf', 'anyOf']) {
206+
if (json.isJsonArray(schema[key])) {
207+
for (const item in schema[key]) {
208+
if (isJsonObject(item)) {
209+
const result = getSupportedArrayType(item);
210+
if (result) return result;
211+
}
212+
}
213+
}
214+
}
215+
216+
return 'string';
217+
}
218+
180219
/**
181220
* Gets the supported types for a JSON schema node.
182221
* @param current The JSON schema node to get the supported types for.
@@ -198,7 +237,7 @@ function getSupportedTypes(
198237
case 'string':
199238
return true;
200239
case 'array':
201-
return isJsonObject(current.items) && isSupportedArrayItemSchema(current.items);
240+
return isSupportedArrayItemSchema(current);
202241
case 'object':
203242
return isStringMap(current);
204243
default:
@@ -377,9 +416,13 @@ export async function parseJsonSchemaToOptions(
377416
type: 'array',
378417
itemValueType: 'string',
379418
}
380-
: {
381-
type,
382-
}),
419+
: type === 'array'
420+
? {
421+
type: getSupportedArrayType(current),
422+
}
423+
: {
424+
type,
425+
}),
383426
};
384427

385428
options.push(option);

packages/angular/cli/src/command-builder/utilities/json-schema_spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ describe('parseJsonSchemaToOptions', () => {
4242
'enum': ['always', 'never', 'default-array'],
4343
},
4444
},
45+
'arrayWithNumbers': {
46+
'type': 'array',
47+
'items': { 'type': 'number' },
48+
},
4549
'extendable': {
4650
'type': 'object',
4751
'properties': {},
@@ -115,6 +119,9 @@ describe('parseJsonSchemaToOptions', () => {
115119
],
116120
},
117121
},
122+
'oneOfAtRoot': {
123+
'oneOf': [{ 'type': 'array', 'items': { 'type': 'string' } }, { 'type': 'boolean' }],
124+
},
118125
},
119126
};
120127

@@ -230,6 +237,35 @@ describe('parseJsonSchemaToOptions', () => {
230237
});
231238
});
232239

240+
describe('type=array, oneOf at root', () => {
241+
it('parses valid option value', async () => {
242+
expect(
243+
await parse([
244+
'--oneOfAtRoot',
245+
'first',
246+
'--oneOfAtRoot',
247+
'second',
248+
'--oneOfAtRoot',
249+
'third',
250+
]),
251+
).toEqual(jasmine.objectContaining({ 'oneOfAtRoot': ['first', 'second', 'third'] }));
252+
});
253+
254+
it('parses --no prefix', async () => {
255+
expect(await parse(['--no-oneOfAtRoot'])).toEqual(
256+
jasmine.objectContaining({ 'oneOfAtRoot': false }),
257+
);
258+
});
259+
});
260+
261+
describe('type=Array<number>', () => {
262+
it('parses valid option value', async () => {
263+
expect(await parse(['--arrayWithNumbers', '42', '--arrayWithNumbers', '24'])).toEqual(
264+
jasmine.objectContaining({ 'arrayWithNumbers': [42, 24] }),
265+
);
266+
});
267+
});
268+
233269
describe('type=string, enum', () => {
234270
it('parses valid option value', async () => {
235271
expect(await parse(['--ssr', 'never'])).toEqual(

0 commit comments

Comments
 (0)