diff --git a/core/src/main/java/io/github/belgif/rest/guide/validator/core/model/PathDefinition.java b/core/src/main/java/io/github/belgif/rest/guide/validator/core/model/PathDefinition.java index 9acafb3..2386f7b 100644 --- a/core/src/main/java/io/github/belgif/rest/guide/validator/core/model/PathDefinition.java +++ b/core/src/main/java/io/github/belgif/rest/guide/validator/core/model/PathDefinition.java @@ -14,11 +14,15 @@ public class PathDefinition extends OpenApiDefinition { public PathDefinition(PathItem model, PathsDefinition parent, String identifier) { super(model, parent, identifier, JsonPointer.relative(identifier)); - this.isDirectPath = parent.isInMainFile(); + this.isDirectPath = parent.isInMainFile() && !hasReusableDefinitionsOnly(); } @Override public PathItem getModel() { return super.getModel(); } + + private boolean hasReusableDefinitionsOnly() { + return this.getResult().getSrc().get(this.getOpenApiFile().getAbsolutePath()).hasReusableDefinitionsOnly(); + } } diff --git a/core/src/main/java/io/github/belgif/rest/guide/validator/core/parser/Parser.java b/core/src/main/java/io/github/belgif/rest/guide/validator/core/parser/Parser.java index 471d5ef..66a70b8 100644 --- a/core/src/main/java/io/github/belgif/rest/guide/validator/core/parser/Parser.java +++ b/core/src/main/java/io/github/belgif/rest/guide/validator/core/parser/Parser.java @@ -564,7 +564,8 @@ private void constructExamples(OpenApiDefinition definition, ParserResult res private static List getLines(File file) throws IOException { var lines = Files.readAllLines(file.toPath()); - if (lines.size() < 1) throw new RuntimeException("[Internal error] File: " + file.getName() + " appears to be empty!"); + if (lines.isEmpty()) + throw new RuntimeException("[Internal error] File: " + file.getName() + " appears to be empty!"); // lines > 1 then is a yaml or a pretty json file if (lines.size() > 1) return lines; diff --git a/core/src/main/java/io/github/belgif/rest/guide/validator/core/parser/SourceDefinition.java b/core/src/main/java/io/github/belgif/rest/guide/validator/core/parser/SourceDefinition.java index fa64f60..f3619ea 100644 --- a/core/src/main/java/io/github/belgif/rest/guide/validator/core/parser/SourceDefinition.java +++ b/core/src/main/java/io/github/belgif/rest/guide/validator/core/parser/SourceDefinition.java @@ -9,12 +9,14 @@ @Getter public class SourceDefinition { + private static final String REF_ONLY_KEY = "x-reusable-definitions-only"; private final String fileName; private final File file; private final String src; private final boolean isYaml; private final OpenAPI openApi; + private final boolean hasReusableDefinitionsOnly; public SourceDefinition(File file, OpenAPI openApi) throws IOException { this.file = file; @@ -22,9 +24,23 @@ public SourceDefinition(File file, OpenAPI openApi) throws IOException { this.src = Files.readString(file.toPath()); this.isYaml = checkIsYaml(this.fileName); this.openApi = openApi; + this.hasReusableDefinitionsOnly = findHasReusableDefinitionsOnly(openApi); } public static boolean checkIsYaml(String fileName) { return fileName.endsWith("yaml") || fileName.endsWith("yml"); } + + public boolean hasReusableDefinitionsOnly() { + return hasReusableDefinitionsOnly; + } + + private static boolean findHasReusableDefinitionsOnly(OpenAPI openApi) { + if (openApi.getExtensions() != null && + openApi.getExtensions().containsKey(REF_ONLY_KEY) && + openApi.getExtensions().get(REF_ONLY_KEY) instanceof Boolean) { + return ((Boolean) openApi.getExtensions().get(REF_ONLY_KEY)); + } + return false; + } } diff --git a/core/src/test/java/io/github/belgif/rest/guide/validator/core/ParserTest.java b/core/src/test/java/io/github/belgif/rest/guide/validator/core/ParserTest.java index 7fe695d..003c1f8 100644 --- a/core/src/test/java/io/github/belgif/rest/guide/validator/core/ParserTest.java +++ b/core/src/test/java/io/github/belgif/rest/guide/validator/core/ParserTest.java @@ -288,4 +288,33 @@ void testNonValidExampleIsParsed() { Parser.ParserResult result = new Parser(file).parse(oas); assertEquals(2, result.getExamples().size()); } + + @Test + void testRefOnly() { + var oas = new ViolationReport(); + var file = new File(Objects.requireNonNull(getClass().getResource("../rules/referenceOnly/refOnly.yaml")).getFile()); + + Parser.ParserResult result = new Parser(file).parse(oas); + assertFalse(result.getPathDefinitions().stream().findFirst().get().isDirectPath()); + } + + @Test + void testRefOnlyFalse() { + var oas = new ViolationReport(); + var file = new File(Objects.requireNonNull(getClass().getResource("../rules/referenceOnly/refOnlyFalse.yaml")).getFile()); + + Parser.ParserResult result = new Parser(file).parse(oas); + assertEquals(1, result.getServers().size()); + assertTrue(result.getPathDefinitions().stream().findFirst().get().isDirectPath()); + } + + @Test + void testRefOnlyRandomString() { + var oas = new ViolationReport(); + var file = new File(Objects.requireNonNull(getClass().getResource("../rules/referenceOnly/refOnlyRandomString.yaml")).getFile()); + + Parser.ParserResult result = new Parser(file).parse(oas); + assertEquals(1, result.getServers().size()); + assertTrue(result.getPathDefinitions().stream().findFirst().get().isDirectPath()); + } } diff --git a/core/src/test/resources/io/github/belgif/rest/guide/validator/rules/referenceOnly/refOnly.yaml b/core/src/test/resources/io/github/belgif/rest/guide/validator/rules/referenceOnly/refOnly.yaml new file mode 100644 index 0000000..fb3add7 --- /dev/null +++ b/core/src/test/resources/io/github/belgif/rest/guide/validator/rules/referenceOnly/refOnly.yaml @@ -0,0 +1,18 @@ +openapi: 3.0.0 +x-reusable-definitions-only: true +info: + title: test + description: test + version: "1.0" +servers: + - url: https://myserver.com +paths: + /myPath: + get: + responses: + default: + content: + application/json: + schema: + type: object + description: myDescription \ No newline at end of file diff --git a/core/src/test/resources/io/github/belgif/rest/guide/validator/rules/referenceOnly/refOnlyFalse.yaml b/core/src/test/resources/io/github/belgif/rest/guide/validator/rules/referenceOnly/refOnlyFalse.yaml new file mode 100644 index 0000000..6467cd6 --- /dev/null +++ b/core/src/test/resources/io/github/belgif/rest/guide/validator/rules/referenceOnly/refOnlyFalse.yaml @@ -0,0 +1,18 @@ +openapi: 3.0.0 +x-reusable-definitions-only: false +info: + title: test + description: test + version: "1.0" +servers: + - url: https://myserver.com +paths: + /myPath: + get: + responses: + default: + content: + application/json: + schema: + type: object + description: myDescription \ No newline at end of file diff --git a/core/src/test/resources/io/github/belgif/rest/guide/validator/rules/referenceOnly/refOnlyRandomString.yaml b/core/src/test/resources/io/github/belgif/rest/guide/validator/rules/referenceOnly/refOnlyRandomString.yaml new file mode 100644 index 0000000..35fe471 --- /dev/null +++ b/core/src/test/resources/io/github/belgif/rest/guide/validator/rules/referenceOnly/refOnlyRandomString.yaml @@ -0,0 +1,18 @@ +openapi: 3.0.0 +x-reusable-definitions-only: "random string" +info: + title: test + description: test + version: "1.0" +servers: + - url: https://myserver.com +paths: + /myPath: + get: + responses: + default: + content: + application/json: + schema: + type: object + description: myDescription \ No newline at end of file diff --git a/integrationtest/src/it/reusableDefinitionsOnly/common.yaml b/integrationtest/src/it/reusableDefinitionsOnly/common.yaml new file mode 100644 index 0000000..9bb87b9 --- /dev/null +++ b/integrationtest/src/it/reusableDefinitionsOnly/common.yaml @@ -0,0 +1,170 @@ +openapi: 3.0.0 +x-reusable-definitions-only: true +info: + title: common technical data types + version: ${project.version} + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +paths: + /health: # Operation may be secured, but access should be allowed to all clients. In order to add a security scheme, copy this definition instead of referencing it. + get: + tags: + - Monitoring + summary: Check health of the service + operationId: checkHealth + externalDocs: + url: "https://www.belgif.be/specification/rest/api-guide/#health" + security: [] + responses: + "200": + "$ref": "#/components/responses/HealthUpOrDegradedResponse" + "503": + "$ref": "#/components/responses/HealthDownResponse" + default: + $ref: "problem.yaml#/components/responses/ProblemResponse" +components: + parameters: + LangQueryParameter: + in: query + name: lang + schema: + $ref: "#/components/schemas/BelgianLanguage" + PageQueryParameter: + description: Number of requested page in a paged resource collection. Page numbers are 1-based. + in: query + name: page + schema: + type: integer + minimum: 1 + SelectQueryParameter: + description: Only return selected parts of resource's representation. Parameter value is in BNF-notation. + in: query + name: select + schema: + type: string + responses: + HealthUpOrDegradedResponse: + description: The service is UP or DEGRADED + content: + application/json: + schema: + $ref: "#/components/schemas/HealthStatus" + examples: + responseUp: + description: API is available + value: + { + "status": "UP" + } + responseDegraded: + description: API is available, but with reduced functionality + value: + { + "status": "DEGRADED" + } + HealthDownResponse: + description: The service is DOWN + content: + application/json: + schema: + $ref: "#/components/schemas/HealthStatus" + examples: + responseDown: + value: + { + "status": "DOWN" + } + schemas: + BelgianLanguage: + description: One of the official Belgian languages represented by an ISO-639-1 code + type: string + enum: + - fr + - nl + - de + Language: + description: A language represented by an ISO-639-1 code + type: string + pattern: "^[a-z]{2}$" + example: "en" + LongRunningTaskStatus: + description: status of a long running task + type: object + properties: + state: + type: string + enum: + - processing + - failed + - done + pollAfter: + description: hint when to check the status again + type: string + format: date-time + completed: + description: when the task has completed (in case state is done or failed) + type: string + format: date-time + problem: + $ref: "problem.yaml#/components/schemas/Problem" + required: [state] + example: + { + "state": "failed", + "completed": "2018-09-13T02:10:00.000Z", + "problem": { + "instance": "urn:uuid:d9e35127-e9b1-4201-a211-2b52e52508df", + "title": "Bad Request", + "status": 400, + "type": "urn:problem-type:example:invalidImageFormat", + "detail": "Invalid image format" + } + } + HttpLink: + description: A base type of objects representing links to resources. + type: object + properties: + href: + description: Any absolute URI that is using http or https protocol + type: string + format: uri + readOnly: true + LocalizedString: + description: A description specified in multiple languages + type: object + properties: + #nl, fr, de are predefined, but additional languages can be added + nl: + type: string + fr: + type: string + de: + type: string + MergePatch: + deprecated: true # Define a specific schema for JSON Merge patch request body instead of using this marker type (see [doc-patch] rule) + description: JSON Merge Patch (RFC 7386) request body + type: object + additionalProperties: true + SelfLink: + description: A base type representing a link to the resource's own location within its representation + type: object + properties: + self: + description: Any absolute URI that is using http or https protocol + type: string + format: uri + readOnly: true + Uuid: + description: Universally Unique Identifier, as standardized in RFC 4122 and ISO/IEC 9834-8 + type: string + pattern: '^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$' + HealthStatus: + description: Response message for the API health + type: object + properties: + status: + description: "Level indicating the health status of the service: UP (functioning as expected), DOWN (suffering unexpected failures), DEGRADED (partly unavailable but service can be continued with reduced functionality), or a custom status value" + type: string + required: + - status \ No newline at end of file diff --git a/integrationtest/src/it/reusableDefinitionsOnly/invoker.properties b/integrationtest/src/it/reusableDefinitionsOnly/invoker.properties new file mode 100644 index 0000000..ab0e38d --- /dev/null +++ b/integrationtest/src/it/reusableDefinitionsOnly/invoker.properties @@ -0,0 +1,2 @@ +invoker.goals = prepare-package +invoker.buildResult = success diff --git a/integrationtest/src/it/reusableDefinitionsOnly/pom.xml b/integrationtest/src/it/reusableDefinitionsOnly/pom.xml new file mode 100644 index 0000000..bf247a7 --- /dev/null +++ b/integrationtest/src/it/reusableDefinitionsOnly/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + io.github.belgif.rest.guide.validator + plugin-test + 1.0.0-SNAPSHOT + + + + latest + + + + + + io.github.belgif.rest.guide.validator + belgif-rest-guide-validator-maven-plugin + ${pluginVersion} + + + + validate + + + + + + common.yaml + + + + + + + diff --git a/integrationtest/src/it/reusableDefinitionsOnly/problem.yaml b/integrationtest/src/it/reusableDefinitionsOnly/problem.yaml new file mode 100644 index 0000000..ad6a785 --- /dev/null +++ b/integrationtest/src/it/reusableDefinitionsOnly/problem.yaml @@ -0,0 +1,150 @@ +openapi: 3.0.0 +info: + title: problem types + version: ${project.version} + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +servers: [] +paths: {} +components: + responses: + ProblemResponse: + content: + application/problem+json: + schema: + $ref: "#/components/schemas/Problem" + description: a problem + InputValidationProblemResponse: + content: + application/problem+json: + schema: + $ref: "#/components/schemas/InputValidationProblem" + description: A problem caused by invalid input + schemas: + Problem: + description: | + A Problem Details object (RFC 9457). + + Additional properties specific to the problem type may be present. + type: object + properties: + type: + type: string + format: uri + description: An absolute URI that identifies the problem type + default: about:blank # kept for backwards-compatibility, type will be mandatory in problem-v2 + href: + type: string + format: uri + description: An absolute URI that, when dereferenced, provides human-readable documentation for the problem type (e.g. using HTML). + title: + type: string + description: A short summary of the problem type. Written in English and readable for engineers (usually not suited for non technical stakeholders and not localized). + example: Service Unavailable + status: + type: integer + format: int32 + description: The HTTP status code generated by the origin server for this occurrence of the problem. + minimum: 400 + maximum: 600 + exclusiveMaximum: true + example: 503 + detail: + type: string + description: A human-readable explanation specific to this occurrence of the problem + instance: + type: string + format: uri + description: An absolute URI that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. + example: + type: urn:problem-type:exampleOrganization:exampleProblem # "exampleOrganization" should be a short identifier for the organization that defines the problem type. "belgif" is used for problem types standardized in the Belgif REST guide + href: "https://www.belgif.be/specification/rest/api-guide/#standardized-problem-types" # optional, should refer to documentation of the problem type, either of a belgif standardized or a custom problem type + title: Description of the type of problem that occurred + status: 400 # HTTP response status, appropriate for the problem type + detail: Description of specific occurrence of the problem + instance: urn:uuid:123e4567-e89b-12d3-a456-426614174000 + InvalidParamProblem: + deprecated: true # deprecated by InputValidationProblem + description: Problem details for invalid input parameter(s) + type: object + allOf: + - $ref: "#/components/schemas/Problem" + properties: + invalidParams: + type: array + description: An array of parameter OpenAPI violations + items: + $ref: "#/components/schemas/InvalidParam" + InvalidParam: + deprecated: true # deprecated by InputValidationIssue (within an InputValidationProblem) + type: object + properties: + in: + description: The location of the invalid parameter (cfr Swagger parameters) + type: string + enum: + - body + - path + - query + - header + name: + description: The name of the invalid parameter + type: string + value: + description: The value of the erroneous parameter + # no type specified, allowing any type. This is valid OpenAPI 3.0 even though some editors may indicate an error (issue #25) + InputValidationProblem: + type: object + allOf: + - $ref: "#/components/schemas/Problem" + properties: + issues: + type: array + items: + $ref: "#/components/schemas/InputValidationIssue" + example: + type: urn:problem-type:belgif:badRequest + href: https://www.belgif.be/specification/rest/api-guide/problems/badRequest.html + title: Bad Request + status: 400 + detail: "The input message is incorrect" + instance: urn:uuid:123456-1234-1235-4567489798 + issues: + - type: urn:problem-type:belgif:input-validation:schemaViolation # type is mandatory, can be a standard belgif issue type or a custom one + # href: (TODO: provide dereferenceable link with documentation for standard belgif input validation problems) + detail: exampleNumericProperty with value xyz should be numeric # detail is optional + in: path + name: exampleNumericProperty + value: abc + - type: urn:problem-type:belgif:input-validation:schemaViolation + title: "Input isn't valid with respect to schema" + detail: "examplePropertyWithPattern a2345678901 doesn't match pattern '^\\d{11}$'" + in: body + name: items[0].examplePropertyWithPattern # location within the body + value: "a2345678901" + InputValidationIssue: + type: object + description: | + An issue detected during input validation. + + `status` is usually not present. + `href`, if present, refers to documentation of the issue type. + Additional properties specific to the issue type may be present. + allOf: + - $ref: "#/components/schemas/Problem" + properties: + in: + type: string + description: The location of the invalid input + enum: + - body + - header + - path + - query + name: + type: string + description: The name of the invalid input + value: + description: The value of the erroneous input + # no type specified, allowing any type. This is valid in OpenAPI even though some editors may indicate an error \ No newline at end of file diff --git a/integrationtest/src/it/reusableDefinitionsOnly/verify.groovy b/integrationtest/src/it/reusableDefinitionsOnly/verify.groovy new file mode 100644 index 0000000..a78fe46 --- /dev/null +++ b/integrationtest/src/it/reusableDefinitionsOnly/verify.groovy @@ -0,0 +1,4 @@ +String log = new File(basedir, "build.log").text + +assert log.contains("[INFO] Start creation of KieBase: defaultKieBase") +assert ! log.contains("[ERROR]") diff --git a/readme.md b/readme.md index 8638914..70f17a0 100644 --- a/readme.md +++ b/readme.md @@ -111,6 +111,11 @@ Example: ``` +## Reusable definitions only +The `x-reusable-definitions-only` tag can be added on the top-level in an OpenAPI document to modify validation behavior. +This tag indicates that the OpenAPI document only contains definitions that can be referenced from other OpenAPI documents, and doesn't specify a REST API. This affects some validation rules. +If undefined, the OpenAPI is considered as a definitions-only document when `paths` is empty. + ## Maven Plugin ### Prerequisites diff --git a/rules/src/main/resources/io/github/belgif/rest/guide/validator/rules/oas/Rule-ServerUrlFormat.drl b/rules/src/main/resources/io/github/belgif/rest/guide/validator/rules/oas/Rule-ServerUrlFormat.drl index 7962ae6..fca0cdd 100644 --- a/rules/src/main/resources/io/github/belgif/rest/guide/validator/rules/oas/Rule-ServerUrlFormat.drl +++ b/rules/src/main/resources/io/github/belgif/rest/guide/validator/rules/oas/Rule-ServerUrlFormat.drl @@ -19,8 +19,8 @@ function void violationServerUrlFormat(ViolationReport oas, ServerDefinition ser rule "Server Url or basePath Format" when - eval( !parserResult.getPathDefinitions().isEmpty() ) $server: ServerDefinition($url: /model/url) + eval( !parserResult.getSrc().get($server.getOpenApiFile().getAbsolutePath()).hasReusableDefinitionsOnly() && !parserResult.getPathDefinitions().isEmpty() ) not String( this matches(urlPattern()) ) from $url then violationServerUrlFormat(oas, $server);