Skip to content
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

Draft
wants to merge 7 commits into
base: next
Choose a base branch
from

Conversation

dmchurch
Copy link

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.

@dmchurch dmchurch changed the title Merge compatible definitions in union types fix: Merge compatible definitions in union types Mar 25, 2021
@dmchurch
Copy link
Author

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.
Copy link
Member

@domoritz domoritz left a 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.

src/Utils/mergeDefinitions.ts Outdated Show resolved Hide resolved
@domoritz domoritz changed the title fix: Merge compatible definitions in union types fix: merge compatible definitions in union types Mar 25, 2021
@domoritz
Copy link
Member

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>
@dmchurch
Copy link
Author

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.

@domoritz
Copy link
Member

Try yarn test:update

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).
@dmchurch
Copy link
Author

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.
@dmchurch
Copy link
Author

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.

@dmchurch
Copy link
Author

...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.

@dmchurch
Copy link
Author

...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.

@domoritz
Copy link
Member

Thank you for working on this and making sure the feature is clean. I definitely think this is a good improvement.

@domoritz domoritz marked this pull request as draft March 28, 2021 15:57
Copy link
Member

@domoritz domoritz left a 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! >.<
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix this ;-)

Copy link
Author

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.

Copy link
Member

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()
: () =>
Copy link
Member

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": [
Copy link
Member

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.

Copy link
Author

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".

Copy link
Member

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];
Copy link
Member

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.

Copy link
Author

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";
Copy link
Member

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 };
Copy link
Member

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;
Copy link
Member

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great

@domoritz
Copy link
Member

domoritz commented May 7, 2021

@dmchurch do you plan to wrap this up soon?

@domoritz
Copy link
Member

@dmchurch can you please finish up this pull request?

@domoritz domoritz changed the base branch from master to next October 5, 2021 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants