Skip to content

Commit b9b858a

Browse files
committed
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 9850ebc commit b9b858a

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
@@ -43,6 +43,10 @@ describe('parseJsonSchemaToOptions', () => {
4343
'enum': ['always', 'never', 'default-array'],
4444
},
4545
},
46+
'arrayWithNumbers': {
47+
'type': 'array',
48+
'items': { 'type': 'number' },
49+
},
4650
'extendable': {
4751
'type': 'object',
4852
'properties': {},
@@ -116,6 +120,9 @@ describe('parseJsonSchemaToOptions', () => {
116120
],
117121
},
118122
},
123+
'oneOfAtRoot': {
124+
'oneOf': [{ 'type': 'array', 'items': { 'type': 'string' } }, { 'type': 'boolean' }],
125+
},
119126
},
120127
};
121128
const registry = new schema.CoreSchemaRegistry();
@@ -199,6 +206,35 @@ describe('parseJsonSchemaToOptions', () => {
199206
});
200207
});
201208

209+
describe('type=array, oneOf at root', () => {
210+
it('parses valid option value', async () => {
211+
expect(
212+
await parse([
213+
'--oneOfAtRoot',
214+
'first',
215+
'--oneOfAtRoot',
216+
'second',
217+
'--oneOfAtRoot',
218+
'third',
219+
]),
220+
).toEqual(jasmine.objectContaining({ 'oneOfAtRoot': ['first', 'second', 'third'] }));
221+
});
222+
223+
it('parses --no prefix', async () => {
224+
expect(await parse(['--no-oneOfAtRoot'])).toEqual(
225+
jasmine.objectContaining({ 'oneOfAtRoot': false }),
226+
);
227+
});
228+
});
229+
230+
describe('type=Array<number>', () => {
231+
it('parses valid option value', async () => {
232+
expect(await parse(['--arrayWithNumbers', '42', '--arrayWithNumbers', '24'])).toEqual(
233+
jasmine.objectContaining({ 'arrayWithNumbers': [42, 24] }),
234+
);
235+
});
236+
});
237+
202238
describe('type=string, enum', () => {
203239
it('parses valid option value', async () => {
204240
expect(await parse(['--ssr', 'never'])).toEqual(

0 commit comments

Comments
 (0)