Skip to content

Commit 07dc2be

Browse files
committed
polish and extract common patterns
1 parent dfbe989 commit 07dc2be

File tree

1 file changed

+71
-130
lines changed

1 file changed

+71
-130
lines changed

src/parser/jsonParser.ts

Lines changed: 71 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -485,136 +485,7 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
485485
const testAlternatives = (alternatives: JSONSchemaRef[], maxOneMatch: boolean) => {
486486
const matches = [];
487487

488-
// Try to detect and apply discriminator optimization dynamically
489-
let alternativesToTest = alternatives;
490-
491-
// Attempt to find a common discriminator property across alternatives
492-
const tryDiscriminatorOptimization = () => {
493-
if (alternatives.length < 2) {
494-
return; // No point optimizing with less than 2 alternatives
495-
}
496-
497-
if (node.type === 'object') {
498-
// Early exit: if node has no properties, can't apply optimization
499-
if (!node.properties || node.properties.length === 0) {
500-
return;
501-
}
502-
503-
// Find properties that have const values in ALL alternatives
504-
const propertyConstMap = new Map<string, Map<any, number[]>>();
505-
506-
for (let i = 0; i < alternatives.length; i++) {
507-
const altSchema = asSchema(alternatives[i]);
508-
if (!altSchema.properties) {
509-
// If any alternative has no properties, we can't use object property discrimination
510-
return;
511-
}
512-
for (const propName in altSchema.properties) {
513-
const propSchema = asSchema(altSchema.properties[propName]);
514-
if (propSchema.const !== undefined) {
515-
if (!propertyConstMap.has(propName)) {
516-
propertyConstMap.set(propName, new Map());
517-
}
518-
const constMap = propertyConstMap.get(propName)!;
519-
if (!constMap.has(propSchema.const)) {
520-
constMap.set(propSchema.const, []);
521-
}
522-
constMap.get(propSchema.const)!.push(i);
523-
}
524-
}
525-
}
526-
527-
// Early exit: if no const properties found, can't apply optimization
528-
if (propertyConstMap.size === 0) {
529-
return;
530-
}
531-
532-
// Find a property where ALL alternatives have a const value (one per alternative)
533-
for (const [propName, constMap] of Array.from(propertyConstMap.entries())) {
534-
const coveredAlternatives = new Set<number>();
535-
constMap.forEach((indices) => {
536-
indices.forEach(idx => coveredAlternatives.add(idx));
537-
});
538-
// This property is a valid discriminator ONLY if ALL alternatives have a const value for it
539-
if (coveredAlternatives.size === alternatives.length) {
540-
// Extract the discriminator value from the current node
541-
const prop = node.properties.find(p => p.keyNode.value === propName);
542-
if (prop?.valueNode?.type === 'string') {
543-
const discriminatorValue = prop.valueNode.value;
544-
const matchingIndices = constMap.get(discriminatorValue);
545-
if (matchingIndices && matchingIndices.length > 0) {
546-
alternativesToTest = matchingIndices.map(idx => alternatives[idx]);
547-
return;
548-
}
549-
}
550-
// If this property covers all alternatives but we couldn't match,
551-
break;
552-
}
553-
}
554-
} else if (node.type === 'array') {
555-
// Early exit: if node has no items, can't apply optimization
556-
if (!node.items || node.items.length === 0) {
557-
return;
558-
}
559-
560-
// Find array item indices that have const values in ALL alternatives
561-
const indexConstMap = new Map<number, Map<any, number[]>>();
562-
563-
for (let i = 0; i < alternatives.length; i++) {
564-
const altSchema = asSchema(alternatives[i]);
565-
const itemSchemas = altSchema.prefixItems || (Array.isArray(altSchema.items) ? altSchema.items : undefined);
566-
567-
if (!itemSchemas) {
568-
// If any alternative has no item schemas, we can't use array index discrimination
569-
return;
570-
}
571-
572-
itemSchemas.forEach((itemSchemaRef, itemIndex) => {
573-
const itemSchema = asSchema(itemSchemaRef);
574-
if (itemSchema.const !== undefined) {
575-
if (!indexConstMap.has(itemIndex)) {
576-
indexConstMap.set(itemIndex, new Map());
577-
}
578-
const constMap = indexConstMap.get(itemIndex)!;
579-
if (!constMap.has(itemSchema.const)) {
580-
constMap.set(itemSchema.const, []);
581-
}
582-
constMap.get(itemSchema.const)!.push(i);
583-
}
584-
});
585-
}
586-
587-
// Early exit: if no const items found, can't apply optimization
588-
if (indexConstMap.size === 0) {
589-
return;
590-
}
591-
592-
// Find an index where ALL alternatives have a const value (one per alternative)
593-
for (const [itemIndex, constMap] of Array.from(indexConstMap.entries())) {
594-
const coveredAlternatives = new Set<number>();
595-
constMap.forEach((indices) => {
596-
indices.forEach(idx => coveredAlternatives.add(idx));
597-
});
598-
// This index is a valid discriminator ONLY if ALL alternatives have a const value at this index
599-
if (coveredAlternatives.size === alternatives.length) {
600-
// Extract the discriminator value from the current node
601-
const item = node.items[itemIndex];
602-
if (item?.type === 'string') {
603-
const discriminatorValue = item.value;
604-
const matchingIndices = constMap.get(discriminatorValue);
605-
if (matchingIndices && matchingIndices.length > 0) {
606-
alternativesToTest = matchingIndices.map(idx => alternatives[idx]);
607-
return;
608-
}
609-
}
610-
// If this index covers all alternatives but we couldn't match,
611-
break;
612-
}
613-
}
614-
}
615-
};
616-
617-
tryDiscriminatorOptimization();
488+
const alternativesToTest = _tryDiscriminatorOptimization(alternatives) ?? alternatives;
618489

619490
// remember the best match that is used for error messages
620491
let bestMatch: { schema: JSONSchema; validationResult: ValidationResult; matchingSchemas: ISchemaCollector; } | undefined = undefined;
@@ -751,7 +622,77 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
751622
}
752623
}
753624

