Skip to content

Commit

Permalink
feat(ns-openapi-3-1): add idempotence to header example plugin
Browse files Browse the repository at this point in the history
Refs #4134
  • Loading branch information
char0n committed May 23, 2024
1 parent aa63af5 commit 9704a76
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 328 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/apidom-ns-openapi-3-1/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@babel/runtime-corejs3": "^7.20.7",
"@swagger-api/apidom-ast": "^1.0.0-alpha.3",
"@swagger-api/apidom-core": "^1.0.0-alpha.3",
"@swagger-api/apidom-json-pointer": "^1.0.0-alpha.3",
"@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-alpha.3",
"@types/ramda": "~0.30.0",
"ramda": "~0.30.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { cloneDeep } from '@swagger-api/apidom-core';
import { Element, ArrayElement, toValue, cloneDeep } from '@swagger-api/apidom-core';

import HeaderElement from '../../elements/Header';
import ExampleElement from '../../elements/Example';
import { Predicates } from '../toolbox';
import type { Toolbox } from '../toolbox';
import OpenApi3_1Element from '../../elements/OpenApi3-1';

/**
* Override of Schema.example and Schema.examples field inside the Header Objects.
Expand All @@ -15,15 +16,51 @@ import { Predicates } from '../toolbox';
*
* The example value SHALL override the example provided by the schema.
* Furthermore, if referencing a schema that contains an example, the examples value SHALL override the example provided by the schema.
*
* NOTE: this plugin is idempotent
*/
type JSONPointer = string;
type JSONPointerTokens = string[];

interface PluginOptions {
scope?: JSONPointer | JSONPointerTokens;
storageField?: string;
}

