Skip to content

Commit ad5d55c

Browse files
committed
Support spread and erasure in object literals
1 parent b5d40c6 commit ad5d55c

File tree

12 files changed

+215
-44
lines changed

12 files changed

+215
-44
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ The following properties are available in expressions:
116116
| String | A single-quoted Unicode string literal; to escape `'`, double it | `'pie'`, `'isn''t'`, `'😋'` |
117117
| Boolean | A Boolean value | `true`, `false` |
118118
| Array | An array of values, in square brackets | `[1, 'two', null]` |
119-
| Object | A mapping of string keys to values; keys that are valid identifiers do not need to be quoted | `{a: 1, 'b c': 2}` |
119+
| Object | A mapping of string keys to values; keys that are valid identifiers do not need to be quoted | `{a: 1, 'b c': 2, d}` |
120+
121+
Array and object literals support the spread operator: `[1, 2, ..rest]`, `{a: 1, ..other}`. Specifying an undefined
122+
property in an object literal will remove it from the result: `{..User, Email: Undefined()}`
120123

121124
### Operators and conditionals
122125

@@ -166,6 +169,7 @@ calling a function will be undefined if:
166169
| `Substring(s, start, [length])` | Return the substring of string `s` from `start` to the end of the string, or of `length` characters, if this argument is supplied. |
167170
| `TagOf(o)` | Returns the `TypeTag` field of a captured object (i.e. where `TypeOf(x)` is `'object'`). |
168171
| `TypeOf(x)` | Returns a string describing the type of expression `x`: a .NET type name if `x` is scalar and non-null, or, `'array'`, `'object'`, `'dictionary'`, `'null'`, or `'undefined'`. |
172+
| `Undefined()` | Explicitly mark an undefined value. |
169173

170174
Functions that compare text accept an optional post-fix `ci` modifier to select case-insensitive comparisons:
171175

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Serilog.Expressions.Ast
2+
{
3+
abstract class Member
4+
{
5+
}
6+
}
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1-
using System.Collections.Generic;
21
using System.Linq;
3-
using Serilog.Events;
42

