Value Resolution
When Fluid encounters an identifier, it needs to find the value for it. It does two things to accomplish this:
- By searching the scopes for the value
- By successively calling
GetValue
on each additional “member,” if more than one exists
If the identifier is “simple”, like this –
My name is {{ name }}.
– then Fluid just does #1 above. It retrieves a FluidValue
from a Scope or the Model (see Passing Data into Templates). It probably gets a StringValue
in this case, then calls WriteTo
on it.
However, if the token contains dots – and therefore multiple members – Fluid has to “resolve” the true value. It has to successively modify the base FluidValue
by calling GetValue
and passing in each successive member string (Note: the “.” separator is hard-coded in FluidParser
.)
Consider:
My name has {{ person.Name.size }} letters.
In this case, we clearly don’t want a value under the string key person.Name.size
. Rather, we want person
, progressively “modified” by Name
and size
.
What we’re effectively saying is:
- Get the value of
person
… - On that value, get the value of whatever
Name
means… - On that value, get the value of whatever
size
means… - Tell that value to write itself out.
The entire token person.Name.size
is parsed by Fluid into an MemberExpression
which is made up of three IdentiferSegment
objects: person
, Name
, and size
.
Fluid iterates over each member in an attempt to resolve the final value. Each member will result in a FluidValue
until WriteTo
is called on the final value.
I’ll number the descriptions below to match the four-step logical flow from above:
#1
If the value is null
(so, on the first iteration – person
in this case), Fluid will search the Scopes for a FluidValue
that maps to the person
identifier.
Let’s assume it finds one – an ObjectValue
which wraps a Person
object.
(If there were no dots in the token – so no further members – we’d be done here, and Fluid would just call WriteTo
on that ObjectValue
instance.)
#2
On the second iteration (Name
in this case), the initial value is already set, so Fluid instead calls GetValue
on the FluidValue
it has (which, remember, everything coming out of a Scope has to extend from FluidValue
) and passes the member as a string: “Name”.
What GetValue
does internally is up to the underlying class. It has to return another FluidValue
, but can return any subclass of that using whatever logic it likes.
(Also remember that GetValue
isn’t WriteTo
. At this point, Fluid isn’t writing anything out, it’s just resolving a value. Once it has the final value, it will then call WriteTo
on whatever it ends up with.)
So, on our second iteration, Fluid calls GetValue
on the ObjectValue
wrapper around a Person
object, passing it “Name”. In the case of an ObjectValue
, that reflects the underlying Person
object and returns the value of the Name
property, which is a StringValue
.
(See Member Access and Controlling Template Data for how to safeguard this.)
Note that person
was an ObjectValue
, but its GetValue
method returned a StringValue
. This is fine, because they’re both FluidValue
objects. Each member iteration can return different subclasses of FluidValue
(you might even end up back at the same type you started with…).
#3
On the third iteration (size
), Fluid now calls GetValue
on StringValue
. That method looks at the member string that’s passed in – if it’s the literal string “size” it returns a NumberValue
which represents the length of the wrapped string. In all other cases, it returns a NilValue
.
(Why return a NilValue
for anything other than “size”? Well, what should it return? The only valid member that Fluid supports on StringValue
is size
.
If we did this: person.Name.foo
so that GetValue
took in a member string of “foo”…what would that even mean? In no case other than “size” does it make sense to add a dot-something after a StringValue
. In any other case, the StringValue
should be the final segment.)
We’ve now reached the end of the resolution process, and we have a NumberValue
.
If any of the above iterations returned a NilValue
, Fluid would abandon any further iterations and just use the NilValue
. A NilValue
writes nothing out, so this is why looking for something invalid like “foo” on a StringValue
doesn’t throw an error, it just appears to do…nothing. A NilValue
is actually doing something, but that something is just to write nothing out.
#4
At the end of this resolution process, we have a NumberValue
. Fluid calls WriteTo
on that, which writes out a numeric value.