Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/design/sdk-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ static LookupSpec lookupKeyFromHeader(String name) // key.from {"headers":
// .index(n) key.index, Mountebank's meaning: regex 0 = whole match, n = nth capture group;
// jsonPath/xPath = nth selected value. Unset = first group / first value.
// .fromCsv(path, keyColumn, ';') csv.delimiter (engine default ','); both emitted only when set
// .fromCsv(path, keyColumn, "||") a multi-character csv.delimiter (rift >= 0.24.0, rift#1566); empty is refused
```

### 7.4 New `StubSpec` methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ Stream<DynamicTest> indexDelimiterAndHeaderKeyResolveTheRow() {
});
}

@TestFactory
Stream<DynamicTest> aMultiCharacterDelimiterSplitsTheRow() {
return gated("lookup reads a '||'-delimited CSV (rift#1566, rift >= 0.24.0)", () -> {
Path dir = Files.createTempDirectory("rift-lookup-");
Path csv = Files.writeString(dir.resolve("pipes.csv"), "id||name\n7||seven\n");
try (Rift rift = engine(dir)) {
Imposter imp = rift.create(imposter("lookup-pipes")
.stub(onGet("/tenant").willReturn(ok().withTextBody("${row}[name]")
.lookup(lookupKeyFromHeader("X-Tenant").using(regex(".+"))
.fromCsv(csv.toString(), "id", "||").into("${row}")))));

assertEquals("seven", get(imp, "/tenant", "7"));
} finally {
Files.deleteIfExists(csv);
Files.deleteIfExists(dir);
}
});
}

private static String get(Imposter imp, String path, String tenant) throws Exception {
HttpRequest.Builder request = HttpRequest.newBuilder(imp.uri().resolve(path)).timeout(Duration.ofSeconds(5)).GET();
if (tenant != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public final class LookupSpec {
private final OptionalInt index;
private final Optional<String> csvPath;
private final Optional<String> csvKeyColumn;
private final Optional<Character> csvDelimiter;
private final Optional<String> csvDelimiter;
private final Optional<String> into;

private LookupSpec(
Expand All @@ -37,7 +37,7 @@ private LookupSpec(
OptionalInt index,
Optional<String> csvPath,
Optional<String> csvKeyColumn,
Optional<Character> csvDelimiter,
Optional<String> csvDelimiter,
Optional<String> into) {
this.keyFrom = keyFrom;
this.using = using;
Expand Down Expand Up @@ -101,9 +101,37 @@ public LookupSpec fromCsv(String path, String keyColumn) {
* encoding on its own, so it would reach the engine as {@code ?}
*/
public LookupSpec fromCsv(String path, String keyColumn, char delimiter) {
if (Character.isSurrogate(delimiter)) {
throw new IllegalArgumentException(String.format(
"csv delimiter must be a whole character, not half of a surrogate pair (U+%04X)", (int) delimiter));
return fromCsv(path, keyColumn, String.valueOf(delimiter));
}

/**
* {@link #fromCsv(String, String, char)} for a delimiter of several characters, such as {@code "||"}
* — {@code csv.delimiter}, sent as given. Needs rift &ge; 0.24.0 when it is longer than one
* character (rift#1566, as Mountebank accepts it); an older engine refuses the imposter with
* {@code 400}.
*
* @throws NullPointerException if {@code delimiter} is null
* @throws IllegalArgumentException if {@code delimiter} is empty (the engine reads that as
* {@code ,}, which {@link #fromCsv(String, String)} already says) or
* holds half of a surrogate pair, which would reach the engine as
* {@code ?}
*/
public LookupSpec fromCsv(String path, String keyColumn, String delimiter) {
Objects.requireNonNull(delimiter, "delimiter");
if (delimiter.isEmpty()) {
throw new IllegalArgumentException(
"csv delimiter must not be empty; use fromCsv(path, keyColumn) for the engine's default ','");
}
for (int i = 0; i < delimiter.length(); i++) {
char c = delimiter.charAt(i);
boolean paired = Character.isHighSurrogate(c) && i + 1 < delimiter.length()
&& Character.isLowSurrogate(delimiter.charAt(i + 1));
if (paired) {
i++;
} else if (Character.isSurrogate(c)) {
throw new IllegalArgumentException(String.format(
"csv delimiter must be a whole character, not half of a surrogate pair (U+%04X)", (int) c));
}
}
return new LookupSpec(keyFrom, using, index, Optional.of(path), Optional.of(keyColumn), Optional.of(delimiter), into);
}
Expand Down Expand Up @@ -132,7 +160,7 @@ JsonValue build() {
JsonObject.Builder csv = JsonObject.builder()
.put("path", new JsonString(path))
.put("keyColumn", new JsonString(keyColumn));
csvDelimiter.ifPresent(d -> csv.put("delimiter", new JsonString(String.valueOf(d))));
csvDelimiter.ifPresent(d -> csv.put("delimiter", new JsonString(d)));
JsonObject fromDataSource = JsonObject.builder().put("csv", csv.build()).build();

return JsonObject.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static io.github.achirdlabs.rift.dsl.RiftDsl.regex;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/** The {@code lookup} grammar the engine accepts (#290): {@code key.index}, {@code csv.delimiter}, an object {@code key.from}. */
class LookupSpecWireTest {
Expand Down Expand Up @@ -70,4 +71,43 @@ void theTwoArgumentFromCsvClearsAnEarlierDelimiter() {
lookupKey("body").using(jsonPath("$.code")).fromCsv("p.csv", "code", ';').fromCsv("p.csv", "code")
.into("$ROW").build().toJson());
}

@Test
void aMultiCharacterDelimiterIsSentVerbatim() {
assertEquals("{\"key\":{\"from\":\"body\",\"using\":{\"method\":\"jsonpath\",\"selector\":\"$.code\"}},"
+ "\"fromDataSource\":{\"csv\":{\"path\":\"p.csv\",\"keyColumn\":\"code\",\"delimiter\":\"||\"}},\"into\":\"$ROW\"}",
lookupKey("body").using(jsonPath("$.code")).fromCsv("p.csv", "code", "||").into("$ROW").build().toJson());
}

@Test
void aOneCharacterStringDelimiterIsTheSameAsTheCharForm() {
assertEquals(lookupKey("body").using(jsonPath("$.code")).fromCsv("p.csv", "code", ';').into("$ROW").build().toJson(),
lookupKey("body").using(jsonPath("$.code")).fromCsv("p.csv", "code", ";").into("$ROW").build().toJson());
}

@Test
void anEmptyDelimiterIsRefusedSinceTheTwoArgumentFormAlreadyMeansTheDefault() {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> lookupKey("body").fromCsv("p.csv", "code", ""));
assertEquals("csv delimiter must not be empty; use fromCsv(path, keyColumn) for the engine's default ','",
e.getMessage());
}

@Test
void aNullDelimiterIsRefused() {
assertThrows(NullPointerException.class, () -> lookupKey("body").fromCsv("p.csv", "code", (String) null));
}

@Test
void anUnpairedSurrogateInAStringDelimiterIsRefused() {
IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
() -> lookupKey("body").fromCsv("p.csv", "code", "|\uD83D"));
assertEquals("csv delimiter must be a whole character, not half of a surrogate pair (U+D83D)", e.getMessage());
}

@Test
void aSupplementaryCharacterDelimiterIsAccepted() {
assertTrue(lookupKey("body").using(jsonPath("$.code")).fromCsv("p.csv", "code", "\uD83D\uDE00")
.into("$ROW").build().toJson().contains("\"delimiter\":\"\uD83D\uDE00\""));
}
}
Loading