diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..080d0b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# Deployed apps should consider commenting this line out: +# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git +node_modules + +# Ignore test config file +test/config.js +test.html +test.js + +# Dist directory - for now +dist/ diff --git a/.jscs.json b/.jscs.json new file mode 100644 index 0000000..1d9bbee --- /dev/null +++ b/.jscs.json @@ -0,0 +1,125 @@ +{ + "requireCurlyBraces": [ + "if", + "else", + "for", + "while", + "do", + "try", + "catch", + "case", + "default" + ], + "requireSpaceAfterKeywords": [ + "if", + "else", + "for", + "while", + "do", + "switch", + "return", + "try", + "catch" + ], + "requireSpaceBeforeBlockStatements": true, + "requireParenthesesAroundIIFE": true, + "requireSpacesInConditionalExpression": { + "afterTest": true, + "beforeConsequent": true, + "afterConsequent": true, + "beforeAlternate": true + }, + "requireSpacesInFunctionExpression": { + "beforeOpeningRoundBrace": true, + "beforeOpeningCurlyBrace": true + }, + "requireSpacesInAnonymousFunctionExpression": { + "beforeOpeningRoundBrace": true, + "beforeOpeningCurlyBrace": true + }, + "requireSpacesInNamedFunctionExpression": { + "beforeOpeningRoundBrace": true, + "beforeOpeningCurlyBrace": true + }, + "requireSpacesInFunctionDeclaration": { + "beforeOpeningRoundBrace": true, + "beforeOpeningCurlyBrace": true + }, + "requireMultipleVarDecl": true, + "requireBlocksOnNewline": true, + "disallowPaddingNewlinesInBlocks": true, + "disallowEmptyBlocks": true, + "requireSpacesInsideObjectBrackets": "all", + "disallowSpacesInsideParentheses": true, + "disallowSpaceAfterObjectKeys": true, + "requireCommaBeforeLineBreak": true, + "disallowLeftStickedOperators": [ + "?", + "=", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<", + "<=" + ], + "disallowRightStickedOperators": [ + "?", + "=", + ":", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<", + "<=" + ], + "requireSpaceBeforeBinaryOperators": [ + "=", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<", + "<=" + ], + "requireSpaceAfterBinaryOperators": [ + "=", + "==", + "===", + "!=", + "!==", + ">", + ">=", + "<", + "<=" + ], + "requireCamelCaseOrUpperCaseIdentifiers": true, + "disallowMultipleLineBreaks": true, + "validateQuoteMarks": { + "mark": "'", + "escape": true + }, + "validateIndentation": 2, + "disallowMixedSpacesAndTabs": true, + "disallowTrailingWhitespace": true, + "disallowTrailingComma": true, + "disallowKeywordsOnNewLine": [ + "else" + ], + "requireLineFeedAtFileEnd": true, + "maximumLineLength": 120, + "requireCapitalizedConstructors": true, + "disallowYodaConditions": true, + "validateJSDoc": { + "checkParamNames": true, + "checkRedundantParams": true, + "requireParamTypes": true + } +} diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..879a5ac --- /dev/null +++ b/.jshintrc @@ -0,0 +1,22 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "newcap": true, + "noarg": true, + "regexp": true, + "quotmark": "single", + "unused": "vars", + "undef": true, + "trailing": true, + "smarttabs": true, + "sub": true, + "globals": { + "toString": false + } +} diff --git a/.jshintrc-test b/.jshintrc-test new file mode 100644 index 0000000..7f2ba87 --- /dev/null +++ b/.jshintrc-test @@ -0,0 +1,27 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "curly": true, + "eqeqeq": true, + "expr": true, + "immed": true, + "indent": 2, + "newcap": true, + "noarg": true, + "regexp": true, + "quotmark": "single", + "unused": "vars", + "undef": true, + "trailing": true, + "smarttabs": true, + "globals": { + "describe": false, + "it": false, + "before": false, + "beforeEach": false, + "after": false, + "afterEach": false + } +} diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..35c953c --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,175 @@ +'use strict'; +/*global module:false*/ + +// Configure the grunt modules we want to use here +var gruntModules = [ + 'grunt-contrib-nodeunit', + 'grunt-contrib-jshint', + 'grunt-contrib-copy', + 'grunt-contrib-clean', + 'grunt-mocha-test', + 'grunt-blanket', + 'grunt-jscs-checker', + 'grunt-contrib-compress', + 'grunt-browserify', + 'grunt-contrib-uglify' +]; + +module.exports = function(grunt) { + + // The `time-grunt` module provides a handy output of the run time of each + // grunt task + require('time-grunt')(grunt); + + // Load these necessary tasks + gruntModules.forEach(function (gruntModule) { + grunt.loadNpmTasks(gruntModule); + }); + + + // Project configuration. + grunt.initConfig({ + // Metadata. + pkg: grunt.file.readJSON('package.json'), + concat: { + options: { + banner: '<%= banner %>', + stripBanners: true + }, + dist: { + src: ['lib/<%= pkg.name %>.js'], + dest: 'dist/<%= pkg.name %>.js' + } + }, + uglify: { + robin: { + options: { + drop_console: true, + report: 'gzip' + }, + files: { + 'dist/robin.browser.min.js': ['dist/robin.browser.js'] + } + } + }, + jshint: { + src: { + options: { + jshintrc: '.jshintrc', + reporter: require('jshint-stylish') + }, + src: ['Gruntfile.js', 'robin.js', 'lib/**/*.js'], + }, + test: { + options: { + jshintrc: '.jshintrc-test', + reporter: require('jshint-stylish'), + }, + src: ['test/**/*.js'] + }, + postInstall: { + options: { + jshintrc: '.jshintrc', + reporter: require('jshint-stylish'), + }, + src: ['scripts/postInstall.js'] + } + }, + browserify: { + robin: { + files: { + 'dist/robin.browser.js': ['robin.js'] + }, + options: { + browserifyOptions: { + basedir: '.' + }, + bundleOptions: { + standalone: 'Robin', + } + } + }, + tests: { + files: { + 'dist/robin.browser.tests.js': ['test/**/test*.js'] + }, + options: { + browserifyOptions: { + basedir: '.' + } + } + }, + }, + compress: { + robin: { + files: { + 'dist/robin.browser.min.js.gzip': ['dist/robin.browser.min.js'] + }, + options: { + mode: 'gzip', + level: 9, + pretty: true + } + } + }, + jscs: { + lib: { + src: ['./robin.js', 'lib/**/*.js'], + options: { + config: '.jscs.json', + reporter: 'console' + } + } + }, + clean: { + coverage: { + src: ['coverage/'] + } + }, + copy: { + src: { + src: ['robin.js', 'lib/**/*.js'], + dest: 'coverage/' + }, + test: { + src: ['test/**/test*.js'], + dest: 'coverage/' + } + }, + blanket: { + coverage: { + src: ['coverage/'], + dest: 'coverage/' + } + }, + mochaTest: { + test: { + options: { + reporter: 'spec', + timeout: 1000 + }, + src: ['test/**/test*.js'] + }, + coverage: { + options: { + reporter: 'html-cov', + // use the quiet flag to suppress the mocha console output + quiet: true, + // specify a destination file to capture the mocha + // output (the quiet option does not suppress this) + captureFile: 'coverage/coverage.html' + }, + src: ['coverage/test/**/test*.js'] + } + }, + }); + + // Default task. + grunt.registerTask('lint', ['jshint']); + grunt.registerTask('style', ['jscs:lib']); + grunt.registerTask('coverage', ['copy:src', 'blanket', 'copy:test', 'mochaTest:coverage']); + grunt.registerTask('test', ['mochaTest:test']); + grunt.registerTask('browser', ['browserify', 'uglify:robin']); + grunt.registerTask('build', ['lint', 'test', 'browser']); + +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..0eadd9f --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Robin JavaScript SDK + +A JavaScript SDK to communicate with the [Robin](http://getrobin.com/) platform. + +This SDK provides the ability to communicate both with the Robin API and the Robin Grid. + +### Installation + +##### node + +`npm install git@github.com:robinpowered/robin-js-sdk.git --save` will save this to the `dependencies` section of your `package.json` + +You may then `require` this `sdk` as follows: + +````javascript +var Robin = require('robin-js-sdk'); +```` + +##### browser + +Include the script from `https://static.robinpowered.com/js/sdk/$version/robin.browser.min.js`, where `$version` is the npm version number in `package.json`. `robin-js-sdk` will be automatically attached to the `window` object as `window.Robin`. + +### Instantiation + +`robin-js-sdk` is instantiated with two arguments, the first is a Robin Access Token. The second is an optional argument that can be `null`, a `string`, an `object` or `undefined`. + +##### Options + +If the second argument is `undefined` or `null`, this `SDK` will be instantiated with endpoints pointing to `production`. + +If it is a string, it must be a valid `robin` endpoint. Valid values are `test`, `staging` or `production`. Otherwise an error is thrown. + +If it is an object: + +* An empty object will result in all endpoints defaulting to production. +* If it contains an `env` property, all endpoints will default to that env. Values other than string types for this property will throw errors. +* If it has a `urls` property, then values for `core`, `grid` or `places` will override the default endpoint for any of those apps. Types for this property other than `object` will throw errors. Properties of this object other than `core`, `grid` or `places` will throw errors. + + +### Robin API + +The Robin API is a REST based API. Calls to the API return a `promise`. + +Core API Routes: + +| Route | Source | +| ------ | -------- | +| Accounts | [accounts.js](lib/api/modules/accounts.js) | +| Apps | [apps.js](lib/api/modules/apps.js) | +| Auth | [auth.js](lib/api/modules/auth.js) | +| Channels | [channels.js](lib/api/modules/channels.js) | +| DeviceManifests | [devicemanifests.js](lib/api/modules/devicemanifests.js) | +| Devices | [devices.js](lib/api/modules/devices.js) | +| Identifiers | [identifiers.js](lib/api/modules/identifiers.js) | +| Locations | [locations.js](lib/api/modules/locations.js) | +| Me | [me.js](lib/api/modules/me.js) | [testMe.js](test/testMe.js) +| Organizations | [organizations.js](lib/api/modules/organizations.js) | +| Projects | [projects.js](lib/api/modules/projects.js) | +| Spaces | [spaces.js](lib/api/modules/spaces.js) | +| Triggers | [triggers.js](lib/api/modules/triggers.js) | +| Users | [users.js](lib/api/modules/users.js) | + + +Places API Routes: + +| Route | Source | +| ------ | -------- | +| Events | [events.js](lib/api/modules/events.js) | + +### Robin Grid + +_TODO: Improve this section in a future PR_ + +The Grid is a websocket server that allows PubSub between clients and devices through configured channels. The Grid module is an `EventEmitter`, to allow real-time updates. + +The Grid exposes several modules, which allow you to connect and listen: + +* `join` - Allows clients to listen to updates from channels +* `leave` - Disconnects the client from receiving updates for a particular channel +* `send` - Allows a client to send messages to a channel. + +## Development Roadmap + +The following should be implemented: + +* [x] Support for use in browsers, as well as node.js +* ~~[ ] Add winston for logging - will this work in browsers?~~ +* [x] Handle scope of access tokens for extended functionality (such as retrieving all API items) +* [ ] Adhere to Robin Javascript Coding Standards - this is TBD. +* [x] Implement API function arguments based on API documentation +* [ ] Expand documentation to include all API modules +* [ ] Can we autogenerate API module functions based on a object template? + +## Browserify + +Running `grunt browser` generates two files in the `browser/` folder. + +* `browser/robin.browser.js` is the full file after being run through browserify. +* `browser/robin.browser.min.js` is the minified version. + diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..835cd66 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,56 @@ +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'], + + // list of files / patterns to load in the browser + files: [ + 'lib/**/*.js', + 'test/**/test*.js' + ], + + // list of files to exclude + exclude: [ + + ], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'lib/**/*.js': 'coverage' + }, + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['dots', 'coverage'], + + // web server port + port: 9876, + + // cli runner port + runnerPort: 9100, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome', 'Firefox', 'Safari'], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false + }); +}; diff --git a/lib/api/api.js b/lib/api/api.js new file mode 100644 index 0000000..a256bc7 --- /dev/null +++ b/lib/api/api.js @@ -0,0 +1,137 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var apiModules, + util = require('util'), + RbnUtil = require('../util'), + RobinApiBase = require('./base'), + RequestBase = require('./requestBase'); + +// Having an object here is an awful hack, but it has to be done for browserify. +apiModules = { + accounts: require('./modules/accounts'), + apps: require('./modules/apps'), + auth: require('./modules/auth'), + channels: require('./modules/channels'), + deviceManifests: require('./modules/devicemanifests'), + devices: require('./modules/devices'), + identifiers: require('./modules/identifiers'), + me: require('./modules/me'), + organizations: require('./modules/organizations'), + events: require('./modules/events'), + locations: require('./modules/locations'), + spaces: require('./modules/spaces') +}; + +module.exports = (function () { + /** + * The Robin API constructor + * @param {String} accessToken A Robin Access Token + * @param {String} coreUrl The Base URL for the Core API. + * @param {String} placesUrl The Base URL for the Places API + */ + function RobinApi (accessToken, coreUrl, placesUrl) { + if (accessToken) { + RobinApi.super_.apply(this, arguments); + this.setAccessToken(accessToken); + this.setupCore(coreUrl); + this.setupPlaces(placesUrl); + this.loadApiModules(); + } else { + throw new TypeError('The access token is missing or malformed'); + } + } + + util.inherits(RobinApi, RobinApiBase); + + /** + * Setup requests to the Core API + * @param {String} coreUrl The url of the core API. + */ + RobinApi.prototype.setupCore = function (coreUrl) { + var coreApi; + if (coreUrl) { + coreApi = new RequestBase(); + this.Core = RbnUtil.applyScope(coreApi, this); + this.Core.setBaseUrl(coreUrl); + } else { + throw new TypeError('The url of the core API is not present or malformed'); + } + }; + + /** + * Setup requests to the Places API + * @param {String} placesUrl The url of the places API. + */ + RobinApi.prototype.setupPlaces = function (placesUrl) { + var placesApi; + if (placesUrl) { + placesApi = new RequestBase(); + this.Places = RbnUtil.applyScope(placesApi, this); + this.Places.setBaseUrl(placesUrl); + } else { + throw new TypeError('The url of the places API is not present or malformed'); + } + }; + + /** + * Load the API module objects and bind their functions to this API class. + */ + RobinApi.prototype.loadApiModules = function () { + var module, + clonedModule, + moduleName; + for (moduleName in apiModules) { + module = apiModules[moduleName]; + clonedModule = RbnUtil.cloneObject(module); + this[moduleName] = RbnUtil.applyScope(clonedModule, this); + } + }; + + /** + * Proxy HTTP request methods for the Core API, to make requests easier + */ + RobinApi.prototype.GET = function (path, params) { + return this.Core.GET(path, params); + }; + + RobinApi.prototype.HEAD = function (path, params) { + return this.Core.HEAD(path, params); + }; + + RobinApi.prototype.POST = function (path, data) { + return this.Core.POST(path, data); + }; + + RobinApi.prototype.PUT = function (path, data) { + return this.Core.PUT(path, data); + }; + + RobinApi.prototype.PATCH = function (path, data) { + return this.Core.PATCH(path, data); + }; + + RobinApi.prototype.DELETE = function (path, data) { + return this.Core.DELETE(path, data); + }; + + RobinApi.prototype.OPTIONS = function (path, data) { + return this.Core.OPTIONS(path, data); + }; + + /** + * Proxy the paginate-all function from core too. + */ + RobinApi.prototype.all = function (pathOrPromise, params) { + return this.Core.all(pathOrPromise, params); + }; + + return RobinApi; +}).call(this); diff --git a/lib/api/base.js b/lib/api/base.js new file mode 100644 index 0000000..0bbc55c --- /dev/null +++ b/lib/api/base.js @@ -0,0 +1,99 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Promise = require('bluebird'); + +module.exports = (function () { + /** + * The Robin API Base class constructor + */ + function RobinApiBase () { + } + + /** + * Set the Robin Access Token + * @param {String} token A Robin Access Token + */ + RobinApiBase.prototype.setAccessToken = function (token) { + if (token) { + this._accessToken = token; + } + }; + + /** + * Get the Robin Access Token + * @return {String} A Robin Access Token + */ + RobinApiBase.prototype.getAccessToken = function () { + if (this._accessToken) { + return this._accessToken; + } + }; + + /** + * Set the Robin Relay Identifier + * @return {String} relayIdentifier A Robin Relay Identifier + */ + RobinApiBase.prototype.setRelayIdentifier = function (relayIdentifier) { + if (relayIdentifier) { + this._relayIdentifier = relayIdentifier; + } + }; + + /** + * Get the Robin Relay Identifier + * @return {String} A Robin Relay Identifier + */ + RobinApiBase.prototype.getRelayIdentifier = function () { + if (this._relayIdentifier) { + return this._relayIdentifier; + } + }; + + /** + * Reject an invalid API request + * @param {*} rejection Any type of variable that can be used to reject this promise + * @return {Function} A promise that get's rejected. + */ + RobinApiBase.prototype.rejectRequest = function (rejection) { + return Promise.reject(rejection); + }; + + /** + * A utility function to construct a url path + * It iterates through the arguments and joins them all into a + * string separated by a '/' character + * @return {String} A url path + */ + RobinApiBase.prototype.constructPath = function () { + var path, + arg, + argument, + args = []; + + for (arg in arguments) { + argument = arguments[arg]; + if (argument) { + args.push(argument); + } + } + path = args.join('/'); + if (path) { + if (path.slice(0, 1) !== '/') { + path = '/' + path; + } + } else { + path = '/'; + } + return path; + }; + + return RobinApiBase; +}).apply(this, arguments); diff --git a/lib/api/index.js b/lib/api/index.js new file mode 100644 index 0000000..89e10b6 --- /dev/null +++ b/lib/api/index.js @@ -0,0 +1,13 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var RobinApi = require('./api'); + +module.exports = RobinApi; diff --git a/lib/api/modules/accounts.js b/lib/api/modules/accounts.js new file mode 100644 index 0000000..a461e09 --- /dev/null +++ b/lib/api/modules/accounts.js @@ -0,0 +1,29 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get an account + * @param {String|Integer} accountIdOrSlug A Robin account identifier or slug + * @param {Object} params A querystring object + * @return {Function} A Promise + */ + get: function (accountIdOrSlug, params) { + var path; + if (accountIdOrSlug) { + path = this.constructPath(constants.ACCOUNTS, accountIdOrSlug); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: An account id or slug must be supplied for this operation'); + } + } +}; diff --git a/lib/api/modules/apps.js b/lib/api/modules/apps.js new file mode 100644 index 0000000..64b95a7 --- /dev/null +++ b/lib/api/modules/apps.js @@ -0,0 +1,55 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get an app + * @param {String|Integer|undefined} appIdOrSlug A Robin app identifier or slug + * @param {Object|undefnied} params A querystring object + * @return {Function} A Promise + */ + get: function (appIdOrSlug, params) { + var path = this.constructPath(constants.APPS, appIdOrSlug); + return this.Core.GET(path, params); + }, + + /** + * Update an app + * @param {String|Integer} appIdOrSlug A Robin app identifier or slug + * @param {Object} data A data object + * @return {Function} A Promise + */ + update: function (appIdOrSlug, data) { + var path; + if (appIdOrSlug && data) { + path = this.constructPath(constants.APPS, appIdOrSlug); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: An app id or slug and a data object are required.'); + } + }, + + /** + * Delete an app + * @param {String|Integer} appIdOrSlug A Robin app identifier or slug + * @return {Function} A Promise + */ + delete: function (appIdOrSlug) { + var path; + if (appIdOrSlug) { + path = this.constructPath(constants.APPS, appIdOrSlug); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: An app id or slug is required.'); + } + } +}; diff --git a/lib/api/modules/auth.js b/lib/api/modules/auth.js new file mode 100644 index 0000000..23f3fb4 --- /dev/null +++ b/lib/api/modules/auth.js @@ -0,0 +1,23 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get access token info + * @param {Object} params A querystring object + * @return {Function} A Promise + */ + getAccessTokenInfo: function (params) { + var path = this.constructPath(constants.AUTH); + return this.Core.GET(path, params); + } +}; diff --git a/lib/api/modules/channels.js b/lib/api/modules/channels.js new file mode 100644 index 0000000..3397121 --- /dev/null +++ b/lib/api/modules/channels.js @@ -0,0 +1,199 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get all the channels or a particular channel identified by the `identifier` parameter + * @param {String|Integer|undefined} identifier A Robin channel identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (identifier, params) { + var path = this.constructPath(constants.CHANNELS, identifier); + return this.Core.GET(path, params); + }, + + /** + * Create a channel + * @param {Object} data A data object + * @return {Function} A Promise + */ + create: function (data) { + var path; + if (data) { + path = this.constructPath(constants.CHANNELS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A data object is required.'); + } + }, + + /** + * Update a channel + * @param {String|Integer|undefined} identifier A Robin channel identifier + * @param {Object} data A data object + * @return {Function} A Promise + */ + update: function (identifier, data) { + var path; + if (identifier && data) { + path = this.constructPath(constants.CHANNELS, identifier); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request: A channel identifier and a data object are required.'); + } + }, + + /** + * Delete a channel + * @param {String|Integer} identifier A Robin channel identifier + * @return {Function} A Promise + */ + delete: function (identifier) { + var path; + if (identifier) { + path = this.constructPath(constants.CHANNELS, identifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A channel identifier is required.'); + } + }, + + /** + * Channel Data + * @type {Object} + */ + data: { + /** + * Get all the data from a channel or a particular channel data point identified by `dataIdentifier` + * @param {String|Integer} channelIdentifier A Robin channel identifier + * @param {String|Integer|undefined} dataIdentifier A Robin channel data point identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (channelIdentifier, dataIdentifier, params) { + var path; + if (channelIdentifier) { + path = this.constructPath(constants.CHANNELS, channelIdentifier, constants.DATA, dataIdentifier); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A channel identifier is required.'); + } + }, + + /** + * Add data to a channel + * @param {String|Integer} channelIdentifier A Robin channel identifier + * @param {Object} data A querystring object + * @return {Function} A Promise + */ + add: function (channelIdentifier, data) { + var path; + if (channelIdentifier) { + path = this.constructPath(constants.CHANNELS, channelIdentifier, constants.DATA); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A channel identifier is required.'); + } + }, + + /** + * Delete a data point from a channel + * @param {String|Integer} channelIdentifier A Robin channel identifier + * @param {String|Integer} dataIdentifier A Robin channel data point identifier + * @return {Function} A Promise + */ + delete: function (channelIdentifier, dataIdentifier) { + var path; + if (channelIdentifier && dataIdentifier) { + path = this.constructPath(constants.CHANNELS, channelIdentifier, constants.DATA, dataIdentifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A channel identifier and a data point identifier are required.'); + } + } + }, + + /** + * Channel Triggers + * @type {Object} + */ + triggers: { + /** + * Get all the triggers on a channel or a particular channel trigger identified by `triggerIdentifier` + * @param {String|Integer} channelIdentifier A Robin channel identifier + * @param {String|Integer|undefined} triggerIdentifier A Robin channel trigger identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (channelIdentifier, triggerIdentifier, params) { + var path; + if (channelIdentifier) { + path = this.constructPath(constants.CHANNELS, channelIdentifier, constants.TRIGGERS, triggerIdentifier); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A channel identifier is required.'); + } + }, + + /** + * Add a trigger to a channel + * @param {String|Integer} channelIdentifier A Robin channel identifier + * @param {Object} data A querystring object + * @return {Function} A Promise + */ + add: function (channelIdentifier, data) { + var path; + if (channelIdentifier) { + path = this.constructPath(constants.CHANNELS, channelIdentifier, constants.TRIGGERS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A channel identifier and a data object are required.'); + } + }, + + /** + * Update a trigger on a channel + * @param {String|Integer} channelIdentifier A Robin channel identifier + * @param {String|Integer} triggerIdentifier A Robin channel trigger identifier + * @param {Object} data A querystring object + * @return {Function} A Promise + */ + update: function (channelIdentifier, triggerIdentifier, data) { + var path, + rejectMsg; + if (channelIdentifier && triggerIdentifier && data) { + path = this.constructPath(constants.CHANNELS, channelIdentifier, constants.TRIGGERS, triggerIdentifier); + return this.Core.PATCH(path, data); + } else { + rejectMsg = 'Bad Request: A channel identifier, a feed identifier and a trigger object are required.'; + return this.rejectRequest(rejectMsg); + } + }, + + /** + * Delete a trigger from a channel + * @param {String|Integer} channelIdentifier A Robin channel identifier + * @param {String|Integer} triggerIdentifier A Robin channel data point identifier + * @return {Function} A Promise + */ + delete: function (channelIdentifier, triggerIdentifier) { + var path; + if (channelIdentifier && triggerIdentifier) { + path = this.constructPath(constants.CHANNELS, channelIdentifier, constants.TRIGGERS, triggerIdentifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A channel identifier and a trigger identifier are required.'); + } + } + } +}; diff --git a/lib/api/modules/devicemanifests.js b/lib/api/modules/devicemanifests.js new file mode 100644 index 0000000..91c0f8b --- /dev/null +++ b/lib/api/modules/devicemanifests.js @@ -0,0 +1,171 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get all the device manifests or a particular device manifest identified by the `identifier` parameter + * @param {String|Integer|undefined} identifier A Robin device manifest identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (identifier, params) { + var path = this.constructPath(constants.DEVICE_MANIFESTS, identifier); + return this.Core.GET(path, params); + }, + + /** + * Create a device manifest + * @param {Object} data A data object + * @return {Function} A Promise + */ + create: function (data) { + var path; + if (data) { + path = this.constructPath(constants.DEVICE_MANIFESTS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A data object is required'); + } + }, + + /** + * Update a device manifest + * @param {String|Integer|undefined} identifier A Robin device manifest identifier + * @param {Object} data A data object + * @return {Function} A Promise + */ + update: function (identifier, data) { + var path; + if (identifier && data) { + path = this.constructPath(constants.DEVICE_MANIFESTS, identifier); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request: A device manifest identifier and a data object are required.'); + } + }, + + /** + * Delete a device manifest + * @param {String|Integer|undefined} identifier A Robin device manifest identifier + * @return {Function} A Promise + */ + delete: function (identifier) { + var path; + if (identifier) { + path = this.constructPath(constants.DEVICE_MANIFESTS, identifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A device manifest identifier is required.'); + } + }, + + /** + * Device Manifest Feeds + * @type {Object} + */ + feeds: { + /** + * Get all the feeds for a device manifest or a particular feed identified by `feedIdentifier` + * @param {String|Integer} deviceManifestIdentifier A Robin device manifest identifier + * @param {String|Integer|undefined} feedIdentifier A Robin feed identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (deviceManifestIdentifier, feedIdentifier, params) { + var path; + if (deviceManifestIdentifier) { + path = this.constructPath(constants.DEVICE_MANIFESTS, deviceManifestIdentifier, + constants.FEEDS, feedIdentifier); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A device manifest identifier is required.'); + } + }, + + /** + * Add a feed to a device manifest + * @param {String|Integer} deviceManifestIdentifier A Robin device manifest identifier + * @param {Object} data A querystring object + * @return {Function} A Promise + */ + add: function (deviceManifestIdentifier, data) { + var path; + if (deviceManifestIdentifier) { + path = this.constructPath(constants.DEVICE_MANIFESTS, deviceManifestIdentifier, constants.FEEDS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A device manifest identifier and a data object are required.'); + } + }, + + /** + * Update a feed on a device manifest + * @param {String|Integer} deviceManifestIdentifier A Robin device manifest identifier + * @param {String|Integer} feedIdentifier A Robin feed identifier + * @param {Object} data A querystring object + * @return {Function} A Promise + */ + update: function (deviceManifestIdentifier, feedIdentifier, data) { + var path, + rejectMsg; + if (deviceManifestIdentifier && feedIdentifier && data) { + path = this.constructPath(constants.DEVICE_MANIFESTS, deviceManifestIdentifier, + constants.FEEDS, feedIdentifier); + return this.Core.PATCH(path, data); + } else { + rejectMsg = 'Bad Request: A device manifest identifier, a feed identifier and a data object are required.'; + return this.rejectRequest(rejectMsg); + } + }, + + /** + * Delete a feed from a device manifest + * @param {String|Integer} deviceManifestIdentifier A Robin device manifest identifier + * @param {String|Integer} feedIdentifier A Robin feed identifier + * @return {Function} A Promise + */ + delete: function (deviceManifestIdentifier, feedIdentifier) { + var path; + if (deviceManifestIdentifier && feedIdentifier) { + path = this.constructPath(constants.DEVICE_MANIFESTS, deviceManifestIdentifier, + constants.FEEDS, feedIdentifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A device manifest identifier and a feed identifier are required.'); + } + } + }, + + /** + * Device Manifest Devices + * @type {Object} + */ + devices: { + /** + * Get all the devices for a device manifest or a particular device identified by `deviceIdentifier` + * @param {String|Integer} deviceManifestIdentifier A Robin device manifest identifier + * @param {String|Integer|undefined} deviceIdentifier A Robin device identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (deviceManifestIdentifier, deviceIdentifier, params) { + var path; + if (deviceManifestIdentifier) { + path = this.constructPath(constants.DEVICE_MANIFESTS, deviceManifestIdentifier, + constants.DEVICES, deviceIdentifier); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A device manifest identifier is required.'); + } + } + } +}; diff --git a/lib/api/modules/devices.js b/lib/api/modules/devices.js new file mode 100644 index 0000000..236779a --- /dev/null +++ b/lib/api/modules/devices.js @@ -0,0 +1,238 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get all the devices or a particular device identified by the `identifier` parameter + * @param {String|Integer|undefined} identifier A Robin device identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (identifier, params) { + var path = this.constructPath(constants.DEVICES, identifier); + return this.Core.GET(path, params); + }, + + /** + * Create a device + * @param {Object} data A data object + * @return {Function} A Promise + */ + create: function (data) { + var path; + if (data) { + path = this.constructPath(constants.DEVICES); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A data object is required'); + } + }, + + /** + * Update a device + * @param {String|Integer|undefined} identifier A Robin device identifier + * @param {Object} data A data object + * @return {Function} A Promise + */ + update: function (identifier, data) { + var path; + if (identifier && data) { + path = this.constructPath(constants.DEVICES, identifier); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request: A device identifier and a data object are required.'); + } + }, + + /** + * Delete a device + * @param {String|Integer} identifier A Robin device identifier + * @return {Function} A Promise + */ + delete: function (identifier) { + var path; + if (identifier) { + path = this.constructPath(constants.DEVICES, identifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A device identifier is required.'); + } + }, + + /** + * Device Identifiers + * @type {Object} + */ + identifiers: { + /** + * Get all the identifiers for a device or a particular identifier identified by `identifierURN` + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @param {String|Integer|undefined} identifierURN A Robin feed identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (deviceIdentifier, identifierURN, params) { + var path; + if (deviceIdentifier) { + path = this.constructPath(constants.DEVICES, deviceIdentifier, constants.IDENTIFIERS, identifierURN); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A device identifier is required.'); + } + }, + + /** + * Create an identifier for a device + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @param {Object} data A querystring object + * @return {Function} A Promise + */ + create: function (deviceIdentifier, data) { + var path; + if (deviceIdentifier) { + path = this.constructPath(constants.DEVICES, deviceIdentifier, constants.IDENTIFIERS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A device identifier and a data object are required.'); + } + }, + + /** + * Add an identifier for a device + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @param {String|Integer} identifierUrn A Robin identifier URN + * @param {Object} data A querystring object + * @return {Function} A Promise + */ + add: function (deviceIdentifier, identifierUrn, data) { + var path; + if (deviceIdentifier) { + path = this.constructPath(constants.DEVICES, deviceIdentifier, constants.IDENTIFIERS, identifierUrn); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request: A device identifier, an identifier id and a data object are required.'); + } + }, + + /** + * Delete an identifier from a device + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @param {String|Integer} identifierUrn A Robin identifier URN + * @return {Function} A Promise + */ + delete: function (deviceIdentifier, identifierUrn) { + var path; + if (deviceIdentifier && identifierUrn) { + path = this.constructPath(constants.DEVICES, deviceIdentifier, constants.IDENTIFIERS, identifierUrn); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A device identifier and an identifier id are required.'); + } + } + }, + + /** + * Device Channels + * @type {Object} + */ + channels: { + /** + * Get all the channels for a device or a particular channel identified by `channelIdentifier` + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @param {String|Integer|undefined} channelIdentifier A Robin channel identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (deviceIdentifier, channelIdentifier, params) { + var path; + if (deviceIdentifier) { + path = this.constructPath(constants.DEVICES, deviceIdentifier, constants.CHANNELS, channelIdentifier); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A device identifier is required.'); + } + }, + + /** + * Add a channel to a device + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @param {Object} data A querystring object + * @return {Function} A Promise + */ + create: function (deviceIdentifier, data) { + var path; + if (deviceIdentifier) { + path = this.constructPath(constants.DEVICES, deviceIdentifier, constants.CHANNELS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A device identifier and a data object are required.'); + } + }, + + /** + * Update a feed on a channel + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @param {String|Integer} channelIdentifier A Robin channel identifier + * @param {Object} data A querystring object + * @return {Function} A promise + */ + update: function (deviceIdentifier, channelIdentifier, data) { + var path, + rejectMsg; + if (deviceIdentifier && channelIdentifier && data) { + path = this.constructPath(constants.DEVICES, deviceIdentifier, constants.CHANNELS, channelIdentifier); + return this.Core.PATCH(path, data); + } else { + rejectMsg = 'Bad Request: A device identifier, a feed identifier and a data object are required.'; + return this.rejectRequest(rejectMsg); + } + }, + + /** + * Delete a channel from a device + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @param {String|Integer} channelIdentifier A Robin channel identifier + * @return {Function} A Promise + */ + delete: function (deviceIdentifier, channelIdentifier) { + var path; + if (deviceIdentifier && channelIdentifier) { + path = this.constructPath(constants.DEVICES, deviceIdentifier, constants.CHANNELS, channelIdentifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A device identifier and a channel identifier are required.'); + } + } + }, + + /** + * Device Spaces + * @type {Object} + */ + spaces: { + /** + * Get a device's spaces + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A promise + */ + get: function (deviceIdentifier, params) { + var path; + if (deviceIdentifier) { + path = this.constructPath(constants.DEVICES, deviceIdentifier, constants.SPACES); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A device identifier is required'); + } + } + } +}; diff --git a/lib/api/modules/events.js b/lib/api/modules/events.js new file mode 100644 index 0000000..c840e07 --- /dev/null +++ b/lib/api/modules/events.js @@ -0,0 +1,28 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Create an event + * @param {Object} data A data object + * @return {Function} A promise + */ + create: function (data) { + var path; + if (data) { + path = this.constructPath(constants.EVENTS); + return this.Places.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A data object is required.'); + } + } +}; diff --git a/lib/api/modules/identifiers.js b/lib/api/modules/identifiers.js new file mode 100644 index 0000000..f4db088 --- /dev/null +++ b/lib/api/modules/identifiers.js @@ -0,0 +1,38 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get all the identifiers or a particular identifier identified by the `identifierUrn` parameter + * @param {String|Integer|undefined} identifierUrn A Robin identifier URN + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (identifierUrn, params) { + var path = this.constructPath(constants.IDENTIFIERS, identifierUrn); + return this.Core.GET(path, params); + }, + + /** + * Delete an identifier + * @param {String|Integer} identifierUrn A Robin identifier URN + * @return {Function} A Promise + */ + delete: function (identifierUrn) { + if (identifierUrn) { + var path = this.constructPath(constants.IDENTIFIERS, identifierUrn); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad request: An identifier URN is required.'); + } + } +}; diff --git a/lib/api/modules/locations.js b/lib/api/modules/locations.js new file mode 100644 index 0000000..ae95dc9 --- /dev/null +++ b/lib/api/modules/locations.js @@ -0,0 +1,116 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get all the locations or a particular location identified by `identifier` + * @param {String|Integer|undefined} identifier A location identifier + * @param {Object} params A querystring object + * @return {Function} A promise + */ + get: function (identifier, params) { + var path = this.constructPath(constants.LOCATIONS, identifier); + return this.Core.GET(path, params); + }, + + /** + * [update description] + * @param {[type]} identifier [description] + * @param {[type]} data [description] + * @return {[type]} [description] + */ + update: function (identifier, data) { + var path; + if (identifier && data) { + path = this.constructPath(constants.LOCATIONS, identifier); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request. A location identifier and a data object are required'); + } + }, + + /** + * Delete a location + * @param {String|Integer} identifier A Robin location identifier + * @return {Function} A Promise + */ + delete: function (identifier) { + var path; + if (identifier) { + path = this.constructPath(constants.LOCATIONS, identifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A location identifier is required.'); + } + }, + + /** + * Location Spaces + * @type {Object} + */ + spaces: { + /** + * Get all the spaces in a location or a particular space in a location identified by `spaceIdentifier` + * @param {String|Integer} locationIdentifier A Robin channel identifier + * @param {String|Integer|undefined} spaceIdentifier A Robin channel data point identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (locationIdentifier, spaceIdentifier, params) { + var path; + if (locationIdentifier) { + path = this.constructPath(constants.LOCATIONS, locationIdentifier, constants.SPACES, spaceIdentifier); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A location identifier is required.'); + } + }, + + /** + * Add space to a location + * @param {String|Integer} locationIdentifier A Robin location identifier + * @param {Object} data A querystring object + * @return {Function} A Promise + */ + add: function (locationIdentifier, data) { + var path; + if (locationIdentifier) { + path = this.constructPath(constants.LOCATIONS, locationIdentifier, constants.SPACES); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A location identifier is required.'); + } + } + }, + + /** + * Location Presence + * @type {Object} + */ + presence: { + /** + * Get all the current presence for all the spaces in a location + * @param {String|Integer} locationIdentifier A Robin channel identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (locationIdentifier, params) { + var path; + if (locationIdentifier) { + path = this.constructPath(constants.LOCATIONS, locationIdentifier, constants.PRESENCE); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A location identifier is required.'); + } + } + } +}; diff --git a/lib/api/modules/me.js b/lib/api/modules/me.js new file mode 100644 index 0000000..95f9b51 --- /dev/null +++ b/lib/api/modules/me.js @@ -0,0 +1,323 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get /me, which is an alias for /users/:id, where :id is the id of the current user + * @param {Object} params A querystring object + * @return {Function} A promise + */ + get: function (params) { + var path = this.constructPath(constants.ME); + return this.Core.GET(path); + }, + + /** + * Update the current user + * @param {Object} data A data object + * @return {Function} A promise + */ + update: function (data) { + if (data) { + var path = this.constructPath(constants.ME); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request. A data object is required'); + } + }, + + /** + * Update the current user's email + * @param {Object} data A data object + * @return {Function} A promise + */ + updateEmail: function (data) { + if (data) { + var path = this.constructPath(constants.ME, constants.EMAIL); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request. A data object is required'); + } + }, + + /** + * Update the current user's password + * @param {Object} data A data object + * @return {Function} A promise + */ + changePassword: function (data) { + if (data) { + var path = this.constructPath(constants.ME, constants.PASSWORD); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request. A data object is required'); + } + }, + + /** + * Delete the current user + * @return {Function} A Promise + */ + delete: function () { + var path = this.constructPath(constants.ME); + return this.Core.DELETE(path); + }, + + /** + * User's organizations + * @type {Object} + */ + organizations: { + /** + * Get a user's organizations + * @param {Object} params A querystring object + * @return {Function} A promise + */ + get: function (params) { + var path = this.constructPath(constants.ME, constants.ORGANIZATIONS); + return this.Core.GET(path); + } + }, + + /** + * User's app/account authorizations + * @type {Object} + */ + authorizations: { + /** + * Get a user's authorizations + * @param {Object} params A querystring object + * @return {Function} A promise + */ + get: function (params) { + var path = this.constructPath(constants.ME, constants.AUTHORIZATIONS); + return this.Core.GET(path); + }, + + /** + * Add an authorization for the current user + * @param {String} type Authorization Type + * @param {String} identifier Authorization Identifier + * @param {Object} data A data object + * @return {Function} A promise + */ + add: function (type, identifier, data) { + var path; + if (type && identifier && data) { + path = this.constructPath(constants.ME, constants.AUTHORIZATIONS, type, identifier); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: Authorization type and identifier and data payload are required'); + } + }, + + /** + * Create an authorization for the current user + * @param {String} type Authorization Type + * @param {String} identifier Authorization Identifier + * @return {Function} A promise + */ + delete: function (type, identifier) { + var path; + if (type && identifier) { + path = this.constructPath(constants.ME, constants.AUTHORIZATIONS, type, identifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: Authorization type and identifier are both required'); + } + } + }, + + /** + * User's devices + * @type {Object} + */ + devices: { + /** + * Get a user's devices + * @param {Object} params A querystring object + * @return {Function} A promise + */ + get: function (params) { + var path = this.constructPath(constants.ME, constants.DEVICES); + return this.Core.GET(path); + }, + + /** + * Add a device for the current user + * @param {Object} data A data object + * @return {Function} A promise + */ + add: function (data) { + var path; + if (data) { + path = this.constructPath(constants.ME, constants.DEVICES); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A data object is required'); + } + } + }, + + /** + * User's projects + * @type {Object} + */ + projects: { + /** + * Get a user's projects + * @param {Object} params A querystring object + * @return {Function} A promise + */ + get: function (params) { + var path = this.constructPath(constants.ME, constants.PROJECTS); + return this.Core.GET(path); + }, + + /** + * Add a device for the current user + * @param {Object} data A data object + * @return {Function} A promise + */ + add: function (data) { + var path; + if (data) { + path = this.constructPath(constants.ME, constants.PROJECTS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A data object is required'); + } + } + }, + + /** + * User's channels + * @type {Object} + */ + channels: { + /** + * Get a user's channels + * @param {Object} params A querystring object + * @return {Function} A promise + */ + get: function (params) { + var path = this.constructPath(constants.ME, constants.CHANNELS); + return this.Core.GET(path); + }, + + /** + * Create a channel for the current user + * @param {Object} data A data object + * @return {Function} A promise + */ + create: function (data) { + var path; + if (data) { + path = this.constructPath(constants.ME, constants.CHANNELS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A data object is required'); + } + }, + + /** + * Add a channel for the current user + * @param {String|Integer} channelIdentifier A robin channel identifier + * @return {Function} A promise + */ + add: function (channelIdentifier) { + var path; + if (channelIdentifier) { + path = this.constructPath(constants.ME, constants.CHANNELS, channelIdentifier); + return this.Core.PUT(path); + } else { + return this.rejectRequest('Bad Request: A channel identifier is required'); + } + }, + + /** + * Delete a channel for the current user + * @param {String|Integer} channelIdentifier A robin channel identifier + * @return {Function} A promise + */ + delete: function (channelIdentifier) { + var path; + if (channelIdentifier) { + path = this.constructPath(constants.ME, constants.CHANNELS, channelIdentifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A channel identifier is required'); + } + } + }, + + /** + * User's identifiers + * @type {Object} + */ + identifiers: { + /** + * Get a user's identifiers + * @param {Object} params A querystring object + * @return {Function} A promise + */ + get: function (params) { + var path = this.constructPath(constants.ME, constants.IDENTIFIERS); + return this.Core.GET(path); + }, + + /** + * Create an identifier for the current user + * @param {Object} data A data object + * @return {Function} A promise + */ + create: function (data) { + var path; + if (data) { + path = this.constructPath(constants.ME, constants.IDENTIFIERS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A data object is required'); + } + }, + + /** + * Add an identifier for the current user + * @param {String|Integer} identifierUrn A robin identifier urn + * @return {Function} A promise + */ + add: function (identifierUrn) { + var path; + if (identifierUrn) { + path = this.constructPath(constants.ME, constants.IDENTIFIERS, identifierUrn); + return this.Core.PUT(path); + } else { + return this.rejectRequest('Bad Request: An identifier urn is required'); + } + }, + + /** + * Delete an identifier for the current user + * @param {String|Integer} identifierUrn A robin identifier urn + * @return {Function} A promise + */ + delete: function (identifierUrn) { + var path; + if (identifierUrn) { + path = this.constructPath(constants.ME, constants.IDENTIFIERS, identifierUrn); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: An identifier urn is required'); + } + } + } +}; diff --git a/lib/api/modules/organizations.js b/lib/api/modules/organizations.js new file mode 100644 index 0000000..c3afa80 --- /dev/null +++ b/lib/api/modules/organizations.js @@ -0,0 +1,390 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get all the organizations or a particular organization identified by the `orgIdOrSlug` parameter + * @param {String|Integer|undefined} orgIdOrSlug A Robin organization id or slug + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (orgIdOrSlug, params) { + var path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug); + return this.Core.GET(path, params); + }, + + /** + * Create an organization + * @param {Object} data A data object + * @return {Function} A Promise + */ + create: function (data) { + var path; + if (data) { + path = this.constructPath(constants.ORGANIZATIONS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A data object is required'); + } + }, + + /** + * Update an organization + * @param {String|Integer|undefined} orgIdOrSlug A Robin organization id or slug + * @param {Object} data A data object + * @return {Function} A Promise + */ + update: function (orgIdOrSlug, data) { + var path; + if (orgIdOrSlug && data) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request: An organization id or slug and a data object are required.'); + } + }, + + /** + * Delete an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @return {Function} A Promise + */ + delete: function (orgIdOrSlug) { + var path; + if (orgIdOrSlug) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: An organization id or slug is required.'); + } + }, + + /** + * Organization Users + * @type {Object} + */ + users: { + /** + * Get an organizations users or a particular user identified by `userId` + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {String|Integer|undefined} userId A Robin user id + * @param {Object|undefined} params A querystring object + * @return {Function} A promise + */ + get: function (orgIdOrSlug, userId, params) { + var path; + if (orgIdOrSlug) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.USERS, userId); + return this.Core.GET(path); + } else { + return this.rejectRequest('Bad Request: An organization id or slug is required.'); + } + }, + + /** + * Add a user to an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {String|Integer} userId A Robin user id + * @return {Function} A promise + */ + add: function (orgIdOrSlug, userId) { + var path; + if (orgIdOrSlug && userId) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.USERS, userId); + return this.Core.PUT(path); + } else { + return this.rejectRequest('Bad Request: An organization id or slug and a user id is required.'); + } + }, + + /** + * Update a user in an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {String|Integer} userId A Robin user id + * @param {Object} data A data object + * @return {Function} A promise + */ + update: function (orgIdOrSlug, userId, data) { + var path, + rejectMsg; + if (orgIdOrSlug && userId && data) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.USERS, userId); + return this.Core.PATCH(path, data); + } else { + rejectMsg = 'Bad Request: An organization id or slug and a user id and data is required.'; + return this.rejectRequest(rejectMsg); + } + }, + + /** + * Remove a user from an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {String|Integer} userId A Robin user id + * @return {Function} A promise + */ + delete: function (orgIdOrSlug, userId) { + var path; + if (orgIdOrSlug && userId) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.USERS, userId); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: An organization id or slug and a user id is required.'); + } + } + }, + + /** + * Organization Managers + * @type {Object} + */ + managers: { + /** + * Get an organizations managers + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object|undefined} params A querystring object + * @return {Function} A promise + */ + get: function (orgIdOrSlug, params) { + var path; + if (orgIdOrSlug) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.MANAGERS); + return this.Core.GET(path); + } else { + return this.rejectRequest('Bad Request: An organization id or slug is required.'); + } + } + }, + + /** + * Organization Apps + * @type {Object} + */ + apps: { + /** + * Get an organizations apps + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object|undefined} params A querystring object + * @return {Function} A promise + */ + get: function (orgIdOrSlug, params) { + var path; + if (orgIdOrSlug) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.APPS); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: An organization id or slug is required.'); + } + }, + + /** + * Add an app for an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object} data A data object + * @return {Function} A Promise + */ + add: function (orgIdOrSlug, data) { + var path; + if (orgIdOrSlug && data) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.APPS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: An organization id or slug and a data object is required.'); + } + } + }, + + /** + * Organization Devices + * @type {Object} + */ + devices: { + /** + * Get an organizations devices + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object|undefined} params A querystring object + * @return {Function} A promise + */ + get: function (orgIdOrSlug, params) { + var path; + if (orgIdOrSlug) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.DEVICES); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: An organization id or slug is required.'); + } + }, + + /** + * Add a device for an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object} data A data object + * @return {Function} A Promise + */ + add: function (orgIdOrSlug, data) { + var path; + if (orgIdOrSlug && data) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.DEVICES); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: An organization id or slug and a data object is required.'); + } + } + }, + + /** + * Organization Projects + * @type {Object} + */ + projects: { + /** + * Get an organizations projects + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object|undefined} params A querystring object + * @return {Function} A promise + */ + get: function (orgIdOrSlug, params) { + var path; + if (orgIdOrSlug) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.PROJECTS); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: An organization id or slug is required.'); + } + }, + + /** + * Add a project for an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object} data A data object + * @return {Function} A Promise + */ + add: function (orgIdOrSlug, data) { + var path; + if (orgIdOrSlug && data) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.PROJECTS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: An organization id or slug and a data object is required.'); + } + } + }, + + /** + * Organization Channels + * @type {Object} + */ + channels: { + /** + * Get an organizations channels + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object|undefined} params A querystring object + * @return {Function} A promise + */ + get: function (orgIdOrSlug, params) { + var path; + if (orgIdOrSlug) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.USERS); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: An organization id or slug is required.'); + } + }, + + /** + * Add a channel to an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {String|Integer} channelIdentifier A Robin user id + * @return {Function} A promise + */ + add: function (orgIdOrSlug, channelIdentifier) { + var path; + if (orgIdOrSlug && channelIdentifier) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.USERS, channelIdentifier); + return this.Core.PUT(path); + } else { + return this.rejectRequest('Bad Request: An organization id or slug and a channel id is required.'); + } + }, + + /** + * Create a channel in an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object} data A data object + * @return {Function} A promise + */ + create: function (orgIdOrSlug, data) { + var path, + rejectMsg; + if (orgIdOrSlug && data) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.USERS); + return this.Core.POST(path, data); + } else { + rejectMsg = 'Bad Request: An organization id or slug and data is required.'; + return this.rejectRequest(rejectMsg); + } + }, + + /** + * Remove a user from an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {String|Integer} channelIdentifier A Robin user id + * @return {Function} A promise + */ + delete: function (orgIdOrSlug, channelIdentifier) { + var path; + if (orgIdOrSlug && channelIdentifier) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.USERS, channelIdentifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: An organization id or slug and a channel id is required.'); + } + } + }, + + /** + * Organization Locations + * @type {Object} + */ + locations: { + /** + * Get all of an organizations locations + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object|undefined} params A querystring object + * @return {Function} A promise + */ + get: function (orgIdOrSlug, params) { + var path; + if (orgIdOrSlug) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.LOCATIONS); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: An organization id or slug is required.'); + } + }, + + /** + * Add a location to an organization + * @param {String|Integer} orgIdOrSlug A Robin organization id or slug + * @param {Object} data A data object + * @return {Function} A Promise + */ + add: function (orgIdOrSlug, data) { + var path; + if (orgIdOrSlug && data) { + path = this.constructPath(constants.ORGANIZATIONS, orgIdOrSlug, constants.LOCATIONS); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: An organization id or slug and a data object is required.'); + } + } + } +}; diff --git a/lib/api/modules/spaces.js b/lib/api/modules/spaces.js new file mode 100644 index 0000000..c2cac7a --- /dev/null +++ b/lib/api/modules/spaces.js @@ -0,0 +1,180 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var constants = require('../../util').constants; + +module.exports = { + /** + * Get all spaces or a particular space identified by `spaceIdentifier` + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A promise + */ + get: function (spaceIdentifier, params) { + var path = this.constructPath(constants.SPACES, spaceIdentifier); + return this.Core.GET(path, params); + }, + + /** + * Update a space + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @param {Object} data A data object + * @return {Function} A promise + */ + update: function (spaceIdentifier, data) { + var path; + if (data) { + path = this.constructPath(constants.SPACES, spaceIdentifier); + return this.Core.PATCH(path, data); + } else { + return this.rejectRequest('Bad Request: Space data is required'); + } + }, + + /** + * Delete a space + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @return {Function} A promise + */ + delete: function (spaceIdentifier) { + var path; + if (spaceIdentifier) { + path = this.constructPath(constants.SPACES, spaceIdentifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A space identifier is required'); + } + }, + + /** + * Space Devices + * @type {Object} + */ + devices: { + /** + * Get all the devices for a space or a particular device identified by `deviceIdentifier` + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @param {String|Integer|undefined} deviceIdentifier A Robin device identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (spaceIdentifier, deviceIdentifier, params) { + var path; + if (spaceIdentifier) { + path = this.constructPath(constants.SPACES, spaceIdentifier, constants.DEVICES, deviceIdentifier); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A space identifier is required.'); + } + }, + + /** + * Create a device in a space + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @param {Object} data A data object + * @return {Function} A Promise + */ + create: function (spaceIdentifier, data) { + var path; + if (spaceIdentifier && data) { + path = this.constructPath(constants.SPACES, spaceIdentifier, constants.DEVICES); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A space identifier and device data are required'); + } + }, + + /** + * Add a device to a space + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @return {Function} A Promise + */ + add: function (spaceIdentifier, deviceIdentifier) { + var path; + if (spaceIdentifier && deviceIdentifier) { + path = this.constructPath(constants.SPACES, spaceIdentifier, constants.DEVICES, deviceIdentifier); + return this.Core.PUT(path); + } else { + return this.rejectRequest('Bad Request: A space identifier and device identifier are required'); + } + }, + + /** + * Delete a device from a space + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @param {String|Integer} deviceIdentifier A Robin device identifier + * @return {Function} A Promise + */ + delete: function (spaceIdentifier, deviceIdentifier) { + var path; + if (spaceIdentifier && deviceIdentifier) { + path = this.constructPath(constants.SPACES, spaceIdentifier, constants.DEVICES, deviceIdentifier); + return this.Core.DELETE(path); + } else { + return this.rejectRequest('Bad Request: A space identifier and device identifier are required'); + } + } + }, + + /** + * Space presence + * @type {Object} + */ + presence: { + /** + * Get all the presence for a space + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @param {Object|undefined} params A querystring object + * @return {Function} A Promise + */ + get: function (spaceIdentifier, params) { + var path; + if (spaceIdentifier) { + path = this.constructPath(constants.SPACES, spaceIdentifier, constants.PRESENCE); + return this.Core.GET(path, params); + } else { + return this.rejectRequest('Bad Request: A space identifier is required.'); + } + }, + + /** + * Add presence to a space + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @param {Object} data A data payload + * @return {Function} A Promise + */ + add: function (spaceIdentifier, data) { + var path; + if (spaceIdentifier && data) { + path = this.constructPath(constants.SPACES, spaceIdentifier, constants.PRESENCE); + return this.Core.POST(path, data); + } else { + return this.rejectRequest('Bad Request: A space identifier and data object are required'); + } + }, + + /** + * Delete a device from a space + * @param {String|Integer} spaceIdentifier A Robin space identifier + * @param {Object} data A data payload + * @return {Function} A Promise + */ + delete: function (spaceIdentifier, data) { + var path; + if (spaceIdentifier && data) { + path = this.constructPath(constants.SPACES, spaceIdentifier, constants.PRESENCE); + return this.Core.DELETE(path, data); + } else { + return this.rejectRequest('Bad Request: A space identifier and data object are required'); + } + } + } +}; diff --git a/lib/api/requestBase.js b/lib/api/requestBase.js new file mode 100644 index 0000000..6adac0d --- /dev/null +++ b/lib/api/requestBase.js @@ -0,0 +1,247 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Promise = require('bluebird'), + error = require('../error'), + rbnUtil = require('../util'), + request = Promise.promisify(require('request')); + +/** + * This is a somewhat abstract base class for modules + * Methods implemented: + * * get + * * add + * * update + * * + * @return {Function} A constructor + */ + +module.exports = (function () { + /** + * The Request Base Class Constructor. It contains private and privileged variables + * and methods. + */ + function RequestBase () { + // `_baseUrl` and `self` are private variables + var _baseUrl, + self = this; + + this.getBaseUrl = function () { + if (_baseUrl) { + return _baseUrl; + } + }; + + this.setBaseUrl = function (baseUrl) { + _baseUrl = baseUrl; + }; + + /** + * Checks to make sure the request was a success + * @param {Object} res A response object + * @return {Boolean} A boolean denoting whether the request was a success + */ + this.isSuccess = function (res) { + var success = function (status) { + return status >= 200 && status < 300; + }; + if (res && success(res.statusCode)) { + return true; + } + return false; + }; + + this.buildOptions = function (path, method, payload) { + var options, + accessToken = this.getAccessToken(), + relayIdentifier = this.getRelayIdentifier(), + baseUrl = self.getBaseUrl(), + url = self.constructUrl(baseUrl, path); + + options = { + uri: url, + method: method, + headers: {} + }; + + if (accessToken) { + options.headers.Authorization = 'Access-Token ' + accessToken; + } else { + throw new Error('The required Access Token is missing or malformed'); + } + + if (relayIdentifier) { + options.headers['Relay-Identifier'] = relayIdentifier; + } + + if (method && method.toUpperCase() === 'GET') { + options.qs = payload; + } else { + options.json = payload; + } + return options; + }; + + this.constructUrl = function (url, path) { + if (url.slice(-1) === '/' && path.slice(0, 1) === '/') { + return url.slice(0, -1) + path; + } else if (url.slice(-1) !== '/' && path.slice(0, 1) !== '/') { + return url + '/' + path; + } + return url + path; + }; + + this.sendRequest = function (path, method, payload) { + var options = self.buildOptions(path, method, payload), + reqHandle = request(options).spread(function (response, body) { + var resp = {}, + responseBody; + try { + if (typeof body === 'string') { + responseBody = JSON.parse(body); + } else { + responseBody = body; + } + } catch (err) { + throw new Error('An error occurred parsing the following response from the server: ' + body); + } + resp.body = responseBody; + if (self.isSuccess(response)) { + resp.getData = function () { + return resp.body.data; + }; + resp.isEmpty = function () { + return Object.getOwnPropertyNames(resp.getData()).length === 0; + }; + resp.getDataLength = function () { + var data = resp.getData(); + if (resp.isEmpty()) { + return 0; + } else if (!(data instanceof Array)) { + return 1; + } else { + return data.length; + } + }; + if (method === 'GET' && resp.body.paging) { + var pageSize, thisPage, prevPage, nextPage, pageFunc; + pageSize = resp.body.paging['per_page']; + thisPage = resp.body.paging.page; + prevPage = (thisPage === 1 ? thisPage : thisPage - 1); + nextPage = thisPage + 1; + pageFunc = function (pageNum) { + var _pageNum; + _pageNum = pageNum; + return function () { + payload = (payload === undefined || !payload) ? {} : payload; + payload.page = _pageNum; + payload['per_page'] = pageSize; + return self.sendRequest(path, method, payload); + }; + }; + resp.nextPage = pageFunc(nextPage); + resp.prevPage = pageFunc(prevPage); + } + return resp; + } else { + throw new error.ApiError(resp); + } + }); + return reqHandle; + }; + } + + /** + * Implement HTTP request methods, to make requests easier + */ + + RequestBase.prototype.GET = function (path, params) { + return this.sendRequest(path, 'GET', params); + }; + + RequestBase.prototype.HEAD = function (path, params) { + return this.sendRequest(path, 'HEAD', params); + }; + + RequestBase.prototype.POST = function (path, data) { + return this.sendRequest(path, 'POST', data); + }; + + RequestBase.prototype.PUT = function (path, data) { + return this.sendRequest(path, 'PUT', data); + }; + + RequestBase.prototype.PATCH = function (path, data) { + return this.sendRequest(path, 'PATCH', data); + }; + + RequestBase.prototype.DELETE = function (path, data) { + return this.sendRequest(path, 'DELETE', data); + }; + + RequestBase.prototype.OPTIONS = function (path, data) { + return this.sendRequest(path, 'OPTIONS', data); + }; + + /** + * Paginate through all items in a request and resolve them all as a promise + * @param {Object|String} pathOrPromise The path of the items you wish to GET - a string or a Promise + * @param {Object|undefined} params Optional query parameters + * @return {Promise} A promise that resolves with an array of all items in the API. + */ + RequestBase.prototype.all = function (pathOrPromise, params) { + var promiseFunc, + allApiItems = [], + apiPromise, + path; + if (rbnUtil.isObject(pathOrPromise)) { + if (pathOrPromise instanceof Promise) { + apiPromise = pathOrPromise; + } else { + throw new TypeError('Path must be a Promise Object or a String'); + } + } else if (typeof pathOrPromise === 'string') { + path = pathOrPromise; + if (params && params['per_page']) { + delete params['per_page']; + } + apiPromise = this.GET(path, params); + } else { + throw new TypeError('Path must be a Promise Object or a String'); + } + promiseFunc = function (promise) { + return promise.then(function (resp) { + var data = resp.getData(); + if (data) { + if (rbnUtil.isArray(data)) { + Array.prototype.push.apply(allApiItems, data); + } else if (rbnUtil.isObject(data)) { + allApiItems.push(data); + } else { + throw new Error('The server response was malformed'); + } + } else { + return Promise.resolve(allApiItems); + } + if (resp.nextPage) { + return promiseFunc(resp.nextPage()); + } else { + return Promise.resolve(allApiItems); + } + }.bind(this)) + .catch(function (err) { + throw err; + }); + }; + return promiseFunc(apiPromise); + }; + + return RequestBase; +}).call(this); diff --git a/lib/error.js b/lib/error.js new file mode 100644 index 0000000..35413dc --- /dev/null +++ b/lib/error.js @@ -0,0 +1,38 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + * When this module is required in JavaScript source, it will allow a + * child class to extend a parent class, by invoking the function exported + * by this module. + */ + +var util = require('util'), + rbnUtil = require('./util'); + +function ApiError (err) { + ApiError.super_.call(this); + if (!rbnUtil.isObject(err)) { + this.message = err.toString(); + } else { + if (!rbnUtil.isObject(err.body)) { + this.message = err.toString(); + } else { + if (!rbnUtil.isObject(err.body.meta)) { + this.message = err.toString(); + } else { + this['status_code'] = err.body.meta['status_code']; + this.status = err.body.meta.status; + this.message = err.body.meta.message; + this.moreInfo = err.body.meta['more_info']; + } + } + } +} +util.inherits(ApiError, Error); + +module.exports.ApiError = ApiError; diff --git a/lib/grid/base.js b/lib/grid/base.js new file mode 100644 index 0000000..a0ee17e --- /dev/null +++ b/lib/grid/base.js @@ -0,0 +1,137 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var RobinGridBase, + util = require('util'), + faye = require('faye'), + EventEmitter = require('events').EventEmitter; + +/* + * We're overriding the faye channel names regex to allow `:` characters through + */ +faye.Grammar.CHANNEL_NAME = new RegExp( + '^\\/(((([a-z]|[A-Z])|[0-9])|(\\:|\\-|\\_|\\!|\\~|\\(|\\)|\\$|\\@)))' + + '+(\\/(((([a-z]|[A-Z])|[0-9])|(\\:|\\-|\\_|\\!|\\~|\\(|\\)|\\$|\\@)))+)*$' + ); +faye.Grammar.CHANNEL_PATTERN = new RegExp( + '^(\\/(((([a-z]|[A-Z])|[0-9])|' + + '(\\:|\\-|\\_|\\!|\\~|\\(|\\)|\\$|\\@)))+)*\\/\\*{1,2}$' + ); + +/** + * This is the base class for the Robin Grid integration. This should be the + * place where authentication and authorization is set up. + * @return {Function} The Robin Grid base class object. + */ +RobinGridBase = (function (_super) { + function _RobinGridBase () { + + } + + util.inherits(_RobinGridBase, _super); + + _RobinGridBase.prototype.setGridUrl = function (gridUrl) { + if (gridUrl) { + this._gridUrl = gridUrl; + } + }; + + _RobinGridBase.prototype.getGridUrl = function () { + if (this._gridUrl) { + return this._gridUrl; + } + }; + + _RobinGridBase.prototype.setAccessToken = function (token) { + if (token) { + this._accessToken = token; + } + }; + + _RobinGridBase.prototype.getAccessToken = function () { + if (this._accessToken) { + return this._accessToken; + } + }; + + _RobinGridBase.prototype.setRelayIdentifier = function (relayIdentifier) { + if (relayIdentifier) { + this._relayIdentifier = relayIdentifier; + } + }; + + _RobinGridBase.prototype.getRelayIdentifier = function () { + if (this._relayIdentifier) { + return this._relayIdentifier; + } + }; + + _RobinGridBase.prototype.setupGridMessageHandler = function () { + var incomingExt, + outgoingExt, + gridUrl = this.getGridUrl(); + + if (gridUrl) { + this.gridClient = new faye.Client(gridUrl); + this.gridClient.on('transport:down', function () { + this.emit('error', 'Grid transport is down.'); + }.bind(this)); + incomingExt = function (message, callback) { + try { + var messageChannel = message.channel, + messageChannelArr, + messageEndpoint, + messageIdentifier, + messageType, + messageData; + if (message.data) { + messageChannelArr = messageChannel.replace(/^\//, '').split('/'); + messageEndpoint = messageChannelArr[0]; + messageIdentifier = messageChannelArr[1]; + messageType = messageChannelArr[2]; + messageData = message.data; + message.data = { + data: messageData + }; + message.data.ext = { + channel: messageChannel, + endpoint: messageEndpoint, + identifier: messageIdentifier, + type: messageType + }; + } + callback(message); + } catch (err) { + message.error = err; + callback(message); + } + }.bind(this); + outgoingExt = function (message, callback) { + // Add ext field if it's not present + if (!message.ext) { + message.ext = {}; + } + message.ext.accessToken = this.getAccessToken(); + message.ext.relayIdentifier = this.getRelayIdentifier(); + callback(message); + }.bind(this); + this.gridClient.addExtension({ + incoming: incomingExt, + outgoing: outgoingExt + }); + } else { + throw new Error('A Grid url is required'); + } + }; + + return _RobinGridBase; +}).apply(this, [EventEmitter]); + +module.exports = RobinGridBase; diff --git a/lib/grid/connection.js b/lib/grid/connection.js new file mode 100644 index 0000000..a8862cf --- /dev/null +++ b/lib/grid/connection.js @@ -0,0 +1,118 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +module.exports = (function () { + var util = require('util'), + RbnUtil = require('../util'), + EventEmitter = require('events').EventEmitter; + + /** + * This connection class is instantiated as an object when you call the `connect` + * method on a grid module. It sets up the connection to a specific point entity + * on the grid and emits messages as they come in. You can also sent messages of a specific type + * to this connection. + * @param {Object} gridModule A previously instantiated `grid` module. + */ + function Connection (gridModule) { + try { + Connection.super_.apply(this, arguments); + RbnUtil.__copyProperties(this, gridModule); + this.validate(); + this.connectionStub = '/' + this.endpoint + '/' + this.identifier; + } catch (err) { + throw err; + } + } + + util.inherits(Connection, EventEmitter); + + /** + * Validates the endpoint and identifier we're using + */ + Connection.prototype.validate = function () { + if (!this.endpoint) { + throw new Error('The supplied endpoint is invalid or malformed.'); + } + if (!this.identifier) { + throw new Error('The supplied identifier is invalid or malformed.'); + } + }; + + /** + * Creates the connection with the grid and listens for messages. Emits each message + * based on the `type` of that message. + * @param {Function|undefined} callback An optional callback to execute. After the message is sent. + * `callback` should expect arguments of the form (error, response). + */ + Connection.prototype.listen = function (callback) { + //Asterisk listens to all message types, so we can emit for different message types + var listeningChannel = this.connectionStub + '/*'; + this.connection = this.gridClient.subscribe(listeningChannel, this.messageCallback.bind(this)); + this.connection.then(function (resp) { + if (callback) { + callback(null, resp); + } + }, function (err) { + if (callback) { + callback(err); + } + }); + }; + + /** + * Function to call every time a message is received on this connection + * @param {Object} message A message received from this connection on the Grid. + */ + Connection.prototype.messageCallback = function (message) { + this.emit(message.ext.type, message.data); + }; + + /** + * Stops emitting messages from this connection + * @param {Function} callback [description] + */ + Connection.prototype.stop = function (callback) { + if (this.connection) { + this.connection.cancel(); + if (callback) { + callback(null); + } + } else { + if (callback) { + callback('No connection found'); + } + } + }; + + /** + * Send a message along the grid + * @param {String} messageType The type of message to send. + * This will be used when `emitting` to listeners. + * @param {Object} message The message payload to send along the grid + * @param {Function|undefined} callback An optional callback to execute. After the message is sent. + * `callback` should expect arguments of the form (error, response). + */ + Connection.prototype.send = function (messageType, message, callback) { + var published, + sendingChannel = this.connectionStub + '/' + messageType; + published = this.gridClient.publish(sendingChannel, message); + published.then(function (resp) { + if (callback) { + callback(null, resp); + } + }, function (err) { + if (callback) { + callback(err); + } + }); + }; + + return Connection; +}).call(this); diff --git a/lib/grid/grid.js b/lib/grid/grid.js new file mode 100644 index 0000000..dfeb451 --- /dev/null +++ b/lib/grid/grid.js @@ -0,0 +1,52 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var RobinGridBase = require('./base'), + util = require('util'), + _gridConnectionModules; + +// Initialize these there so that they're available to the RobinGrid when it's constructed +_gridConnectionModules = { + channel: require('./modules/channel'), + device: require('./modules/device') +}; + +/** + * This is the Robin Grid class + * It exposes various module endpoints and allows connections to be made to each of these. + * @return {Function} The Robin Grid object. + */ +module.exports = (function () { + function RobinGrid (accessToken, baseUrl) { + RobinGrid.super_.apply(this, arguments); + this.subscriptions = {}; + this.setAccessToken(accessToken); + this.setGridUrl(baseUrl); + this.setupGridMessageHandler(); + this.loadConnectionModules(); + } + + util.inherits(RobinGrid, RobinGridBase); + + /** + * Instantiates each Robin Grid module class + */ + RobinGrid.prototype.loadConnectionModules = function () { + var gridModuleKey, + GridModule; + + for (gridModuleKey in _gridConnectionModules) { + GridModule = _gridConnectionModules[gridModuleKey]; + this[gridModuleKey] = new GridModule(this); + } + }; + + return RobinGrid; +}).call(); diff --git a/lib/grid/index.js b/lib/grid/index.js new file mode 100644 index 0000000..aed73a5 --- /dev/null +++ b/lib/grid/index.js @@ -0,0 +1,13 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var RobinGrid = require('./grid'); + +module.exports = RobinGrid; diff --git a/lib/grid/modules/channel.js b/lib/grid/modules/channel.js new file mode 100644 index 0000000..7f571ff --- /dev/null +++ b/lib/grid/modules/channel.js @@ -0,0 +1,37 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Connection = require('../connection'), + util = require('util'), + RbnUtil = require('../../util'); + +module.exports = (function () { + function Channel (grid) { + if (grid) { + RbnUtil.__copyProperties(this, grid); + } else { + throw new Error('An instance of the Grid is required'); + } + } + + util.inherits(Channel, Connection); + + Channel.prototype.connect = function (identifier) { + if (identifier) { + this.endpoint = 'channels'; + this.identifier = identifier; + return new Connection(this); + } else { + throw new TypeError('An the identifier of the entity to which you wish to connect must be supplied'); + } + }; + + return Channel; +}).call(this); diff --git a/lib/grid/modules/device.js b/lib/grid/modules/device.js new file mode 100644 index 0000000..3e95167 --- /dev/null +++ b/lib/grid/modules/device.js @@ -0,0 +1,37 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Connection = require('../connection'), + util = require('util'), + RbnUtil = require('../../util'); + +module.exports = (function () { + function Device (grid) { + if (grid) { + RbnUtil.__copyProperties(this, grid); + } else { + throw new Error('An instance of the Grid is required'); + } + } + + util.inherits(Device, Connection); + + Device.prototype.connect = function (identifier) { + if (identifier) { + this.endpoint = 'devices'; + this.identifier = identifier; + return new Connection(this); + } else { + throw new TypeError('An the identifier of the entity to which you wish to connect must be supplied'); + } + }; + + return Device; +}).call(this); diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..8c0b76c --- /dev/null +++ b/lib/util.js @@ -0,0 +1,247 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + * When this module is required in JavaScript source, it will allow a + * child class to extend a parent class, by invoking the function exported + * by this module. + */ + +/** + * A function that copies all the properties of one object into + * another + * @param {Object} child The object receiving the properties + * @param {Object} parent The object whose properties are being copied + */ +var __copyProperties = function (child, parent) { + // This means we have a previously instantiated class, + // so just pass in the properties + if (parent !== child) { + for (var key in parent) { + if (parent.hasOwnProperty(key)) { + child[key] = parent[key]; + } + } + // child.super_ = parent; + } +}; + +module.exports.__copyProperties = __copyProperties; + +/** + * Determines whether a value is a function + * @param {*} value A variable of any type + * @return {Boolean} A boolean denoting whether `value` is a function or not. + */ +var isFunction = function (value) { + return value && typeof value === 'function'; +}; + +module.exports.isFunction = isFunction; + +/** + * Determines whether a value is a true object + * @param {*} value A variable of any type + * @return {Boolean} A boolean denoting whether `value` is an object + */ +var isObject = function (value) { + return value && Object.prototype.toString.call(value) === '[object Object]'; +}; + +module.exports.isObject = isObject; + +/** + * Determines whether a value is an array + * @param {*} value A variable of any type + * @return {Boolean} A boolean denoting whether `value` is an array + */ +var isArray = function (value) { + return (value && typeof value === 'object' && typeof value.length === 'number' && + toString.call(value) === '[object Array]') || false; +}; + +module.exports.isArray = isArray; + +/** + * Apply a scope to all object properties that are functions + * @param {Object} object An object + * @param {Object} scope A scope (e.g. `this`) + */ +var applyScope = function (object, scope) { + var key, + value; + for (key in object) { + if (object.hasOwnProperty(key)) { + value = object[key]; + if (isFunction(value)) { + object[key] = value.bind(scope); + } else if (isObject(value)) { + applyScope(value, scope); + } + } + } + return object; +}; + +module.exports.applyScope = applyScope; + +/** + * Clone a javascript object + * @param {Object} object The object to be cloned + * @return {Object} A clone of the object argument. + */ +var cloneObject = function (object) { + var clone; + if (object === null || !isObject(object)) { + return object; + } + clone = object.constructor(); + __copyProperties(clone, object); + return clone; +}; + +module.exports.cloneObject = cloneObject; + +/** + * An object that contains useful constants for the SDK + * @type {Object} + */ +var constants = { + ACCOUNT: 'account', + ACCOUNTS: 'accounts', + APP: 'app', + APPS: 'apps', + AUTH: 'auth', + AUTHORIZATIONS: 'authorizations', + CHANNELS: 'channels', + DATA: 'data', + DEVICE_MANIFESTS: 'device-manifests', + DEVICES: 'devices', + EMAIL: 'email', + EVENTS: 'events', + FEEDS: 'feeds', + IDENTIFIERS: 'identifiers', + LOCATIONS: 'locations', + MANAGERS: 'managers', + ME: 'me', + ORGANIZATIONS: 'organizations', + PASSWORD: 'password', + PERSONAS: 'personas', + PRESENCE: 'presence', + PROJECTS: 'projects', + SPACES: 'spaces', + TRIGGERS: 'triggers', + USERS: 'users', + VALID_APP_ENDPOINT_NAMES: ['api', 'apps', 'grid'], + VALID_APP_MAPPING: { + 'core': 'api', + 'grid': 'grid', + 'places': 'apps' + }, + VALID_APP_NAMES: ['core', 'places', 'grid'], + VALID_ENVS: ['test', 'staging', 'production'] +}; + +module.exports.constants = constants; + +/** + * Construct a robin url for the api, grid or whatever other platform we have. + * This defaults to production at all times except for 'staging' or 'test'. + * @param {String} robinType The type of Robin app we're using. Currently 'grid' or 'api' + * @param {String} env An optional environment type we can use to + * direct our requests to. If blank, defaults to production - + * otherwise goes to 'staging' or 'test'. + * @return {String} The url for the selected Robin platform + */ +var constructRobinUrl = function (robinType, env) { + var _robinUrl = '', + _env = '', + _version = 'v1.0', + _robinStub = '.robinpowered.com', + _protocol = 'https://'; + + if (!robinType) { + throw new TypeError('`robinType` is a required parameter'); + } else { + if (constants.VALID_APP_ENDPOINT_NAMES.indexOf(robinType) === -1) { + throw new TypeError('Invalid value for `robinType`. Value can be one of: ' + constants.VALID_APP_ENDPOINT_NAMES.join(',')); + } + } + + if (env && env !== 'production') { + if (constants.VALID_ENVS.indexOf(env) !== -1) { + _env = '.' + env; + if (env === 'test') { + _protocol = 'http://'; + } + } else { + throw new TypeError(env + ' is not a valid Robin environment'); + } + } + + _robinUrl = _protocol + robinType + _env + _robinStub + '/' + _version; + + return _robinUrl; +}; + +module.exports.constructRobinUrl = constructRobinUrl; + +/** + * Build an object of Robin app urls based on a arguments. The argument can be a string or an object. + * If the argument is a string, it must be a valid environment name. + * If it is an object, the structure can be found in README.md. + * Null or undefined arguments will default to production endpoints. + * @param {String|Object|undefined|null} opts An environment name, options object, undefined or null. + * @return {Object} An object that contains a mapping between robin app type names and urls. + */ +var buildRobinUrls = function (opts) { + var urls = {}, + env, + vam, + ou; + if (opts) { + if (typeof opts === 'string') { + env = opts.toLowerCase(); + } else if (isObject(opts)) { + if (opts.env) { + if (typeof opts.env === 'string') { + if (constants.VALID_ENVS.indexOf(opts.env.toLowerCase()) !== -1) { + env = opts.env.toLowerCase(); + } else { + throw new TypeError('Environment name is invalid.'); + } + } else { + throw new TypeError('The `env` property of the options object must be a string'); + } + } + if (opts.urls) { + if (!isObject(opts.urls)) { + throw new TypeError('The urls property of the options object must be an object'); + } + for (ou in opts.urls) { + if (opts.urls.hasOwnProperty(ou)) { + if (constants.VALID_APP_NAMES.indexOf(ou.toLowerCase()) !== -1) { + urls[ou.toLowerCase()] = opts.urls[ou]; + } else { + throw new TypeError(ou + ' is not a valid robin app name'); + } + } + } + } + } else { + throw new TypeError('Argument must be object, string null or undefined.'); + } + } + for (vam in constants.VALID_APP_MAPPING) { + if (!urls[vam]) { + urls[vam] = constructRobinUrl(constants.VALID_APP_MAPPING[vam], env); + } + } + return urls; +}; + +module.exports.buildRobinUrls = buildRobinUrls; diff --git a/package.json b/package.json new file mode 100644 index 0000000..aa09ee6 --- /dev/null +++ b/package.json @@ -0,0 +1,74 @@ +{ + "name": "robin-js-sdk", + "version": "0.5.1", + "description": "A JavaScript SDK to communicate with the Robin platform.", + "homepage": "https://github.com/robinpowered/robin-js-sdk-public", + "repository": { + "type": "git", + "url": "git@github.com:robinpowered/robin-js-sdk-public.git" + }, + "engines": { + "node": ">= 0.10.0" + }, + "scripts": { + "test": "grunt test", + "postinstall": "scripts/postInstall.js" + }, + "licenses": [ + { + "type": "Apache", + "url": "https://github.com/robinpowered/robin-js-sdk-public/blob/master/LICENSE" + } + ], + "main": "robin", + "devDependencies": { + "grunt": "^0.4.5", + "grunt-contrib-jshint": "~0.7.2", + "grunt-contrib-watch": "~0.5.3", + "grunt-contrib-qunit": "~0.3.0", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-uglify": "^0.4.0", + "grunt-cli": "~0.1.13", + "jshint-stylish": "~0.1.5", + "grunt-contrib-nodeunit": "^0.3.3", + "grunt-browserify": "^2.0.8", + "remapify": "^0.1.6", + "grunt-contrib-compress": "^0.8.0", + "grunt-jscs-checker": "^0.4.4", + "time-grunt": "^0.3.2", + "mocha": "^1.20.1", + "chai": "^1.9.1", + "should": "^4.0.4", + "grunt-simple-mocha": "^0.4.0", + "chai-as-promised": "^4.1.1", + "chai-http": "^0.4.0", + "blanket": "^1.1.6", + "grunt-blanket": "0.0.8", + "karma": "^0.12.16", + "karma-mocha": "^0.1.4", + "karma-chai": "^0.1.0", + "sinon": "^1.10.2", + "karma-sinon": "^1.0.3", + "karma-chrome-launcher": "^0.1.4", + "karma-firefox-launcher": "^0.1.3", + "karma-safari-launcher": "^0.1.1", + "karma-coverage": "^0.2.4", + "grunt-contrib-copy": "^0.5.0", + "grunt-contrib-clean": "^0.5.0", + "grunt-mocha-test": "^0.11.0" + }, + "dependencies": { + "bluebird": "^2.3.0", + "brfs": "^1.1.1", + "browser-request": "^0.3.2", + "browserify": "^4.1.11", + "faye": "^1.0.1", + "request": "^2.34.0", + "uglify-js": "^2.4.14", + "winston": "~0.7.2" + }, + "browser": { + "fs": "brfs", + "request": "browser-request" + } +} diff --git a/robin.js b/robin.js new file mode 100644 index 0000000..88a0290 --- /dev/null +++ b/robin.js @@ -0,0 +1,40 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +/** + * The Robin SDK provides the interface for interactions with the API + * as well as the grid. + * @return {Function} The Robin SDK Object. + */ +module.exports = (function () { + var RobinApi = require('./lib/api'), + RobinGrid = require('./lib/grid'), + RbnUtil = require('./lib/util'); + + function Robin (accessToken, opts) { + if (!accessToken) { + throw new TypeError('A Robin Access Token must be supplied'); + } + var urls = RbnUtil.buildRobinUrls(opts); + this.api = new RobinApi(accessToken, urls.core, urls.places); + this.grid = new RobinGrid(accessToken, urls.grid); + } + + /** + * Set a relay identifier for requests to Robin + * @param {String} relayIdentifier A Robin Identifier for a relay device. + */ + Robin.prototype.setRelayIdentifier = function (relayIdentifier) { + this.api.setRelayIdentifier(relayIdentifier); + this.grid.setRelayIdentifier(relayIdentifier); + }; + + return Robin; +}).call(this); diff --git a/scripts/postInstall.js b/scripts/postInstall.js new file mode 100755 index 0000000..267db5d --- /dev/null +++ b/scripts/postInstall.js @@ -0,0 +1,123 @@ +#!/usr/bin/env node +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + + +/** + * This is a self-contained script that is run after an npm install. + * It generates a both verbose and minified versions of this sdk for web browsers + */ +(function () { + var Browserify = require('browserify'), + Uglify = require('uglify-js'), + fs = require('fs'), + browserify, + browserifyOptions, + startTime, + endTime, + progressInterval, + inputFile = __dirname + '/../robin.js', + distDir = __dirname + '/../dist', + outputFileName = 'robin.browser.js', + outputFilePath = distDir + '/' + outputFileName, + outputFileMinifiedName = 'robin.browser.min.js', + outputFileMinifiedPath = distDir + '/' + outputFileMinifiedName, + generateBrowserFiles, + copyrightNotice = '/*' + + '* robin-js-sdk' + + '* http://getrobin.com/' + + '*' + + '* Copyright (c) 2014 Robin Powered Inc.' + + '* Licensed under the Apache v2 license.' + + '* https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE' + + '*' + + '*/'; + + generateBrowserFiles = function () { + fs.mkdir(distDir, function (err) { + if (err) { + if (err.code !== 'EEXIST') { + throw err; + } + } + process.stdout.write('Generating a version of Robin for the browser: [='); + progressInterval = setInterval(function () { + process.stdout.write('='); + }, 100); + browserify = new Browserify(); + browserifyOptions = { + standalone: 'Robin', + basedir: './' + }; + browserify.add(inputFile); + startTime = +new Date(); + browserify.bundle(browserifyOptions, function (err, verboseSrc) { + var duration; + if (err) { + throw err; + } + clearInterval(progressInterval); + endTime = +new Date(); + duration = (endTime - startTime)/1000; + duration = duration.toFixed(2); + console.log('=] 100%'); + console.log('Generated verbose source file in: ' + duration + ' seconds.'); + fs.writeFile(outputFilePath, verboseSrc, function (err) { + var uglified, + toplevel, + compressor; + if (err) { + throw err; + } + console.log('Verbose source file saved to dist/robin.browser.js'); + process.stdout.write('Generating minified version of Robin: [='); + progressInterval = setInterval(function () { + process.stdout.write('='); + }, 100); + + startTime = +new Date(); + toplevel = Uglify.parse(fs.readFileSync(outputFilePath, 'utf8'), {filename: 'robin.browser.js'}); + + uglified = Uglify.OutputStream({ + ascii_only: true, + source_map: false + }); + + compressor = Uglify.Compressor({ + warnings: false + }); + + toplevel.figure_out_scope(); + toplevel = toplevel.transform(compressor); + toplevel.figure_out_scope(); + toplevel.compute_char_frequency(true); + toplevel.mangle_names(true); + toplevel.print(uglified); + + clearInterval(progressInterval); + + endTime = +new Date(); + duration = (endTime - startTime)/1000; + duration = duration.toFixed(2); + console.log('=] 100%'); + console.log('Generated minified browser file in: ' + duration + ' seconds.\n'); + fs.writeFile(outputFileMinifiedPath, copyrightNotice + uglified.toString(), function (err) { + if (err) { + throw err; + } + console.log('Minified source file saved to dist/robin.browser.min.js'); + }); + }); + }); + }); + }; + + return generateBrowserFiles(); +}).call(this); diff --git a/test/api/testApi.js b/test/api/testApi.js new file mode 100644 index 0000000..5ec075a --- /dev/null +++ b/test/api/testApi.js @@ -0,0 +1,36 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Api = require('../../lib/api'), + chai = require('chai'), + expect = chai.expect; + +describe('api', function () { + describe('instantiate', function () { + it('should throw an error', function () { + var api; + expect(function () { + api = new Api(); + }).to.throw(Error); + }); + it('should throw an error when setting up the Core API', function () { + var api; + expect(function () { + api = new Api('foo'); + }).to.throw(Error); + }); + it('should throw an error when setting up the Places API', function () { + var api; + expect(function () { + api = new Api('foo', 'bar'); + }).to.throw(Error); + }); + }); +}); diff --git a/test/api/testBase.js b/test/api/testBase.js new file mode 100644 index 0000000..bcdce40 --- /dev/null +++ b/test/api/testBase.js @@ -0,0 +1,82 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var ApiBase = require('../../lib/api/base'), + chai = require('chai'), + chaiAsPromised = require('chai-as-promised'), + assert, + expect, + should; + +before(function () { + chai.use(chaiAsPromised); + assert = chai.assert; + expect = chai.expect; + should = chai.should(); +}); + +describe('api - base', function () { + describe('instantiate', function () { + it('should instantiate without error', function () { + var apiBase = new ApiBase(); + expect(apiBase).to.be.an.instanceof(ApiBase); + }); + }); + describe('acccess token operations', function () { + it('should return undefined', function () { + var apiBase = new ApiBase(); + expect(apiBase.getAccessToken()).to.be.undefined; + }); + it('should set the correct access token', function () { + var apiBase = new ApiBase(), + accessToken = 'foo'; + apiBase.setAccessToken(accessToken); + expect(apiBase.getAccessToken()).to.equal(accessToken); + }); + }); + describe('relay identifier operations', function () { + it('should return undefined', function () { + var apiBase = new ApiBase(); + expect(apiBase.getRelayIdentifier()).to.be.undefined; + }); + it('should set the correct relay identifier', function () { + var apiBase = new ApiBase(), + relayIdentifier = 'bar'; + apiBase.setRelayIdentifier(relayIdentifier); + expect(apiBase.getRelayIdentifier()).to.equal(relayIdentifier); + }); + }); + describe('reject request', function () { + it('should reject a request', function (done) { + var apiBase = new ApiBase(); + apiBase.rejectRequest().should.be.rejected.and.notify(done); + }); + }); + describe('construct path', function () { + describe('empty string', function () { + it('should return an empty string', function () { + var apiBase = new ApiBase(); + expect(apiBase.constructPath()).to.equal('/'); + }); + }); + describe('multiple strings', function () { + it('should return an empty string', function () { + var apiBase = new ApiBase(); + expect(apiBase.constructPath('foo', 'bar', 'baz')).to.equal('/foo/bar/baz'); + }); + }); + describe('multiple strings, including empty', function () { + it('should return an empty string', function () { + var apiBase = new ApiBase(); + expect(apiBase.constructPath('foo', '', 'bar', '', 'baz')).to.equal('/foo/bar/baz'); + }); + }); + }); +}); diff --git a/test/grid/modules/testChannel.js b/test/grid/modules/testChannel.js new file mode 100644 index 0000000..243e026 --- /dev/null +++ b/test/grid/modules/testChannel.js @@ -0,0 +1,57 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Channel = require('../../../lib/grid/modules/channel'), + Connection = require('../../../lib/grid/connection'), + Grid = require('../../../lib/grid'), + chai = require('chai'), + expect = chai.expect, + grid; + +before(function () { + var accessToken = 'foo', + gridUrl = 'http://grid.localhost/v1.0'; + grid = new Grid(accessToken, gridUrl); +}); + +describe('grid channel module', function () { + describe('instantiate', function () { + var channel; + it('should throw an error', function () { + expect(function () { + channel = new Channel(); + }).to.throw(Error); + }); + before(function () { + channel = new Channel(grid); + }); + it('should instantiate correctly', function () { + expect(channel).to.be.instanceof(Channel); + }); + }); + describe('connect', function () { + var channel, + connection; + before(function () { + channel = new Channel(grid); + }); + it('should throw an error', function () { + expect(function () { + connection = channel.connect(); + }).to.throw(Error); + }); + before(function () { + connection = channel.connect(15); + }); + it('should instantiate a connection', function () { + expect(connection).to.be.instanceof(Connection); + }); + }); +}); diff --git a/test/grid/modules/testDevice.js b/test/grid/modules/testDevice.js new file mode 100644 index 0000000..d2fdefc --- /dev/null +++ b/test/grid/modules/testDevice.js @@ -0,0 +1,57 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Device = require('../../../lib/grid/modules/device'), + Connection = require('../../../lib/grid/connection'), + Grid = require('../../../lib/grid'), + chai = require('chai'), + expect = chai.expect, + grid; + +before(function () { + var accessToken = 'foo', + gridUrl = 'http://grid.localhost/v1.0'; + grid = new Grid(accessToken, gridUrl); +}); + +describe('grid device module', function () { + describe('instantiate', function () { + var device; + it('should throw an error', function () { + expect(function () { + device = new Device(); + }).to.throw(Error); + }); + before(function () { + device = new Device(grid); + }); + it('should instantiate correctly', function () { + expect(device).to.be.instanceof(Device); + }); + }); + describe('connect', function () { + var device, + connection; + before(function () { + device = new Device(grid); + }); + it('should throw an error', function () { + expect(function () { + connection = device.connect(); + }).to.throw(Error); + }); + before(function () { + connection = device.connect(15); + }); + it('should instantiate a connection', function () { + expect(connection).to.be.instanceof(Connection); + }); + }); +}); diff --git a/test/grid/testConnection.js b/test/grid/testConnection.js new file mode 100644 index 0000000..250c1a6 --- /dev/null +++ b/test/grid/testConnection.js @@ -0,0 +1,209 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Connection = require('../../lib/grid/connection'), + chai = require('chai'), + expect = chai.expect, + Grid = require('../../lib/grid'), + grid, + Promise = require('bluebird'), + sinon = require('sinon'); + +before(function () { + var accessToken = 'foo', + gridUrl = 'http://grid.localhost/v1.0'; + grid = new Grid(accessToken, gridUrl); +}); + +describe('grid - connection', function () { + describe('instantiate', function () { + it('should throw an error', function () { + var connection; + expect(function () { + connection = new Connection(); + }).to.throw(Error); + }); + describe('empty grid module argument', function () { + var gridModuleMock; + before(function () { + gridModuleMock = {}; + }); + it('should throw an error with empty module', function () { + var connection; + expect(function () { + connection = new Connection(gridModuleMock); + }).to.throw(Error); + }); + }); + describe('invalid grid module argument', function () { + var gridModuleMock; + before(function () { + gridModuleMock = { + endpoint: '/devices' + }; + }); + it('should throw an error with an invalid module', function () { + var connection; + expect(function () { + connection = new Connection(gridModuleMock); + }).to.throw(Error); + }); + }); + describe('valid grid module argument', function () { + var gridModuleMock; + before(function () { + gridModuleMock = { + endpoint: '/devices', + identifier: 15 + }; + }); + it('should instantiate correctly', function () { + var connection = new Connection(gridModuleMock); + expect(connection).to.be.instanceof(Connection); + }); + }); + describe('listen to connection', function () { + describe('error', function () { + var connection, + resolvedStub; + it('should receive an error', function (done) { + connection = grid.device.connect(15); + resolvedStub = sinon.stub(connection.gridClient, 'subscribe').returns(Promise.reject('error')); + connection.gridClient.subscribe = resolvedStub; + connection.listen(function (err, resp) { + expect(err).to.not.be.null; + done(); + }); + }); + after(function () { + connection.gridClient.subscribe.restore(); + }); + }); + describe('resolved', function () { + var connection, + resolvedStub; + before(function () { + connection = grid.device.connect(15); + resolvedStub = sinon.stub(connection.gridClient, 'subscribe').returns(Promise.resolve('Resolved message')); + connection.gridClient.subscribe = resolvedStub; + }); + it('should set up a listener', function (done) { + connection.listen(function (err, resp) { + expect(resp).to.not.be.null; + done(); + }); + }); + after(function () { + connection.gridClient.subscribe.restore(); + }); + }); + describe('emit message', function () { + var connection, + message; + before(function () { + connection = grid.device.connect(15); + message = { + ext: { + type: 'data' + }, + data: { + foo: 'bar' + } + }; + }); + it('should set up a listener', function (done) { + connection.on(message.ext.type, function (data) { + expect(data).to.have.property('foo'); + done(); + }); + connection.messageCallback(message); + }); + }); + describe('stop listening', function () { + var resolvedStub; + describe('error', function () { + var connection; + before(function () { + connection = grid.device.connect(15); + resolvedStub = sinon.stub(connection.gridClient, 'subscribe').returns(Promise.resolve('Resolved Message')); + connection.gridClient.subscribe = resolvedStub; + }); + it('should pass an error message', function (done) { + connection.stop(function (err) { + expect(err).to.not.be.null; + done(); + }); + }); + after(function () { + connection.gridClient.subscribe.restore(); + }); + }); + describe('stop gracefully', function () { + var connection; + before(function () { + connection = grid.device.connect(15); + resolvedStub = sinon.stub(connection.gridClient, 'subscribe').returns(Promise.resolve('Resolved Message')); + connection.gridClient.subscribe = resolvedStub; + connection.listen(); + }); + it('should stop listening without a message', function (done) { + connection.stop(function (err) { + expect(err).to.be.null; + done(); + }); + }); + after(function () { + connection.gridClient.subscribe.restore(); + }); + }); + }); + }); + describe('send messages to connection', function () { + describe('send error', function () { + var connection, + resolvedStub, + message = {}; + message.foo = 'bar'; + it('should callback with an error', function (done) { + connection = grid.device.connect(15); + resolvedStub = sinon.stub(connection.gridClient, 'publish').returns(Promise.reject('Can\'t send this message now')); + connection.gridClient.publish = resolvedStub; + connection.send('data', message, function (err, resp) { + expect(err).to.not.be.null; + done(); + }); + }); + after(function () { + connection.gridClient.publish.restore(); + }); + }); + describe('send success', function () { + var connection, + resolvedStub, + message = {}; + message.foo = 'bar'; + before(function () { + connection = grid.device.connect(15); + resolvedStub = sinon.stub(connection.gridClient, 'publish').returns(Promise.resolve('Message Sent Successfully')); + connection.gridClient.publish = resolvedStub; + }); + it('should callback with an error', function (done) { + connection.send('data', message, function (err, resp) { + expect(err).to.be.null; + done(); + }); + }); + after(function () { + connection.gridClient.publish.restore(); + }); + }); + }); + }); +}); diff --git a/test/grid/testGrid.js b/test/grid/testGrid.js new file mode 100644 index 0000000..9cd4384 --- /dev/null +++ b/test/grid/testGrid.js @@ -0,0 +1,104 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Grid = require('../../lib/grid'), + chai = require('chai'), + expect = chai.expect; + +describe('grid', function () { + describe('instantiate', function () { + var grid, + accessToken = 'foo', + gridUrl = 'http://grid.localhost/v1.0'; + before(function () { + grid = new Grid(accessToken, gridUrl); + }); + it('should instantiate without error', function () { + expect(grid).to.be.an.instanceof(Grid); + }); + }); + describe('access token', function () { + var grid, + accessToken = 'foo', + gridUrl = 'http://grid.localhost/v1.0'; + before(function () { + grid = new Grid(accessToken, gridUrl); + }); + it('should retrieve the access token correctly', function () { + expect(grid.getAccessToken()).to.equal(accessToken); + }); + }); + describe('no url', function () { + var grid, + accessToken = 'foo'; + it('should throw an error', function () { + expect(function () { + grid = new Grid(accessToken); + }).to.throw(Error); + }); + }); + describe('event handlers', function () { + var grid, + accessToken = 'foo', + gridUrl = 'http://grid.localhost/v1.0'; + before(function () { + grid = new Grid(accessToken, gridUrl); + grid.setupGridMessageHandler(); + }); + it('should emit an error', function (done) { + grid.on('error', function (err) { + done(); + }); + grid.gridClient.emit('transport:down'); + }); + }); + describe('incoming messages', function () { + var grid, + accessToken = 'foo', + gridUrl = 'http://grid.localhost/v1.0'; + before(function () { + grid = new Grid(accessToken, gridUrl); + grid.setupGridMessageHandler(); + }); + it('should throw an error on an empty message', function (done) { + var message = {}; + grid.gridClient._extensions[0].incoming(message, function (message) { + expect(message).to.have.property('error'); + done(); + }); + }); + it('should throw an error on an invalid message', function (done) { + var message = { + channel: '/foo/bar', + data: {} + }; + grid.gridClient._extensions[0].incoming(message, function (message) { + expect(message).to.have.property('error'); + done(); + }); + }); + }); + describe('outgoing messages', function () { + var grid, + accessToken = 'foo', + gridUrl = 'http://grid.localhost/v1.0'; + before(function () { + grid = new Grid(accessToken, gridUrl); + grid.setupGridMessageHandler(); + }); + it('should have an `ext` field with an empty message', function (done) { + var message = {}; + grid.gridClient._extensions[0].outgoing(message, function (message) { + expect(message).to.have.property('ext'); + done(); + }); + }); + }); +}); diff --git a/test/testError.js b/test/testError.js new file mode 100644 index 0000000..f8ea9df --- /dev/null +++ b/test/testError.js @@ -0,0 +1,44 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var error = require('../lib/error'), + chai = require('chai'), + expect = chai.expect; + +describe('error', function () { + it('is a string', function () { + var err, foo; + foo = 'foo'; + err = new error.ApiError('foo'); + expect(err.message).to.equal(foo); + }); + it('is a shallow error object', function () { + var err, foo; + foo = { + body: { + foo: 'bar' + } + }; + err = new error.ApiError(foo); + expect(err.message).to.equal(foo.toString()); + }); + it('is a sufficiently deep object but is invalid', function () { + var err, foo; + foo = { + body: { + meta: { + foo: 'bar' + } + } + }; + err = new error.ApiError(foo); + expect(err.message).to.not.equal(foo.toString()); + }); +}); diff --git a/test/testRobin.js b/test/testRobin.js new file mode 100644 index 0000000..06ac843 --- /dev/null +++ b/test/testRobin.js @@ -0,0 +1,44 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var Robin = require('../robin'), + chai = require('chai'), + expect = chai.expect; + +describe('robin', function () { + describe('instantiate', function () { + it('should throw an error', function () { + var robin; + expect(function () { + robin = new Robin(); + }).to.throw(TypeError); + }); + it('should instantiate without error', function () { + var robin = new Robin('SampleAccessToken'); + expect(robin).to.be.an.instanceof(Robin); + expect(robin.api.Core.getBaseUrl()).to.equal('https://api.robinpowered.com/v1.0'); + expect(robin.api.Places.getBaseUrl()).to.equal('https://apps.robinpowered.com/v1.0'); + expect(robin.grid.getGridUrl()).to.equal('https://grid.robinpowered.com/v1.0'); + }); + }); + describe('relay identifier', function () { + var robin = new Robin('SampleAccessToken'), + relayIdentifier = 'foo'; + before(function () { + robin.setRelayIdentifier(relayIdentifier); + }); + it('should be the expected api relay identifier', function () { + expect(robin.grid.getRelayIdentifier()).to.equal(relayIdentifier); + }); + it('should be the expected grid relay identifier', function () { + expect(robin.api.getRelayIdentifier()).to.equal(relayIdentifier); + }); + }); +}); diff --git a/test/testUtil.js b/test/testUtil.js new file mode 100644 index 0000000..37a86fd --- /dev/null +++ b/test/testUtil.js @@ -0,0 +1,403 @@ +/* + * robin-js-sdk + * http://getrobin.com/ + * + * Copyright (c) 2014 Robin Powered Inc. + * Licensed under the Apache v2 license. + * https://github.com/robinpowered/robin-js-sdk/blob/master/LICENSE + * + */ + +var rbnUtil = require('../lib/util.js'), + chai = require('chai'), + expect = chai.expect, + mocks; + +// Constants +var validApiTestUrl = 'http://api.test.robinpowered.com/v1.0', + validApiStagingUrl = 'https://api.staging.robinpowered.com/v1.0', + validApiProductionUrl = 'https://api.robinpowered.com/v1.0', + validAppsTestUrl = 'http://apps.test.robinpowered.com/v1.0', + validAppsStagingUrl = 'https://apps.staging.robinpowered.com/v1.0', + validAppsProductionUrl = 'https://apps.robinpowered.com/v1.0', + validGridTestUrl = 'http://grid.test.robinpowered.com/v1.0', + validGridStagingUrl = 'https://grid.staging.robinpowered.com/v1.0', + validGridProductionUrl = 'https://grid.robinpowered.com/v1.0'; + +mocks = { + urls: { + production: { + core: validApiProductionUrl, + grid: validGridProductionUrl, + places: validAppsProductionUrl + }, + staging: { + core: validApiStagingUrl, + grid: validGridStagingUrl, + places: validAppsStagingUrl + }, + test: { + core: validApiTestUrl, + grid: validGridTestUrl, + places: validAppsTestUrl + } + } +}; + +describe('robin util', function () { + describe('construct robin url', function () { + describe('nonsense', function () { + it('should throw an error when creating a test url', function () { + var testUrl; + expect(function () { + testUrl = rbnUtil.constructRobinUrl('nonsense', 'test'); + }).to.throw(TypeError); + }); + it('should throw an error when creating a staging url', function () { + var stagingUrl; + expect(function () { + stagingUrl = rbnUtil.constructRobinUrl('nonsense', 'staging'); + }).to.throw(TypeError); + }); + it('should throw an error when creating a production url', function () { + var productionUrl; + expect(function () { + productionUrl = rbnUtil.constructRobinUrl('nonsense', 'production'); + }).to.throw(TypeError); + }); + it('should throw an error when creating a production url', function () { + var productionUrl; + expect(function () { + productionUrl = rbnUtil.constructRobinUrl('nonsense'); + }).to.throw(TypeError); + }); + it('should throw an error when creating a nonsense url', function () { + var fooUrl; + expect(function () { + fooUrl = rbnUtil.constructRobinUrl('nonsense', 'foo'); + }).to.throw(TypeError); + }); + }); + describe('api', function () { + describe('test env argument', function () { + it('should construct a valid api test url', function () { + var testUrl = rbnUtil.constructRobinUrl('api', 'test'); + expect(testUrl).to.equal(validApiTestUrl); + }); + }); + describe('staging env argument', function () { + it('should construct a valid api staging url', function () { + var stagingUrl = rbnUtil.constructRobinUrl('api', 'staging'); + expect(stagingUrl).to.equal(validApiStagingUrl); + }); + }); + describe('production env argument', function () { + it('should construct a valid api production url', function () { + var productionUrl = rbnUtil.constructRobinUrl('api', 'production'); + expect(productionUrl).to.equal(validApiProductionUrl); + }); + }); + describe('no env argument', function () { + it('should construct a valid api production url', function () { + var productionUrl = rbnUtil.constructRobinUrl('api'); + expect(productionUrl).to.equal(validApiProductionUrl); + }); + }); + describe('nonsense env argument', function () { + it('should throw an error', function () { + expect(function () { + rbnUtil.constructRobinUrl('api', 'foo'); + }).to.throw(TypeError); + }); + }); + }); + describe('apps', function () { + describe('test env argument', function () { + it('should construct a valid apps test url', function () { + var testUrl = rbnUtil.constructRobinUrl('apps', 'test'); + expect(testUrl).to.equal(validAppsTestUrl); + }); + }); + describe('staging env argument', function () { + it('should construct a valid apps staging url', function () { + var stagingUrl = rbnUtil.constructRobinUrl('apps', 'staging'); + expect(stagingUrl).to.equal(validAppsStagingUrl); + }); + }); + describe('production env argument', function () { + it('should construct a valid apps production url', function () { + var productionUrl = rbnUtil.constructRobinUrl('apps', 'production'); + expect(productionUrl).to.equal(validAppsProductionUrl); + }); + }); + describe('no env argument', function () { + it('should construct a valid apps production url', function () { + var productionUrl = rbnUtil.constructRobinUrl('apps'); + expect(productionUrl).to.equal(validAppsProductionUrl); + }); + }); + describe('nonsense env argument', function () { + it('should throw an error', function () { + expect(function () { + rbnUtil.constructRobinUrl('api', 'foo'); + }).to.throw(TypeError); + }); + }); + }); + describe('grid', function () { + describe('test env argument', function () { + it('should construct a valid grid test url', function () { + var testUrl = rbnUtil.constructRobinUrl('grid', 'test'); + expect(testUrl).to.equal(validGridTestUrl); + }); + }); + describe('staging env argument', function () { + it('should construct a valid grid staging url', function () { + var stagingUrl = rbnUtil.constructRobinUrl('grid', 'staging'); + expect(stagingUrl).to.equal(validGridStagingUrl); + }); + }); + describe('production env argument', function () { + it('should construct a valid grid production url', function () { + var productionUrl = rbnUtil.constructRobinUrl('grid', 'production'); + expect(productionUrl).to.equal(validGridProductionUrl); + }); + }); + describe('no env argument', function () { + it('should construct a valid grid production url', function () { + var productionUrl = rbnUtil.constructRobinUrl('grid'); + expect(productionUrl).to.equal(validGridProductionUrl); + }); + }); + describe('nonsense env argument', function () { + it('should throw an error', function () { + expect(function () { + rbnUtil.constructRobinUrl('api', 'foo'); + }).to.throw(TypeError); + }); + }); + }); + }); + describe('build robin urls', function () { + describe('undefined arg', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls(); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('null arg', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls(null); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('env string args', function () { + describe('empty string', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls(''); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('production env', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls('production'); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('staging env', function () { + it('should build a valid staging url object', function () { + var robinUrls = rbnUtil.buildRobinUrls('staging'); + expect(robinUrls).to.deep.equal(mocks.urls.staging); + }); + }); + describe('test env', function () { + it('should build a valid test url object', function () { + var robinUrls = rbnUtil.buildRobinUrls('test'); + expect(robinUrls).to.deep.equal(mocks.urls.test); + }); + }); + describe('invalid env', function () { + it('should throw an error', function () { + expect(function () { + rbnUtil.buildRobinUrls('invalid'); + }).to.throw(TypeError); + }); + }); + }); + describe('env object args', function () { + describe('invalid env property', function () { + describe('object', function () { + it('should throw an error', function () { + expect(function () { + rbnUtil.buildRobinUrls({ + env: { + foo: 'bar' + } + }); + }).to.throw(TypeError); + }); + }); + describe('array', function () { + it('should throw an error', function () { + expect(function () { + rbnUtil.buildRobinUrls({ + env: [1,2,3] + }); + }).to.throw(TypeError); + }); + }); + }); + describe('empty object arg', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({}); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('object arg with env property set', function () { + describe('empty string', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + env: '' + }); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('undefined', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + env: undefined + }); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('null', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + env: null + }); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('production', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + env: 'production' + }); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('staging', function () { + it('should build a valid staging url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + env: 'staging' + }); + expect(robinUrls).to.deep.equal(mocks.urls.staging); + }); + }); + describe('test', function () { + it('should build a valid test url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + env: 'test' + }); + expect(robinUrls).to.deep.equal(mocks.urls.test); + }); + }); + }); + describe('object arg with urls property', function () { + describe('invalid urls property', function () { + describe('string', function () { + it('should throw an error', function () { + expect(function () { + rbnUtil.buildRobinUrls({ + urls: 'invalid' + }); + }).to.throw(TypeError); + }); + }); + describe('array', function () { + it('should throw an error', function () { + expect(function () { + rbnUtil.buildRobinUrls({ + urls: [1,2,3] + }); + }).to.throw(TypeError); + }); + }); + }); + describe('null urls object', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + urls: null + }); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('undefined urls object', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + urls: undefined + }); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('empty urls object', function () { + it('should build a valid production url object', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + urls: {} + }); + expect(robinUrls).to.deep.equal(mocks.urls.production); + }); + }); + describe('invalid urls object', function () { + it('should throw an error', function () { + expect(function () { + rbnUtil.buildRobinUrls({ + urls: { + foo: 'http://localhost' + } + }); + }).to.throw(TypeError); + }); + }); + describe('set grid url to local, others to production', function () { + it('should create a custom set of urls', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + urls: { + grid: 'http://localhost/robin-grid' + } + }); + expect(robinUrls.core).to.equal(validApiProductionUrl); + expect(robinUrls.grid).to.equal('http://localhost/robin-grid'); + expect(robinUrls.places).to.equal(validAppsProductionUrl); + }); + }); + describe('set core url to local, others to staging', function () { + it('should create a custom set of urls', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + env: 'staging', + urls: { + core: 'http://localhost/robin-api' + } + }); + expect(robinUrls.core).to.equal('http://localhost/robin-api'); + expect(robinUrls.grid).to.equal(validGridStagingUrl); + expect(robinUrls.places).to.equal(validAppsStagingUrl); + }); + }); + describe('set places url to local, others to test', function () { + it('should create a custom set of urls', function () { + var robinUrls = rbnUtil.buildRobinUrls({ + env: 'test', + urls: { + places: 'http://localhost/robin-apps' + } + }); + expect(robinUrls.core).to.equal(validApiTestUrl); + expect(robinUrls.grid).to.equal(validGridTestUrl); + expect(robinUrls.places).to.equal('http://localhost/robin-apps'); + }); + }); + }); + }); + }); +});