From 6000c2983cd4c82e76fe11e0358035881abd89ea Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Tue, 26 Apr 2022 18:45:05 +0200 Subject: [PATCH] issue-4 --- pom.xml | 18 +-- .../java/com/ethlo/zally/ExtractMojo.java | 106 +++++++++++++----- .../com/ethlo/zally/ExtractionDefinition.java | 54 +++++++++ .../java/com/ethlo/zally/OpenApiParser.java | 11 +- .../java/com/ethlo/zally/OperationFilter.java | 99 ++++++++++++++++ .../java/com/ethlo/zally/ReportingMojo.java | 4 +- .../java/com/ethlo/zally/ZallyRunner.java | 4 +- .../java/com/ethlo/zally/ExtractMojoTest.java | 16 +-- ...ListedPluralizeNamesForArraysRuleTest.java | 2 +- 9 files changed, 265 insertions(+), 49 deletions(-) create mode 100644 src/main/java/com/ethlo/zally/ExtractionDefinition.java create mode 100644 src/main/java/com/ethlo/zally/OperationFilter.java diff --git a/pom.xml b/pom.xml index dd32080..6a43037 100644 --- a/pom.xml +++ b/pom.xml @@ -212,33 +212,33 @@ org.jetbrains.kotlin kotlin-stdlib-jdk8 - 1.5.30-M1 + 1.6.21 org.slf4j slf4j-simple - 1.7.29 + 1.7.36 org.apache.maven maven-plugin-api - 3.6.3 + 3.8.5 org.apache.maven.plugin-tools maven-plugin-annotations - 3.6.0 + 3.6.4 provided org.apache.maven - maven-project - 2.2.1 + maven-core + 3.8.5 io.github.classgraph classgraph - 4.8.115 + 4.8.146 @@ -251,13 +251,13 @@ org.assertj assertj-core - 3.20.2 + 3.22.0 test junit junit - 4.13.1 + 4.13.2 test diff --git a/src/main/java/com/ethlo/zally/ExtractMojo.java b/src/main/java/com/ethlo/zally/ExtractMojo.java index 80bfe5c..71d424a 100644 --- a/src/main/java/com/ethlo/zally/ExtractMojo.java +++ b/src/main/java/com/ethlo/zally/ExtractMojo.java @@ -29,8 +29,10 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.plugins.annotations.LifecyclePhase; @@ -47,6 +49,7 @@ import io.swagger.v3.core.filter.AbstractSpecFilter; import io.swagger.v3.core.filter.SpecFilter; import io.swagger.v3.core.model.ApiDescription; +import io.swagger.v3.core.util.Yaml; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; @@ -57,16 +60,22 @@ public class ExtractMojo extends AbstractMojo @Parameter(required = true, defaultValue = "${project.basedir}/src/main/resources/api.yaml", property = "zally.source") private String source; - @Parameter(property = "targetFile") - private File targetFile; + @Parameter(property = "outputFile") + private File outputFile; - @Parameter(property = "zally.skip", defaultValue = "false") + @Parameter(property = "configFile") + private File configFile; + + @Parameter(property = "skip", defaultValue = "false") private boolean skip; @Parameter(property = "filters") - private List filters; + private List filters; + + @Parameter(property = "description") + private String description; - @Parameter(property = "name", required = true) + @Parameter(property = "name") private String name; @Parameter(property = "title") @@ -75,21 +84,59 @@ public class ExtractMojo extends AbstractMojo @Parameter(defaultValue = "${project}", required = true, readonly = true) private MavenProject project; + @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true) + private MojoExecution mojoExecution; + + private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory() + .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) + .enable(YAMLGenerator.Feature.SPLIT_LINES) + .enable(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS)) + .setSerializationInclusion(JsonInclude.Include.NON_EMPTY); private static final ObjectMapper mapper = new ObjectMapper(); @Override public void execute() throws MojoFailureException { - final Optional loaded = load(getLog(), skip, source); + final Optional loaded = load(getLog(), skip, source, false); + + if (name == null) + { + name = mojoExecution.getExecutionId(); + } - if (targetFile == null) + if (outputFile == null) { - targetFile = Paths.get(project.getBuild().getOutputDirectory()).resolve(name + ".yaml").toFile(); + outputFile = Paths.get(project.getBuild().getOutputDirectory()).resolve(name + ".yaml").toFile(); + } + + if (configFile != null) + { + try + { + final ExtractionDefinition extractionDefinition = yamlMapper.readValue(configFile, ExtractionDefinition.class); + if (this.title == null) + { + this.title = extractionDefinition.getTitle(); + } + + if (this.description == null) + { + this.description = extractionDefinition.getDescription(); + } + + this.filters = extractionDefinition.getFilters(); + } + catch (IOException exc) + { + throw new UncheckedIOException(exc); + } } loaded.ifPresent(openAPI -> { - getLog().info(String.format("Processing filter %s", name)); + getLog().info(String.format("Processing extraction %s", name)); + final AtomicInteger totalEvaluated = new AtomicInteger(0); + final AtomicInteger totalMatched = new AtomicInteger(0); final OpenAPI filtered = new SpecFilter().filter(openAPI, new AbstractSpecFilter() { @Override @@ -100,9 +147,18 @@ public Optional filterOperation(final Operation operation, final ApiD final Map extensionMap = new LinkedHashMap<>(Optional.ofNullable(pathItem.getExtensions()).orElse(Collections.emptyMap())); extensionMap.putAll(operation.getExtensions()); final OperationData operationData = new OperationData(operation, api, extensionMap); - if (filters.stream().anyMatch(filter -> match(filter, operationData))) + if (filters.stream().anyMatch(filter -> + { + final boolean matched = match(filter, operationData); + if (matched) + { + totalMatched.incrementAndGet(); + getLog().info(String.format("Including '%s' in '%s' because '%s'", operation.getOperationId(), name, filter.asString())); + } + totalEvaluated.incrementAndGet(); + return matched; + })) { - getLog().info(String.format("Including operation %s in %s", operation.getOperationId(), name)); return Optional.of(operation); } return Optional.empty(); @@ -116,16 +172,15 @@ public boolean isRemovingUnreferencedDefinitions() }, null, null, null); - // Set title of document Optional.ofNullable(title).ifPresent(t -> filtered.getInfo().setTitle(t)); + Optional.ofNullable(description).ifPresent(t -> filtered.getInfo().setDescription(t)); + getLog().info(String.format("Included %s/%s API operations in extract", totalMatched.get(), totalEvaluated.get())); try { - getLog().info("Writing extracted APIs to " + targetFile); - Files.createDirectories(targetFile.toPath().getParent()); - new ObjectMapper(new YAMLFactory() - .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)). - setSerializationInclusion(JsonInclude.Include.NON_EMPTY).writeValue(targetFile, filtered); + getLog().info("Writing extracted APIs to " + outputFile); + Files.createDirectories(outputFile.toPath().getParent()); + Yaml.mapper().writeValue(outputFile, filtered); } catch (IOException e) { @@ -134,11 +189,10 @@ public boolean isRemovingUnreferencedDefinitions() }); } - public static boolean match(final String filter, final OperationData data) + public static boolean match(final OperationFilter filter, final OperationData data) { - final String[] expression = filter.split("="); - final String path = expression[0].startsWith("/") ? expression[0] : "/" + expression[0]; - final String regexp = expression[1]; + final String path = filter.getPointer().startsWith("/") ? filter.getPointer() : "/" + filter.getPointer(); + final String regexp = filter.getExpression(); try { final JsonNode jsonNode = mapper.readTree(mapper.writeValueAsString(data)); @@ -148,7 +202,7 @@ public static boolean match(final String filter, final OperationData data) final Iterator fieldNames = value.fieldNames(); while (fieldNames.hasNext()) { - if (fieldNames.next().matches(regexp)) + if (filter.isMatch(fieldNames.next())) { return true; } @@ -159,7 +213,7 @@ else if (value.isArray()) final Iterator elements = value.elements(); while (elements.hasNext()) { - if (elements.next().textValue().matches(regexp)) + if (filter.isMatch(elements.next().textValue())) { return true; } @@ -167,7 +221,7 @@ else if (value.isArray()) } else if (value.isTextual()) { - return value.textValue().matches(regexp); + return filter.isMatch(value.textValue()); } return false; @@ -178,7 +232,7 @@ else if (value.isTextual()) } } - public static Optional load(final Log log, final boolean skip, final String source) throws MojoFailureException + public static Optional load(final Log log, final boolean skip, final String source, final boolean inlined) throws MojoFailureException { if (skip) { @@ -194,6 +248,6 @@ public static Optional load(final Log log, final boolean skip, final St } log.info("Reading file '" + source + "'"); - return Optional.of(new OpenApiParser().parse(source)); + return Optional.of(inlined ? new OpenApiParser().parseInlined(source) : new OpenApiParser().parse(source)); } } diff --git a/src/main/java/com/ethlo/zally/ExtractionDefinition.java b/src/main/java/com/ethlo/zally/ExtractionDefinition.java new file mode 100644 index 0000000..97214c8 --- /dev/null +++ b/src/main/java/com/ethlo/zally/ExtractionDefinition.java @@ -0,0 +1,54 @@ +package com.ethlo.zally; + +/*- + * #%L + * zally-maven-plugin + * %% + * Copyright (C) 2021 - 2022 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ExtractionDefinition +{ + private final String title; + private final String description; + private final List filters; + + public ExtractionDefinition(@JsonProperty("title") final String title, @JsonProperty("description") final String description, @JsonProperty("filters") final List filters) + { + this.title = title; + this.description = description; + this.filters = filters; + } + + public String getTitle() + { + return title; + } + + public List getFilters() + { + return filters; + } + + public String getDescription() + { + return description; + } +} diff --git a/src/main/java/com/ethlo/zally/OpenApiParser.java b/src/main/java/com/ethlo/zally/OpenApiParser.java index 3257f0a..31f088d 100644 --- a/src/main/java/com/ethlo/zally/OpenApiParser.java +++ b/src/main/java/com/ethlo/zally/OpenApiParser.java @@ -26,7 +26,7 @@ public class OpenApiParser { - public OpenAPI parse(String url) + public OpenAPI parseInlined(String url) { final ParseOptions parseOptions = new ParseOptions(); parseOptions.setResolve(true); @@ -34,4 +34,13 @@ public OpenAPI parse(String url) new ResolverFully(true).resolveFully(parseResult); return parseResult; } + + public OpenAPI parse(final String url) + { + final ParseOptions parseOptions = new ParseOptions(); + parseOptions.setResolve(true); + final OpenAPI parseResult = new OpenAPIV3Parser().read(url); + //new ResolverFully(true).resolveFully(parseResult); + return parseResult; + } } diff --git a/src/main/java/com/ethlo/zally/OperationFilter.java b/src/main/java/com/ethlo/zally/OperationFilter.java new file mode 100644 index 0000000..11eac10 --- /dev/null +++ b/src/main/java/com/ethlo/zally/OperationFilter.java @@ -0,0 +1,99 @@ +package com.ethlo.zally; + +/*- + * #%L + * zally-maven-plugin + * %% + * Copyright (C) 2021 - 2022 Morten Haraldsen (ethlo) + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.util.Objects; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class OperationFilter +{ + private Pattern matcher; + private String pointer; + private String expression; + + public OperationFilter() + { + + } + + public OperationFilter(@JsonProperty("pointer") final String pointer, @JsonProperty("expression") final String expression) + { + this.pointer = pointer; + setExpression(expression); + } + + public String getPointer() + { + return pointer; + } + + public String getExpression() + { + return expression; + } + + public void setPointer(final String pointer) + { + this.pointer = pointer; + } + + public void setExpression(final String expression) + { + this.expression = expression; + this.matcher = Pattern.compile(expression); + } + + @Override + public boolean equals(final Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final OperationFilter that = (OperationFilter) o; + return Objects.equals(pointer, that.pointer) && Objects.equals(expression, that.expression); + } + + @Override + public int hashCode() + { + return Objects.hash(pointer, expression); + } + + @Override + public String toString() + { + return "OperationFilter{" + + "pointer='" + pointer + '\'' + + ", expression='" + expression + '\'' + + '}'; + } + + public String asString() + { + return pointer + "=" + expression; + } + + public boolean isMatch(String value) + { + return matcher.matcher(value).matches(); + } +} diff --git a/src/main/java/com/ethlo/zally/ReportingMojo.java b/src/main/java/com/ethlo/zally/ReportingMojo.java index 40e51e1..80669d1 100644 --- a/src/main/java/com/ethlo/zally/ReportingMojo.java +++ b/src/main/java/com/ethlo/zally/ReportingMojo.java @@ -47,14 +47,14 @@ public class ReportingMojo extends AbstractMojo @Override public void execute() throws MojoFailureException { - final Optional loaded = load(getLog(), skip, source); + final Optional loaded = load(getLog(), skip, source, true); loaded.ifPresent(openAPI -> { getLog().info("Analyzing file '" + source + "'"); getLog().info(""); getLog().info("API path hierarchy:"); - final String hierarchy = new ApiReporter(new OpenApiParser().parse(source)).render(); + final String hierarchy = new ApiReporter(new OpenApiParser().parseInlined(source)).render(); Arrays.stream(hierarchy.split("\n")).forEach(line -> getLog().info(line)); getLog().info(""); }); diff --git a/src/main/java/com/ethlo/zally/ZallyRunner.java b/src/main/java/com/ethlo/zally/ZallyRunner.java index 280eb27..69b57b3 100644 --- a/src/main/java/com/ethlo/zally/ZallyRunner.java +++ b/src/main/java/com/ethlo/zally/ZallyRunner.java @@ -22,6 +22,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; @@ -43,7 +44,6 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; -import edu.emory.mathcs.backport.java.util.Collections; import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; import io.github.classgraph.ClassInfoList; @@ -70,7 +70,7 @@ public ZallyRunner(final Config ruleConfigs, final Log logger) public Map> validate(String url, final Set skipped) throws IOException { - final OpenAPI openApi = new OpenApiParser().parse(url); + final OpenAPI openApi = new OpenApiParser().parseInlined(url); final Context context = new DefaultContext("", openApi, null); final Map> returnValue = new LinkedHashMap<>(); diff --git a/src/test/java/com/ethlo/zally/ExtractMojoTest.java b/src/test/java/com/ethlo/zally/ExtractMojoTest.java index bc068f7..9d13b7e 100644 --- a/src/test/java/com/ethlo/zally/ExtractMojoTest.java +++ b/src/test/java/com/ethlo/zally/ExtractMojoTest.java @@ -9,9 +9,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,31 +36,31 @@ public class ExtractMojoTest @Test public void matchOperation() { - assertThat(ExtractMojo.match("operationId=my-operation", operationData)).isTrue(); + assertThat(ExtractMojo.match(new OperationFilter("operationId", "my-operation"), operationData)).isTrue(); } @Test public void testOperationRegex() { - assertThat(ExtractMojo.match("operationId=my-ope.*", operationData)).isTrue(); + assertThat(ExtractMojo.match(new OperationFilter("operationId", "my-ope.*"), operationData)).isTrue(); } @Test public void testOperationRegexNegative() { - assertThat(ExtractMojo.match("operationId=!^my-ope.*", operationData)).isFalse(); + assertThat(ExtractMojo.match(new OperationFilter("operationId", "!^my-ope.*"), operationData)).isFalse(); } @Test public void testPathRegex() { - assertThat(ExtractMojo.match("path=/foo/.*", operationData)).isTrue(); - assertThat(ExtractMojo.match("path=/foo/baz/.*", operationData)).isFalse(); + assertThat(ExtractMojo.match(new OperationFilter("path", "/foo/.*"), operationData)).isTrue(); + assertThat(ExtractMojo.match(new OperationFilter("path", "/foo/baz/.*"), operationData)).isFalse(); } @Test public void testMatchExtensionKey() { - assertThat(ExtractMojo.match("extensions=x-foo", operationData)).isTrue(); + assertThat(ExtractMojo.match(new OperationFilter("extensions", "x-foo"), operationData)).isTrue(); } } diff --git a/src/test/java/com/ethlo/zally/rules/WhiteListedPluralizeNamesForArraysRuleTest.java b/src/test/java/com/ethlo/zally/rules/WhiteListedPluralizeNamesForArraysRuleTest.java index 798ca00..746f55f 100644 --- a/src/test/java/com/ethlo/zally/rules/WhiteListedPluralizeNamesForArraysRuleTest.java +++ b/src/test/java/com/ethlo/zally/rules/WhiteListedPluralizeNamesForArraysRuleTest.java @@ -39,7 +39,7 @@ public class WhiteListedPluralizeNamesForArraysRuleTest public void checkArrayPropertyNamesArePlural() { final String url = "modified_petstore/petstore.yaml"; - final OpenAPI openApi = new OpenApiParser().parse(url); + final OpenAPI openApi = new OpenApiParser().parseInlined(url); final Context context = new DefaultContext("", openApi, null); final List violations = new WhiteListedPluralizeNamesForArraysRule(ConfigFactory.empty()).checkArrayPropertyNamesArePlural(context); assertThat(violations).isEmpty();