Skip to content

Commit

Permalink
Add support for mapping merge key << (#136)
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Edgar <[email protected]>
  • Loading branch information
MikeEdgar authored Feb 16, 2024
1 parent a1d18c8 commit 1fcf9d3
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 13 deletions.
66 changes: 53 additions & 13 deletions src/main/java/io/xlate/yamljson/YamlParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,23 @@ enum NumberType {

private static final Logger LOGGER = Logger.getLogger(YamlParser.class.getName());

static final String MERGE_KEY = "<<";
static final String STREAM_START = "StreamStart";
static final String STREAM_END = "StreamEnd";
static final String DOCUMENT_START = "DocumentStart";
static final String DOCUMENT_END = "DocumentEnd";
static final String MAPPING_START = "MappingStart";
static final String MAPPING_END = "MappingEnd";
static final String SEQUENCE_START = "SequenceStart";
static final String SEQUENCE_END = "SequenceEnd";
static final String SCALAR = "Scalar";
static final String ALIAS = "Alias";
static final String COMMENT = "Comment";

static final List<String> MAPPING_BOUNDARIES = List.of(MAPPING_START, MAPPING_END);
static final String MSG_EXCEPTION = "Exception reading the YAML stream as JSON";
static final String MSG_UNEXPECTED = "Unexpected jsonEvent reached parsing YAML: ";
static final String MSG_INVALID_MERGE_ALIAS = "Unable to expand merge key (<<). Alias '%s' must reference a YAML mapping, but found %s/%s";

// Support all the values from the Core Schema (https://yaml.org/spec/1.2/spec.html#id2804923)
static final Set<String> VALUES_NULL = Set.of("null", "Null", "NULL", "~");
Expand Down Expand Up @@ -112,6 +127,7 @@ enum NumberType {

final Boolean[] valueIsKey = new Boolean[200];
final List<Event> eventStack = new ArrayList<>();
final List<Boolean> mapMerge = new ArrayList<>();
int depth = -1;
final Deque<AnchorMetadata> anchorStack = new ArrayDeque<>();

Expand Down Expand Up @@ -178,8 +194,24 @@ void advanceEvent() {
if (alias != null) {
Event jsonEventOverride = currentEvent != Event.VALUE_NULL ? currentEvent : null;
List<AnchoredEvent<E>> events = anchoredEvents.get(alias);

if (Boolean.TRUE.equals(mapMerge.get(depth))) {
String firstEvent = getEventId(events.get(0).yamlEvent);
String finalEvent = getEventId(events.get(events.size() - 1).yamlEvent);

if (List.of(firstEvent, finalEvent).equals(MAPPING_BOUNDARIES)) {
events = events.subList(1, events.size() - 1);
} else {
String message = String.format(MSG_INVALID_MERGE_ALIAS, alias, firstEvent, finalEvent);
throw new JsonParsingException(message, getLocation(currentYamlEvent));
}
}

ListIterator<AnchoredEvent<E>> iterator = events.listIterator(events.size());

/*
* Reverse iteration and add each event to the front of the queue.
**/
while (iterator.hasPrevious()) {
enqueue(iterator.previous(), jsonEventOverride);
}
Expand Down Expand Up @@ -394,10 +426,14 @@ void enqueueDataElement(E yamlEvent, final String dataText) {
}
}

void enqueueDataElement(E yamlEvent, Boolean needKeyName) {
boolean enqueueDataElement(E yamlEvent, Boolean needKeyName) {
final String dataText = getValue(yamlEvent);

if (Boolean.TRUE.equals(needKeyName)) {
if (MERGE_KEY.equals(dataText)) {
mapMerge.set(depth, true);
return false;
}
enqueueString(yamlEvent, Event.KEY_NAME, dataText);
} else if (isPlain(yamlEvent)) {
if (dataText.isEmpty()) {
Expand All @@ -408,6 +444,8 @@ void enqueueDataElement(E yamlEvent, Boolean needKeyName) {
} else {
enqueueString(yamlEvent, Event.VALUE_STRING, dataText);
}

return true;
}

void enqueueAlias(E yamlEvent, Boolean needKeyName) {
Expand Down Expand Up @@ -449,7 +487,7 @@ long countExpansion(String alias, long limit) {

if (nestedAlias != null) {
count += countExpansion(nestedAlias, limit);
} else if ("Scalar".equals(getEventId(event.yamlEvent))) {
} else if (SCALAR.equals(getEventId(event.yamlEvent))) {
count++;
}

Expand All @@ -473,10 +511,12 @@ void incrementDepth(Event levelEvent, Boolean keyExpected) {
depth++;
this.valueIsKey[depth] = keyExpected;
eventStack.add(depth, levelEvent);
mapMerge.add(depth, false);
}

void decrementDepth() {
eventStack.remove(depth);
mapMerge.remove(depth);
depth--;
}

Expand Down Expand Up @@ -505,39 +545,39 @@ boolean enqueueEvent(E yamlEvent) {
String eventId = getEventId(yamlEvent);

switch (eventId) {
case "DocumentStart":
case "DocumentEnd":
case DOCUMENT_START:
case DOCUMENT_END:
eventFound = false;
break;

case "SequenceStart":
case SEQUENCE_START:
addAnchorMetadata(getAnchor(yamlEvent));
incrementDepth(Event.START_ARRAY, null);
enqueue(yamlEvent, Event.START_ARRAY, NumberType.NONE, "", UNSET_NUMBER);
break;

case "SequenceEnd":
case SEQUENCE_END:
enqueue(yamlEvent, Event.END_ARRAY, NumberType.NONE, "", UNSET_NUMBER);
decrementDepth();
removeAnchorMetadata(yamlEvent, Event.END_ARRAY);
break;

case "MappingStart":
case MAPPING_START:
addAnchorMetadata(getAnchor(yamlEvent));
incrementDepth(Event.START_OBJECT, Boolean.TRUE);
enqueue(yamlEvent, Event.START_OBJECT, NumberType.NONE, "", UNSET_NUMBER);
break;

case "MappingEnd":
case MAPPING_END:
enqueue(yamlEvent, Event.END_OBJECT, NumberType.NONE, "", UNSET_NUMBER);
decrementDepth();
removeAnchorMetadata(yamlEvent, Event.END_OBJECT);
break;

case "Scalar": {
case SCALAR: {
addAnchorMetadata(getAnchor(yamlEvent));
Boolean keyExpected = isKeyExpected();
enqueueDataElement(yamlEvent, keyExpected);
eventFound = enqueueDataElement(yamlEvent, keyExpected);

if (keyExpected != null) {
this.valueIsKey[depth] = Boolean.valueOf(!keyExpected);
Expand All @@ -546,7 +586,7 @@ boolean enqueueEvent(E yamlEvent) {
break;
}

case "Alias": {
case ALIAS: {
Boolean keyExpected = isKeyExpected();

enqueueAlias(yamlEvent, keyExpected);
Expand All @@ -559,8 +599,8 @@ boolean enqueueEvent(E yamlEvent) {
break;
}

case "StreamStart":
case "StreamEnd":
case STREAM_START:
case STREAM_END:
eventFound = false;
break;
default:
Expand Down
46 changes: 46 additions & 0 deletions src/test/java/io/xlate/yamljson/YamlParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -482,4 +482,50 @@ void testSkipArrayFromNestedSequence(String version) throws IOException {
assertTrue(conditionMet);
}
}

@ParameterizedTest
@MethodSource(VERSIONS_SOURCE)
void testMergeKeyWithOverrides(String version) throws IOException {
try (InputStream source = getClass().getResourceAsStream("/merge-key.yaml");
JsonParser parser = createParser(version, source)) {
parser.next();
JsonObject value = parser.getObject();
assertEquals(
Json.createObjectBuilder()
.add("key1", "value1")
.add("key2", Json.createObjectBuilder()
.add("key2_1", "value2_1")
.add("key2_2", "value2_2")
.add("key2_3", "value2_3")
)
.add("key3", "value3")
.add("key4", Json.createObjectBuilder()
.add("key2_1", "value2_1")
.add("key2_2", "value2_2")
.add("key2_3", "value2_3_override_by_key4")
.add("key4_1", "value4_1")
)
.add("key5", Json.createObjectBuilder()
.add("key2_1", "value2_1")
.add("key2_2", "value2_2")
.add("key2_3", "value2_3_override_by_key4")
.add("key4_1", "value4_1_override_by_key5")
.add("key5_1", "value5_1")
)
.build(),
value
);
}
}

@ParameterizedTest
@MethodSource(VERSIONS_SOURCE)
void testMergeKeyWithInvalidSequenceAlias(String version) throws IOException {
try (InputStream source = getClass().getResourceAsStream("/merge-key-invalid.yaml");
JsonParser parser = createParser(version, source)) {
parser.next();
Throwable thrown = assertThrows(JsonParsingException.class, () -> parser.getObject());
assertTrue(thrown.getMessage().contains("Unable to expand merge key (<<)"));
}
}
}
9 changes: 9 additions & 0 deletions src/test/resources/merge-key-invalid.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
key1: value1
key2: &value2
- value2_1
- value2_2
- value2_3
key3: value3
key4:
# Invalid - merge key may not reference a sequence/array
<< : *value2
14 changes: 14 additions & 0 deletions src/test/resources/merge-key.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
key1: value1
key2: &value2
key2_1: value2_1
key2_2: value2_2
key2_3: value2_3
key3: value3
key4: &value4
<< : *value2
key2_3: value2_3_override_by_key4
key4_1: value4_1
key5:
<< : *value4
key4_1: value4_1_override_by_key5
key5_1: value5_1

0 comments on commit 1fcf9d3

Please sign in to comment.