22using System . Text . Json ;
33using 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}
0 commit comments