Skip to content

Commit 6791f1c

Browse files
committed
introduce DebuggableTransformerFunctions to debug each step in tranformer operation. (java-0.6.0, js-1.1.0, js-core-1.1.1)
1 parent 9542199 commit 6791f1c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+667
-176
lines changed

docs/docs/functions/date.md

+52
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,58 @@ Date part of ISO 8601,
140140
</div>
141141
```
142142

143+
## DIFF
144+
145+
Calculate the difference between two dates in specified units.
146+
147+
### Usage
148+
```transformers
149+
"$$date(DIFF,{units},{end}):{input}"
150+
```
151+
### Returns
152+
`integer`
153+
### Arguments
154+
| Argument | Type | Values | Required / Default&nbsp;Value | Description |
155+
|----------|-----------------------|-----------------------------------|-------------------------------|--------------------------------|
156+
| `units` | `Enum` (`ChronoUnit`) | `SECONDS`/`MINUTES`/ ... /`DAYS` | Yes | The units of calculated result |
157+
| `end` | `Date` | | Yes | End date |
158+
159+
### Examples
160+
161+
```mdx-code-block
162+
<div className="examples_grid">
163+
```
164+
165+
**Input**
166+
167+
**Definition**
168+
169+
**Output**
170+
171+
```json
172+
"2024-01-01"
173+
```
174+
```transformers
175+
"$$date(DIFF,DAYS,2025-01-01):$"
176+
```
177+
```json
178+
366
179+
```
180+
181+
```json
182+
"2025-01-01"
183+
```
184+
```transformers
185+
"$$date(DIFF,DAYS,2026-01-01):$"
186+
```
187+
```json
188+
365
189+
```
190+
191+
```mdx-code-block
192+
</div>
193+
```
194+
143195
## EPOCH
144196

145197
Seconds passed since 1970-01-01; unless `type`=`MS` then milliseconds,

java/json-transform/build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins {
99
}
1010

1111
group 'co.nlighten'
12-
version = '0.5.2'
12+
version = '0.6.0'
1313

1414
ext {
1515
gsonVersion = "2.10.1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package co.nlighten.jsontransform;
2+
3+
import co.nlighten.jsontransform.adapters.JsonAdapter;
4+
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
public class DebuggableTransformerFunctions<JE, JA extends Iterable<JE>, JO extends JE> extends TransformerFunctions<JE, JA, JO>{
9+
private final Map<String, TransformerDebugInfo> debugResults;
10+
11+
public record TransformerDebugInfo(Object result) {}
12+
13+
public DebuggableTransformerFunctions(JsonAdapter<JE, JA, JO> adapter) {
14+
super(adapter);
15+
debugResults = new HashMap<>();
16+
}
17+
18+
private TransformerFunctions.FunctionMatchResult<Object> auditAndReturn(String path, TransformerFunctions.FunctionMatchResult<Object> matchResult) {
19+
if (matchResult == null) {
20+
return null;
21+
}
22+
// if the function result is the transformer's output, don't audit it
23+
if ("$".equals(path)) return matchResult;
24+
25+
if (matchResult.result() instanceof JsonElementStreamer<?,?,?> streamer) {
26+
debugResults.put(matchResult.resultPath(), new TransformerDebugInfo(streamer.toJsonArray()));
27+
return matchResult;
28+
}
29+
debugResults.put(matchResult.resultPath(), new TransformerDebugInfo(matchResult.result()));
30+
return matchResult;
31+
}
32+
33+
public TransformerFunctions.FunctionMatchResult<Object> matchObject(String path, JO definition, ParameterResolver resolver, JsonTransformerFunction<JE> transformer) {
34+
return auditAndReturn(path, super.matchObject(path, definition, resolver, transformer));
35+
}
36+
37+
public TransformerFunctions.FunctionMatchResult<Object> matchInline(String path, String value, ParameterResolver resolver, JsonTransformerFunction<JE> transformer) {
38+
return auditAndReturn(path, super.matchInline(path, value, resolver, transformer));
39+
}
40+
41+
public Map<String, TransformerDebugInfo> getDebugResults() {
42+
return debugResults;
43+
}
44+
}

java/json-transform/src/main/java/co/nlighten/jsontransform/JsonElementStreamer.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
public class JsonElementStreamer<JE, JA extends Iterable<JE>, JO extends JE> {
99
private final FunctionContext<JE, JA, JO> context;
1010
private final boolean transformed;
11-
private final JA value;
11+
private JA value;
1212
private final Stream<JE> stream;
1313

1414
private JsonElementStreamer(FunctionContext<JE, JA, JO> context, JA arr, boolean transformed) {
@@ -34,7 +34,7 @@ public Stream<JE> stream() {
3434
}
3535

3636
public Stream<JE> stream(Long skip, Long limit) {
37-
if (this.stream != null) {
37+
if (this.stream != null && this.value == null) {
3838
var skipped = skip != null ? this.stream.skip(skip) : this.stream;
3939
return limit != null ? skipped.limit(limit) : skipped;
4040
}
@@ -72,6 +72,7 @@ public JA toJsonArray() {
7272
if (stream != null) {
7373
stream.forEach(item -> context.jArray.add(ja, item));
7474
}
75+
value = ja;
7576
return ja;
7677
}
7778
}

java/json-transform/src/main/java/co/nlighten/jsontransform/JsonTransformer.java

+23-19
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,27 @@
44
import com.google.gson.JsonNull;
55

66
import java.util.Map;
7+
import java.util.concurrent.atomic.AtomicInteger;
78

89
/**
910
* A transformer is used to transform data from one layout to another
1011
*/
1112
public abstract class JsonTransformer<JE, JA extends Iterable<JE>, JO extends JE> implements Transformer {
1213

1314
static final String OBJ_DESTRUCT_KEY = "*";
14-
static final String FUNCTION_PREFIX = "$$";
15+
static final String NULL_VALUE = "#null";
1516

1617
private final JsonAdapter<JE, JA, JO> adapter;
1718
protected final JE definition;
1819

1920
private final JsonTransformerFunction<JE> JSON_TRANSFORMER;
20-
private final TransformerFunctions<JE, JA, JO> transformerFunctions;
21+
private final TransformerFunctionsAdapter<JE, JA, JO> transformerFunctions;
2122

2223
public JsonTransformer(
23-
final TransformerFunctions<JE, JA, JO> transformerFunctions,
2424
final JsonAdapter<JE, JA, JO> adapter,
25-
final JE definition) {
26-
this.transformerFunctions = transformerFunctions;
25+
final JE definition,
26+
final TransformerFunctionsAdapter<JE, JA, JO> functionsAdapter) {
27+
this.transformerFunctions = functionsAdapter;
2728
this.adapter = adapter;
2829
this.definition = definition;
2930
this.JSON_TRANSFORMER = this::fromJsonElement;
@@ -34,18 +35,18 @@ public Object transform(Object payload, Map<String, Object> additionalContext, b
3435
return JsonNull.INSTANCE;
3536
}
3637
var resolver = adapter.createPayloadResolver(payload, additionalContext, false);
37-
return fromJsonElement(definition, resolver, allowReturningStreams);
38+
return fromJsonElement("$", definition, resolver, allowReturningStreams);
3839
}
3940

40-
protected Object fromJsonPrimitive(JE definition, co.nlighten.jsontransform.ParameterResolver resolver, boolean allowReturningStreams) {
41+
protected Object fromJsonPrimitive(String path, JE definition, co.nlighten.jsontransform.ParameterResolver resolver, boolean allowReturningStreams) {
4142
if (!adapter.isJsonString(definition))
4243
return definition;
4344
try {
4445
var val = adapter.getAsString(definition);
4546
// test for inline function (e.g. $$function:...)
46-
var match = transformerFunctions.matchInline(val, resolver, JSON_TRANSFORMER);
47+
var match = transformerFunctions.matchInline(path, val, resolver, JSON_TRANSFORMER);
4748
if (match != null) {
48-
var matchResult = match.getResult();
49+
var matchResult = match.result();
4950
if (matchResult instanceof JsonElementStreamer streamer) {
5051
return allowReturningStreams ? streamer : streamer.toJsonArray();
5152
}
@@ -61,10 +62,10 @@ protected Object fromJsonPrimitive(JE definition, co.nlighten.jsontransform.Para
6162
}
6263

6364

64-
protected Object fromJsonObject(JO definition, co.nlighten.jsontransform.ParameterResolver resolver, boolean allowReturningStreams) {
65-
var match = transformerFunctions.matchObject(definition, resolver, JSON_TRANSFORMER);
65+
protected Object fromJsonObject(String path, JO definition, co.nlighten.jsontransform.ParameterResolver resolver, boolean allowReturningStreams) {
66+
var match = transformerFunctions.matchObject(path, definition, resolver, JSON_TRANSFORMER);
6667
if (match != null) {
67-
var res = match.getResult();
68+
var res = match.result();
6869
return res instanceof JsonElementStreamer s
6970
? (allowReturningStreams ? s : s.toJsonArray())
7071
: adapter.wrap(res);
@@ -73,7 +74,7 @@ protected Object fromJsonObject(JO definition, co.nlighten.jsontransform.Paramet
7374
var result = adapter.jObject.create();
7475
if (adapter.jObject.has(definition, OBJ_DESTRUCT_KEY)) {
7576
var val = adapter.jObject.get(definition, OBJ_DESTRUCT_KEY);
76-
var res = (JE) fromJsonElement(val, resolver, false);
77+
var res = (JE) fromJsonElement(path + "[\"*\"]", val, resolver, false);
7778
if (res != null) {
7879
var isArray = adapter.jArray.is(val);
7980
if (isArray && adapter.jArray.is(res)) {
@@ -98,13 +99,15 @@ protected Object fromJsonObject(JO definition, co.nlighten.jsontransform.Paramet
9899
if (kv.getKey().equals(OBJ_DESTRUCT_KEY)) continue;
99100
var localKey = kv.getKey();
100101
var localValue = kv.getValue();
101-
if (adapter.isJsonString(localValue) && adapter.getAsString(localValue).equals("#null")) {
102+
if (adapter.isJsonString(localValue) && adapter.getAsString(localValue).equals(NULL_VALUE)) {
102103
// don't define key if #null was used
103104
// might already exist, so try removing it
104105
adapter.jObject.remove(result, localKey);
105106
continue;
106107
}
107-
var value = (JE) fromJsonElement(localValue, resolver, false);
108+
var value = (JE) fromJsonElement(
109+
path + JsonTransformerUtils.toObjectFieldPath(adapter, localKey),
110+
localValue, resolver, false);
108111
if (!adapter.isNull(value) || adapter.jObject.has(result, localKey) /* we allow overriding with null*/) {
109112
adapter.jObject.add(result, localKey, value);
110113
}
@@ -113,20 +116,21 @@ protected Object fromJsonObject(JO definition, co.nlighten.jsontransform.Paramet
113116
return result;
114117
}
115118

116-
protected Object fromJsonElement(JE definition, ParameterResolver resolver, boolean allowReturningStreams) {
119+
protected Object fromJsonElement(String path, JE definition, ParameterResolver resolver, boolean allowReturningStreams) {
117120
if (adapter.isNull(definition))
118121
return adapter.jsonNull();
119122
if (adapter.jArray.is(definition)) {
120123
var result = adapter.jArray.create();
124+
var index = new AtomicInteger(0);
121125
adapter.jArray.stream((JA)definition)
122-
.map(d -> (JE)fromJsonElement(d, resolver, false))
126+
.map(d -> (JE)fromJsonElement(path + "[" + index.getAndIncrement() + "]", d, resolver, false))
123127
.forEachOrdered(item -> adapter.jArray.add(result, item));
124128
return result;
125129
}
126130
if (adapter.jObject.is(definition)) {
127-
return fromJsonObject((JO)definition, resolver, allowReturningStreams);
131+
return fromJsonObject(path, (JO)definition, resolver, allowReturningStreams);
128132
}
129-
return fromJsonPrimitive(definition, resolver, allowReturningStreams);
133+
return fromJsonPrimitive(path, definition, resolver, allowReturningStreams);
130134
}
131135

132136
public JE getDefinition() {

java/json-transform/src/main/java/co/nlighten/jsontransform/JsonTransformerFunction.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ public interface JsonTransformerFunction<JE> {
55
/**
66
* @return JsonElement | JsonElementStreamer
77
*/
8-
Object transform(JE definition, ParameterResolver resolver, boolean allowReturningStreams);
8+
Object transform(String path, JE definition, ParameterResolver resolver, boolean allowReturningStreams);
99
}

java/json-transform/src/main/java/co/nlighten/jsontransform/JsonTransformerUtils.java

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
public class JsonTransformerUtils {
1111

1212
private static Pattern variableDetectionRegExp = variableDetectionRegExpFactory(null, null);
13+
static final Pattern validIdRegExp = Pattern.compile("^[a-zA-Z_$][a-zA-Z0-9_$]*$");
1314

1415
public static Pattern variableDetectionRegExpFactory(Integer flags, List<String> altNames) {
1516
var altPrefixes = altNames != null && !altNames.isEmpty()
@@ -78,4 +79,8 @@ public static void setVariableDetectionRegExp(Integer flags, List<String> altNam
7879
public static Pattern getVariableDetectionRegExp() {
7980
return variableDetectionRegExp;
8081
}
82+
83+
public static <JE, JA extends Iterable<JE>, JO extends JE> String toObjectFieldPath(JsonAdapter<JE, JA, JO> adapter, String key) {
84+
return validIdRegExp.matcher(key).matches() ? "." + key : "[" + adapter.toString(key) + "]";
85+
}
8186
}

java/json-transform/src/main/java/co/nlighten/jsontransform/TransformerFunctions.java

+17-29
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import java.util.regex.Pattern;
1515
import java.util.stream.Stream;
1616

17-
public class TransformerFunctions<JE, JA extends Iterable<JE>, JO extends JE> {
17+
public class TransformerFunctions<JE, JA extends Iterable<JE>, JO extends JE> implements TransformerFunctionsAdapter<JE, JA, JO>{
1818
static final Logger log = LoggerFactory.getLogger(TransformerFunctions.class);
1919

2020
private static final Pattern inlineFunctionRegex = Pattern.compile("^\\$\\$(\\w+)(\\((.*?)\\))?(:|$)");
@@ -135,7 +135,7 @@ public void registerFunctions(Map.Entry<String, TransformerFunction<JE, JA, JO>>
135135
/**
136136
* Checks the context for a registered object function and returns the result if matched
137137
*/
138-
public FunctionMatchResult<Object> matchObject(JO definition, co.nlighten.jsontransform.ParameterResolver resolver, JsonTransformerFunction<JE> transformer) {
138+
public FunctionMatchResult<Object> matchObject(String path, JO definition, co.nlighten.jsontransform.ParameterResolver resolver, JsonTransformerFunction<JE> transformer) {
139139
if (definition == null) {
140140
return null;
141141
}
@@ -145,23 +145,25 @@ public FunctionMatchResult<Object> matchObject(JO definition, co.nlighten.jsontr
145145
if (jsonAdapter.jObject.has(definition, FUNCTION_KEY_PREFIX + key)) {
146146
var func = functions.get(key);
147147
var context = new ObjectFunctionContext<>(
148+
path,
148149
definition,
149150
jsonAdapter,
150151
FUNCTION_KEY_PREFIX + key,
151152
func, resolver, transformer);
153+
var resolvedPath = path + "." + FUNCTION_KEY_PREFIX + key;
152154
try {
153-
return new FunctionMatchResult<>(func.apply(context));
155+
return new FunctionMatchResult<>(func.apply(context), resolvedPath);
154156
} catch (Throwable ex) {
155-
log.warn("Failed running object function ", ex);
156-
return new FunctionMatchResult<>(null);
157+
log.warn("Failed running object function (at {})", resolvedPath, ex);
158+
return new FunctionMatchResult<>(null, resolvedPath);
157159
}
158160
}
159161
}
160162
// didn't find an object function
161163
return null;
162164
}
163165

164-
private InlineFunctionContext<JE, JA, JO> tryParseInlineFunction(String value, co.nlighten.jsontransform.ParameterResolver resolver,
166+
private InlineFunctionContext<JE, JA, JO> tryParseInlineFunction(String path, String value, co.nlighten.jsontransform.ParameterResolver resolver,
165167
JsonTransformerFunction<JE> transformer) {
166168
var matcher = inlineFunctionRegex.matcher(value);
167169
if (matcher.find()) {
@@ -195,6 +197,7 @@ private InlineFunctionContext<JE, JA, JO> tryParseInlineFunction(String value, c
195197
input = value.substring(matchEndIndex);
196198
}
197199
return new InlineFunctionContext<>(
200+
path + "/" + FUNCTION_KEY_PREFIX + functionKey,
198201
input, args,
199202
jsonAdapter,
200203
functionKey,
@@ -205,20 +208,21 @@ private InlineFunctionContext<JE, JA, JO> tryParseInlineFunction(String value, c
205208
return null;
206209
}
207210

208-
public FunctionMatchResult<Object> matchInline(String value, ParameterResolver resolver, JsonTransformerFunction<JE> transformer) {
211+
public FunctionMatchResult<Object> matchInline(String path, String value, ParameterResolver resolver, JsonTransformerFunction<JE> transformer) {
209212
if (value == null) return null;
210-
var context = tryParseInlineFunction(value, resolver, transformer);
213+
var context = tryParseInlineFunction(path, value, resolver, transformer);
211214
if (context == null) {
212215
return null;
213216
}
214217
// at this point we detected an inline function, we must return a match result
218+
var resolvedPath = context.getPathFor(null);
215219
try {
216220
var result = functions.get(context.getAlias()).apply(context);
217-
return new FunctionMatchResult<>(result);
221+
return new FunctionMatchResult<>(result, resolvedPath);
218222
} catch (Throwable ex) {
219-
log.warn("Failed running inline function ", ex);
223+
log.warn("Failed running inline function (at {})", resolvedPath, ex);
220224
}
221-
return new FunctionMatchResult<>(null);
225+
return new FunctionMatchResult<>(null, resolvedPath);
222226
}
223227

224228
public Map<String, TransformerFunction<JE, JA, JO>> getFunctions() {
@@ -227,24 +231,8 @@ public Map<String, TransformerFunction<JE, JA, JO>> getFunctions() {
227231

228232
/**
229233
* The purpose of this class is to differentiate between null and null result
234+
*
230235
* @param <T>
231236
*/
232-
static class FunctionMatchResult<T> {
233-
final T result;
234-
235-
/**
236-
* Wrap a function result.
237-
* @param result the result of the function
238-
*/
239-
public FunctionMatchResult(T result) {
240-
this.result = result;
241-
}
242-
243-
/**
244-
* @return the result of the function
245-
*/
246-
public T getResult() {
247-
return result;
248-
}
249-
}
237+
public record FunctionMatchResult<T>(T result, String resultPath) {}
250238
}

0 commit comments

Comments
 (0)