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

Create amp-validator-rules package #377

Merged
merged 13 commits into from
Jul 11, 2019
Merged

Conversation

fstanis
Copy link
Collaborator

@fstanis fstanis commented Jul 5, 2019

This creates a new tool, amp-validator-query, that makes it easier to query AMP validator JSON rules to extract information.

@fstanis fstanis changed the title Create amp-validatory-query package Create amp-validator-query package Jul 5, 2019
Copy link
Collaborator

@ithinkihaveacat ithinkihaveacat left a comment

Choose a reason for hiding this comment

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

Cool, dropped some comments! I think either @sebastianbenz or @andreban should review as well. (I also don't have merge rights.)

#### ES Module (Browser)

```javascript
import { load } from 'amp-toolbox-validator-query';

This comment was marked as resolved.

#### CommonJs (Node)

```javascript
const { load } = require('amp-toolbox-validator-query');

This comment was marked as resolved.

* `noCache`: true to always fetch latest rules (by default, subsequent calls to `load` reuse the same result).
* `rules`: object to use locally specified rules instead of fetching them from the AMP CDN.
* `url`: override the URL where validator rules are fetched from.
* `source`: one of `'local'` (load rules from local file named "validator.json"), `'remote'` (fetch rules from CDN) or `'auto'` which is the default (tries looking for the local file first, then tries to fetch from CDN).
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not for this PR, but we should create some standard way to handle these options (noCache is also related).

Other places where a similar mechanism exists:

Copy link
Collaborator

Choose a reason for hiding this comment

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

Migrated this to its own issue: #378

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FWIW: this is the only toolbox package that works with validator.json (as opposed to validator.js).

In theory, this is meant to be a low-level interface to that file, so no other tool should ever need to load validator.json directly.

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 to Michael's comment

@@ -0,0 +1,27 @@
{
"name": "@ampproject/validator-query",

This comment was marked as resolved.

return cached;
}

module.exports = { load };

This comment was marked as resolved.

@@ -0,0 +1,171 @@
/**

This comment was marked as resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, it's not obvious from the log, but if I try to make a test break (e.g. change one expected result), npm test complains, so I'm quite confident it runs.

The pattern also matches this file if I'm reading it correctly (** should also work for .)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

You're right, comment withdrawn!

*/

const https = require('https');
const {promisify} = require('util');

This comment was marked as resolved.

Copy link
Collaborator

@sebastianbenz sebastianbenz left a comment

Choose a reason for hiding this comment

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

Awesome! Left a few comments inline.

Two higher level comments:

  • What are typical use cases for API? Are these covered by the two methods we provide? Would it make sense to always fetch rules per format? e.g.: ValidationRules.fetch('AMPHTML')?
  • Suggestion: can we rename the project to toolbox-validation-rules? I think this better explains what it does.

#### ES Module (Browser)

```javascript
import { load } from '@ampproject/toolbox-validator-query';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we use a more descriptive name? Suggestion:

import { ValidationRules } from '@ampproject/toolbox-validator-query';

const validationRules = await ValidationRules.fetch()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, I'm OK with renaming, but I have two problems with that:

  • fetch sounds like it misrepresents what that method does since it may not fetch anything, depending on the options provided.
  • What other methods would ValidationRules have? I'm kind of reluctant to expose the AmpValidatorQuery class itself since the end user shouldn't need to instantiate it on their own and I don't see any other functions we'd need. That said, I'm OK with having ValidationRules an object with a single method if you feel that makes sense.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The reason I prefer to export a class is that it becomes easier to test:

Users will use:

const validationRules = await ValidationRules.fetch()

For testing you can do:

const validationRules = new ValidationRules(fetchMock).fetch()

fetch was just a suggestion. What I like about it is that it makes it clear that this might perform a network request (as opposed to ValidationRules.get().

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So, I don't see a reason to make the class public since testing is already possible by directly including (see AmpValidatorRulesSpec.js) - so this is the closest we can get to package-private constructor. The user can also specify a mock to the load function via the rules parameter (e.g. load({ rules: { ... } })). Exposing the class makes me feel like it can cause a lot of confusion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Even better if we don't need to export a class, however, we still should export an object in that case:

module.exports = {
  fetch: ... (or whatever name we decide on)
}

This is to keep the possibility to add more methods in the future and to be consistent with the other modules, e.g.:

const AmpOptimizer = require('@ampproject/toolbox-optimizer');

const ampOptimizer = AmpOptimizer.create();

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done, renamed to fetch and ensured it's exported as an object (it was already, but I made it clear in the readme).

* `noCache`: true to always fetch latest rules (by default, subsequent calls to `load` reuse the same result).
* `rules`: object to use locally specified rules instead of fetching them from the AMP CDN.
* `url`: override the URL where validator rules are fetched from.
* `source`: one of `'local'` (load rules from local file named "validator.json"), `'remote'` (fetch rules from CDN) or `'auto'` which is the default (tries looking for the local file first, then tries to fetch from CDN).
Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 to Michael's comment

return JSON.parse(data);
}

async function loadLocal() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should move this functionality into the core package (we need it for optimizer as well) and find a common approach. As a start, I'd suggest only using OneBehindFetch and add caching later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed, sort of. I consider the current loadRules.js temporary, so when we figure out what to do with #378, most of it will probably go away.

});

function makeQuery(rules) {
return new AmpValidatorQuery(

This comment was marked as resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The unit test doesn't do any network requests - in fact, the AmpValidatorQuery can't fetch the rules, it expects them to be provided (load fetches them and passes them to AmpValidatorQuery). Here the rules are provided as a local object.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You're right, I was one step ahead in my mind.

@@ -0,0 +1,57 @@
# AMP-Toolbox Validator Query

Makes it easier to query published AMP Validator rules and extract information

This comment was marked as resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed

const query = load();

// Get all tag names used in AMP for Email
const names = query.getTagsForFormat('AMP4EMAIL').map(tag => tag.tagName);

This comment was marked as resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Linked to relevant proto definitions and added examples for all methods.

this.initRules_(rules);
}

getTagsForFormat(format, transformed) {

This comment was marked as resolved.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Add JSDoc

});
}

getExtensionsForFormat(format) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add JSDoc

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd expect this method to return an array. Why return an object instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  • There shouldn't ever be more than one extension with the same name and...
  • ...I assumed the query would usually be "give me information about amp-bind", so an object seems more appropriate. Also, it's cheap to transform an object back into an array with Object.keys and/or Object.values.

Copy link
Collaborator

Choose a reason for hiding this comment

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

How about turning this into two different APIs?

const ampBindRule = validationRules.getExtension('amp-bind');

const extensions = validationRules.getExtensions() <- returns an array containing all extensions
const mailExtensions = validationRules.getExtensions({format: 'AMP4EMAIL'}) <- with optional format parameter

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I removed getExtensions and instead exposed it as an extensions property and added getExtension which follows your suggestion.

* limitations under the License.
*/

class AmpValidatorQuery {

This comment was marked as resolved.

if (transformed) {
return this.checkEntityFormat_(entity, 'transformed');
}
if (entity.enabledBy && entity.enabledBy.includes('transformed')) {

This comment was marked as outdated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Looking back, I wonder if I can just reuse checkEntityFormat_ and call it as !this.checkEntityFormat_(entity, 'transformed')...

Will need to give it some thought.

@fstanis fstanis changed the title Create amp-validator-query package Create amp-validator-rules package Jul 9, 2019
@fstanis
Copy link
Collaborator Author

fstanis commented Jul 9, 2019

All comments should be addressed.

What are typical use cases for API? Are these covered by the two methods we provide?

Hard to say: I went with a conservative API that I hope will evolve over time as we identify more use-cases.

Would it make sense to always fetch rules per format?

I'd be OK with that, but also don't see any advantages in that approach, especially considering the underlying data is the same for all formats.

Suggestion: can we rename the project to toolbox-validation-rules?

Done.


```javascript
// Loads the validator rules remotely with default options
const rules = validatorRules.fetch();

This comment was marked as resolved.

console.log(tags.map(tag => tag.tagName));

// Get information about an extension
const ext = rules.getExtension('amp-carousel', 'AMP4EMAIL');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do you need to provide the format here? This seems counterintuitive.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is just how validator rules are written - there's more than one extension definition with the same name, but different formats.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The implementation uses a different order getExtension(format, extension).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ouch, good catch. Fixed.


Specifically:

- The `raw` property is unprocessed [ValidatorRules](https://github.com/ampproject/amphtml/blob/master/validator/validator.proto#L643)

This comment was marked as resolved.

let cached = null;

async function load(opt) {
opt = opt || {};

This comment was marked as resolved.

}

let rules = opt.rules;
delete opt.rules;

This comment was marked as resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't even remember why I had it there. Thanks for flagging, removed.

console.log(tags.map(tag => tag.tagName));

// Get information about an extension
const ext = rules.getExtension('amp-carousel', 'AMP4EMAIL');

This comment was marked as resolved.

for (const attrList of tag.attrLists) {
tag.attrs.push(...this.attrLists_[attrList]);
}
delete tag.attrLists;

This comment was marked as resolved.

async function loadRules({source, url}) {
url = url || VALIDATOR_RULES_URL;
switch (source) {
case 'local':
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of configuring this via options, you could also provide to methods fetch and loadFile on the ValidationRules object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I feel the functionality is not different enough to vouch having two different functions and that also prevents me from easily having the default option which is "try local if possible".

That said, maybe source is not needed if the url is a local file, i.e. use something like

async function loadUrlOrFile(urlOrPath) {

Ideally, whatever the resolution of #378 ends up being will replace this file anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sounds good. I'd suggest removing loadLocal until we settled on a common approach. Once it's out there it's harder to deprecate and I don't think that the feature is critical.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fair point, done.

@fstanis
Copy link
Collaborator Author

fstanis commented Jul 10, 2019

Addressed all comments.

Copy link
Collaborator

@sebastianbenz sebastianbenz left a comment

Choose a reason for hiding this comment

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

Looks good! Few nits.

@@ -60,7 +61,7 @@ The rules used closely follow the proto definitions from [validator.proto](https

Specifically:

- The `raw` property is unprocessed [ValidatorRules](https://github.com/ampproject/amphtml/blob/master/validator/validator.proto#L643)
- The `raw` property is unprocessed [ValidatorRules](https://github.com/ampproject/amphtml/blob/master/validator/validator.proto#L643), the same format used by `https://cdn.ampproject.org/v0/validator.json`
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: turn this into a link

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Don't think that makes sense, clicking that link is probably not very useful, it's more meaningful as an identifier.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd want to click it :-). But I agree unformatted JSON is not helpful.

@@ -158,15 +158,21 @@ class AmpValidatorRules {
.filter((tag) => !tag.extensionSpec)
.map((tag) => {
tag.attrs = tag.attrs || [];

// Merge global attribute lists into atrrs
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: s/atrrs/attrLists

Your comment explains what you're doing, but I get that from the code. I'm more interested in the why (which I don't understand)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Tried rewriting. The key thing is that attrLists contains a list of IDs that are looked up from the top-level attrLists property. It doesn't contain any real attribute definition. This bit of code merges the real attribute definitions (from the global object) into attrs.

Let me know if the current comment makes sense to convey that.

async function loadRules({source, url}) {
url = url || VALIDATOR_RULES_URL;
switch (source) {
case 'local':
Copy link
Collaborator

Choose a reason for hiding this comment

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

Sounds good. I'd suggest removing loadLocal until we settled on a common approach. Once it's out there it's harder to deprecate and I don't think that the feature is critical.

Copy link
Collaborator

@sebastianbenz sebastianbenz left a comment

Choose a reason for hiding this comment

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

One nit

console.log(tags.map(tag => tag.tagName));

// Get information about an extension
const ext = rules.getExtension('amp-carousel', 'AMP4EMAIL');
Copy link
Collaborator

Choose a reason for hiding this comment

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

The implementation uses a different order getExtension(format, extension).

@@ -60,7 +61,7 @@ The rules used closely follow the proto definitions from [validator.proto](https

Specifically:

- The `raw` property is unprocessed [ValidatorRules](https://github.com/ampproject/amphtml/blob/master/validator/validator.proto#L643)
- The `raw` property is unprocessed [ValidatorRules](https://github.com/ampproject/amphtml/blob/master/validator/validator.proto#L643), the same format used by `https://cdn.ampproject.org/v0/validator.json`
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd want to click it :-). But I agree unformatted JSON is not helpful.

return req.json();
}

async function loadRules({url}) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I learned something, was't aware that restructuring works for params.

@fstanis fstanis force-pushed the validator branch 2 times, most recently from d9e83d6 to 9d85829 Compare July 11, 2019 14:09
@sebastianbenz sebastianbenz merged commit 651f3d9 into ampproject:master Jul 11, 2019
@sebastianbenz
Copy link
Collaborator

Whoop whoop! Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants