This is an automated email from the ASF dual-hosted git repository.
bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new da19d925b7 Fix#9041 YAML input list-of-maps reading and null-path
failures (#9082)
da19d925b7 is described below
commit da19d925b70b21a931c71dfa71d772f0f47dc604
Author: Lance <[email protected]>
AuthorDate: Fri Oct 23 13:59:27 2026 +0800
Fix#9041 YAML input list-of-maps reading and null-path failures (#9082)
* Fix YAML input list-of-maps reading and null-path failures
Signed-off-by: leehaut <[email protected]>
* Fix YAML input list-of-maps reading and null-path failures
Signed-off-by: leehaut <[email protected]>
---------
Signed-off-by: leehaut <[email protected]>
---
.../pipeline/transforms/yamlinput/YamlInput.java | 48 ++++---
.../pipeline/transforms/yamlinput/YamlReader.java | 49 +++++--
.../yamlinput/messages/messages_en_US.properties | 8 +-
.../yamlinput/messages/messages_zh_CN.properties | 6 +-
.../transforms/yamlinput/YamlInputTests.java | 154 +++++++++++++++++++++
.../transforms/yamlinput/YamlReaderTests.java | 120 ++++++++++++++++
6 files changed, 343 insertions(+), 42 deletions(-)
diff --git
a/plugins/transforms/yamlinput/src/main/java/org/apache/hop/pipeline/transforms/yamlinput/YamlInput.java
b/plugins/transforms/yamlinput/src/main/java/org/apache/hop/pipeline/transforms/yamlinput/YamlInput.java
index 93a7cfe0cc..df807ed196 100644
---
a/plugins/transforms/yamlinput/src/main/java/org/apache/hop/pipeline/transforms/yamlinput/YamlInput.java
+++
b/plugins/transforms/yamlinput/src/main/java/org/apache/hop/pipeline/transforms/yamlinput/YamlInput.java
@@ -204,28 +204,24 @@ public class YamlInput extends
BaseTransform<YamlInputMeta, YamlInputData> {
private boolean openNextFile() {
try {
- if (data.fileIndex >= data.files.nrOfFiles()) {
- // finished processing!
- if (isDetailed()) {
- logDetailed(BaseMessages.getString(PKG,
"YamlInput.Log.FinishedProcessing"));
- }
- return false;
- }
- // Get file to process from list
- data.file = data.files.getFile(data.fileIndex);
-
- // Move file pointer ahead!
- data.fileIndex++;
-
- if (meta.isIgnoringEmptyFile() && data.file.getContent().getSize() == 0)
{
- if (isBasic()) {
- logBasic(
- BaseMessages.getString(PKG, "YamlInput.Error.FileSizeZero",
data.file.getName()));
+ while (data.fileIndex < data.files.nrOfFiles()) {
+ // Get file to process from list
+ data.file = data.files.getFile(data.fileIndex);
+
+ // Move file pointer ahead!
+ data.fileIndex++;
+
+ if (meta.isIgnoringEmptyFile() && data.file.getContent().getSize() ==
0) {
+ // Do not keep a reader from a previous file, and do not report
success: there is
+ // nothing to read until a non-empty file is opened.
+ data.yaml = null;
+ if (isBasic()) {
+ logBasic(
+ BaseMessages.getString(PKG, "YamlInput.Error.FileSizeZero",
data.file.getName()));
+ }
+ continue;
}
- // Let's open the next file
- openNextFile();
- } else {
if (isDetailed()) {
logDetailed(
BaseMessages.getString(PKG, "YamlInput.Log.OpeningFile",
data.file.toString()));
@@ -246,7 +242,14 @@ public class YamlInput extends
BaseTransform<YamlInputMeta, YamlInputData> {
logDetailed(
BaseMessages.getString(PKG, "YamlInput.Log.FileOpened",
data.file.toString()));
}
+ return true;
}
+
+ // finished processing!
+ if (isDetailed()) {
+ logDetailed(BaseMessages.getString(PKG,
"YamlInput.Log.FinishedProcessing"));
+ }
+ return false;
} catch (Exception e) {
logError(
BaseMessages.getString(
@@ -260,7 +263,6 @@ public class YamlInput extends BaseTransform<YamlInputMeta,
YamlInputData> {
logError(Const.getStackTracker(e));
return false;
}
- return true;
}
@Override
@@ -321,11 +323,11 @@ public class YamlInput extends
BaseTransform<YamlInputMeta, YamlInputData> {
private Object[] getOneRowFromFileMode() throws HopException {
while (true) {
- if (data.file == null && !openNextFile()) {
+ if ((data.file == null || data.yaml == null) && !openNextFile()) {
return new Object[0];
}
- if (data.file == null) {
+ if (data.file == null || data.yaml == null) {
continue;
}
diff --git
a/plugins/transforms/yamlinput/src/main/java/org/apache/hop/pipeline/transforms/yamlinput/YamlReader.java
b/plugins/transforms/yamlinput/src/main/java/org/apache/hop/pipeline/transforms/yamlinput/YamlReader.java
index dd529cdedf..a531445867 100644
---
a/plugins/transforms/yamlinput/src/main/java/org/apache/hop/pipeline/transforms/yamlinput/YamlReader.java
+++
b/plugins/transforms/yamlinput/src/main/java/org/apache/hop/pipeline/transforms/yamlinput/YamlReader.java
@@ -86,6 +86,12 @@ public class YamlReader {
private final Deque<Object[]> pendingRows = new ArrayDeque<>();
+ /**
+ * Rows queued from one element of a list document. Draining them must not
finish the document, or
+ * the remaining elements are skipped.
+ */
+ private boolean pendingRowsFromListElement;
+
/** Bytes read from the last loaded file (for data volume tracking). */
@Getter private long bytesReadFromFile;
@@ -148,11 +154,7 @@ public class YamlReader {
/** get row */
public Object[] getRow(IRowMeta rowMeta) {
if (!pendingRows.isEmpty()) {
- Object[] row = pendingRows.poll();
- if (pendingRows.isEmpty()) {
- finishDocument();
- }
- return row;
+ return pollPendingRow();
}
if (document == null) {
@@ -164,7 +166,9 @@ public class YamlReader {
}
Object[] row = fetchRow(rowMeta);
- if (row.length == 0 && !isFinishedDocument()) {
+ // An empty row finishes the current list document. Keep going when
another document
+ // remains: YamlInput treats an empty row as the end of the file.
+ if (row.length == 0 && (!isFinishedDocument() || documentIt.hasNext())) {
return getRow(rowMeta);
}
return row;
@@ -224,13 +228,27 @@ public class YamlReader {
private Object[] processMapRow(IRowMeta rowMeta, Object root, boolean
listElement) {
if (pendingRows.isEmpty()) {
prepareRowsFromPaths(rowMeta, root, listElement);
+ pendingRowsFromListElement = listElement;
}
+ Object[] row = pollPendingRow();
+ return row == null ? new Object[0] : row;
+ }
+
+ /**
+ * Returns the next prepared row. A map document is finished when its rows
are consumed. A list
+ * element is not: the list iterator still has to walk the remaining
elements.
+ */
+ private Object[] pollPendingRow() {
Object[] row = pendingRows.poll();
if (pendingRows.isEmpty()) {
- finishDocument();
+ if (pendingRowsFromListElement) {
+ pendingRowsFromListElement = false;
+ } else {
+ finishDocument();
+ }
}
- return row == null ? new Object[0] : row;
+ return row;
}
private void prepareRowsFromPaths(IRowMeta rowMeta, Object root, boolean
listElement) {
@@ -283,7 +301,7 @@ public class YamlReader {
private List<Object> readPathAsList(Object root, JsonPath path) {
if (root == null) {
- return List.of((Object) null);
+ return listWithNull();
}
try {
@@ -291,24 +309,31 @@ public class YamlReader {
Object result = context.read(path);
return toValueList(result);
} catch (Exception e) {
- return List.of((Object) null);
+ return listWithNull();
}
}
private static List<Object> toValueList(Object result) {
if (result == null) {
- return List.of((Object) null);
+ return listWithNull();
}
if (result instanceof List<?> list) {
if (list.isEmpty()) {
- return List.of((Object) null);
+ return listWithNull();
}
return new ArrayList<>(list);
}
return List.of(result);
}
+ /** {@link List#of} rejects null elements. A missing path is a null cell,
not an error. */
+ private static List<Object> listWithNull() {
+ List<Object> values = new ArrayList<>(1);
+ values.add(null);
+ return values;
+ }
+
private Object[] advanceListIterator() {
if (!dataListIt.hasNext()) {
finishDocument();
diff --git
a/plugins/transforms/yamlinput/src/main/resources/org/apache/hop/pipeline/transforms/yamlinput/messages/messages_en_US.properties
b/plugins/transforms/yamlinput/src/main/resources/org/apache/hop/pipeline/transforms/yamlinput/messages/messages_en_US.properties
index 79d49f83de..ce333e5b71 100644
---
a/plugins/transforms/yamlinput/src/main/resources/org/apache/hop/pipeline/transforms/yamlinput/messages/messages_en_US.properties
+++
b/plugins/transforms/yamlinput/src/main/resources/org/apache/hop/pipeline/transforms/yamlinput/messages/messages_en_US.properties
@@ -37,7 +37,7 @@ YamlInput.Log.UnableToOpenFile=Could not open file \#{0} \:
{1} --> {2}
YamlInput.Log.UnexpectedError=Unexpected Error \: {0}
YamlInput.Log.YAMLStream=The field [{0}] contain [{1}]
YamlInput.Name=YAML input
-YamlInputDialog.AddResult.Label=Add files to result filesname
+YamlInputDialog.AddResult.Label=Add files to result filenames
YamlInputDialog.AddResult.Tooltip=Add files to result filenames
YamlInputDialog.Button.PreviewRows=\ &Preview rows
YamlInputDialog.Content.Tab=Content
@@ -80,12 +80,12 @@ YamlInputDialog.FilesReadSelection.DialogMessage=Files
read\:
YamlInputDialog.FilesReadSelection.DialogTitle=Files read
YamlInputDialog.GetFields.Button=\ &Get fields
YamlInputDialog.IgnoreEmptyFile.Label=Ignore empty file
-YamlInputDialog.IgnoreEmptyFile.Tooltip=Check this option if you want to
ignore (skip) empty files.Otherwise, Apache Hop will fail making the process
stop\!
+YamlInputDialog.IgnoreEmptyFile.Tooltip=Skip empty files. Without this option
an empty file produces no rows and does not stop the pipeline.
YamlInputDialog.InclFilename.Label=Include filename in output
YamlInputDialog.InclFilename.Tooltip=Check this to add a field (String)
containing the filename.
YamlInputDialog.InclFilenameField.Label=Filename fieldname
YamlInputDialog.InclRownum.Label=Rownum in output
-YamlInputDialog.InclRownum.Tooltip=Check this to add a field (String)
containing the row number.
+YamlInputDialog.InclRownum.Tooltip=Check this to add a field (Integer)
containing the row number.
YamlInputDialog.InclRownumField.Label=Rownum fieldname
YamlInputDialog.IncludeSubDirs.Column=Include subfolders
YamlInputDialog.IncludeSubDirs.Tooltip=Check this option if you want to fetch
subfolders
@@ -96,7 +96,7 @@ YamlInputDialog.NumberRows.DialogMessage=Enter the number of
rows you would like
YamlInputDialog.NumberRows.DialogTitle=Enter preview size
YamlInputDialog.RegExp.Label=Regular Expression
YamlInputDialog.Required.Column=Required
-YamlInputDialog.Required.Tooltip=Is this file required? \nOnly used for files
without wildcards.
+YamlInputDialog.Required.Tooltip=Is this file or folder required?\nA required
file must exist. A required folder must exist and match at least one file.
YamlInputDialog.ShowFiles.Button=&Show filename(s)...
YamlInputDialog.wAddFileResult.Label=Add to result filename
YamlInputDialog.wAdditionalFields.Label=Additional fields
diff --git
a/plugins/transforms/yamlinput/src/main/resources/org/apache/hop/pipeline/transforms/yamlinput/messages/messages_zh_CN.properties
b/plugins/transforms/yamlinput/src/main/resources/org/apache/hop/pipeline/transforms/yamlinput/messages/messages_zh_CN.properties
index e23c2a12cb..0cd710e963 100644
---
a/plugins/transforms/yamlinput/src/main/resources/org/apache/hop/pipeline/transforms/yamlinput/messages/messages_zh_CN.properties
+++
b/plugins/transforms/yamlinput/src/main/resources/org/apache/hop/pipeline/transforms/yamlinput/messages/messages_zh_CN.properties
@@ -81,12 +81,12 @@
YamlInputDialog.FilesReadSelection.DialogMessage=\u8BFB\u6587\u4EF6\:
YamlInputDialog.FilesReadSelection.DialogTitle=\u8BFB\u53D6\u6587\u4EF6
YamlInputDialog.GetFields.Button=\u83B7\u53D6\u5B57\u6BB5(&G)
YamlInputDialog.IgnoreEmptyFile.Label=\u5FFD\u7565\u7A7A\u6587\u4EF6\:
-YamlInputDialog.IgnoreEmptyFile.Tooltip=\u9009\u4E2D\u8BE5\u9009\u9879\u5C06\u5FFD\u7565(\u8DF3\u8FC7)\u7A7A\u6587\u4EF6,
\u5426\u5219\u7EC8\u6B62\u5904\u7406\u8FDB\u7A0B!
+YamlInputDialog.IgnoreEmptyFile.Tooltip=\u9009\u4E2D\u8BE5\u9009\u9879\u5C06\u5FFD\u7565(\u8DF3\u8FC7)\u7A7A\u6587\u4EF6\u3002\u4E0D\u9009\u4E2D\u65F6\uFF0C\u7A7A\u6587\u4EF6\u4E0D\u4F1A\u4EA7\u751F\u6570\u636E\u884C\uFF0C\u4E5F\u4E0D\u4F1A\u7EC8\u6B62\u5904\u7406\u3002
YamlInputDialog.InclFilename.Label=\u8F93\u51FA\u6587\u4EF6\u540D:
YamlInputDialog.InclFilename.Tooltip=\u6DFB\u52A0\u6587\u4EF6\u540D\u5230\u8F93\u51FA\u5B57\u6BB5\u5217\u8868\u4E2D
YamlInputDialog.InclFilenameField.Label=\u5B57\u6BB5\u540D\u79F0\:
YamlInputDialog.InclRownum.Label=\u8F93\u51FA\u884C\u53F7:
-YamlInputDialog.InclRownum.Tooltip=\u6DFB\u52A0\u884C\u53F7\u5230\u8F93\u51FA\u5B57\u6BB5\u5217\u8868\u4E2D
+YamlInputDialog.InclRownum.Tooltip=\u6DFB\u52A0\u884C\u53F7\uFF08\u6574\u6570\uFF09\u5230\u8F93\u51FA\u5B57\u6BB5\u5217\u8868\u4E2D
YamlInputDialog.InclRownumField.Label=\u5B57\u6BB5\u540D\u79F0:
YamlInputDialog.IncludeSubDirs.Column=\u5305\u542B\u5B50\u76EE\u5F55
YamlInputDialog.IncludeSubDirs.Tooltip=\u904D\u5386\u5B50\u76EE\u5F55
@@ -97,7 +97,7 @@
YamlInputDialog.NumberRows.DialogMessage=\u8F93\u5165\u60A8\u60F3\u9884\u89C8\u7
YamlInputDialog.NumberRows.DialogTitle=\u9884\u89C8\u884C\u6570
YamlInputDialog.RegExp.Label=\u6B63\u5219\u8868\u8FBE\u5F0F:
YamlInputDialog.Required.Column=\u5FC5\u9700
-YamlInputDialog.Required.Tooltip=\u6307\u5B9A\u6587\u4EF6\u5FC5\u9700\u5B58\u5728(\u4F7F\u7528\u901A\u914D\u7B26\u65F6,
\u8BE5\u9009\u9879\u65E0\u6548)
+YamlInputDialog.Required.Tooltip=\u8BE5\u6587\u4EF6\u6216\u76EE\u5F55\u662F\u5426\u5FC5\u9700\uFF1F\n\u5FC5\u9700\u7684\u6587\u4EF6\u5FC5\u987B\u5B58\u5728\u3002\u5FC5\u9700\u7684\u76EE\u5F55\u5FC5\u987B\u5B58\u5728\uFF0C\u5E76\u4E14\u81F3\u5C11\u5339\u914D\u4E00\u4E2A\u6587\u4EF6\u3002
YamlInputDialog.ShowFiles.Button=\u663E\u793A\u6587\u4EF6\u540D(&S)
YamlInputDialog.wAddFileResult.Label=Transform \u8F93\u51FA\u6587\u4EF6
YamlInputDialog.wAdditionalFields.Label=\u9644\u52A0\u5B57\u6BB5
diff --git
a/plugins/transforms/yamlinput/src/test/java/org/apache/hop/pipeline/transforms/yamlinput/YamlInputTests.java
b/plugins/transforms/yamlinput/src/test/java/org/apache/hop/pipeline/transforms/yamlinput/YamlInputTests.java
index 8f4723f88f..9678769e27 100644
---
a/plugins/transforms/yamlinput/src/test/java/org/apache/hop/pipeline/transforms/yamlinput/YamlInputTests.java
+++
b/plugins/transforms/yamlinput/src/test/java/org/apache/hop/pipeline/transforms/yamlinput/YamlInputTests.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -295,6 +296,159 @@ class YamlInputTests {
assertEquals(40L, ((Number) written.getFirst()[1]).longValue());
}
+ @Test
+ void testProcessRow_listOfMaps_emitsEveryElement() throws Exception {
+ Path tempFile = tempDir.resolve("people.yaml");
+ Files.writeString(
+ tempFile,
+ """
+ - id: 1
+ name: Anna
+ - id: 2
+ name: Ben
+ - id: 3
+ name: Chris
+ """);
+
+ YamlInputMeta meta = new YamlInputMeta();
+ meta.setInFields(false);
+ meta.setDoNotFailIfNoFile(true);
+ meta.getInputFields().add(field("id", "$.id", IValueMeta.TYPE_INTEGER));
+ meta.getInputFields().add(field("name", "$.name", IValueMeta.TYPE_STRING));
+ meta.getYamlFiles().add(yamlFile(tempFile));
+
+ List<Object[]> written = new ArrayList<>();
+ YamlInput transform = runningTransform(meta, written);
+
+ assertTrue(transform.processRow());
+ assertTrue(transform.processRow());
+ assertTrue(transform.processRow());
+ assertFalse(transform.processRow());
+ assertEquals(0, transform.getErrors());
+ assertEquals(3, written.size());
+ assertEquals("Anna", written.get(0)[1]);
+ assertEquals("Ben", written.get(1)[1]);
+ assertEquals("Chris", written.get(2)[1]);
+ }
+
+ @Test
+ void testProcessRow_missingPath_emitsNull() throws Exception {
+ Path tempFile = tempDir.resolve("person.yaml");
+ Files.writeString(tempFile, """
+ id: 1
+ name: Anna
+ """);
+
+ YamlInputMeta meta = new YamlInputMeta();
+ meta.setInFields(false);
+ meta.setDoNotFailIfNoFile(true);
+ meta.getInputFields().add(field("id", "$.id", IValueMeta.TYPE_INTEGER));
+ meta.getInputFields().add(field("missing", "$.nope",
IValueMeta.TYPE_STRING));
+ meta.getYamlFiles().add(yamlFile(tempFile));
+
+ List<Object[]> written = new ArrayList<>();
+ YamlInput transform = runningTransform(meta, written);
+
+ assertTrue(transform.processRow());
+ assertFalse(transform.processRow());
+ assertEquals(0, transform.getErrors());
+ assertEquals(1, written.size());
+ assertEquals(1L, ((Number) written.getFirst()[0]).longValue());
+ assertNull(written.getFirst()[1]);
+ }
+
+ @Test
+ void testProcessRow_ignoreEmptyFile_allEmpty_finishesWithoutError() throws
Exception {
+ Path emptyFile = tempDir.resolve("empty.yaml");
+ Files.writeString(emptyFile, "");
+
+ YamlInputMeta meta = new YamlInputMeta();
+ meta.setInFields(false);
+ meta.setDoNotFailIfNoFile(true);
+ meta.setIgnoringEmptyFile(true);
+ meta.getInputFields().add(field("id", "$.id", IValueMeta.TYPE_INTEGER));
+ meta.getYamlFiles().add(yamlFile(emptyFile));
+
+ List<Object[]> written = new ArrayList<>();
+ YamlInput transform = runningTransform(meta, written);
+
+ assertFalse(transform.processRow());
+ assertFalse(transform.processRow());
+ assertEquals(0, transform.getErrors());
+ assertEquals(0, written.size());
+ }
+
+ @Test
+ void testProcessRow_ignoreEmptyFile_skipsEmptyThenReadsNext() throws
Exception {
+ Path emptyFile = tempDir.resolve("empty.yaml");
+ Path peopleFile = tempDir.resolve("people.yaml");
+ Files.writeString(emptyFile, "");
+ Files.writeString(
+ peopleFile,
+ """
+ - id: 1
+ name: Anna
+ - id: 2
+ name: Ben
+ """);
+
+ YamlInputMeta meta = new YamlInputMeta();
+ meta.setInFields(false);
+ meta.setDoNotFailIfNoFile(true);
+ meta.setIgnoringEmptyFile(true);
+ meta.getInputFields().add(field("id", "$.id", IValueMeta.TYPE_INTEGER));
+ meta.getInputFields().add(field("name", "$.name", IValueMeta.TYPE_STRING));
+ meta.getYamlFiles().add(yamlFile(emptyFile));
+ meta.getYamlFiles().add(yamlFile(peopleFile));
+
+ List<Object[]> written = new ArrayList<>();
+ YamlInput transform = runningTransform(meta, written);
+
+ assertTrue(transform.processRow());
+ assertTrue(transform.processRow());
+ assertFalse(transform.processRow());
+ assertEquals(0, transform.getErrors());
+ assertEquals(List.of("Anna", "Ben"), List.of(written.get(0)[1],
written.get(1)[1]));
+ }
+
+ private static YamlInputField field(String name, String path, int type) {
+ YamlInputField field = new YamlInputField();
+ field.setName(name);
+ field.setPath(path);
+ field.setType(type);
+ return field;
+ }
+
+ private static YamlInputMeta.YamlFile yamlFile(Path path) {
+ YamlInputMeta.YamlFile file = new YamlInputMeta.YamlFile();
+ file.setFilename(path.toAbsolutePath().toString());
+ file.setFileMask(".*\\.ya?ml");
+ file.setFileRequired(false);
+ return file;
+ }
+
+ private YamlInput runningTransform(YamlInputMeta meta, List<Object[]>
written) throws Exception {
+ YamlInput transform =
+ spy(
+ new YamlInput(
+ helper.transformMeta,
+ meta,
+ new YamlInputData(),
+ 0,
+ helper.pipelineMeta,
+ helper.pipeline));
+ doAnswer(
+ invocation -> {
+ Object[] row = invocation.getArgument(1);
+ written.add(Arrays.copyOf(row, row.length));
+ return null;
+ })
+ .when(transform)
+ .putRow(any(IRowMeta.class), any());
+ assertTrue(transform.init());
+ return transform;
+ }
+
@AfterEach
void tearDown() {
helper.cleanUp();
diff --git
a/plugins/transforms/yamlinput/src/test/java/org/apache/hop/pipeline/transforms/yamlinput/YamlReaderTests.java
b/plugins/transforms/yamlinput/src/test/java/org/apache/hop/pipeline/transforms/yamlinput/YamlReaderTests.java
index 08e3050403..b3bbebdce5 100644
---
a/plugins/transforms/yamlinput/src/test/java/org/apache/hop/pipeline/transforms/yamlinput/YamlReaderTests.java
+++
b/plugins/transforms/yamlinput/src/test/java/org/apache/hop/pipeline/transforms/yamlinput/YamlReaderTests.java
@@ -20,6 +20,7 @@ package org.apache.hop.pipeline.transforms.yamlinput;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.FileWriter;
@@ -267,6 +268,125 @@ class YamlReaderTests {
assertEquals("t_order", row2[0]);
}
+ @Test
+ void testListOfMaps_readsEveryElement() throws Exception {
+ String yaml =
+ """
+ - id: 1
+ name: Anna
+ - id: 2
+ name: Ben
+ - id: 3
+ name: Chris
+ """;
+
+ reader.loadString(yaml);
+
+ RowMeta rowMeta =
+ buildRowMeta(
+ new String[] {"id", "name"},
+ new String[] {"$.id", "$.name"},
+ new int[] {IValueMeta.TYPE_INTEGER, IValueMeta.TYPE_STRING});
+
+ Object[] row1 = reader.getRow(rowMeta);
+ Object[] row2 = reader.getRow(rowMeta);
+ Object[] row3 = reader.getRow(rowMeta);
+ Object[] done = reader.getRow(rowMeta);
+
+ assertEquals(1L, row1[0]);
+ assertEquals("Anna", row1[1]);
+ assertEquals(2L, row2[0]);
+ assertEquals("Ben", row2[1]);
+ assertEquals(3L, row3[0]);
+ assertEquals("Chris", row3[1]);
+ assertEquals(0, done.length);
+ }
+
+ @Test
+ void testListOfMaps_readsEveryDocument() throws Exception {
+ String yaml =
+ """
+ - id: 1
+ name: Anna
+ - id: 2
+ name: Ben
+ ---
+ - id: 3
+ name: Chris
+ - id: 4
+ name: Dana
+ """;
+
+ reader.loadString(yaml);
+
+ RowMeta rowMeta =
+ buildRowMeta(
+ new String[] {"id", "name"},
+ new String[] {"$.id", "$.name"},
+ new int[] {IValueMeta.TYPE_INTEGER, IValueMeta.TYPE_STRING});
+
+ Object[] row1 = reader.getRow(rowMeta);
+ Object[] row2 = reader.getRow(rowMeta);
+ Object[] row3 = reader.getRow(rowMeta);
+ Object[] row4 = reader.getRow(rowMeta);
+ Object[] done = reader.getRow(rowMeta);
+
+ assertEquals(1L, row1[0]);
+ assertEquals("Anna", row1[1]);
+ assertEquals(2L, row2[0]);
+ assertEquals("Ben", row2[1]);
+ assertEquals(3L, row3[0]);
+ assertEquals("Chris", row3[1]);
+ assertEquals(4L, row4[0]);
+ assertEquals("Dana", row4[1]);
+ assertEquals(0, done.length);
+ }
+
+ @Test
+ void testListOfMaps_wildcardPathReadsEveryElement() throws Exception {
+ String yaml =
+ """
+ - id: 1
+ name: Anna
+ - id: 2
+ name: Ben
+ """;
+
+ reader.loadString(yaml);
+
+ RowMeta rowMeta =
+ buildRowMeta(
+ new String[] {"id", "name"},
+ new String[] {"$[*].id", "$[*].name"},
+ new int[] {IValueMeta.TYPE_INTEGER, IValueMeta.TYPE_STRING});
+
+ assertEquals("Anna", reader.getRow(rowMeta)[1]);
+ assertEquals("Ben", reader.getRow(rowMeta)[1]);
+ assertEquals(0, reader.getRow(rowMeta).length);
+ }
+
+ @Test
+ void testMissingPath_returnsNullInsteadOfFailing() throws Exception {
+ String yaml = """
+ id: 1
+ name: Anna
+ """;
+
+ reader.loadString(yaml);
+
+ RowMeta rowMeta =
+ buildRowMeta(
+ new String[] {"id", "missing"},
+ new String[] {"$.id", "$.nope"},
+ new int[] {IValueMeta.TYPE_INTEGER, IValueMeta.TYPE_STRING});
+
+ Object[] row = reader.getRow(rowMeta);
+
+ assertEquals(1L, row[0]);
+ assertNull(row[1]);
+ assertEquals(0, reader.getRow(rowMeta).length);
+ }
+
@Test
void testGetFieldsAndGetRow_listOfMap() {
String yaml = """