Note: Before reading this documentation, please read the overview in the project home.

Fluent Interface

FluTe stands for, more or less, Fluent Text Templating. Thus, it is unsurprising that a large part of its codebase is dedicated to creating a fluent interface. While I feel I have sufficiently expounded upon the template string syntax in the overview, I've given less attention to the fluent interface in use here.

Fluent Interface means, well, this:
	var template = FluTe.Create("{token}")
		.For("token").Do(x => x.ToUpper()).Next
		.Bind(token => "Greg").Resolve();
The API allows us to code as though we're writing fluent sequences of piped instructions, instead of isolated imperative statements. The fluent API is actually very separate from the library proper; it's even contained in another assembly, called FluTeLib.Fluent. However, FluTe doesn't really support working with the lower-level, functional API directly. It just encourages developing or changing the interface.

About Assemblies

FluTe consists of all sorts of objects which share the namespace FluTeLib. These objects are divided into three separate assemblies:
  • FluTeLib.Core, which contains most of the functionality provided by the library. This assembly references the free library ImpromptuInterface for performing high-performance dynamic invocations. It doesn't reference the other assemblies.
  • FluTeLib.Parser, an assembly written in F# which contains the FluTe template string parser. The code within this assembly is somewhat opaque. It references the free parser-combinator library FParsec, and the assembly FluTeLib.Core.
  • FluTeLib.Fluent, which hides the other assemblies and can access their internal members. Most user interaction with the library is made through this assembly. It provides a robust fluent interface; the namesake of the library. It references both the other libraries.

About Typing

FluTe is a strongly-typed library, but it uses lots of inference, and has many forms of shorthand and implicit conversion. This makes it resemble ducktyping somewhat, in some cases. However, you don't need to use e.g. implicit conversion if you don't want to. It's pretty easy to edit the codebase and remove these conversions if they really bother you a lot. Don't worry, the conversions are part of the FluTeLib.Fluent extension assembly, rather than the FluTeLib.Core assembly. They aren't connected to functionality, and aren't used within the FluTeLib.Fluent library either.

FluTe is built using a variant of the object prototype design pattern, and uses two primary objects:
  • FluTePrototype, which is defined using a template string, and can be modified using processing steps and similar to create the exact template you want to use. The class is completely immutable. You can do several things with this object.
    • Attach processing steps of all sorts to the template.
    • Bind static inputs.
    • Perform other activities which change the definition of the template.
  • FluTeInstance, which is a finalized template derived by invoking the Construct() method on the Prototype object. The object is used pretty much for a single purpose only.
    • Bind instance inputs.
    • Resolve into a string, using the Resolve() method.

Fluent API

Instead of providing a simple list of the different methods and their descriptions, we'll go over the fluent API in steps, from the first step of defining a FluTePrototype object using a string template, to resolving the FluTe template into a finalized product string.

Defining a FluTePrototype Object

You can define a FluTePrototype object in several different ways.
  • The static FluTe.Create method, e.g. FluTe.Create("Hi, my name is {name}").
  • The FluTe() extension method for string, e.g. "Hi, my name is {name}.FluTe().
  • Implicit conversion from string to FluTePrototype or FluTeInstance, e.g. FluTePrototype proto = "Hi, my name is {name}";.

Configuring Processing Steps

You configure processing steps for tokens using methods usually named For. These methods return a IForClause object, which remembers the token you are configuring, allowing you to append processing steps to it using sequential and fluent method invocations. Note that For methods appear in two general flavors: strongly typed, and dynamically typed. Strongly typed For<T> methods allow you to specify a parameter which the token value is assumed to be.
  • For(string), which accepts the name of the token, e.g. {{For("name")}.
  • For(Func<object, T>), an implicit syntax that infers the name of the token from the single parameter name of the supplied delegate (this it does via reflection), and also adds a Do processing step that performs the transformation specified by the delegate. I.e. For(name => name.ToUpper()) would start configuring the token with the name of 'name', and add to it the processing step Do(name => name.ToUpper()).
  • For2(string), starts configuring a token that accepts 2 inputs. This is shorthand for For<IMultiInput2>(string).
  • For3(string), as above, except for three inputs.
  • ForN{string), as above, except for any number of inputs. Shorthand for For<IMultiInput>(string).

A somewhat similar method, Finally, allows you to attach post-processing steps to the whole template string. The method comes in similar varieties: Finally(string), Finally(Func<object, T), etc.

In order to get the result of your configuration (the final template with all the processing steps you've specified), use the Next property. I.e.:
var template = FluTe.Create("Hi, my name is {name} and my age is {age}.")
	.For("name").Do(name => name.ToUpper()).Next
	.For(x => x + 5).Next;
Alternatively, if you want to configure a different token, you can eschew the Next call for the more concise, implicit syntax:
var template = FluTe.Create("Hi, my name is {name} and my age is {age}.")
	.For("name").Do(name => name.ToUpper())
	.For("age").Do(x => x + 5).Next;

Processing steps are objects that implement the IProcessingStep and possibly the IProcessingStep<TIn, TOut> interfaces. However, instead of adding them using the cumbersome and occassionally unsafe syntax of e.g. template.Attach(ProcessingStep.Do(x => x + 1)) FLuTe that is used internally, FluTe defines a series of various fluent extension methods similar to the For method above.
Somewhat similarly to the above, some methods are strongly typed using generics, while others use dynamic typing. Refer to the documentation of each extension method.

Converting FluTePrototype to FluTeInstance

As mentioned previously, FluTePrototype is a prototype for creating objects of type FluTeInstance. These latter objects can be bound to values using e.g. the Bind method. There are several ways of constructing a FluTeInstance object:
  • Explicitly, using the Construct() method of FluTePrototype. E.g. var instance = FluTe.Create("Hi {name}").Construct();
  • Using an implicit conversion, e.g. FluTeInstance instance = FluTe.Create("");
  • Using an implicit conversion from string, e.g. FluTeInstance instance = "My name is {name}";
  • Using the Bind method of FluTePrototype allows you to simulanteously construct an instance and bind it to a value: var instance = FluTe.Create("My name is {name}").Bind(name => "Greg");

Binding

  • You can use the Bind(string, object) method to explicitly bind a value to an input.
  • Bind(Func<object, object>) accepts a delegate, and uses the name of the parameter to infer which input you want to bind. The delegate is then immediately evaluated, with the argument supplied being arbitrary (usually null). Don't reference the argument in the delegate.
  • Bind(string).To(object). This fluent, two-step syntax is somewhat similar to the For method described above. The method Bind(string) returns a special IBindClause object, which exposes the method To(object), which in turn returns the resulting instance.
  • Bind(params Func<object, object>[] lets you use several delegates to quickly bind more than one input.
Note: When you bind a value to an input, you get a FluTeInstance with that input bound. The object you get is fully functionality, and you can pass it around as you see fit, with the exception that the bound input cannot be rebound. You could bind some inputs in one scope, and another inputs in another scope. Alternatively, since FluTeInstance is immutable, you can keep a partially bound FluTeInstance around and treat it as a template by itself.

Resolving

Once all inputs are bound, the template can be resolved into a string. There are several ways of doing this.
  • Explicitly, using the Resolve() method.
  • By an implicit conversion to a string.

Last edited Jun 20, 2012 at 4:00 PM by GregRos, version 2

Comments

No comments yet.