From 5e2bde329b894730d00d57daf2d28d4c0583b14e Mon Sep 17 00:00:00 2001 From: Tom Simmons Date: Mon, 29 Aug 2022 22:06:29 -0400 Subject: [PATCH] Issues/19 (#24) Add path parameter support. --- .../github/tomboyo/lily/example/Example.java | 91 +++++-- .../lily/compiler/ast/AstOperation.java | 14 +- .../lily/compiler/ast/AstParameter.java | 3 +- .../compiler/ast/AstParameterLocation.java | 18 ++ .../tomboyo/lily/compiler/cg/AstToJava.java | 34 ++- .../lily/compiler/icg/OasOperationToAst.java | 93 +++++++ .../lily/compiler/icg/OasParameterToAst.java | 27 ++ .../lily/compiler/icg/OasPathsToAst.java | 85 +------ .../tomboyo/lily/compiler/AstSupport.java | 12 + .../tomboyo/lily/compiler/PathsTest.java | 46 ++++ .../compiler/icg/OasOperationToAstTest.java | 230 ++++++++++++++++++ .../compiler/icg/OasParameterToAstTest.java | 72 ++++++ .../lily/compiler/icg/OasPathsToAstTest.java | 181 +++----------- .../tomboyo/lily/http/encoding/Encoding.java | 17 +- 14 files changed, 673 insertions(+), 250 deletions(-) create mode 100644 modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstParameterLocation.java create mode 100644 modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasOperationToAst.java create mode 100644 modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasParameterToAst.java create mode 100644 modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/AstSupport.java create mode 100644 modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasOperationToAstTest.java create mode 100644 modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasParameterToAstTest.java diff --git a/modules/example/src/test/java/io/github/tomboyo/lily/example/Example.java b/modules/example/src/test/java/io/github/tomboyo/lily/example/Example.java index 5c3d93f..683d618 100644 --- a/modules/example/src/test/java/io/github/tomboyo/lily/example/Example.java +++ b/modules/example/src/test/java/io/github/tomboyo/lily/example/Example.java @@ -11,6 +11,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import io.github.tomboyo.lily.http.JacksonBodyHandler; import io.github.tomboyo.lily.http.encoding.Encoding; +import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import org.junit.jupiter.api.Test; @@ -18,11 +19,60 @@ @WireMockTest public class Example { + private static final HttpClient client = HttpClient.newBuilder().build(); + + /* + * In this example, we use as much of the Lily generated code support as possible to automate away the complexity of + * http API integration. If the OAS is malformed or Lily has limited support for a feature, however, the user is + * always able to access underlying data from the generated API and customize their request arbitrarily. + */ + @Test + void happyPath(WireMockRuntimeInfo info) throws Exception { + /* This wiremock stub imitates the petstore yaml's showPetById operation. */ + stubFor( + get("/pets/1234") + .willReturn(ok(""" + {"id": 1234, "name": "Reginald"} + """))); + + /* Use the API to build a well-formed URI to the showPetById operation for resource 1234. At time of writing, the + * user must then use the underlying uriTemplate to retrieve the complete URI and send the request manually with + * java.net.http. + * + * Query, header, and cookie parameters are not yet supported. These must be set manually. + */ + var uri = + Api.newBuilder() + .uri(info.getHttpBaseUrl()) + .build() + .petsOperations() // All operations with the `pets` tag. (We could also use + // .allOperations()) + .showPetById() // The GET /pets/{petId} operation + .petId("1234") // bind "1234" to the {petId} parameter of the OAS operation + .uriTemplate() // Access the underlying URI to finish the request manually + .toURI(); + + /* Finish and send the request manually. Note the use of the generated Pet type. All component and path parameter + * schema are generated. Also note the use of the provided lily-http JacksonBodyHandler + */ + var response = + client.send( + HttpRequest.newBuilder().GET().uri(uri).build(), + JacksonBodyHandler.of(new ObjectMapper(), new TypeReference() {})); + + assertEquals(200, response.statusCode()); + assertEquals(new Pet(1234L, "Reginald", null), response.body().get()); + } + + /* This test case demonstrates ways a user can dip below the generated API to directly manipulate HTTP requests. This + * may be necessary whenever the source OAS document is incomplete (or wrong), or whenever Lily does not support a + * feature. + */ @Test - void example(WireMockRuntimeInfo info) throws Exception { + void manual(WireMockRuntimeInfo info) throws Exception { /* This wiremock stub imitates the petstore yaml's showPetById operation. */ stubFor( - get("/foo/pets/1234") + get("/pets/1234?foo=foo&bar=bar") .willReturn(ok(""" {"id": 1234, "name": "Reginald"} """))); @@ -36,28 +86,29 @@ void example(WireMockRuntimeInfo info) throws Exception { * here, even if already addressable from another tag). */ var operation = - Api.newBuilder().uri(info.getHttpBaseUrl()).build().petsOperations().showPetById(); + Api.newBuilder() + .uri(info.getHttpBaseUrl()) + .build() + .petsOperations() // All operations with the `pets` tag. (We could also use + // .allOperations()) + .showPetById(); // the GET /pets/{petId} operation - /* - * Every operation exposes a uriTemplate which supports parameter interpolation. It may be used to create a URI for - * a specific operation. This capability will always exist as a means to work around OAS and Lily flaws. Currently, - * this is the only option -- Lily will support builders at the operation API level in the future to hide this - * complexity. - * - * Note the use of Encoding, which has support for different parameter encoding strategies like "simple" for path - * parameters and "formExplode" for query parameters. - */ - var uri = operation.uriTemplate().put("petId", Encoding.simple(1234)).toURI(); + var uri = + operation + // Access the underlying uri template to finish the request manually + .uriTemplate() + // Bind "1234" to the petId path parameter. The Encoding class supports several formats, + // like formExplode for + // query parameters. We can override values already set using the operation API. + .put("petId", Encoding.simple(1234)) + .toURI(); + + uri = URI.create(uri.toString() + "?foo=foo&bar=bar"); /* - * The user can construct a native java 11+ java.net.http request using the templated URI and lily-http helpers like - * the JacksonBodyHandler. Eventually, this will happen automatically, but will always be exposed to work around - * flaws in the OAS or Lily. - * - * Note the use of the generated Pet type. All components schemas and parameter schemas are generated! No hand- - * writing models! + * Finish and send the request manually. Note the use of the generated Pet type. All components schemas and + * parameter schemas are generated. Also note the use of the provided lily-http JacksonBodyHandler. */ - var client = HttpClient.newBuilder().build(); var response = client.send( HttpRequest.newBuilder().GET().uri(uri).build(), diff --git a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstOperation.java b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstOperation.java index fd5baa7..1727a18 100644 --- a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstOperation.java +++ b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstOperation.java @@ -1,5 +1,15 @@ package io.github.tomboyo.lily.compiler.ast; -/** An operation, such as "createNewBlogPost" corresponding to an OAS operation. */ -public record AstOperation(String operationName, AstReference operationClass, String relativePath) +import java.util.List; + +/** + * An operation, such as "createNewBlogPost" corresponding to an OAS operation. + * + *

The parameters field list reflects the order of parameters as specified in the OAS. + */ +public record AstOperation( + String operationName, + AstReference operationClass, + String relativePath, + List parameters) implements Ast {} diff --git a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstParameter.java b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstParameter.java index 962064b..ccd994c 100644 --- a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstParameter.java +++ b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstParameter.java @@ -1,4 +1,5 @@ package io.github.tomboyo.lily.compiler.ast; /** A path parameter for an operation */ -public record AstParameter(String name, AstReference astReference) implements Ast {} +public record AstParameter(String name, AstParameterLocation location, AstReference astReference) + implements Ast {} diff --git a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstParameterLocation.java b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstParameterLocation.java new file mode 100644 index 0000000..e9c076b --- /dev/null +++ b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/ast/AstParameterLocation.java @@ -0,0 +1,18 @@ +package io.github.tomboyo.lily.compiler.ast; + +public enum AstParameterLocation { + PATH, + QUERY, + COOKIE, + HEADER; + + public static AstParameterLocation fromString(String raw) { + return switch (raw) { + case "path" -> PATH; + case "query" -> QUERY; + case "cookie" -> COOKIE; + case "header" -> HEADER; + default -> throw new IllegalArgumentException("Unrecognized parameter location: " + raw); + }; + } +} diff --git a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/cg/AstToJava.java b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/cg/AstToJava.java index 6e481de..948134b 100644 --- a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/cg/AstToJava.java +++ b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/cg/AstToJava.java @@ -1,5 +1,7 @@ package io.github.tomboyo.lily.compiler.cg; +import static io.github.tomboyo.lily.compiler.ast.AstParameterLocation.PATH; +import static io.github.tomboyo.lily.compiler.icg.Support.lowerCamelCase; import static java.util.stream.Collectors.toList; import com.github.mustachejava.DefaultMustacheFactory; @@ -79,7 +81,7 @@ private String recordField(AstField field) { "recordField", Map.of( "fqpt", fullyQualifiedParameterizedType(field.astReference()), - "name", Support.lowerCamelCase(field.name()))); + "name", lowerCamelCase(field.name()))); } private static String fullyQualifiedParameterizedType(AstReference ast) { @@ -167,7 +169,7 @@ public static class {{className}}Builder { tag -> Map.of( "fqReturnType", Fqns.fqn(tag), - "methodName", Support.lowerCamelCase(tag.name()))) + "methodName", lowerCamelCase(tag.name()))) .collect(toList()))); return sourceForFqn(ast, content); @@ -223,7 +225,22 @@ public class {{className}} { this.uriTemplate = io.github.tomboyo.lily.http.UriTemplate.forPath(uri, "{{{relativePath}}}"); } + {{#pathParameters}} + private {{{fqpt}}} {{name}}; + public {{className}} {{name}}({{{fqpt}}} {{name}}) { + this.{{name}} = {{name}}; + return this; + } + {{/pathParameters}} + public io.github.tomboyo.lily.http.UriTemplate uriTemplate() { + {{#pathParameters}} + if (this.{{name}} != null) { + uriTemplate.put( + "{{oasName}}", + io.github.tomboyo.lily.http.encoding.Encoding.simple(this.{{name}})); + } + {{/pathParameters}} return uriTemplate; } } @@ -232,7 +249,18 @@ public io.github.tomboyo.lily.http.UriTemplate uriTemplate() { Map.of( "packageName", ast.operationClass().packageName(), "className", Support.capitalCamelCase(ast.operationClass().name()), - "relativePath", ast.relativePath())); + "relativePath", ast.relativePath(), + "pathParameters", + ast.parameters().stream() + .filter(parameter -> parameter.location() == PATH) + .map( + parameter -> + Map.of( + "fqpt", + fullyQualifiedParameterizedType(parameter.astReference()), + "name", lowerCamelCase(parameter.name()), + "oasName", parameter.name())) + .collect(toList()))); return sourceForFqn(ast.operationClass(), content); } diff --git a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasOperationToAst.java b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasOperationToAst.java new file mode 100644 index 0000000..bd3a82e --- /dev/null +++ b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasOperationToAst.java @@ -0,0 +1,93 @@ +package io.github.tomboyo.lily.compiler.icg; + +import static io.github.tomboyo.lily.compiler.icg.Support.capitalCamelCase; +import static io.github.tomboyo.lily.compiler.icg.Support.joinPackages; +import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +import io.github.tomboyo.lily.compiler.ast.Ast; +import io.github.tomboyo.lily.compiler.ast.AstOperation; +import io.github.tomboyo.lily.compiler.ast.AstReference; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.parameters.Parameter; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class OasOperationToAst { + + private final String basePackage; + + private OasOperationToAst(String basePackage) { + this.basePackage = basePackage; + } + + public static TagsOperationAndAst evaluateOperaton( + String basePackage, + String relativePath, + Operation operation, + List inheritedParameters) { + return new OasOperationToAst(basePackage) + .evaluateOperation(relativePath, operation, inheritedParameters); + } + + private TagsOperationAndAst evaluateOperation( + String relativePath, Operation operation, List inheritedParameters) { + var operationName = requireNonNull(capitalCamelCase(operation.getOperationId())) + "Operation"; + var subordinatePackageName = joinPackages(basePackage, operationName); + var ownParameters = requireNonNullElse(operation.getParameters(), List.of()); + + var parametersAndAst = + mergeParameters(inheritedParameters, ownParameters).stream() + .map( + parameter -> OasParameterToAst.evaluateParameter(subordinatePackageName, parameter)) + .collect(Collectors.toList()); + var ast = + parametersAndAst.stream() + .flatMap(OasParameterToAst.ParameterAndAst::ast) + .collect(Collectors.toSet()); + var parameters = + parametersAndAst.stream() + .map(OasParameterToAst.ParameterAndAst::parameter) + .collect(Collectors.toList()); + + return new TagsOperationAndAst( + getOperationTags(operation), + new AstOperation( + operation.getOperationId(), + new AstReference(basePackage, operationName, List.of(), false), + relativePath, + parameters), + ast); + } + + /** Merge owned parameters with inherited parameters. Owned parameters take precedence. */ + private static Collection mergeParameters( + List inherited, List owned) { + return Stream.concat(inherited.stream(), owned.stream()) + .collect( + toMap( + param -> new ParameterId(param.getName(), param.getIn()), identity(), (a, b) -> b)) + .values(); + } + + private static Set getOperationTags(Operation operation) { + var set = new HashSet<>(requireNonNullElse(operation.getTags(), List.of())); + if (set.isEmpty()) { + set.add("other"); + } + set.add("all"); + return set; + } + + /** Holds the tags, AstOperation, and other Ast from evaluating an OAS Operation. */ + public static record TagsOperationAndAst( + Set tags, AstOperation operation, Set ast) {} + + private static record ParameterId(String name, String in) {} +} diff --git a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasParameterToAst.java b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasParameterToAst.java new file mode 100644 index 0000000..e2c41d4 --- /dev/null +++ b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasParameterToAst.java @@ -0,0 +1,27 @@ +package io.github.tomboyo.lily.compiler.icg; + +import static io.github.tomboyo.lily.compiler.icg.Support.capitalCamelCase; + +import io.github.tomboyo.lily.compiler.ast.Ast; +import io.github.tomboyo.lily.compiler.ast.AstParameter; +import io.github.tomboyo.lily.compiler.ast.AstParameterLocation; +import io.swagger.v3.oas.models.parameters.Parameter; +import java.util.stream.Stream; + +public class OasParameterToAst { + + public static ParameterAndAst evaluateParameter(String packageName, Parameter parameter) { + var parameterRefAndAst = + OasSchemaToAst.evaluate( + packageName, capitalCamelCase(parameter.getName()), parameter.getSchema()); + + return new ParameterAndAst( + new AstParameter( + parameter.getName(), + AstParameterLocation.fromString(parameter.getIn()), + parameterRefAndAst.left()), + parameterRefAndAst.right()); + } + + public static record ParameterAndAst(AstParameter parameter, Stream ast) {} +} diff --git a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasPathsToAst.java b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasPathsToAst.java index 5cf8a4f..8bcf743 100644 --- a/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasPathsToAst.java +++ b/modules/lily-compiler/src/main/java/io/github/tomboyo/lily/compiler/icg/OasPathsToAst.java @@ -1,29 +1,19 @@ package io.github.tomboyo.lily.compiler.icg; -import static io.github.tomboyo.lily.compiler.icg.Support.capitalCamelCase; -import static io.github.tomboyo.lily.compiler.icg.Support.joinPackages; -import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNullElse; -import static java.util.function.Function.identity; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.mapping; -import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet; -import io.github.tomboyo.lily.compiler.ast.Ast; import io.github.tomboyo.lily.compiler.ast.AstApi; -import io.github.tomboyo.lily.compiler.ast.AstOperation; -import io.github.tomboyo.lily.compiler.ast.AstReference; import io.github.tomboyo.lily.compiler.ast.AstTaggedOperations; +import io.github.tomboyo.lily.compiler.icg.OasOperationToAst.TagsOperationAndAst; import io.github.tomboyo.lily.compiler.util.Pair; -import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.parameters.Parameter; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; public class OasPathsToAst { @@ -47,21 +37,18 @@ public static AstApi evaluateApi(String basePackage, Set ta * a Stream describing AstTaggedOperations which group evaluated operations by their OAS tags. */ public static Stream evaluateTaggedOperations( - String basePackage, Collection results) { + String basePackage, Collection results) { return new OasPathsToAst(basePackage).evaluateTaggedOperations(results); } - /** - * Evaluate a single PathItem (and its operations, nested schemas, etc) to AST. Returns one - * EvaluatedOperation per operation in the PathItem. - */ - public static Stream evaluatePathItem( + /** Evaluate a single PathItem (and its operations, nested schemas, etc) to AST. */ + public static Stream evaluatePathItem( String basePackage, String relativePath, PathItem pathItem) { return new OasPathsToAst(basePackage).evaluatePathItem(relativePath, pathItem); } private Stream evaluateTaggedOperations( - Collection results) { + Collection results) { return results.stream() .flatMap(result -> result.tags().stream().map(tag -> new Pair<>(tag, result.operation()))) .collect(groupingBy(Pair::left, mapping(Pair::right, toSet()))) @@ -73,64 +60,12 @@ private Stream evaluateTaggedOperations( basePackage, entry.getKey() + "Operations", entry.getValue())); } - private Stream evaluatePathItem(String relativePath, PathItem pathItem) { + private Stream evaluatePathItem(String relativePath, PathItem pathItem) { var inheritedParameters = requireNonNullElse(pathItem.getParameters(), List.of()); - return pathItem.readOperationsMap().entrySet().stream() + return pathItem.readOperationsMap().values().stream() .map( - entry -> { - var operation = entry.getValue(); - return evaluateOperation(operation, relativePath, inheritedParameters); - }); - } - - private EvaluatedOperation evaluateOperation( - Operation operation, String relativePath, List inheritedParameters) { - var operationName = requireNonNull(operation.getOperationId()) + "Operation"; - var subordinatePackageName = joinPackages(basePackage, operationName); - var ownParameters = requireNonNullElse(operation.getParameters(), List.of()); - var ast = - mergeParameters(inheritedParameters, ownParameters).stream() - .flatMap(parameter -> evaluateParameter(subordinatePackageName, parameter)) - .collect(Collectors.toList()); - - return new EvaluatedOperation( - getOperationTags(operation), - new AstOperation( - operation.getOperationId(), - new AstReference(basePackage, operationName, List.of(), false), - relativePath), - ast); - } - - private static Set getOperationTags(Operation operation) { - var set = new HashSet<>(requireNonNullElse(operation.getTags(), List.of())); - if (set.isEmpty()) { - set.add("other"); - } - set.add("all"); - return set; + operation -> + OasOperationToAst.evaluateOperaton( + basePackage, relativePath, operation, inheritedParameters)); } - - /** Merge owned parameters with inherited parameters. Owned parameters take precedence. */ - private static Collection mergeParameters( - List inherited, List owned) { - return Stream.concat(inherited.stream(), owned.stream()) - .collect( - toMap( - param -> new ParameterId(param.getName(), param.getIn()), identity(), (a, b) -> b)) - .values(); - } - - private Stream evaluateParameter(String packageName, Parameter parameter) { - var parameterRefAndAst = - OasSchemaToAst.evaluate( - packageName, capitalCamelCase(parameter.getName()), parameter.getSchema()); - - return parameterRefAndAst.right(); - } - - public static record EvaluatedOperation( - Set tags, AstOperation operation, List ast) {} - - private static record ParameterId(String name, String in) {} } diff --git a/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/AstSupport.java b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/AstSupport.java new file mode 100644 index 0000000..de1caba --- /dev/null +++ b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/AstSupport.java @@ -0,0 +1,12 @@ +package io.github.tomboyo.lily.compiler; + +import static io.github.tomboyo.lily.compiler.icg.StdlibAstReferences.astBoolean; + +import io.github.tomboyo.lily.compiler.ast.AstReference; + +public class AstSupport { + /** Used in test cases to create an AstReference whose contents are not under test. */ + public static AstReference astReferencePlaceholder() { + return astBoolean(); + } +} diff --git a/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/PathsTest.java b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/PathsTest.java index 0b219ad..5b4e9c9 100644 --- a/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/PathsTest.java +++ b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/PathsTest.java @@ -184,4 +184,50 @@ void templatesAreExposedForAllOperations() { is(URI.create("https://example.com/pets/some-uuid-here"))); } } + + @Nested + class PathParameterSupport { + private static String packageName; + + @BeforeAll + static void beforeAll() throws Exception { + packageName = + compileOas( + """ + openapi: 3.0.2 + paths: + /pets/{id}: + get: + operationId: getPetById + parameters: + - name: id + in: path + required: true + schema: + type: string + """); + } + + @Test + void hasPathParameterSetters() { + var actual = + evaluate( + """ + return %s.Api.newBuilder() + .uri("https://example.com/") + .build() + .allOperations() + .getPetById() + .id("1234") + .uriTemplate() + .toURI(); + """ + .formatted(packageName), + URI.class); + assertThat( + "Named path parameters may be set via the operation API", + actual, + is(URI.create("https://example.com/pets/1234"))); + } + } } diff --git a/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasOperationToAstTest.java b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasOperationToAstTest.java new file mode 100644 index 0000000..abf13b5 --- /dev/null +++ b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasOperationToAstTest.java @@ -0,0 +1,230 @@ +package io.github.tomboyo.lily.compiler.icg; + +import static io.github.tomboyo.lily.compiler.AstSupport.astReferencePlaceholder; +import static io.github.tomboyo.lily.compiler.ast.AstParameterLocation.PATH; +import static io.github.tomboyo.lily.compiler.ast.AstParameterLocation.QUERY; +import static io.github.tomboyo.lily.compiler.icg.StdlibAstReferences.astBoolean; +import static io.github.tomboyo.lily.compiler.icg.StdlibAstReferences.astString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; + +import io.github.tomboyo.lily.compiler.ast.AstParameter; +import io.github.tomboyo.lily.compiler.ast.AstReference; +import io.github.tomboyo.lily.compiler.icg.OasOperationToAst.TagsOperationAndAst; +import io.github.tomboyo.lily.compiler.icg.OasParameterToAst.ParameterAndAst; +import io.github.tomboyo.lily.compiler.util.Pair; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.BooleanSchema; +import io.swagger.v3.oas.models.media.DateSchema; +import io.swagger.v3.oas.models.media.IntegerSchema; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class OasOperationToAstTest { + @Nested + class EvaluateOperation { + @Test + void evaluatesAllParametersToAst() { + try (var mock = mockStatic(OasSchemaToAst.class)) { + var ast = astReferencePlaceholder(); + mock.when(() -> OasSchemaToAst.evaluate(any(), any(), any())) + .thenAnswer( + invocation -> new Pair<>(astBoolean(), Stream.of(astReferencePlaceholder()))); + + var actual = + OasOperationToAst.evaluateOperaton( + "p", + "/relative/path", + new Operation() + .operationId("operationId") + .addParametersItem( + new Parameter().name("a").in("path").schema(new IntegerSchema())) + .addParametersItem( + new Parameter().name("b").in("query").schema(new DateSchema())), + List.of( + new Parameter().name("c").in("header").schema(new BooleanSchema()), + new Parameter().name("d").in("cookie").schema(new StringSchema()))); + + assertThat( + "Parameter AST is returned", actual.ast(), is(Set.of(astReferencePlaceholder()))); + + // Each parameter is evaluated to AST in turn + mock.verify( + () -> + OasSchemaToAst.evaluate( + eq("p.operationidoperation"), eq("A"), eq(new IntegerSchema()))); + mock.verify( + () -> + OasSchemaToAst.evaluate( + eq("p.operationidoperation"), eq("B"), eq(new DateSchema()))); + mock.verify( + () -> + OasSchemaToAst.evaluate( + eq("p.operationidoperation"), eq("C"), eq(new BooleanSchema()))); + mock.verify( + () -> + OasSchemaToAst.evaluate( + eq("p.operationidoperation"), eq("D"), eq(new StringSchema()))); + } + } + + @Test + void overridesInheritedParameters() { + try (var mock = mockStatic(OasSchemaToAst.class)) { + mock.when(() -> OasSchemaToAst.evaluate(any(), any(), any())) + .thenAnswer(invocation -> new Pair<>(astBoolean(), Stream.of())); + + var actual = + OasOperationToAst.evaluateOperaton( + "p", + "/relative/path", + new Operation() + .operationId("operationId") + .addParametersItem( + new Parameter() + .name("a") + .in("query") + .schema(new IntegerSchema())), // the only IntegerSchema + List.of( + new Parameter() + .name("a") + .in("path") + .schema(new BooleanSchema()), // different `in` + new Parameter() + .name("b") + .in("query") + .schema(new BooleanSchema()), // different `name` + new Parameter() + .name("a") + .in("query") + .schema(new BooleanSchema()) // equal `name` and `in` + )); + + // The PathItem's "a" query parameter is overridden by the Operation's query parameter with + // the same name. + mock.verify( + () -> + OasSchemaToAst.evaluate( + eq("p.operationidoperation"), eq("A"), eq(new IntegerSchema()))); + // The other parameters are not affected. + mock.verify( + () -> + OasSchemaToAst.evaluate( + eq("p.operationidoperation"), eq("A"), eq(new BooleanSchema()))); + mock.verify( + () -> + OasSchemaToAst.evaluate( + eq("p.operationidoperation"), eq("B"), eq(new BooleanSchema()))); + } + } + + @Nested + class Tags { + @Test + void whenOasOperationHasTags() { + try (var mock = mockStatic(OasSchemaToAst.class)) { + mock.when(() -> OasSchemaToAst.evaluate(any(), any(), any())) + .thenAnswer(invocation -> new Pair<>(astBoolean(), Stream.of())); + + var actual = + OasOperationToAst.evaluateOperaton( + "p", + "/relative/path/", + new Operation().operationId("operationId").tags(List.of("tagA", "tagB")), + List.of()); + + assertThat( + "The result contains all OAS tags and the 'all' tag", + actual.tags(), + is(Set.of("tagA", "tagB", "all"))); + } + } + + @Test + void whenOasOperationHasNoTags() { + try (var mock = mockStatic(OasSchemaToAst.class)) { + mock.when(() -> OasSchemaToAst.evaluate(any(), any(), any())) + .thenAnswer(invocation -> new Pair<>(astBoolean(), Stream.of())); + + var actual = + OasOperationToAst.evaluateOperaton( + "p", + "/relative/path/", + new Operation().operationId("operationId").tags(List.of()), // empty! + List.of()); + + assertThat( + "The result contains the default 'other' tag and the 'all' tag", + actual.tags(), + is(Set.of("other", "all"))); + } + } + } + + @Nested + class AstOperation { + TagsOperationAndAst actual() { + return OasOperationToAst.evaluateOperaton( + "p", + "/relative/path/", + new Operation() + .operationId("operationId") + .addParametersItem(new Parameter().name("a").in("path").schema(new IntegerSchema())) + .addParametersItem( + new Parameter().name("b").in("query").schema(new IntegerSchema())), + List.of()); + } + + @Test + void containsOasOperationName() { + assertThat( + "The operation name is taken from the globally unique OAS operationID", + actual().operation().operationName(), + is("operationId")); + } + + @Test + void referencesNewOperationClass() { + assertThat( + "The AstReference points to a generated type named after the operation ID", + actual().operation().operationClass(), + is(new AstReference("p", "OperationIdOperation", List.of(), false))); + } + + @Test + void containsRelativePath() { + assertThat( + "The operation's relative path is as given", + actual().operation().relativePath(), + is("/relative/path/")); + } + + @Test + void containsParametersList() { + try (var mock = mockStatic(OasParameterToAst.class)) { + mock.when(() -> OasParameterToAst.evaluateParameter(any(), any())) + .thenReturn( + new ParameterAndAst(new AstParameter("1", PATH, astBoolean()), Stream.of())) + .thenReturn( + new ParameterAndAst(new AstParameter("2", QUERY, astString()), Stream.of())); + + assertThat( + "The parameters list is in the original OAS order", + actual().operation().parameters(), + is( + List.of( + new AstParameter("1", PATH, astBoolean()), + new AstParameter("2", QUERY, astString())))); + } + } + } + } +} diff --git a/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasParameterToAstTest.java b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasParameterToAstTest.java new file mode 100644 index 0000000..09f7768 --- /dev/null +++ b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasParameterToAstTest.java @@ -0,0 +1,72 @@ +package io.github.tomboyo.lily.compiler.icg; + +import static io.github.tomboyo.lily.compiler.AstSupport.astReferencePlaceholder; +import static io.github.tomboyo.lily.compiler.ast.AstParameterLocation.PATH; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; + +import io.github.tomboyo.lily.compiler.ast.AstParameterLocation; +import io.github.tomboyo.lily.compiler.util.Pair; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +public class OasParameterToAstTest { + @Test + void parameterNamedForOasParameterName() { + var actual = + OasParameterToAst.evaluateParameter( + "p", new Parameter().name("name").in("path").schema(new StringSchema())); + + assertThat(actual.parameter().name(), is("name")); + } + + @Test + void parameterLocationIsAsGiven() { + try (var astParameterLocation = mockStatic(AstParameterLocation.class); + var oasSchemaToAst = mockStatic(OasSchemaToAst.class)) { + oasSchemaToAst + .when(() -> OasSchemaToAst.evaluate(any(), any(), any())) + .thenReturn(new Pair<>(astReferencePlaceholder(), Stream.of())); + astParameterLocation.when(() -> AstParameterLocation.fromString(any())).thenReturn(PATH); + + var actual = + OasParameterToAst.evaluateParameter("p", new Parameter().name("name").in("location")); + + assertThat( + "The parameter location is as given by AstParameterLocation", + actual.parameter().location(), + is(PATH)); + } + } + + @Test + void parameterAstIsAsGiven() { + try (var mock = mockStatic(OasSchemaToAst.class)) { + var ref = astReferencePlaceholder(); + var ast = astReferencePlaceholder(); + mock.when(() -> OasSchemaToAst.evaluate(any(), any(), any())) + .thenReturn(new Pair<>(ref, Stream.of(ast))); + + var actual = + OasParameterToAst.evaluateParameter( + "p", new Parameter().name("name").in("path").schema(new ObjectSchema())); + + assertThat( + "The astReference is returned as given by OasSchemaToAst", + actual.parameter().astReference(), + sameInstance(ref)); + assertThat( + "The ast stream is returned as given by OasSchemaToAst", + actual.ast().collect(Collectors.toSet()), + hasItem(sameInstance(ast))); + } + } +} diff --git a/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasPathsToAstTest.java b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasPathsToAstTest.java index 938f968..7d51cd9 100644 --- a/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasPathsToAstTest.java +++ b/modules/lily-compiler/src/test/java/io/github/tomboyo/lily/compiler/icg/OasPathsToAstTest.java @@ -1,28 +1,22 @@ package io.github.tomboyo.lily.compiler.icg; -import static io.github.tomboyo.lily.compiler.icg.StdlibAstReferences.astBoolean; +import static io.github.tomboyo.lily.compiler.AstSupport.astReferencePlaceholder; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; import io.github.tomboyo.lily.compiler.ast.AstOperation; -import io.github.tomboyo.lily.compiler.ast.AstReference; import io.github.tomboyo.lily.compiler.ast.AstTaggedOperations; -import io.github.tomboyo.lily.compiler.icg.OasPathsToAst.EvaluatedOperation; -import io.github.tomboyo.lily.compiler.util.Pair; +import io.github.tomboyo.lily.compiler.icg.OasOperationToAst.TagsOperationAndAst; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; -import io.swagger.v3.oas.models.media.BooleanSchema; -import io.swagger.v3.oas.models.media.IntegerSchema; -import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; -import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -31,154 +25,51 @@ public class OasPathsToAstTest { @Nested class EvaluatePathItem { - @Nested - class Parameters { - @Test - void areEvaluatedToAst() { - try (var mock = mockStatic(OasSchemaToAst.class)) { - mock.when(() -> OasSchemaToAst.evaluate(any(), any(), any())) - .thenAnswer(invocation -> new Pair<>(astBoolean(), Stream.of())); - - OasPathsToAst.evaluatePathItem( - "p", - "get/", - new PathItem() - .addParametersItem( - new Parameter().name("a").in("path").schema(new BooleanSchema())) - .addParametersItem( - new Parameter().name("b").in("path").schema(new StringSchema())) - .get( - new Operation() - .operationId("Get") - .addParametersItem( - new Parameter().name("c").in("query").schema(new IntegerSchema())) - .addParametersItem( - new Parameter() - .name("d") - .in("header") - .schema(new BooleanSchema())))) - .forEach(x -> {}); // consume the stream. - - // Schema are generated for all parameters according to OasSchemaToAst. - mock.verify( - () -> - OasSchemaToAst.evaluate(eq("p.getoperation"), eq("A"), eq(new BooleanSchema()))); - mock.verify( - () -> OasSchemaToAst.evaluate(eq("p.getoperation"), eq("B"), eq(new StringSchema()))); - mock.verify( - () -> - OasSchemaToAst.evaluate(eq("p.getoperation"), eq("C"), eq(new IntegerSchema()))); - mock.verify( - () -> - OasSchemaToAst.evaluate(eq("p.getoperation"), eq("D"), eq(new BooleanSchema()))); - } - } - - @Test - void mayBeOverridden() { - try (var mock = mockStatic(OasSchemaToAst.class)) { - mock.when(() -> OasSchemaToAst.evaluate(any(), any(), any())) - .thenAnswer(invocation -> new Pair<>(astBoolean(), Stream.of())); - - OasPathsToAst.evaluatePathItem( - "p", - "get/", - new PathItem() - .addParametersItem( - new Parameter().name("a").in("query").schema(new BooleanSchema())) - .get( - new Operation() - .operationId("Get") - .addParametersItem( - new Parameter() - .name("a") - .in("query") - .schema(new IntegerSchema())))) - .forEach(x -> {}); - - mock.verify( - () -> - OasSchemaToAst.evaluate(eq("p.getoperation"), eq("A"), eq(new IntegerSchema()))); - } - } - } - - @Nested - class OperationTags { - EvaluatedOperation actual(String... tags) { - return OasPathsToAst.evaluatePathItem( + @Test + void evaluatesAllOperations() { + try (var mock = mockStatic(OasOperationToAst.class)) { + OasPathsToAst.evaluatePathItem( "p", - "get/", - new PathItem().get(new Operation().operationId("Get").tags(Arrays.asList(tags)))) - .findAny() - .orElseThrow(); // Exactly one expected since there's one OAS operation - } - - @Test - void whenOperationHasTags() { - assertThat( - "The OAS tags and the default 'all' tag are added to the result", - actual("tagA", "tagB").tags(), - is(Set.of("tagA", "tagB", "all"))); - } - - @Test - void whenOperationHasNoTags() { - assertThat( - "The default 'other' and 'all' tags are added to the result", - actual().tags(), - is(Set.of("other", "all"))); - } - } - - @Nested - class AstOperation { - EvaluatedOperation actual() { - return OasPathsToAst.evaluatePathItem( - "p", "get/", new PathItem().get(new Operation().operationId("Get"))) - .findAny() - .orElseThrow(); // Exactly one expected since there's one OAS operation - } - - @Test - void containsOasOperationName() { - assertThat(actual().operation().operationName(), is("Get")); - } - - @Test - void referencesNewOperationClass() { - assertThat( - "The result references a new operation named after the operation ID", - actual().operation().operationClass(), - is(new AstReference("p", "GetOperation", List.of(), false))); - } - - @Test - void containsRelativePath() { - assertThat( - "The resulting operation contains its relative path from the Path Item", - actual().operation().relativePath(), - is("get/")); + "/operation/path", + new PathItem() + .addParametersItem(new Parameter().name("name").in("path")) + .get(new Operation()) + .put(new Operation()) + .post(new Operation()) + .patch(new Operation()) + .delete(new Operation()) + .head(new Operation()) + .options(new Operation())) + .forEach(x -> {}); // consume the stream + + // It should evaluate each operation in turn, passing down the base package, operation + // relative path, and inherited parameters from the PathItem. + mock.verify( + () -> + OasOperationToAst.evaluateOperaton( + eq("p"), + eq("/operation/path"), + any(), + eq(List.of(new Parameter().name("name").in("path")))), + times(7)); } } } @Nested - class TaggedOperations { + class EvaluateTaggedOperations { + @Test - void groupsByTags() { - var getAOperation = - new AstOperation( - "GetA", new AstReference("p", "GetAOperation", List.of(), false), "getA/"); + void groupsOperationsByTag() { + var getAOperation = new AstOperation("GetA", astReferencePlaceholder(), "getA/", List.of()); var getABOperation = - new AstOperation( - "GetAB", new AstReference("p", "GetABOperation", List.of(), false), "getAB/"); + new AstOperation("GetAB", astReferencePlaceholder(), "getAB/", List.of()); var actual = OasPathsToAst.evaluateTaggedOperations( "p", List.of( - new EvaluatedOperation(Set.of("TagA"), getAOperation, List.of()), - new EvaluatedOperation(Set.of("TagA", "TagB"), getABOperation, List.of()))) + new TagsOperationAndAst(Set.of("TagA"), getAOperation, Set.of()), + new TagsOperationAndAst(Set.of("TagA", "TagB"), getABOperation, Set.of()))) .collect(Collectors.toSet()); assertThat( diff --git a/modules/lily-http/src/main/java/io/github/tomboyo/lily/http/encoding/Encoding.java b/modules/lily-http/src/main/java/io/github/tomboyo/lily/http/encoding/Encoding.java index c9abd7b..8f90105 100644 --- a/modules/lily-http/src/main/java/io/github/tomboyo/lily/http/encoding/Encoding.java +++ b/modules/lily-http/src/main/java/io/github/tomboyo/lily/http/encoding/Encoding.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.UncheckedIOException; public class Encoding { @@ -20,11 +21,19 @@ public class Encoding { private Encoding() {} - public static String simple(Object o) throws JsonProcessingException { - return simpleMapper.writer().writeValueAsString(o); + public static String simple(Object o) { + try { + return simpleMapper.writer().writeValueAsString(o); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } } - public static String formExplode(Object o) throws JsonProcessingException { - return formExplodeMapper.writer().writeValueAsString(o); + public static String formExplode(Object o) { + try { + return formExplodeMapper.writer().writeValueAsString(o); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } } }