ColdFusion Client For The Word-Finding Datamuse API
As a side-project to learn more about Docker and Redis, I've been working on an application that will help me write poetry. A big part of writing poetry is understanding rhyme schemes, syllable counts, and words that have similar meanings. My application hopes to bring all of these tools together in one nice, neat user interface (UI). And, it will be doing so with the help of the Datamuse API. The Datamuse API is a super flexible word-finding API that allows you to search for words based on sounds, meanings, rhymes, synonyms, consonants, lengths, prefixes, suffixes, and a host of other constraints. In order to leverage this API in my application, I've created a thin ColdFusion wrapper about the Datamuse HTTP schema.
View my ColdFusion Datamuse project on GitHub.
At first, I wanted to build a ColdFusion library that added some semantic abstractions over the Datamuse API. But, when I started to dig into the API, I realized that it was so flexible that any attempt at add a semantic abstraction would be a limitation rather than an facilitation. As such, I ended up writing a ColdFusion component that does little more than make the HTTP request a bit easier to execute.
At this time, my DatamuseClient.cfc provides two methods, each of which correspond to an HTTP end-point in the Datamuse API. The arguments for these methods mirror the arguments defined in the Datamuse API - again, I'm not trying to create a new abstraction, I'm just trying to make the API easier to consume. And, to be honest, I'm not even sure what all the arguments really mean.
getWords( [all arguments optional] ) - Returns words that match constraints.
- ml - Means like term.
- sl - Sounds like term.
- sp - Spelled like term.
- rel_jja
- rel_jjb
- rel_syn - Synonyms for term.
- rel_trg
- rel_ant - Antonyms for term.
- rel_spc
- rel_gen
- rel_com
- rel_par
- rel_bga
- rel_bgb
- rel_rhy - Perfect rhymes for term.
- rel_nry - Near rhymes for term.
- rel_hom
- rel_cns
- v
- topics
- lc
- rc
- max - Max number of results (defaults to 10).
- md - Metadata for term.
- qe - Query echo for term.
getSuggestions( s [,v [, max]] ) - Returns words that are suggestions for a prefix.
- s - The term being searched.
- v
- max - Max number of results (defaults to 10).
To see the DatamuseClient.cfc in action, I've put together a few examples:
<cfscript>
// Get words that start with the string "st" and are 5 letters long.
words = new lib.DatamuseClient().getWords(
sp = "st???"
);
// Returns:
// state, stick, stand, stock, style, stone, store, study, steel, story
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Get words that start with "s" and end with "y".
words = new lib.DatamuseClient().getWords(
sp = "s*y"
);
// Returns:
// strategy, say, study, serendipity, story, security, society, savvy, synergy, survey
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Get words that start with "s" and rhyme with "bad".
words = new lib.DatamuseClient().getWords(
rel_rhy = "bad",
sp = "s*"
);
// Returns:
// sad, scad, shad, strad, sociedad, sinbad, soledad, stalingrad, scantily clad, sketch pad
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Get words that mean something similar to "awesome".
words = new lib.DatamuseClient().getWords(
ml = "awesome"
);
// Returns:
// amazing, awful, impressive, awing, awe-inspiring, unbelievable, fantastic, incredible, wonderful, terrific
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Get words that are synonyms to "insane" and are 4-letters long.
words = new lib.DatamuseClient().getWords(
rel_syn = "insane",
sp = "????"
);
// Returns:
// wild, sick, daft, nuts, amok, loco, bats
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Get the number of syllables in "countenance".
// --
// NOTE: There is no "end point" for syllable count. So, in order to get it, we're
// going to use the Query Echo parameter, which will echo the value in the given
// parameter (sp) as the first item in the result. We can then use the metadata
// parameter (md) to tell Datamuse to return the (s) syllable count in the result.
words = new lib.DatamuseClient().getWords(
sp = "countenance",
qe = "sp",
md = "s",
max = 1
);
// Returns:
// countenance with syllable count metadata, 3.
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Get word suggesions for "bla".
words = new lib.DatamuseClient().getSuggestions(
s = "bla"
);
// Returns:
// black, blancard, blanford, blanket, blast, blazen, blatant, blade, blaze, blank
</cfscript>
In the above commenting, I've only listed the actual words that get returned; but, someone of the actions return additional metadata, like syllable count. Plus, you can include the "md" argument to return explicit metadata like syllable count, pronunciation, and definitions.
The Datamuse API is free for up to 100,000 requests a day. If you plan to use more than that, you can request an API Key for your application. I've requested an API Key, but have not yet heard back. As such, my DatamuseClient.cfc accepts an API Key value, but does not apply it to the underlying HTTP request. Once I hear back from Datamuse - and can see how to apply the API Key - I will up date the ColdFusion code. Unfortunately, this approach is not documented.
Here's the code for my initial release of the ColdFusion client:
component
output = false
hint = "I provide a low-level HTTP client for the Datamuse API."
{
/**
* I initialize the Datamuse API client with the given settings.
*
* @apiKey I am the API key used to authenticate the HTTP requests.
* @timeout I am the timeout (in seconds) used to manage the HTTP requests.
* @output false
*/
public any function init(
string apiKey = "",
numeric timeout = 5
) {
setApiKey( apiKey );
setTimeout( timeout );
}
// ---
// PUBLIC METHODS.
// ---
/**
* I make a request to the /sug end-point in Datamuse.
*
* For documentation on parameters, see: https://www.datamuse.com/api/
* @output false
*/
public array function getSuggestions(
required string s,
string v = "",
numeric max = 10
) {
return( makeRequest( "sug", filterOutEmptyValues( arguments ) ) );
}
/**
* I make a request to the /words end-point in Datamuse.
*
* For documentation on parameters, see: https://www.datamuse.com/api/
* @output false
*/
public array function getWords(
string ml = "",
string sl = "",
string sp = "",
string rel_jja = "",
string rel_jjb = "",
string rel_syn = "",
string rel_trg = "",
string rel_ant = "",
string rel_spc = "",
string rel_gen = "",
string rel_com = "",
string rel_par = "",
string rel_bga = "",
string rel_bgb = "",
string rel_rhy = "",
string rel_nry = "",
string rel_hom = "",
string rel_cns = "",
string v = "",
string topics = "",
string lc = "",
string rc = "",
numeric max = 10,
string md = "",
string qe = ""
) {
return( makeRequest( "words", filterOutEmptyValues( arguments ) ) );
}
/**
* I set the Datamuse API key to be sent with each request.
*
* CAUTION: At this time, the API key does not get applied (I am waiting on a response
* from Datamuse on how to actually implement the API key).
*
* @newApiKey I am the Datamuse API key.
* @output false
*/
public void function setApiKey( required string newApiKey ) {
apiKey = newApiKey;
}
/**
* I set the timeout (in seconds) for the HTTP request.
*
* @newTimeout I am the timeout (in seconds) to be applied to the HTTP request.
* @output false
*/
public void function setTimeout( required numeric newTimeout ) {
timeout = newTimeout;
}
// ---
// PRIVATE METHODS.
// ---
/**
* I return a collection that contains only the defined, non-empty, simple values
* contained within the given data structure.
*
* @data I am the collection being filtered.
* @output false
*/
private struct function filterOutEmptyValues( required struct data ) {
var filtered = {};
for ( var key in data ) {
// Skip if null.
if ( ! structKeyExists( data, key ) ) {
continue;
}
var value = data[ key ];
// Skip if complex or empty.
if ( ! isSimpleValue( value ) || ! len( value ) ) {
continue;
}
filtered[ key ] = value;
}
return( filtered );
}
/**
* I make the HTTP request to the Datamuse API.
*
* @resource I am the end-point being accessed.
* @urlParams I am the collection of key-value pairs to add to the search-string.
* @output false
*/
private array function makeRequest(
required string resource,
required struct urlParams
) {
var apiRequest = new Http(
method = "GET",
url = "https://api.datamuse.com/#resource#",
getAsBinary = "yes",
timeout = timeout
);
for ( var key in urlParams ) {
apiRequest.addParam(
type = "url",
name = lcase( key ),
value = urlParams[ key ]
);
}
var apiResponse = apiRequest.send().getPrefix();
// NOTE: Even though we are using "getAsBinary" in the HTTP request, the
// fileContent will only be binary if the request is successful. If, for example,
// the request fails to connect, the fileContent may contain a plain string with
// something like, "Connection Failure," in it.
var fileContent = isSimpleValue( apiResponse.fileContent )
? apiResponse.fileContent
: charsetEncode( apiResponse.fileContent, "utf-8" )
;
if ( ! reFind( "200", apiResponse.statusCode ) ) {
throw(
type = "Datamuse.HttpError",
message = "HTTP request returned with a non-200 status code.",
detail = "HTTP Status Code: #apiResponse.statusCode#"
extendedInfo = "HTTP File Content: #fileContent#"
);
}
try {
return( deserializeJson( fileContent ) );
} catch ( any error ) {
throw(
type = "Datamuse.ContentError",
message = "Content could not be deserialized.",
detail = "HTTP File Content: #fileContent#",
extendedInfo = "Root Error Message: #error.message#"
);
}
}
}
As you can see, it does little more than make the HTTP request a bit easier to construct and consume. All semantic abstractions probably make more sense in the calling context, not in the client itself.
The Datamuse API is flexible, powerful, and seems to be blazing fast. I'm looking forward to consuming it in my poetry application!
Want to use code from this post? Check out the license.
Reader Comments
How are you using their API? What problem does it solve for you?
@Chris,
I'll be using it to provide information in the context of a poetry app:
Need to get your awesome API on ForgeBox to make it easier for others to install, update, and use. Would you like me to send you a pull to add the box.json to your repo? Then all you'll need to do is:
forgebox login
bump --major
:)
This is really great. Just out of interest, in CFScript, is it necessary to prefix variables with the 'variables' scope. I rarely use CFSCRIPT, but I am about to start a new job and the client only uses CFSCRIPT.
And one other thing:
Is this a new way of connecting to a CFC?
I can see what you are doing, but I haven't seen this syntax before?
Charles, all the same variable scoping rules apply to cfscript the same as tags. Really no difference there.
The new keyword is the same as doing
createObject( 'component', 'lib.DatamuseClient' ).init()
Nice one. I like this way of referencing CFCs. Very cool...
So any tag that can have a child tag like:
Is assigned to an object, using the 'new' keyword?
No, the "new" keyword is nothing more than a shortcut for creating CFC instances. There is a generic way to call any tag in script, but that's a different thing entirely.
OK. I am with you. It's just that I noticed this pattern with TAGs that have a children tags, but I didn't realise they were CFCs themselves. Interesting.
@Brad,
Yeah, I really gotta get on ForgeBox. I've looked through it, and I've read most of the documentation on the Box CLI; I just haven't taken that next step. But, it is something I want to be better about doing. There's other stuff that I think could go in there as well.
@Charles, @Brad,
Well, you are somewhat right in that recent versions (since 9??) do have ColdFusion components for several of the built-in tags. Like
Mail.cfc
,Http.cfc
, andQuery.cfc
. But, as Brad was saying, there is now syntax that allows you to invoke any tag in CFScript.As far as the
variables
scope, it is the "implied" scope. So, most of the time, you can assume that these two statements are the same:variables.foo = "bar";
foo = "bar";
In the latter case,
variables.
is implied.That said, there are huge caveats to that statement. For example, if you do that inside a
CFThread
, it will save to the thread-local scope. And, if the variable is already assigned to a scope (such as withvar foo
, then it will state in that scope. And, I believe in Lucee, you can actually configure the behavior of an unscoped assignment inside a Function body.But, allllll those caveats aside, the
variables.
scope is the "page scope".Thanks for that. I actually understand all of the stuff about the 'variables' scope, I just wondered whether these same rules applied in CFSCRIPT. So, thank you for giving me a definitive 'yes'. My understanding of CFSCRIPT is very limited, but I have been reading Adam Cameron's 'github' repo on the subject and it looks fairly straightforward:
https://github.com/tonyjunkes/cfml-tags-to-cfscript/blob/master/README.md
And also Brad has given me some excellent reading material...
I also see that in CF11, it is possible to use the following pattern:
@Charles,
Yes, exactly! Good stuff.
And also your explanation of scope helped to validate my current understanding. The CFTHREAD stuff was enlightening. I usually just use 'attributes' & 'caller', but I have always wondered how to create a thread local scope. So thanks again.
@Charles,
CFThread
is a funny beast. Technically, under the hood, it's actually just a Function that gets called asynchronously. So, like a Function, it has anarguments
scope, which is not really documented. And, like a Function, it also as alocal
scope as well. Which is why you canvar
variables inside of aCFThread
body and they are local to the thread. And then, it also has thethread
scope which is bridge between the thread and the parent context (ie, if you want to pass values "back" from the thread to the parent context).The biggest hurdle with
CFThread
is that if you want to set a variables-scoped value from inside a thread, you have to explicitly provide thevariables.
prefix; otherwise, the value will get saved into the thread-local scope.Other than that, threads basically "just work" :D
Oh crap, that line:
... was supposed to be:
This is a superb explanation of CFTHREAD. Like a function. Perfect!
So, am I write in assuming:
@Charles,
Yes, 100%. In fact, there's no way (that I know of) to alter a Function-local variable from inside a CFThread body. So, the first
var bar = "foo";
you have in the Function body, cannot be touched by the thread-body.Wow. This gives me so many different options now. I feel like for the first time, I truly understand CFTHREAD scoping. But, I am like a dog with a bone. I now want to know more about the CFTHREAD ARGUMENTS scope. I am intrigued?
@Charles,
Ha ha, honestly, don't even think about the "arguments" scope for threads. It's not really even documented. It's just there because it's technically a function under the hood.
That said, if you want to have to some fun, run this:
In the output, you will be able to see that the name of the Function that represents the thread. For example, when I run this, the output of
cfthread.mythread.funcName
is_cffunccfthread_cftest2ecfm13918505741
.These look like attributes to me?
But, what you are saying makes perfect sense, because if CFTHREAD is a function, then it must have ARGUMENTS.
I thought CFTHREAD was like CFMODULE, and that you could pass attributes in.
So are you saying that the ARGUMENTS scope is synonymous for the ARGUMENTS scope. Or, am I just going mad, and there is no ATTRIBUTES scope?
@Charles,
You are not mad. If you run that code, you will see that the
attributes
scope, in the context ofCFThread
is actually an argument to the "hidden" function. So, when you reference:attributes.a
... you are implicitly referencing:
arguments.attributes.a
.It's best not to think about it too much :D
Brilliant. I love this kind of deep analysis. Although sometimes this frustrates people around me!