Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ terminal output:
```csharp
var template = new ExpressionTemplate(
"[{@Timestamp:HH:mm:ss} {@Level:u3}] {@Message}\n{@Exception}",
theme: TemplateTheme.Code);
encoder: TemplateOutputEncoder.Ansi(TemplateTheme.Code));
```

Themes can be customized by overriding the styles of a base theme:
Expand All @@ -102,14 +102,14 @@ var custom = new AnsiTheme((AnsiTheme)TemplateTheme.Literate, new Dictionary<Tem

### Escaping text inserted into HTML message bodies

`TemplateOutputEscaper.Html` escapes event-derived values automatically, so they can be safely
`TemplateOutputEncoder.Html` escapes event-derived values automatically, so they can be safely
inserted into HTML attributes and element bodies (excluding script and style contexts, in which
no safe escaping is possible).

```csharp
var template = new ExpressionTemplate(
"<p>{@Message}</p>",
escaper: TemplateOutputEscaper.Html);
encoder: TemplateOutputEncoder.Html);
```

Where an event property is known to contain trusted, well-formed HTML, `{unsafe(Markup)}`
Expand Down
5 changes: 2 additions & 3 deletions src/Seq.Syntax/Compatibility/V1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
using Seq.Syntax.Templates.Compilation.NameResolution;
using Seq.Syntax.Templates.Encoding;
using Seq.Syntax.Templates.Parsing;
using Seq.Syntax.Templates.Themes;

namespace Seq.Syntax.Compatibility;

Expand All @@ -37,7 +36,7 @@ public static class V1
public static bool TryCompileExpression(
string expression,
CultureInfo? formatProvider,
NameResolver nameResolver,
NameResolver? nameResolver,
[MaybeNullWhen(false)] out CompiledExpression result,
[MaybeNullWhen(true)] out string error)
{
Expand All @@ -59,7 +58,7 @@ public static bool TryCompileExpression(
return true;
}

/// <inheritdoc cref="ExpressionTemplate.TryParse(string,CultureInfo?,NameResolver?,TemplateTheme?,TemplateOutputEncoder?,out ExpressionTemplate,out string)"/>
/// <inheritdoc cref="ExpressionTemplate.TryParse(string,CultureInfo?,NameResolver?,TemplateOutputEncoder?,out ExpressionTemplate,out string)"/>
public static bool TryParseTemplate(
string template,
CultureInfo? culture,
Expand Down
3 changes: 3 additions & 0 deletions src/Seq.Syntax/Diagnostics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public static class ErrorKinds
// A regular expression driven to its match timeout by adversarial input.
public const string RegexTimeout = "regex_timeout";

// A `FromJson()` argument that couldn't be parsed as JSON.
public const string InvalidJson = "invalid_json";

// A comparison or render abandoned because the data nested too deeply for the stack.
public const string RecursionDepth = "recursion_depth";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,10 @@ protected override ExpressionBody Transform(AmbientNameExpression px)
return Splice(context => Intrinsics.GetPropertyValue(context, "@ra"));
case KeywordProperties.Scope:
return Splice(context => Intrinsics.GetPropertyValue(context, "@sa"));
case KeywordProperties.Data:
return Splice(context => KeywordProperties.GetData(context.Document));
case KeywordProperties.Arrived:
case KeywordProperties.Document:
case KeywordProperties.Data:
return UndefinedConstant;
}

Expand Down
6 changes: 6 additions & 0 deletions src/Seq.Syntax/Expressions/KeywordProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ public static EvaluationResult GetProperties(JsonObject eventJson)
return properties;
}

// The complete event document, verbatim.
public static EvaluationResult GetData(JsonObject eventJson)
{
return Values.Clone(eventJson);
}

public static EvaluationResult GetStart(JsonObject eventJson)
{
return GetTimestampField(eventJson, "@st") is { } dto
Expand Down
23 changes: 23 additions & 0 deletions src/Seq.Syntax/Expressions/Runtime/RuntimeOperators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,29 @@ public static EvaluationResult UriEncode(string value)
return JsonValue.Create(Uri.EscapeDataString(value));
}

public static EvaluationResult ToJson(JsonNode? value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonNode is already JSON

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonNode? is the object type - i.e. you might get a JsonArray etc. there. The function converts whatever object it's given into a raw JSON string 👍

{
// Serializes over the *inserted* form of a typed scalar: `Values.Clone` degrades a level
// to its string moniker and rejects pre-encoded `unsafe()` output. Nodes nested within
// containers were already degraded when they were inserted.
var node = value is JsonValue ? Values.Clone(value) : value;
return JsonValue.Create(node?.ToJsonString() ?? "null");
}

public static EvaluationResult FromJson(string json)
{
try
{
// `Parse` returns null for the JSON literal `null`.
return EvaluationResult.Defined(JsonNode.Parse(json));
}
catch (JsonException)
{
Diagnostics.RecordSuppressedError(Diagnostics.ErrorKinds.InvalidJson);
return EvaluationResult.Undefined;
}
}

public static EvaluationResult IsSpan(JsonObject eventJson)
{
return ScalarBoolean(eventJson.ContainsKey("@tr") &&
Expand Down
28 changes: 28 additions & 0 deletions test/Seq.Syntax.Tests/Cases/expression-evaluation-cases.asv
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,37 @@ uriencode(undefined()) ⇶ undefined()
uriencode('') ⇶ ''
uriencode(' ') ⇶ '%20'

// JSON serialization
tojson(42) ⇶ '42'
tojson('a') ⇶ '"a"'
tojson(true) ⇶ 'true'
tojson(null) ⇶ 'null'
tojson(undefined()) ⇶ undefined()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined is not a function

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 that's the convention we've used for some time in these tests; we could use undefined the ambient property, but it's possible (though unlikely) there could be an event property called, literally, undefined. Because we control the set of available functions, undefined() is safer - it's never defined, and data can't bring new functions into existence. A bit wishy-washy, I know :)

tojson([1, 'b', null]) ⇶ '[1,"b",null]'
tojson({a: 1}) ⇶ '{"a":1}'
tojson(@Level) ⇶ '"Information"'

// JSON deserialization
fromjson('{"a": [1, null, "x"]}') ⇶ {a: [1, null, 'x']}
fromjson(' true ') ⇶ true
fromjson('null') ⇶ null
fromjson('') ⇶ undefined()
fromjson('{"a":') ⇶ undefined()
fromjson(42) ⇶ undefined()
fromjson(null) ⇶ undefined()
fromjson(undefined()) ⇶ undefined()
fromjson(tojson({a: [1, 'b']})) ⇶ {a: [1, 'b']}

tostring(@Level, 'u3') ⇶ 'INF'
tostring(@Elapsed) ⇶ '00:10:00'

// The whole event document via @Data (@Document is deprecated and thus left undefined).
@Data['@mt'] ⇶ @mt
@Data['User']['Name'] ⇶ 'nblumhardt'
@Data['@Nonexistent'] ⇶ undefined()
@Document ⇶ undefined()
@Arrived ⇶ undefined()

// Typed values keep their string forms when inserted into constructed containers
[@Level][0] ⇶ 'Information'
{l: @Level}['l'] ⇶ 'Information'
Expand Down
126 changes: 0 additions & 126 deletions test/Seq.Syntax.Tests/Cases/harness-baseline-cases.asv

This file was deleted.

20 changes: 20 additions & 0 deletions test/Seq.Syntax.Tests/Expressions/IntrinsicsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,24 @@ public void EventTypeFallsBackToMessageTemplateHash()
Assert.True(Values.TryGetClrValue<uint>(node, out var i));
Assert.Equal(Seq.Syntax.Expressions.Compilation.Linq.EventIdHash.Compute("Hello, {Name}!"), i);
}

[Fact]
public void DataIsACloneOfTheEventDocument()
{
var evt = new JsonObject
{
["@mt"] = "Hello, {Name}!",
["Name"] = "World"
};

var data = KeywordProperties.GetData(evt);
Assert.True(data.TryGetValue(out var node));
var clone = Assert.IsType<JsonObject>(node);
Assert.NotSame(evt, clone);
Assert.True(JsonNode.DeepEquals(evt, clone));

clone["Name"] = "Modified";
Assert.True(Values.TryGetString(evt["Name"], out var original));
Assert.Equal("World", original);
}
}
Loading