Storyteller 5.1.0


Next

Selection Lists

Previous

Cells

Data Conversion within Specifications Edit on GitHub


Specifications are largely created by entering text into the editor and stored on disk as Xml (I know, but backward compatibility mattered). Storyteller 3 features a new conversion function to help convert the raw string data into the correct Types your fixture and grammar code needs to execute.

Even though the subsystem and this topic both use the "Conversion" nomenclature, the resolution from a raw string to the real value in actual usage has been:

  1. Doing a straight conversion from a string to a scalar value like a number
  2. Parsing text to create a more complex object to make specifications more terse
  3. Using the raw text to lookup data in the system under test using the system's persistence infrastructure
  4. Creating missing system data on the fly if it did not already exist

The conversion subsystem supports the most common Cell types out of the box, but also provides a couple of extension points that should accomodate any need.

What's in the Box?

Out of the box, the conversions can handle converting raw string input to .Net types for:

  • Strings -- with the small caveat that "NULL" and "EMPTY" have special meaning as a null value and an empty, zero length string
  • Every number type in .Net using the default int.Parse() method in .Net
  • Booleans -- uses a case insensitive match on "true" or "false"
  • Enumerations -- converts based on the string option name for the value
  • Nullable<T> -- assuming that Storyteller knows how to convert a string to whatever "T" is. "NULL" is converted to a null value.
  • Array of T -- again, assuming that Storyteller understands how to convert a string to the type "T", this conversion works by treating the raw text as a comma delimited string of values.
  • ctor(string) -- Any concrete type that has a public constructor function with a single parameter that takes in a string can be converted by calling that constructor function with the raw value. Or "NULL" to denote a null value.
  • DateTimes -- more on this below.

By ctor(string)

A very powerful mechanism in Storyteller's conversion subsystem is the ability to just call a public constructor of a Type that accepts a single string argument.

From the unit tests in the Storyteller code, we have a type called Color that exposes a public constructor matching this criteria:


public class Color
{
    public string Name { get; set; }

    public Color(string name)
    {
        this.Name = name;
    }
}

In action, the conversion code selects the "call ctor(string)" strategy and this test below passes:


[Fact]
public void string_ctor_conversion()
{
    new Conversions().Convert(typeof(Color), "Red")
        .ShouldBeOfType<Color>()
        .Name.ShouldBe("Red");
}

The inspiration for this feature was taken from a software system that tracked steel members. We frequently used a value type called Dimension that was frequently described in short hand like: 1 x 1 x 36 that expressed the physical cross section and length of a steel piece. That type looked something like this:


public class Dimensions
{
    public Dimensions()
    {
    }

    // This constructor would be called by Storyteller
    public Dimensions(string text)
    {
        var parts = text.ToDelimitedArray('x');
        Width = int.Parse(parts[0]);
        Thickness = int.Parse(parts[1]);
        Length = int.Parse(parts[2]);
    }

    public int Width { get; set; }
    public int Length { get; set; }
    public int Thickness { get; set; }
}

Using this conversion may allow you to express hierarchical data in a concise, more readable format than you would be able to achieve by strictly supplying raw values for every raw property of a type.

You can think of this pattern as a built in interpreter pattern -- crude as it may be.

Custom Conversions

Custom conversions come in two different flavors, with the simpler version being the IConversionProvider shown below:


public interface IConversionProvider
{
    // Given the type argument, either return a
    // Func that can parse a string into that Type
    // or return null to let another IConversionProvider
    // handle this type
    Func<string, object> ConverterFor(Type type);
}

Internally, Storyteller treats all the conversion providers as a chain of responsibility such that the first provider that "knows" how to handle a type provides the actual conversion strategy as a simple Func<string, object> func. User supplied providers are evaluated before the built in providers.

A sample provider is the EnumerationConversion taken from the Storyteller code itself that handles any .Net Enumeration type:


public class EnumerationConversion : IConversionProvider
{
    public Func<string, object> ConverterFor(Type type)
    {
        if (type.GetTypeInfo().IsEnum)
        {
            return x => Enum.Parse(type, x);
        }

        return null;
    }
}

Runtime Conversions

The more complex extension point is the IRuntimeConverter that is allowed to use anything exposed by the ISpecContext, including system services, to do the conversion/location/resolution from raw text to the requested type.

The signature is shown below:


public interface IRuntimeConverter
{
    object Convert(string raw, ISpecContext context);
    bool Matches(Type type);
}

A sample usage from the internal unit tests is shown below:


public class PlayerConverter : IRuntimeConverter
{
    public static readonly IList<Player> Players = new List<Player>
    {
        new Player {FirstName = "Justin", LastName = "Houston", Position = "LB"},
        new Player {FirstName = "Jeremy", LastName = "Maclin", Position = "WR"},
        new Player {FirstName = "Jamaal", LastName = "Charles", Position = "RB"}
    };

    public object Convert(string raw, ISpecContext context)
    {
        return Players.FirstOrDefault(x => x.FullName() == raw);
    }

    public bool Matches(Type type)
    {
        return typeof (Player) == type;
    }
}

public class Player
{
    public string FirstName;
    public string LastName;
    public string Position;

    public string FullName()
    {
        return "{0} {1}".ToFormat(FirstName, LastName);
    }
}

Registering Custom Converters

Part of the signature of a custom ISystem is to return a CellHandling object that holds everything Storyteller needs to know in order to, well, handle all the cells in the specification system -- including any custom conversions.

The Conversions property of CellHandling allows you to add custom IConversionProvider and IRuntimeConverter types and objects as in this sample below:


public CellHandling Start()
{
    var handling = CellHandling.Basic();

    // Adding a system wide list. 
    handling.AddSystemLevelList("positions", new[] {"LB", "OL", "DL", "WR", "RB"});

    // This is where you can register a custom runtime conversion
    handling.RegisterRuntimeConversion<PlayerConverter>();

    return handling;
}

DateTimes

Date and time logic is so common that Storyteller has some special handling for expressing dates. The following rules in order of precedence are available:

  1. "[Day of the Week] [24 hour time]." -- So that "Wednesday 14:30" translates as 2:30 PM on the first Wednesday in the future. If today's day matches the day of the week, it select today's date.
  2. "TODAY" -- equivalent to DateTime.Today()
  3. "TODAY+#" or "TODAY-#" -- equivalent to DateTime.Today().Add(#.Days()).
  4. ISO 8601 formatted dates
  5. The default .Net DateTime.Parse(text) method