Skip to main content
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Katie Maher
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Katie Maher

Code Kata: Creating A Fluent, Closure-Based "Builder" API In Lucee CFML 5.3.6.61

By on
Tags:

As of late, I've been feeling very creatively blocked. Right now, work is taking every ounce of mental energy that I have, which is leaving me with little left over with which to create magic. As such, I just wanted to do something - anything - to create a little neural activity to keep the old brain-meat lubricated. I thought it might be fun to experiment with a fluent, closure-based "builder" API in Lucee CFML 5.3.6.61.

Whether or not you know it, you are almost certainly familiar with what a "fluent" API is. If you've used jQuery, you've used a fluent API. A fluent API is one that depends heavily on method-chaining. For example, in jQuery, you might see something like:

jQuery( "div.items" ).first().addClass( "selected" )

The key to a fluent API is that many of the methods return a reference back to an API rather than a null or void value.

For this code kata, I'm going to create a "builder" API, which is a "fluent" API in which a series of methods work together to create some sort of aggregate value. And, in this case, that aggregate value is going to be a URL. So, the builder API will expose methods for defining parts of that URL independently, followed by a .build() method which will collapse the independent parts down into a final value: the URL.

Here's what I came up with - it's not perfect, it's just a little fun to get my brain working:

<cfscript>

	echo(
		urlBuilder()
			.withProtocol( "//" )
			.withHost( "www.bennadel.com/" )
			.withPath( "/people" )
			.withParam( "bff" )
			.withParam( "filter", "cool beans" )
		.build()
	);

	echo( "<br />" );

	echo(
		urlBuilder()
			.withPath( "people" )
			.withParam( "bff" )
		.build()
	);

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I return a builder that can construct a URL from its various parts. Calling
	* .build() will flatten all the components down into a string.
	*/
	public struct function urlBuilder() {

		var protocol = "";
		var host = "";
		var path = "";
		var searchParams = [];

		// As we define MOST of our API methods, we want them to implicitly return a
		// reference back to the API itself so that the interface can be fluent (ie, rely
		// on method-chaining). However, so as not to have to do this in every SETTER,
		// this utility method will proxy any callback that is passed to it.
		var makeFluent = ( required function callback ) => {

			var fluentProxy = () => {

				callback( argumentCollection = arguments );
				return( builderApi );

			};

			return( fluentProxy );

		}

		// Define the public API of our fluent builder.
		var builderApi = {
			withProtocol: makeFluent(( required string newProtocol ) => {

				protocol = newProtocol;

			}),
			withHost: makeFluent(( required string newHost ) => {

				// Strip-off any trailing slash - it will be deferred to the path.
				host = newHost.reReplace( "[\\/]+$", "", "one" );

			}),
			withPath: makeFluent(( required string newPath ) => {

				// Ensure leading slash.
				path = ( newPath.left( 1 ) == "/" )
					? newPath
					: ( "/" & newPath )
				;

			}),
			withParam: makeFluent(( required string key, string value ) => {

				// NOTE: A NULL value will be encoded as a key-only parameter.
				searchParams.append([ key, ( value ?: nullValue() ) ]);

			}),
			// The BUILD method will flatten all the URL components down into a string.
			build: () => {

				var parts = [];

				if ( protocol.len() ) {

					parts.append( protocol );

				}

				if ( host.len() ) {

					parts.append( host );

				}

				if ( path.len() ) {

					parts.append( path );

				}

				// Flatten the search parameters down into a string.
				var searchString = searchParams
					.map(
						( tuple ) => {

							if ( tuple.isDefined( 2 ) ) {

								return( encodeForUrl( tuple[ 1 ] ) & "=" & encodeForUrl( tuple[ 2 ] ) );

							} else {

								return( encodeForUrl( tuple[ 1 ] ) );

							}

						}
					)
					.toList( "&" )
				;

				if ( searchString.len() ) {

					parts.append( "?" );
					parts.append( searchString );

				}

				return( parts.toList( "" ) );

			}
		};

		return( builderApi );

	}

</cfscript>

As you can see, the urlBuilder() function returns a Struct that is our API. Now, each method in the API could have returned the builderApi reference directly; but, again, my goal here was to have some fun and experiment. So, instead of having the explicit return, I'm proxying each API method through a makeFluent() function, which invokes the API method and then returns the API reference implicitly.

Now, if we run this ColdFusion code, we get the following output:

//www.bennadel.com/people?bff&filter=cool+beans

/people?bff

As you can see, the .build() method collapsed all of the independent URL components down into a single URL string.

There's not much more to say - this was just a fun experiment. It wasn't meant to be super robust. I'm not even saying that I would recommend doing this kind of thing with Closures (as opposed to an instantiated ColdFusion Component). It's just good to play with aspects of the Lucee CFML language.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel