Skip to content

Implementation of parameter tags #18

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

Merged
merged 3 commits into from
Oct 9, 2016
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
5 changes: 5 additions & 0 deletions src/Strinken/Engine/SpecialCharacter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,10 @@ internal static class SpecialCharacter
/// Separator that indicates the start of a token.
/// </summary>
public const int TokenStartIndicator = '{';

/// <summary>
/// Separator that indicates the start of a parameter tag.
/// </summary>
public const int ParameterTagIndicator = '!';
}
}
13 changes: 9 additions & 4 deletions src/Strinken/Engine/StrinkenEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ private static ParseResult<Token> ParseArgument(Cursor cursor)
{
subtype = TokenSubtype.Tag;
}
else if (result.Value.Subtype == TokenSubtype.ParameterTag)
{
subtype = TokenSubtype.ParameterTag;
}

return ParseResult<Token>.Success(new Token(result.Value.Data, TokenType.Argument, subtype));
}
Expand Down Expand Up @@ -269,10 +273,11 @@ private static ParseResult<IList<Token>> ParseString(Cursor cursor)
private static ParseResult<Token> ParseTag(Cursor cursor, bool isArgument = false)
{
var subtype = TokenSubtype.Base;

/*
Special character before the token can be parsed here.
*/
if (cursor.Value == SpecialCharacter.ParameterTagIndicator)
{
subtype = TokenSubtype.ParameterTag;
cursor.Next();
}

var ends = new List<int> { SpecialCharacter.FilterSeparator };
if (isArgument)
Expand Down
7 changes: 6 additions & 1 deletion src/Strinken/Engine/TokenSubtype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ internal enum TokenSubtype
/// <summary>
/// The token is also a tag token.
/// </summary>
Tag
Tag,

/// <summary>
/// The token is also a parameter tag token.
/// </summary>
ParameterTag
}
}
16 changes: 16 additions & 0 deletions src/Strinken/Parser/IParameterTag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// stylecop.header

namespace Strinken.Parser
{
/// <summary>
/// Interface describing a parameter tag.
/// </summary>
public interface IParameterTag : IToken
{
/// <summary>
/// Resolves the parameter tag.
/// </summary>
/// <returns>The resulting value.</returns>
string Resolve();
}
}
32 changes: 32 additions & 0 deletions src/Strinken/Parser/ParserExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,37 @@ public static Parser<T> WithTags<T>(this Parser<T> parser, IEnumerable<ITag<T>>

return copiedParser;
}

/// <summary>
/// Add a parameter tag to the parser.
/// </summary>
/// <param name="parser">The parser on which the method is called.</param>
/// <param name="parameterTag">The parameter tag to add.</param>
/// <returns>A <see cref="Parser{T}"/> for chaining.</returns>
/// <typeparam name="T">The type related to the parser.</typeparam>
public static Parser<T> WithParameterTag<T>(this Parser<T> parser, IParameterTag parameterTag)
{
var copiedParser = parser.DeepCopy();
copiedParser.AddParameterTag(parameterTag);
return copiedParser;
}

