Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,14 @@ jobs:
env:
RELEASE_TYPE: ${{ github.event.inputs.release-type }}
run: |
VERSION=$(npm version $RELEASE_TYPE)
git commit --amend -m "chore: release $VERSION"
# Bump root and all workspaces to the same version, no git tag yet
npm version $RELEASE_TYPE --workspaces --no-git-tag-version
# Read the new version from root package.json
VERSION="v$(node -p "require('./package.json').version")"
# Commit all bumps and create a single tag
git add package.json api-guidelines-redocly2-ruleset/package.json
git commit -m "chore: release $VERSION"
git tag $VERSION
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Push changes
Expand All @@ -56,4 +62,4 @@ jobs:
- name: Publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm publish
run: npm publish --workspaces --include-workspace-root
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ jobs:
cache: npm

- run: npm ci
- run: npm run build
- run: npm run tsc
- run: npm run test
- run: npm run build:all
- run: npm run tsc:all
- run: npm run test:all
40 changes: 40 additions & 0 deletions api-guidelines-redocly2-ruleset/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@otto-de/api-guidelines-redocly2-ruleset",
"version": "1.0.1",
"description": "Redocly v2 linting plugin for the OTTO API Guidelines ruleset",
"author": "",
"license": "ISC",
"homepage": "https://github.com/otto-de/api-guidelines#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/otto-de/api-guidelines.git"
},
"bugs": {
"url": "https://github.com/otto-de/api-guidelines/issues"
},
"engines": {
"node": ">=16"
},
"type": "module",
"main": "dist/plugin.js",
"files": [
"dist",
"src",
"!**/*.spec.ts",
"!**/__tests__"
],
"scripts": {
"build": "npm run clean && npm run build:redocly",
"build:redocly": "esbuild src/plugin.ts --bundle --platform=node --format=esm --outfile=dist/plugin.js --log-level=warning --external:@redocly/openapi-core",
"clean": "rm -rf ./dist",
"test": "vitest run",
"tsc": "tsc --noEmit"
},
"devDependencies": {
"@redocly/openapi-core": "^2.25.4",
"@types/node": "^20.9.4",
"esbuild": "^0.19.7",
"typescript": "^5.3.2",
"vitest": "^0.34.6"
}
}
84 changes: 84 additions & 0 deletions api-guidelines-redocly2-ruleset/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { UseTLS } from "./rules/use-tls.js";
import { AlwaysReturnJsonObject } from "./rules/always-return-json-object.js";
import { DefinePermissionsWithScope } from "./rules/define-permissions-with-scope.js";
import { FormatEnumerationUpperSnakeCase } from "./rules/format-enumeration-upper-snake-case.js";
import { NoRequestBodyInGetMethod } from "./rules/no-request-body-in-get-method.js";
import { NotUseNullForEmptyArray } from "./rules/not-use-null-for-empty-array.js";
import { OmitOptionalProperty } from "./rules/omit-optional-property.js";
import { SecureEndpointsWithOAuth20 } from "./rules/secure-endpoints-with-oauth-2.0.js";
import { UseAbsoluteCustomLinkRelationUrl } from "./rules/use-absolute-custom-link-relation-url.js";
import { UseAbsoluteProfileUrl } from "./rules/use-absolute-profile-url.js";
import { UseAuthorizationGrant } from "./rules/use-authorization-grant.js";
import { UseCamelCaseForPropertyName } from "./rules/use-camel-case-for-property-name.js";
import { UseCamelCaseForQueryParameter } from "./rules/use-camel-case-for-query-parameter.js";
import { UseCommonDateAndTimeFormat } from "./rules/use-common-date-and-time-format.js";
import { UseCuriedLinkRelationTypes } from "./rules/use-curied-link-relation-types.js";
import { UseExtensibleEnum } from "./rules/use-extensible-enum.js";
import { UseKebabCaseForPathParameter } from "./rules/use-kebab-case-for-path-parameter.js";
import { UseKebabCaseInUri } from "./rules/use-kebab-case-in-uri.js";
import { UseOas3 } from "./rules/use-oas-3.js";
import { UseStringEnum } from "./rules/use-string-enum.js";
import { Operation4xxProblemDetailsRfc9457 } from "./rules/operation-4xx-problem-details-rfc9457.js";

