Skip to content

Commit

Permalink
Issues/37 (#38)
Browse files Browse the repository at this point in the history
Support query parameters in generated code
  • Loading branch information
Tomboyo authored Oct 14, 2022
1 parent 64a7358 commit e09c7dd
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.ok;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static io.github.tomboyo.lily.http.encoding.Encoders.Modifiers.EXPLODE;
import static io.github.tomboyo.lily.http.encoding.Encoders.form;
import static io.github.tomboyo.lily.http.encoding.Encoders.formExploded;
import static io.github.tomboyo.lily.http.encoding.Encoders.simple;
import static org.junit.jupiter.api.Assertions.assertEquals;

Expand Down Expand Up @@ -105,7 +104,7 @@ void manual(WireMockRuntimeInfo info) throws Exception {
// The operation doesn't have any query parameter templates, so we'll add one and bind a
// value to it.
.appendTemplate("{queryParameters}")
.bind("queryParameters", Map.of("foo", "foo", "bar", "bar"), form(EXPLODE))
.bind("queryParameters", Map.of("foo", "foo", "bar", "bar"), formExploded())
.toURI();

/*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.tomboyo.lily.compiler.ast;

/** The string expansion strategy for a parameter. */
public record AstEncoding(Style style, boolean explode) {
public enum Style {
SIMPLE,
FORM,
UNSUPPORTED
}

public static AstEncoding simple() {
return new AstEncoding(Style.SIMPLE, false);
}

public static AstEncoding simpleExplode() {
return new AstEncoding(Style.SIMPLE, true);
}

public static AstEncoding form() {
return new AstEncoding(Style.FORM, false);
}

public static AstEncoding formExplode() {
return new AstEncoding(Style.FORM, true);
}

public static AstEncoding unsupported() {
return new AstEncoding(Style.UNSUPPORTED, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@

/** A path parameter for an operation */
public record AstParameter(
SimpleName name, AstParameterLocation location, AstReference astReference) implements Ast {}
SimpleName name, AstParameterLocation location, AstEncoding encoding, AstReference astReference)
implements Ast {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.github.tomboyo.lily.compiler.cg;

import static io.github.tomboyo.lily.compiler.ast.AstEncoding.Style.FORM;
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.astByteBuffer;
import static java.util.stream.Collectors.toList;

Expand All @@ -10,6 +12,7 @@
import io.github.tomboyo.lily.compiler.ast.AstApi;
import io.github.tomboyo.lily.compiler.ast.AstClass;
import io.github.tomboyo.lily.compiler.ast.AstClassAlias;
import io.github.tomboyo.lily.compiler.ast.AstEncoding;
import io.github.tomboyo.lily.compiler.ast.AstField;
import io.github.tomboyo.lily.compiler.ast.AstOperation;
import io.github.tomboyo.lily.compiler.ast.AstReference;
Expand Down Expand Up @@ -98,7 +101,7 @@ private String recordField(AstField field) {
} else {
return writeString(
"""
@com.fasterxml.jackson.annotation.JsonProperty(\"{{rawName}}\")
@com.fasterxml.jackson.annotation.JsonProperty("{{rawName}}")
{{{fqpt}}} {{name}}
""",
"recordField",
Expand Down Expand Up @@ -252,26 +255,29 @@ public class {{className}} {
public {{className}}(String uri) {
// We assume uri is non-null and ends with a trailing '/'.
this.uriTemplate = io.github.tomboyo.lily.http.UriTemplate.of(uri + "{{{relativePath}}}");
this.uriTemplate = io.github.tomboyo.lily.http.UriTemplate.of(uri + "{{{relativePath}}}{{{queryTemplate}}}");
}
{{#pathParameters}}
{{#urlParameters}}
private {{{fqpt}}} {{name}};
public {{className}} {{name}}({{{fqpt}}} {{name}}) {
this.{{name}} = {{name}};
return this;
}
{{/pathParameters}}
{{/urlParameters}}
public io.github.tomboyo.lily.http.UriTemplate uriTemplate() {
{{#pathParameters}}
{{#smartFormEncoder}}
var smartFormEncoder = io.github.tomboyo.lily.http.encoding.Encoders.smartFormExploded(); // stateful
{{/smartFormEncoder}}
{{#urlParameters}}
if (this.{{name}} != null) {
uriTemplate.bind(
"{{oasName}}",
this.{{name}},
io.github.tomboyo.lily.http.encoding.Encoders.simple());
{{{encoder}}});
}
{{/pathParameters}}
{{/urlParameters}}
return uriTemplate;
}
}
Expand All @@ -281,21 +287,45 @@ public io.github.tomboyo.lily.http.UriTemplate uriTemplate() {
"packageName", ast.operationClass().name().packageName(),
"className", ast.operationClass().name().simpleName(),
"relativePath", withoutLeadingSlash(ast.relativePath()),
"pathParameters",
"queryTemplate",
ast.parameters().stream()
.filter(parameter -> parameter.location() == QUERY)
.map(parameter -> "{" + parameter.name().raw() + "}")
.collect(Collectors.joining("")),
"smartFormEncoder",
ast.parameters().stream().anyMatch(parameter -> parameter.location() == QUERY),
// path and query parameters -- anything in the URL itself
"urlParameters",
ast.parameters().stream()
.filter(parameter -> parameter.location() == PATH)
.filter(
parameter ->
parameter.location() == PATH || parameter.location() == QUERY)
.map(
parameter ->
Map.of(
"fqpt",
fullyQualifiedParameterizedType(parameter.astReference()),
"name", parameter.name().lowerCamelCase(),
"oasName", parameter.name().raw()))
"oasName", parameter.name().raw(),
"encoder", getEncoder(parameter.encoding())))
.collect(toList())));

return createSource(ast.operationClass().name(), content);
}

private static String getEncoder(AstEncoding encoding) {
if (encoding.style() == FORM) {
// use the stateful smartFormEncoder local variable.
return "smartFormEncoder";
}

if (encoding.explode()) {
return "io.github.tomboyo.lily.http.encoding.Encoders.simpleExplode()";
} else {
return "io.github.tomboyo.lily.http.encoding.Encoders.simple()";
}
}

private static Source createSource(Fqn fqn, String content) {
return new Source(fqn.toPath(), fqn.toString(), content);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private TagsOperationAndAst evaluateOperation(
mergeParameters(inheritedParameters, ownParameters).stream()
.map(
parameter -> OasParameterToAst.evaluateParameter(subordinatePackageName, parameter))
.collect(Collectors.toList());
.toList();
var ast =
parametersAndAst.stream()
.flatMap(OasParameterToAst.ParameterAndAst::ast)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package io.github.tomboyo.lily.compiler.icg;

import static io.github.tomboyo.lily.compiler.ast.AstEncoding.form;
import static io.github.tomboyo.lily.compiler.ast.AstEncoding.formExplode;
import static io.github.tomboyo.lily.compiler.ast.AstEncoding.simple;
import static io.github.tomboyo.lily.compiler.ast.AstEncoding.simpleExplode;
import static io.github.tomboyo.lily.compiler.ast.AstEncoding.unsupported;

import io.github.tomboyo.lily.compiler.ast.Ast;
import io.github.tomboyo.lily.compiler.ast.AstEncoding;
import io.github.tomboyo.lily.compiler.ast.AstParameter;
import io.github.tomboyo.lily.compiler.ast.AstParameterLocation;
import io.github.tomboyo.lily.compiler.ast.PackageName;
import io.github.tomboyo.lily.compiler.ast.SimpleName;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.Parameter.StyleEnum;
import java.util.stream.Stream;

public class OasParameterToAst {
Expand All @@ -15,13 +23,33 @@ public static ParameterAndAst evaluateParameter(PackageName packageName, Paramet
OasSchemaToAst.evaluate(
packageName, SimpleName.of(parameter.getName()), parameter.getSchema());

var location = AstParameterLocation.fromString(parameter.getIn());

var encoding =
parameter.getStyle() != null
? getExplicitEncoding(parameter.getStyle(), parameter.getExplode())
: getDefaultEncoding(location);

return new ParameterAndAst(
new AstParameter(
SimpleName.of(parameter.getName()),
AstParameterLocation.fromString(parameter.getIn()),
parameterRefAndAst.left()),
SimpleName.of(parameter.getName()), location, encoding, parameterRefAndAst.left()),
parameterRefAndAst.right());
}

private static AstEncoding getExplicitEncoding(StyleEnum style, boolean explode) {
return switch (style) {
case SIMPLE -> explode ? simpleExplode() : simple();
case FORM -> explode ? formExplode() : form();
case MATRIX, DEEPOBJECT, LABEL, PIPEDELIMITED, SPACEDELIMITED -> unsupported();
};
}

private static AstEncoding getDefaultEncoding(AstParameterLocation location) {
return switch (location) {
case PATH, HEADER -> simple();
case QUERY, COOKIE -> formExplode();
};
}

public static record ParameterAndAst(AstParameter parameter, Stream<Ast> ast) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,62 @@ void hasPathParameterSetters() {
is(URI.create("https://example.com/pets/1234")));
}
}

@Nested
class QueryParameterSupport {
private static String packageName;

@BeforeAll
static void beforeAll() throws Exception {
packageName =
compileOas(
"""
openapi: 3.0.2
paths:
/pets:
get:
operationId: listPets
parameters:
- name: limit
in: query
schema:
type: integer
format: int32
# Path parameters are simple by default
# style: simple
- name: include
in: query
schema:
type: array
items:
type: string
# Query parameters are form-explode by default
# style: form
# explode: true
""");
}

@Test
void hasQueryParameterSetters() {
var actual =
evaluate(
"""
return %s.Api.newBuilder()
.uri("https://example.com/")
.build()
.allOperations()
.listPets()
.limit(5)
.include(java.util.List.of("name", "age"))
.uriTemplate()
.toURI();
"""
.formatted(packageName),
URI.class);
assertThat(
"Query parameters may be set via the operation API",
actual,
is(URI.create("https://example.com/pets?include=name&include=age&limit=5")));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;

import io.github.tomboyo.lily.compiler.ast.AstEncoding;
import io.github.tomboyo.lily.compiler.ast.AstParameter;
import io.github.tomboyo.lily.compiler.ast.Fqn;
import io.github.tomboyo.lily.compiler.ast.PackageName;
Expand Down Expand Up @@ -225,18 +226,24 @@ void containsParametersList() {
mock.when(() -> OasParameterToAst.evaluateParameter(any(), any()))
.thenReturn(
new ParameterAndAst(
new AstParameter(SimpleName.of("A"), PATH, astBoolean()), Stream.of()))
new AstParameter(
SimpleName.of("A"), PATH, AstEncoding.simple(), astBoolean()),
Stream.of()))
.thenReturn(
new ParameterAndAst(
new AstParameter(SimpleName.of("B"), QUERY, astString()), Stream.of()));
new AstParameter(
SimpleName.of("B"), QUERY, AstEncoding.formExplode(), astString()),
Stream.of()));

assertThat(
"The parameters list is in the original OAS order",
actual().operation().parameters(),
is(
List.of(
new AstParameter(SimpleName.of("A"), PATH, astBoolean()),
new AstParameter(SimpleName.of("B"), QUERY, astString()))));
new AstParameter(
SimpleName.of("A"), PATH, AstEncoding.simple(), astBoolean()),
new AstParameter(
SimpleName.of("B"), QUERY, AstEncoding.formExplode(), astString()))));
}
}
}
Expand Down
Loading

0 comments on commit e09c7dd

Please sign in to comment.