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

chore(core): improve ruleset typings #2132

Merged
merged 1 commit into from Apr 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 8 additions & 5 deletions packages/core/package.json
Expand Up @@ -19,11 +19,15 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js"
"default": "./dist/index.js"
},
"./ruleset": {
"types": "./dist/ruleset/index.d.ts",
"default": "./dist/ruleset/index.js"
},
"./ruleset/validation": {
"types": "./dist/ruleset/validation/index.d.ts",
"require": "./dist/ruleset/validation/index.js"
"default": "./dist/ruleset/validation/index.js"
}
},
"engines": {
Expand All @@ -40,12 +44,12 @@
"@stoplight/spectral-parsers": "^1.0.0",
"@stoplight/spectral-ref-resolver": "^1.0.0",
"@stoplight/spectral-runtime": "^1.0.0",
"@stoplight/types": "12.3.0",
"@stoplight/types": "13.0.0",
Comment on lines -43 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

Plans to bump to 13 in the other spectral packages?

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, I intend to do it

"@types/json-schema": "^7.0.7",
"ajv": "^8.6.0",
"ajv-errors": "~3.0.0",
"ajv-formats": "~2.1.0",
"blueimp-md5": "2.18.0",
"json-schema": "0.4.0",
"jsonpath-plus": "6.0.1",
"lodash": "~4.17.21",
"lodash.topath": "^4.5.2",
Expand All @@ -60,7 +64,6 @@
"@stoplight/spectral-functions": "*",
"@stoplight/spectral-parsers": "*",
"@stoplight/yaml": "^4.2.2",
"@types/json-schema": "^7.0.7",
"@types/minimatch": "^3.0.5",
"@types/treeify": "^1.0.0",
"nock": "^13.1.0",
Expand Down
26 changes: 12 additions & 14 deletions packages/core/src/ruleset/__tests__/ruleset.test.ts
Expand Up @@ -9,8 +9,8 @@ import { print } from './__helpers__/print';
import { RulesetValidationError } from '../validation/index';
import { isPlainObject } from '@stoplight/json';
import { Format } from '../format';
import { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema';
import { FormatsSet } from '../utils/formatsSet';
import type { JSONSchema4, JSONSchema6, JSONSchema7 } from 'json-schema';
import { Formats } from '../formats';

async function loadRuleset(mod: Promise<{ default: RulesetDefinition }>, source?: string): Promise<Ruleset> {
return new Ruleset((await mod).default, { source });
Expand Down Expand Up @@ -1378,24 +1378,22 @@ describe('Ruleset', () => {
},
});

expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft4]))).toStrictEqual(['$..id']);
expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft6]))).toStrictEqual(['$..$id']);
expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft7]))).toStrictEqual(['$..$id']);
expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft6, draft7]))).toStrictEqual([
'$..$id',
]);
expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft4]))).toStrictEqual(['$..id']);
expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft6]))).toStrictEqual(['$..$id']);
expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft7]))).toStrictEqual(['$..$id']);
expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft6, draft7]))).toStrictEqual(['$..$id']);

expect(ruleset.rules['valid-parameter'].getGivenForFormats(new FormatsSet([oas2]))).toStrictEqual([
expect(ruleset.rules['valid-parameter'].getGivenForFormats(new Formats([oas2]))).toStrictEqual([
'$.parameters[*]',
'$.paths[*].parameters[?(@ && !@.$ref)]',
'$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@ && !@.$ref)]',
]);
expect(ruleset.rules['valid-parameter'].getGivenForFormats(new FormatsSet([oas3]))).toStrictEqual([
expect(ruleset.rules['valid-parameter'].getGivenForFormats(new Formats([oas3]))).toStrictEqual([
'$.components.parameters[*]',
'$.paths[*].parameters[?(@ && !@.$ref)]',
'$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@ && !@.$ref)]',
]);
expect(ruleset.rules['valid-parameter'].getGivenForFormats(new FormatsSet([oas2, oas3]))).toStrictEqual([
expect(ruleset.rules['valid-parameter'].getGivenForFormats(new Formats([oas2, oas3]))).toStrictEqual([
'$.components.parameters[*]',
'$.paths[*].parameters[?(@ && !@.$ref)]',
'$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@ && !@.$ref)]',
Expand Down Expand Up @@ -1442,7 +1440,7 @@ describe('Ruleset', () => {
},
});

expect(() => ruleset.rules['valid-header'].getGivenForFormats(new FormatsSet([oas3]))).toThrowError(
expect(() => ruleset.rules['valid-header'].getGivenForFormats(new Formats([oas3]))).toThrowError(
ReferenceError(
'Alias "HeaderObject" is circular. Resolution stack: HeaderObject -> HeaderObjects -> Components -> HeaderObject',
),
Expand Down Expand Up @@ -1479,8 +1477,8 @@ describe('Ruleset', () => {
},
});

expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft7]))).toStrictEqual([]);
expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([]))).toStrictEqual([]);
expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([draft7]))).toStrictEqual([]);
expect(ruleset.rules['valid-id'].getGivenForFormats(new Formats([]))).toStrictEqual([]);
});

