Skip to content

Commit

Permalink
feat: add 'when' statement support
Browse files Browse the repository at this point in the history
SO-23
  • Loading branch information
Chris Miaskowski authored and Marc MacLeod committed Dec 11, 2018
1 parent bc83dcf commit 0618337
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 28 deletions.
126 changes: 126 additions & 0 deletions src/linter/index.ts
@@ -0,0 +1,126 @@
import { PathComponent } from 'jsonpath';
import { filter, omitBy } from 'lodash';
import { IRule, IRuleOpts } from '../types';
import { IRuleEntry, IRuleResult, IRunOpts } from '../types/spectral';
const get = require('lodash/get');
const has = require('lodash/has');

// TODO(SO-23): unit test but mock whatShouldBeLinted
export function lintNode(
ruleEntry: IRuleEntry,
opts: IRunOpts,
node: { path: PathComponent[]; value: any }
): IRuleResult[] {
const conditioning = whatShouldBeLinted(node.value, ruleEntry.rule);
// If the 'when' condition is not satisfied, simply don't run the linter
if (!conditioning.lint) {
return [];
}

const opt: IRuleOpts = {
object: conditioning.value,
rule: ruleEntry.rule,
meta: {
path: node.path,
name: ruleEntry.name,
rule: ruleEntry.rule,
},
};

if (ruleEntry.rule.given === '$') {
// allow resolved and stringified targets to be passed to rules when operating on
// the root path
if (opts.resolvedTarget) {
opt.resObj = opts.resolvedTarget;
}
}

return ruleEntry.apply(opt);
}

// TODO(SO-23): unit test idividually
export function whatShouldBeLinted(originalValue: any, rule: IRule): { lint: boolean; value: any } {
const when = rule.when;
if (!when) {
return {
lint: true,
value: originalValue,
};
}

const pattern = when.pattern;
const field = when.field;
// what if someone's field is called '@key'?
const isKey = field === '@key';

if (!pattern) {
// - if no pattern given
// - if field is @key THEN check if object has ANY key
// - else check if object[field] exists (MAY exist and be undefined!)
if (isKey) {
return keyAndOptionalPattern(originalValue);
}
return {
lint: has(originalValue, field),
value: originalValue,
};
}

if (isKey) {
return keyAndOptionalPattern(originalValue, pattern);
}

const fieldValue = String(get(originalValue, when.field));

return {
lint: fieldValue.match(pattern) !== null,
value: originalValue,
};
}

function keyAndOptionalPattern(originalValue: any, pattern?: string) {
const type = typeof originalValue;
switch (type) {
case 'boolean':
case 'string':
case 'number':
case 'bigint':
case 'undefined':
case 'function':
return {
lint: false,
value: originalValue,
};
case 'object':
if (originalValue === null) {
return {
lint: false,
value: originalValue,
};
} else if (Array.isArray(originalValue)) {
const leanValue = pattern
? filter(originalValue, (v, index) => {
return String(index).match(pattern) === null;
})
: originalValue;
return {
lint: !!leanValue.length,
value: leanValue,
};
} else {
const leanValue = pattern
? omitBy(originalValue, (v, key) => {
return key.match(pattern) === null;
})
: originalValue;
return {
lint: !!Object.keys(leanValue).length,
value: leanValue,
};
}
default:
throw new Error(
`value: "${originalValue}" of type: "${type}" is an unsupported type of value for the "when" statement`
);
}
}
30 changes: 2 additions & 28 deletions src/spectral.ts
Expand Up @@ -4,8 +4,8 @@ const compact = require('lodash/compact');
const flatten = require('lodash/flatten');
import * as jp from 'jsonpath';

import { PathComponent } from 'jsonpath';
import { functions as defaultFunctions } from './functions';
import { lintNode } from './linter';
import * as types from './types';
import { IFunctionCollection, IRuleCollection, IRuleEntry, IRunOpts, IRunResult } from './types/spectral';

Expand Down Expand Up @@ -109,7 +109,7 @@ export class Spectral {
nodes.map(node => {
const { path: nPath } = node;
try {
return this.lintNode(ruleEntry, opts, node);
return lintNode(ruleEntry, opts, node);
} catch (e) {
console.warn(`Encountered error when running rule '${ruleEntry.name}' on node at path '${nPath}':\n${e}`);
return null;
Expand All @@ -119,32 +119,6 @@ export class Spectral {
);
}

private lintNode(
ruleEntry: IRuleEntry,
opts: IRunOpts,
node: { path: PathComponent[]; value: any }
): types.IRuleResult[] {
const opt: types.IRuleOpts = {
object: node.value,
rule: ruleEntry.rule,
meta: {
path: node.path,
name: ruleEntry.name,
rule: ruleEntry.rule,
},
};

if (ruleEntry.rule.given === '$') {
// allow resolved and stringified targets to be passed to rules when operating on
// the root path
if (opts.resolvedTarget) {
opt.resObj = opts.resolvedTarget;
}
}

return ruleEntry.apply(opt);
}

private parseRuleDefinition(name: string, format: string, rule: types.Rule): IRuleEntry {
const ruleIndex = this.toRuleIndex(name, format);
try {
Expand Down
9 changes: 9 additions & 0 deletions src/types/rule.ts
Expand Up @@ -36,6 +36,15 @@ export interface IRule<O = any> {
// Filter the target down to a subset[] with a JSON path
given: string;

when?: {
// the `path.to.prop` to field, or special `@key` value to target keys for matched `given` object
// EXAMPLE: if the target object is an oas object and given = `$..responses[*]`, then `@key` would be the response code (200, 400, etc)
field: string;

// a regex pattern
pattern?: string;
};

then: {
// the `path.to.prop` to field, or special `@key` value to target keys for matched `given` object
// EXAMPLE: if the target object is an oas object and given = `$..responses[*]`, then `@key` would be the response code (200, 400, etc)
Expand Down

0 comments on commit 0618337

Please sign in to comment.