Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better separation of built-in string.Format and Smart.Format features #175

Merged
merged 1 commit into from
Jun 4, 2021
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
3 changes: 2 additions & 1 deletion src/SmartFormat.Tests/Core/FormatterTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using SmartFormat.Core.Formatting;
using SmartFormat.Core.Output;
Expand Down Expand Up @@ -140,7 +141,7 @@ public void LeadingBackslashMustNotEscapeBraces()
{
var smart = Smart.CreateDefaultSmartFormat();
smart.Settings.Parser.ConvertCharacterStringLiterals = false;
smart.Settings.UseStringFormatCompatibility = true;
smart.Settings.StringFormatCompatibility = true;

var expected = "\\Hello";
var actual = smart.Format("\\{Test}", new { Test = "Hello" });
Expand Down
126 changes: 74 additions & 52 deletions src/SmartFormat.Tests/Core/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ namespace SmartFormat.Tests.Core
[TestFixture]
public class ParserTests
{
private static Parser GetRegularParser()
{
var parser = new SmartFormatter { Settings = { StringFormatCompatibility = false, Parser = {ErrorAction = ParseErrorAction.ThrowError }}}.Parser;
return parser;
}

[Test]
public void TestParser()
public void Basic_Parser_Test()
{
var parser = new SmartFormatter {Settings = { Parser = {ErrorAction = ParseErrorAction.ThrowError}}}.Parser;
parser.AddAlphanumericSelectors();
parser.AddAdditionalSelectorChars("_");
parser.AddOperators(".");
var parser = GetRegularParser();

var formats = new[]{
" aaa {bbb.ccc: ddd {eee} fff } ggg ",
Expand Down Expand Up @@ -89,7 +92,9 @@ public void Parser_Exception_ErrorDescription()
[Test]
public void Parser_Ignores_Exceptions()
{
var parser = new SmartFormatter() { Settings = { Parser = {ErrorAction = ParseErrorAction.Ignore }}}.Parser;
var parser = GetRegularParser();
parser.Settings.Parser.ErrorAction = ParseErrorAction.Ignore;

var invalidFormats = new[] {
"{",
"{0",
Expand All @@ -116,9 +121,6 @@ public void Parser_Error_Action_Ignore()
// | Literal | Erroneous | | Okay |
var invalidTemplate = "Hello, I'm {Name from {City} {Street}";

var smart = Smart.CreateDefaultSmartFormat();
smart.Settings.Parser.ErrorAction = ParseErrorAction.Ignore;

var parser = GetRegularParser();
parser.Settings.Parser.ErrorAction = ParseErrorAction.Ignore;
var parsed = parser.ParseFormat(invalidTemplate);
Expand Down Expand Up @@ -309,12 +311,6 @@ .comment img {
"NO placeholder");
}
}

private static Parser GetRegularParser()
{
var parser = new SmartFormatter() { Settings = { Parser = {ErrorAction = ParseErrorAction.ThrowError }}}.Parser;
return parser;
}

[Test]
public void Test_Format_Substring()
Expand Down Expand Up @@ -402,22 +398,22 @@ public void Test_Format_IndexOf()
{
var parser = GetRegularParser();
var format = " a|aa {bbb: ccc dd|d {:|||} {eee} ff|f } gg|g ";
var Format = parser.ParseFormat(format);
var result = parser.ParseFormat(format);

Assert.That(Format.IndexOf('|'), Is.EqualTo(2));
Assert.That(Format.IndexOf('|', 3), Is.EqualTo(43));
Assert.That(Format.IndexOf('|', 44), Is.EqualTo(-1));
Assert.That(Format.IndexOf('#'), Is.EqualTo(-1));
Assert.That(result.IndexOf('|'), Is.EqualTo(2));
Assert.That(result.IndexOf('|', 3), Is.EqualTo(43));
Assert.That(result.IndexOf('|', 44), Is.EqualTo(-1));
Assert.That(result.IndexOf('#'), Is.EqualTo(-1));

// Test nested formats:
var placeholder = (Placeholder) Format.Items[1];
Format = placeholder.Format!;
Assert.That(Format.ToString(), Is.EqualTo(" ccc dd|d {:|||} {eee} ff|f "));

Assert.That(Format.IndexOf('|'), Is.EqualTo(7));
Assert.That(Format.IndexOf('|', 8), Is.EqualTo(25));
Assert.That(Format.IndexOf('|', 26), Is.EqualTo(-1));
Assert.That(Format.IndexOf('#'), Is.EqualTo(-1));
var placeholder = (Placeholder) result.Items[1];
result = placeholder.Format!;
Assert.That(result.ToString(), Is.EqualTo(" ccc dd|d {:|||} {eee} ff|f "));

Assert.That(result.IndexOf('|'), Is.EqualTo(7));
Assert.That(result.IndexOf('|', 8), Is.EqualTo(25));
Assert.That(result.IndexOf('|', 26), Is.EqualTo(-1));
Assert.That(result.IndexOf('#'), Is.EqualTo(-1));
}

[Test]
Expand Down Expand Up @@ -461,22 +457,23 @@ private Format Parse(string format, string[] formatterExentionNames )
public void Name_of_registered_NamedFormatter_will_be_parsed(string format, string expectedName, string expectedOptions, string expectedFormat)
{
// The parser will only find names of named formatters which are registered. Names are case-sensitive.
var formatterExtensions = new[] { "name" };
var parser = GetRegularParser();

// Named formatters will only be recognized by the parser, if their name occurs in one of FormatterExtensions.
// If the name of the formatter does not exists, the string is treaded as format for the DefaultFormatter.
var placeholder = (Placeholder) Parse(format, formatterExtensions).Items[0];
var placeholder = (Placeholder) parser.ParseFormat(format).Items[0];
Assert.AreEqual(expectedName, placeholder.FormatterName);
Assert.AreEqual(expectedOptions, placeholder.FormatterOptions);
Assert.AreEqual(expectedFormat, placeholder.Format!.ToString());
}

[Test]
public void Name_of_unregistered_NamedFormatter_will_not_be_parsed()
public void Name_of_unregistered_NamedFormatter_will_be_parsed()
{
// find formatter formattername, which does not exist in the (empty) list of formatter extensions
var placeholderWithNonExistingName = (Placeholder)Parse("{0:formattername:}", new string[] {} ).Items[0];
Assert.AreEqual("formattername", placeholderWithNonExistingName.FormatterName); // name is only treaded as a literal
// find formatter formatter name, which does not exist in the (empty) list of formatter extensions
var parser = GetRegularParser();
var placeholderWithNonExistingName = (Placeholder)parser.ParseFormat("{0:formattername:}").Items[0];
Assert.AreEqual("formattername", placeholderWithNonExistingName.FormatterName);
}

// Incomplete:
Expand Down Expand Up @@ -506,7 +503,16 @@ public void Name_of_unregistered_NamedFormatter_will_not_be_parsed()
public void NamedFormatter_should_be_null_when_empty_or_invalid_or_escaped(string format)
{
var expectedLiteralText = format.Substring(3, format.Length - 3 - 1);
AssertNoNamedFormatter(format, expectedLiteralText);

var parser = GetRegularParser();
parser.Settings.Parser.ConvertCharacterStringLiterals = false;

var placeholder = (Placeholder) parser.ParseFormat(format).Items[0];
var literalText = placeholder.Format?.GetLiteralText();

Assert.That(placeholder.FormatterName, Is.Empty);
Assert.That(placeholder.FormatterOptions, Is.Empty);
Assert.That(literalText, Is.EqualTo(expectedLiteralText));
}

[TestCase(@"{0:format{}}", "format")]
Expand All @@ -516,50 +522,66 @@ public void NamedFormatter_should_be_null_when_empty_or_invalid_or_escaped(strin
[TestCase(@"{0:for{}mat()}", "format()")]
[TestCase(@"{0:for(){}mat}", "for()mat")]
public void NamedFormatter_should_be_null_when_has_nesting(string format, string expectedLiteralText)
{
AssertNoNamedFormatter(format, expectedLiteralText);
}

private void AssertNoNamedFormatter(string format, string expectedLiteralText)
{
var parser = GetRegularParser();
parser.UseAlternativeEscapeChar('\\');
parser.Settings.ConvertCharacterStringLiterals = false;
parser.Settings.Parser.ConvertCharacterStringLiterals = false;

var placeholder = (Placeholder) parser.ParseFormat(format).Items[0];
Assert.IsEmpty(placeholder.FormatterName);
Assert.IsEmpty(placeholder.FormatterOptions);
var literalText = placeholder.Format?.GetLiteralText();
Assert.AreEqual(expectedLiteralText, literalText);
}

Assert.That(placeholder.FormatterName, Is.Empty);
Assert.That(placeholder.FormatterOptions, Is.Empty);
Assert.That(literalText, Is.EqualTo(expectedLiteralText));
}

[Test]
public void Parser_NotifyParsingError()
{
ParsingErrors? parsingError = null;
var formatter = Smart.CreateDefaultSmartFormat();
formatter.Settings.Formatter.ErrorAction = FormatErrorAction.Ignore;
formatter.Settings.Parser.ErrorAction = ParseErrorAction.Ignore;

formatter.Parser.OnParsingFailure += (o, args) => parsingError = args.Errors;
var res = formatter.Format("{NoName {Other} {Same", default(object)!);
Assert.That(parsingError!.Issues.Count == 3);
Assert.That(parsingError.Issues[2].Issue == new SmartFormat.Core.Parsing.Parser.ParsingErrorText()[SmartFormat.Core.Parsing.Parser.ParsingError.MissingClosingBrace]);

Assert.That(parsingError!.Issues.Count, Is.EqualTo(3));
Assert.That(parsingError.Issues[2].Issue, Is.EqualTo(new Parser.ParsingErrorText()[SmartFormat.Core.Parsing.Parser.ParsingError.MissingClosingBrace]));
}

[Test]
public void Missing_Curly_Brace_Should_Throw()
{
var parser = GetRegularParser();
var format = "{0:yyyy/MM/dd HH:mm:ss";

Assert.That(() => parser.ParseFormat(format),
Throws.Exception.InstanceOf(typeof(ParsingErrors)).And.Message
.Contains(new Parser.ParsingErrorText()[Parser.ParsingError.MissingClosingBrace]));
}

[Test]
public void Alternative_Escaping_In_Literal()
public void Literal_Escaping_In_Literal()
{
var parser = GetRegularParser();
parser.UseAlternativeEscapeChar('\\');
parser.Settings.StringFormatCompatibility = false;
Assert.That(parser.ParseFormat("\\{\\}").ToString(), Is.EqualTo("{}"));
}

[Test]
public void Nested_format_with_alternative_escaping()
public void StringFormat_Escaping_In_Literal()
{
var parser = GetRegularParser();
parser.Settings.StringFormatCompatibility = true;
Assert.That(parser.ParseFormat("{{}}").ToString(), Is.EqualTo("{}"));
}


[Test]
public void Nested_format_with_literal_escaping()
{
var parser = GetRegularParser();
// necessary because of the consecutive }}}, which would otherwise be escaped as }} and lead to "missing brace" exception:
parser.UseAlternativeEscapeChar('\\');
var placeholders = parser.ParseFormat("{c1:{c2:{c3}}}");

var c1 = (Placeholder) placeholders.Items[0];
Expand Down
22 changes: 15 additions & 7 deletions src/SmartFormat.Tests/Core/StringFormatCompatibilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class StringFormatCompatibilityTests
[SetUp]
public void Setup()
{
_formatter.Settings.UseStringFormatCompatibility = true;
_formatter.Settings.StringFormatCompatibility = true;
}

[Test]
Expand Down Expand Up @@ -58,7 +58,6 @@ public void NamedPlaceholderDateTimeHHmmss()
var smartFmt = "It is now {Date:yyyy/MM/dd HH:mm:ss}";
var stringFmt = $"It is now {now.Date:yyyy/MM/dd HH:mm:ss}";
Assert.That(_formatter.Format(smartFmt, now), Is.EqualTo(stringFmt));

}

[Test]
Expand Down Expand Up @@ -92,7 +91,7 @@ public void NamedPlaceholderDecimal()
[Test]
public void NamedPlaceholderDateTime()
{
var now = DateTime.Now;
var now = new DateTime(2021, 12, 22, 14, 18, 12);
var smartFmt = "It is now {Date:d} at {Date:t}";
var stringFmt = $"It is now {now.Date:d} at {now.Date:t}";

Expand All @@ -112,7 +111,7 @@ public void NamedPlaceholderAlignment()
[Test]
public void NamedPlaceholder_DecimalArg()
{
_formatter.Settings.UseStringFormatCompatibility = false;
_formatter.Settings.StringFormatCompatibility = false;
var pricePerOunce = 17.36m;
var format = "The current price is {0} per ounce.";

Expand All @@ -122,7 +121,7 @@ public void NamedPlaceholder_DecimalArg()
[Test]
public void NamedPlaceholder_DecimalCurrencyArg()
{
_formatter.Settings.UseStringFormatCompatibility = false;
_formatter.Settings.StringFormatCompatibility = false;
var pricePerOunce = 17.36m;
var format = "The current price is {0:C2} per ounce.";

Expand All @@ -134,9 +133,18 @@ public void NamedPlaceholder_DecimalCurrencyArg()
/// </summary>
[TestCase("{0:FormatterName(true|false):one|two|default}", true)]
[TestCase("{0:FormatterName(string|String):one|two|default}", "String")]
public void Choose_should_be_case_sensitive(string format, object arg0)
[TestCase("{0,10:FormatterName(string|String):one|two|default}", "value")]
[TestCase("{0:d:FormatterName(string|String):one|two|default}", "2021-12-01")]
public void FormatterName_And_Options_Should_Be_Ignored(string format, object arg0)
{
Assert.That(_formatter.Format(format, arg0), Is.EqualTo(string.Format(format, arg0)));
}

[TestCase("{0:yyyy/MM/dd HH:mm:ss FormatterName(string|String):one|two|default}", "2021-12-01")] // results in "nonsense"
[TestCase("{0:yyyy/MM/dd HH:mm:ss}", "2021-12-01")]
public void FormatterName_And_Options_Should_Be_Ignored2(string format, DateTime arg0)
{
Assert.That(_formatter.Format(format, arg0), Does.StartWith("FormatterName"));
Assert.That(_formatter.Format(format, arg0), Is.EqualTo(string.Format(format, arg0)));
}
}
}
2 changes: 1 addition & 1 deletion src/SmartFormat.Tests/Extensions/ListFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void List_of_anonymous_types_and_enumerables()
Persons = data.Where(p => p.Gender == "M")
};

Smart.Default.Settings.UseStringFormatCompatibility = false; // mandatory for this test case because of consecutive curly braces
Smart.Default.Settings.StringFormatCompatibility = false; // mandatory for this test case because of consecutive curly braces
Smart.Default.Settings.Formatter.ErrorAction = SmartFormat.Core.Settings.FormatErrorAction.ThrowError;
Smart.Default.Settings.Parser.ErrorAction = SmartFormat.Core.Settings.ParseErrorAction.ThrowError;

Expand Down
4 changes: 2 additions & 2 deletions src/SmartFormat.Tests/Extensions/XmlSourceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ public void Format_XmlWithNamespaces_IgnoringNamespace()
public void Format_SingleLevelXml_TemplateWithCurlyBraces_Escaped()
{
var sf = Smart.CreateDefaultSmartFormat();
sf.Settings.UseStringFormatCompatibility = true;
sf.Settings.StringFormatCompatibility = false;
// arrange
var xmlEl = XElement.Parse(OneLevelXml);
// act
var res = sf.Format("Mr. {{{LastName}}}", xmlEl);
var res = sf.Format("Mr. \\{{LastName}\\}", xmlEl);
// assert
Assert.AreEqual("Mr. {Doe}", res);
}
Expand Down
68 changes: 68 additions & 0 deletions src/SmartFormat.Tests/Utilities/CustomFormatProviderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using SmartFormat.Extensions;

namespace SmartFormat.Tests.Utilities
{
[TestFixture]
public class CustomFormatProviderTests
{
private SmartFormatter GetSimpleFormatter()
{
var formatter = new SmartFormatter();
formatter.FormatterExtensions.Add(new DefaultFormatter());
formatter.SourceExtensions.Add(new ReflectionSource(formatter));
formatter.SourceExtensions.Add(new DefaultSource(formatter));
return formatter;
}

#region *** Format with custom formatter ***

[TestCase("format", "value", true)]
[TestCase("tamrof", "eulav", true)]
[TestCase("format", "value", false)]
[TestCase("tamrof", "eulav", false)]
public void Format_With_CustomFormatter(string format, string value, bool stringFormatCompatible)
{
var smart = GetSimpleFormatter();
smart.Settings.StringFormatCompatibility = stringFormatCompatible;
var expected = new string(format.Reverse().Select(c => c).ToArray()) + ": " +
new string(value.Reverse().Select(c => c).ToArray());
var resultSmartFormat = smart.Format(new ReverseFormatProvider(), $"{{0:{format}}}", value);
var resultStringFormat = string.Format(new ReverseFormatProvider(), $"{{0:{format}}}", value);
Assert.That(resultSmartFormat, Is.EqualTo(expected));
Assert.That(resultStringFormat, Is.EqualTo(expected));
}

/// <summary>
/// Used for Format_With_CustomFormatter test
/// </summary>
public class ReverseFormatProvider : IFormatProvider
{
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter)) return new ReverseFormatAndArgumentFormatter();

return new object();
}
}

/// <summary>
/// Used for Format_With_CustomFormatter test
/// </summary>
public class ReverseFormatAndArgumentFormatter : ICustomFormatter
{
public string Format(string format, object arg, IFormatProvider formatProvider)
{
return new string(format.Reverse().Select(c => c).ToArray()) + ": " +
new string((arg as string ?? "?").Reverse().Select(c => c).ToArray());
}
}

#endregion
}
}
Loading