Skip to content

Commit

Permalink
Issues/19 (#23)
Browse files Browse the repository at this point in the history
Add uriTemplate support to the generated api
  • Loading branch information
Tomboyo authored Aug 28, 2022
1 parent ecfa059 commit 2be1f56
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 35 deletions.
5 changes: 3 additions & 2 deletions modules/example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@
<version>${revision}</version>
<configuration>
<uri>https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml</uri>
<!-- Uncomment to customize the default generated sources directory. -->
<!-- <outputDir>target/generated-sources</outputDir> -->
<basePackage>com.example</basePackage>
<basePackage>io.github.tomboyo.lily.example</basePackage>
</configuration>
<executions>
<execution>
Expand Down Expand Up @@ -94,7 +95,7 @@
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.33.1</version>
<version>2.33.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package io.github.tomboyo.lily.example;

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 org.junit.jupiter.api.Assertions.assertEquals;

import com.fasterxml.jackson.core.type.TypeReference;
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.http.JacksonBodyHandler;
import io.github.tomboyo.lily.http.encoding.Encoding;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import org.junit.jupiter.api.Test;

@WireMockTest
public class Example {

@Test
void example(WireMockRuntimeInfo info) throws Exception {
/* This wiremock stub imitates the petstore yaml's showPetById operation. */
stubFor(
get("/foo/pets/1234")
.willReturn(ok("""
{"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.
*
* All operations are organized by their tags, if any, like: `petsOperations()` (for the 'pets' tag),
* `otherOperations()` (for operations without a tag), and `allOperations()` (every operation is addressable from
* here, even if already addressable from another tag).
*/
var operation =
Api.newBuilder().uri(info.getHttpBaseUrl()).build().petsOperations().showPetById();

/*
* 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();

/*
* 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!
*/
var client = HttpClient.newBuilder().build();
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());
}
}
7 changes: 7 additions & 0 deletions modules/lily-compiler/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<!-- Added to the class path for test source compilation (see CompilerSupport) -->
<groupId>io.github.tomboyo.lily</groupId>
<artifactId>lily-http</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
package io.github.tomboyo.lily.compiler.ast;

/** An operation, such as "createNewBlogPost" corresponding to an OAS operation. */
public record AstOperation(String operationName, AstReference operationClass) implements Ast {}
public record AstOperation(String operationName, AstReference operationClass, String relativePath)
implements Ast {}
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,36 @@ private Source renderAstAPi(AstApi ast) {
"""
package {{packageName}};
public class {{className}} {
private final String uri;
private {{className}}(String uri) {
this.uri = java.util.Objects.requireNonNull(uri);
}
public static {{className}}Builder newBuilder() {
return new {{className}}Builder();
}
{{#tags}}
{{! Note: Tag types are never parameterized }}
public {{fqReturnType}} {{methodName}}() {
return new {{fqReturnType}}();
return new {{fqReturnType}}(uri);
}
{{/tags}}
public static class {{className}}Builder {
private String uri;
private {{className}}Builder() {}
public {{className}}Builder uri(String uri) { this.uri = uri; return this; }
public {{className}} build() {
return new {{className}}(uri);
}
}
}
""",
"renderAstApi",
Expand All @@ -157,10 +180,16 @@ private Source renderAstTaggedOperations(AstTaggedOperations ast) {
package {{packageName}};
public class {{className}} {
private final String uri;
public {{className}}(String uri) {
this.uri = uri;
}
{{#operations}}
{{! Note: Operation types are never parameterized }}
public {{fqReturnType}} {{methodName}}() {
return new {{fqReturnType}}();
return new {{fqReturnType}}(uri);
}
{{/operations}}
Expand Down Expand Up @@ -188,15 +217,22 @@ private Source renderAstOperation(AstOperation ast) {
"""
package {{packageName}};
public class {{className}} {
public java.net.http.HttpRequest request() {
return java.net.http.HttpRequest.newBuilder().build();
private final io.github.tomboyo.lily.http.UriTemplate uriTemplate;
public {{className}}(String uri) {
this.uriTemplate = io.github.tomboyo.lily.http.UriTemplate.forPath(uri, "{{{relativePath}}}");
}
public io.github.tomboyo.lily.http.UriTemplate uriTemplate() {
return uriTemplate;
}
}
""",
"renderAstOperation",
Map.of(
"packageName", ast.operationClass().packageName(),
"className", Support.capitalCamelCase(ast.operationClass().name())));
"className", Support.capitalCamelCase(ast.operationClass().name()),
"relativePath", ast.relativePath()));

return sourceForFqn(ast.operationClass(), content);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ private Stream<Ast> evaluatePaths(OpenAPI openAPI) {
.stream()
.flatMap(
entry -> {
var relativePath = entry.getKey();
var pathItem = entry.getValue();
return OasPathsToAst.evaluatePathItem(basePackage, pathItem);
return OasPathsToAst.evaluatePathItem(basePackage, relativePath, pathItem);
})
.collect(Collectors.toSet());
var taggedOperations =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ public static Stream<AstTaggedOperations> evaluateTaggedOperations(
* Evaluate a single PathItem (and its operations, nested schemas, etc) to AST. Returns one
* EvaluatedOperation per operation in the PathItem.
*/
public static Stream<EvaluatedOperation> evaluatePathItem(String basePackage, PathItem pathItem) {
return new OasPathsToAst(basePackage).evaluatePathItem(pathItem);
public static Stream<EvaluatedOperation> evaluatePathItem(
String basePackage, String relativePath, PathItem pathItem) {
return new OasPathsToAst(basePackage).evaluatePathItem(relativePath, pathItem);
}

private Stream<AstTaggedOperations> evaluateTaggedOperations(
Expand All @@ -72,18 +73,18 @@ private Stream<AstTaggedOperations> evaluateTaggedOperations(
basePackage, entry.getKey() + "Operations", entry.getValue()));
}

private Stream<EvaluatedOperation> evaluatePathItem(PathItem pathItem) {
private Stream<EvaluatedOperation> evaluatePathItem(String relativePath, PathItem pathItem) {
var inheritedParameters = requireNonNullElse(pathItem.getParameters(), List.<Parameter>of());
return pathItem.readOperationsMap().entrySet().stream()
.map(
entry -> {
var operation = entry.getValue();
return evaluateOperation(operation, inheritedParameters);
return evaluateOperation(operation, relativePath, inheritedParameters);
});
}

private EvaluatedOperation evaluateOperation(
Operation operation, List<Parameter> inheritedParameters) {
Operation operation, String relativePath, List<Parameter> inheritedParameters) {
var operationName = requireNonNull(operation.getOperationId()) + "Operation";
var subordinatePackageName = joinPackages(basePackage, operationName);
var ownParameters = requireNonNullElse(operation.getParameters(), List.<Parameter>of());
Expand All @@ -96,7 +97,8 @@ private EvaluatedOperation evaluateOperation(
getOperationTags(operation),
new AstOperation(
operation.getOperationId(),
new AstReference(basePackage, operationName, List.of(), false)),
new AstReference(basePackage, operationName, List.of(), false),
relativePath),
ast);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;
import javax.tools.ToolProvider;
Expand Down Expand Up @@ -105,7 +109,8 @@ private static void compileJavaSources(Path classesDir, Collection<Path> sourceP
null,
fileManager,
listener,
List.of("-d", classesDir.toString()),
List.of(
"-d", classesDir.toString(), "-classpath", System.getProperty("java.class.path")),
null,
compilationUnits)
.call();
Expand All @@ -114,16 +119,33 @@ private static void compileJavaSources(Path classesDir, Collection<Path> sourceP
.findFirst()
.ifPresent(
(it) -> {
var source = readAll(it.getSource());
var affectedLines =
getLinesAround(source, it.getStartPosition(), it.getEndPosition());
throw new RuntimeException(
"Compilation error: code=%s kind=%s pos=%d startPosition=%d endPosition=%d source=%s\n%s%n"
.formatted(
it.getCode(),
it.getKind(),
it.getPosition(),
it.getStartPosition(),
it.getEndPosition(),
it.getSource(),
it.getMessage(null)));
"Compilation error in %s:%n%n%s%n```%n%s%n```%n"
.formatted(it.getSource(), it.getMessage(null), affectedLines));
});
}

private static String readAll(JavaFileObject o) {
try (var raw = o.openInputStream();
var reader = new InputStreamReader(raw);
var buffered = new BufferedReader(reader)) {
return buffered.lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private static String getLinesAround(String source, long fromLong, long toLong) {
int from = (int) fromLong;
int to = (int) toLong;

// Move pointers to the beginning and end of their respective lines if they aren't already there
from = source.lastIndexOf('\n', from);
to = source.indexOf('\n', to);

return source.substring(from, to).trim();
}
}
Loading

0 comments on commit 2be1f56

Please sign in to comment.