diff --git a/Solutions/Endjin.Templify.Domain/Contracts/Packager/Tokeniser/IFunctionTokenizer.cs b/Solutions/Endjin.Templify.Domain/Contracts/Packager/Tokeniser/IFunctionTokenizer.cs new file mode 100644 index 0000000..5043e48 --- /dev/null +++ b/Solutions/Endjin.Templify.Domain/Contracts/Packager/Tokeniser/IFunctionTokenizer.cs @@ -0,0 +1,8 @@ +namespace Endjin.Templify.Domain.Contracts.Packager.Tokeniser { + + public interface IFunctionTokenizer { + + string TokenizeContent( string Content ); + + } +} \ No newline at end of file diff --git a/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/DateFunctionTokenizer.cs b/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/DateFunctionTokenizer.cs new file mode 100644 index 0000000..e1070e6 --- /dev/null +++ b/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/DateFunctionTokenizer.cs @@ -0,0 +1,55 @@ +namespace Endjin.Templify.Domain.Domain.Packager.Tokeniser { + using System; + using System.ComponentModel.Composition; + using System.Text.RegularExpressions; + using Endjin.Templify.Domain.Contracts.Packager.Tokeniser; + + [Export( typeof(IFunctionTokenizer) )] + public class DateFunctionTokenizer : IFunctionTokenizer { + private readonly IDateTimeNowFactory dateTimeNowFactory; + private readonly Regex dateRegex; + + [ImportingConstructor] + public DateFunctionTokenizer( IDateTimeNowFactory DateTimeNowFactory ) { + if ( DateTimeNowFactory == null ) { + throw new ArgumentNullException( "DateTimeNowFactory" ); + } + this.dateTimeNowFactory = DateTimeNowFactory; + this.dateRegex = new Regex( @"__DATE\(([^)]*)\)__", RegexOptions.Compiled ); + } + + public string TokenizeContent( string Content ) { + string results = Content; + if ( !string.IsNullOrWhiteSpace( Content ) ) { + results = dateRegex.Replace( + Content, match => { + string format = ""; + if ( match.Groups.Count > 1 ) { + format = match.Groups[1].Value; + } + if ( string.IsNullOrWhiteSpace( format ) ) { + format = "d"; // Short date + } + return this.dateTimeNowFactory.Now().ToString( format ); + } + ); + } + return results; + } + + } + + // For testability: + + public interface IDateTimeNowFactory { + DateTime Now(); + } + + [Export( typeof(IDateTimeNowFactory) )] + public class DateTimeNowFactory : IDateTimeNowFactory { + public DateTime Now() { + return DateTime.Now; + } + } + +} \ No newline at end of file diff --git a/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/GuidFunctionTokenizer.cs b/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/GuidFunctionTokenizer.cs new file mode 100644 index 0000000..9f1b05d --- /dev/null +++ b/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/GuidFunctionTokenizer.cs @@ -0,0 +1,53 @@ +namespace Endjin.Templify.Domain.Domain.Packager.Tokeniser { + using System; + using System.ComponentModel.Composition; + using System.Text.RegularExpressions; + using Endjin.Templify.Domain.Contracts.Packager.Tokeniser; + + [Export( typeof(IFunctionTokenizer) )] + public class GuidFunctionTokenizer : IFunctionTokenizer { + private readonly IGuidFactory guidFactory; + private readonly Regex guidRegex; + + [ImportingConstructor] + public GuidFunctionTokenizer( IGuidFactory GuidFactory ) { + if ( GuidFactory == null ) { + throw new ArgumentNullException( "GuidFactory" ); + } + this.guidFactory = GuidFactory; + this.guidRegex = new Regex( @"__GUID\(([A-Za-z]?)\)__", RegexOptions.Compiled ); + } + + public string TokenizeContent( string Content ) { + string results = Content; + if ( !string.IsNullOrWhiteSpace( Content ) ) { + results = guidRegex.Replace( + Content, match => { + string format = ""; + if ( match.Groups.Count > 1 ) { + format = match.Groups[1].Value; + } + Guid guid = this.guidFactory.NewGuid(); + return guid.ToString( format ); // Valid format values: http://msdn.microsoft.com/en-us/library/97af8hh4.aspx + } + ); + } + return results; + } + + } + + // For testability: + + public interface IGuidFactory { + Guid NewGuid(); + } + + [Export( typeof(IGuidFactory) )] + public class GuidFactory : IGuidFactory { + public Guid NewGuid() { + return Guid.NewGuid(); + } + } + +} \ No newline at end of file diff --git a/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/TemplateTokeniser.cs b/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/TemplateTokeniser.cs index fdf2b66..2f90672 100644 --- a/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/TemplateTokeniser.cs +++ b/Solutions/Endjin.Templify.Domain/Domain/Packager/Tokeniser/TemplateTokeniser.cs @@ -16,13 +16,15 @@ namespace Endjin.Templify.Domain.Domain.Packager.Tokeniser public class TemplateTokeniser : ITemplateTokeniser { private readonly IFileContentProcessor fileContentProcessor; + private readonly IEnumerable functionTokenizers; private readonly IRenameFileProcessor renameFileProcessor; [ImportingConstructor] - public TemplateTokeniser(IRenameFileProcessor renameFileProcessor, IFileContentProcessor fileContentProcessor) + public TemplateTokeniser(IRenameFileProcessor renameFileProcessor, IFileContentProcessor fileContentProcessor, [ImportMany] IEnumerable FunctionTokenizers) { this.renameFileProcessor = renameFileProcessor; this.fileContentProcessor = fileContentProcessor; + this.functionTokenizers = FunctionTokenizers; } public void TokeniseDirectoryAndFilePaths(string file, Dictionary tokens) @@ -38,9 +40,13 @@ public void TokeniseFileContent(string file, Dictionary tokens) this.fileContentProcessor.WriteContents(file, contents); } - private static string Replace(Dictionary tokens, string value) + private string Replace(Dictionary tokens, string value) { - return tokens.Aggregate(value, (current, token) => Regex.Replace(current, token.Key, match => token.Value)); + string val = tokens.Aggregate(value, (current, token) => Regex.Replace(current, token.Key, match => token.Value)); + if ( this.functionTokenizers != null ) { + val = this.functionTokenizers.Aggregate(val, (current, tokenizer) => tokenizer.TokenizeContent(current)); + } + return val; } } } \ No newline at end of file diff --git a/Solutions/Endjin.Templify.Domain/Endjin.Templify.Domain.csproj b/Solutions/Endjin.Templify.Domain/Endjin.Templify.Domain.csproj index acf3c62..c87edac 100644 --- a/Solutions/Endjin.Templify.Domain/Endjin.Templify.Domain.csproj +++ b/Solutions/Endjin.Templify.Domain/Endjin.Templify.Domain.csproj @@ -67,6 +67,7 @@ + @@ -93,7 +94,9 @@ + + diff --git a/Solutions/Endjin.Templify.Specifications/Domain/Packager/Tokeniser/DateFunctionTokenizerSpecs.cs b/Solutions/Endjin.Templify.Specifications/Domain/Packager/Tokeniser/DateFunctionTokenizerSpecs.cs new file mode 100644 index 0000000..df4e61a --- /dev/null +++ b/Solutions/Endjin.Templify.Specifications/Domain/Packager/Tokeniser/DateFunctionTokenizerSpecs.cs @@ -0,0 +1,75 @@ +namespace Endjin.Templify.Specifications.Domain.Packager.Tokeniser { + using System; + using Endjin.Templify.Domain.Domain.Packager.Tokeniser; + using NUnit.Framework; + + [TestFixture] + public class DateFunctionTokenizerSpecs { + + private static readonly DateTime testDate = new DateTime( 2000, 5, 12, 2, 52, 19 ); // FRAGILE: Tests hard-code this in expected results + + [TestCase( "", "" )] + [TestCase( (string)null, (string)null )] + [TestCase( "__DATE()__", "5/12/2000" )] + [TestCase( "abc__DATE()__def", "abc5/12/2000def" )] + [TestCase( "abc__DATE(__def", "abc__DATE(__def" )] + [TestCase( "abc__DATE()__def__DATE()__ghi", "abc5/12/2000def5/12/2000ghi" )] + [TestCase( "abc\n__DATE()__\ndef\n__DATE()__\nghi", "abc\n5/12/2000\ndef\n5/12/2000\nghi" )] + public void ReplacementExpressions_Work( string Content, string Expected ) { + + // Arrange + MockDateTimeNowFactory mock = new MockDateTimeNowFactory( testDate ); + DateFunctionTokenizer d = new DateFunctionTokenizer( mock ); + + // Act + string actual = d.TokenizeContent( Content ); + + // Assert + Assert.That( actual, Is.EqualTo( Expected ) ); + + } + + [TestCase( "" )] + [TestCase( "d" )] + [TestCase( "D" )] + [TestCase( "t" )] + [TestCase( "T" )] + [TestCase( "f" )] + [TestCase( "F" )] + [TestCase( "dd-MM-yyyy hh:mm:ss tt" )] + [TestCase( "yyyy" )] + [TestCase( "ddd" )] + public void Format_Works( string Format ) { + + // Arrange + string content = "__DATE(" + Format + ")__"; + if ( string.IsNullOrEmpty( Format ) ) { + Format = "d"; // FRAGILE: Duplicates business logic in class under test + } + MockDateTimeNowFactory mock = new MockDateTimeNowFactory( testDate ); + DateFunctionTokenizer d = new DateFunctionTokenizer( mock ); + + // Act + string actual = d.TokenizeContent( content ); + + // Assert + string expected = mock.Now().ToString( Format ); + Assert.That( actual, Is.EqualTo( expected ) ); + } + + + public class MockDateTimeNowFactory : IDateTimeNowFactory { + private readonly DateTime dateTime; + + public MockDateTimeNowFactory( DateTime DateTime ) { + this.dateTime = DateTime; + } + + public DateTime Now() { + return dateTime; + } + + } + + } +} \ No newline at end of file diff --git a/Solutions/Endjin.Templify.Specifications/Domain/Packager/Tokeniser/GuidFunctionTokenizerSpecs.cs b/Solutions/Endjin.Templify.Specifications/Domain/Packager/Tokeniser/GuidFunctionTokenizerSpecs.cs new file mode 100644 index 0000000..c58ee7e --- /dev/null +++ b/Solutions/Endjin.Templify.Specifications/Domain/Packager/Tokeniser/GuidFunctionTokenizerSpecs.cs @@ -0,0 +1,102 @@ +namespace Endjin.Templify.Specifications.Domain.Packager.Tokeniser { + using System; + using System.Collections.Generic; + using Endjin.Templify.Domain.Domain.Packager.Tokeniser; + using NUnit.Framework; + + [TestFixture] + public class GuidFunctionTokenizerSpecs { + + [TestCase( "", "" )] + [TestCase( (string)null, (string)null )] + [TestCase( "__GUID()__", "00000000-0000-0000-0000-000000000000" )] + [TestCase( "abc__GUID()__def", "abc00000000-0000-0000-0000-000000000000def" )] + [TestCase( "abc__GUID(__def", "abc__GUID(__def" )] + [TestCase( "abc__GUID()__def__GUID()__ghi", "abc00000000-0000-0000-0000-000000000000def00000000-0000-0000-0000-000000000000ghi" )] + [TestCase( "abc\n__GUID()__\ndef\n__GUID()__\nghi", "abc\n00000000-0000-0000-0000-000000000000\ndef\n00000000-0000-0000-0000-000000000000\nghi" )] + public void ReplacementExpressions_Work( string Content, string Expected ) { + + // Arrange + MockGuidFactory mock = new MockGuidFactory( Guid.Empty ); + GuidFunctionTokenizer g = new GuidFunctionTokenizer( mock ); + + // Act + string actual = g.TokenizeContent( Content ); + + // Assert + Assert.That( actual, Is.EqualTo( Expected ) ); + } + + [Test] + public void EachTokenGetsUniqueGuid() { + + // Arrange + List guids = new List() { + Guid.NewGuid(), + Guid.NewGuid(), + Guid.NewGuid() + }; + string content = "__GUID()__ __GUID()__ __GUID()__"; + string expected = string.Format( "{0} {1} {2}", guids[0], guids[1], guids[2] ); + MockGuidListFactory mock = new MockGuidListFactory( guids ); + GuidFunctionTokenizer g = new GuidFunctionTokenizer( mock ); + + // Act + string actual = g.TokenizeContent( content ); + + // Assert + Assert.That( actual, Is.EqualTo( expected ) ); + } + + [TestCase( "" )] + [TestCase( "N" )] + [TestCase( "D" )] + [TestCase( "B" )] + [TestCase( "P" )] + [TestCase( "X" )] + public void Format_Works( string Format ) { + + // Arrange + string content = "__GUID(" + Format + ")__"; + MockGuidFactory mock = new MockGuidFactory( Guid.NewGuid() ); + GuidFunctionTokenizer g = new GuidFunctionTokenizer( mock ); + + // Act + string actual = g.TokenizeContent( content ); + + // Assert + string expected = mock.NewGuid().ToString( Format ); + Assert.That( actual, Is.EqualTo( expected ) ); + } + + + public class MockGuidFactory : IGuidFactory { + private readonly Guid guid; + + public MockGuidFactory( Guid Guid ) { + this.guid = Guid; + } + + public Guid NewGuid() { + return guid; + } + } + + public class MockGuidListFactory : IGuidFactory { + private readonly List guids; + private int step = -1; + + public MockGuidListFactory( List Guids ) { + this.guids = Guids; + } + + public Guid NewGuid() { + // The next one + step++; + Guid g = guids.Count > step ? guids[step] : Guid.Empty; + return g; + } + } + + } +} \ No newline at end of file diff --git a/Solutions/Endjin.Templify.Specifications/Endjin.Templify.Specifications.csproj b/Solutions/Endjin.Templify.Specifications/Endjin.Templify.Specifications.csproj index bb1d9ea..477656f 100644 --- a/Solutions/Endjin.Templify.Specifications/Endjin.Templify.Specifications.csproj +++ b/Solutions/Endjin.Templify.Specifications/Endjin.Templify.Specifications.csproj @@ -66,7 +66,6 @@ Endjin.Templify.Domain -