53
namespace Serilog.Expressions.Ast
64
{
75
class ObjectExpression : Expression
86
{
9-
public ObjectExpression(KeyValuePair<string, Expression>[] members)
7+
public ObjectExpression(Member[] members)
108
{
119
Members = members;
1210
}
1311

14-
public KeyValuePair<string, Expression>[] Members { get; }
12+
public Member[] Members { get; }
1513

1614
public override string ToString()
1715
{
18-
return "{" + string.Join(", ", Members.Select(m => $"{new ScalarValue(m.Key)}: {m.Value}")) + "}";
16+
return "{" + string.Join(", ", Members.Select(m => m.ToString())) + "}";
1917
}
2018
}
2119
}
20+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Serilog.Events;
2+
3+
namespace Serilog.Expressions.Ast
4+
{
5+
class Property : Member
6+
{
7+
public string Name { get; }
8+
public Expression Value { get; }
9+
10+
public Property(string name, Expression value)
11+
{
12+
Name = name;
13+
Value = value;
14+
}
15+
16+
public override string ToString()
17+
{
18+
return $"{new ScalarValue(Name)}: {Value}";
19+
}
20+
}
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Serilog.Expressions.Ast
2+
{
3+
class Spread : Member
4+
{
5+
public Expression Content { get; }
6+
7+
public Spread(Expression content)
8+
{
9+
Content = content;
10+
}
11+
12+
public override string ToString()
13+
{
14+
return $"..{Content}";
15+
}
16+
}
17+
}

src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,87 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Specialized;
34
using System.IO;
45
using System.Linq;
56
using System.Text.RegularExpressions;
67
using Serilog.Events;
78
using Serilog.Formatting.Display;
9+
// ReSharper disable ParameterTypeCanBeEnumerable.Global
810

911
namespace Serilog.Expressions.Compilation.Linq
1012
{
1113
static class Intrinsics
1214
{
1315
static readonly LogEventPropertyValue NegativeOne = new ScalarValue(-1);
16+
static readonly LogEventPropertyValue Tombstone = new ScalarValue("😬 (if you see this you have found a bug.)");
1417
static readonly MessageTemplateTextFormatter MessageFormatter = new MessageTemplateTextFormatter("{Message:lj}");
1518

16-
public static LogEventPropertyValue? ConstructSequenceValue(LogEventPropertyValue?[] elements)
19+
public static LogEventPropertyValue ConstructSequenceValue(LogEventPropertyValue?[] elements)
1720
{
18-
// Avoid upsetting Serilog's (currently) fragile `SequenceValue.Render()`.
1921
if (elements.Any(el => el == null))
20-
return null;
22+
return new SequenceValue(elements.Where(el => el != null));
23+
2124
return new SequenceValue(elements);
2225
}
2326

24-
public static LogEventPropertyValue? ConstructStructureValue(string[] names, LogEventPropertyValue?[] values)
27+
public static List<LogEventProperty> CollectStructureProperties(string[] names, LogEventPropertyValue?[] values)
2528
{
2629
var properties = new List<LogEventProperty>();
2730
for (var i = 0; i < names.Length; ++i)
2831
{
32+
var name = names[i];
2933
var value = values[i];
30-
31-
// Avoid upsetting Serilog's `Structure.Render()`.
32-
if (value == null) return null;
33-
34-
properties.Add(new LogEventProperty(names[i], value));
34+
properties.Add(new LogEventProperty(name, value ?? Tombstone));
3535
}
36+
37+
return properties;
38+
}
39+
40+
public static LogEventPropertyValue ConstructStructureValue(List<LogEventProperty> properties)
41+
{
42+
if (properties.Any(p => p == null || p.Value == Tombstone))
43+
return new StructureValue(properties.Where(p => p != null && p.Value != Tombstone));
44+
3645
return new StructureValue(properties);
3746
}
3847

48+
public static List<LogEventProperty> ExtendStructureValueWithSpread(
49+
List<LogEventProperty> properties,
50+
LogEventPropertyValue? content)
51+
{
52+
if (content is StructureValue structure)
53+
{
54+
foreach (var property in structure.Properties)
55+
if (property != null)
56+
properties.Add(property);
57+
}
58+
59+
return properties;
60+
}
61+
62+
public static List<LogEventProperty> ExtendStructureValueWithProperty(
63+
List<LogEventProperty> properties,
64+
string name,
65+
LogEventPropertyValue? value)
66+
{
67+
// Mutates the list; returned so we can nest calls instead of emitting a block.
68+
properties.Add(new LogEventProperty(name, value ?? Tombstone));
69+
return properties;
70+
}
71+
72+
public static LogEventPropertyValue CompleteStructureValue(List<LogEventProperty> properties)
73+
{
74+
var result = new OrderedDictionary();
75+
foreach (var property in properties)
76+
{
77+
if (result.Contains(property.Name))
78+
result.Remove(property.Name);
79+
if (property.Value != Tombstone)
80+
result.Add(property.Name, new LogEventProperty(property.Name, property.Value));
81+
}
82+
return new StructureValue(result.Values.Cast<LogEventProperty>().ToList());
83+
}
84+
3985
public static bool CoerceToScalarBoolean(LogEventPropertyValue value)
4086
{
4187
if (value is ScalarValue sv && sv.Value is bool b)

src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,21 @@ class LinqExpressionCompiler : SerilogExpressionTransformer<ExpressionBody>
2121
static readonly MethodInfo ConstructSequenceValueMethod = typeof(Intrinsics)
2222
.GetMethod(nameof(Intrinsics.ConstructSequenceValue), BindingFlags.Static | BindingFlags.Public)!;
2323

24+
static readonly MethodInfo CollectStructurePropertiesMethod = typeof(Intrinsics)
25+
.GetMethod(nameof(Intrinsics.CollectStructureProperties), BindingFlags.Static | BindingFlags.Public)!;
26+
2427
static readonly MethodInfo ConstructStructureValueMethod = typeof(Intrinsics)
2528
.GetMethod(nameof(Intrinsics.ConstructStructureValue), BindingFlags.Static | BindingFlags.Public)!;
2629

30+
static readonly MethodInfo CompleteStructureValueMethod = typeof(Intrinsics)
31+
.GetMethod(nameof(Intrinsics.CompleteStructureValue), BindingFlags.Static | BindingFlags.Public)!;
32+
33+
static readonly MethodInfo ExtendStructureValueWithSpreadMethod = typeof(Intrinsics)
34+
.GetMethod(nameof(Intrinsics.ExtendStructureValueWithSpread), BindingFlags.Static | BindingFlags.Public)!;
35+
36+
static readonly MethodInfo ExtendStructureValueWithPropertyMethod = typeof(Intrinsics)
37+
.GetMethod(nameof(Intrinsics.ExtendStructureValueWithProperty), BindingFlags.Static | BindingFlags.Public)!;
38+
2739
static readonly MethodInfo CoerceToScalarBooleanMethod = typeof(Intrinsics)
2840
.GetMethod(nameof(Intrinsics.CoerceToScalarBoolean), BindingFlags.Static | BindingFlags.Public)!;
2941

@@ -35,7 +47,7 @@ class LinqExpressionCompiler : SerilogExpressionTransformer<ExpressionBody>
3547

3648
ParameterExpression Context { get; } = LX.Variable(typeof(LogEvent), "evt");
3749

38-
public LinqExpressionCompiler(NameResolver nameResolver)
50+
LinqExpressionCompiler(NameResolver nameResolver)
3951
{
4052
_nameResolver = nameResolver;
4153
}
@@ -173,22 +185,63 @@ protected override ExpressionBody Transform(ObjectExpression ox)
173185
{
174186
var names = new List<string>();
175187
var values = new List<ExpressionBody>();
176-
foreach (var member in ox.Members)
188+
189+
var i = 0;
190+
for (; i < ox.Members.Length; ++i)
177191
{
178-
if (names.Contains(member.Key))
192+
var member = ox.Members[i];
193+
if (member is Property property)
179194
{
180-
var oldPos = names.IndexOf(member.Key);
181-
values[oldPos] = Transform(member.Value);
195+
if (names.Contains(property.Name))
196+
{
197+
var oldPos = names.IndexOf(property.Name);
198+
values[oldPos] = Transform(property.Value);
199+
}
200+
else
201+
{
202+
names.Add(property.Name);
203+
values.Add(Transform(property.Value));
204+
}
182205
}
183206
else
184207
{
185-
names.Add(member.Key);
186-
values.Add(Transform(member.Value));
208+
break;
187209
}
188210
}
211+
189212
var namesConstant = LX.Constant(names.ToArray(), typeof(string[]));
190213
var valuesArr = LX.NewArrayInit(typeof(LogEventPropertyValue), values.ToArray());
191-
return LX.Call(ConstructStructureValueMethod, namesConstant, valuesArr);
214+
var collect = LX.Call(CollectStructurePropertiesMethod, namesConstant, valuesArr);
215+
216+
if (i == ox.Members.Length)
217+
{
218+
// No spreads
219+
return LX.Call(ConstructStructureValueMethod, collect);
220+
}
221+
222+
var extended = collect;
223+
for (; i < ox.Members.Length; ++i)
224+
{
225+
var member = ox.Members[i];
226+
if (member is Property property)
227+
{
228+
extended = LX.Call(
229+
ExtendStructureValueWithPropertyMethod,
230+
extended,
231+
LX.Constant(property.Name),
232+
Transform(property.Value));
233+
}
234+
else
235+
{
236+
var spread = (Spread) member;
237+
extended = LX.Call(
238+
ExtendStructureValueWithSpreadMethod,
239+
extended,
240+
Transform(spread.Content));
241+
}
242+
}
243+
244+
return LX.Call(CompleteStructureValueMethod, extended);
192245
}
193246

194247
protected override ExpressionBody Transform(IndexerExpression ix)

src/Serilog.Expressions/Expressions/Compilation/Transformations/IdentityTransformer.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,22 @@ protected override Expression Transform(ArrayExpression ax)
8686
protected override Expression Transform(ObjectExpression ox)
8787
{
8888
var any = false;
89-
var members = new List<KeyValuePair<string, Expression>>();
89+
var members = new List<Member>();
9090
foreach (var m in ox.Members)
9191
{
92-
if (TryTransform(m.Value, out var result))
93-
any = true;
94-
members.Add(KeyValuePair.Create(m.Key, result));
92+
if (m is Property p)
93+
{
94+
if (TryTransform(p.Value, out var result))
95+
any = true;
96+
members.Add(new Property(p.Name, result));
97+
}
98+
else
99+
{
100+
var s = (Spread) m;
101+
if (TryTransform(s.Content, out var result))
102+
any = true;
103+
members.Add(new Spread(result));
104+
}
95105
}
96106

97107
if (!any)

src/Serilog.Expressions/Expressions/Parsing/ExpressionTextParsers.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ static class ExpressionTextParsers
99
static readonly TextParser<ExpressionToken> LessOrEqual = Span.EqualTo("<=").Value(ExpressionToken.LessThanOrEqual);
1010
static readonly TextParser<ExpressionToken> GreaterOrEqual = Span.EqualTo(">=").Value(ExpressionToken.GreaterThanOrEqual);
1111
static readonly TextParser<ExpressionToken> NotEqual = Span.EqualTo("<>").Value(ExpressionToken.NotEqual);
12+
static readonly TextParser<ExpressionToken> Spread = Span.EqualTo("..").Value(ExpressionToken.Spread);
1213

13-
public static readonly TextParser<ExpressionToken> CompoundOperator = GreaterOrEqual.Or(LessOrEqual.Try().Or(NotEqual));
14+
public static readonly TextParser<ExpressionToken> CompoundOperator = GreaterOrEqual.Or(LessOrEqual.Try().Or(NotEqual)).Or(Spread);
1415

1516
public static readonly TextParser<string> HexInteger =
1617
Span.EqualTo("0x")

src/Serilog.Expressions/Expressions/Parsing/ExpressionToken.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ enum ExpressionToken
2727
[Token(Example = ".")]
2828
Period,
2929

30+
[Token(Example = "..")]
31+
Spread,
32+
3033
[Token(Example = "[")]
3134
LBracket,
3235

0 commit comments

Comments
 (0)