it('should be serializable', () => {
Expand Down
@@ -1,10 +1,10 @@
import type { Format } from '../format';
import type { Format } from './format';

function printFormat(format: Format): string {
return format.displayName ?? format.name;
}

export class FormatsSet<T extends Format = Format> extends Set<T> {
export class Formats<T extends Format = Format> extends Set<T> {
public toJSON(): string[] {
return Array.from(this).map(printFormat);
}
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/ruleset/index.ts
@@ -1,7 +1,7 @@
export { assertValidRuleset, RulesetValidationError } from './validation/index';
export { getDiagnosticSeverity } from './utils';
export { createRulesetFunction, SchemaDefinition as RulesetFunctionSchemaDefinition } from './rulesetFunction';
export { getDiagnosticSeverity } from './utils/severity';
export { createRulesetFunction, SchemaDefinition as RulesetFunctionSchemaDefinition } from './function';
export { Format } from './format';
export { RulesetDefinition, RuleDefinition, ParserOptions, HumanReadableDiagnosticSeverity } from './types';
export { Ruleset } from './ruleset';
export { Rule } from './rule';
export { Ruleset, StringifiedRuleset } from './ruleset';
export { Rule, StringifiedRule } from './rule';
13 changes: 7 additions & 6 deletions packages/core/src/ruleset/rule.ts
Expand Up @@ -13,9 +13,10 @@ import type {
RuleDefinition,
RulesetAliasesDefinition,
RulesetScopedAliasDefinition,
Stringifable,
} from './types';
import { minimatch } from './utils/minimatch';
import { FormatsSet } from './utils/formatsSet';
import { Formats } from './formats';
import { isSimpleAliasDefinition } from './utils/guards';

const ALIAS = /^#([A-Za-z0-9_-]+)/;
Expand All @@ -25,7 +26,7 @@ export interface IRule {
message: string | null;
severity: DiagnosticSeverity;
resolved: boolean;
formats: Set<Format> | null;
formats: Formats | null;
enabled: boolean;
recommended: boolean;
documentationUrl: string | null;
Expand All @@ -35,7 +36,7 @@ export interface IRule {

export type StringifiedRule = Omit<IRule, 'formats' | 'then'> & {
name: string;
formats: FormatsSet | null;
formats: string[] | null;
then: (Pick<IRuleThen, 'field'> & { function: string; functionOptions?: string })[];
owner: number;
};
Expand All @@ -45,7 +46,7 @@ export class Rule implements IRule {
public message: string | null;
#severity!: DiagnosticSeverity;
public resolved: boolean;
public formats: FormatsSet | null;
public formats: Formats | null;
#enabled: boolean;
public recommended: boolean;
public documentationUrl: string | null;
Expand All @@ -64,7 +65,7 @@ export class Rule implements IRule {
this.documentationUrl = definition.documentationUrl ?? null;
this.severity = definition.severity;
this.resolved = definition.resolved !== false;
this.formats = 'formats' in definition ? new FormatsSet(definition.formats) : null;
this.formats = 'formats' in definition ? new Formats(definition.formats) : null;
this.then = definition.then;
this.given = definition.given;
}
Expand Down Expand Up @@ -241,7 +242,7 @@ export class Rule implements IRule {
return new Rule(this.name, this.definition, this.owner);
}

public toJSON(): StringifiedRule {
public toJSON(): Stringifable<StringifiedRule> {
return {
name: this.name,
recommended: this.recommended,
Expand Down
27 changes: 12 additions & 15 deletions packages/core/src/ruleset/ruleset.ts
@@ -1,20 +1,21 @@
import { dirname, relative } from '@stoplight/path';
import { isPlainObject, extractPointerFromRef, extractSourceFromRef } from '@stoplight/json';
import { DiagnosticSeverity } from '@stoplight/types';
import { minimatch } from './utils/minimatch';
import { Rule, StringifiedRule } from './rule';
import {
import type {
FileRulesetSeverityDefinition,
ParserOptions,
RulesetAliasesDefinition,
RulesetDefinition,
RulesetOverridesDefinition,
Stringifable,
} from './types';
import { assertValidRuleset } from './validation';
import { assertValidRuleset } from './validation/index';
import { mergeRule } from './mergers/rules';
import { DEFAULT_PARSER_OPTIONS, getDiagnosticSeverity } from '..';
import { mergeRulesets } from './mergers/rulesets';
import { isPlainObject, extractPointerFromRef, extractSourceFromRef } from '@stoplight/json';
import { DiagnosticSeverity } from '@stoplight/types';
import { FormatsSet } from './utils/formatsSet';
import { Formats } from './formats';
import { isSimpleAliasDefinition } from './utils/guards';

const STACK_SYMBOL = Symbol('@stoplight/spectral/ruleset/#stack');
Expand All @@ -33,7 +34,7 @@ export type StringifiedRuleset = {
extends: StringifiedRuleset[] | null;
source: string | null;
aliases: RulesetAliasesDefinition | null;
formats: FormatsSet | null;
formats: Formats | null;
rules: Record<string, StringifiedRule>;
overrides: RulesetOverridesDefinition | null;
parserOptions: ParserOptions;
Expand All @@ -43,7 +44,7 @@ export class Ruleset {
public readonly id = SEED++;

protected readonly extends: Ruleset[] | null;
public readonly formats = new FormatsSet();
public readonly formats = new Formats();
public readonly overrides: RulesetOverridesDefinition | null;
public readonly aliases: RulesetAliasesDefinition | null;
public readonly hasComplexAliases: boolean;
Expand Down Expand Up @@ -86,7 +87,7 @@ export class Ruleset {
hasComplexAliases = true;

const targets = value.targets.map(target => ({
formats: new FormatsSet(target.formats),
formats: new Formats(target.formats),
given: target.given,
}));

Expand Down Expand Up @@ -286,10 +287,9 @@ export class Ruleset {
this.formats.add(format);
}
} else if (rule.owner !== this) {
rule.formats =
rule.owner.definition.formats === void 0 ? null : new FormatsSet(rule.owner.definition.formats);
rule.formats = rule.owner.definition.formats === void 0 ? null : new Formats(rule.owner.definition.formats);
} else if (this.definition.formats !== void 0) {
rule.formats = new FormatsSet(this.definition.formats);
rule.formats = new Formats(this.definition.formats);
}

if (this.definition.documentationUrl !== void 0 && rule.documentationUrl === null) {
Expand All @@ -309,10 +309,7 @@ export class Ruleset {
return DEFAULT_RULESET_FILE.test(uri);
}

public toJSON(): Omit<StringifiedRuleset, 'extends' | 'rules'> & {
extends: Ruleset['extends'];
rules: Ruleset['rules'];
} {
public toJSON(): Stringifable<StringifiedRuleset> {
return {
id: this.id,
extends: this.extends,
Expand Down
21 changes: 14 additions & 7 deletions packages/core/src/ruleset/types.ts
@@ -1,7 +1,7 @@
import { DiagnosticSeverity } from '@stoplight/types';
import { Format } from './format';
import { RulesetFunction, RulesetFunctionWithValidator } from '../types';
import { FormatsSet } from './utils/formatsSet';
import type { DiagnosticSeverity } from '@stoplight/types';
import type { Format } from './format';
import type { RulesetFunction, RulesetFunctionWithValidator } from '../types';
import type { Formats } from './formats';

export type HumanReadableDiagnosticSeverity = 'error' | 'warn' | 'info' | 'hint' | 'off';
export type FileRuleSeverityDefinition = DiagnosticSeverity | HumanReadableDiagnosticSeverity | boolean;
Expand All @@ -17,7 +17,7 @@ export type ParserOptions = {
export type RuleDefinition = {
type?: 'validation' | 'style';

formats?: Format[];
formats?: Formats | Format[];

documentationUrl?: string;

Expand Down Expand Up @@ -81,7 +81,7 @@ export type RulesetOverridesDefinition = ReadonlyArray<{ files: string[] } & Rul
export type RulesetScopedAliasDefinition = {
description?: string;
targets: {
formats: FormatsSet | Format[];
formats: Formats | Format[];
given: string[];
}[];
};
Expand All @@ -92,7 +92,7 @@ export type RulesetDefinition = Readonly<
{
documentationUrl?: string;
description?: string;
formats?: FormatsSet | Format[];
formats?: Formats | Format[];
parserOptions?: Partial<ParserOptions>;
overrides?: RulesetOverridesDefinition;
aliases?: RulesetAliasesDefinition;
Expand All @@ -112,3 +112,10 @@ export type RulesetDefinition = Readonly<
}
>
>;

// eslint-disable-next-line @typescript-eslint/ban-types
export type Stringifable<T> = T extends object
? {
[P in keyof T]: Stringifable<T[P]> | { toJSON?(): Stringifable<T[P]> };
}
: T;
Comment on lines +116 to +121
Copy link
Contributor

Choose a reason for hiding this comment

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

Neat

1 change: 0 additions & 1 deletion packages/core/src/ruleset/utils/index.ts

This file was deleted.

4 changes: 3 additions & 1 deletion tsconfig.build.json
@@ -1,7 +1,9 @@
{
"include": [],
"extends": "./tsconfig.json",
"composite": true,
"compilerOptions": {
"composite": true
},
Comment on lines -4 to +6
Copy link
Contributor

Choose a reason for hiding this comment

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

🤦‍♂️

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, luckily newer TS caught this

"references": [
{
"path": "./packages/cli/tsconfig.build.json"
Expand Down