diff --git a/src/dapi/DataApi.js b/src/dapi/DataApi.js new file mode 100644 index 0000000..b1bc59d --- /dev/null +++ b/src/dapi/DataApi.js @@ -0,0 +1,51 @@ +/** + * Generic interface for data transmission + * + * Copyright (C) 2014 LoVullo Associates, Inc. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Interface = require( 'easejs' ).Interface; + + +/** + * Provies a generic interface for data transmission. The only assumption that a + * user of this API shall make is that data may be sent and received in some + * arbitrary, implementation-defined format, and that every request for data + * shall yield some sort of response via a callback. + */ +module.exports = Interface( 'DataApi', +{ + /** + * Perform an asynchronous request and invoke the callback with the reply + * + * If an implementation is synchronous, the callback must still be invoked. + * + * The data format is implementation-defined. The data parameter is + * documented as binary as it is the most permissive, but any data may be + * transferred that is supported by the protocol. + * + * The first parameter of the callback shall contain an Error in the event + * of a failure; otherwise, it shall be null. + * + * @param {string} data binary data to transmit + * @param {function(?Error,*)} callback continuation upon reply + * + * @return {DataApi} self + */ + 'public request': [ 'data', 'callback' ] +} ); diff --git a/src/dapi/http/HttpDataApi.js b/src/dapi/http/HttpDataApi.js new file mode 100644 index 0000000..fed8bb2 --- /dev/null +++ b/src/dapi/http/HttpDataApi.js @@ -0,0 +1,160 @@ +/** + * Data transmission over HTTP(S) + * + * Copyright (C) 2014 LoVullo Associates, Inc. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Class = require( 'easejs' ).Class, + DataApi = require( '../DataApi' ), + HttpImpl = require( './HttpImpl' ), + + // RFC 2616 methods + rfcmethods = { + DELETE: true, + GET: true, + HEAD: true, + OPTIONS: true, + POST: true, + PUT: true, + TRACE: true + }; + + +/** + * HTTP request abstraction. Does minor validation, but delegates to a specific + * HTTP implementation for the actual request. + */ +module.exports = Class( 'HttpDataApi' ) + .implement( DataApi ) + .extend( +{ + /** + * Request URL + * @type {string} + */ + 'private _url': '', + + /** + * HTTP method + * @type {string} + */ + 'private _method': '', + + /** + * HTTP implementation to perfom request + * @type {HttpImpl} + */ + 'private _impl': null, + + + /** + * Initialize Data API with destination and HTTP implementation + * + * The supplied HTTP implementation will be used to perform the HTTP + * requests, which permits the user to use whatever implementation works + * well with their existing system. + * + * @param {string} url destination URL + * @param {string} method RFC-2616-compliant HTTP method + * @param {HttpImpl} impl HTTP implementation + * + * @throws {TypeError} when non-HttpImpl is provided + */ + __construct: function( url, method, impl ) + { + if ( !( Class.isA( HttpImpl, impl ) ) ) + { + throw TypeError( "Expected HttpImpl" ); + } + + this._url = ''+url; + this._method = this._validateMethod( method ); + this._impl = impl; + }, + + + /** + * Perform an asynchronous request and invoke the callback with the reply + * + * In the event of an error, the first parameter is the error; otherwise, it + * is null. The return data shall not be used in the event of an error. + * + * The return value shall be a raw string; conversion to other formats must + * be handled by a wrapper. + * + * @param {string} data binary data to transmit + * @param {function(?Error,*):string} callback continuation upon reply + * + * @return {DataApi} self + * + * @throws {TypeError} on validation failure + */ + 'public request': function( data, callback ) + { + this._validateDataType( data ); + + this._impl.requestData( + this._url, this._method, data, callback + ); + + return this; + }, + + + /** + * Ensures that the provided method conforms to RFC 2616 + * + * @param {string} method HTTP method + * @return {string} provided method + * + * @throws {Error} on non-conforming method + */ + 'private _validateMethod': function( method ) + { + if ( !( rfcmethods[ method ] ) ) + { + throw Error( "Invalid RFC 2616 method: " + method ); + } + + return method; + }, + + + /** + * Validates that the provided data type is accepted by the Data API + * + * @param {*} data data to validate + * @return {undefined} + * + * @throws {TypeError} on validation failure + */ + 'private _validateDataType': function( data ) + { + var type = typeof data; + + if ( ( data === null ) + || !( ( type === 'string' ) || ( type === 'object' ) ) + ) + { + throw TypeError( + "Data must be a string of raw data or object containing " + + "key-value params" + ); + } + } +} ); diff --git a/src/dapi/http/HttpImpl.js b/src/dapi/http/HttpImpl.js new file mode 100644 index 0000000..dd214d5 --- /dev/null +++ b/src/dapi/http/HttpImpl.js @@ -0,0 +1,51 @@ +/** + * HTTP protocol implementation + * + * Copyright (C) 2014 LoVullo Associates, Inc. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var Interface = require( 'easejs' ).Interface; + + +/** + * HTTP protocol implementation that will perform the actual transfer. This + * abstraction allows use of whatever library the user prefers (e.g. + * XMLHttpRequest, jQuery, etc). + */ +module.exports = Interface( 'HttpImpl', +{ + /** + * Perform HTTP request + * + * If the request is synchronous, it must still return the data via the + * provided callback. The provided data is expected to be key-value if an + * object is given, otherwise a string of binary data. + * + * An implementation is not required to implement every HTTP method, + * although that is certainly preferred; a user of the API is expected to + * know when an implementation does not support a given method. + * + * @param {string} url destination URL + * @param {string} method RFC-2616-compliant HTTP method + * @param {Object|string} data request params + * @param {function(Error, Object)} callback server response callback + * + * @return {HttpImpl} self + */ + 'public requestData': [ 'url', 'method', 'data', 'callback' ] +} ); diff --git a/test/dapi/http/HttpDataApiTest.js b/test/dapi/http/HttpDataApiTest.js new file mode 100644 index 0000000..eeeb7d6 --- /dev/null +++ b/test/dapi/http/HttpDataApiTest.js @@ -0,0 +1,179 @@ +/** + * Test case for data transmission over HTTP(S) + * + * Copyright (C) 2014 LoVullo Associates, Inc. + * + * This file is part of the Liza Data Collection Framework + * + * Liza is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var dapi = require( '../../../' ).dapi, + expect = require( 'chai' ).expect, + Class = require( 'easejs' ).Class, + Sut = dapi.http.HttpDataApi, + + dummy_url = 'http://foo', + dummy_impl = Class + .implement( dapi.http.HttpImpl ) + .extend( { requestData: function( _, __, ___, ____ ) {} } ), + + dummy_sut = Sut( dummy_url, 'GET', dummy_impl ); + + +describe( 'HttpDataApi', function() +{ + it( 'is a DataApi', function() + { + expect( Class.isA( dapi.DataApi, dummy_sut ) ).to.be.ok; + } ); + + + it( 'permits RFC 2616 HTTP methods', function() + { + var m = [ 'GET', 'POST', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'TRACE' ]; + + m.forEach( function( method ) + { + expect( function() + { + Sut( dummy_url, method, dummy_impl ); + } ).to.not.throw( Error ); + } ); + } ); + + + it( 'does not permit non-RFC-2616 HTTP methods', function() + { + expect( function() + { + Sut( dummy_url, 'FOO', dummy_impl ); + } ).to.throw( Error, 'FOO' ); + } ); + + + it( 'rejects non-HttpImpl objects', function() + { + expect( function() + { + Sut( dummy_url, 'GET', {} ); + } ).to.throw( TypeError, 'HttpImpl' ); + } ); + + + describe( '.request', function() + { + var impl = Class( 'StubHttpImpl' ) + .implement( dapi.http.HttpImpl ) + .extend( + { + provided: [], + data: "", + err: null, + + requestData: function( url, method, data, c ) + { + this.provided = arguments; + c( this.err, this.data ); + } + } )(); + + + /** + * The actual request is performed by some underling implementation. + * This additional level of indirection allows the general concept of an + * "HTTP Data API" to vary from an underyling HTTP protocol + * implementation; they are separate concerns, although the distinction + * may seem subtle. + */ + it( 'delegates to provided HTTP implementation', function() + { + var method = 'POST', + data = {}, + c = function() {}; + + Sut( dummy_url, method, impl ).request( data, c ); + + var provided = impl.provided; + expect( provided[ 0 ] ).to.equal( dummy_url ); + expect( provided[ 1 ] ).to.equal( method ); + expect( provided[ 2 ] ).to.equal( data ); + expect( provided[ 3 ] ).to.equal( c ); + } ); + + + /** + * Method chaining + */ + it( 'returns self', function() + { + var sut = Sut( dummy_url, 'GET', impl ), + ret = sut.request( "", function() {} ); + + expect( ret ).to.equal( sut ); + } ); + + + /** + * String requests are intended to be raw messages, whereas objects are + * treated as key-value params. + */ + it( 'accepts string and object data', function() + { + expect( function() + { + Sut( dummy_url, 'GET', impl ) + .request( "", function() {} ) // string + .request( {}, function() {} ); // object + } ).to.not.throw( Error ); + } ); + + + it( 'rejects all other data types', function() + { + var sut = Sut( dummy_url, 'GET', impl ); + + [ 123, null, Infinity, undefined, NaN, function() {} ] + .forEach( function( data ) + { + expect( function() + { + sut.request( data, function() {} ); + } ).to.throw( TypeError ); + } ); + } ); + + + it( 'returns error provided by HTTP implementation', function( done ) + { + impl.err = Error( "Test impl error" ); + Sut( dummy_url, 'GET', impl ).request( "", function( err, resp ) + { + expect( err ).to.equal( impl.err ); + done(); + } ); + } ); + + + it( 'returns response provided by HTTP implementation', function( done ) + { + impl.data = {}; + Sut( dummy_url, 'GET', impl ).request( "", function( err, resp ) + { + expect( resp ).to.equal( impl.data ); + done(); + } ); + } ); + } ); +} );