/* eslint-disable no-param-reassign */
const plugin =
() =>
({ predicates }: { predicates: Predicates }) => {
({ scope = '/', storageField = 'x-normalized-header-examples' }: PluginOptions = {}) =>
(toolbox: Toolbox) => {
const { predicates, ancestorLineageToJSONPointer, compileJSONPointerTokens } = toolbox;
const scopeJSONPointer = Array.isArray(scope) ? compileJSONPointerTokens(scope) : scope;
let storage: ArrayElement | undefined;

return {
visitor: {
OpenApi3_1Element: {
enter(element: OpenApi3_1Element) {
// initialize the normalized storage
storage = element.get(storageField);
if (!predicates.isArrayElement(storage)) {
storage = new ArrayElement();
element.set(storageField, storage);
}
},
leave(element: OpenApi3_1Element) {
// make items in storage unique and release it
storage = new ArrayElement(Array.from(new Set(toValue(storage))));
element.set(storageField, storage);
storage = undefined;
},
},
HeaderElement: {
leave(headerElement: HeaderElement, key: any, parent: any, path: any, ancestors: any[]) {
leave(
headerElement: HeaderElement,
key: string | number,
parent: Element | undefined,
path: (string | number)[],
ancestors: [Element | Element[]],
) {
// skip visiting this Header Object
if (ancestors.some(predicates.isComponentsElement)) {
return;
Expand All @@ -44,6 +81,22 @@ const plugin =
return;
}

const headerJSONPointer = ancestorLineageToJSONPointer([
...ancestors,
parent!,
headerElement,
]);

// skip visiting this Header Object if it's already normalized
if (storage!.includes(headerJSONPointer)) {
return;
}

// skip visiting this Header Object if we're outside the assigned scope
if (!headerJSONPointer.startsWith(scopeJSONPointer)) {
return;
}

/**
* Header.examples and Schema.examples have preferences over the older
* and deprected `example` field.
Expand All @@ -59,9 +112,11 @@ const plugin =

if (typeof headerElement.schema.examples !== 'undefined') {
headerElement.schema.set('examples', examples);
storage!.push(headerJSONPointer);
}
if (typeof headerElement.schema.example !== 'undefined') {
headerElement.schema.set('example', examples);
headerElement.schema.set('example', examples[0]);
storage!.push(headerJSONPointer);
}
return;
}
Expand All @@ -72,9 +127,11 @@ const plugin =
if (typeof headerElement.example !== 'undefined') {
if (typeof headerElement.schema.examples !== 'undefined') {
headerElement.schema.set('examples', [cloneDeep(headerElement.example)]);
storage!.push(headerJSONPointer);
}
if (typeof headerElement.schema.example !== 'undefined') {
headerElement.schema.set('example', cloneDeep(headerElement.example));
storage!.push(headerJSONPointer);
}
}
},
Expand Down
40 changes: 38 additions & 2 deletions packages/apidom-ns-openapi-3-1/src/refractor/toolbox.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import {
Element,
Namespace,
ArrayElement,
isElement,
isStringElement,
isArrayElement,
isObjectElement,
isMemberElement,
toValue,
createNamespace,
includesClasses,
hasElementSourceMap,
} from '@swagger-api/apidom-core';
import { compile as compileJSONPointerTokens } from '@swagger-api/apidom-json-pointer';
import { isServersElement } from '@swagger-api/apidom-ns-openapi-3-0';

import * as openApi3_1Predicates from '../predicates';
Expand All @@ -24,7 +29,38 @@ export type Predicates = typeof openApi3_1Predicates & {
hasElementSourceMap: typeof hasElementSourceMap;
};

const createToolbox = () => {
export interface Toolbox {
predicates: Predicates;
compileJSONPointerTokens: typeof compileJSONPointerTokens;
ancestorLineageToJSONPointer: typeof ancestorLineageToJSONPointer;
namespace: Namespace;
}

/**
* Translates visitor ancestor lineage to a JSON Pointer tokens.
* Ancestor lineage is constructed of following visitor method arguments:
*
* - ancestors
* - parent
* - element
*/
const ancestorLineageToJSONPointer = <T extends (Element | Element[])[]>(elementPath: T) => {
const jsonPointerTokens = elementPath.reduce((path, element, index) => {
if (isMemberElement(element)) {
const token = String(toValue(element.key));
path.push(token);
} else if (isArrayElement(elementPath[index - 2])) {
const token = String((elementPath[index - 2] as ArrayElement).content.indexOf(element));
path.push(token);
}

return path;
}, [] as string[]);

return compileJSONPointerTokens(jsonPointerTokens);
};

const createToolbox = (): Toolbox => {
const namespace = createNamespace(openApi3_1Namespace);
const predicates: Predicates = {
...openApi3_1Predicates,
Expand All @@ -38,7 +74,7 @@ const createToolbox = () => {
hasElementSourceMap,
};

return { predicates, namespace };
return { predicates, ancestorLineageToJSONPointer, compileJSONPointerTokens, namespace };
};

export default createToolbox;
7 changes: 6 additions & 1 deletion packages/apidom-ns-openapi-3-1/test/refractor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ describe('refractor', function () {
plugins: [plugin1],
});

assert.hasAllKeys(plugin1.firstCall.args[0], ['predicates', 'namespace']);
assert.hasAllKeys(plugin1.firstCall.args[0], [
'predicates',
'namespace',
'ancestorLineageToJSONPointer',
'compileJSONPointerTokens',
]);
});

specify('should have predicates in toolbox object', function () {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect } from 'chai';
import dedent from 'dedent';
import { ObjectElement, toValue } from '@swagger-api/apidom-core';
import { parse } from '@swagger-api/apidom-parser-adapter-yaml-1-2';

import { OpenApi3_1Element, refractorPluginNormalizeHeaderExamples } from '../../../../../src';

describe('refractor', function () {
context('plugins', function () {
context('normalize-header-examples', function () {
context('given scope to limit the normalization', function () {
specify('should limit the scope of normalization', async function () {
const yamlDefinition = dedent`
openapi: 3.1.0
paths:
/:
get:
responses:
"200":
headers:
content-type:
schema:
type: number
example: 1
examples:
example1:
value: 2
"400":
headers:
content-type:
schema:
type: number
example: 1
examples:
example1:
value: 2
`;
const apiDOM = await parse(yamlDefinition);
const openApiElement = OpenApi3_1Element.refract(apiDOM.result, {
plugins: [
refractorPluginNormalizeHeaderExamples({ scope: '/paths/~1/get/responses/200' }),
],
}) as OpenApi3_1Element;

expect(toValue(openApiElement)).toMatchSnapshot();
});
});

context('given scope and running normalization multiple times', function () {
specify('should avoid normalizing the same scope multiple times', async function () {
const yamlDefinition = dedent`
openapi: 3.1.0
paths:
/:
get:
responses:
"200":
headers:
content-type:
schema:
type: number
example: 1
examples:
example1:
value: 2
`;
const apiDOM = await parse(yamlDefinition);
const result = apiDOM.result as ObjectElement;
result.set('x-normalized', ['/paths/~1/get/responses/200']);
const openApiElement = OpenApi3_1Element.refract(apiDOM.result, {
plugins: [
refractorPluginNormalizeHeaderExamples({ scope: '/paths/~1/get/responses/200' }),
],
}) as OpenApi3_1Element;

expect(toValue(openApiElement)).toMatchSnapshot();
});
});
});
});
});
Loading

0 comments on commit 9704a76

Please sign in to comment.