Skip to content

Commit dd45c79

Browse files
committed
- fix code to satisfy tests
1 parent 8298c9f commit dd45c79

File tree

5 files changed

+393
-190
lines changed

5 files changed

+393
-190
lines changed

src/eAuthor.API/Services/Expressions/ExpressionEvaluator.cs

Lines changed: 186 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,101 +2,204 @@
22
using System.Text.Json;
33
using System.Text.RegularExpressions;
44

5-
namespace eAuthor.Services.Expressions;
5+
namespace eAuthor.Services.Expressions
6+
{
7+
public class ExpressionEvaluator : IExpressionEvaluator
8+
{
9+
private static readonly Regex IndexedPartRx = new(
10+
@"^(?<name>[A-Za-z0-9_]+)(\[(?<idx>\d+)\])?$",
11+
RegexOptions.Compiled);
612

7-
public interface IExpressionEvaluator {
8-
string Evaluate(ParsedExpression expression, JsonElement root, JsonElement? relativeContext = null);
9-
JsonElement? ResolvePath(JsonElement root, string path, JsonElement? relativeContext = null);
10-
}
13+
public string Evaluate(ParsedExpression expression, JsonElement root, JsonElement? relativeContext = null)
14+
{
15+
var value = ResolvePath(root, expression.DataPath, relativeContext);
16+
var str = ValueToString(value);
1117

12-
public class ExpressionEvaluator : IExpressionEvaluator {
13-
private static readonly Regex IndexedPartRx = new(@"^(?<name>[A-Za-z0-9_]+)(\[(?<idx>\d+)\])?$", RegexOptions.Compiled);
18+
foreach (var filter in expression.Filters)
19+
str = ApplyFilter(str, filter, value);
1420

15-
public string Evaluate(ParsedExpression expression, JsonElement root, JsonElement? relativeContext = null) {
16-
var value = ResolvePath(root, expression.DataPath, relativeContext);
17-
var str = ValueToString(value);
18-
foreach (var filter in expression.Filters) str = ApplyFilter(str, filter);
19-
return str;
20-
}
21+
return str;
22+
}
2123

22-
public JsonElement? ResolvePath(JsonElement root, string path, JsonElement? relativeContext = null) {
23-
// If relative (no leading slash), try relativeContext first
24-
if (!path.StartsWith("/")) {
25-
if (relativeContext == null)
26-
return null;
27-
return Traverse(relativeContext.Value, path);
24+
public bool EvaluateBoolean(JsonElement root, ParsedExpression expression, JsonElement? relativeContext = null)
25+
{
26+
// If filters exist we evaluate them first (string-based truthiness).
27+
if (expression.Filters.Count > 0)
28+
{
29+
var evaluated = Evaluate(expression, root, relativeContext);
30+
return CoerceStringToBool(evaluated);
31+
}
32+
33+
var value = ResolvePath(root, expression.DataPath, relativeContext);
34+
return CoerceElementToBool(value);
2835
}
29-
return Traverse(root, path.TrimStart('/'));
30-
}
3136

32-
private JsonElement? Traverse(JsonElement start, string path) {
33-
var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
34-
var current = start;
35-
foreach (var part in parts) {
36-
var m = IndexedPartRx.Match(part);
37-
if (!m.Success) return null;
38-
var name = m.Groups["name"].Value;
39-
40-
if (current.ValueKind != JsonValueKind.Object)
41-
return null;
42-
43-
if (!current.TryGetProperty(name, out var child))
44-
return null;
45-
46-
if (m.Groups["idx"].Success) {
47-
var idx = int.Parse(m.Groups["idx"].Value, CultureInfo.InvariantCulture);
48-
if (child.ValueKind == JsonValueKind.Array) {
49-
if (idx < 0 || idx >= child.GetArrayLength()) return null;
50-
child = child.EnumerateArray().ElementAt(idx);
51-
} else if (child.ValueKind == JsonValueKind.Object) {
52-
// Try find first array property
53-
var arrProp = child.EnumerateObject().FirstOrDefault(p => p.Value.ValueKind == JsonValueKind.Array);
54-
if (arrProp.Value.ValueKind == JsonValueKind.Array) {
55-
if (idx < 0 || idx >= arrProp.Value.GetArrayLength()) return null;
56-
child = arrProp.Value.EnumerateArray().ElementAt(idx);
57-
} else return null;
58-
} else return null;
37+
public JsonElement? ResolvePath(JsonElement root, string path, JsonElement? relativeContext = null)
38+
{
39+
if (!path.StartsWith("/"))
40+
{
41+
if (relativeContext == null)
42+
return null;
43+
return Traverse(relativeContext.Value, path);
5944
}
60-
current = child;
45+
return Traverse(root, path.TrimStart('/'));
6146
}
62-
return current;
63-
}
6447

65-
private string ValueToString(JsonElement? el) {
66-
if (el == null) return "";
67-
return el.Value.ValueKind switch {
68-
JsonValueKind.String => el.Value.GetString() ?? "",
69-
JsonValueKind.Number => el.Value.ToString(),
70-
JsonValueKind.True => "true",
71-
JsonValueKind.False => "false",
72-
_ => ""
73-
};
74-
}
48+
private JsonElement? Traverse(JsonElement start, string path)
49+
{
50+
var parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
51+
var current = start;
7552

76-
private string ApplyFilter(string input, ExpressionFilter filter) {
77-
switch (filter.Name) {
78-
case "upper": return input.ToUpperInvariant();
79-
case "lower": return input.ToLowerInvariant();
80-
case "trim": return input.Trim();
81-
case "date":
82-
if (DateTime.TryParse(input, out var dt)) {
83-
var fmt = filter.Args.FirstOrDefault() ?? "yyyy-MM-dd";
84-
return dt.ToString(fmt, CultureInfo.InvariantCulture);
85-
}
86-
return input;
87-
case "number":
88-
if (decimal.TryParse(input, NumberStyles.Any, CultureInfo.InvariantCulture, out var dec)) {
89-
var fmt = filter.Args.FirstOrDefault() ?? "0.##";
90-
return dec.ToString(fmt, CultureInfo.InvariantCulture);
91-
}
92-
return input;
93-
case "bool":
53+
foreach (var part in parts)
54+
{
55+
var m = IndexedPartRx.Match(part);
56+
if (!m.Success)
57+
return null;
58+
var name = m.Groups["name"].Value;
59+
60+
if (current.ValueKind != JsonValueKind.Object)
61+
return null;
62+
63+
if (!current.TryGetProperty(name, out var child))
64+
return null;
65+
66+
if (m.Groups["idx"].Success)
9467
{
95-
var yes = filter.Args.ElementAtOrDefault(0) ?? "Yes";
96-
var no = filter.Args.ElementAtOrDefault(1) ?? "No";
97-
return input.Equals("true", StringComparison.OrdinalIgnoreCase) ? yes : no;
68+
var idx = int.Parse(m.Groups["idx"].Value, CultureInfo.InvariantCulture);
69+
70+
if (child.ValueKind == JsonValueKind.Array)
71+
{
72+
if (idx < 0 || idx >= child.GetArrayLength())
73+
return null;
74+
child = child.EnumerateArray().ElementAt(idx);
75+
}
76+
else if (child.ValueKind == JsonValueKind.Object)
77+
{
78+
// Best-effort fallback: first array property if indexing used on object
79+
var arrProp = child.EnumerateObject().FirstOrDefault(p => p.Value.ValueKind == JsonValueKind.Array);
80+
if (arrProp.Value.ValueKind == JsonValueKind.Array)
81+
{
82+
if (idx < 0 || idx >= arrProp.Value.GetArrayLength())
83+
return null;
84+
child = arrProp.Value.EnumerateArray().ElementAt(idx);
85+
}
86+
else
87+
return null;
88+
}
89+
else
90+
return null;
9891
}
99-
default: return input;
92+
93+
current = child;
94+
}
95+
96+
return current;
97+
}
98+
99+
private string ValueToString(JsonElement? el)
100+
{
101+
if (el == null)
102+
return "";
103+
104+
var v = el.Value;
105+
return v.ValueKind switch
106+
{
107+
JsonValueKind.String => v.GetString() ?? "",
108+
JsonValueKind.Number => v.ToString(),
109+
JsonValueKind.True => "true",
110+
JsonValueKind.False => "false",
111+
JsonValueKind.Array => v.GetArrayLength().ToString(CultureInfo.InvariantCulture),
112+
JsonValueKind.Object => v.EnumerateObject().Any() ? "[object]" : "",
113+
_ => ""
114+
};
115+
}
116+
117+
private bool CoerceElementToBool(JsonElement? el)
118+
{
119+
if (el == null)
120+
return false;
121+
var v = el.Value;
122+
123+
return v.ValueKind switch
124+
{
125+
JsonValueKind.True => true,
126+
JsonValueKind.False => false,
127+
JsonValueKind.Number => NumberIsNonZero(v),
128+
JsonValueKind.String => CoerceStringToBool(v.GetString() ?? ""),
129+
JsonValueKind.Array => v.GetArrayLength() > 0,
130+
JsonValueKind.Object => v.EnumerateObject().Any(),
131+
_ => false
132+
};
133+
}
134+
135+
private bool NumberIsNonZero(JsonElement numberElement)
136+
{
137+
if (numberElement.TryGetInt64(out var intVal))
138+
return intVal != 0;
139+
if (double.TryParse(numberElement.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var dbl))
140+
return Math.Abs(dbl) > double.Epsilon;
141+
return false;
142+
}
143+
144+
private bool CoerceStringToBool(string s)
145+
{
146+
if (string.IsNullOrWhiteSpace(s))
147+
return false;
148+
149+
var lower = s.Trim().ToLowerInvariant();
150+
if (lower is "false" or "0" or "null" or "undefined" or "nan")
151+
return false;
152+
153+
return true;
154+
}
155+
156+
private string ApplyFilter(string input, ExpressionFilter filter, JsonElement? originalElement)
157+
{
158+
switch (filter.Name)
159+
{
160+
case "upper":
161+
return input.ToUpperInvariant();
162+
163+
case "lower":
164+
return input.ToLowerInvariant();
165+
166+
case "trim":
167+
return input.Trim();
168+
169+
case "date":
170+
// If original looks like a date string, format it.
171+
if (DateTime.TryParse(input, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var dt))
172+
{
173+
var fmt = filter.Args.FirstOrDefault() ?? "yyyy-MM-dd";
174+
return dt.ToString(fmt, CultureInfo.InvariantCulture);
175+
}
176+
return input;
177+
178+
case "number":
179+
if (decimal.TryParse(input, NumberStyles.Any, CultureInfo.InvariantCulture, out var dec))
180+
{
181+
var fmt = filter.Args.FirstOrDefault();
182+
if (!string.IsNullOrWhiteSpace(fmt))
183+
return dec.ToString(fmt, CultureInfo.InvariantCulture);
184+
return dec.ToString(CultureInfo.InvariantCulture);
185+
}
186+
return input;
187+
188+
case "bool":
189+
// bool:TrueVal:FalseVal
190+
var trueText = filter.Args.Length > 0 ? filter.Args[0] : "true";
191+
var falseText = filter.Args.Length > 1 ? filter.Args[1] : "false";
192+
bool truthy;
193+
if (originalElement is { } elRef)
194+
truthy = CoerceElementToBool(elRef);
195+
else
196+
truthy = CoerceStringToBool(input);
197+
return truthy ? trueText : falseText;
198+
199+
default:
200+
// Unknown filter => no transformation
201+
return input;
202+
}
100203
}
101204
}
102205
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Text.Json;
2+
3+
namespace eAuthor.Services.Expressions;
4+
5+
public interface IExpressionEvaluator
6+
{
7+
string Evaluate(ParsedExpression expression, JsonElement root, JsonElement? relativeContext = null);
8+
9+
JsonElement? ResolvePath(JsonElement root, string path, JsonElement? relativeContext = null);
10+
11+
bool EvaluateBoolean(JsonElement root, ParsedExpression expression, JsonElement? relativeContext = null);
12+
}

0 commit comments

Comments
 (0)