Skip to content

Commit

Permalink
Provide the fluent API for CentralDogma client (#651)
Browse files Browse the repository at this point in the history
Motivation:
Currently, `CentralDogma` client has a bunch of methods that take a bunch of parameters.
It makes it harder to add another optional parameter to the existing methods because we need to add another pair of existing methods. For example:
```java
// Existing methods:
void watch(String a);
void watch(String a, String b);
void watch(String a, String b, String c);

// If we want to add an optional long value then we need another three methods:
void watch(String a, long d);
void watch(String a, String b, long d);
void watch(String a, String b, String c, long d);
```

Modifications:
- Add `PathPattern` interface to pass as the parameter instead of String.
- Add `CentralDogma.forRepo(...)` method that returns `CentralDogmaRepository`.
  - Users can use the instance to send a request:
     ```java
     centralDogma.forRepo("foo", "bar").file("/foo.json").get();
     centralDogma.forRepo("foo", "bar").watch("/foo.json").start();
     centralDogma.forRepo("foo", "bar").watcher(PathPattern.all()).start();
     ```
Result:
- You can now send a request to the Central Dogma server via fluent APIs.
- (Deprecated) Various methods in `CentralDogma` client are deprecated. Use fluent APIs. 

Co-authored-by: Din <[email protected]>
  • Loading branch information
minwoox and di-seo authored Jan 4, 2022
1 parent aa4b789 commit e93bc5a
Show file tree
Hide file tree
Showing 57 changed files with 3,414 additions and 922 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import com.linecorp.centraldogma.common.Markup;
import com.linecorp.centraldogma.common.MergeQuery;
import com.linecorp.centraldogma.common.MergeSource;
import com.linecorp.centraldogma.common.PathPattern;
import com.linecorp.centraldogma.common.PushResult;
import com.linecorp.centraldogma.common.Query;
import com.linecorp.centraldogma.common.Revision;
Expand Down Expand Up @@ -242,7 +243,7 @@ void listFiles() throws Exception {
callback.onComplete(ImmutableList.of(entry));
return null;
}).when(iface).listFiles(anyString(), anyString(), any(), anyString(), any());
assertThat(client.listFiles("project", "repo", new Revision(1), "/a.txt").get())
assertThat(client.listFiles("project", "repo", new Revision(1), PathPattern.of("/a.txt")).get())
.isEqualTo(ImmutableMap.of("/a.txt", EntryType.TEXT));
verify(iface).listFiles(anyString(), anyString(), any(), anyString(), any());
}
Expand All @@ -256,7 +257,7 @@ void getFiles() throws Exception {
callback.onComplete(ImmutableList.of(entry));
return null;
}).when(iface).getFiles(anyString(), anyString(), any(), anyString(), any());
assertThat(client.getFiles("project", "repo", new Revision(1), "path").get())
assertThat(client.getFiles("project", "repo", new Revision(1), PathPattern.of("path")).get())
.isEqualTo(ImmutableMap.of("/b.txt", Entry.ofText(new Revision(1), "/b.txt", "world")));
verify(iface).getFiles(anyString(), anyString(), any(), anyString(), any());
}
Expand All @@ -274,12 +275,13 @@ void getHistory() throws Exception {
ImmutableList.of(new TChange("/a.txt", ChangeType.UPSERT_TEXT).setContent("content")))));
return null;
}).when(iface).getHistory(any(), any(), any(), any(), any(), any());
assertThat(client.getHistory("project", "repo", new Revision(1), new Revision(3), "path").get())
assertThat(client.getHistory("project", "repo", new Revision(1), new Revision(3),
PathPattern.of("path")).get())
.isEqualTo(ImmutableList.of(new Commit(new Revision(1),
new Author("name", "[email protected]"),
Instant.parse(TIMESTAMP).toEpochMilli(),
"summary", "detail", Markup.PLAINTEXT)));
verify(iface).getHistory(eq("project"), eq("repo"), any(), any(), eq("path"), any());
verify(iface).getHistory(eq("project"), eq("repo"), any(), any(), eq("/**/path"), any());
}

