Skip to content
This repository has been archived by the owner on May 17, 2024. It is now read-only.

attempt to hoist better errors #62

Merged
merged 5 commits into from
Mar 1, 2019
Merged

attempt to hoist better errors #62

merged 5 commits into from
Mar 1, 2019

Conversation

xavdid
Copy link
Contributor

@xavdid xavdid commented Feb 20, 2019

attempt to hoist the real issue when validation throws a generic "is not one of x,y"

before

┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ = 1 =                                                                                                                        │
│     Property │ App.triggers.blah.operation                                                                                   │
│     Message  │ is not any of </BasicPollingOperationSchema>,</BasicHookOperationSchema>                                      │
│     Links    │ https://github.com/zapier/zapier-platform-schema/blob/v7.6.1/docs/build/schema.md#basicpollingoperationschema │
│              │ https://github.com/zapier/zapier-platform-schema/blob/v7.6.1/docs/build/schema.md#basichookoperationschema    │
└──────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

after

┌───────────────────────────────────────────────────────────────┐
│ = 1 =                                                         │
│     Property │ App.triggers.blah.operation.inputFields[0].key │
│     Message  │ does not meet minimum length of 1              │
│     Links    │                                                │
└──────────────┴────────────────────────────────────────────────┘

@@ -1,7 +1,7 @@
{
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 8
"ecmaVersion": 2018
Copy link
Contributor Author

Choose a reason for hiding this comment

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

eslint was complaining about

const {x, ...y} = someObj

so I bumped it. Side note, I'd love to move this to use a more standard config. Standard.js is my personal favorite, but the front-end team's, airbnb's, or others are good bets too.

@xavdid xavdid marked this pull request as ready for review February 23, 2019 04:06
@xavdid xavdid requested a review from eliangcs February 23, 2019 04:06
Copy link
Member

@eliangcs eliangcs left a comment

Choose a reason for hiding this comment

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

The unreadability of anyOf error messages is something that bugs me for a long time. Thanks for fixing it! I tested locally and found some error messages are still unclear when it comes to anonymous schema. I wonder if it's fixable.

if (ambiguousTypes.includes(validationError.name)) {
// TODO: make this smarter?
// if it's triggerSchema, check for "type"
// if it's request/function, check for "source" or "require". if neither are present, validate against request
Copy link
Member

Choose a reason for hiding this comment

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

(Non-blocker) For this TODO, do you think if it's possible to have a generic solution instead of relying on the prior knowledge we have on the app schema? I was thinking we can measure the "relevance" and find out the best match, similar to what python-jsonschema does.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that... is very cool. It's a little weird because it looks like they just de-prioritize the anyOf errors. In that link, STRONG_MATCHES is an empty set, so i'm a little confused at how much value there is there.

And yeah, i was going back and forth here. On the one hand, if we only fish on polling/hook operation schema, we're already seeing a big improvement. doing more is even better, but i'd happily ship with just that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One idea I had was to recurse down each option (in this case pollingOperation and hookOperation) and then surface whichever path had fewer errors. Basically, the way we do it now, we could send someone down the wrong path if they do have a very malformed string request or something. Very possible I'm overthinking it though.

My other thought is fix polling vs hook explicitly (since that's the most confusing one - it's usually an issue with an input field or something and it doesn't actually matter which of the two we validate against) and then putting a line in for all other cases of anyOf that says "Please file an issue if you see this so we can better cover this case". And then we can build out this section as needed.

what do you think?

const namedSchema = validator.schemas[validationError.schema];

if (!namedSchema) {
return processBaseError(validationError, path);
Copy link
Member

Choose a reason for hiding this comment

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

If I do this for a perform:

perform: {
  url: 'https://example.com',
  body: 123
}

I see this error:

┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ = 1 =                                                                                                          │
│     Property │ App.triggers.movie.operation.perform.body                                                       │
│     Message  │ is not exactly one from [subschema 0],[subschema 1],[subschema 2],[subschema 3]                 │
│     Links    │ https://github.com/zapier/zapier-platform-schema/blob/v7.6.1/docs/build/schema.md#[subschema 0] │
│              │ https://github.com/zapier/zapier-platform-schema/blob/v7.6.1/docs/build/schema.md#[subschema 1] │
│              │ https://github.com/zapier/zapier-platform-schema/blob/v7.6.1/docs/build/schema.md#[subschema 2] │
│              │ https://github.com/zapier/zapier-platform-schema/blob/v7.6.1/docs/build/schema.md#[subschema 3] │
└──────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────┘

Is it possible to replace [subschema x] with a more readable text?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

maybe! i noticed the same thing. I think it's a problem of going one level too far down. I'll jump back in, but my guess is that the error structure is something like

{
  argument: ['subschema 0', 'subschema 1', ...],
  schema: [{type: 'number'}, {type: 'object'}... ]
  ...
}

so we could pull those types out. I agree with the above that we should be careful doing this too coupled to the structure of the schema itself, but some of these edge cases are pretty wild.

@xavdid
Copy link
Contributor Author

xavdid commented Feb 27, 2019

Ok @eliangcs I've cleaned it up and done what I can with the links and errors. I don't think it's perfect, but it's a step in the right direction and we'll be able to iterate on it as needed.

@xavdid
Copy link
Contributor Author

xavdid commented Feb 27, 2019

Oh and the python implementation was fine, but changed the structure of the errors enough that it wasn't going to be worth the time to redo the cli code to accomodate it. Plus, best_error only returned a single item from the entire validation while there could be multiples across an entire app.

Copy link
Member

@eliangcs eliangcs left a comment

Choose a reason for hiding this comment

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

I dug into this a bit. This is truly not an easy problem. Thanks for the effort! There's a couple of edge cases I think we should fix before we merge, but I'm approving this in advance not to block you.

const completePath = makePath(path, err.property).replace(
/\.instance\./g,
'.'
);
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 there are some cases where the path ends with .instance. Maybe we can change it to something like:

  const completePath = _.trimEnd(
    makePath(path, err.property).replace(/\.instance($|\.)/g, '.'),
    '.'
  );

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, good call. If you've still got it, it would be great to get an input + schema that generated that so I can write a test against it

Copy link
Member

Choose a reason for hiding this comment

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

One example is when you validate a primitive value against FieldSchema. Like inputFields: [123].

throw new Error(
'If you see this, please file an issue at https://github.com/zapier/zapier-platform-cli/issues'
);
}
Copy link
Member

Choose a reason for hiding this comment

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

Throwing a hard error here blocks the dev from continuing their work, which is worse than the original behavior IMO. Maybe we want to take a milder approach here. Can we print a warning message and keep validation going, so the dev can see the validation errors at least?

I found one of the reasons why res.errors is an empty array below is because of this jsonschema bug, where validationError.instance is undefined when the value being validated is falsy. To reproduce, try using a falsy value like an empty string in inputFields:

{
  inputFields: [''],
}

To work around this bug, I suggest we do this before we do validator.validate(validationError.instance, nextSchema):

      if (validationError.instance === undefined) {
        // Work around a jsonschema bug: When the value being validated is a
        // falsy, such as false and '', validationError.instance isn't available.
        // See https://github.com/tdegrunt/jsonschema/issues/263
        const fullPath = path.replace(/^instance\./, '') + validationError.property.replace(/^instance\./, '.');
        validationError.instance = _.get(definition, fullPath);
      }

Important: You need to pass definition to cleanErrors if you want to use this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ha, that's why I made this change but didn't connect the dots. Great find!

The reason I was throwing the error was that there should be an error here (since validation has failed) but we have no error to throw. If we don't throw, we fail to validate but there's nothing to show, which I think is worse behavior. Given that, do you think it's appropriate to throw an error?

I do like pulling the instance out from above though, if we can.

Copy link
Member

@eliangcs eliangcs Feb 28, 2019

Choose a reason for hiding this comment

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

I was thinking like what if they have other errors? So for example, if I do inputFields: [false] and another violation like a trigger description being too short, this would just show me this block of red text:

I'd have no idea what's wrong with my code if I've ever seen this. However, if we print a warning message instead of throwing an error over this, we can still see the other error:

All that is to say even if validation has failed on one branch, we can still keep running validation on the other branches. Does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah, sure. I guess in my head we could return an error instead of throwing it because we don't want to actually halt execution. good call!

});

it('should have decent messages for minimum length not met', () => {
const results = CreateSchema.validate({
const results = TriggerSchema.validate({
Copy link
Member

Choose a reason for hiding this comment

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

Any reasons why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Triggerschema operation is itself an anyOf (polling/hook) so I wanted to make sure errors in a nested field (like inputData) correctly surfaces in that situation. Sort of the base case we're trying to handle

@xavdid xavdid merged commit 80ecfc2 into master Mar 1, 2019
@xavdid xavdid deleted the better-schema-errors branch March 1, 2019 03:06
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants