From 9a1d4a05129aa1fd0d32790b605f27e1330979c3 Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Mon, 25 Apr 2022 23:14:44 +0200 Subject: [PATCH] issue-4 --- .../java/com/ethlo/zally/ApiReporter.java | 11 - .../java/com/ethlo/zally/ExtractMojo.java | 199 ++++++++++++++++++ .../java/com/ethlo/zally/OperationData.java | 77 +++++++ .../java/com/ethlo/zally/ReportingMojo.java | 34 ++- .../java/com/ethlo/zally/ExtractMojoTest.java | 66 ++++++ ...ListedPluralizeNamesForArraysRuleTest.java | 22 +- 6 files changed, 377 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/ethlo/zally/ExtractMojo.java create mode 100644 src/main/java/com/ethlo/zally/OperationData.java create mode 100644 src/test/java/com/ethlo/zally/ExtractMojoTest.java diff --git a/src/main/java/com/ethlo/zally/ApiReporter.java b/src/main/java/com/ethlo/zally/ApiReporter.java index 618450a..046e2e9 100644 --- a/src/main/java/com/ethlo/zally/ApiReporter.java +++ b/src/main/java/com/ethlo/zally/ApiReporter.java @@ -24,13 +24,11 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; -import java.util.AbstractMap; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; @@ -64,15 +62,6 @@ private Map getOperations(PathItem value) return operations; } - private Optional> getOperationByMethod(Method method, Operation operation) - { - if (operation != null) - { - return Optional.of(new AbstractMap.SimpleEntry<>(method, operation)); - } - return Optional.empty(); - } - public String render() { final Paths paths = this.openAPI.getPaths(); diff --git a/src/main/java/com/ethlo/zally/ExtractMojo.java b/src/main/java/com/ethlo/zally/ExtractMojo.java new file mode 100644 index 0000000..80bfe5c --- /dev/null +++ b/src/main/java/com/ethlo/zally/ExtractMojo.java @@ -0,0 +1,199 @@ +package com.ethlo.zally;/*- + * #%L + * zally-maven-plugin + * %% + * Copyright (C) 2021 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.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +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.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; + +@Mojo(threadSafe = true, name = "extract", defaultPhase = LifecyclePhase.GENERATE_SOURCES) +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 = "zally.skip", defaultValue = "false") + private boolean skip; + + @Parameter(property = "filters") + private List filters; + + @Parameter(property = "name", required = true) + private String name; + + @Parameter(property = "title") + private String title; + + @Parameter(defaultValue = "${project}", required = true, readonly = true) + private MavenProject project; + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Override + public void execute() throws MojoFailureException + { + final Optional loaded = load(getLog(), skip, source); + + if (targetFile == null) + { + targetFile = Paths.get(project.getBuild().getOutputDirectory()).resolve(name + ".yaml").toFile(); + } + + loaded.ifPresent(openAPI -> + { + getLog().info(String.format("Processing filter %s", name)); + final OpenAPI filtered = new SpecFilter().filter(openAPI, new AbstractSpecFilter() + { + @Override + public Optional filterOperation(final Operation operation, final ApiDescription api, final Map> params, final Map cookies, final Map> headers) + { + final String path = api.getPath(); + final PathItem pathItem = openAPI.getPaths().get(path); + 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))) + { + getLog().info(String.format("Including operation %s in %s", operation.getOperationId(), name)); + return Optional.of(operation); + } + return Optional.empty(); + } + + @Override + public boolean isRemovingUnreferencedDefinitions() + { + return true; + } + }, null, null, null); + + + // Set title of document + Optional.ofNullable(title).ifPresent(t -> filtered.getInfo().setTitle(t)); + + 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); + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } + }); + } + + public static boolean match(final String filter, final OperationData data) + { + final String[] expression = filter.split("="); + final String path = expression[0].startsWith("/") ? expression[0] : "/" + expression[0]; + final String regexp = expression[1]; + try + { + final JsonNode jsonNode = mapper.readTree(mapper.writeValueAsString(data)); + final JsonNode value = jsonNode.at(JsonPointer.compile(path)); + if (value.isObject()) + { + final Iterator fieldNames = value.fieldNames(); + while (fieldNames.hasNext()) + { + if (fieldNames.next().matches(regexp)) + { + return true; + } + } + } + else if (value.isArray()) + { + final Iterator elements = value.elements(); + while (elements.hasNext()) + { + if (elements.next().textValue().matches(regexp)) + { + return true; + } + } + } + else if (value.isTextual()) + { + return value.textValue().matches(regexp); + } + + return false; + } + catch (IOException exc) + { + throw new UncheckedIOException(exc); + } + } + + public static Optional load(final Log log, final boolean skip, final String source) throws MojoFailureException + { + if (skip) + { + log.info("Skipping execution as requested"); + return Optional.empty(); + } + + final boolean existsOnClassPath = ExtractMojo.class.getClassLoader().getResourceAsStream(source) != null; + final boolean existsOnFilesystem = Files.exists(Paths.get(source)); + if (!existsOnClassPath && !existsOnFilesystem) + { + throw new MojoFailureException("The specified source file could not be found: " + source); + } + + log.info("Reading file '" + source + "'"); + return Optional.of(new OpenApiParser().parse(source)); + } +} diff --git a/src/main/java/com/ethlo/zally/OperationData.java b/src/main/java/com/ethlo/zally/OperationData.java new file mode 100644 index 0000000..6ff866d --- /dev/null +++ b/src/main/java/com/ethlo/zally/OperationData.java @@ -0,0 +1,77 @@ +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 io.swagger.v3.core.model.ApiDescription; +import io.swagger.v3.oas.models.Operation; + +import java.util.List; +import java.util.Map; + +public class OperationData +{ + private final Map extensions; + private final String method; + private final String path; + private final Boolean deprecated; + private final String operationId; + private final List tags; + + public OperationData(final Operation operation, final ApiDescription api, final Map extensions) + { + this.method = api.getMethod(); + this.path = api.getPath(); + this.deprecated = operation.getDeprecated(); + this.operationId = operation.getOperationId(); + this.tags = operation.getTags(); + this.extensions = extensions; + } + + public Map getExtensions() + { + return extensions; + } + + public String getMethod() + { + return method; + } + + public String getPath() + { + return path; + } + + public Boolean getDeprecated() + { + return deprecated; + } + + public String getOperationId() + { + return operationId; + } + + public List getTags() + { + return tags; + } +} diff --git a/src/main/java/com/ethlo/zally/ReportingMojo.java b/src/main/java/com/ethlo/zally/ReportingMojo.java index a51a4c7..40e51e1 100644 --- a/src/main/java/com/ethlo/zally/ReportingMojo.java +++ b/src/main/java/com/ethlo/zally/ReportingMojo.java @@ -18,9 +18,10 @@ * #L% */ -import java.nio.file.Files; -import java.nio.file.Paths; +import static com.ethlo.zally.ExtractMojo.load; + import java.util.Arrays; +import java.util.Optional; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoFailureException; @@ -29,6 +30,8 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; +import io.swagger.v3.oas.models.OpenAPI; + @Mojo(threadSafe = true, name = "report", defaultPhase = LifecyclePhase.GENERATE_SOURCES) public class ReportingMojo extends AbstractMojo { @@ -44,25 +47,16 @@ public class ReportingMojo extends AbstractMojo @Override public void execute() throws MojoFailureException { - if (skip) - { - getLog().info("Skipping execution as requested"); - return; - } + final Optional loaded = load(getLog(), skip, source); - final boolean existsOnClassPath = getClass().getClassLoader().getResourceAsStream(source) != null; - final boolean existsOnFilesystem = Files.exists(Paths.get(source)); - if (!existsOnClassPath && !existsOnFilesystem) + loaded.ifPresent(openAPI -> { - throw new MojoFailureException("The specified source file could not be found: " + source); - } - - getLog().info("Analyzing file '" + source + "'"); - - getLog().info(""); - getLog().info("API path hierarchy:"); - final String hierarchy = new ApiReporter(new OpenApiParser().parse(source)).render(); - Arrays.stream(hierarchy.split("\n")).forEach(line -> getLog().info(line)); - getLog().info(""); + getLog().info("Analyzing file '" + source + "'"); + getLog().info(""); + getLog().info("API path hierarchy:"); + final String hierarchy = new ApiReporter(new OpenApiParser().parse(source)).render(); + Arrays.stream(hierarchy.split("\n")).forEach(line -> getLog().info(line)); + getLog().info(""); + }); } } diff --git a/src/test/java/com/ethlo/zally/ExtractMojoTest.java b/src/test/java/com/ethlo/zally/ExtractMojoTest.java new file mode 100644 index 0000000..bc068f7 --- /dev/null +++ b/src/test/java/com/ethlo/zally/ExtractMojoTest.java @@ -0,0 +1,66 @@ +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 static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; + +import org.junit.Test; + +import io.swagger.v3.core.model.ApiDescription; +import io.swagger.v3.oas.models.Operation; + +public class ExtractMojoTest +{ + private final OperationData operationData = new OperationData(new Operation().operationId("my-operation"), new ApiDescription("/foo/bar/{baz}", "get"), Collections.singletonMap("x-foo", true)); + + @Test + public void matchOperation() + { + assertThat(ExtractMojo.match("operationId=my-operation", operationData)).isTrue(); + } + + @Test + public void testOperationRegex() + { + assertThat(ExtractMojo.match("operationId=my-ope.*", operationData)).isTrue(); + } + + @Test + public void testOperationRegexNegative() + { + assertThat(ExtractMojo.match("operationId=!^my-ope.*", operationData)).isFalse(); + } + + @Test + public void testPathRegex() + { + assertThat(ExtractMojo.match("path=/foo/.*", operationData)).isTrue(); + assertThat(ExtractMojo.match("path=/foo/baz/.*", operationData)).isFalse(); + } + + @Test + public void testMatchExtensionKey() + { + assertThat(ExtractMojo.match("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 3cfb505..798ca00 100644 --- a/src/test/java/com/ethlo/zally/rules/WhiteListedPluralizeNamesForArraysRuleTest.java +++ b/src/test/java/com/ethlo/zally/rules/WhiteListedPluralizeNamesForArraysRuleTest.java @@ -1,5 +1,25 @@ package com.ethlo.zally.rules; +/*- + * #%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 static org.assertj.core.api.Assertions.assertThat; import java.util.List; @@ -24,4 +44,4 @@ public void checkArrayPropertyNamesArePlural() final List violations = new WhiteListedPluralizeNamesForArraysRule(ConfigFactory.empty()).checkArrayPropertyNamesArePlural(context); assertThat(violations).isEmpty(); } -} \ No newline at end of file +}