export default function ApiGuidelinesPlugin() {
return {
id: "api-guidelines",
rules: {
oas2: {
"use-oas-3": UseOas3,
},
oas3: {
"always-return-json-object": AlwaysReturnJsonObject,
"define-permissions-with-scope": DefinePermissionsWithScope,
"format-enumeration-upper-snake-case": FormatEnumerationUpperSnakeCase,
"no-request-body-in-get-method": NoRequestBodyInGetMethod,
"not-use-null-for-empty-array": NotUseNullForEmptyArray,
"omit-optional-property": OmitOptionalProperty,
"secure-endpoints-with-oauth-2.0": SecureEndpointsWithOAuth20,
"use-absolute-custom-link-relation-url": UseAbsoluteCustomLinkRelationUrl,
"use-absolute-profile-url": UseAbsoluteProfileUrl,
"use-authorization-grant": UseAuthorizationGrant,
"use-camel-case-for-property-name": UseCamelCaseForPropertyName,
"use-camel-case-for-query-parameter": UseCamelCaseForQueryParameter,
"use-common-date-and-time-format": UseCommonDateAndTimeFormat,
"use-curied-link-relation-types": UseCuriedLinkRelationTypes,
"use-extensible-enum": UseExtensibleEnum,
"use-kebab-case-for-path-parameter": UseKebabCaseForPathParameter,
"use-kebab-case-in-uri": UseKebabCaseInUri,
"use-string-enum": UseStringEnum,
"use-tls": UseTLS,
"operation-4xx-problem-details-rfc9457": Operation4xxProblemDetailsRfc9457,
},
},
configs: {
recommended: {
rules: {
"info-contact": "error", // https://api.otto.de/portal/guidelines/r000078
"operation-2xx-response": "error", // https://api.otto.de/portal/guidelines/r000011
"api-guidelines/operation-4xx-problem-details-rfc9457": "error", // https://api.otto.de/portal/guidelines/r000034
"no-path-trailing-slash": "error", // https://api.otto.de/portal/guidelines/r000020
"api-guidelines/always-return-json-object": "error", // https://api.otto.de/portal/guidelines/r004030
"api-guidelines/define-permissions-with-scope": "error", // https://api.otto.de/portal/guidelines/r000047
"api-guidelines/format-enumeration-upper-snake-case": "error", // https://api.otto.de/portal/guidelines/r004090
"api-guidelines/no-request-body-in-get-method": "error", // https://api.otto.de/portal/guidelines/r000007
"api-guidelines/not-use-null-for-empty-array": "warn", // https://api.otto.de/portal/guidelines/r004060
"api-guidelines/omit-optional-property": "warn", // https://api.otto.de/portal/guidelines/r004021
"api-guidelines/secure-endpoints-with-oauth-2.0": "error", // https://api.otto.de/portal/guidelines/r000051
"api-guidelines/use-absolute-custom-link-relation-url": "error", // https://api.otto.de/portal/guidelines/r100037
"api-guidelines/use-absolute-profile-url": "error", // https://api.otto.de/portal/guidelines/r100066
"api-guidelines/use-authorization-grant": "error", // https://api.otto.de/portal/guidelines/r000052
"api-guidelines/use-camel-case-for-property-name": "warn", // https://api.otto.de/portal/guidelines/r004010
"api-guidelines/use-camel-case-for-query-parameter": "error", // https://api.otto.de/portal/guidelines/r000022
"api-guidelines/use-common-date-and-time-format": "error", // https://api.otto.de/portal/guidelines/r100072
"api-guidelines/use-curied-link-relation-types": "error", // https://api.otto.de/portal/guidelines/r100038
"api-guidelines/use-extensible-enum": "warn", // https://api.otto.de/portal/guidelines/r000035
"api-guidelines/use-kebab-case-for-path-parameter": "off", // TODO guideline rule will follow
"api-guidelines/use-kebab-case-in-uri": "error", // https://api.otto.de/portal/guidelines/r000023
"api-guidelines/use-oas-3": "error", // https://api.otto.de/portal/guidelines/r000003
"api-guidelines/use-string-enum": "error", // https://api.otto.de/portal/guidelines/r004080
"api-guidelines/use-tls": "error", // https://api.otto.de/portal/guidelines/r000046
},
},
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
createConfig,
type Config,
type Plugin,
type RuleConfig,
type RawUniversalConfig,
type Oas3Rule,
type Oas2Rule,
type Async2Rule,
type Async3Rule,
} from "@redocly/openapi-core";

type AnyRule = Oas3Rule | Oas2Rule | Async2Rule | Async3Rule;


export type CustomRulesConfig = Partial<Record<keyof NonNullable<Plugin["rules"]>, Record<string, AnyRule>>>;

export async function createTestConfig(customRulesConfig: CustomRulesConfig): Promise<Config> {
const pluginId = "test-plugin";

const prefixedRules = Object.fromEntries(
Object.values(customRulesConfig).flatMap((ruleSet) =>
Object.keys(ruleSet ?? {}).map((rule) => [`${pluginId}/${rule}`, "error" as RuleConfig]),
),
);

return createConfig({
plugins: [{ id: pluginId, rules: customRulesConfig as NonNullable<Plugin["rules"]> }],
rules: prefixedRules,
} as RawUniversalConfig);
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe("redocly visitor", () => {
it("should enter SchemaProperties only once", async () => {
const spy = vi.fn();

const config = createTestConfig({
const config = await createTestConfig({
oas3: {
// @ts-ignore
"test-rule": () => ({
Expand All @@ -82,7 +82,7 @@ describe("redocly visitor", () => {
it("should enter SchemaProperties multiple times", async () => {
const spy = vi.fn();

const config = createTestConfig({
const config = await createTestConfig({
oas3: {
// @ts-ignore
"test-rule": () => ({
Expand All @@ -102,7 +102,7 @@ describe("redocly visitor", () => {
it("should enter SchemaProperties multiple times", async () => {
const spy = vi.fn();

const config = createTestConfig({
const config = await createTestConfig({
oas3: {
// @ts-ignore
"test-rule": () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { lintFromString } from "@redocly/openapi-core";
import { lintFromString, type Config } from "@redocly/openapi-core";
import { AlwaysReturnJsonObject } from "./always-return-json-object.js";
import { createTestConfig } from "./__tests__/createTestConfig.js";
import { removeClutter } from "./__tests__/removeClutter.js";

const config = createTestConfig({
oas3: {
// @ts-ignore
"test-rule": AlwaysReturnJsonObject,
},
let config: Config;
beforeAll(async () => {
config = await createTestConfig({
oas3: {
// @ts-ignore
"test-rule": AlwaysReturnJsonObject,
},
});
});

it("should not find any error", async () => {
Expand Down Expand Up @@ -128,7 +131,7 @@ paths:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
]
Expand Down Expand Up @@ -211,7 +214,7 @@ components:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
]
Expand Down Expand Up @@ -254,7 +257,7 @@ components:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
]
Expand Down Expand Up @@ -300,7 +303,7 @@ components:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
{
Expand All @@ -311,7 +314,7 @@ components:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
]
Expand Down Expand Up @@ -357,7 +360,7 @@ components:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
{
Expand All @@ -368,7 +371,7 @@ components:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
]
Expand Down Expand Up @@ -443,7 +446,7 @@ components:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
{
Expand All @@ -454,7 +457,7 @@ components:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
{
Expand All @@ -465,7 +468,7 @@ components:
},
],
"message": "Top-level type must be an object. See https://api.otto.de/portal/guidelines/r004030",
"ruleId": "test-rule",
"ruleId": "test-plugin/test-rule",
"suggest": [],
},
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import type { Oas3Rule } from "@redocly/openapi-core/lib/visitors.d.js";
import { isRef, type Oas3Schema } from "@redocly/openapi-core";
import type { Location } from "@redocly/openapi-core/lib/ref-utils.d.js";
import type { ResolveFn } from "@redocly/openapi-core/lib/walk.d.js";
import {
isRef,
type Oas3Rule,
type Oas3Schema,
type Oas3_1Schema,
type Location,
type UserContext,
} from "@redocly/openapi-core";
import { isJsonContentType } from "./utils/isJsonContentType.js";

type ResolveFn = UserContext["resolve"];

type Schema = Oas3Schema | Oas3_1Schema;

const findNonObjectLocations = (
schema: Oas3Schema,
schema: Schema,
location: Location,
resolve: ResolveFn,
): Location[] => {
Expand Down
Loading
Loading