Skip to content

Commit

Permalink
# added validation for global spec
Browse files Browse the repository at this point in the history
Signed-off-by: Theo Truong <[email protected]>
# updated docs
  • Loading branch information
nhtruong committed Apr 16, 2024
1 parent 9a2a397 commit 5188911
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 7 deletions.
1 change: 1 addition & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ This repository includes several penAPI Specification Extensions to fill in any
- `x-version-removed`: OpenSearch version when the operation/parameter was removed.
- `x-deprecation-message`: Reason for deprecation and guidance on how to prepare for the next major version.
- `x-ignorable`: Denotes that the operation should be ignored by the client generator. This is used in operation groups where some operations have been replaced by newer ones, but we still keep them in the specs because the server still supports them.
- `x-global`: Denotes that the parameter is a global parameter that is included in every operation. These parameters are listed in the [root file](spec/opensearch-openapi.yaml).

## Linting
We have a linter that validates every `yaml` file in the `./spec` folder to assure that they follow the guidelines we have set. Check out the [Linter](tools/README.md#linter) tool for more information on how to run it locally. Make sure to run the linter before submitting a PR.
Expand Down
7 changes: 6 additions & 1 deletion tools/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function sortByKey(obj: Record<string, any>, priorities: string[] = []) {
}

export function write2file(file_path: string, content: Record<string, any>): void {
fs.writeFileSync(file_path, quoteRefs(YAML.stringify(content, {lineWidth: 0, singleQuote: true})));
fs.writeFileSync(file_path, quoteRefs(YAML.stringify(removeAnchors(content), {lineWidth: 0, singleQuote: true})));
}

function quoteRefs(str: string): string {
Expand All @@ -39,4 +39,9 @@ function quoteRefs(str: string): string {
}
return line
}).join('\n');
}

function removeAnchors(content: Record<string, any>): Record<string, any> {
const replacer = (key: string, value: any) => key === '$anchor' ? undefined : value;
return JSON.parse(JSON.stringify(content, replacer));
}
18 changes: 16 additions & 2 deletions tools/linter/components/RootFile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ValidationError} from "../../types";
import {ParameterSpec, ValidationError} from "../../types";
import FileValidator from "./base/FileValidator";

export default class RootFile extends FileValidator {
Expand All @@ -8,7 +8,10 @@ export default class RootFile extends FileValidator {
}

validate_file(): ValidationError[] {
return this.validate_paths();
return [
this.validate_paths(),
this.validate_params(),
].flat();
}

validate_paths(): ValidationError[] {
Expand All @@ -17,4 +20,15 @@ export default class RootFile extends FileValidator {
return this.error(`Every path must be a reference object to a path in a namespace file.`, `Path: ${path}`);
}).filter((e) => e) as ValidationError[];
}

validate_params(): ValidationError[] {
const params = (this.spec().components?.parameters || {}) as Record<string, ParameterSpec>;
return Object.entries(params).map(([name, param]) => {
const expected = `_global::${param.in}.${param.name}`;
if(name !== expected)
return this.error(`Parameters in root file must be in the format '_global::{in}.{name}'. Expected '${expected}'.`, `#/components/parameters/${name}`);
if(!param['x-global'])
return this.error(`Parameters in root file must have 'x-global' extension set to true.`, `#/components/parameters/${name}`);
}).filter((e) => e) as ValidationError[];
}
}
21 changes: 18 additions & 3 deletions tools/merger/OpenApiMerger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { OpenAPIV3 } from "openapi-types";
import fs from 'fs';
import _ from 'lodash';
import yaml from 'yaml';
import { write2file } from '../helpers';

