Provide more details when keys not found.

A request was made to provide the original key as well as the member
that was not found. In compound keys {{Customer.Address.ZipCode}}, any
of the names could be invalid, but knowing the fully-qualified name is
useful for error handling.

I also detected a bug where the code wasn't handling the case that a
sub-key didn't exist. This needs to be handled using the KeyNotFound
event.
This commit is contained in:
Travis Parks 2013-04-19 08:56:21 -04:00
parent ac09c8fc38
commit a6c7933bab
5 changed files with 66 additions and 25 deletions

View File

@ -151,6 +151,31 @@ namespace mustache.test
Assert.AreEqual(expected, actual, "The wrong message was generated."); Assert.AreEqual(expected, actual, "The wrong message was generated.");
} }
/// <summary>
/// If part of a key is wrong, the full details should be provided.
/// </summary>
[TestMethod]
public void TestCompile_MultipartKey_PartMissing_ProvidesFullDetail()
{
FormatCompiler compiler = new FormatCompiler();
const string format = @"{{Customer.Name}}";
Generator generator = compiler.Compile(format);
generator.KeyNotFound += (obj, args) =>
{
args.Substitute = args.Key + "," + args.MissingMember;
args.Handled = true;
};
string actual = generator.Render(new
{
Customer = new
{
FirstName = "Bob"
}
});
string expected = "Customer.Name,Name";
Assert.AreEqual(expected, actual, "The wrong message was generated.");
}
/// <summary> /// <summary>
/// If we specify an alignment with a key, the alignment should /// If we specify an alignment with a key, the alignment should
/// be used when rending the value. /// be used when rending the value.

View File

@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
// //
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
[assembly: AssemblyVersion("0.0.4.0")] [assembly: AssemblyVersion("0.0.5.0")]
[assembly: AssemblyFileVersion("0.0.4.0")] [assembly: AssemblyFileVersion("0.0.5.0")]

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using mustache.Properties; using mustache.Properties;
namespace mustache namespace mustache
@ -63,39 +64,47 @@ namespace mustache
object nextLevel = _source; object nextLevel = _source;
if (member != "this") if (member != "this")
{ {
nextLevel = find(member); nextLevel = find(name, member);
} }
for (int index = 1; index < names.Length; ++index) for (int index = 1; index < names.Length; ++index)
{ {
IDictionary<string, object> context = toLookup(nextLevel); IDictionary<string, object> context = toLookup(nextLevel);
member = names[index]; member = names[index];
nextLevel = context[member]; if (!context.TryGetValue(member, out nextLevel))
{
nextLevel = handleKeyNotFound(name, member);
}
} }
return nextLevel; return nextLevel;
} }
private object find(string name) private object find(string fullName, string memberName)
{ {
IDictionary<string, object> lookup = toLookup(_source); IDictionary<string, object> lookup = toLookup(_source);
if (lookup.ContainsKey(name)) if (lookup.ContainsKey(memberName))
{ {
return lookup[name]; return lookup[memberName];
} }
if (_parent == null) if (_parent == null)
{ {
MissingKeyEventArgs args = new MissingKeyEventArgs(name); return handleKeyNotFound(fullName, memberName);
if (KeyNotFound != null)
{
KeyNotFound(this, args);
}
if (args.Handled)
{
return args.Substitute;
}
string message = String.Format(CultureInfo.CurrentCulture, Resources.KeyNotFound, name);
throw new KeyNotFoundException(message);
} }
return _parent.find(name); return _parent.find(fullName, memberName);
}
private object handleKeyNotFound(string fullName, string memberName)
{
MissingKeyEventArgs args = new MissingKeyEventArgs(fullName, memberName);
if (KeyNotFound != null)
{
KeyNotFound(this, args);
}
if (args.Handled)
{
return args.Substitute;
}
string message = String.Format(CultureInfo.CurrentCulture, Resources.KeyNotFound, memberName);
throw new KeyNotFoundException(message);
} }
private static IDictionary<string, object> toLookup(object value) private static IDictionary<string, object> toLookup(object value)

View File

@ -10,16 +10,23 @@ namespace mustache
/// <summary> /// <summary>
/// Initializes a new instance of a MissingKeyEventArgs. /// Initializes a new instance of a MissingKeyEventArgs.
/// </summary> /// </summary>
/// <param name="missingKey">The key that had no match.</param> /// <param name="key">The fully-qualified key.</param>
internal MissingKeyEventArgs(string missingKey) /// <param name="missingMember">The part of the key that could not be found.</param>
internal MissingKeyEventArgs(string key, string missingMember)
{ {
MissingKey = missingKey; Key = key;
MissingMember = missingMember;
} }
/// <summary> /// <summary>
/// Gets the key that could not be found. /// Gets the fully-qualified key.
/// </summary> /// </summary>
public string MissingKey { get; private set; } public string Key { get; private set; }
/// <summary>
/// Gets the part of the key that could not be found.
/// </summary>
public string MissingMember { get; private set; }
/// <summary> /// <summary>
/// Gets or sets whether to use the substitute. /// Gets or sets whether to use the substitute.

View File

@ -34,6 +34,6 @@ using System.Runtime.CompilerServices;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.0.4.0")] [assembly: AssemblyVersion("0.0.5.0")]
[assembly: AssemblyFileVersion("0.0.4.0")] [assembly: AssemblyFileVersion("0.0.4.0")]
[assembly: InternalsVisibleTo("mustache-sharp.test")] [assembly: InternalsVisibleTo("mustache-sharp.test")]