Add filter, name processor and value processor support to `JsonWriter`
Update `JsonWriter` to support filtering and processing of names/values. This update will allow us to offer better customization options with structured logging. See gh-42486
This commit is contained in:
parent
763266f20d
commit
27c59b8cb5
|
@ -24,9 +24,15 @@ import java.util.Deque;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.springframework.boot.json.JsonWriter.MemberPath;
|
||||||
|
import org.springframework.boot.json.JsonWriter.NameProcessor;
|
||||||
|
import org.springframework.boot.json.JsonWriter.ValueProcessor;
|
||||||
|
import org.springframework.boot.util.LambdaSafe;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.util.function.ThrowingConsumer;
|
import org.springframework.util.function.ThrowingConsumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,6 +46,10 @@ class JsonValueWriter {
|
||||||
|
|
||||||
private final Appendable out;
|
private final Appendable out;
|
||||||
|
|
||||||
|
private MemberPath path = MemberPath.ROOT;
|
||||||
|
|
||||||
|
private final Deque<JsonWriterFiltersAndProcessors> filtersAndProcessors = new ArrayDeque<>();
|
||||||
|
|
||||||
private final Deque<ActiveSeries> activeSeries = new ArrayDeque<>();
|
private final Deque<ActiveSeries> activeSeries = new ArrayDeque<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,6 +60,14 @@ class JsonValueWriter {
|
||||||
this.out = out;
|
this.out = out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void pushProcessors(JsonWriterFiltersAndProcessors jsonProcessors) {
|
||||||
|
this.filtersAndProcessors.addLast(jsonProcessors);
|
||||||
|
}
|
||||||
|
|
||||||
|
void popProcessors() {
|
||||||
|
this.filtersAndProcessors.removeLast();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write a name value pair, or just a value if {@code name} is {@code null}.
|
* Write a name value pair, or just a value if {@code name} is {@code null}.
|
||||||
* @param <N> the name type in the pair
|
* @param <N> the name type in the pair
|
||||||
|
@ -82,6 +100,7 @@ class JsonValueWriter {
|
||||||
* @param value the value to write
|
* @param value the value to write
|
||||||
*/
|
*/
|
||||||
<V> void write(V value) {
|
<V> void write(V value) {
|
||||||
|
value = processValue(value);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
append("null");
|
append("null");
|
||||||
}
|
}
|
||||||
|
@ -119,7 +138,7 @@ class JsonValueWriter {
|
||||||
*/
|
*/
|
||||||
void start(Series series) {
|
void start(Series series) {
|
||||||
if (series != null) {
|
if (series != null) {
|
||||||
this.activeSeries.push(new ActiveSeries());
|
this.activeSeries.push(new ActiveSeries(series));
|
||||||
append(series.openChar);
|
append(series.openChar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,8 +183,10 @@ class JsonValueWriter {
|
||||||
<E> void writeElement(E element) {
|
<E> void writeElement(E element) {
|
||||||
ActiveSeries activeSeries = this.activeSeries.peek();
|
ActiveSeries activeSeries = this.activeSeries.peek();
|
||||||
Assert.notNull(activeSeries, "No series has been started");
|
Assert.notNull(activeSeries, "No series has been started");
|
||||||
activeSeries.appendCommaIfRequired();
|
this.path = activeSeries.updatePath(this.path);
|
||||||
|
activeSeries.incrementIndexAndAddCommaIfRequired();
|
||||||
write(element);
|
write(element);
|
||||||
|
this.path = activeSeries.restorePath(this.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -196,12 +217,17 @@ class JsonValueWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private <N, V> void writePair(N name, V value) {
|
private <N, V> void writePair(N name, V value) {
|
||||||
ActiveSeries activeSeries = this.activeSeries.peek();
|
this.path = this.path.child(name.toString());
|
||||||
Assert.notNull(activeSeries, "No series has been started");
|
if (!isFilteredPath()) {
|
||||||
activeSeries.appendCommaIfRequired();
|
String processedName = processName(name.toString());
|
||||||
writeString(name);
|
ActiveSeries activeSeries = this.activeSeries.peek();
|
||||||
append(":");
|
Assert.notNull(activeSeries, "No series has been started");
|
||||||
write(value);
|
activeSeries.incrementIndexAndAddCommaIfRequired();
|
||||||
|
writeString(processedName);
|
||||||
|
append(":");
|
||||||
|
write(value);
|
||||||
|
}
|
||||||
|
this.path = this.path.parent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void writeString(Object value) {
|
private void writeString(Object value) {
|
||||||
|
@ -256,6 +282,48 @@ class JsonValueWriter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isFilteredPath() {
|
||||||
|
for (JsonWriterFiltersAndProcessors filtersAndProcessors : this.filtersAndProcessors) {
|
||||||
|
for (Predicate<MemberPath> pathFilter : filtersAndProcessors.pathFilters()) {
|
||||||
|
if (pathFilter.test(this.path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String processName(String name) {
|
||||||
|
for (JsonWriterFiltersAndProcessors filtersAndProcessors : this.filtersAndProcessors) {
|
||||||
|
for (NameProcessor nameProcessor : filtersAndProcessors.nameProcessors()) {
|
||||||
|
name = processName(name, nameProcessor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String processName(String name, NameProcessor nameProcessor) {
|
||||||
|
name = nameProcessor.processName(this.path, name);
|
||||||
|
Assert.state(StringUtils.hasLength(name), "NameProcessor " + nameProcessor + " returned an empty result");
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <V> V processValue(V value) {
|
||||||
|
for (JsonWriterFiltersAndProcessors filtersAndProcessors : this.filtersAndProcessors) {
|
||||||
|
for (ValueProcessor<?> valueProcessor : filtersAndProcessors.valueProcessors()) {
|
||||||
|
value = processValue(value, valueProcessor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "unchecked", "unchecked" })
|
||||||
|
private <V> V processValue(V value, ValueProcessor<?> valueProcessor) {
|
||||||
|
return (V) LambdaSafe.callback(ValueProcessor.class, valueProcessor, this.path, value)
|
||||||
|
.invokeAnd((call) -> call.processValue(this.path, value))
|
||||||
|
.get(value);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A series of items that can be written to the JSON output.
|
* A series of items that can be written to the JSON output.
|
||||||
*/
|
*/
|
||||||
|
@ -287,16 +355,27 @@ class JsonValueWriter {
|
||||||
*/
|
*/
|
||||||
private final class ActiveSeries {
|
private final class ActiveSeries {
|
||||||
|
|
||||||
private boolean commaRequired;
|
private final Series series;
|
||||||
|
|
||||||
private ActiveSeries() {
|
private int index;
|
||||||
|
|
||||||
|
private ActiveSeries(Series series) {
|
||||||
|
this.series = series;
|
||||||
}
|
}
|
||||||
|
|
||||||
void appendCommaIfRequired() {
|
MemberPath updatePath(MemberPath path) {
|
||||||
if (this.commaRequired) {
|
return (this.series != Series.ARRAY) ? path : path.child(this.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
MemberPath restorePath(MemberPath path) {
|
||||||
|
return (this.series != Series.ARRAY) ? path : path.parent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void incrementIndexAndAddCommaIfRequired() {
|
||||||
|
if (this.index > 0) {
|
||||||
append(',');
|
append(',');
|
||||||
}
|
}
|
||||||
this.commaRequired = true;
|
this.index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.function.UnaryOperator;
|
||||||
|
|
||||||
import org.springframework.boot.json.JsonValueWriter.Series;
|
import org.springframework.boot.json.JsonValueWriter.Series;
|
||||||
import org.springframework.boot.json.JsonWriter.Member.Extractor;
|
import org.springframework.boot.json.JsonWriter.Member.Extractor;
|
||||||
|
@ -147,7 +148,8 @@ public interface JsonWriter<T> {
|
||||||
* @see Members
|
* @see Members
|
||||||
*/
|
*/
|
||||||
static <T> JsonWriter<T> of(Consumer<Members<T>> members) {
|
static <T> JsonWriter<T> of(Consumer<Members<T>> members) {
|
||||||
Members<T> initializedMembers = new Members<>(members, false); // Don't inline
|
// Don't inline 'new Members' (must be outside of lambda)
|
||||||
|
Members<T> initializedMembers = new Members<>(members, false);
|
||||||
return (instance, out) -> initializedMembers.write(instance, new JsonValueWriter(out));
|
return (instance, out) -> initializedMembers.write(instance, new JsonValueWriter(out));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,6 +177,8 @@ public interface JsonWriter<T> {
|
||||||
|
|
||||||
private final Series series;
|
private final Series series;
|
||||||
|
|
||||||
|
private final JsonWriterFiltersAndProcessors jsonProcessors = new JsonWriterFiltersAndProcessors();
|
||||||
|
|
||||||
Members(Consumer<Members<T>> members, boolean contributesToExistingSeries) {
|
Members(Consumer<Members<T>> members, boolean contributesToExistingSeries) {
|
||||||
Assert.notNull(members, "'members' must not be null");
|
Assert.notNull(members, "'members' must not be null");
|
||||||
members.accept(this);
|
members.accept(this);
|
||||||
|
@ -290,6 +294,33 @@ public interface JsonWriter<T> {
|
||||||
return addMember(null, extractor);
|
return addMember(null, extractor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a filter that will be used to restrict the members written to the JSON.
|
||||||
|
* @param predicate the predicate used to filter members
|
||||||
|
*/
|
||||||
|
public void applyingPathFilter(Predicate<MemberPath> predicate) {
|
||||||
|
Assert.notNull(predicate, "'predicate' must not be null");
|
||||||
|
this.jsonProcessors.pathFilters().add(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the a {@link NameProcessor} to be applied when the JSON is written.
|
||||||
|
* @param nameProcessor the name processor to add
|
||||||
|
*/
|
||||||
|
public void applyingNameProcessor(NameProcessor nameProcessor) {
|
||||||
|
Assert.notNull(nameProcessor, "'nameProcessor' must not be null");
|
||||||
|
this.jsonProcessors.nameProcessors().add(nameProcessor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the a {@link ValueProcessor} to be applied when the JSON is written.
|
||||||
|
* @param valueProcessor the value processor to add
|
||||||
|
*/
|
||||||
|
public void applyingValueProcessor(ValueProcessor<?> valueProcessor) {
|
||||||
|
Assert.notNull(valueProcessor, "'valueProcessor' must not be null");
|
||||||
|
this.jsonProcessors.valueProcessors().add(valueProcessor);
|
||||||
|
}
|
||||||
|
|
||||||
private <V> Member<V> addMember(String name, Function<T, V> extractor) {
|
private <V> Member<V> addMember(String name, Function<T, V> extractor) {
|
||||||
Member<V> member = new Member<>(this.members.size(), name, Extractor.of(extractor));
|
Member<V> member = new Member<>(this.members.size(), name, Extractor.of(extractor));
|
||||||
this.members.add(member);
|
this.members.add(member);
|
||||||
|
@ -302,11 +333,13 @@ public interface JsonWriter<T> {
|
||||||
* @param valueWriter the JSON value writer to use
|
* @param valueWriter the JSON value writer to use
|
||||||
*/
|
*/
|
||||||
void write(T instance, JsonValueWriter valueWriter) {
|
void write(T instance, JsonValueWriter valueWriter) {
|
||||||
|
valueWriter.pushProcessors(this.jsonProcessors);
|
||||||
valueWriter.start(this.series);
|
valueWriter.start(this.series);
|
||||||
for (Member<?> member : this.members) {
|
for (Member<?> member : this.members) {
|
||||||
member.write(instance, valueWriter);
|
member.write(instance, valueWriter);
|
||||||
}
|
}
|
||||||
valueWriter.end(this.series);
|
valueWriter.end(this.series);
|
||||||
|
valueWriter.popProcessors();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -718,6 +751,117 @@ public interface JsonWriter<T> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A path used to identify a specific JSON member. Paths can be represented as strings
|
||||||
|
* in form {@code "my.json[1].item"} where elements are separated by {@code '.' } or
|
||||||
|
* {@code [<index>]}. Reserved characters are escaped using {@code '\'}.
|
||||||
|
*
|
||||||
|
* @param parent the parent of this path
|
||||||
|
* @param name the name of the member or {@code null} if the member is indexed. Path
|
||||||
|
* names are provided as they were defined when the member was added and do not
|
||||||
|
* include any {@link NameProcessor name processing}.
|
||||||
|
* @param index the index of the member or {@link MemberPath#UNINDEXED}
|
||||||
|
*/
|
||||||
|
record MemberPath(MemberPath parent, String name, int index) {
|
||||||
|
|
||||||
|
private static final String[] ESCAPED = { "\\", ".", "[", "]" };
|
||||||
|
|
||||||
|
public MemberPath {
|
||||||
|
Assert.isTrue((name != null && index < 0) || (name == null && index >= 0),
|
||||||
|
"'name' and 'index' cannot be mixed");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the member has no index.
|
||||||
|
*/
|
||||||
|
public static final int UNINDEXED = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The root of all member paths.
|
||||||
|
*/
|
||||||
|
static final MemberPath ROOT = new MemberPath(null, "", UNINDEXED);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new child from this path with the specified index.
|
||||||
|
* @param index the index of the child
|
||||||
|
* @return a new {@link MemberPath} instance
|
||||||
|
*/
|
||||||
|
public MemberPath child(int index) {
|
||||||
|
return new MemberPath(this, null, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new child from this path with the specified name.
|
||||||
|
* @param name the name of the child
|
||||||
|
* @return a new {@link MemberPath} instance
|
||||||
|
*/
|
||||||
|
public MemberPath child(String name) {
|
||||||
|
return (!StringUtils.hasLength(name)) ? this : new MemberPath(this, name, UNINDEXED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final String toString() {
|
||||||
|
return toString(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a string representation of the path without any escaping.
|
||||||
|
* @return the unescaped string representation
|
||||||
|
*/
|
||||||
|
public final String toUnescapedString() {
|
||||||
|
return toString(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toString(boolean escape) {
|
||||||
|
StringBuilder string = new StringBuilder((this.parent != null) ? this.parent.toString(escape) : "");
|
||||||
|
if (this.index >= 0) {
|
||||||
|
string.append("[").append(this.index).append("]");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
string.append((!string.isEmpty()) ? "." : "").append((!escape) ? this.name : escape(this.name));
|
||||||
|
}
|
||||||
|
return string.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escape(String name) {
|
||||||
|
for (String escape : ESCAPED) {
|
||||||
|
name = name.replace(escape, "\\" + escape);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link MemberPath} instance from the given string.
|
||||||
|
* @param value the path value
|
||||||
|
* @return a new {@link MemberPath} instance
|
||||||
|
*/
|
||||||
|
public static MemberPath of(String value) {
|
||||||
|
MemberPath path = MemberPath.ROOT;
|
||||||
|
StringBuilder buffer = new StringBuilder();
|
||||||
|
boolean escape = false;
|
||||||
|
for (char ch : value.toCharArray()) {
|
||||||
|
if (!escape && ch == '\\') {
|
||||||
|
escape = true;
|
||||||
|
}
|
||||||
|
else if (!escape && (ch == '.' || ch == '[')) {
|
||||||
|
path = path.child(buffer.toString());
|
||||||
|
buffer.setLength(0);
|
||||||
|
}
|
||||||
|
else if (!escape && ch == ']') {
|
||||||
|
path = path.child(Integer.parseUnsignedInt(buffer.toString()));
|
||||||
|
buffer.setLength(0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buffer.append(ch);
|
||||||
|
escape = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path = path.child(buffer.toString());
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface that can be used to extract name/value pairs from an element.
|
* Interface that can be used to extract name/value pairs from an element.
|
||||||
*
|
*
|
||||||
|
@ -771,4 +915,130 @@ public interface JsonWriter<T> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface that can be {@link Members#applyingNameProcessor(NameProcessor)
|
||||||
|
* applied} to {@link Members} to change names or filter members.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
interface NameProcessor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new name for the JSON member or {@code null} if the member should be
|
||||||
|
* filtered entirely.
|
||||||
|
* @param path the path of the member
|
||||||
|
* @param existingName the existing and possibly already processed name.
|
||||||
|
* @return the new name
|
||||||
|
*/
|
||||||
|
String processName(MemberPath path, String existingName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create a new {@link NameProcessor} for the given operation.
|
||||||
|
* @param operation the operation to apply
|
||||||
|
* @return a new {@link NameProcessor} instance
|
||||||
|
*/
|
||||||
|
static NameProcessor of(UnaryOperator<String> operation) {
|
||||||
|
Assert.notNull(operation, "'operation' must not be null");
|
||||||
|
return (path, existingName) -> operation.apply(existingName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback interface that can be
|
||||||
|
* {@link Members#applyingValueProcessor(ValueProcessor) applied} to {@link Members}
|
||||||
|
* to process values before they are written. Typically used to filter values, for
|
||||||
|
* example to reduce superfluous information or sanitize sensitive data.
|
||||||
|
*
|
||||||
|
* @param <T> the value type
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
interface ValueProcessor<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the value at the given path.
|
||||||
|
* @param path the path of the member containing the value
|
||||||
|
* @param value the value being written (may be {@code null})
|
||||||
|
* @return the processed value
|
||||||
|
*/
|
||||||
|
T processValue(MemberPath path, T value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new processor from this one that only applied to members with the
|
||||||
|
* given path (ignoring escape characters).
|
||||||
|
* @param path the patch to match
|
||||||
|
* @return a new {@link ValueProcessor} that only applies when the path matches
|
||||||
|
*/
|
||||||
|
default ValueProcessor<T> whenHasUnescapedPath(String path) {
|
||||||
|
return whenHasPath((candidate) -> candidate.toString(false).equals(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new processor from this one that only applied to members with the
|
||||||
|
* given path.
|
||||||
|
* @param path the patch to match
|
||||||
|
* @return a new {@link ValueProcessor} that only applies when the path matches
|
||||||
|
*/
|
||||||
|
default ValueProcessor<T> whenHasPath(String path) {
|
||||||
|
return whenHasPath(MemberPath.of(path)::equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new processor from this one that only applied to members that match
|
||||||
|
* the given path predicate.
|
||||||
|
* @param predicate the predicate that must match
|
||||||
|
* @return a new {@link ValueProcessor} that only applies when the predicate
|
||||||
|
* matches
|
||||||
|
*/
|
||||||
|
default ValueProcessor<T> whenHasPath(Predicate<MemberPath> predicate) {
|
||||||
|
return (path, value) -> (predicate.test(path)) ? processValue(path, value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new processor from this one that only applies to member with values of
|
||||||
|
* the given type.
|
||||||
|
* @param type the type that must match
|
||||||
|
* @return a new {@link ValueProcessor} that only applies when value is the given
|
||||||
|
* type.
|
||||||
|
*/
|
||||||
|
default ValueProcessor<T> whenInstanceOf(Class<?> type) {
|
||||||
|
return when(type::isInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new processor from this one that only applies to member with values
|
||||||
|
* that match the given predicate.
|
||||||
|
* @param predicate the predicate that must match
|
||||||
|
* @return a new {@link ValueProcessor} that only applies when the predicate
|
||||||
|
* matches
|
||||||
|
*/
|
||||||
|
default ValueProcessor<T> when(Predicate<T> predicate) {
|
||||||
|
return (name, value) -> (predicate.test(value)) ? processValue(name, value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to crate a new {@link ValueProcessor} that applies the given
|
||||||
|
* action.
|
||||||
|
* @param <T> the value type
|
||||||
|
* @param type the value type
|
||||||
|
* @param action the action to apply
|
||||||
|
* @return a new {@link ValueProcessor} instance
|
||||||
|
*/
|
||||||
|
static <T> ValueProcessor<T> of(Class<? extends T> type, UnaryOperator<T> action) {
|
||||||
|
return of(action).whenInstanceOf(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to crate a new {@link ValueProcessor} that applies the given
|
||||||
|
* action.
|
||||||
|
* @param <T> the value type
|
||||||
|
* @param action the action to apply
|
||||||
|
* @return a new {@link ValueProcessor} instance
|
||||||
|
*/
|
||||||
|
static <T> ValueProcessor<T> of(UnaryOperator<T> action) {
|
||||||
|
Assert.notNull(action, "'action' must not be null");
|
||||||
|
return (name, value) -> action.apply(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.json;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.springframework.boot.json.JsonWriter.MemberPath;
|
||||||
|
import org.springframework.boot.json.JsonWriter.NameProcessor;
|
||||||
|
import org.springframework.boot.json.JsonWriter.ValueProcessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal record used to hold {@link NameProcessor} and {@link ValueProcessor}
|
||||||
|
* instances.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @param pathFilters the path filters
|
||||||
|
* @param nameProcessors the name processors
|
||||||
|
* @param valueProcessors the value processors
|
||||||
|
*/
|
||||||
|
record JsonWriterFiltersAndProcessors(List<Predicate<MemberPath>> pathFilters, List<NameProcessor> nameProcessors,
|
||||||
|
List<ValueProcessor<?>> valueProcessors) {
|
||||||
|
|
||||||
|
JsonWriterFiltersAndProcessors() {
|
||||||
|
this(new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package org.springframework.boot.json;
|
package org.springframework.boot.json;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -26,9 +27,16 @@ import java.util.function.Function;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.json.JsonWriter.Member;
|
||||||
|
import org.springframework.boot.json.JsonWriter.MemberPath;
|
||||||
|
import org.springframework.boot.json.JsonWriter.Members;
|
||||||
|
import org.springframework.boot.json.JsonWriter.NameProcessor;
|
||||||
import org.springframework.boot.json.JsonWriter.PairExtractor;
|
import org.springframework.boot.json.JsonWriter.PairExtractor;
|
||||||
|
import org.springframework.boot.json.JsonWriter.ValueProcessor;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -454,6 +462,485 @@ class JsonWriterTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link MemberPath}.
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
class MemberPathTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createWhenIndexAndNamedThrowException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new MemberPath(null, "boot", 0))
|
||||||
|
.withMessage("'name' and 'index' cannot be mixed");
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new MemberPath(null, null, -1))
|
||||||
|
.withMessage("'name' and 'index' cannot be mixed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toStringReturnsUsefulString() {
|
||||||
|
assertThat(MemberPath.ROOT).hasToString("");
|
||||||
|
MemberPath spring = new MemberPath(MemberPath.ROOT, "spring", MemberPath.UNINDEXED);
|
||||||
|
MemberPath springDotBoot = new MemberPath(spring, "boot", MemberPath.UNINDEXED);
|
||||||
|
MemberPath springZero = new MemberPath(spring, null, 0);
|
||||||
|
MemberPath springZeroDotBoot = new MemberPath(springZero, "boot", MemberPath.UNINDEXED);
|
||||||
|
assertThat(spring).hasToString("spring");
|
||||||
|
assertThat(springDotBoot).hasToString("spring.boot");
|
||||||
|
assertThat(springZero).hasToString("spring[0]");
|
||||||
|
assertThat(springZeroDotBoot).hasToString("spring[0].boot");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void childWithNameCreatesChild() {
|
||||||
|
assertThat(MemberPath.ROOT.child("spring").child("boot")).hasToString("spring.boot");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void childWithNameWhenNameSpecialChars() {
|
||||||
|
assertThat(MemberPath.ROOT.child("spring.io").child("boot")).hasToString("spring\\.io.boot");
|
||||||
|
assertThat(MemberPath.ROOT.child("spring[io]").child("boot")).hasToString("spring\\[io\\].boot");
|
||||||
|
assertThat(MemberPath.ROOT.child("spring.[io]").child("boot")).hasToString("spring\\.\\[io\\].boot");
|
||||||
|
assertThat(MemberPath.ROOT.child("spring\\io").child("boot")).hasToString("spring\\\\io.boot");
|
||||||
|
assertThat(MemberPath.ROOT.child("spring.\\io").child("boot")).hasToString("spring\\.\\\\io.boot");
|
||||||
|
assertThat(MemberPath.ROOT.child("spring[\\io]").child("boot")).hasToString("spring\\[\\\\io\\].boot");
|
||||||
|
assertThat(MemberPath.ROOT.child("123").child("boot")).hasToString("123.boot");
|
||||||
|
assertThat(MemberPath.ROOT.child("1.2.3").child("boot")).hasToString("1\\.2\\.3.boot");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void childWithIndexCreatesChild() {
|
||||||
|
assertThat(MemberPath.ROOT.child("spring").child(0)).hasToString("spring[0]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ofParsesPaths() {
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("spring").child("boot"));
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("spring").child(0));
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("spring.io").child("boot"));
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("spring[io]").child("boot"));
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("spring.[io]").child("boot"));
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("spring\\io").child("boot"));
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("spring.\\io").child("boot"));
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("spring[\\io]").child("boot"));
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("123").child("boot"));
|
||||||
|
assertOfFromToString(MemberPath.ROOT.child("1.2.3").child("boot"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOfFromToString(MemberPath path) {
|
||||||
|
assertThat(MemberPath.of(path.toString())).isEqualTo(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link Members#applyingPathFilter(java.util.function.Predicate)}.
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
class PathFilterTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void filteringMember() {
|
||||||
|
JsonWriter<Person> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add("first", Person::firstName);
|
||||||
|
members.add("last", Person::lastName);
|
||||||
|
members.applyingPathFilter((path) -> path.name().equals("first"));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(new Person("spring", "boot", 10))).isEqualTo("""
|
||||||
|
{"last":"boot"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void filteringInMap() {
|
||||||
|
JsonWriter<Map<?, ?>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingPathFilter((path) -> path.name().equals("spring"));
|
||||||
|
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(Map.of("spring", "boot", "test", "test"))).isEqualTo("""
|
||||||
|
{"test":"test"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link NameProcessor}.
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
class NameProcessorTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenSimpleValue() {
|
||||||
|
JsonWriter<String> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString("test")).isEqualTo("\"test\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenMember() {
|
||||||
|
JsonWriter<Person> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add("first", Person::firstName);
|
||||||
|
members.add("last", Person::lastName);
|
||||||
|
members.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(new Person("spring", "boot", 10))).isEqualTo("""
|
||||||
|
{"FIRST":"spring","LAST":"boot"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenInMap() {
|
||||||
|
JsonWriter<Map<?, ?>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(Map.of("spring", "boot"))).isEqualTo("""
|
||||||
|
{"SPRING":"boot"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenInNestedMap() {
|
||||||
|
JsonWriter<Map<?, ?>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(Map.of("test", Map.of("spring", "boot")))).isEqualTo("""
|
||||||
|
{"TEST":{"SPRING":"boot"}}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenInPairs() {
|
||||||
|
JsonWriter<Map<?, ?>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add().usingPairs(Map::forEach);
|
||||||
|
members.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(Map.of("spring", "boot"))).isEqualTo("""
|
||||||
|
{"SPRING":"boot"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenHasNestedMembers() {
|
||||||
|
Couple couple = new Couple(PERSON, new Person("Spring", "Framework", 20));
|
||||||
|
JsonWriter<Couple> writer = JsonWriter.of((members) -> {
|
||||||
|
members.from(Couple::person1)
|
||||||
|
.usingMembers((personMembers) -> personMembers.add("one", Person::toString));
|
||||||
|
members.from(Couple::person2)
|
||||||
|
.usingMembers((personMembers) -> personMembers.add("two", Person::toString));
|
||||||
|
members.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(couple)).isEqualTo("""
|
||||||
|
{"ONE":"Spring Boot (10)","TWO":"Spring Framework (20)"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenHasNestedMembersWithAdditionalValueProcessor() {
|
||||||
|
Couple couple = new Couple(PERSON, new Person("Spring", "Framework", 20));
|
||||||
|
JsonWriter<Couple> writer = JsonWriter.of((members) -> {
|
||||||
|
members.from(Couple::person1)
|
||||||
|
.usingMembers((personMembers) -> personMembers.add("one", Person::toString));
|
||||||
|
members.from(Couple::person2).usingMembers((personMembers) -> {
|
||||||
|
personMembers.add("two", Person::toString);
|
||||||
|
personMembers.applyingNameProcessor(NameProcessor.of(String::toUpperCase));
|
||||||
|
});
|
||||||
|
members.applyingNameProcessor(NameProcessor.of((name) -> name + "!"));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(couple)).isEqualTo("""
|
||||||
|
{"one!":"Spring Boot (10)","TWO!":"Spring Framework (20)"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenDeeplyNestedUsesCompoundPaths() {
|
||||||
|
List<String> paths = new ArrayList<>();
|
||||||
|
JsonWriter<Couple> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add("one", Couple::person1).usingMembers((personMembers) -> {
|
||||||
|
personMembers.add("first", Person::firstName);
|
||||||
|
personMembers.add("last", Person::lastName);
|
||||||
|
});
|
||||||
|
members.add("two", Couple::person2).usingMembers((personMembers) -> {
|
||||||
|
personMembers.add("first", Person::firstName);
|
||||||
|
personMembers.add("last", Person::lastName);
|
||||||
|
});
|
||||||
|
members.applyingNameProcessor((path, existingName) -> {
|
||||||
|
paths.add(path.toString());
|
||||||
|
return existingName;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Couple couple = new Couple(PERSON, new Person("Spring", "Framework", 20));
|
||||||
|
writer.writeToString(couple);
|
||||||
|
assertThat(paths).containsExactly("one", "one.first", "one.last", "two", "two.first", "two.last");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenReturnsNullThrowsException() {
|
||||||
|
JsonWriter<Person> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add("first", Person::firstName);
|
||||||
|
members.add("last", Person::lastName);
|
||||||
|
members
|
||||||
|
.applyingNameProcessor((path, existingName) -> !"first".equals(existingName) ? existingName : null);
|
||||||
|
});
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> writer.writeToString(new Person("spring", "boot", 10)))
|
||||||
|
.withMessageContaining("NameProcessor")
|
||||||
|
.withMessageContaining("returned an empty result");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processNameWhenReturnsEmptyStringThrowsException() {
|
||||||
|
JsonWriter<Person> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add("first", Person::firstName);
|
||||||
|
members.add("last", Person::lastName);
|
||||||
|
members
|
||||||
|
.applyingNameProcessor((path, existingName) -> !"first".equals(existingName) ? existingName : "");
|
||||||
|
});
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> writer.writeToString(new Person("spring", "boot", 10)))
|
||||||
|
.withMessageContaining("NameProcessor")
|
||||||
|
.withMessageContaining("returned an empty result");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ValueProcessor}.
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
class ValueProcessorTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void of() {
|
||||||
|
ValueProcessor<String> processor = ValueProcessor.of(String::toUpperCase);
|
||||||
|
assertThat(processor.processValue(null, "test")).isEqualTo("TEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ofWhenNull() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> ValueProcessor.of(null))
|
||||||
|
.withMessage("'action' must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenHasPathWithStringWhenPathMatches() {
|
||||||
|
ValueProcessor<String> processor = ValueProcessor.<String>of(String::toUpperCase).whenHasPath("foo");
|
||||||
|
assertThat(processor.processValue(MemberPath.ROOT.child("foo"), "test")).isEqualTo("TEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenHasPathWithStringWhenPathDoesNotMatch() {
|
||||||
|
ValueProcessor<String> processor = ValueProcessor.<String>of(String::toUpperCase).whenHasPath("foo");
|
||||||
|
assertThat(processor.processValue(MemberPath.ROOT.child("bar"), "test")).isEqualTo("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenHasPathWithPredicateWhenPathMatches() {
|
||||||
|
ValueProcessor<String> processor = ValueProcessor.<String>of(String::toUpperCase)
|
||||||
|
.whenHasPath((path) -> path.toString().startsWith("f"));
|
||||||
|
assertThat(processor.processValue(MemberPath.ROOT.child("foo"), "test")).isEqualTo("TEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenHasPathWithPredicateWhenPathDoesNotMatch() {
|
||||||
|
ValueProcessor<String> processor = ValueProcessor.<String>of(String::toUpperCase)
|
||||||
|
.whenHasPath((path) -> path.toString().startsWith("f"));
|
||||||
|
assertThat(processor.processValue(MemberPath.ROOT.child("bar"), "test")).isEqualTo("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenInstanceOfWhenInstanceMatches() {
|
||||||
|
ValueProcessor<Object> processor = ValueProcessor.of((value) -> value.toString().toUpperCase())
|
||||||
|
.whenInstanceOf(String.class);
|
||||||
|
assertThat(processor.processValue(null, "test")).hasToString("TEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenInstanceOfWhenInstanceDoesNotMatch() {
|
||||||
|
ValueProcessor<Object> processor = ValueProcessor.of((value) -> value.toString().toUpperCase())
|
||||||
|
.whenInstanceOf(String.class);
|
||||||
|
assertThat(processor.processValue(null, new StringBuilder("test"))).hasToString("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenWhenPredicateMatches() {
|
||||||
|
ValueProcessor<String> processor = ValueProcessor.<String>of(String::toUpperCase).when("test"::equals);
|
||||||
|
assertThat(processor.processValue(null, "test")).isEqualTo("TEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenWhenPredicateDoesNotMatch() {
|
||||||
|
ValueProcessor<String> processor = ValueProcessor.<String>of(String::toUpperCase).when("test"::equals);
|
||||||
|
assertThat(processor.processValue(null, "other")).isEqualTo("other");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenSimpleValue() {
|
||||||
|
JsonWriter<String> writer = simpleWriterWithUppercaseProcessor();
|
||||||
|
assertThat(writer.writeToString("test")).isEqualTo("\"TEST\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenMemberValue() {
|
||||||
|
JsonWriter<Person> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add("first", Person::firstName);
|
||||||
|
members.add("last", Person::lastName);
|
||||||
|
members.applyingValueProcessor(ValueProcessor.of(StringUtils::capitalize));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(new Person("spring", "boot", 10))).isEqualTo("""
|
||||||
|
{"first":"Spring","last":"Boot"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenInMap() {
|
||||||
|
JsonWriter<Map<?, ?>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingValueProcessor(ValueProcessor.of(StringUtils::capitalize));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(Map.of("spring", "boot"))).isEqualTo("""
|
||||||
|
{"spring":"Boot"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenInNestedMap() {
|
||||||
|
JsonWriter<Map<?, ?>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingValueProcessor(ValueProcessor.of(StringUtils::capitalize));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(Map.of("test", Map.of("spring", "boot")))).isEqualTo("""
|
||||||
|
{"test":{"spring":"Boot"}}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenInPairs() {
|
||||||
|
JsonWriter<Map<?, ?>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add().usingPairs(Map::forEach);
|
||||||
|
members.applyingValueProcessor(ValueProcessor.of(StringUtils::capitalize));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(Map.of("spring", "boot"))).isEqualTo("""
|
||||||
|
{"spring":"Boot"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenCalledWithMultipleTypesIgnoresLambdaErrors() {
|
||||||
|
JsonWriter<Object> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingValueProcessor(ValueProcessor.of(StringUtils::capitalize));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString("spring")).isEqualTo("\"Spring\"");
|
||||||
|
assertThat(writer.writeToString(123)).isEqualTo("123");
|
||||||
|
assertThat(writer.writeToString(true)).isEqualTo("true");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenLimitedToPath() {
|
||||||
|
JsonWriter<Map<?, ?>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingValueProcessor(ValueProcessor.of(StringUtils::capitalize).whenHasPath("spring"));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(Map.of("spring", "boot"))).isEqualTo("""
|
||||||
|
{"spring":"Boot"}""");
|
||||||
|
assertThat(writer.writeToString(Map.of("boot", "spring"))).isEqualTo("""
|
||||||
|
{"boot":"spring"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhen() {
|
||||||
|
JsonWriter<Map<?, ?>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingValueProcessor(
|
||||||
|
ValueProcessor.of(StringUtils::capitalize).when((candidate) -> candidate.startsWith("b")));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(Map.of("spring", "boot"))).isEqualTo("""
|
||||||
|
{"spring":"Boot"}""");
|
||||||
|
assertThat(writer.writeToString(Map.of("boot", "spring"))).isEqualTo("""
|
||||||
|
{"boot":"spring"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenHasNestedMembers() {
|
||||||
|
Couple couple = new Couple(PERSON, new Person("Spring", "Framework", 20));
|
||||||
|
JsonWriter<Couple> writer = JsonWriter.of((members) -> {
|
||||||
|
members.from(Couple::person1)
|
||||||
|
.usingMembers((personMembers) -> personMembers.add("one", Person::toString));
|
||||||
|
members.from(Couple::person2)
|
||||||
|
.usingMembers((personMembers) -> personMembers.add("two", Person::toString));
|
||||||
|
members.applyingValueProcessor(ValueProcessor.of(String.class, String::toUpperCase));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(couple)).isEqualTo("""
|
||||||
|
{"one":"SPRING BOOT (10)","two":"SPRING FRAMEWORK (20)"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenHasNestedMembersWithAdditionalValueProcessor() {
|
||||||
|
Couple couple = new Couple(PERSON, new Person("Spring", "Framework", 20));
|
||||||
|
JsonWriter<Couple> writer = JsonWriter.of((members) -> {
|
||||||
|
members.from(Couple::person1)
|
||||||
|
.usingMembers((personMembers) -> personMembers.add("one", Person::toString));
|
||||||
|
members.from(Couple::person2).usingMembers((personMembers) -> {
|
||||||
|
personMembers.add("two", Person::toString);
|
||||||
|
personMembers.applyingValueProcessor(ValueProcessor.of(String.class, (item) -> item + "!"));
|
||||||
|
});
|
||||||
|
members.applyingValueProcessor(ValueProcessor.of(String.class, String::toUpperCase));
|
||||||
|
});
|
||||||
|
assertThat(writer.writeToString(couple)).isEqualTo("""
|
||||||
|
{"one":"SPRING BOOT (10)","two":"SPRING FRAMEWORK (20)!"}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenDeeplyNestedUsesCompoundPaths() {
|
||||||
|
List<String> paths = new ArrayList<>();
|
||||||
|
JsonWriter<Couple> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add("one", Couple::person1).usingMembers((personMembers) -> {
|
||||||
|
personMembers.add("first", Person::firstName);
|
||||||
|
personMembers.add("last", Person::lastName);
|
||||||
|
});
|
||||||
|
members.add("two", Couple::person2).usingMembers((personMembers) -> {
|
||||||
|
personMembers.add("first", Person::firstName);
|
||||||
|
personMembers.add("last", Person::lastName);
|
||||||
|
});
|
||||||
|
members.applyingValueProcessor((path, value) -> {
|
||||||
|
paths.add(path.toString());
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Couple couple = new Couple(PERSON, new Person("Spring", "Framework", 20));
|
||||||
|
writer.writeToString(couple);
|
||||||
|
assertThat(paths).containsExactly("one", "one.first", "one.last", "two", "two.first", "two.last");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueWhenUsingListUsesIndexedPaths() {
|
||||||
|
List<String> paths = new ArrayList<>();
|
||||||
|
JsonWriter<List<String>> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingValueProcessor((path, value) -> {
|
||||||
|
paths.add(path.toString());
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
writer.writeToString(List.of("a", "b", "c"));
|
||||||
|
assertThat(paths).containsExactly("", "[0]", "[1]", "[2]");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void processValueUsesUnprocessedNameInPath() {
|
||||||
|
List<String> paths = new ArrayList<>();
|
||||||
|
JsonWriter<Person> writer = JsonWriter.of((members) -> {
|
||||||
|
members.add("first", Person::firstName);
|
||||||
|
members.add("last", Person::lastName);
|
||||||
|
members.applyingValueProcessor((path, value) -> {
|
||||||
|
paths.add(path.toString());
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
members.applyingNameProcessor((path, existingName) -> "the-" + existingName);
|
||||||
|
});
|
||||||
|
writer.writeToString(PERSON);
|
||||||
|
assertThat(paths).containsExactly("first", "last");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonWriter<String> simpleWriterWithUppercaseProcessor() {
|
||||||
|
return JsonWriter.of((members) -> {
|
||||||
|
members.add();
|
||||||
|
members.applyingValueProcessor(ValueProcessor.of(String.class, String::toUpperCase));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
record Person(String firstName, String lastName, int age) {
|
record Person(String firstName, String lastName, int age) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Reference in New Issue