Matthew Clemente
REpresentational State Transfer
REST is CRUD over HTTP. Some exceptions to this rule are allowed as long as they don’t become the rule.
Method + Host + Path + Query Params + Body + Headers
Status Code + Status Text + Headers + Data
api-wrapper-template
A CommandBox tool for scaffolding CFML API Clients
Quickly create an API wrapper project
Scaffold boilerplate HTTP request handling
Structured to mirror API documentation
Handle various response formats
Provide additional information about request/response
box install api-wrapper-template
Authentication / Authorization
Endpoint / Test Endpoint
Rate limits
Response Format
Other limitations (size of request, etc.)
apiWrapper scaffold --wizard
apiName* | Name of the API this library will wrap. [i.e. Stripe] |
apiEndpointUrl* | Base endpoint URL for API calls. [i.e. https://api.stripe.com/v1] |
apiAuthentication | Type of authentication used [None, Basic, Apikey, Other] |
apiDocUrl | URL of the API documentation homepage |
name | Name for the wrapper [i.e. StripeCFC] |
description | A short description of the wrapper. |
author | Name of the author of the wrapper. |
quickStart | Do you want to quickStart the project? |
apiwrapper scaffold apiName="Cat API" apiEndpointUrl="http://thecatapi.com/api" apiAuthentication="none" apiDocUrl="https://thecatapi.com/docs.html" name="CFCat" description="Some people are cat people. Some people are dog people. This wrapper will give the former random cats on demand." && cd catWrapper && server start openbrowser=false
apiName | Cat API |
apiEndpointUrl | http://thecatapi.com/api |
apiAuthentication | none |
apiDocUrl | https://thecatapi.com/docs.html |
name | CFCat |
description | Some people are cat people. Some people are dog people. This wrapper will give the former random cats on demand. |
/**
* http://thecatapi.com/docs.html#images-get
* @hint List cats
*/
public struct function listCats( string category = '' ) {
var params = { 'format' : 'html' };
if ( category.len() ) params[ 'category' ] = category;
return apiCall( 'GET', '/images/get', params );
}
createResource()
getResource()
listResources()
updateResource()
deleteResource()
(Make sure there's a method to your madness)
cat = new cat.cat();
catImage = cat.listCats().data;
writeOutput( catImage );
cat = new cat.cat();
catImage = cat.listCats();
writeDump( catImage );
RequestBin / Hookbin / Mockbin
Being able to send your API wrappers requests
to a different endpoint is helpful when debugging.
cat = new cat.cat( includeRaw = true );
catImage = cat.listCats();
writeDump( catImage );
Authentication typically involves providing credentials via Headers
No Authentication | This is easy! You don't need to do anything! Yay! |
Basic Authentication | Base64 encoded username:password in Authorization header |
API Key Authentication | Unique identifiers passed via headers ( or, less frequently, query params or in the body) |
Open Authorization (OAuth) | Access Token used, following authentication and permission |
How do we handle it in our CFML API clients?
No Authentication | This is easy! You don't need to do anything! Yay! |
Basic Authentication |
cfhttp
username and
password attributes
|
API Key Authentication |
cfhttpparam( type = "header" ... )
Handled via getBaseHttpHeaders() in the
api-wrapper-template
|
Open Authorization (OAuth) | https://github.com/coldfumonkeh/oauth2 |
Keeping secrets in environment variables
provides practical and security benefits
// Get access to the system class.
system = createObject( "java", "java.lang.System" );
value = system.getenv( 'key' );
// Calling getenv() without an argument returns a map / struct of all of the
// keys available in the current environment (to the ColdFusion user).
environment = system.getenv();
Handled automatically in
api-wrapper-template
var secrets = {
'applicationId': 'AYLIEN_APPLICATION_ID',
'applicationKey': 'AYLIEN_APPLICATION_KEY'
};
apiwrapper scaffold apiName="Aylien" apiEndpointUrl="https://api.aylien.com/api/v1" apiAuthentication="apikey" apiDocUrl="http://docs.aylien.com/textapi/#getting-started" name="ayliencfc" description="Access Aylien's Natural Language Processing (NLP) API to extract meaning and insight from textual content." && cd aylienWrapper && server start openbrowser=false
apiName | Aylien |
apiEndpointUrl | https://api.aylien.com/api/v1 |
apiAuthentication | apikey |
apiDocUrl | http://docs.aylien.com/textapi/#getting-started |
name | ayliencfc |
description | Access Aylien's Natural Language Processing (NLP) API to extract meaning and insight from textual content. |
public any function init(
string applicationId = '',
string applicationKey = '',
string baseUrl = "https://api.aylien.com/api/v1",
boolean includeRaw = false ) {
private struct function getBaseHttpHeaders() {
return {
'Accept' : 'application/json',
'Content-Type' : 'application/json',
'X-AYLIEN-TextAPI-Application-Key' : variables.applicationKey,
'X-AYLIEN-TextAPI-Application-ID' : variables.applicationId,
'User-Agent' : 'ayliencfc/#variables._ayliencfc_version# (ColdFusion)'
};
}
/**
* https://docs.aylien.com/textapi/endpoints/#entity-extraction
* @hint Extracts different types of notable entities from a document
*/
public struct function entities( string text = '', string url = '', string language ) {
var params = {
'language' : language ?: 'auto'
};
if ( text.len() )
params[ 'text' ] = text;
if ( url.len() )
params[ 'url' ] = url;
return apiCall( 'POST', '/entities', params );
}
aylien = new aylien.aylien();
entities = aylien.entities( url = 'http://bit.ly/intb2018' ).data;
writeDump( entities );
aylien = new aylien.aylien();
entities = aylien.entities( url = 'http://bit.ly/intb2018' );
writeDump( entities );
aylien = new aylien.aylien();
entities = aylien.entities( url = 'I am not a URL!' );
writeDump( entities );
entities = aylien.entities( 'http://bit.ly/intb2018' ).data;
apiwrapper scaffold apiName="Lob" apiEndpointUrl="https://api.lob.com/v1" apiAuthentication="basic" apiDocUrl="https://lob.com/docs#intro" name="lobcfc" description="Wrap the Lob API to verify addresses and send physical mail programmatically." && cd lobWrapper && server start openbrowser=false
apiName | Lob |
apiEndpointUrl | https://api.lob.com/v1 |
apiAuthentication | basic |
apiDocUrl | https://lob.com/docs#intro |
name | lobcfc |
description | Wrap the Lob API to verify addresses and send physical mail programmatically. |
public any function init(
string apiKey = '',
string testApiKey = '',
boolean forceTestMode = false,
string baseUrl = "https://api.lob.com/v1",
boolean includeRaw = false ) {
var secrets = {
'apiKey': 'LOB_LIVE_API_KEY',
'testApiKey': 'LOB_TEST_API_KEY'
};
cfhttp( url = fullPath, method = httpMethod, username = !variables.forceTestMode ? variables.apikey : variables.testApiKey, password = '', result = 'result' ) {
lob = new lob.lob( forceTestMode = true );
//etc etc
result = lob.createPostcard( postcard );
(The fast and easy approach)
(Helper components with fluent interfaces)
/helpers/address.cfc
/**
* lobcfc
* Copyright 2018 Matthew J. Clemente, John Berquist
* Licensed under MIT (https://mit-license.org)
*/
component accessors="true" {
property name="description" default="";
property name="name" default="";
property name="company" default="";
property name="address_line1" default="";
property name="address_line2" default="";
property name="address_city" default="";
property name="address_state" default="";
property name="address_zip" default="";
property name="address_country" default="";
property name="phone" default="";
property name="email" default="";
property name="metadata" default="";
/**
* https://lob.com/docs#addresses_object
* @hint No parameters can be passed to init this component. They must be built manually. When creating and updating addresses, the following fields are required:
* name or company (both can be provided)
* address_line1
* address_city (if in US)
* address_zip (if in US)
*/
public any function init() {
setMetadata( {} );
return this;
}
/**
* @hint An internal description that identifies this resource.
*/
public any function description( required string description ) {
setDescription( description );
return this;
}
/**
* @hint Either name or company is required
*/
public any function name( required string name ) {
setName( name );
return this;
}
/**
* @hint Either name or company is required
*/
public any function company( required string company ) {
setCompany( company );
return this;
}
/**
* @hint Required
*/
public any function addressLine1( required string address ) {
setAddress_line1( address );
return this;
}
/**
* @hint alias for setting address line 1
*/
public any function address( required string address ) {
return addressLine1( address );
}
public any function addressLine2( required string address ) {
setAddress_line2( address );
return this;
}
/**
* @hint alias for setting address line 2
*/
public any function address2( required string address ) {
return addressLine2( address );
}
/**
* @hint Required if address is in US
*/
public any function city( required string city ) {
setAddress_city( city );
return this;
}
/**
* @hint alias for setting city
*/
public any function addressCity( required string city ) {
return city( city );
}
/**
* @hint Required if address is in US. Can accept either a 2 letter state short-name code or a valid full state name
*/
public any function state( required string state ) {
setAddress_state( state );
return this;
}
/**
* @hint alias for setting state
*/
public any function addressState( required string state ) {
return state( state );
}
/**
* @hint Required if address is in US. Can accept either a ZIP format of 12345 or ZIP+4 format of 12345-1234
*/
public any function zip( required string zip ) {
setAddress_zip( zip );
return this;
}
/**
* @hint alias for setting zip
*/
public any function addressZip( required string zip ) {
return zip( zip );
}
/**
* @hint 2 letter country short-name code (ISO 3166). Defaults to US.
*/
public any function country( required string country ) {
setAddress_country( country );
return this;
}
/**
* @hint alias for setting country
*/
public any function addressCountry( required string country ) {
return country( country );
}
public any function phone( required string phone ) {
setPhone( phone );
return this;
}
public any function email( required string email ) {
setEmail( email );
return this;
}
/**
* @hint Use metadata to store custom information for tagging and labeling back to your internal systems. Must be an object with up to 20 key-value pairs. Keys must at most 40 characters and values must be at most 500 characters. Neither can contain the characters " and \. Nested objects are not supported. See https://lob.com/docs#metadata for more information.
* @metadata if a struct is provided, it will be serialized. Otherwise the string will be set as provided
*/
public any function metadata( required any metadata ) {
if ( isStruct( metadata ) )
setMetadata( serializeJSON( metadata ) );
else
setMetadata( metadata );
return this;
}
/**
* @hint The zip code needs to be handled via a custom method, to force it to be passed as a string
*/
public string function build() {
var body = '';
var properties = getPropertyValues();
var count = properties.len();
properties.each(
function( property, index ) {
var value = property.key != 'address_zip' ? serializeJSON( property.value ) : serializeValuesAsString( property.value );
body &= '"#property.key#": ' & value & '#index NEQ count ? "," : ""#';
}
);
return '{' & body & '}';
}
/**
* @hint helper that forces object value serialization to strings. This is needed in some cases, where CF's loose typing causes problems
*/
private string function serializeValuesAsString( required any data ) {
var result = '';
if ( isStruct( data ) ) {
var serializedData = data.reduce(
function( result, key, value ) {
if ( result.len() ) result &= ',';
return result & '"#key#": "#value#"';
}, ''
);
result = '{' & serializedData & '}';
} else if ( isNumeric( data ) ) {
result = '"#data#"';
} else if ( isArray( data ) ) {
var serializedData = data.reduce(
function( result, item, index ) {
if ( result.len() ) result &= ',';
return result & '"#item#"';
}, ''
);
result = '[' & serializedData & ']';
}
return result;
}
/**
* @hint converts the array of properties to an array of their keys/values, while filtering those that have not been set
*/
private array function getPropertyValues() {
var propertyValues = getProperties().map(
function( item, index ) {
return {
"key" : item.name,
"value" : getPropertyValue( item.name )
};
}
);
return propertyValues.filter(
function( item, index ) {
if ( isStruct( item.value ) )
return !item.value.isEmpty();
else
return item.value.len();
}
);
}
private array function getProperties() {
var metaData = getMetaData( this );
var properties = [];
for( var prop in metaData.properties ) {
properties.append( prop );
}
return properties;
}
private any function getPropertyValue( string key ){
var method = this["get#key#"];
var value = method();
return value;
}
}
/helpers/address.cfc
lob = new lob.lob();
address = new lob.helpers.address()
.description( 'Into The Box 2018 - Demo' )
.company( 'Hyatt Place Houston/The Woodlands' )
.name( 'Guest: Matthew Clemente (4/25)' )
.address( '1909 Research Forest Drive' )
.city( 'The Woodlands' ).state( 'Texas' ).zip( '77380' );
result = lob.createAddress( address );
writeDump( var='#result#', format='html', abort='true' );
/**
* https://lob.com/docs#addresses_create
* @hint Create an address
* @address this should be an instance of the `helpers.address` component. However, if you want to create and pass in the struct or json yourself, you can.
*/
public struct function createAddress( required any address ) {
var body = {};
if ( isValid( 'component', address ) )
body = address.build();
else
body = address;
return apiCall( 'POST', '/addresses', {}, body );
}
(Because don't you prefer well documented repositories)