diff --git a/README.md b/README.md
index bd406cb..5e1e25f 100644
--- a/README.md
+++ b/README.md
@@ -24,34 +24,65 @@ Pre-release packages are available on my MyGet feed:
++ **7.1**
+ - Add support for dynamic content which CakeMail already supports when sending bulk emails. In other words, you can have `IF ... ELSEIF ... ELSE ... ENDIF` in your HTML content, text content and subject as well.
+ - Please keep in mind the following the rules established by CakeMail for dynamic content:
+ - `IF`, `ELSEIF`, `ELSE` and `ENDIF` must be upper case which means that ``[IF `myfield` = "myValue"]`` is valid but ``[if `myfield` = "myValue"]`` is not.
+ - square bracket is the delimeter which means that ``[IF `myfield` = "myValue"]`` is valid but ``{IF `myfield` = "myValue"}`` is not.
+ - the name of the data field must be surrounded by back ticks (not to be confused with single quotes) which means that ``[IF `firstname` = "Bob"]`` is valid but ``[IF 'firstname' = "Bob"]`` is not.
+ - you can only compare a field to a constant value which means that ``[IF `firstname` = "Bob"]`` is valid but ``[IF `firstname` = `nickname`]`` is not.
+ - the constant value must be surrounded with double quotes when it's a string which means that ``[IF `firstname` = "Bob"]`` is valid but ``[IF `firstname` = 'Bob']`` is not.
+ - the constant value must not be surrounded by any quotes is a numeric valu when it's a numeric value which means that ``[IF `age` >= 18]`` is valid.
+ - the data field must be on the left side of the comparison which means that ``[IF `gender` = \"Male\"]`` is valid but ``[IF \"Male\" = `gender`]`` is not.
+ - you can have multiple conditions seperated by `AND` or `OR` which means that ``IF `firstname` = "Bob" AND `lastname` = "Smith"]`` is valid.
+ - the acceptable operators when comparing a field to a string value: `<`, `<=`, `=`, `!=`, `>=`, `>`, `LIKE` and `NOT LIKE`
+ - the acceptable operators when comparing a field to a mumeric value: `<`, `<=`, `=`, `!=`, `>=` and `>`
+```csharp
+var subject = "Special sale ends today";
+var html = "
[IF `gender` = \"Male\"]Men's clothing is on sale[ELSE]Women's clothing is on sale[ENDIF]";
+var text = "[IF `gender` = \"Male\"]Men's clothing is on sale[ELSE]Women's clothing is on sale[ENDIF]";
+var mergeData = new Dictionary
+{
+ { "gender", "Male" }
+};
+var sent = await api.Relays.SendWithoutTrackingAsync(userKey, "recipient@example.com", subject, html, text, "you@yourcompany.com", "Your name", mergeData, null, null, clientId).ConfigureAwait(false);
+```
+
+ **7.0**
- - Add support for merge fields in html, text content and subject line when sending an email. CakeMail already support merge fields when sending bulk emails.
+ - Add support for merge fields in html, text content and subject line when sending an email. CakeMail already supports merge fields when sending bulk emails.
+ - Please keep in mind the following the rules for merge fields:
+ - square bracket is the delimeter which means that `[firstname]` is valid but `{firstname}` is not.
+ - you can specify a default value (sometimes called "fallback value") by adding a comma after the field name followed by the desired value. This default value is used when the value for the data field is undefined. For example: `Dear [firstname, friend]` will result in `Dear friend` if the firstname field is omitted or contains a null value for the current recipient.
+ - you can use `[TODAY]` to print the current date. Please note that `[NOW]` and `[DATE]` are also acceptable.
+ - when the merge field contains a numeric value or a datetime or when you use the `[TODAY]` merge field, you can specify a format string by adding the pipe character after the field name followed by the desired format string like so: `[TODAY | MMM d yyyy]`.
+ - Documentation for [datetime format string](https://msdn.microsoft.com/en-us/library/8kb3ddd4(v=vs.110).aspx)
+ - Documentation for [numeric format string](https://msdn.microsoft.com/en-us/library/0c899ak8(v=vs.110).aspx)
```csharp
var subject = "Special sale ends today";
var html = "Dear [firstname, friend], our annual sale ends today [TODAY | MMMM d].";
var text = "[salutation] [lastname], our annual sale ends today [TODAY | MMMM d].";
var mergeData = new Dictionary
{
- { "salutation", "Mr." },
- { "firstname", "Bob" },
- { "lastname", "Smith" }
+ { "salutation", "Mr." },
+ { "firstname", "Bob" },
+ { "lastname", "Smith" }
};
var sent = await api.Relays.SendWithoutTrackingAsync(userKey, "recipient@example.com", subject, html, text, "you@yourcompany.com", "Your name", mergeData, null, null, clientId).ConfigureAwait(false);
```
+ **6.0**
- - Fix bug when retrieving a Client record and the 'last_activity' field contains empty date
- - Add support for .NET STANDARD 1.3
- - Replace RestSharp with PathosChild.Http.FluentClient
- - Switch unit testing to xUnit
- - Implement GitFlow and repeatable build process
+ - Fix bug when retrieving a Client record and the 'last_activity' field contains empty date
+ - Add support for .NET STANDARD 1.3
+ - Replace RestSharp with PathosChild.Http.FluentClient
+ - Switch unit testing to xUnit
+ - Implement GitFlow and repeatable build process
+ **5.0**
- - Upgraded to .NET 4.5.2
+ - Upgraded to .NET 4.5.2
+ **4.0**
- - All methods are now async.
- - You can pass a cancellation token when invoking an async method.
+ - All methods are now async.
+ - You can pass a cancellation token when invoking an async method.
This means, for example, that the following v3.0 call:
@@ -66,8 +97,8 @@ var count = await cakeMail.Campaigns.GetCountAsync(userKey, MailingStatus.Ongoin
```
+ **3.0**
- - Methods are now logically grouped in separate resources. For instance, all methods related to users are grouped in a resource called 'Users', all methods related to campaigns are grouped in a resource called 'Campaigns', and so on.
- - Methods have been renamed to avoid repetition. For example, GetCampaignsCount has been renamed GetCount off of the new 'Campaigns' resource.
+ - Methods are now logically grouped in separate resources. For instance, all methods related to users are grouped in a resource called 'Users', all methods related to campaigns are grouped in a resource called 'Campaigns', and so on.
+ - Methods have been renamed to avoid repetition. For example, GetCampaignsCount has been renamed GetCount off of the new 'Campaigns' resource.
This means, for example, that the following v2.0 call:
@@ -83,16 +114,16 @@ var count = cakeMail.Campaigns.GetCount(userKey, MailingStatus.Ongoing);
+ **2.0**
- - Unique identifiers changed to 'long' instead of 'int'.
- - "Magic strings" replaced with enums. For example, instead of specifying sort direction with 'asc' and 'desc', you can now use SortDirection.Ascending and SortDirection.Descending.
- - Fix bug in CreateTemplateCategory which prevents creating new categories
- - Fix bug in DeleteTemplateCategory which causes an exception to be thrown despite the fact the category was successfuly deleted
- - Fix bug in GetListMembers which causes exception: 'Json does not contain property members'
- - Fix GetTriggerLinksLogs
- - Added XML comments file for convenient intellisense in Visual Studio
+ - Unique identifiers changed to 'long' instead of 'int'.
+ - "Magic strings" replaced with enums. For example, instead of specifying sort direction with 'asc' and 'desc', you can now use SortDirection.Ascending and SortDirection.Descending.
+ - Fix bug in CreateTemplateCategory which prevents creating new categories
+ - Fix bug in DeleteTemplateCategory which causes an exception to be thrown despite the fact the category was successfuly deleted
+ - Fix bug in GetListMembers which causes exception: 'Json does not contain property members'
+ - Fix GetTriggerLinksLogs
+ - Added XML comments file for convenient intellisense in Visual Studio
+ **1.0**
- - Initial release
+ - Initial release
## Installation
@@ -161,14 +192,14 @@ You can add members to your list like so:
await cakeMail.Lists.SubscribeAsync(userKey, listId, "bob_the_customer@hotmail.com", true, true, new[]
{
- new KeyValuePair("first_name", "Bob"),
- new KeyValuePair("last_name", "Smith"),
- new KeyValuePair("customer_since", DateTime.UtcNow)
+ new KeyValuePair("first_name", "Bob"),
+ new KeyValuePair("last_name", "Smith"),
+ new KeyValuePair("customer_since", DateTime.UtcNow)
});
await cakeMail.Lists.SubscribeAsync(userKey, listId, "jane_the_prospect@hotmail.com", true, true, new[]
{
- new KeyValuePair("first_name", "Jane"),
- new KeyValuePair("last_name", "Doe")
+ new KeyValuePair("first_name", "Jane"),
+ new KeyValuePair("last_name", "Doe")
});
```
@@ -177,23 +208,23 @@ or you can import a group of members:
```csharp
var member1 = new ListMember()
{
- Email = "bob_the_customer@hotmail.com",
- CustomFields = new Dictionary()
- {
- { "first_name", "Bob" },
- { "last_name", "Smith" },
- { "customer_since", DateTime.UtcNow }
- }
+ Email = "bob_the_customer@hotmail.com",
+ CustomFields = new Dictionary()
+ {
+ { "first_name", "Bob" },
+ { "last_name", "Smith" },
+ { "customer_since", DateTime.UtcNow }
+ }
};
var member2 = new ListMember()
{
- Email = "jane_the_prospect@hotmail.com",
- CustomFields = new Dictionary()
- {
- { "first_name", "Jane" },
- { "last_name", "Doe" }
- }
+ Email = "jane_the_prospect@hotmail.com",
+ CustomFields = new Dictionary()
+ {
+ { "first_name", "Jane" },
+ { "last_name", "Doe" }
+ }
};
var importResult = await cakeMail.Lists.ImportAsync(userKey, listId, new[] { member1, member2 });
diff --git a/Source/CakeMail.RestClient.UnitTests/Utilities/CakeMailContentParser.cs b/Source/CakeMail.RestClient.UnitTests/Utilities/CakeMailContentParser.cs
new file mode 100644
index 0000000..b144470
--- /dev/null
+++ b/Source/CakeMail.RestClient.UnitTests/Utilities/CakeMailContentParser.cs
@@ -0,0 +1,537 @@
+using CakeMail.RestClient.Utilities;
+using Shouldly;
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace CakeMail.RestClient.UnitTests.Utilities
+{
+ public class CakeMailContentParserTests
+ {
+ [Fact]
+ public void Returns_empty_string_when_content_is_null()
+ {
+ // Arrange
+ var content = (string)null;
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, null);
+
+ // Assert
+ result.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Returns_empty_string_when_content_is_empty_string()
+ {
+ // Arrange
+ var content = string.Empty;
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, null);
+
+ // Assert
+ result.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Field_without_fallback_is_successfully_merged_when_data_provided()
+ {
+ // Arrange
+ var content = "Dear [firstname]";
+ var data = new Dictionary
+ {
+ { "firstname", "Bob" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Dear Bob");
+ }
+
+ [Fact]
+ public void Field_without_fallback_is_merged_with_empty_string_when_data_is_omitted()
+ {
+ // Arrange
+ var content = "Dear [firstname]";
+ var data = new Dictionary
+ {
+ { "lastname", "Smith" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Dear ");
+ }
+
+ [Fact]
+ public void Fallback_is_merged_when_data_is_omitted()
+ {
+ // Arrange
+ var content = "Dear [firstname,friend]";
+ var data = new Dictionary
+ {
+ { "lastname", "Smith" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Dear friend");
+ }
+
+ [Fact]
+ public void Fallback_is_merged_when_null_value_is_specified()
+ {
+ // Arrange
+ var content = "Dear [firstname,friend]";
+ var data = new Dictionary
+ {
+ { "firstname", null }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Dear friend");
+ }
+
+ [Fact]
+ public void Datetime_with_format()
+ {
+ // Arrange
+ var content = "Thank you for being a customer since [customer_since | yyyy]";
+ var data = new Dictionary
+ {
+ { "customer_since", new DateTime(2017, 4, 30, 4, 57, 0) }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Thank you for being a customer since 2017");
+ }
+
+ [Fact]
+ public void Short_with_format()
+ {
+ // Arrange
+ var content = "We drove [miles_driven|#,#] miles during this trip";
+ var data = new Dictionary
+ {
+ { "miles_driven", (short)12345 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("We drove 12,345 miles during this trip");
+ }
+
+ [Fact]
+ public void Int_with_format()
+ {
+ // Arrange
+ var content = "We drove [miles_driven|#,#] miles during this trip";
+ var data = new Dictionary
+ {
+ { "miles_driven", (int)12345 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("We drove 12,345 miles during this trip");
+ }
+
+ [Fact]
+ public void Long_with_format()
+ {
+ // Arrange
+ var content = "We drove [miles_driven|#,#] miles during this trip";
+ var data = new Dictionary
+ {
+ { "miles_driven", (long)12345 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("We drove 12,345 miles during this trip");
+ }
+
+ [Fact]
+ public void Decimal_with_format()
+ {
+ // Arrange
+ var content = "We drove [miles_driven|0.00] miles today";
+ var data = new Dictionary
+ {
+ { "miles_driven", (decimal)12.345 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("We drove 12.35 miles today");
+ }
+
+ [Fact]
+ public void Float_with_format()
+ {
+ // Arrange
+ var content = "We drove [miles_driven|0.00] miles today";
+ var data = new Dictionary
+ {
+ { "miles_driven", (float)12.345 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("We drove 12.35 miles today");
+ }
+
+ [Fact]
+ public void Double_with_format()
+ {
+ // Arrange
+ var content = "We drove [miles_driven|0.00] miles today";
+ var data = new Dictionary
+ {
+ { "miles_driven", (double)12.345 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("We drove 12.35 miles today");
+ }
+
+ [Fact]
+ public void DATE_without_format()
+ {
+ // Arrange
+ var content = "The current date is: [DATE]";
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, null);
+
+ // Assert
+ Assert.StartsWith("The current date is: ", content);
+ }
+
+ [Fact]
+ public void NOW_without_format()
+ {
+ // Arrange
+ var content = "The current date is: [NOW]";
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, null);
+
+ // Assert
+ Assert.StartsWith("The current date is: ", content);
+ }
+
+ [Fact]
+ public void TODAY_without_format()
+ {
+ // Arrange
+ var content = "The current date is: [TODAY]";
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, null);
+
+ // Assert
+ Assert.StartsWith("The current date is: ", content);
+ }
+
+ [Fact]
+ public void DATE_with_format()
+ {
+ // Arrange
+ var content = "The current year is: [DATE | yyyy]";
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, null);
+
+ // Assert
+ result.ShouldBe($"The current year is: {DateTime.UtcNow.Year}");
+ }
+
+ [Fact]
+ public void NOW_with_format()
+ {
+ // Arrange
+ var content = "The current year is: [NOW|yyyy]";
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, null);
+
+ // Assert
+ result.ShouldBe($"The current year is: {DateTime.UtcNow.Year}");
+ }
+
+ [Fact]
+ public void TODAY_with_format()
+ {
+ // Arrange
+ var content = "The current year is: [TODAY |yyyy]";
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, null);
+
+ // Assert
+ result.ShouldBe($"The current year is: {DateTime.UtcNow.Year}");
+ }
+
+ [Fact]
+ public void IF_true()
+ {
+ // Arrange
+ var content = "[IF `firstname` = \"Bob\"]Yes[ELSE]No[ENDIF]";
+ var data = new Dictionary
+ {
+ { "firstname", "Bob" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Yes");
+ }
+
+ [Fact]
+ public void IF_false_with_ELSE()
+ {
+ // Arrange
+ var content = "[IF `firstname` = \"Robert\"]Yes[ELSE]No[ENDIF]";
+ var data = new Dictionary
+ {
+ { "firstname", "Bob" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("No");
+ }
+
+ [Fact]
+ public void IF_false_without_ELSE()
+ {
+ // Arrange
+ var content = "[IF `firstname` = \"Robert\"]Yes[ENDIF]";
+ var data = new Dictionary
+ {
+ { "firstname", "Bob" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void IF_false_with_ELSEIF_true()
+ {
+ // Arrange
+ var content = "[IF `firstname` = \"Robert\"]Yes - Robert[ELSEIF `firstname` = \"Bob\"]Yes - Bob[ELSE]No[ENDIF]";
+ var data = new Dictionary
+ {
+ { "firstname", "Bob" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Yes - Bob");
+ }
+
+ [Fact]
+ public void IF_false_with_ELSEIF_false()
+ {
+ // Arrange
+ var content = "[IF `firstname` = \"Robert\"]Yes - Robert[ELSEIF `firstname` = \"Bob\"]Yes - Bob[ELSE]No[ENDIF]";
+ var data = new Dictionary
+ {
+ { "firstname", "Jim" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("No");
+ }
+
+ [Fact]
+ public void Two_conditions_both_true()
+ {
+ // Arrange
+ var content = "[IF `firstname` = \"Jim\" AND `lastname` = \"Halpert\"]Yes[ELSE]No[ENDIF]";
+ var data = new Dictionary
+ {
+ { "firstname", "Jim" },
+ { "lastname", "Halpert" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Yes");
+ }
+
+ [Fact]
+ public void Two_conditions_one_true_one_false_AND()
+ {
+ // Arrange
+ var content = "[IF `firstname` = \"Jim\" AND `lastname` = \"Halpert\"]Yes[ELSE]No[ENDIF]";
+ var data = new Dictionary
+ {
+ { "firstname", "John" },
+ { "lastname", "Halpert" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("No");
+ }
+
+ [Fact]
+ public void Two_conditions_one_true_one_false_OR()
+ {
+ // Arrange
+ var content = "[IF `firstname` = \"Jim\" OR `lastname` = \"Halpert\"]Yes[ELSE]No[ENDIF]";
+ var data = new Dictionary
+ {
+ { "firstname", "John" },
+ { "lastname", "Halpert" }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Yes");
+ }
+
+ [Fact]
+ public void Numeric_true()
+ {
+ // Arrange
+ var content = "[IF `age` > \"18\"]Yes[ELSE]No[ENDIF]";
+ var data = new Dictionary
+ {
+ { "age", 25 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Yes");
+ }
+
+ [Fact]
+ public void Numeric_CAKEMAIL_BUG()
+ {
+ // CakeMail only allows comparing merge fields with string values (e.g.:[ IF `age` >= "18"]content for adults[ELSE]content for minors[ENDIF])
+ // which does not work properly in some cases. Let's say we want to display display content for adults aged 18 or over and different content
+ // for minors and let's say you have a recipient age 9. We will treat 9 as a string (per the CakeMail logic) and compare it to the string "18".
+ // Well... guess what: the character "9" is greater than the character "1" (which is the first character in the string "18") and therefore this
+ // person who is clearly underage would receive an email with content intended to adults. Conversely, if you have a recipient age 100, this
+ // person would receive an email with the content for minors because "0" (the second character in the string "100") is smaller than "8" (the
+ // second character in the string "18").
+
+ // Arrange
+ var content = "[IF `age` > \"18\"]Adult[ELSE]Minor[ENDIF]";
+ var data = new Dictionary
+ {
+ { "age", 9 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Adult");
+ }
+
+ [Fact]
+ public void Numeric_CAKEMAIL_BUG_Fix_false()
+ {
+ // The CakeMail.RestClient allows comparing merge fields with numeric values (e.g.: [ IF `age` >= 18]content for adults[ELSE]content for minors[ENDIF])
+ // which solves the problem I describe in the Numeric_CAKEMAIL_BUG unit test.
+
+ // Arrange
+ var content = "[IF `age` >= 18]Adult[ELSE]Minor[ENDIF]";
+ var data = new Dictionary
+ {
+ { "age", 9 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Minor");
+ }
+
+ [Fact]
+ public void Numeric_CAKEMAIL_BUG_FIX_true()
+ {
+ // Arrange
+ var content = "[IF `age` >= 18]Adult[ELSE]Minor[ENDIF]";
+ var data = new Dictionary
+ {
+ { "age", 18 }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Adult");
+ }
+
+ [Fact]
+ public void Numeric_CAKEMAIL_double()
+ {
+ // Arrange
+ var content = "[IF `amount` == 2.0]Yes[ELSE]No[ENDIF]";
+ var data = new Dictionary
+ {
+ { "amount", Math.Sqrt(2) * Math.Sqrt(2) }
+ };
+
+ // Act
+ var result = CakeMailContentParser.Parse(content, data);
+
+ // Assert
+ result.ShouldBe("Yes");
+ }
+ }
+}
diff --git a/Source/CakeMail.RestClient.UnitTests/Utilities/CakeMailMergeFieldsParser.cs b/Source/CakeMail.RestClient.UnitTests/Utilities/CakeMailMergeFieldsParser.cs
deleted file mode 100644
index 17fc450..0000000
--- a/Source/CakeMail.RestClient.UnitTests/Utilities/CakeMailMergeFieldsParser.cs
+++ /dev/null
@@ -1,305 +0,0 @@
-using CakeMail.RestClient.Utilities;
-using Newtonsoft.Json;
-using Shouldly;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text;
-using Xunit;
-
-namespace CakeMail.RestClient.UnitTests.Utilities
-{
- public class CakeMailMergeFieldsParserTests
- {
- [Fact]
- public void Returns_empty_string_when_content_is_null()
- {
- // Arrange
- var content = (string)null;
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, null);
-
- // Assert
- result.ShouldBeEmpty();
- }
-
- [Fact]
- public void Returns_empty_string_when_content_is_empty_string()
- {
- // Arrange
- var content = string.Empty;
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, null);
-
- // Assert
- result.ShouldBeEmpty();
- }
-
- [Fact]
- public void Field_without_fallback_is_successfully_merged_when_data_provided()
- {
- // Arrange
- var content = "Dear [firstname]";
- var data = new Dictionary
- {
- { "firstname", "Bob" }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("Dear Bob");
- }
-
- [Fact]
- public void Field_without_fallback_is_merged_with_empty_string_when_data_is_omitted()
- {
- // Arrange
- var content = "Dear [firstname]";
- var data = new Dictionary
- {
- { "lastname", "Smith" }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("Dear ");
- }
-
- [Fact]
- public void Fallback_is_merged_when_data_is_omitted()
- {
- // Arrange
- var content = "Dear [firstname,friend]";
- var data = new Dictionary
- {
- { "lastname", "Smith" }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("Dear friend");
- }
-
- [Fact]
- public void Fallback_is_merged_when_null_value_is_specified()
- {
- // Arrange
- var content = "Dear [firstname,friend]";
- var data = new Dictionary
- {
- { "firstname", null }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("Dear friend");
- }
-
- [Fact]
- public void Datetime_with_format()
- {
- // Arrange
- var content = "Thank you for being a customer since [customer_since | yyyy]";
- var data = new Dictionary
- {
- { "customer_since", new DateTime(2017, 4, 30, 4, 57, 0) }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("Thank you for being a customer since 2017");
- }
-
- [Fact]
- public void Short_with_format()
- {
- // Arrange
- var content = "We drove [miles_driven|#,#] miles during this trip";
- var data = new Dictionary
- {
- { "miles_driven", (short)12345 }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("We drove 12,345 miles during this trip");
- }
-
- [Fact]
- public void Int_with_format()
- {
- // Arrange
- var content = "We drove [miles_driven|#,#] miles during this trip";
- var data = new Dictionary
- {
- { "miles_driven", (int)12345 }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("We drove 12,345 miles during this trip");
- }
-
- [Fact]
- public void Long_with_format()
- {
- // Arrange
- var content = "We drove [miles_driven|#,#] miles during this trip";
- var data = new Dictionary
- {
- { "miles_driven", (long)12345 }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("We drove 12,345 miles during this trip");
- }
-
- [Fact]
- public void Decimal_with_format()
- {
- // Arrange
- var content = "We drove [miles_driven|0.00] miles today";
- var data = new Dictionary
- {
- { "miles_driven", (decimal)12.345 }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("We drove 12.35 miles today");
- }
-
- [Fact]
- public void Float_with_format()
- {
- // Arrange
- var content = "We drove [miles_driven|0.00] miles today";
- var data = new Dictionary
- {
- { "miles_driven", (float)12.345 }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("We drove 12.35 miles today");
- }
-
- [Fact]
- public void Double_with_format()
- {
- // Arrange
- var content = "We drove [miles_driven|0.00] miles today";
- var data = new Dictionary
- {
- { "miles_driven", (double)12.345 }
- };
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, data);
-
- // Assert
- result.ShouldBe("We drove 12.35 miles today");
- }
-
- [Fact]
- public void DATE_without_format()
- {
- // Arrange
- var content = "The current date is: [DATE]";
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, null);
-
- // Assert
- Assert.StartsWith("The current date is: ", content);
- }
-
- [Fact]
- public void NOW_without_format()
- {
- // Arrange
- var content = "The current date is: [NOW]";
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, null);
-
- // Assert
- Assert.StartsWith("The current date is: ", content);
- }
-
- [Fact]
- public void TODAY_without_format()
- {
- // Arrange
- var content = "The current date is: [TODAY]";
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, null);
-
- // Assert
- Assert.StartsWith("The current date is: ", content);
- }
-
- [Fact]
- public void DATE_with_format()
- {
- // Arrange
- var content = "The current year is: [DATE | yyyy]";
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, null);
-
- // Assert
- result.ShouldBe($"The current year is: {DateTime.UtcNow.Year}");
- }
-
- [Fact]
- public void NOW_with_format()
- {
- // Arrange
- var content = "The current year is: [NOW|yyyy]";
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, null);
-
- // Assert
- result.ShouldBe($"The current year is: {DateTime.UtcNow.Year}");
- }
-
- [Fact]
- public void TODAY_with_format()
- {
- // Arrange
- var content = "The current year is: [TODAY |yyyy]";
-
- // Act
- var result = CakeMailMergeFieldsParser.Parse(content, null);
-
- // Assert
- result.ShouldBe($"The current year is: {DateTime.UtcNow.Year}");
- }
- }
-}
diff --git a/Source/CakeMail.RestClient/Resources/Relays.cs b/Source/CakeMail.RestClient/Resources/Relays.cs
index 81617f1..1a15afd 100644
--- a/Source/CakeMail.RestClient/Resources/Relays.cs
+++ b/Source/CakeMail.RestClient/Resources/Relays.cs
@@ -44,9 +44,9 @@ public Relays(IClient client)
/// True if the email is sent
public Task SendWithoutTrackingAsync(string userKey, string recipientEmailAddress, string subject, string html, string text, string senderEmail, string senderName = null, IDictionary mergeData = null, MessageEncoding? encoding = null, long? clientId = null, CancellationToken cancellationToken = default(CancellationToken))
{
- subject = CakeMailMergeFieldsParser.Parse(subject, mergeData);
- html = CakeMailMergeFieldsParser.Parse(html, mergeData);
- text = CakeMailMergeFieldsParser.Parse(text, mergeData);
+ subject = CakeMailContentParser.Parse(subject, mergeData);
+ html = CakeMailContentParser.Parse(html, mergeData);
+ text = CakeMailContentParser.Parse(text, mergeData);
var parameters = new List>
{
@@ -89,9 +89,9 @@ public Relays(IClient client)
/// True if the email is sent
public Task SendWithTrackingAsync(string userKey, long trackingId, string recipientEmailAddress, string subject, string html, string text, string senderEmail, string senderName = null, IDictionary mergeData = null, MessageEncoding? encoding = null, long? clientId = null, CancellationToken cancellationToken = default(CancellationToken))
{
- subject = CakeMailMergeFieldsParser.Parse(subject, mergeData);
- html = CakeMailMergeFieldsParser.Parse(html, mergeData);
- text = CakeMailMergeFieldsParser.Parse(text, mergeData);
+ subject = CakeMailContentParser.Parse(subject, mergeData);
+ html = CakeMailContentParser.Parse(html, mergeData);
+ text = CakeMailContentParser.Parse(text, mergeData);
var parameters = new List>
{
diff --git a/Source/CakeMail.RestClient/Utilities/CakeMailContentParser.cs b/Source/CakeMail.RestClient/Utilities/CakeMailContentParser.cs
new file mode 100644
index 0000000..72547f0
--- /dev/null
+++ b/Source/CakeMail.RestClient/Utilities/CakeMailContentParser.cs
@@ -0,0 +1,307 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace CakeMail.RestClient.Utilities
+{
+ public static class CakeMailContentParser
+ {
+ private const string DYNAMIC_CONTENT_START_TAG = @"\[IF (.*?)\]";
+ private const string DYNAMIC_CONTENT_END_TAG = @"\[ENDIF\]";
+ private const string DYNAMIC_CONTENT_ELSEIF_TAG = "\\[ELSEIF (.*?)\\]";
+ private const string DYNAMIC_CONTENT_ELSE_TAG = "\\[ELSE\\]";
+ private const string DYNAMIC_CONTENT_CONDITION_STRING = "`(.*?)` (<=|>=|<|>|!=|==|=) \"(.*?)\"";
+ private const string DYNAMIC_CONTENT_CONDITION_NUMERIC = "`(.*?)` (<=|>=|<|>|!=|==|=) (.*?)$";
+
+ private static readonly Regex _currentDateRegex = new Regex(@"\[(NOW|TODAY|DATE)\s*(?:\|\s*(.*?))?\]", RegexOptions.Compiled);
+ private static readonly Regex _mergeFieldsRegex = new Regex(@"\[(.*?)\s*(?:\|\s*(.*?))?\]", RegexOptions.Compiled);
+
+ public static string Parse(string content, IDictionary data)
+ {
+ if (string.IsNullOrEmpty(content))
+ {
+ return string.Empty;
+ }
+
+ var mergedContent = ParseDynamicContent(content, data);
+ mergedContent = _currentDateRegex.Replace(mergedContent, (Match m) => CurrentDateMatchEval(m));
+ mergedContent = _mergeFieldsRegex.Replace(mergedContent, (Match m) => MergeFieldMatchEval(m, data));
+ return mergedContent;
+ }
+
+ private static string CurrentDateMatchEval(Match m)
+ {
+ var format = (m.Groups.Count >= 3 ? m.Groups[2] : null)?.Value ?? string.Empty;
+ var dateAsString = DateTime.UtcNow.ToString(format);
+
+ return dateAsString;
+ }
+
+ private static string MergeFieldMatchEval(Match m, IDictionary data)
+ {
+ var group1 = m.Groups[1].Value.Split(',');
+ var group2 = m.Groups.Count >= 3 ? m.Groups[2] : null;
+
+ var fieldName = group1[0].Trim();
+ var fallbackValue = (group1.Length >= 2 ? group1[1] : string.Empty).Trim();
+ var format = group2?.Value?.Trim() ?? string.Empty;
+
+ var returnValue = FieldDataAsString(fieldName, data, format, fallbackValue);
+ return returnValue;
+ }
+
+ private static string ParseDynamicContent(string content, IDictionary data)
+ {
+ if (string.IsNullOrEmpty(content)) return string.Empty;
+
+ var matchStartCondition = Regex.Match(content, DYNAMIC_CONTENT_START_TAG);
+ var matchEndCondition = Regex.Match(content, DYNAMIC_CONTENT_END_TAG);
+
+ // Make sure there is at least one "IF" statement
+ if (!matchStartCondition.Success) return content;
+
+ // Make sure the number of "IF" match the number of "ENDIF"
+ if (matchStartCondition.Success != matchEndCondition.Success || matchStartCondition.Captures.Count != matchEndCondition.Captures.Count)
+ {
+ throw new Exception("Dynamic content does not seem valid. Typically this is caused by a \"IF\" condition missing the corresponding \"ENDIF\"");
+ }
+
+ // Make sure the "ENDIF" does not preceed the "IF"
+ if (matchEndCondition.Index < matchStartCondition.Index + matchStartCondition.Length) return content;
+
+ var condition = matchStartCondition.Groups[1].Value;
+ var conditionalContent = content.Substring(matchStartCondition.Index + matchStartCondition.Length, matchEndCondition.Index - matchStartCondition.Index - matchStartCondition.Length);
+
+ var isTrue = EvaluateCondition(condition, data);
+ var parsedConditionalContent = ParseConditionalContent(isTrue, conditionalContent, data);
+
+ var parsedContent = new StringBuilder();
+ parsedContent.Append(content.Substring(0, matchStartCondition.Index));
+ parsedContent.Append(parsedConditionalContent);
+ parsedContent.Append(content.Substring(matchEndCondition.Index + matchEndCondition.Length));
+
+ return ParseDynamicContent(parsedContent.ToString(), data);
+ }
+
+ private static string ParseConditionalContent(bool condition, string content, IDictionary data)
+ {
+ if (string.IsNullOrEmpty(content)) return content;
+
+ var matchCondition = Regex.Match(content, DYNAMIC_CONTENT_ELSEIF_TAG);
+ if (matchCondition.Success && condition) return content.Substring(0, matchCondition.Index);
+
+ if (!matchCondition.Success)
+ {
+ matchCondition = Regex.Match(content, DYNAMIC_CONTENT_ELSE_TAG);
+ if (matchCondition.Success)
+ {
+ if (condition) return content.Substring(0, matchCondition.Index);
+ else return content.Substring(matchCondition.Index + matchCondition.Length);
+ }
+ else if (condition)
+ {
+ return content;
+ }
+ else
+ {
+ return string.Empty;
+ }
+ }
+
+ var isTrue = EvaluateCondition(matchCondition.Groups[1].Value, data);
+ var conditionalContent = ParseConditionalContent(isTrue, content.Substring(matchCondition.Index + matchCondition.Length), data);
+
+ return conditionalContent;
+ }
+
+ private static bool EvaluateCondition(string condition, IDictionary data)
+ {
+ return EvaluateCondition(condition, " AND ", data);
+ }
+
+ private static bool EvaluateCondition(string condition, string logicalOperator, IDictionary data)
+ {
+ if (string.IsNullOrEmpty(condition)) return false;
+
+ var subConditions = Regex.Split(condition, logicalOperator);
+ if (subConditions == null || subConditions.Length == 0)
+ {
+ return false;
+ }
+ else if (subConditions.Length > 1)
+ {
+ foreach (var subCondition in subConditions)
+ {
+ var result = EvaluateCondition(subCondition, data);
+ if (logicalOperator == " AND " && !result) return false;
+ else if (logicalOperator == " OR " && result) return true;
+ }
+
+ return logicalOperator == " AND ";
+ }
+ else if (logicalOperator == " AND ")
+ {
+ return EvaluateCondition(condition, " OR ", data);
+ }
+ else
+ {
+ return EvaluateSingleCondition(condition, data);
+ }
+ }
+
+ private static bool EvaluateSingleCondition(string condition, IDictionary data)
+ {
+ var matches = Regex.Match(condition, DYNAMIC_CONTENT_CONDITION_STRING);
+ if (matches.Success)
+ {
+ switch (matches.Groups[2].Value)
+ {
+ case "=":
+ case "==":
+ return IsEqual(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case "!=":
+ return !IsEqual(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case "LIKE":
+ return IsLike(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case "NOT LIKE":
+ return !IsLike(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case "<=":
+ return IsSmaller(matches.Groups[1].Value, matches.Groups[3].Value, data) || IsEqual(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case ">=":
+ return IsGreater(matches.Groups[1].Value, matches.Groups[3].Value, data) || IsEqual(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case "<":
+ return IsSmaller(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case ">":
+ return IsGreater(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ }
+ }
+
+ matches = Regex.Match(condition, DYNAMIC_CONTENT_CONDITION_NUMERIC);
+ if (matches.Success)
+ {
+ switch (matches.Groups[2].Value)
+ {
+ case "=":
+ case "==":
+ return IsEqualNumeric(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case "!=":
+ return !IsEqualNumeric(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case "<=":
+ return IsSmallerNumeric(matches.Groups[1].Value, matches.Groups[3].Value, data) || IsEqualNumeric(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case ">=":
+ return IsGreaterNumeric(matches.Groups[1].Value, matches.Groups[3].Value, data) || IsEqualNumeric(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case "<":
+ return IsSmallerNumeric(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ case ">":
+ return IsGreaterNumeric(matches.Groups[1].Value, matches.Groups[3].Value, data);
+ }
+ }
+
+ return false;
+ }
+
+ private static bool IsEqual(string fieldName, string value, IDictionary data)
+ {
+ value = value.TrimStart(new char[] { '"', '\'' }).TrimEnd(new char[] { '"', '\'' });
+ if (!data.ContainsKey(fieldName)) return string.IsNullOrEmpty(value);
+ return FieldDataAsString(fieldName, data, null, null) == value;
+ }
+
+ private static bool IsEqualNumeric(string fieldName, string value, IDictionary data)
+ {
+ var fieldData = (object)null;
+ data.TryGetValue(fieldName, out fieldData);
+
+ if (fieldData is short) return (short)fieldData == Convert.ToInt16(value);
+ else if (fieldData is int) return (int)fieldData == Convert.ToInt32(value);
+ else if (fieldData is long) return (long)fieldData == Convert.ToInt64(value);
+ else if (fieldData is decimal) return (decimal)fieldData == Convert.ToDecimal(value);
+
+ // Cannot compare float/double because they are stored as binary fractions, not decimal fractions.
+ // Explanation: https://stackoverflow.com/questions/21895756/why-are-floating-point-numbers-inaccurate
+ // A solution described in this blog post (https://csharp.2000things.com/2011/09/21/416-use-an-epsilon-to-compare-two-floating-point-numbers/) suggests checking if the two values are "nearly" identical
+ else if (fieldData is float) return Math.Abs((float)fieldData - Convert.ToSingle(value)) < 0.00001;
+ else if (fieldData is double) return Math.Abs((double)fieldData - Convert.ToDouble(value)) < 0.00001;
+
+ return false;
+ }
+
+ private static bool IsSmaller(string fieldName, string value, IDictionary data)
+ {
+ if (!data.ContainsKey(fieldName)) return false;
+ value = value.TrimStart(new char[] { '"', '\'' }).TrimEnd(new char[] { '"', '\'' });
+ return string.Compare(data[fieldName].ToString(), value, StringComparison.Ordinal) < 0;
+ }
+
+ private static bool IsSmallerNumeric(string fieldName, string value, IDictionary data)
+ {
+ var fieldData = (object)null;
+ data.TryGetValue(fieldName, out fieldData);
+
+ if (fieldData is short) return (short)fieldData < Convert.ToInt16(value);
+ else if (fieldData is int) return (int)fieldData < Convert.ToInt32(value);
+ else if (fieldData is long) return (long)fieldData < Convert.ToInt64(value);
+ else if (fieldData is decimal) return (decimal)fieldData < Convert.ToDecimal(value);
+
+ // When checking if a float/double is smaller than another, we must make sure the two values are not "nearly" identical
+ else if (fieldData is float) return (float)fieldData < Convert.ToSingle(value) && Math.Abs((float)fieldData - Convert.ToSingle(value)) > 0.00001;
+ else if (fieldData is double) return (double)fieldData < Convert.ToDouble(value) && Math.Abs((double)fieldData - Convert.ToDouble(value)) > 0.00001;
+
+ return false;
+ }
+
+ private static bool IsGreater(string fieldName, string value, IDictionary data)
+ {
+ if (!data.ContainsKey(fieldName)) return false;
+ value = value.TrimStart(new char[] { '"', '\'' }).TrimEnd(new char[] { '"', '\'' });
+ return string.Compare(data[fieldName].ToString(), value, StringComparison.Ordinal) > 0;
+ }
+
+ private static bool IsGreaterNumeric(string fieldName, string value, IDictionary data)
+ {
+ var fieldData = (object)null;
+ data.TryGetValue(fieldName, out fieldData);
+
+ if (fieldData is short) return (short)fieldData > Convert.ToInt16(value);
+ else if (fieldData is int) return (int)fieldData > Convert.ToInt32(value);
+ else if (fieldData is long) return (long)fieldData > Convert.ToInt64(value);
+ else if (fieldData is decimal) return (decimal)fieldData > Convert.ToDecimal(value);
+
+ // When checking if a float/double is greater than another, we must make sure the two values are not "nearly" identical
+ else if (fieldData is float) return (float)fieldData > Convert.ToSingle(value) && Math.Abs((float)fieldData - Convert.ToSingle(value)) > 0.00001;
+ else if (fieldData is double) return (double)fieldData > Convert.ToDouble(value) && Math.Abs((double)fieldData - Convert.ToDouble(value)) > 0.00001;
+
+ return false;
+ }
+
+ private static bool IsLike(string fieldName, string value, IDictionary data)
+ {
+ if (!data.ContainsKey(fieldName)) return false;
+ value = value.TrimStart(new char[] { '"', '\'' }).TrimEnd(new char[] { '"', '\'' });
+ var a = Regex.IsMatch(data[fieldName].ToString(), value.Replace("%", "(.*?)"));
+ return a;
+ }
+
+ private static string FieldDataAsString(string fieldName, IDictionary data, string format = null, string fallbackValue = null)
+ {
+ var fieldData = (object)null;
+ data.TryGetValue(fieldName, out fieldData);
+
+ var returnValue = (string)null;
+ if (fieldData != null)
+ {
+ if (fieldData is DateTime) returnValue = ((DateTime)fieldData).ToString(format ?? "yyyy-MM-dd HH-mm-ss");
+ else if (fieldData is short) returnValue = ((short)fieldData).ToString(format);
+ else if (fieldData is int) returnValue = ((int)fieldData).ToString(format);
+ else if (fieldData is long) returnValue = ((long)fieldData).ToString(format);
+ else if (fieldData is decimal) returnValue = ((decimal)fieldData).ToString(format);
+ else if (fieldData is float) returnValue = ((float)fieldData).ToString(format);
+ else if (fieldData is double) returnValue = ((double)fieldData).ToString(format);
+ else returnValue = fieldData.ToString();
+ }
+
+ return returnValue ?? fallbackValue ?? string.Empty;
+ }
+ }
+}
diff --git a/Source/CakeMail.RestClient/Utilities/CakeMailMergeFieldsParser.cs b/Source/CakeMail.RestClient/Utilities/CakeMailMergeFieldsParser.cs
deleted file mode 100644
index 9ad7819..0000000
--- a/Source/CakeMail.RestClient/Utilities/CakeMailMergeFieldsParser.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.RegularExpressions;
-
-namespace CakeMail.RestClient.Utilities
-{
- public static class CakeMailMergeFieldsParser
- {
- private static readonly Regex _currentDateRegex = new Regex(@"\[(NOW|TODAY|DATE)\s*(?:\|\s*(.*?))?\]", RegexOptions.Compiled);
- private static readonly Regex _mergeFieldsRegex = new Regex(@"\[(.*?)\s*(?:\|\s*(.*?))?\]", RegexOptions.Compiled);
-
- public static string Parse(string content, IDictionary data)
- {
- if (string.IsNullOrEmpty(content))
- {
- return string.Empty;
- }
-
- var mergedContent = _currentDateRegex.Replace(content, (Match m) => CurrentDateMatchEval(m));
- mergedContent = _mergeFieldsRegex.Replace(mergedContent, (Match m) => MatchEval(m, data));
-
- return mergedContent;
- }
-
- private static string CurrentDateMatchEval(Match m)
- {
- var format = (m.Groups.Count >= 3 ? m.Groups[2] : null)?.Value ?? string.Empty;
- var dateAsString = DateTime.UtcNow.ToString(format);
-
- return dateAsString;
- }
-
- private static string MatchEval(Match m, IDictionary data)
- {
- var group1 = m.Groups[1].Value.Split(',');
- var group2 = (m.Groups.Count >= 3 ? m.Groups[2] : null);
-
- var fieldName = group1[0].Trim();
- var fallbackValue = (group1.Length >= 2 ? group1[1] : string.Empty).Trim();
- var format = group2?.Value?.Trim() ?? string.Empty;
-
- if (data != null && data.ContainsKey(fieldName))
- {
- if (data[fieldName] is DateTime) return ((DateTime)data[fieldName]).ToString(format);
- else if (data[fieldName] is short) return ((short)data[fieldName]).ToString(format);
- else if (data[fieldName] is int) return ((int)data[fieldName]).ToString(format);
- else if (data[fieldName] is long) return ((long)data[fieldName]).ToString(format);
- else if (data[fieldName] is decimal) return ((decimal)data[fieldName]).ToString(format);
- else if (data[fieldName] is float) return ((float)data[fieldName]).ToString(format);
- else if (data[fieldName] is double) return ((double)data[fieldName]).ToString(format);
- else return data[fieldName]?.ToString() ?? fallbackValue;
- }
- else
- {
- return fallbackValue;
- }
- }
- }
-}