Skip to content

Commit

Permalink
Issues/19 (#24)
Browse files Browse the repository at this point in the history
Add path parameter support.
  • Loading branch information
Tomboyo authored Aug 30, 2022
1 parent 2be1f56 commit 5e2bde3
Show file tree
Hide file tree
Showing 14 changed files with 673 additions and 250 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,68 @@
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;

@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<Pet>() {}));

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"}
""")));
Expand All @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>The parameters field list reflects the order of parameters as specified in the OAS.
*/
public record AstOperation(
String operationName,
AstReference operationClass,
String relativePath,
List<AstParameter> parameters)
implements Ast {}
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
@@ -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);
};
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Parameter> inheritedParameters) {
return new OasOperationToAst(basePackage)
.evaluateOperation(relativePath, operation, inheritedParameters);
}

private TagsOperationAndAst evaluateOperation(
String relativePath, Operation operation, List<Parameter> inheritedParameters) {
var operationName = requireNonNull(capitalCamelCase(operation.getOperationId())) + "Operation";
var subordinatePackageName = joinPackages(basePackage, operationName);
var ownParameters = requireNonNullElse(operation.getParameters(), List.<Parameter>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<Parameter> mergeParameters(
List<Parameter> inherited, List<Parameter> 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<String> 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<String> tags, AstOperation operation, Set<Ast> ast) {}

private static record ParameterId(String name, String in) {}
}
Original file line number Diff line number Diff line change
@@ -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> ast) {}
}
Loading

0 comments on commit 5e2bde3

Please sign in to comment.