Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

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

By Ben Nadel on
Tags: ColdFusion

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.



Reader Comments

What has two thumbs and hopes you leave a comment? This Guy! (Ben Nadel).

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.