Skip to content

Commit 49e6209

Browse files
committed
Support for YAML
1 parent 170d425 commit 49e6209

34 files changed

Lines changed: 1206 additions & 137 deletions

File tree

client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
import com.linecorp.centraldogma.internal.HistoryConstants;
118118
import com.linecorp.centraldogma.internal.Jackson;
119119
import com.linecorp.centraldogma.internal.Util;
120+
import com.linecorp.centraldogma.internal.Yaml;
120121
import com.linecorp.centraldogma.internal.api.v1.WatchTimeout;
121122

122123
import io.micrometer.core.instrument.MeterRegistry;
@@ -179,7 +180,8 @@ public CompletableFuture<Void> whenEndpointReady() {
179180
return whenReady;
180181
}
181182

182-
return client.endpointGroup().whenReady().thenRun(() -> {});
183+
return client.endpointGroup().whenReady().thenRun(() -> {
184+
});
183185
}
184186

185187
@Override
@@ -1062,25 +1064,36 @@ private static String toString(AggregatedHttpResponse res) {
10621064
private static <T> Entry<T> toEntry(Revision revision, JsonNode node, QueryType queryType,
10631065
boolean viewRaw) {
10641066
final String entryPath = getField(node, "path").asText();
1065-
final EntryType receivedEntryType = EntryType.valueOf(getField(node, "type").asText());
1067+
final EntryType receivedEntryType = EntryType.guessFromPath(entryPath);
10661068
switch (queryType) {
10671069
case IDENTITY_TEXT:
10681070
return entryAsText(revision, node, entryPath, viewRaw);
10691071
case IDENTITY_JSON:
10701072
case JSON_PATH:
1071-
if (receivedEntryType != EntryType.JSON) {
1072-
throw new CentralDogmaException("invalid entry type. entry type: " + receivedEntryType +
1073-
" (expected: " + queryType + ')');
1073+
switch (receivedEntryType) {
1074+
case JSON:
1075+
return entryAsJson(revision, node, entryPath, viewRaw);
1076+
case YAML:
1077+
return entryAsYaml(revision, node, entryPath, viewRaw);
1078+
default:
1079+
throw new CentralDogmaException("invalid entry type. entry type: " + receivedEntryType +
1080+
" (expected: " + queryType + ')');
10741081
}
1075-
return entryAsJson(revision, node, entryPath, viewRaw);
1082+
case IDENTITY_YAML:
1083+
return entryAsYaml(revision, node, entryPath, viewRaw);
10761084
case IDENTITY:
10771085
switch (receivedEntryType) {
10781086
case JSON:
10791087
return entryAsJson(revision, node, entryPath, viewRaw);
1088+
case YAML:
1089+
return entryAsYaml(revision, node, entryPath, viewRaw);
10801090
case TEXT:
10811091
return entryAsText(revision, node, entryPath, viewRaw);
10821092
case DIRECTORY:
10831093
return unsafeCast(Entry.ofDirectory(revision, entryPath));
1094+
default:
1095+
throw new CentralDogmaException("invalid entry type. entry type: " + receivedEntryType +
1096+
" (expected: " + queryType + ')');
10841097
}
10851098
}
10861099
throw new Error(); // Should never reach here.
@@ -1126,6 +1139,36 @@ private static <T> Entry<T> entryAsJson(Revision revision, JsonNode node, String
11261139
return unsafeCast(Entry.ofJson(revision, entryPath, getField(node, "content")));
11271140
}
11281141

1142+
private static <T> Entry<T> entryAsYaml(Revision revision, JsonNode node, String entryPath,
1143+
boolean viewRaw) {
1144+
if (viewRaw) {
1145+
final JsonNode rawContent = node.get("rawContent");
1146+
if (rawContent != null) {
1147+
try {
1148+
return unsafeCast(Entry.ofYaml(revision, entryPath, rawContent.asText()));
1149+
} catch (JsonParseException e) {
1150+
// Should never reach here as the raw JSON text was already validated by the server.
1151+
throw new IllegalStateException(e);
1152+
}
1153+
}
1154+
// Should never reach here as the server already supports YAML entries.
1155+
throw new IllegalStateException("The server does not provide rawContent for YAML entry: " +
1156+
entryPath);
1157+
}
1158+
final JsonNode content = getField(node, "content");
1159+
if (content.isTextual()) {
1160+
// For backward compatibility, the server might return the YAML content as a string.
1161+
try {
1162+
final JsonNode jsonNode = Yaml.readTree(content.asText());
1163+
return unsafeCast(Entry.ofYaml(revision, entryPath, jsonNode));
1164+
} catch (JsonParseException e) {
1165+
// Should never reach here as the YAML content was already validated by the server.
1166+
throw new IllegalStateException(e);
1167+
}
1168+
}
1169+
return unsafeCast(Entry.ofYaml(revision, entryPath, content));
1170+
}
1171+
11291172
private static Commit toCommit(JsonNode node) {
11301173
final Revision revision = new Revision(getField(node, "revision").asInt());
11311174
final JsonNode authorNode = getField(node, "author");

common-legacy/src/main/java/com/linecorp/centraldogma/internal/thrift/EntryConverter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public static Entry convert(com.linecorp.centraldogma.common.Entry<?> entry) {
4646
case TEXT:
4747
file.setContent((String) entry.content());
4848
break;
49+
case YAML:
50+
file.setContent(entry.rawContent());
51+
break;
4952
case DIRECTORY:
5053
break;
5154
default:

common/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ dependencies {
77
implementation libs.jackson.core
88
implementation libs.jackson.databind
99
implementation libs.jackson.datatype.jsr310
10+
// Jackson YAML
11+
implementation libs.jackson.dataformat.yaml
1012

1113
// Guava
1214
implementation libs.guava

common/src/main/java/com/linecorp/centraldogma/common/Change.java

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ static Change<String> ofTextUpsert(String path, String text) {
8787
*/
8888
static Change<JsonNode> ofJsonUpsert(String path, String jsonText) {
8989
requireNonNull(jsonText, "jsonText");
90-
return new JsonChange(path, ChangeType.UPSERT_JSON, jsonText);
90+
return new StructuredChange(path, ChangeType.UPSERT_JSON, jsonText);
9191
}
9292

9393
/**
@@ -98,7 +98,18 @@ static Change<JsonNode> ofJsonUpsert(String path, String jsonText) {
9898
*/
9999
static Change<JsonNode> ofJsonUpsert(String path, JsonNode jsonNode) {
100100
requireNonNull(jsonNode, "jsonNode");
101-
return new JsonChange(path, ChangeType.UPSERT_JSON, jsonNode);
101+
return new StructuredChange(path, ChangeType.UPSERT_JSON, jsonNode);
102+
}
103+
104+
/**
105+
* Returns a newly-created {@link Change} whose type is {@link ChangeType#UPSERT_YAML}.
106+
*
107+
* @param path the path of the file
108+
* @param yamlText the content of the file
109+
*/
110+
static Change<JsonNode> ofYamlUpsert(String path, String yamlText) {
111+
requireNonNull(yamlText, "yamlText");
112+
return new StructuredChange(path, ChangeType.UPSERT_YAML, yamlText);
102113
}
103114

104115
/**
@@ -192,14 +203,15 @@ static Change<JsonNode> ofJsonPatch(String path, @Nullable String oldJsonText, S
192203
final JsonNode newJsonNode;
193204
try {
194205
oldJsonNode = oldJsonText == null ? Jackson.nullNode
195-
: Jackson.readTree(oldJsonText);
196-
newJsonNode = Jackson.readTree(newJsonText);
206+
: Jackson.readTree(path, oldJsonText);
207+
newJsonNode = Jackson.readTree(path, newJsonText);
197208
} catch (IOException e) {
198209
throw new ChangeFormatException("failed to read a value as a JSON tree", e);
199210
}
200211

201-
return new JsonChange(path, ChangeType.APPLY_JSON_PATCH,
202-
JsonPatch.generate(oldJsonNode, newJsonNode, ReplaceMode.SAFE).toJson());
212+
return new StructuredChange(path, ChangeType.APPLY_JSON_PATCH,
213+
JsonPatch.generate(oldJsonNode, newJsonNode, ReplaceMode.SAFE)
214+
.toJson());
203215
}
204216

205217
/**
@@ -216,24 +228,33 @@ static Change<JsonNode> ofJsonPatch(String path, @Nullable JsonNode oldJsonNode,
216228
oldJsonNode = Jackson.nullNode;
217229
}
218230

219-
return new JsonChange(path, ChangeType.APPLY_JSON_PATCH,
220-
JsonPatch.generate(oldJsonNode, newJsonNode, ReplaceMode.SAFE).toJson());
231+
return new StructuredChange(path, ChangeType.APPLY_JSON_PATCH,
232+
JsonPatch.generate(oldJsonNode, newJsonNode, ReplaceMode.SAFE)
233+
.toJson());
221234
}
222235

223236
/**
224237
* Returns a newly-created {@link Change} whose type is {@link ChangeType#APPLY_JSON_PATCH}.
238+
* The JSON patch operation can be applied to JSON, JSON5, YAML files.
239+
*
240+
* <p>Note that the JSON patch operation normalizes the original data before applying the patch,
241+
* so contextual information such as comments cannot be retained when the patch is applied.
225242
*
226243
* @param path the path of the file
227244
* @param jsonPatch the patch in <a href="https://datatracker.ietf.org/doc/html/rfc6902">JSON patch format</a>
228245
*/
229246
static Change<JsonNode> ofJsonPatch(String path, JsonPatchOperation jsonPatch) {
230247
requireNonNull(path, "path");
231248
requireNonNull(jsonPatch, "jsonPatch");
232-
return new JsonChange(path, ChangeType.APPLY_JSON_PATCH, jsonPatch.toJsonNode());
249+
return new StructuredChange(path, ChangeType.APPLY_JSON_PATCH, jsonPatch.toJsonNode());
233250
}
234251

235252
/**
236253
* Returns a newly-created {@link Change} whose type is {@link ChangeType#APPLY_JSON_PATCH}.
254+
* The JSON patch operation can be applied to JSON, JSON5, YAML files.
255+
*
256+
* <p>Note that the JSON patch operation normalizes the original data before applying the patch,
257+
* so contextual information such as comments cannot be retained when the patch is applied.
237258
*
238259
* @param path the path of the file
239260
* @param jsonPatches the list of patches in <a href="https://datatracker.ietf.org/doc/html/rfc6902">JSON patch format</a>
@@ -245,6 +266,10 @@ static Change<JsonNode> ofJsonPatch(String path, JsonPatchOperation... jsonPatch
245266

246267
/**
247268
* Returns a newly-created {@link Change} whose type is {@link ChangeType#APPLY_JSON_PATCH}.
269+
* The JSON patch operation can be applied to JSON, JSON5, YAML files.
270+
*
271+
* <p>Note that the JSON patch operation normalizes the original data before applying the patch,
272+
* so contextual information such as comments cannot be retained when the patch is applied.
248273
*
249274
* @param path the path of the file
250275
* @param jsonPatches the list of patches in <a href="https://datatracker.ietf.org/doc/html/rfc6902">JSON patch format</a>
@@ -253,12 +278,16 @@ static Change<JsonNode> ofJsonPatch(String path, Iterable<? extends JsonPatchOpe
253278
requireNonNull(path, "path");
254279
requireNonNull(jsonPatches, "jsonPatches");
255280
checkArgument(!Iterables.isEmpty(jsonPatches), "jsonPatches cannot be empty");
256-
return new JsonChange(path, ChangeType.APPLY_JSON_PATCH,
257-
JsonPatchOperation.asJsonArray(jsonPatches));
281+
return new StructuredChange(path, ChangeType.APPLY_JSON_PATCH,
282+
JsonPatchOperation.asJsonArray(jsonPatches));
258283
}
259284

260285
/**
261286
* Returns a newly-created {@link Change} whose type is {@link ChangeType#APPLY_JSON_PATCH}.
287+
* The JSON patch operation can be applied to JSON, JSON5, YAML files.
288+
*
289+
* <p>Note that the JSON patch operation normalizes the original data before applying the patch,
290+
* so contextual information such as comments cannot be retained when the patch is applied.
262291
*
263292
* @param path the path of the file
264293
* @param jsonPatchText the patch in <a href="https://datatracker.ietf.org/doc/html/rfc6902">JSON patch format</a>
@@ -280,14 +309,18 @@ static Change<JsonNode> ofJsonPatch(String path, String jsonPatchText) {
280309

281310
/**
282311
* Returns a newly-created {@link Change} whose type is {@link ChangeType#APPLY_JSON_PATCH}.
312+
* The JSON patch operation can be applied to JSON, JSON5, YAML files.
313+
*
314+
* <p>Note that the JSON patch operation normalizes the original data before applying the patch,
315+
* so contextual information such as comments cannot be retained when the patch is applied.
283316
*
284317
* @param path the path of the file
285318
* @param jsonPatchNode the patch in <a href="https://datatracker.ietf.org/doc/html/rfc6902">JSON patch format</a>
286319
*/
287320
static Change<JsonNode> ofJsonPatch(String path, JsonNode jsonPatchNode) {
288321
requireNonNull(jsonPatchNode, "jsonPatchNode");
289322

290-
return new JsonChange(path, ChangeType.APPLY_JSON_PATCH, jsonPatchNode);
323+
return new StructuredChange(path, ChangeType.APPLY_JSON_PATCH, jsonPatchNode);
291324
}
292325

293326
/**

common/src/main/java/com/linecorp/centraldogma/common/ChangeDeserializer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ private static Change<?> deserialize(ChangeType type, String path, @Nullable Jso
108108
result = Change.ofTextUpsert(path, content.asText());
109109
}
110110
break;
111+
case UPSERT_YAML:
112+
if (rawContent == null) {
113+
throw new IllegalArgumentException("rawContent is required for YAML_UPSERT");
114+
}
115+
result = Change.ofYamlUpsert(path, rawContent);
116+
break;
111117
case RENAME:
112118
assert content != null;
113119
result = Change.ofRename(path, content.asText());

common/src/main/java/com/linecorp/centraldogma/common/ChangeType.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ public enum ChangeType {
3232
*/
3333
UPSERT_JSON(JsonNode.class),
3434

35+
/**
36+
* Adds a new YAML file or replaces an existing file. {@link Change#content()} will return
37+
* the {@link JsonNode} that represents the content of the file.
38+
*/
39+
UPSERT_YAML(JsonNode.class),
40+
3541
/**
3642
* Adds a new text file or replaces an existing file. {@link Change#content()} will return
3743
* the {@link String} that represents the content of the file.

common/src/main/java/com/linecorp/centraldogma/common/ContentHolder.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.fasterxml.jackson.databind.JsonNode;
2424

2525
import com.linecorp.centraldogma.internal.Jackson;
26+
import com.linecorp.centraldogma.internal.Yaml;
2627

2728
/**
2829
* A holder which has the content and its {@link EntryType}.
@@ -52,7 +53,11 @@ default String contentAsText() {
5253
final T content = content();
5354
if (content instanceof JsonNode) {
5455
try {
55-
return Jackson.writeValueAsString(content);
56+
if (type() == EntryType.YAML) {
57+
return Yaml.writeValueAsString(content);
58+
} else {
59+
return Jackson.writeValueAsString(content);
60+
}
5661
} catch (JsonProcessingException e) {
5762
// Should never happen because it's a JSON tree already.
5863
throw new Error(e);
@@ -72,7 +77,11 @@ default String contentAsPrettyText() {
7277
final T content = content();
7378
if (content instanceof TreeNode) {
7479
try {
75-
return Jackson.writeValueAsPrettyString(content);
80+
if (type() == EntryType.YAML) {
81+
return Yaml.writeValueAsString(content);
82+
} else {
83+
return Jackson.writeValueAsPrettyString(content);
84+
}
7685
} catch (JsonProcessingException e) {
7786
// Should never happen because it's a JSON tree already.
7887
throw new Error(e);
@@ -96,7 +105,11 @@ default JsonNode contentAsJson() throws JsonParseException {
96105
return (JsonNode) content;
97106
}
98107

99-
return Jackson.readTree(contentAsText());
108+
if (type() == EntryType.YAML) {
109+
return Yaml.readTree(contentAsText());
110+
} else {
111+
return Jackson.readTree(contentAsText());
112+
}
100113
}
101114

102115
/**

common/src/main/java/com/linecorp/centraldogma/common/Entry.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
import com.linecorp.centraldogma.internal.Jackson;
3333
import com.linecorp.centraldogma.internal.Json5;
34+
import com.linecorp.centraldogma.internal.Yaml;
3435

3536
/**
3637
* A file or a directory in a repository.
@@ -80,6 +81,29 @@ public static Entry<JsonNode> ofJson(Revision revision, String path, String cont
8081
return new Entry<>(revision, path, EntryType.JSON, jsonNode, content);
8182
}
8283

84+
/**
85+
* Returns a newly-created {@link Entry} of a YAML file with the given content.
86+
* @param revision the revision of the YAML file
87+
* @param path the path of the YAML file
88+
* @param content the content of the YAML file
89+
*/
90+
public static Entry<JsonNode> ofYaml(Revision revision, String path, JsonNode content) {
91+
return new Entry<>(revision, path, EntryType.YAML, content, null);
92+
}
93+
94+
/**
95+
* Returns a newly-created {@link Entry} of a YAML file.
96+
* @param revision the revision of the YAML file
97+
* @param path the path of the YAML file
98+
* @param content the content of the YAML file
99+
* @throws JsonParseException if the {@code content} is not a valid YAML
100+
*/
101+
public static Entry<JsonNode> ofYaml(Revision revision, String path, String content)
102+
throws JsonParseException {
103+
final JsonNode jsonNode = Yaml.readTree(content);
104+
return new Entry<>(revision, path, EntryType.YAML, jsonNode, content);
105+
}
106+
83107
/**
84108
* Returns a newly-created {@link Entry} of a text file.
85109
*
@@ -223,9 +247,11 @@ public JsonNode contentAsJson() throws JsonParseException {
223247
if (content instanceof JsonNode) {
224248
return (JsonNode) content;
225249
}
226-
if (isJson5(path) && rawContent != null) {
227-
return Json5.readTree(rawContent);
250+
251+
if (rawContent != null) {
252+
return Jackson.readTree(path, rawContent);
228253
}
254+
229255
return ContentHolder.super.contentAsJson();
230256
}
231257

0 commit comments

Comments
 (0)