-
Notifications
You must be signed in to change notification settings - Fork 198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: merge compatible definitions in union types #722
base: next
Are you sure you want to change the base?
Conversation
I've classed this as a "fix" because it doesn't change the semantic meaning of the output, and it doesn't enable new features for schema developers. Feel free to upgrade it to a "feat", though. |
Most validation keywords apply to only one of the basic types, so a "string" type and an "array" type can share a definition without colliding as a ["string", "array"] type, as long as they don't have any incompatibilities with each other. Modify UnionTypeFormatter to collapse these disjoint types into a single definition, without using anyOf.
9e8c27d
to
246b65c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the pull request. Please take a look at the tests.
I think you probably just need to update the Vega-Lite test case. You can use the provided script to do it automatically. |
Change the array.flat() call to use spread notation, since both members are guaranteed to be arrays (unlike the types call, above). Thanks for the suggestion, @domoritz! Co-authored-by: Dominik Moritz <domoritz@gmail.com>
Right! I'll get on the tests now. Some of them weren't passing even in my original checkout, but at the very least I can make sure that anything affected by this change gets updated. |
Try |
The vega-lite test originally worked after updating the snapshot, but there was a broken reference that got mistakenly purged by removeUnreachable (which assumed that any definition would only ever have a single property that could hold references). Added a validation check to vega-lite.test to register the failure, updated addReachable to check all possible properties (unless $ref is set; the spec says that everything else gets ignored in that case).
I've actually got a couple test cases I want to add to exercise some of the weirder edge cases involving definition collapse, but I'm setting this down for the night. In any event, the existing test cases already exercise it fairly well. |
In addition to the raw schema tests which just check that nothing has changed since the last snapshot, this adds makeExemplar, which builds a raw value using the parsed BaseType representation of a TypeScript type tree. This way, tests can specify valid/invalid data to be verified using the Ajv validator in addition to raw schema output tests.
In a few simple cases like (1 | number), mergeDefinitions will now correctly collapse two not-quite-disjoint type definitions together, by discarding the validation restrictions on the more-restricted type. This replaces the string-merging logic in UnionTypeFormatter, which was discarding annotations.
Whew! That got much bigger than expected. I added some schema validation tests, which uncovered some issues with the existing code, which required a more elaborate test harness, which means the test harness needs more tests... you get the idea. One of these days I'll get all those yaks shaved. |
...and I literally just now realized that the makeExemplar code ought to be living on-or-near the BaseType classes themselves, which would clean up the huge mess of if-else-if-else in makeExemplar.ts. Oh well, there are more revisions I need to make anyway... Feel free to offer critique/direction if this isn't going in a useful direction, btw, I'm not averse to scrapping things and rebuilding. |
...and mergeDefinitions would get so very much smaller if I just called it twice, once in each direction. Yeah, don't merge this yet, I've got work to do. |
Thank you for working on this and making sure the feature is clean. I definitely think this is a good improvement. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this looks great. Please wrap up the remaining issues and mark the pull request as ready for (final) review.
return new SchemaGenerator(program, createParser(program, config), createFormatter(config), config); | ||
interface Validator<T> { | ||
(value: T): boolean | Promise<unknown>; | ||
// ^^^^^^^^^^^^^^^^ *Why* does validateSchema() sometimes return this? Who knows! >.< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix this ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha, that's not us! Ajv has a very unfortunate coding error, the .d.ts file has a different signature from the online documentation. This is just working around that.
I suppose I could open an issue on their repo, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be great and then add a link to it in the code. Thanks!
pass, | ||
message: pass | ||
? () => hint() + "Expected validation to fail, but it did not." + printReceived() | ||
: () => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use template strings here.
"items": { | ||
"type": "string" | ||
}, | ||
"type": [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is strange. So The type is either string or array but the items
property above only makes sense for array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is as intended/expected, actually! The type-specific JSON schema validation constraints specifically only apply when the value is of the appropriate type, which is why a lot of definitions can get collapsed together. If you included "items" without "type" it would mean "The value can be any type, but if it's an array its elements must be strings". So, this schema means "The value can be an array or a string. If the value is an array, its elements must be strings".
That's why all the type-specific properties have different names - "minLength" only applies to strings, but the same concept applied to arrays is "minItems".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. As long as we can still use the schema to generate APIs from it (which we do), I'm okay.
import { UnionType } from "../Type/UnionType"; | ||
|
||
export function makeExemplar(type: BaseType | undefined): unknown { | ||
return makeExemplars(type)[0]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a lot of untested code in this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I definitely need to refactor this into something cleaner. On my to-do list, certainly.
@@ -0,0 +1,230 @@ | |||
import { AliasType } from "../Type/AliasType"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is only used in tests, right? Then it shouldn't be in lib utils.
import { RawTypeName } from "../Schema/RawType"; | ||
|
||
const isIntegerKey = "\0isInteger"; | ||
type _Definition = Definition & { [isIntegerKey]?: true }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's clean this up. Import Definition above as something else so we don't need the _ here.
result.type = Array.from(new Set(type1.concat(type2))); | ||
} else if (type1 || type2) { | ||
// one is typed and one isn't - unmergeable | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the untested warnings below. Let's add some more tests.
} | ||
} else if (definition.not) { | ||
} else { | ||
addReachable(definition.anyOf, definitions, reachable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great
@dmchurch do you plan to wrap this up soon? |
@dmchurch can you please finish up this pull request? |
Most validation keywords apply to only one of the basic types, so a
"string" type and an "array" type can share a definition without
colliding as a ["string", "array"] type, as long as they don't have any
incompatibilities with each other. Modify UnionTypeFormatter to collapse
these disjoint types into a single definition, without using anyOf.