@Test
Expand All @@ -291,9 +293,10 @@ void getDiffs() throws Exception {
callback.onComplete(ImmutableList.of(change));
return null;
}).when(iface).getDiffs(any(), any(), any(), any(), any(), any());
assertThat(client.getDiffs("project", "repo", new Revision(1), new Revision(3), "path").get())
assertThat(client.getDiff("project", "repo", new Revision(1), new Revision(3), PathPattern.of("path"))
.get())
.isEqualTo(ImmutableList.of(Change.ofTextUpsert("/a.txt", "content")));
verify(iface).getDiffs(eq("project"), eq("repo"), any(), any(), eq("path"), any());
verify(iface).getDiffs(eq("project"), eq("repo"), any(), any(), eq("/**/path"), any());
}

@Test
Expand Down Expand Up @@ -395,7 +398,8 @@ void watchRepository() throws Exception {
callback.onComplete(new WatchRepositoryResult().setRevision(new TRevision(42)));
return null;
}).when(iface).watchRepository(any(), any(), any(), anyString(), anyLong(), any());
assertThat(client.watchRepository("project", "repo", new Revision(1), "/a.txt", 100).get())
assertThat(client.watchRepository("project", "repo", new Revision(1),
PathPattern.of("/a.txt"), 100, false).get())
.isEqualTo(new Revision(42));
verify(iface).watchRepository(eq("project"), eq("repo"), any(), eq("/a.txt"), eq(100L), any());
}
Expand All @@ -407,7 +411,8 @@ void watchRepositoryTimedOut() throws Exception {
callback.onComplete(new WatchRepositoryResult());
return null;
}).when(iface).watchRepository(any(), any(), any(), anyString(), anyLong(), any());
assertThat(client.watchRepository("project", "repo", new Revision(1), "/a.txt", 100).get())
assertThat(client.watchRepository("project", "repo", new Revision(1),
PathPattern.of("/a.txt"), 100, false).get())
.isNull();
verify(iface).watchRepository(eq("project"), eq("repo"), any(), eq("/a.txt"), eq(100L), any());
}
Expand All @@ -421,7 +426,8 @@ void watchFile() throws Exception {
.setContent("foo"));
return null;
}).when(iface).watchFile(any(), any(), any(), any(), anyLong(), any());
assertThat(client.watchFile("project", "repo", new Revision(1), Query.ofText("/a.txt"), 100).get())
assertThat(client.watchFile("project", "repo", new Revision(1),
Query.ofText("/a.txt"), 100, false).get())
.isEqualTo(Entry.ofText(new Revision(42), "/a.txt", "foo"));
verify(iface).watchFile(eq("project"), eq("repo"), any(), any(), eq(100L), any());
}
Expand All @@ -434,7 +440,7 @@ void watchFileTimedOut() throws Exception {
return null;
}).when(iface).watchFile(any(), any(), any(), any(), anyLong(), any());
assertThat(client.watchFile("project", "repo", new Revision(1),
Query.ofText("/a.txt"), 100).get()).isNull();
Query.ofText("/a.txt"), 100, false).get()).isNull();
verify(iface).watchFile(eq("project"), eq("repo"), any(), any(), eq(100L), any());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.linecorp.centraldogma.internal.Util.unsafeCast;
import static com.linecorp.centraldogma.internal.Util.validatePathPattern;
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.PROJECTS_PREFIX;
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.REMOVED;
import static com.linecorp.centraldogma.internal.api.v1.HttpApiV1Constants.REPOS;
Expand Down Expand Up @@ -52,13 +51,11 @@
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.common.math.IntMath;
import com.google.common.math.LongMath;

