-
Notifications
You must be signed in to change notification settings - Fork 13
PE-184 Require samples for non hidden ops #33
Conversation
44ab57d
to
7cd04d7
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.
So the idea here is right, but moving the requirement out of the schema itself and into the function means that the readme says sample is no longer required (which shows up only after you run npm run export && npm run docs
):
Key | Required | Type | Description |
---|---|---|---|
sample |
no | object |
What does a sample of data look like? Will use resource sample if missing. Required, if the display is not hidden. |
To me that reads a little confusingly because I (as a user) would just scan the required
column and be confused when this failed.
I wonder if we could go the other way - make it required but pull out errors if the action is hidden? Do the functional constraints run after the regular checks and do they have access to each others' errors?
If we can't do that ^, maybe we make Required
bold in the note so it stands out a bit more?
Yep, I didn't like that either. I did try something like this (looks like I lost the stash.. so the below is psuedo at this point):
'use strict';
const _ = require('lodash');
const isRequiredSampleError = ({ message }) => message === 'requires property "sample"';
module.exports = (validate) => (definition, schema) => {
const result = validate(definition, schema);
if (_.get(definition, 'display.hidden')) {
result.errors = result.errors.filter(({message}) => message !== 'require "sample"';
}
return result;
}; But the problem is with Validates each, and if any fails then the sub schema fails. Eventually you'll get an error that the sub-schema is not what it would expect. I tried to bubble up the error, but it started feeling very hacky and frail. That's why I ended up going with this approach. |
In general, it's hard to say that the sample is always required. I also tried to update the schema on the fly (e.g. find the BasicOperationSchema and remove the required samples for that definition and then put it back) but again it was a miss. |
Fair. Other thought- doesn't style checks on the server check for this? In that way, it's valid to have no sample, but everything visible will fail the style checks if it doesn't have one. Past that, I might poke on this tomorrow unless @eliangcs has any other flashes of insight. |
Ah, interesting idea, but I wonder about that. Doing a If so, I think we're still running into the issue of the sample required: |
Correct, by default That would sort of make sense. An object isn't invalid if it doesn't have samples, but they're required for public apps and stuff. I think that separates it nicely. |
Alrighty so it seems the approach then is to just revert the required samples here, but make them required in the server style checks? I'll follow-up on this tomorrow. |
So I spent some time on this today. JSON schema has conditionally required fields but since this is across schemas (sample is in operation and hidden is in field) that won't work. I didn't read carefully and basically went down a similar rabbit hole you did, trying to filter errors out right at the end. It works on everything except triggers (since they return a generic I came up with the following, which lives in the // handle exceptions to the rules
const results = v.validate(definition, mainSchema)
results.errors = results.errors.concat(
functionalConstraints.run(definition)
)
// samples aren't required on hidden operations
results.errors = results.errors.filter(e => {
if (e.name !== 'required' && e.argument !== 'sample') {
return true
}
const end = e.property.includes('resource') ? 4 : 3
const operation = e.property.split('.').slice(1, end).join('.')
const disp = _.get(definition, `${operation}.display`)
return !disp.hidden
})
// if operations belong to a resource, they can re-use the parent sample
results.errors = results.errors.filter(e => {
if (e.name !== 'required' && e.argument !== 'sample') {
return true
}
if (e.property.includes('resource')) {
const resource = e.property.split('.').slice(1, 2).join('.')
const sample = _.get(definition, `${resource}.sample`)
return Boolean(sample && Object.keys(sample).length)
} else {
return true
}
})
// filter busted triggers
// re-validate against each schema
// if there's exactly 1 issue and it's missing sample, check hidden; possibly filter
results.errors = results.errors.filter(e => {
if (e.name !== 'anyOf' && e.property.includes('triggers')) {
return true
}
const opPath = e.property.split('.').slice(1, 3).join('.')
const triggerOp = _.get(definition, `${opPath}.operation`)
// if either schema has only one problem, and that problem is sample, we good
const basicOperationSchemas = schemas.filter(s => s.id === '/BasicPollingOperationSchema' || s.id === '/BasicHookOperationSchema')
for (let schema of basicOperationSchemas) {
const res = v.validate(triggerOp, schema)
if (res.errors.length === 1) {
const err = res.errors[0]
if (err.name === 'required' && err.argument === 'sample') {
if (_.get(definition, `${opPath}.display`).hidden) {
return false // the only issue is missing sample, but the display is hidden
}
}
}
}
return true // real error
}) It's a first draft, but the idea is we see what errors are coming out and ignore those missing a sample when hidden. The one big hitch is that we re-validate schema server-side, including the functional constraints. I'm not sure if this is necessary, since pushing an invalid schema will just break your app. If you're tinkering with local installs of our tools in order to get around schema validation, you're only hurting yourself. How does that sound? |
Separately, this would also still work in style checks. i was having some trouble getting them to trigger, so we might need to double check that anyway. |
@xavdid ! thanks for checking in on this further. I was about to get started moving this over to style checks. I've looked through the approach and it makes sense. Let me pull it in to the PR. |
@xavdid I ended up going back to the current approach.. and gave it another try to support resources as well. I think I have something working. Give it a look! This way the backend validation passes (since samples are not required [at least in schema]). We may want to, however, add backend functional constraints that are similar to the ones in JS. With the current approach, we're able to do this. We'd just need to pass the schema into the functional constraint checker. |
Awesome! I pulled it and ran through and it looks good. Past that, I'm torn on which way to go for this (functional constraints vs filtering the error list). To help me think through it, I made a quick table:
So, I think exceptions are the better pattern to develop if we want to hide less complexity. For example, there's nothing in the docs that mention that That said, I'd love to hear input from @bcooksey and @eliangcs. This isn't a hill I'll die on, so I'm happy to 👍 if others feel strongly. ^ I don't like the fact that we duplicate the functional constraints on the server. I understand why we need to if we want to do server validation, but I'd love to see a more cohesive way to do it that doesn't involve adapting our js functions into python. |
const samples = _.get(definition, 'operation.sample', {}); | ||
if (!Object.keys(samples).length) { | ||
return new jsonschema.ValidationError( | ||
'required "sample" for non-hidden operation', |
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 think this gets phrased weird to the end user. Maybe ... requires "sample", because it's not hidden
}); | ||
} | ||
|
||
if (!definitions.length) |
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.
Could you wrap the body in {}
? Our eslint would yell at you, but apparently it was never set up on this repo. 😁
const RESOURCE_METHODS = ['get', 'hook', 'list', 'search', 'create']; | ||
|
||
const check = definition => { | ||
if (!definition.operation || _.get(definition, 'display.hidden')) 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.
curlies here too
definition, | ||
definition.id | ||
); | ||
} |
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.
add else {return null}
to comply with this rule
(added some quick review comments that you don't need to fix until we decide on a path forward. figured i'd do it while the code was fresh) |
@xavdid I read the conversation several times but is still a little bit confused. Questions:
|
Sure! Appreciate you reading through this.
|
@xavdid thanks for the clarification! Assuming you have to override sample: {
description:
'What does a sample of data look like? Will use resource sample if missing. Required, if the display is not hidden.',
type: 'object',
minProperties: 1,
docAnnotations: {
required: "(**conditionally**, see description)"
}
} Then we would render "no (conditionally, see description)" for the Required column in the doc.
I don't see this approach (doc annotation) that way. The annotation comes from the schema, so I think in some sense, the doc and the schema agree. |
Cool! I like that annotation approach. @ibolmo Olmo you can go ahead and make the tweaks above and merge. I'll add a jira task to do the doc modification stuff |
This exempts operations that are hidden.