Skip to content
This repository has been archived by the owner on Jan 27, 2025. It is now read-only.

Commit

Permalink
Introduce type-safe record value abstraction. (#64)
Browse files Browse the repository at this point in the history
Obviates the need for `instanceof` checks and unchecked casts.

A `Value` is a container/wrapper for either

- an `Array` (which in turn is a wrapper for `List<Value>`), or
- a `Hash` (which in turn is a wrapper for `Map<String, Value>`), or
- a `String` (which is the terminal type)
  • Loading branch information
blackwinter committed Nov 16, 2021
1 parent c6e20f9 commit dc8c708
Show file tree
Hide file tree
Showing 8 changed files with 470 additions and 230 deletions.
4 changes: 4 additions & 0 deletions metafix/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ dependencies {

test {
useJUnitPlatform()

testlogger {
showFullStackTraces true
}
}

task install(dependsOn: publishToMavenLocal,
Expand Down
185 changes: 84 additions & 101 deletions metafix/src/main/java/org/metafacture/metafix/FixMethod.java

Large diffs are not rendered by default.

44 changes: 25 additions & 19 deletions metafix/src/main/java/org/metafacture/metafix/FixPredicate.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,52 +23,58 @@
enum FixPredicate {

contain {
public Predicate<Object> of(final Object value) {
return v -> v.toString().contains(value.toString());
@Override
public Predicate<Value> of(final String string) {
return v -> v.toString().contains(string);
}
},
equal {
public Predicate<Object> of(final Object value) {
return v -> v.toString().equals(value.toString());
@Override
public Predicate<Value> of(final String string) {
return v -> v.toString().equals(string);
}
},
match {
public Predicate<Object> of(final Object value) {
return v -> v.toString().matches(value.toString());
@Override
public Predicate<Value> of(final String string) {
return v -> v.toString().matches(string);
}
};

abstract Predicate<Object> of(Object value);
abstract Predicate<Value> of(String string);

enum Quantifier {

all {
@Override
public boolean test(final Record record, final FixPredicate p, final List<String> params) {
return test(record, params.get(0), s -> s.allMatch(p.of(params.get(1))));
protected boolean test(final Record record, final String fieldName, final Predicate<Value> p) {
return testStream(record, fieldName, s -> s.allMatch(p));
}

},
any {
@Override
public boolean test(final Record record, final FixPredicate p, final List<String> params) {
return test(record, params.get(0), s -> s.anyMatch(p.of(params.get(1))));
protected boolean test(final Record record, final String fieldName, final Predicate<Value> p) {
return testStream(record, fieldName, s -> s.anyMatch(p));
}
},
none {
@Override
public boolean test(final Record record, final FixPredicate p, final List<String> params) {
final Object fieldValue = FixMethod.find(record, FixMethod.split(params.get(0)));
final String valueToTest = params.get(1);
return fieldValue == null || Metafix.asList(fieldValue).stream().noneMatch(p.of(valueToTest));
protected boolean test(final Record record, final String fieldName, final Predicate<Value> p) {
return !any.test(record, fieldName, p);
}
};

boolean test(final Record record, final String fieldName, final Predicate<Stream<Object>> f) {
final Object value = FixMethod.find(record, FixMethod.split(fieldName));
return value != null && f.test(Metafix.asList(value).stream());
boolean testStream(final Record record, final String fieldName, final Predicate<Stream<Value>> p) {
final Value value = FixMethod.find(record, FixMethod.split(fieldName));
return value != null && p.test(Metafix.asList(value, null).asArray().stream());
}

abstract boolean test(Record record, FixPredicate p, List<String> params);
public boolean test(final Record record, final FixPredicate p, final List<String> params) {
return test(record, params.get(0), p.of(params.get(1)));
}

protected abstract boolean test(Record record, String fieldName, Predicate<Value> p);
}

}
113 changes: 66 additions & 47 deletions metafix/src/main/java/org/metafacture/metafix/Metafix.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
* Transforms a data stream sent via the {@link StreamReceiver} interface. Use
Expand All @@ -49,7 +49,7 @@
* @author Christoph Böhme (Metamorph)
* @author Fabian Steeg (Metafix)
*/
public class Metafix implements StreamPipe<StreamReceiver> {
public class Metafix implements StreamPipe<StreamReceiver> { // checkstyle-disable-line ClassDataAbstractionCoupling

public static final String VAR_START = "$[";
public static final String VAR_END = "]";
Expand Down Expand Up @@ -146,30 +146,30 @@ public void endRecord() {
}
}

private void emit(final Object key, final Object val) {
if (val == null) {
return;
}
final List<?> vals = asList(val);
final boolean isMulti = vals.size() > 1 || val instanceof List;
if (isMulti) {
outputStreamReceiver.startEntity(key.toString() + "[]");
}
for (int i = 0; i < vals.size(); ++i) {
final Object value = vals.get(i);
if (value instanceof Value.Hash) {
final Value.Hash nested = (Value.Hash) value;
outputStreamReceiver.startEntity(isMulti ? "" : key.toString());
nested.forEach(this::emit);
outputStreamReceiver.endEntity();
private void emit(final String key, final Value val) {
asList(val, vals -> {
final boolean isMulti = vals.size() > 1 || val.isArray();
if (isMulti) {
outputStreamReceiver.startEntity(key + "[]");
}
else {
outputStreamReceiver.literal(isMulti ? (i + 1) + "" : key.toString(), value.toString());

for (int i = 0; i < vals.size(); ++i) {
final Value value = vals.get(i);

if (value.isHash()) {
outputStreamReceiver.startEntity(isMulti ? "" : key);
value.asHash().forEach(this::emit);
outputStreamReceiver.endEntity();
}
else {
outputStreamReceiver.literal(isMulti ? (i + 1) + "" : key, value.toString());
}
}
}
if (isMulti) {
outputStreamReceiver.endEntity();
}

if (isMulti) {
outputStreamReceiver.endEntity();
}
});
}

@Override
Expand All @@ -187,16 +187,15 @@ public void startEntity(final String name) {
}

private Value.Hash currentEntity(final String name, final Value.Hash previousEntity) {
final Object existingValue = previousEntity != null ? previousEntity.get(name) : null;
final Value existingValue = previousEntity != null ? previousEntity.get(name) : null;
final Value.Hash currentEntity;
if (existingValue instanceof Value.Hash) {
@SuppressWarnings("unchecked")
final Value.Hash existingEntity = (Value.Hash) previousEntity.get(name);
currentEntity = existingEntity;
if (existingValue != null && existingValue.isHash()) {
currentEntity = previousEntity.get(name).asHash();
}
else {
currentEntity = new Value.Hash();
add(previousEntity != null ? previousEntity : currentRecord, name, currentEntity);
final Value value = Value.newHash();
currentEntity = value.asHash();
add(previousEntity != null ? previousEntity : currentRecord, name, value);
}
return currentEntity;
}
Expand All @@ -213,7 +212,7 @@ public void literal(final String name, final String value) {
final Integer currentEntityIndex = entityCountStack.peek() - 1;
final Value.Hash currentEntity = currentEntityIndex < 0 ||
entities.size() <= currentEntityIndex ? null : entities.get(currentEntityIndex);
add(currentEntity != null ? currentEntity : currentRecord, name, value);
add(currentEntity != null ? currentEntity : currentRecord, name, new Value(value));
// TODO: keep flattener as option?
// flattener.literal(name, value);
}
Expand Down Expand Up @@ -253,33 +252,53 @@ public Record getCurrentRecord() {
}

static void addAll(final Value.Hash record, final String fieldName, final List<String> values) {
values.forEach(value -> add(record, fieldName, value));
values.forEach(value -> add(record, fieldName, new Value(value)));
}

static void addAll(final Value.Hash record, final Value.Hash values) {
values.forEach((fieldName, value) -> add(record, fieldName, value));
}

static void add(final Value.Hash record, final String name, final Object newValue) {
final Object oldValue = record.get(name);
static void add(final Value.Hash record, final String name, final Value newValue) {
final Value oldValue = record.get(name);
record.put(name, oldValue == null ? newValue : merged(oldValue, newValue));
}

@SuppressWarnings("unchecked")
static Object merged(final Object object1, final Object object2) {
if (object1 instanceof Value.Hash && object2 instanceof Value.Hash) {
final Value.Hash result = (Value.Hash) object1;
((Value.Hash) object2).forEach(result::put);
return result;
static Value merged(final Value value1, final Value value2) {
if (value1.isHash() && value2.isHash()) {
final Value.Hash hash = value1.asHash();
value2.asHash().forEach(hash::put);
return value1;
}
else {
return asList(value1, a1 -> asList(value2, a2 -> a2.forEach(a1::add)));
}
final List<Object> list = asList(object1);
asList(object2).forEach(list::add);
return list;
}

@SuppressWarnings("unchecked")
static List<Object> asList(final Object object) {
return new ArrayList<>(object instanceof List ? (List<Object>) object : Arrays.asList(object));
static Value asList(final Value value, final Consumer<Value.Array> consumer) {
final Value result;

if (Value.isNull(value)) {
result = null;
}
else if (value.isArray()) {
if (consumer != null) {
consumer.accept(value.asArray());
}

result = value;
}
else {
result = Value.newArray(a -> {
a.add(value);

if (consumer != null) {
consumer.accept(a);
}
});
}

return result;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ private void processBind(final Do theDo, final EList<String> params) {
if (theDo.getName().equals("list")) { // TODO impl multiple binds via FixBind enum
final Map<String, String> options = options(theDo.getOptions());
final Record fullRecord = record.shallowClone();
final Object values = FixMethod.find(record, FixMethod.split(options.get("path")));
final Value values = FixMethod.find(record, FixMethod.split(options.get("path")));

Metafix.asList(values).stream().filter(val -> val != null).forEach(val -> {
Metafix.asList(values, a -> a.forEach(val -> {
// for each val, bind the current record/scope/context to the given var name:
record = new Record();
record.put(options.get("var"), val);
Expand All @@ -105,7 +105,7 @@ record = new Record();

// and remember the things we added while bound (this probably needs some tweaking):
Metafix.addAll(fullRecord, record);
});
}));

record = fullRecord;
}
Expand Down
Loading

0 comments on commit dc8c708

Please sign in to comment.