Created
September 15, 2020 11:26
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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