Skip to content

Commit

Permalink
Issues/60 (#63) configure parameters by location
Browse files Browse the repository at this point in the history
Configure parameters within per-location callbacks/namespaces:
```
myOperation
  .path(path -> path.id("1234"))
  .query(query -> query.foo("foo").bar("bar"))
  .sendSync();
```
We've also re-assessed the role of UriTemplate as a tool for customizing requests, by which I mean, it is no longer used for that. Instead, the operation exposes `baseUri()`, `pathString()`, and `queryString()` methods that the user can use to create a new URI. This puts more of the customization work back in the native HTTP API.
  • Loading branch information
Tomboyo authored Feb 10, 2023
1 parent 4313c13 commit 2b94cc1
Show file tree
Hide file tree
Showing 8 changed files with 347 additions and 251 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@
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.formExploded;
import static io.github.tomboyo.lily.http.encoding.Encoders.simple;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import io.github.tomboyo.lily.example.showpetbyidoperation.ShowPetById200;
import io.github.tomboyo.lily.example.showpetbyidoperation.ShowPetByIdDefault;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.Map;
import org.junit.jupiter.api.Test;

/* These are examples of how to use the API, formulated as contrived unit tests. These are not actual tests. */
Expand Down Expand Up @@ -47,10 +43,14 @@ void happyPath(WireMockRuntimeInfo info) throws Exception {
* fluently specify path and query parameters (not yet header or body parameters).
*/
var response =
api.petsOperations() // All operations with the `pets` tag. (see also: everyOperation)
.showPetById() // The GET /pets/{petId} operation
.setPetId("1234") // bind "1234" to the {petId} parameter of the OAS operation
.sendSync(); // execute the request synchronously and get a ShowPetByIdResponse object.
// All operations with the `pets` tag. (see also: everyOperation)
api.petsOperations()
// The GET /pets/{petId} operation
.showPetById()
// bind "1234" to the {petId} path parameter of the OAS operation.
.path(path -> path.petId("1234"))
// execute the request synchronously and get a ShowPetByIdResponse object.
.sendSync();

/* The response object is a sealed interface based on what the OAS says the API can return. In this case, the
* ShowPetByIdResponse may consist of the 200 and Default variants.
Expand Down Expand Up @@ -80,40 +80,33 @@ void manual(WireMockRuntimeInfo info) throws Exception {
{"id": 1234, "name": "Reginald"}
""")));

/*
* All operations (GET /foo, POST /foo, etc) documented by the OAS are captured by the generated API, named
* according to their OAS operation IDs.
*/
var api = Api.newBuilder().uri(info.getHttpBaseUrl()).build();
var operation =
api
// All operations with the `pets` tag. (We could also use .everyOperation())
.petsOperations()
.showPetById(); // the GET /pets/{petId} operation

var uri =
operation
// Access the underlying uri template to finish the request manually
.uriTemplate()
// Bind "1234" to the petId path parameter. The Encoders class implements several common
// formats. We can override bindings set by the operation.
.bind("petId", 1234, simple())
// 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"), formExploded())
.toURI();
/* Customize a request with any parameters documented by the OpenAPI specification. In this
* example we'll assume query parameters are missing from the OpenAPI specification and can't
* be configured via the generated API.
*/
var operation = api.petsOperations().showPetById().path(path -> path.petId("1234"));

/*
* 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.
/* Using the native API, create a new http request. It will use our templated request for
* default values, but we can override any part of the request, like the query string.
*/
var response =
api.httpClient()
.send(HttpRequest.newBuilder().GET().uri(uri).build(), BodyHandlers.ofInputStream());
var request =
HttpRequest.newBuilder(operation.httpRequest(), (k, v) -> true)
// We can use baseUri(), pathString(), and queryString() from the operation to override
// templated URIs. This lets us work around incomplete specifications until they are
// fixed.
.uri(URI.create(operation.baseUri() + operation.pathString() + "?foo=foo&bar=bar"))
.build();

assertEquals(200, response.statusCode());
assertEquals(
new Pet(1234L, "Reginald", null), new ObjectMapper().readValue(response.body(), Pet.class));
/* Dispatch the customized request, taking advantage of the same sendSync behavior we normally
* would.
*/
var response = operation.sendSync(request);
if (response instanceof ShowPetById200 ok) {
assertEquals(new Pet(1234L, "Reginald", null), ok.body());
} else {
fail("Expected a 200 response");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,41 +18,73 @@ public static Source renderAstOperation(AstOperation ast) {
"""
package {{packageName}};
public class {{className}} {
private final io.github.tomboyo.lily.http.UriTemplate uriTemplate;
private final String baseUri;
private final io.github.tomboyo.lily.http.UriTemplate pathTemplate;
private final io.github.tomboyo.lily.http.UriTemplate queryTemplate;
private final java.net.http.HttpClient httpClient;
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
private Query query;
private Path path;
public {{className}}(
String uri,
String baseUri,
java.net.http.HttpClient httpClient,
com.fasterxml.jackson.databind.ObjectMapper objectMapper) {
// We assume uri is non-null and ends with a trailing '/'.
this.uriTemplate = io.github.tomboyo.lily.http.UriTemplate.of(uri + "{{{relativePath}}}{{{queryTemplate}}}");
this.baseUri = baseUri;
this.pathTemplate = io.github.tomboyo.lily.http.UriTemplate.of("{{{pathTemplate}}}");
this.queryTemplate = io.github.tomboyo.lily.http.UriTemplate.of("{{{queryTemplate}}}");
this.httpClient = httpClient;
this.objectMapper = objectMapper;
path = new Path();
query = new Query();
}
{{#urlParameters}}
private {{{fqpt}}} {{name}};
public {{className}} {{name}}({{{fqpt}}} {{name}}) {
this.{{name}} = {{name}};
/** Configure path parameters for this operation, if any. */
public {{className}} path(java.util.function.Function<Path, Path> path) {
this.path = path.apply(this.path);
return this;
}
{{/urlParameters}}
public io.github.tomboyo.lily.http.UriTemplate uriTemplate() {
{{#smartFormEncoder}}
var smartFormEncoder = io.github.tomboyo.lily.http.encoding.Encoders.smartFormExploded(); // stateful
{{/smartFormEncoder}}
{{#urlParameters}}
if (this.{{name}} != null) {
uriTemplate.bind(
"{{apiName}}",
this.{{name}},
{{{encoder}}});
}
{{/urlParameters}}
return uriTemplate;
/** Configure query parameters for this operation, if any. */
public {{className}} query(java.util.function.Function<Query, Query> query) {
this.query = query.apply(this.query);
return this;
}
/** Get the base URI of the service (like {@code "https://example.com/"}). It always
* ends with a trailing slash.
*/
public String baseUri() {
return this.baseUri;
}
/** Get this operation's relative path interpolated with any bound parameters. The
* path is always relative, so it does not start with a "/".
*/
public String pathString() {
{{#pathSmartFormEncoder}}
var smartFormEncoder = io.github.tomboyo.lily.http.encoding.Encoders.smartFormExploded();
{{/pathSmartFormEncoder}}
return this.pathTemplate
{{#pathParameters}}
.bind("{{apiName}}", this.path.{{name}}, {{{encoder}}})
{{/pathParameters}}
.toString();
}
/** Get the query string for this operation and any bound parameters. */
public String queryString() {
{{#querySmartFormEncoder}}
var smartFormEncoder = io.github.tomboyo.lily.http.encoding.Encoders.smartFormExploded();
{{/querySmartFormEncoder}}
return this.queryTemplate
{{#queryParameters}}
.bind("{{apiName}}", this.query.{{name}}, {{{encoder}}})
{{/queryParameters}}
.toString();
}
/**
Expand All @@ -62,7 +94,7 @@ public io.github.tomboyo.lily.http.UriTemplate uriTemplate() {
*/
public java.net.http.HttpRequest httpRequest() {
return java.net.http.HttpRequest.newBuilder()
.uri(uriTemplate().toURI())
.uri(java.net.URI.create(this.baseUri + pathString() + queryString()))
.method("{{method}}", java.net.http.HttpRequest.BodyPublishers.noBody())
.build();
}
Expand All @@ -71,11 +103,48 @@ public java.net.http.HttpRequest httpRequest() {
* Synchronously perform the HTTP request for this operation.
*/
public {{{responseTypeName}}} sendSync() throws java.io.IOException, InterruptedException {
return sendSync(httpRequest());
}
/**
* Synchronously perform the HTTP request for a custom HttpRequest. You will typically
* only use this API when the underlying OpenAPI specification is missing parameters
* or other necessary components. Use the {@link #httpRequest()} method to get a
* template HTTP request from this operation, customize it with
* {@link java.net.http.HttpRequest#newBuilder(java.net.http.HttpRequest, java.util.function.BiPredicate)},
* then use this method to dispatch it.
*/
public {{{responseTypeName}}} sendSync(java.net.http.HttpRequest request)
throws java.io.IOException, InterruptedException {
var httpResponse = this.httpClient.send(
httpRequest(),
request,
java.net.http.HttpResponse.BodyHandlers.ofInputStream());
return {{{responseTypeName}}}.fromHttpResponse(httpResponse, objectMapper);
}
public static class Path {
private Path() {}
{{#pathParameters}}
private {{{fqpt}}} {{name}};
public Path {{name}}({{{fqpt}}} {{name}}) {
this.{{name}} = {{name}};
return this;
}
{{/pathParameters}}
}
public static class Query {
private Query() {}
{{#queryParameters}}
private {{{fqpt}}} {{name}};
public Query {{name}}({{{fqpt}}} {{name}}) {
this.{{name}} = {{name}};
return this;
}
{{/queryParameters}}
}
}
""",
"renderAstOperation",
Expand All @@ -84,27 +153,44 @@ public java.net.http.HttpRequest httpRequest() {
ast.name().packageName(),
"className",
ast.name().typeName(),
"relativePath",
"pathTemplate",
withoutLeadingSlash(ast.relativePath()),
"method",
ast.method(),
"queryTemplate",
ast.parameters().stream()
.filter(parameter -> parameter.location() == QUERY)
.map(parameter -> "{" + parameter.apiName() + "}")
.collect(Collectors.joining("")),
"smartFormEncoder",
ast.parameters().stream().anyMatch(parameter -> parameter.location() == QUERY),
// path and query parameters -- anything in the URL itself
"urlParameters",
"method",
ast.method(),
"pathSmartFormEncoder",
ast.parameters().stream()
.anyMatch(
parameter ->
parameter.location() == PATH && parameter.encoding().style() == FORM),
"querySmartFormEncoder",
ast.parameters().stream()
.anyMatch(
parameter ->
parameter.location() == QUERY && parameter.encoding().style() == FORM),
"pathParameters",
ast.parameters().stream()
.filter(parameter -> parameter.location() == PATH)
.map(
parameter ->
Map.of(
"fqpt", parameter.typeName().toFqpString(),
"name", parameter.name().lowerCamelCase(),
"apiName", parameter.apiName(),
"encoder", getEncoder(parameter.encoding())))
.collect(toList()),
"queryParameters",
ast.parameters().stream()
.filter(
parameter -> parameter.location() == PATH || parameter.location() == QUERY)
.filter(parameter -> parameter.location() == QUERY)
.map(
parameter ->
Map.of(
"fqpt", parameter.typeName().toFqpString(),
"name", "set" + parameter.name().upperCamelCase(),
"name", parameter.name().lowerCamelCase(),
"apiName", parameter.apiName(),
"encoder", getEncoder(parameter.encoding())))
.collect(toList()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class HttpRequestBuildersTest {
public class HttpRequestMethodTest {

private static String packageName;
private static HttpRequest actual;
Expand All @@ -27,21 +27,16 @@ static void beforeAll() throws Exception {
"""
openapi: 3.0.2
paths:
/foo/{id}:
/foo:
get:
operationId: get-foo
post:
operationId: postFoo
parameters:
- name: id
in: path
required: true
schema:
type: string
- name: color
in: query
schema:
type: string
operationId: post-foo
""");
}

@Test
void postFoo() {
actual =
evaluate(
"""
Expand All @@ -50,21 +45,29 @@ static void beforeAll() throws Exception {
.build()
.everyOperation()
.postFoo()
.setId("1234")
.setColor("red")
.httpRequest();
"""
.formatted(packageName),
HttpRequest.class);
}

@Test
void templatesUriWithPathAndQueryParameters() {
assertEquals("https://example.com/foo/1234?color=red", actual.uri().toString());
assertEquals("POST", actual.method());
}

@Test
void templatesHttpMethod() {
assertEquals("POST", actual.method());
void getFoo() {
actual =
evaluate(
"""
return %s.Api.newBuilder()
.uri("https://example.com/")
.build()
.everyOperation()
.getFoo()
.httpRequest();
"""
.formatted(packageName),
HttpRequest.class);

assertEquals("GET", actual.method());
}
}
Loading

0 comments on commit 2b94cc1

Please sign in to comment.