// Create a single-file OpenAPI spec from multiple files for OpenAPI validation and programmatic consumption
export default class OpenApiMerger {
root_path: string;
root_folder: string;
spec: Record<string, any>;
global_param_refs: OpenAPIV3.ReferenceObject[];

paths: Record<string, Record<string, OpenAPIV3.PathItemObject>> = {}; // namespace -> path -> path_item_object
schemas: Record<string, Record<string, OpenAPIV3.SchemaObject>> = {}; // category -> schema -> schema_object
Expand All @@ -16,23 +18,36 @@ export default class OpenApiMerger {
this.root_path = fs.realpathSync(root_path);
this.root_folder = this.root_path.split('/').slice(0, -1).join('/');
this.spec = yaml.parse(fs.readFileSync(this.root_path, 'utf8'));
const global_params: OpenAPIV3.ParameterObject = this.spec.components?.parameters || {};
this.global_param_refs = Object.keys(global_params).map(param => ({$ref: `#/components/parameters/${param}`}));
this.spec.components = {
parameters: this.spec.components?.parameters || {},
parameters: global_params,
requestBodies: {},
responses: {},
schemas: {},
};
}

merge(output_path?: string): OpenAPIV3.Document {
this.#merge_namespaces();
this.#merge_schemas();
this.#merge_namespaces();
this.#apply_global_params();
this.#sort_spec_keys();

if(output_path) fs.writeFileSync(output_path, yaml.stringify(this.spec, {lineWidth: 0, singleQuote: true}))
if(output_path) write2file(output_path, this.spec);
return this.spec as OpenAPIV3.Document;
}

// Apply global parameters to all operations in the spec.
#apply_global_params(): void {
Object.entries(this.spec.paths).forEach(([path, pathItem]) => {
Object.entries(pathItem!).forEach(([method, operation]) => {
const params = operation.parameters || [];
operation.parameters = [...params, ...Object.values(this.global_param_refs)];
});
});
}

// Merge files from <spec_root>/namespaces folder.
#merge_namespaces(): void {
const folder = `${this.root_folder}/namespaces`;
Expand Down
10 changes: 10 additions & 0 deletions tools/test/linter/RootFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ test('validate()', () => {
file: "root.yaml",
location: "Path: /{index}",
message: "Every path must be a reference object to a path in a namespace file."
},
{
file: "root.yaml",
location: "#/components/parameters/_global::query.pretty",
message: "Parameters in root file must be in the format '_global::{in}.{name}'. Expected '_global::query.beautify'."
},
{
file: "root.yaml",
location: "#/components/parameters/_global::query.human",
message: "Parameters in root file must have 'x-global' extension set to true."
}
]);
});
22 changes: 21 additions & 1 deletion tools/test/linter/fixtures/root.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,24 @@ paths:
$ref: '#/components/responses/indices.create@200'
parameters:
- $ref: '#/components/parameters/indices.create::path.index'
- $ref: '#/components/parameters/indices.create::query.pretty'
- $ref: '#/components/parameters/indices.create::query.pretty'
components:
parameters:
_global::query.pretty:
x-global: true
name: beautify
in: query
schema:
type: boolean
_global::query.human:
x-global: false
name: human
in: query
schema:
type: boolean
_global::query.error_trace:
x-global: true
name: error_trace
in: query
schema:
type: boolean
8 changes: 8 additions & 0 deletions tools/test/merger/fixtures/expected.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ info:
version: 1.0.0
paths:
/adopt/{animal}:
get:
parameters:
- $ref: '#/components/parameters/adopt::path.animal'
- $ref: '#/components/parameters/_global::query.human'
responses:
'200':
$ref: '#/components/responses/adopt@200'
post:
parameters:
- $ref: '#/components/parameters/adopt::path.animal'
- $ref: '#/components/parameters/_global::query.human'
requestBody:
$ref: '#/components/requestBodies/adopt'
responses:
Expand Down
6 changes: 6 additions & 0 deletions tools/test/merger/fixtures/spec/namespaces/shelter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ info:
version: 1.0.0
paths:
'/adopt/{animal}':
get:
parameters:
- $ref: '#/components/parameters/adopt::path.animal'
responses:
'200':
$ref: '#/components/responses/adopt@200'
post:
parameters:
- $ref: '#/components/parameters/adopt::path.animal'
Expand Down
1 change: 1 addition & 0 deletions tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ParameterSpec extends OpenAPIV3.ParameterObject {
'x-data-type'?: string;
'x-version-deprecated'?: string;
'x-deprecation-message'?: string;
'x-global'?: boolean;
}

export interface ValidationError {
Expand Down

0 comments on commit 5188911

Please sign in to comment.