This project is read-only.

Overview

FluTe is an advanced alternative to the string.Format method, written using F# and C#. It allows you to define Template objects using a fluent interface.

Features

  • Define string.format-like templates using named tokens.
  • A fluent API that makes template configuration clear and intuitive.
  • Access properties and parameterless methods of input values using a simple syntax.
  • Use delegates to process inputs. Tell your template to join a list, make a string uppercase, or calculate a difference -- decleratively.
  • No awkward string-based syntax that needs to be learned, and no tables you need to search through to find the type of formatting you need. All processing syntax is 100% language integrated.
  • All objects are totally immutable. No need to worry about thread-safety.
  • High-performance parser written using the FParsec parser-combinator library that performs a single pass on the input. Performs exponentially better than regex-based solutions.

Dependencies (Included)

The Simple Stuff

Basically, you use FluTe like this, the same way you would string.Format, except using named tokens and a special syntax for injection
string res = FluTe.Define("My name is {fname} {lname}")
   .Bind(fname => "Greg")
   .Bind(lname => "Ros");
//this actually uses an implicit conversion. The result of the Bind method is not a string.
In this example, the names of the tokens are fname and lname. We bind each of these tokens to the values "Greg" and "Ros". The result is a template object containing the appropriate bindings. This template object is then implicitly converted into a string by being resolved (i.e. all specially marked tokens are replaced with their bound values(.
Note that we match a token to its respective value using the lambdas; they are part of the special, fluent syntax. The lambdas are resolved immediately, and the single parameter only serves to mark the token we want to set. The value it passes is arbitrary.
If you don't like this lambdas syntax, don't worry, FluTe has several additional syntax options to offer. See the documentation.
Tip: FluTe ignores all whitespace within tokens.

A More Complicated Example

Imagine this scenario. You want to format an error message saying the user has exceeded the maximum number of characters in your textbox. Using string.Format, you might write something like,
string.Format
   ("You've put {0} characters in the textbox {1}, more than the maximum of {2}.", numChars, textboxName, maxChars);
However, using FluTe you could actually encapsulate the template string, the input names, and the action of binding values to to those names in a single object. When defining this object, you'll write something like this:
FluTeInstance ErrorTemplate = "You've put {txtbox.Text.Length} characters in the textbox {txtbox.Name}, but it only supports up to {txtbox.MaxLength} characters."; 
//As noted above, an implicit conversion occurs here as well.
This is the definition of the template string. Unlike in the basic example above, in this case we use the special reference syntax -- {txtbox.Name} -- to get some property within the value, rather than the value itself. The property is resolved dynamically, via late binding (using the Impromptu framework, actually). You can also reference parameterless methods using this syntax, e.g. by writing {txtbox.Method()}. You cannot reference other members using this syntax.
string errorMessage = ErrorTemplate
   .Bind(txtbox => inputParam); 
Just like in the simple example above, here we also bind a value. The main difference here, however, is that we have many tokens -- {txtbox.Name}, {txtbox.MaxLength}, etc. -- but they all reference the same value. The value referenced by various tokens is called the input. Inputs have labels (identifiers or names). Here, the various tokens all reference a single input, named txtbox and process it differently. That's why we only need to bind a single input to a value.
Tip: FluTe is case-sensitive.

Static Inputs

Let's say that we want the error message shown above to echo the session information back to the user. Seeing as we want to reduce dependencies, we wouldn't want the same code that analyzes the contents of the textbox to reference the session object and inject it into the string. The text validation code should probably have as little contact with the session as possible.

To facilitate this, we can use the FluTe feature called static inputs. In the previous sections, we've explored inputs we bind on the spot. These inputs weren't part of the definition of the template -- they were actually part of its realization. However, we can also define inputs that are part of the template. These inputs can even reference all sorts of information outside of the immediate scope.
FluTeInstance ErrorTemplate = "The input called {input.Name} of type {input.Type} only supports {input.MaxLength} characters, but you've gone and filled it with {input.Text.Length} characters. You should be ashamed, {$ session.LoginName}";
Inputs marked with the $ character are the aforementioned static inputs. They are somewhat special, in that instead of defining them using a concrete value, we actually define them using a parameterless method (e.g. a lambda). This method gets resolved once, for each token that references the input (meaning, it gets resolved multiple times if multiple tokens reference it).
Using static inputs, we can do something like this:
//define the template in some scope where the session object is accessible...
FluTeInstance ErrorTemplate = "The input called {input.Name} of type {input.Type} only supports {input.MaxLength} characters, but you've gone and filled it with {input.Text.Length} characters. You should be ashamed, {$ session.LoginName}".BindStatic(session => CurrentSessionObject); //CurrentSessionObject is accessible within this scope.
Then, the text validation code can access the template object from somewhere, without even knowing that it references the session object from wherever. When the template is resolved, it will get the value of CurrentSessionObject in the scope where it was referenced (meaning, even if the value changed by that time). You could say that static inputs are evaluated lazily, while instance inputs are evaluated immediately.

By the way, if you don't care about getting the most recent value, you can do something similar to the above, except omit the $ marker and use the standard Bind method. FluTe actually supports partial resolution of templates, so you can just bind the input 'session' to a value, and send the template to another object. In this case, the input gets resolved immediately, and only once. If you later change the value of the CurrentSessionObject property, the template will remain bound to the old value.

Oh, it is not possible to use the BindStatic method after you use the Bind method. This is because the Bind method returns an object that doesn't have the BindStatic method. More on typing later.
Tip: Note that the $ character modifies the input label, which is session, rather than the token name , which is session.LoginName.

Processing and Serializing Inputs

FluTe also allows you to process tokens before they are injected into the result string. Let me demonstrate this using a scenario.
We want to display a list of products to the user. This list is contained within the property Products. Normally, we would have to do something like this:
FluTeInstance products = "Your products are: {products}.";
//...
string str = products.Bind(products => string.Join(", ", Products)); //the Bind() method is immediately resolved.
We can clearly see that the logic joining the products into a string is totally separate from the template. However, from an organizational standpoint, the logic is actually inherently related to it. FluTe allows us to embed this logic into the template using processing steps. These are, essentially, objects that specify delegates which are to be performed sequentially upon the input referenced by a token. Here is an example of how they are used?
FluTeInstance listOfProducts = FluTe
   .Create("This is what you bought: {products}.")
   .For(products => string.Join(", ", products)).Next; //this is where we add the processing step. Note the implicit lambda syntax.
The For method attaches the delegate products => string.Join(",",products to the token called {products}. This means that, before the token {products} (a collection of some sort) is injected into the template, the constituent elemnts of the input it references will first be joined. Having defined the template, we can do this:
listOfProducts.Bind(products => new[] {"Eggs", "Ham", "Butter"});
And the template listOfProducts will automatically join these items when the template is resolved.
FluTe also allows you to concatenate processing steps using a fluent syntax. It incldues special, pre-written processing steps you can parameterize in order to shorten the definition of the template
FluTeInstance listOfProducts = FluTe.Create("This is what you bought: {products}.").For("products").AsSeq<string>().Select(x => x.ToUpper()).WordJoin(", ", ", and ").Next;
This template will now take the token {products}, transform its constituents to upper case, and join them as though they were words. Using the same Bind invocation as above, we'll now get:
This is what you bought: EGGS, HARM, and BUTTER.
Here, note that the method For returns a special configurator object that retains the token it configures in memory. Thus, the invocations of the methods AsSeq, Select, etc, are applied only to the token denoted in the For method. The Next property ends the configuration, and returns the finalized template, with all processing steps added.

If your template has several different tokens, you can used the For method to configure them seperately, by name.
FluTeInstance listOfProducts = FluTe.Create("This is what you bought: {products}. You've made a {choiceKind}.")
	.For("products").AsSeq<string>().Select(x => x.ToUpper()).WordJoin(", ", ", and ")
	.For(choiceKind => choiceKind.ToUpper()).Next; //note that calling the method For in the context of another For configurator involves an implicit call to Next.
Thereafter, you can use the Bind method as normal:
string str = listOfProducts.Bind(products => new[] {"Eggs", "Ham", "Butter"}).Bind(choiceKind => "very good choice");
You can also set post-processing, which is invoked after all tokens have been injected into the template, and applies to the whole string. You do this using the Finally method, which you invoke during the definition of the template, before calling a Bind method.
FluTeInstance listOfProducts = FluTe.Create("This is what you bought: {products}. You've made a {choiceKind}.")
	.For("products").AsSeq<string>().WordJoin(", ", ", and ").Next
	.Finally(x => x.ToUpper()).Next;
This will yield the same string as above, when resolved, except it will be in uppercase.

Oh, by the way, let's say you have the following:
var template = FluTe.Create("The person's name is {person.name}.");
And you want to process the token {person.name}. Well, FluTe converts its identifier from the illegal person.name to the legal person_name. You can then do this:
var template = FluTe.Create("The person's name is {person.name}.").For(person_name => person_name.ToUpper()).Next();

Different Kinds of Tokens

FluTe actually allows you to define different kinds of tokens. Up until now, you've seen tokens that have a single input, and have an identical name as that input. However, what if you want to process the same information, in different ways? Well, FluTe allows you to define tokens differently for such purposes.
var template = FluTe.Create("You bought the products {pNames: products}, which cost {pSum: products}.")
	.For("pNames").AsSeq<Product>().Select(x = x.Name).WordJoin(", ", ", and ")
	.For("pSum").AsSeq<Product>().Select(x => x.Price).Do(x => x.Sum()).Next;
Here, the two tokens accept only one input between thyem -- products -- but process it differently.

You can also have tokens that accept more than one input:
var template = FluTe.Create("I can solve, {left} + {right} = {sum: left, right}")
	.For2("sum").Do((left, right) => left + right).Next;
You can then mix and match using all the different varieties:
var template = FluTe.Create("{bigassToken: one, $static.something, other.prop, my.method()}");

An Aside on Typing

The three primary classes of FluTe are the following:
  • FluTe, a static class containing factory methods.
  • FluTePrototype, a class that is created by the method FluTe.Create, and accepts an implicit conversion from string. Can be modified by adding processing steps, static inputs, etc. You cannot inject instance inputs into it, since it is not a finalized template.
  • FluTeInstance, a class created by an implicit conversion from FluTePrototype, or via the instance method FluTePrototype.Construct(). Cannot be modified by adding processing steps. Used for injecting input values and resolving to a string via implicit conversion, or .Resolve() method. You can only resolve the FluTeInstance if all the inputs have been filled.
I hope that clears up some of your confusion...

Future Feature: Template Serialization.

Template Serialization is a major feature that hasn't been developed yet. Basically, it allows you to serialize many templates into XML and other formats, and do whatever you want with them. This will be made possible using an expression serialization library, though right now I'm not sure which one. I really haven't researched this possibility enough, so the only thing I can gurantee is that it will allow you to serialize some templates and deserialize them while the FluTe assembly is referenced. It's also likely that templates will be naturally restricted in serialization by the data types they reference.

Immutability

Care has been taken to gurantee thread safety by simply ensuring the immutability of all, or nearly all classes. The only mutable objects the classes FluTeLiquid and FluTeSolid employ are private, readonly, and never editted after the object is constructed. Nevertheless, the library hasn't undergone any serious thread safety tests, so right now it should still be considered suspect.

Last edited Jun 24, 2012 at 8:31 PM by GregRos, version 8