Skip to content

Commit 2f9690d

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 2f9690d

File tree

2 files changed

+75
-8
lines changed

2 files changed

+75
-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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ describe('parseJsonSchemaToOptions', () => {
116116
],
117117
},
118118
},
119+
'oneOfAtRoot': {
120+
'oneOf': [{ 'type': 'array', 'items': { 'type': 'string' } }, { 'type': 'boolean' }],
121+
},
119122
},
120123
};
121124
const registry = new schema.CoreSchemaRegistry();
@@ -199,6 +202,27 @@ describe('parseJsonSchemaToOptions', () => {
199202
});
200203
});
201204

205+
describe('type=array, oneOf at root', () => {
206+
it('parses valid option value', async () => {
207+
expect(
208+
await parse([
209+
'--oneOfAtRoot',
210+
'first',
211+
'--oneOfAtRoot',
212+
'second',
213+
'--oneOfAtRoot',
214+
'third',
215+
]),
216+
).toEqual(jasmine.objectContaining({ 'oneOfAtRoot': ['first', 'second', 'third'] }));
217+
});
218+
219+
it('parses --no prefix', async () => {
220+
expect(await parse(['--no-oneOfAtRoot'])).toEqual(
221+
jasmine.objectContaining({ 'oneOfAtRoot': false }),
222+
);
223+
});
224+
});
225+
202226
describe('type=string, enum', () => {
203227
it('parses valid option value', async () => {
204228
expect(await parse(['--ssr', 'never'])).toEqual(

0 commit comments

Comments
 (0)