import com.linecorp.armeria.client.Clients;
Expand Down Expand Up @@ -91,6 +88,7 @@
import com.linecorp.centraldogma.common.Markup;
import com.linecorp.centraldogma.common.MergeQuery;
import com.linecorp.centraldogma.common.MergedEntry;
import com.linecorp.centraldogma.common.PathPattern;
import com.linecorp.centraldogma.common.ProjectExistsException;
import com.linecorp.centraldogma.common.ProjectNotFoundException;
import com.linecorp.centraldogma.common.PushResult;
Expand Down Expand Up @@ -398,19 +396,14 @@ private static Revision normalizeRevision(AggregatedHttpResponse res) {

@Override
public CompletableFuture<Map<String, EntryType>> listFiles(String projectName, String repositoryName,
Revision revision, String pathPattern) {
Revision revision, PathPattern pathPattern) {
try {
validateProjectAndRepositoryName(projectName, repositoryName);
requireNonNull(revision, "revision");
validatePathPattern(pathPattern, "pathPattern");
requireNonNull(pathPattern, "pathPattern");

final StringBuilder path = pathBuilder(projectName, repositoryName);
path.append("/list");
if (pathPattern.charAt(0) != '/') {
path.append("/**/");
}
path.append(encodePathPattern(pathPattern));
path.append("?revision=").append(revision.major());
path.append("/list").append(pathPattern.encoded()).append("?revision=").append(revision.major());

return client.execute(headers(HttpMethod.GET, path.toString()))
.aggregate()
Expand Down Expand Up @@ -471,21 +464,19 @@ private static <T> Entry<T> getFile(Revision normRev, AggregatedHttpResponse res

@Override
public CompletableFuture<Map<String, Entry<?>>> getFiles(String projectName, String repositoryName,
Revision revision, String pathPattern) {
Revision revision, PathPattern pathPattern) {
try {
validateProjectAndRepositoryName(projectName, repositoryName);
requireNonNull(revision, "revision");
validatePathPattern(pathPattern, "pathPattern");
requireNonNull(pathPattern, "pathPattern");

// TODO(trustin) No need to normalize a revision once server response contains it.
return maybeNormalizeRevision(projectName, repositoryName, revision).thenCompose(normRev -> {
final StringBuilder path = pathBuilder(projectName, repositoryName);
path.append("/contents");
if (pathPattern.charAt(0) != '/') {
path.append("/**/");
}
path.append(encodePathPattern(pathPattern));
path.append("?revision=").append(normRev.major());
path.append("/contents")
.append(pathPattern.encoded())
.append("?revision=")
.append(normRev.major());

return client.execute(headers(HttpMethod.GET, path.toString()))
.aggregate()
Expand Down Expand Up @@ -592,17 +583,17 @@ private static <T> MergedEntry<T> mergeFiles(AggregatedHttpResponse res) {
@Override
public CompletableFuture<List<Commit>> getHistory(String projectName, String repositoryName,
Revision from, Revision to,
String pathPattern) {
PathPattern pathPattern) {
try {
validateProjectAndRepositoryName(projectName, repositoryName);
requireNonNull(from, "from");
requireNonNull(to, "to");
validatePathPattern(pathPattern, "pathPattern");
requireNonNull(pathPattern, "pathPattern");

final StringBuilder path = pathBuilder(projectName, repositoryName);
path.append("/commits/").append(from.text());
path.append("?to=").append(to.text());
path.append("&path=").append(encodeParam(pathPattern));
path.append("&path=").append(pathPattern.encoded());

return client.execute(headers(HttpMethod.GET, path.toString()))
.aggregate()
Expand Down Expand Up @@ -668,17 +659,17 @@ private static <T> Change<T> getDiff(AggregatedHttpResponse res) {
}

@Override
public CompletableFuture<List<Change<?>>> getDiffs(String projectName, String repositoryName, Revision from,
Revision to, String pathPattern) {
public CompletableFuture<List<Change<?>>> getDiff(String projectName, String repositoryName, Revision from,
Revision to, PathPattern pathPattern) {
try {
validateProjectAndRepositoryName(projectName, repositoryName);
requireNonNull(from, "from");
requireNonNull(to, "to");
validatePathPattern(pathPattern, "pathPattern");
requireNonNull(pathPattern, "pathPattern");

final StringBuilder path = pathBuilder(projectName, repositoryName);
path.append("/compare");
path.append("?pathPattern=").append(encodeParam(pathPattern));
path.append("?pathPattern=").append(pathPattern.encoded());
path.append("&from=").append(from.text());
path.append("&to=").append(to.text());

Expand Down Expand Up @@ -802,23 +793,19 @@ public CompletableFuture<PushResult> push(String projectName, String repositoryN

@Override
public CompletableFuture<Revision> watchRepository(String projectName, String repositoryName,
Revision lastKnownRevision, String pathPattern,
long timeoutMillis) {
Revision lastKnownRevision, PathPattern pathPattern,
long timeoutMillis, boolean errorOnEntryNotFound) {
try {
validateProjectAndRepositoryName(projectName, repositoryName);
requireNonNull(lastKnownRevision, "lastKnownRevision");
validatePathPattern(pathPattern, "pathPattern");
requireNonNull(pathPattern, "pathPattern");
checkArgument(timeoutMillis > 0, "timeoutMillis: %s (expected: > 0)", timeoutMillis);

final StringBuilder path = pathBuilder(projectName, repositoryName);
path.append("/contents");
if (pathPattern.charAt(0) != '/') {
path.append("/**/");
}
path.append(encodePathPattern(pathPattern));
path.append("/contents").append(pathPattern.encoded());

return watch(lastKnownRevision, timeoutMillis, path.toString(), QueryType.IDENTITY,
ArmeriaCentralDogma::watchRepository);
ArmeriaCentralDogma::watchRepository, errorOnEntryNotFound);
} catch (Exception e) {
return exceptionallyCompletedFuture(e);
}
Expand All @@ -840,7 +827,7 @@ private static Revision watchRepository(AggregatedHttpResponse res, QueryType un
@Override
public <T> CompletableFuture<Entry<T>> watchFile(String projectName, String repositoryName,
Revision lastKnownRevision, Query<T> query,
long timeoutMillis) {
long timeoutMillis, boolean errorOnEntryNotFound) {
try {
validateProjectAndRepositoryName(projectName, repositoryName);
requireNonNull(lastKnownRevision, "lastKnownRevision");
Expand All @@ -859,7 +846,7 @@ public <T> CompletableFuture<Entry<T>> watchFile(String projectName, String repo
}

return watch(lastKnownRevision, timeoutMillis, path.toString(), query.type(),
ArmeriaCentralDogma::watchFile);
ArmeriaCentralDogma::watchFile, errorOnEntryNotFound);
} catch (Exception e) {
return exceptionallyCompletedFuture(e);
}
Expand All @@ -881,10 +868,12 @@ private static <T> Entry<T> watchFile(AggregatedHttpResponse res, QueryType quer

private <T> CompletableFuture<T> watch(Revision lastKnownRevision, long timeoutMillis,
String path, QueryType queryType,
BiFunction<AggregatedHttpResponse, QueryType, T> func) {
BiFunction<AggregatedHttpResponse, QueryType, T> func,
boolean errorOnEntryNotFound) {
final RequestHeadersBuilder builder = headersBuilder(HttpMethod.GET, path);
builder.set(HttpHeaderNames.IF_NONE_MATCH, lastKnownRevision.text())
.set(HttpHeaderNames.PREFER, "wait=" + LongMath.saturatedAdd(timeoutMillis, 999) / 1000L);
.set(HttpHeaderNames.PREFER, "wait=" + LongMath.saturatedAdd(timeoutMillis, 999) / 1000L +
", notify-entry-not-found=" + errorOnEntryNotFound);

try (SafeCloseable ignored = Clients.withContextCustomizer(ctx -> {
final long responseTimeoutMillis = ctx.responseTimeoutMillis();
Expand Down Expand Up @@ -968,31 +957,6 @@ private static String encodeParam(String param) {
}
}

@VisibleForTesting
static String encodePathPattern(String pathPattern) {
// We do not need full escaping because we validated the path pattern already and thus contains only
// -, ' ', /, *, _, ., ',', a-z, A-Z, 0-9.
// See Util.isValidPathPattern() for more information.
int spacePos = pathPattern.indexOf(' ');
if (spacePos < 0) {
return pathPattern;
}

final StringBuilder buf = new StringBuilder(IntMath.saturatedMultiply(pathPattern.length(), 2));
for (int pos = 0;;) {
buf.append(pathPattern, pos, spacePos);
buf.append("%20");
pos = spacePos + 1;
spacePos = pathPattern.indexOf(' ', pos);
if (spacePos < 0) {
buf.append(pathPattern, pos, pathPattern.length());
break;
}
}

return buf.toString();
}

/**
* Encodes the specified {@link JsonNode} into a byte array.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ public static <T> CentralDogmaEndpointGroup<T> of(CentralDogma centralDogma,
String projectName, String repositoryName,
Query<T> query,
EndpointListDecoder<T> endpointListDecoder) {
return ofWatcher(centralDogma.fileWatcher(projectName, repositoryName, query), endpointListDecoder);
return ofWatcher(centralDogma.forRepo(projectName, repositoryName)
.watcher(query)
.start(),
endpointListDecoder);
}

/**
Expand Down
Loading

0 comments on commit e93bc5a

Please sign in to comment.