Upstream serilog/serilog state of the touched files before f35ed4e093
This commit is contained in:
commit
9ffa9b2c8b
2 files changed
+815
No files matched your search
@@ -0,0 +1,346 @@
|
|||||||
|
// Copyright 2013-2015 Serilog Contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
namespace Serilog.Parsing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses message template strings into sequences of text or property
|
||||||
|
/// tokens.
|
||||||
|
/// </summary>
|
||||||
|
public class MessageTemplateParser : IMessageTemplateParser
|
||||||
|
{
|
||||||
|
static readonly TextToken EmptyTextToken = new("");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construct a <see cref="MessageTemplateParser"/>.
|
||||||
|
/// </summary>
|
||||||
|
public MessageTemplateParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse the supplied message template.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageTemplate">The message template to parse.</param>
|
||||||
|
/// <returns>A sequence of text or property tokens. Where the template
|
||||||
|
/// is not syntactically valid, text tokens will be returned. The parser
|
||||||
|
/// will make a best effort to extract valid property tokens even in the
|
||||||
|
/// presence of parsing issues.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">When <paramref name="messageTemplate"/> is <code>null</code></exception>
|
||||||
|
public MessageTemplate Parse(string messageTemplate)
|
||||||
|
{
|
||||||
|
Guard.AgainstNull(messageTemplate);
|
||||||
|
|
||||||
|
return new(messageTemplate, Tokenize(messageTemplate));
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<MessageTemplateToken> Tokenize(string messageTemplate)
|
||||||
|
{
|
||||||
|
if (messageTemplate.Length == 0)
|
||||||
|
{
|
||||||
|
yield return EmptyTextToken;
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextIndex = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var beforeText = nextIndex;
|
||||||
|
var tt = ParseTextToken(nextIndex, messageTemplate, out nextIndex);
|
||||||
|
if (nextIndex > beforeText)
|
||||||
|
yield return tt;
|
||||||
|
|
||||||
|
if (nextIndex == messageTemplate.Length)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
var beforeProp = nextIndex;
|
||||||
|
var pt = ParsePropertyToken(nextIndex, messageTemplate, out nextIndex);
|
||||||
|
if (beforeProp < nextIndex)
|
||||||
|
yield return pt;
|
||||||
|
|
||||||
|
if (nextIndex == messageTemplate.Length)
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageTemplateToken ParsePropertyToken(int startAt, string messageTemplate, out int next)
|
||||||
|
{
|
||||||
|
var first = startAt;
|
||||||
|
startAt++;
|
||||||
|
|
||||||
|
startAt = messageTemplate.IndexOf('}', startAt);
|
||||||
|
if (startAt == -1)
|
||||||
|
{
|
||||||
|
next = messageTemplate.Length;
|
||||||
|
return new TextToken(messageTemplate[first..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
next = startAt + 1;
|
||||||
|
|
||||||
|
var rawText = messageTemplate.Substring(first, next - first);
|
||||||
|
var tagContent = rawText.Substring(1, next - (first + 2));
|
||||||
|
if (tagContent.Length == 0)
|
||||||
|
return new TextToken(rawText);
|
||||||
|
|
||||||
|
if (!TrySplitTagContent(tagContent, out var propertyNameAndDestructuring, out var format, out var alignment))
|
||||||
|
return new TextToken(rawText);
|
||||||
|
|
||||||
|
var propertyName = propertyNameAndDestructuring;
|
||||||
|
var destructuring = Destructuring.Default;
|
||||||
|
if (propertyName.Length != 0 && TryGetDestructuringHint(propertyName[0], out destructuring))
|
||||||
|
propertyName = propertyName[1..];
|
||||||
|
|
||||||
|
if (propertyName.Length == 0)
|
||||||
|
{
|
||||||
|
return new TextToken(rawText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.IsDigit(propertyName[0]))
|
||||||
|
{
|
||||||
|
for (var i = 0; i < propertyName.Length; ++i)
|
||||||
|
{
|
||||||
|
var c = propertyName[i];
|
||||||
|
if (!char.IsDigit(c))
|
||||||
|
return new TextToken(rawText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var beginIdent = true;
|
||||||
|
for (var i = 0; i < propertyName.Length; ++i)
|
||||||
|
{
|
||||||
|
var c = propertyName[i];
|
||||||
|
if (!TryContinuePropertyName(c, ref beginIdent))
|
||||||
|
return new TextToken(rawText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beginIdent)
|
||||||
|
{
|
||||||
|
return new TextToken(rawText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < format.Length; ++i)
|
||||||
|
{
|
||||||
|
var c = format[i];
|
||||||
|
if (!IsValidInFormat(c))
|
||||||
|
return new TextToken(rawText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Alignment? alignmentValue = null;
|
||||||
|
if (alignment != null)
|
||||||
|
{
|
||||||
|
if (alignment[0] == '+')
|
||||||
|
return new TextToken(rawText);
|
||||||
|
|
||||||
|
if (!int.TryParse(alignment, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var width))
|
||||||
|
return new TextToken(rawText);
|
||||||
|
|
||||||
|
var hasDash = alignment[0] == '-';
|
||||||
|
var direction = hasDash ? AlignmentDirection.Left : AlignmentDirection.Right;
|
||||||
|
|
||||||
|
alignmentValue = new(direction, Math.Abs(width));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PropertyToken(
|
||||||
|
propertyName,
|
||||||
|
rawText,
|
||||||
|
format,
|
||||||
|
alignmentValue,
|
||||||
|
destructuring);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TrySplitTagContent(string tagContent, [NotNullWhen(true)] out string? propertyNameAndDestructuring, out string? format, out string? alignment)
|
||||||
|
{
|
||||||
|
var formatDelim = tagContent.IndexOf(':');
|
||||||
|
var alignmentDelim = tagContent.IndexOf(',');
|
||||||
|
if (formatDelim == -1 && alignmentDelim == -1)
|
||||||
|
{
|
||||||
|
propertyNameAndDestructuring = tagContent;
|
||||||
|
format = null;
|
||||||
|
alignment = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alignmentDelim == -1 || (formatDelim != -1 && alignmentDelim > formatDelim))
|
||||||
|
{
|
||||||
|
propertyNameAndDestructuring = tagContent[..formatDelim];
|
||||||
|
format = formatDelim == tagContent.Length - 1 ?
|
||||||
|
null :
|
||||||
|
tagContent.Substring(formatDelim + 1);
|
||||||
|
alignment = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertyNameAndDestructuring = tagContent[..alignmentDelim];
|
||||||
|
if (formatDelim == -1)
|
||||||
|
{
|
||||||
|
if (alignmentDelim == tagContent.Length - 1)
|
||||||
|
{
|
||||||
|
alignment = format = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
format = null;
|
||||||
|
alignment = tagContent[(alignmentDelim + 1)..];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alignmentDelim == formatDelim - 1)
|
||||||
|
{
|
||||||
|
alignment = format = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
alignment = tagContent.Substring(alignmentDelim + 1, formatDelim - alignmentDelim - 1);
|
||||||
|
format = formatDelim == tagContent.Length - 1 ?
|
||||||
|
null :
|
||||||
|
tagContent[(formatDelim + 1)..];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryContinuePropertyName(char c, ref bool beginIdent)
|
||||||
|
{
|
||||||
|
if (beginIdent)
|
||||||
|
{
|
||||||
|
if (char.IsLetter(c) || c is '_')
|
||||||
|
{
|
||||||
|
beginIdent = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.IsLetterOrDigit(c) || c is '_')
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c is '.')
|
||||||
|
{
|
||||||
|
beginIdent = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool TryGetDestructuringHint(char c, out Destructuring destructuring)
|
||||||
|
{
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case '@':
|
||||||
|
destructuring = Destructuring.Destructure;
|
||||||
|
return true;
|
||||||
|
case '$':
|
||||||
|
destructuring = Destructuring.Stringify;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
destructuring = Destructuring.Default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsValidInFormat(char c) => c != '}';
|
||||||
|
static readonly char[] CurlyBraceChars = ['{', '}'];
|
||||||
|
|
||||||
|
static TextToken ParseTextToken(int startAt, string messageTemplate, out int next)
|
||||||
|
{
|
||||||
|
// If we encounter escape sequences like {{ or }}, the result is not a strict substring of the
|
||||||
|
// template. But, this requires allocating a StringBuilder, so we try to parse as far as we can first, and
|
||||||
|
// only allocate the StringBuilder/fall through to the slow string-building path if we actually need to.
|
||||||
|
// Most of the time we won't hit escapes, so we can get away with just a single Substring() allocation at
|
||||||
|
// the end.
|
||||||
|
|
||||||
|
var i = messageTemplate.IndexOfAny(CurlyBraceChars, startAt);
|
||||||
|
if (i == -1)
|
||||||
|
{
|
||||||
|
// No more interesting characters in the template, everything left is text.
|
||||||
|
next = messageTemplate.Length;
|
||||||
|
return new(messageTemplate[startAt..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder accum;
|
||||||
|
var ch = messageTemplate[i];
|
||||||
|
++i;
|
||||||
|
|
||||||
|
// The character must be either `{` or `}`, since we found its index.
|
||||||
|
if (ch == '{')
|
||||||
|
{
|
||||||
|
if (i < messageTemplate.Length && messageTemplate[i] == '{')
|
||||||
|
{
|
||||||
|
// Hit an escape sequence; ignore the second (duplicate) `{`, and push the rest onto the
|
||||||
|
// accumulator to start the slow path.
|
||||||
|
accum = new(messageTemplate, startAt, i - startAt, messageTemplate.Length - startAt);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Hit the start of a property token. We're done, no StringBuilder was required.
|
||||||
|
next = i - 1;
|
||||||
|
return next == startAt ? EmptyTextToken : new(messageTemplate.Substring(startAt, i - 1 - startAt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // ch == '}'
|
||||||
|
{
|
||||||
|
accum = new(messageTemplate, startAt, i - startAt, messageTemplate.Length - startAt);
|
||||||
|
if (i < messageTemplate.Length && messageTemplate[i] == '}')
|
||||||
|
{
|
||||||
|
// Hit an escaped `}`; as before, skip the duplicate and start accumulating the result.
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must have encountered an escaped character sequence: finish the text token, using the
|
||||||
|
// accumulator. This is relatively uncommon so we just do it char-by-char.
|
||||||
|
while (i < messageTemplate.Length)
|
||||||
|
{
|
||||||
|
ch = messageTemplate[i];
|
||||||
|
++i;
|
||||||
|
|
||||||
|
if (ch == '{')
|
||||||
|
{
|
||||||
|
if (i < messageTemplate.Length && messageTemplate[i] == '{')
|
||||||
|
{
|
||||||
|
accum.Append(ch);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
next = i - 1;
|
||||||
|
return new(accum.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
accum.Append(ch);
|
||||||
|
if (ch == '}')
|
||||||
|
{
|
||||||
|
if (i < messageTemplate.Length && messageTemplate[i] == '}')
|
||||||
|
{
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next = i;
|
||||||
|
return new(accum.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
using PropertyToken = Serilog.Parsing.PropertyToken;
|
||||||
|
|
||||||
|
namespace Serilog.Tests.Parsing;
|
||||||
|
|
||||||
|
public class MessageTemplateParserTests
|
||||||
|
{
|
||||||
|
static MessageTemplateToken[] Parse(string messageTemplate)
|
||||||
|
{
|
||||||
|
return new MessageTemplateParser().Parse(messageTemplate).Tokens.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local
|
||||||
|
static void AssertParsedAs(string message, params MessageTemplateToken[] messageTemplateTokens)
|
||||||
|
{
|
||||||
|
var parsed = Parse(message);
|
||||||
|
Assert.Equal(
|
||||||
|
messageTemplateTokens,
|
||||||
|
parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MessageTemplateIsRequired()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => Parse(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnEmptyMessageIsASingleTextToken()
|
||||||
|
{
|
||||||
|
var ts = Parse("");
|
||||||
|
var mt = Assert.Single(ts);
|
||||||
|
var tt = Assert.IsType<TextToken>(mt);
|
||||||
|
Assert.Equal("", tt.Text);
|
||||||
|
Assert.Equal("", tt.ToString());
|
||||||
|
Assert.Equal(0, tt.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnEmptyPropertyIsIsParsedAsText()
|
||||||
|
{
|
||||||
|
var ts = Parse("{}");
|
||||||
|
var mt = Assert.Single(ts);
|
||||||
|
var tt = Assert.IsType<TextToken>(mt);
|
||||||
|
Assert.Equal("{}", tt.Text);
|
||||||
|
Assert.Equal("{}", tt.ToString());
|
||||||
|
Assert.Equal(2, tt.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AMessageWithoutPropertiesIsASingleTextToken()
|
||||||
|
{
|
||||||
|
AssertParsedAs("Hello, world!",
|
||||||
|
new TextToken("Hello, world!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AMessageWithPropertyOnlyIsASinglePropertyToken()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{Hello}",
|
||||||
|
new PropertyToken("Hello", "{Hello}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubledLeftBracketsAreParsedAsASingleBracket()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{{ Hi! }",
|
||||||
|
new TextToken("{ Hi! }"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubledLeftBracketsAreParsedAsASingleBracketInsideText()
|
||||||
|
{
|
||||||
|
AssertParsedAs("Well, {{ Hi!",
|
||||||
|
new TextToken("Well, { Hi!"));
|
||||||
|
|
||||||
|
AssertParsedAs("Hello, {{worl@d}!",
|
||||||
|
new TextToken("Hello, {worl@d}!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubledRightBracketsAreParsedAsASingleBracket()
|
||||||
|
{
|
||||||
|
AssertParsedAs("Nice }}-: mo",
|
||||||
|
new TextToken("Nice }-: mo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubledRightBracketsAfterOneLeftIsParsedAPropertyTokenAndATextToken()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{World}}!",
|
||||||
|
new PropertyToken("World", "{World}"), new TextToken("}!"));
|
||||||
|
|
||||||
|
AssertParsedAs("Hello, {World}}!",
|
||||||
|
new TextToken("Hello, "), new PropertyToken("World", "{World}"), new TextToken("}!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubledBracketsAreParsedAsASingleBracket()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{{Hi}}",
|
||||||
|
new TextToken("{Hi}"));
|
||||||
|
|
||||||
|
AssertParsedAs("Hello, {{worl@d}}!",
|
||||||
|
new TextToken("Hello, {worl@d}!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("{test.name}")]
|
||||||
|
[InlineData("{te.st.na.me}")]
|
||||||
|
public void DashedAndDottedNamesAreAccepted(string template)
|
||||||
|
{
|
||||||
|
var parser = new MessageTemplateParser();
|
||||||
|
var token = Assert.Single(parser.Parse(template).Tokens);
|
||||||
|
var propertyToken = Assert.IsType<PropertyToken>(token);
|
||||||
|
var expected = template.Trim('{', '}');
|
||||||
|
Assert.Equal(expected, propertyToken.PropertyName);
|
||||||
|
Assert.Null(propertyToken.Alignment);
|
||||||
|
Assert.Null(propertyToken.Format);
|
||||||
|
Assert.False(propertyToken.IsPositional);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("{0 space}")]
|
||||||
|
[InlineData("{0 space")]
|
||||||
|
[InlineData("{0_space")]
|
||||||
|
[InlineData("{0_{{space}")]
|
||||||
|
[InlineData("{0_{{space")]
|
||||||
|
[InlineData("{.testname}")]
|
||||||
|
[InlineData("{testname.}")]
|
||||||
|
[InlineData("{.}")]
|
||||||
|
[InlineData("{..}")]
|
||||||
|
[InlineData("{test..name}")]
|
||||||
|
[InlineData("{10.name}")]
|
||||||
|
[InlineData("{0_}")]
|
||||||
|
[InlineData("{0a}")]
|
||||||
|
public void AMalformedPropertyTagIsParsedAsText(string template)
|
||||||
|
{
|
||||||
|
AssertParsedAs(template, new TextToken(template));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ATrailingUnmatchedBracketIsParsedAsText()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{0}}space}", new PropertyToken("0", "{0}"), new TextToken("}space}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AMessageWithAMalformedPropertyTagIsParsedAsManyTextTokens()
|
||||||
|
{
|
||||||
|
AssertParsedAs("Hello, {w@rld}",
|
||||||
|
new TextToken("Hello, "), new TextToken("{w@rld}"));
|
||||||
|
|
||||||
|
AssertParsedAs("Hello, {w@rld",
|
||||||
|
new TextToken("Hello, "), new TextToken("{w@rld"));
|
||||||
|
|
||||||
|
AssertParsedAs("Hello, {w{{rld",
|
||||||
|
new TextToken("Hello, "), new TextToken("{w{{rld"));
|
||||||
|
|
||||||
|
AssertParsedAs("Hello{{, {w{{rld",
|
||||||
|
new TextToken("Hello{, "), new TextToken("{w{{rld"));
|
||||||
|
|
||||||
|
AssertParsedAs("Hello, {w@rld}, HI!",
|
||||||
|
new TextToken("Hello, "), new TextToken("{w@rld}"), new TextToken(", HI!"));
|
||||||
|
|
||||||
|
AssertParsedAs("{w@rld} Hi!",
|
||||||
|
new TextToken("{w@rld}"), new TextToken(" Hi!"));
|
||||||
|
|
||||||
|
AssertParsedAs("{H&llo}, {w@rld}",
|
||||||
|
new TextToken("{H&llo}"), new TextToken(", "), new TextToken("{w@rld}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ASingleIntegerPropertyNameIsParsedAsPositionalProperty()
|
||||||
|
{
|
||||||
|
var parsed = (PropertyToken)Parse("{0}").Single();
|
||||||
|
Assert.Equal("0", parsed.PropertyName);
|
||||||
|
Assert.Equal("{0}", parsed.RawText);
|
||||||
|
Assert.Equal(parsed.RawText, parsed.ToString());
|
||||||
|
Assert.True(parsed.IsPositional);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ManyIntegerPropertyNameIsParsedAsPositionalProperty()
|
||||||
|
{
|
||||||
|
var parsed = Parse("{0}, {1}, {2}");
|
||||||
|
|
||||||
|
var prop1 = (PropertyToken)parsed[0];
|
||||||
|
Assert.Equal("0", prop1.PropertyName);
|
||||||
|
Assert.Equal("{0}", prop1.RawText);
|
||||||
|
Assert.True(prop1.IsPositional);
|
||||||
|
Assert.Equal(3, prop1.Length);
|
||||||
|
|
||||||
|
var prop2 = (TextToken)parsed[1];
|
||||||
|
Assert.Equal(", ", prop2.Text);
|
||||||
|
Assert.Equal(2, prop2.Length);
|
||||||
|
|
||||||
|
var prop3 = (PropertyToken)parsed[2];
|
||||||
|
Assert.Equal("1", prop3.PropertyName);
|
||||||
|
Assert.Equal("{1}", prop3.RawText);
|
||||||
|
Assert.True(prop3.IsPositional);
|
||||||
|
Assert.Equal(3, prop3.Length);
|
||||||
|
|
||||||
|
var prop4 = (TextToken)parsed[3];
|
||||||
|
Assert.Equal(", ", prop4.Text);
|
||||||
|
Assert.Equal(2, prop4.Length);
|
||||||
|
|
||||||
|
var prop5 = (PropertyToken)parsed[4];
|
||||||
|
Assert.Equal("2", prop5.PropertyName);
|
||||||
|
Assert.Equal("{2}", prop5.RawText);
|
||||||
|
Assert.True(prop5.IsPositional);
|
||||||
|
Assert.Equal(3, prop5.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InvalidIntegerPropertyNameIsParsedAsText()
|
||||||
|
{
|
||||||
|
var parsed = Parse("{-1}{-0}{0}{1}{3.1415}");
|
||||||
|
|
||||||
|
var prop1 = (TextToken)parsed[0];
|
||||||
|
Assert.Equal("{-1}", prop1.Text);
|
||||||
|
|
||||||
|
var prop2 = (TextToken)parsed[1];
|
||||||
|
Assert.Equal("{-0}", prop2.Text);
|
||||||
|
|
||||||
|
var prop3 = (PropertyToken)parsed[2];
|
||||||
|
Assert.Equal("0", prop3.PropertyName);
|
||||||
|
Assert.Equal("{0}", prop3.RawText);
|
||||||
|
Assert.True(prop3.IsPositional);
|
||||||
|
|
||||||
|
var prop4 = (PropertyToken)parsed[3];
|
||||||
|
Assert.Equal("1", prop4.PropertyName);
|
||||||
|
Assert.Equal("{1}", prop4.RawText);
|
||||||
|
Assert.True(prop4.IsPositional);
|
||||||
|
|
||||||
|
var prop5 = (TextToken)parsed[4];
|
||||||
|
Assert.Equal("{3.1415}", prop5.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatsCanContainColons()
|
||||||
|
{
|
||||||
|
var parsed = (PropertyToken)Parse("{Time:hh:mm}").Single();
|
||||||
|
Assert.Equal("hh:mm", parsed.Format);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatsCanContainSymbolsAndOperators()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{Hello:HH$MM}",
|
||||||
|
new PropertyToken("Hello", "{Hello:HH$MM}", "HH$MM"));
|
||||||
|
|
||||||
|
AssertParsedAs("{Hello:HH<MM}",
|
||||||
|
new PropertyToken("Hello", "{Hello:HH<MM}", "HH<MM"));
|
||||||
|
|
||||||
|
AssertParsedAs("{Hello:HH#MM}",
|
||||||
|
new PropertyToken("Hello", "{Hello:HH#MM}", "HH#MM"));
|
||||||
|
|
||||||
|
AssertParsedAs("{Hello:HH+MM}",
|
||||||
|
new PropertyToken("Hello", "{Hello:HH+MM}", "HH+MM"));
|
||||||
|
|
||||||
|
AssertParsedAs("{Hello:HH`MM}",
|
||||||
|
new PropertyToken("Hello", "{Hello:HH`MM}", "HH`MM"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertiesCanHaveLeftAlignment()
|
||||||
|
{
|
||||||
|
var prop1 = (PropertyToken)Parse("{Hello,-5}").Single();
|
||||||
|
Assert.Equal("Hello", prop1.PropertyName);
|
||||||
|
Assert.Equal("{Hello,-5}", prop1.RawText);
|
||||||
|
Assert.Equal(new Alignment(AlignmentDirection.Left, 5), prop1.Alignment);
|
||||||
|
|
||||||
|
var prop2 = (PropertyToken)Parse("{Hello,-50}").Single();
|
||||||
|
Assert.Equal("Hello", prop2.PropertyName);
|
||||||
|
Assert.Equal("{Hello,-50}", prop2.RawText);
|
||||||
|
Assert.Equal(new Alignment(AlignmentDirection.Left, 50), prop2.Alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertiesCanHaveRightAlignment()
|
||||||
|
{
|
||||||
|
var prop1 = (PropertyToken)Parse("{Hello,5}").Single();
|
||||||
|
Assert.Equal("Hello", prop1.PropertyName);
|
||||||
|
Assert.Equal("{Hello,5}", prop1.RawText);
|
||||||
|
Assert.Equal(new Alignment(AlignmentDirection.Right, 5), prop1.Alignment);
|
||||||
|
|
||||||
|
var prop2 = (PropertyToken)Parse("{Hello,50}").Single();
|
||||||
|
Assert.Equal("Hello", prop2.PropertyName);
|
||||||
|
Assert.Equal("{Hello,50}", prop2.RawText);
|
||||||
|
Assert.Equal(new Alignment(AlignmentDirection.Right, 50), prop2.Alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertiesCanHaveAlignmentAndFormat()
|
||||||
|
{
|
||||||
|
var prop1 = (PropertyToken)Parse("{Hello,-5:000}").Single();
|
||||||
|
Assert.Equal("Hello", prop1.PropertyName);
|
||||||
|
Assert.Equal("{Hello,-5:000}", prop1.RawText);
|
||||||
|
Assert.Equal("000", prop1.Format);
|
||||||
|
Assert.Equal(new Alignment(AlignmentDirection.Left, 5), prop1.Alignment);
|
||||||
|
|
||||||
|
var prop2 = (PropertyToken)Parse("{Hello,-50:000}").Single();
|
||||||
|
Assert.Equal("Hello", prop2.PropertyName);
|
||||||
|
Assert.Equal("{Hello,-50:000}", prop2.RawText);
|
||||||
|
Assert.Equal("000", prop2.Format);
|
||||||
|
Assert.Equal(new Alignment(AlignmentDirection.Left, 50), prop2.Alignment);
|
||||||
|
|
||||||
|
var prop3 = (PropertyToken)Parse("{Hello,5:000}").Single();
|
||||||
|
Assert.Equal("Hello", prop3.PropertyName);
|
||||||
|
Assert.Equal("{Hello,5:000}", prop3.RawText);
|
||||||
|
Assert.Equal("000", prop3.Format);
|
||||||
|
Assert.Equal(new Alignment(AlignmentDirection.Right, 5), prop3.Alignment);
|
||||||
|
|
||||||
|
var prop4 = (PropertyToken)Parse("{Hello,50:000}").Single();
|
||||||
|
Assert.Equal("Hello", prop4.PropertyName);
|
||||||
|
Assert.Equal("{Hello,50:000}", prop4.RawText);
|
||||||
|
Assert.Equal("000", prop4.Format);
|
||||||
|
Assert.Equal(new Alignment(AlignmentDirection.Right, 50), prop4.Alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatInFrontOfAlignmentWillHaveTheAlignmentBeConsideredPartOfTheFormat()
|
||||||
|
{
|
||||||
|
var prop1 = (PropertyToken)Parse("{Hello:000,-5}").Single();
|
||||||
|
Assert.Equal("Hello", prop1.PropertyName);
|
||||||
|
Assert.Equal("{Hello:000,-5}", prop1.RawText);
|
||||||
|
Assert.Equal("000,-5", prop1.Format);
|
||||||
|
Assert.Null(prop1.Alignment);
|
||||||
|
|
||||||
|
var prop2 = (PropertyToken)Parse("{Hello:000,-50}").Single();
|
||||||
|
Assert.Equal("Hello", prop2.PropertyName);
|
||||||
|
Assert.Equal("{Hello:000,-50}", prop2.RawText);
|
||||||
|
Assert.Equal("000,-50", prop2.Format);
|
||||||
|
Assert.Null(prop2.Alignment);
|
||||||
|
|
||||||
|
var prop3 = (PropertyToken)Parse("{Hello:000,5}").Single();
|
||||||
|
Assert.Equal("Hello", prop3.PropertyName);
|
||||||
|
Assert.Equal("{Hello:000,5}", prop3.RawText);
|
||||||
|
Assert.Equal("000,5", prop3.Format);
|
||||||
|
Assert.Null(prop3.Alignment);
|
||||||
|
|
||||||
|
var prop4 = (PropertyToken)Parse("{Hello:000,50}").Single();
|
||||||
|
Assert.Equal("Hello", prop4.PropertyName);
|
||||||
|
Assert.Equal("{Hello:000,50}", prop4.RawText);
|
||||||
|
Assert.Equal("000,50", prop4.Format);
|
||||||
|
Assert.Null(prop4.Alignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroValuesAlignmentIsParsedAsProperty()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{Hello,-0}",
|
||||||
|
new PropertyToken("Hello", "{Hello,-0}", alignment: new Alignment(AlignmentDirection.Right, 0)));
|
||||||
|
|
||||||
|
AssertParsedAs("{Hello,0}",
|
||||||
|
new PropertyToken("Hello", "{Hello,0}", alignment: new Alignment(AlignmentDirection.Left, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlignmentWithPositiveSignParsedAsText()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{Hello,+10}",
|
||||||
|
new TextToken("{Hello,+10}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NonNumberAlignmentIsParsedAsText()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{Hello,-aa}",
|
||||||
|
new TextToken("{Hello,-aa}"));
|
||||||
|
|
||||||
|
AssertParsedAs("{Hello,aa}",
|
||||||
|
new TextToken("{Hello,aa}"));
|
||||||
|
|
||||||
|
AssertParsedAs("{Hello,-10-1}",
|
||||||
|
new TextToken("{Hello,-10-1}"));
|
||||||
|
|
||||||
|
AssertParsedAs("{Hello,10-1}",
|
||||||
|
new TextToken("{Hello,10-1}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptyAlignmentIsParsedAsText()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{Hello,}",
|
||||||
|
new TextToken("{Hello,}"));
|
||||||
|
|
||||||
|
AssertParsedAs("{Hello,:format}",
|
||||||
|
new TextToken("{Hello,:format}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleTokensHasCorrectIndexes()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{Greeting}, {Name}!",
|
||||||
|
new PropertyToken("Greeting", "{Greeting}"),
|
||||||
|
new TextToken(", "),
|
||||||
|
new PropertyToken("Name", "{Name}"),
|
||||||
|
new TextToken("!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingRightBracketIsParsedAsText()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{Hello",
|
||||||
|
new TextToken("{Hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DestructureHintIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
var parsed = (PropertyToken)Parse("{@Hello}").Single();
|
||||||
|
Assert.Equal(Destructuring.Destructure, parsed.Destructuring);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StringifyHintIsParsedCorrectly()
|
||||||
|
{
|
||||||
|
var parsed = (PropertyToken)Parse("{$Hello}").Single();
|
||||||
|
Assert.Equal(Destructuring.Stringify, parsed.Destructuring);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DestructureWithInvalidHintsIsParsedAsText()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{@$}",
|
||||||
|
new TextToken("{@$}"));
|
||||||
|
|
||||||
|
AssertParsedAs("{$@}",
|
||||||
|
new TextToken("{$@}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DestructuringWithEmptyPropertyNameIsParsedAsText()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{@}",
|
||||||
|
new TextToken("{@}"));
|
||||||
|
|
||||||
|
AssertParsedAs("{$}",
|
||||||
|
new TextToken("{$}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnderscoresAreValidInPropertyNames()
|
||||||
|
{
|
||||||
|
AssertParsedAs("{_123_Hello}", new PropertyToken("_123_Hello", "{_123_Hello}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IndexOutOfRangeExceptionBugHasNotRegressed()
|
||||||
|
{
|
||||||
|
var parser = new MessageTemplateParser();
|
||||||
|
parser.Parse("{,,}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatCanContainMultipleSections()
|
||||||
|
{
|
||||||
|
var parsed = (PropertyToken)Parse("{Number:##.0;-##.0;zero}").Single();
|
||||||
|
Assert.Equal("##.0;-##.0;zero", parsed.Format);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatCanContainPlusSign()
|
||||||
|
{
|
||||||
|
var parsed = (PropertyToken)Parse("{Number:+##.0}").Single();
|
||||||
|
Assert.Equal("+##.0", parsed.Format);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user