625+
function _tryDiscriminatorOptimization(alternatives: JSONSchemaRef[]): JSONSchemaRef[] | undefined {
626+
if (alternatives.length < 2) {
627+
return undefined;
628+
}
629+
630+
const buildConstMap = (getSchemas: (alt: JSONSchema, idx: number) => [string | number, JSONSchema][] | undefined) => {
631+
const constMap = new Map<string | number, Map<any, number[]>>();
632+
633+
for (let i = 0; i < alternatives.length; i++) {
634+
const schemas = getSchemas(asSchema(alternatives[i]), i);
635+
if (!schemas) {
636+
return undefined; // Early exit if any alternative can't be processed
637+
}
638+
639+
schemas.forEach(([key, schema]) => {
640+
if (schema.const !== undefined) {
641+
if (!constMap.has(key)) {
642+
constMap.set(key, new Map());
643+
}
644+
const valueMap = constMap.get(key)!;
645+
if (!valueMap.has(schema.const)) {
646+
valueMap.set(schema.const, []);
647+
}
648+
valueMap.get(schema.const)!.push(i);
649+
}
650+
});
651+
}
652+
return constMap;
653+
};
654+
655+
const findDiscriminator = (constMap: Map<string | number, Map<any, number[]>>, getValue: (key: string | number) => any) => {
656+
for (const [key, valueMap] of constMap) {
657+
const coveredAlts = new Set<number>();
658+
valueMap.forEach(indices => indices.forEach(idx => coveredAlts.add(idx)));
659+
660+
if (coveredAlts.size === alternatives.length) {
661+
const discriminatorValue = getValue(key);
662+
const matchingIndices = valueMap.get(discriminatorValue);
663+
if (matchingIndices?.length) {
664+
return matchingIndices.map(idx => alternatives[idx]);
665+
}
666+
break; // Found valid discriminator but no match
667+
}
668+
}
669+
return undefined;
670+
};
754671

672+
if (node.type === 'object' && node.properties?.length) {
673+
const constMap = buildConstMap((schema) =>
674+
schema.properties ? Object.entries(schema.properties).map(([k, v]) => [k, asSchema(v)]) : undefined
675+
);
676+
if (constMap) {
677+
return findDiscriminator(constMap, (propName) => {
678+
const prop = node.properties.find(p => p.keyNode.value === propName);
679+
return prop?.valueNode?.type === 'string' ? prop.valueNode.value : undefined;
680+
});
681+
}
682+
} else if (node.type === 'array' && node.items?.length) {
683+
const constMap = buildConstMap((schema) => {
684+
const itemSchemas = schema.prefixItems || (Array.isArray(schema.items) ? schema.items : undefined);
685+
return itemSchemas ? itemSchemas.map((item, idx) => [idx, asSchema(item)]) : undefined;
686+
});
687+
if (constMap) {
688+
return findDiscriminator(constMap, (itemIndex) => {
689+
const item = node.items[itemIndex as number];
690+
return item?.type === 'string' ? item.value : undefined;
691+
});
692+
}
693+
}
694+
return undefined;
695+
}
755696

756697
function _validateNumberNode(node: NumberASTNode): void {
757698
const val = node.value;

0 commit comments

Comments
 (0)