/// <summary>
/// Add parameter tags to the parser.
/// </summary>
/// <param name="parser">The parser on which the method is called.</param>
/// <param name="parameterTags">The parameter tags to add.</param>
/// <returns>A <see cref="Parser{T}"/> for chaining.</returns>
/// <typeparam name="T">The type related to the parser.</typeparam>
public static Parser<T> WithParameterTags<T>(this Parser<T> parser, IEnumerable<IParameterTag> parameterTags)
{
var copiedParser = parser.DeepCopy();
foreach (var parameterTag in parameterTags)
{
copiedParser.AddParameterTag(parameterTag);
}

return copiedParser;
}
}
}
51 changes: 51 additions & 0 deletions src/Strinken/Parser/Parser`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@ public class Parser<T>
/// </summary>
private readonly IDictionary<string, ITag<T>> tags;

/// <summary>
/// Parameter tags used by the parser.
/// </summary>
private readonly IDictionary<string, IParameterTag> parameterTags;

/// <summary>
/// Initializes a new instance of the <see cref="Parser{T}"/> class.
/// </summary>
public Parser()
{
this.tags = new Dictionary<string, ITag<T>>();
this.parameterTags = new Dictionary<string, IParameterTag>();
this.filters = new Dictionary<string, IFilter>();

foreach (var filter in FilterHelpers.BaseFilters)
Expand All @@ -49,6 +55,11 @@ public Parser()
/// </summary>
public IReadOnlyCollection<ITag<T>> Tags => new ReadOnlyCollection<ITag<T>>(this.tags.Values.ToList());

/// <summary>
/// Gets the tags used by the parser.
/// </summary>
public IReadOnlyCollection<IParameterTag> ParameterTags => new ReadOnlyCollection<IParameterTag>(this.parameterTags.Values.ToList());

/// <summary>
/// Resolves the input.
/// </summary>
Expand All @@ -63,7 +74,9 @@ public string Resolve(string input, T value)
var actions = new ActionDictionary
{
[TokenType.Tag, TokenSubtype.Base] = a => this.tags[a[0]].Resolve(value),
[TokenType.Tag, TokenSubtype.ParameterTag] = a => this.parameterTags[a[0]].Resolve(),
[TokenType.Argument, TokenSubtype.Tag] = a => this.tags[a[0]].Resolve(value),
[TokenType.Argument, TokenSubtype.ParameterTag] = a => this.parameterTags[a[0]].Resolve(),
[TokenType.Filter, TokenSubtype.Base] = a => this.filters[a[0]].Resolve(a[1], a.Skip(2).ToArray())
};

Expand All @@ -81,6 +94,7 @@ public string Resolve(string input, T value)
public ValidationResult Validate(string input)
{
var tagList = new List<string>();
var parameterTagList = new List<string>();
var filterList = new List<Tuple<string, string[]>>();
var validator = new StrinkenEngine();

Expand All @@ -97,11 +111,21 @@ public ValidationResult Validate(string input)
tagList.Add(a[0]);
return string.Empty;
},
[TokenType.Tag, TokenSubtype.ParameterTag] = a =>
{
parameterTagList.Add(a[0]);
return string.Empty;
},
[TokenType.Argument, TokenSubtype.Tag] = a =>
{
tagList.Add(a[0]);
return string.Empty;
},
[TokenType.Argument, TokenSubtype.ParameterTag] = a =>
{
parameterTagList.Add(a[0]);
return string.Empty;
},
[TokenType.Filter, TokenSubtype.Base] = a =>
{
filterList.Add(Tuple.Create(a[0], a.Skip(2).ToArray()));
Expand All @@ -118,6 +142,13 @@ public ValidationResult Validate(string input)
return new ValidationResult { Message = $"{invalidParameter} is not a valid tag.", IsValid = false };
}

// Find the first parameter tag that was not registered in the parser.
invalidParameter = parameterTagList.FirstOrDefault(parameterTagName => !this.parameterTags.ContainsKey(parameterTagName));
if (invalidParameter != null)
{
return new ValidationResult { Message = $"{invalidParameter} is not a valid parameter tag.", IsValid = false };
}

// Find the first filter that was not registered in the parser.
invalidParameter = filterList.FirstOrDefault(filter => !this.filters.ContainsKey(filter.Item1))?.Item1;
if (invalidParameter != null)
Expand Down Expand Up @@ -165,6 +196,21 @@ public void AddTag(ITag<T> tag)
this.tags.Add(tag.Name, tag);
}

/// <summary>
/// Add a parameter tag to the list of parameter tags.
/// </summary>
/// <param name="parameterTag">The parameter tag to add.</param>
public void AddParameterTag(IParameterTag parameterTag)
{
if (this.parameterTags.ContainsKey(parameterTag.Name))
{
throw new ArgumentException($"{parameterTag.Name} was already registered in the parameter tag list.");
}

ThrowIfInvalidName(parameterTag.Name);
this.parameterTags.Add(parameterTag.Name, parameterTag);
}

/// <summary>
/// Creates a deep copy of the current parser.
/// </summary>
Expand All @@ -177,6 +223,11 @@ internal Parser<T> DeepCopy()
newParser.AddTag(tag);
}

foreach (var parameterTag in this.parameterTags.Values)
{
newParser.AddParameterTag(parameterTag);
}

var parserOwnFilters = this.filters.Where(f => !FilterHelpers.BaseFilters.Select(bf => bf.Name).Contains(f.Value.Name));
foreach (var filter in parserOwnFilters)
{
Expand Down
55 changes: 55 additions & 0 deletions test/Strinken.Tests/ActionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -412,5 +412,60 @@ public void Run_NoActions_ReturnsOutsideString()
const string input = "lorem{ipsum:patse+paku,=malo}aku";
Assert.That(new StrinkenEngine().Run(input).Stack.Resolve(null), Is.EqualTo("loremaku"));
}

[Test]
public void Run_OneParameterTag_ActionOnParameterTagCalledOnce()
{
var numberOfCall = 0;
var tagSeen = new List<string>();
var actions = new ActionDictionary
{
[TokenType.Tag, TokenSubtype.ParameterTag] = a =>
{
numberOfCall++;
tagSeen.Add(a[0]);
return a[0];
}
};

var engine = new StrinkenEngine();
const string input = "lorem{!ispum}tute";
var result = engine.Run(input);
result.Stack.Resolve(actions);

Assert.That(numberOfCall, Is.EqualTo(1));
Assert.That(tagSeen.Count, Is.EqualTo(1));
Assert.That(tagSeen[0], Is.EqualTo("ispum"));
}

[Test]
public void Run_TagWithFilterAndOneArgumentParameterTag_ActionOnFilterCalledOnce()
{
var numberOfCall = 0;
var filterSeen = new Dictionary<string, string[]>();
var actions = new ActionDictionary
{
[TokenType.Tag, TokenSubtype.Base] = a => a[0],
[TokenType.Argument, TokenSubtype.ParameterTag] = a => "KAPOUE",
[TokenType.Filter, TokenSubtype.Base] = a =>
{
numberOfCall++;
filterSeen.Add(a[0], a.Skip(2).ToArray());
return a[0];
}
};

var engine = new StrinkenEngine();
const string input = "lorem{ispum:belli+=!toto}";
var result = engine.Run(input);
result.Stack.Resolve(actions);

Assert.That(numberOfCall, Is.EqualTo(1));
Assert.That(filterSeen, Has.Count.EqualTo(1));
Assert.That(filterSeen, Contains.Key("belli"));
Assert.That(filterSeen["belli"], Has.Length.EqualTo(1));
Assert.That(filterSeen["belli"][0], Is.EqualTo("KAPOUE"));
Assert.That(filterSeen["belli"][0], Is.Not.EqualTo("toto"));
}
}
}
1 change: 1 addition & 0 deletions test/Strinken.Tests/EngineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ public void Run_InvalidCharacterInString_ThrowsFormatException(string input, cha
[TestCase("lorem{ispSIK_}")]
[TestCase("lorem{JuF-_}")]
[TestCase("lorem{JuF-m}09à9")]
[TestCase("lorem{!JuF-m}09à9")]
public void Run_ValidCharacterInString_DoesNotThrow(string input)
{
var result = this.engine.Run(input);
Expand Down
24 changes: 24 additions & 0 deletions test/Strinken.Tests/Parser/ConstructorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ public void Constructor_TwoFilterWithSameName_ThrowsArgumentException()
Throws.ArgumentException.With.Message.EqualTo("Append was already registered in the filter list."));
}

[Test]
public void Constructor_TwoParameterTagsWithSameName_ThrowsArgumentException()
{
Assert.That(
() => new Parser<Data>().WithParameterTags(new IParameterTag[] { new DateTimeParameterTag(), new DateTimeParameterTag() }),
Throws.ArgumentException.With.Message.EqualTo("DateTime was already registered in the parameter tag list."));
}

[Test]
public void Constructor_TagWithEmptyName_ThrowsArgumentException()
{
Expand All @@ -42,6 +50,14 @@ public void Constructor_FilterWithEmptyName_ThrowsArgumentException()
Throws.ArgumentException.With.Message.EqualTo("A name cannot be empty."));
}

[Test]
public void Constructor_ParameterTagWithEmptyName_ThrowsArgumentException()
{
Assert.That(
() => new Parser<string>().WithParameterTag(new EmptyNameParameterTag()),
Throws.ArgumentException.With.Message.EqualTo("A name cannot be empty."));
}

[Test]
public void Constructor_TagWithInvalidName_ThrowsArgumentException()
{
Expand All @@ -60,5 +76,13 @@ public void Constructor_FilterWithInvalidName_ThrowsArgumentException()
() => new Parser<string>().WithTag("tag", "tag", s => s).WithFilter(new InvalidNameFilter()),
Throws.ArgumentException.With.Message.EqualTo("! is an invalid character for a name."));
}

[Test]
public void Constructor_ParameterTagWithInvalidName_ThrowsArgumentException()
{
Assert.That(
() => new Parser<string>().WithParameterTag(new InvalidNameParameterTag()),
Throws.ArgumentException.With.Message.EqualTo("$ is an invalid character for a name."));
}
}
}
20 changes: 20 additions & 0 deletions test/Strinken.Tests/Parser/ExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,25 @@ public void WithFilters_Called_DoesNotModifyCallingInstance()
Assert.That(solver.Validate("The {Tag:Append+One:Custom} is in the kitchen.").IsValid, Is.False);
Assert.That(solver2.Validate("The {Tag:Append+One:Custom} is in the kitchen.").IsValid, Is.True);
}

[Test]
public void WithParameterTag_Called_DoesNotModifyCallingInstance()
{
var solver = new Parser<Data>().WithParameterTag(new DateTimeParameterTag());
var solver2 = solver.WithParameterTag(new MachineNameParameterTag());

Assert.That(solver.Validate("The {!DateTime} {!MachineName} is in the kitchen.").IsValid, Is.False);
Assert.That(solver2.Validate("The {!DateTime} {!MachineName} is in the kitchen.").IsValid, Is.True);
}

[Test]
public void WithParameterTags_Called_DoesNotModifyCallingInstance()
{
var solver = new Parser<Data>().WithParameterTag(new DateTimeParameterTag());
var solver2 = solver.WithParameterTags(new[] { new MachineNameParameterTag() });

Assert.That(solver.Validate("The {!DateTime} {!MachineName} is in the kitchen.").IsValid, Is.False);
Assert.That(solver2.Validate("The {!DateTime} {!MachineName} is in the kitchen.").IsValid, Is.True);
}
}
}
Loading