Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created September 15, 2020 11:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bennadel/5d0e7d438d95c2c8713a3cd1efbb5d2f to your computer and use it in GitHub Desktop.
Save bennadel/5d0e7d438d95c2c8713a3cd1efbb5d2f to your computer and use it in GitHub Desktop.
Code Kata: Creating A Fluent, Closure-Based "Builder" API In Lucee CFML 5.3.6.61
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment