From 974802ab4e13a6e42059ef9a221a323687aa39d2 Mon Sep 17 00:00:00 2001 From: Greg Walden Date: Wed, 1 Mar 2017 13:36:24 -0500 Subject: [PATCH] initial commit --- .editorconfig | 10 ++++++ .gitignore | 40 +++++++++++++++++++++ .travis.yml | 7 ++++ LICENSE | 21 +++++++++++ lib/index.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 30 ++++++++++++++++ test/index.js | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 295 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 lib/index.js create mode 100644 package.json create mode 100644 test/index.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d1d8a41 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b7d03f --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Source: https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore +# Retrieved: 2016-01-14 + +# Logs +logs +*.log +npm-debug.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 + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Custom +.vscode diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5ba5d74 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - "node" + - 7 + - 6 + - 5 + - 4 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7ccf03a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2017 MakerBot https://www.makerbot.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..9e49682 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,98 @@ +'use strict'; + +const got = require('got'); +const url = require('url'); +const _ = require('lodash'); +const assert = require('assert'); + +const defaultHost = { + protocol: 'https:', + slashes: true, + hostname: 'api.thingiverse.com', + pathname: '/' +}; + +function thingiverse(path, opts) { + assert(_.isString(path), `Expected 'path' to be a string, got ${typeof path}`); + + const env = process.env; + opts = _.assign({ + json: true, + token: env.THINGIVERSE_TOKEN, + domain: env.THINGIVERSE_API_DOMAIN || url.format(defaultHost) + }, opts); + + assert(_.isString(opts.token), 'OAuth token not set'); + + opts.headers = _.assign({ + accept: 'application/json', + authorization: `Bearer ${opts.token}`, + 'user-agent': 'https://github.com/makerbot/thingiverse-js' + }, opts.headers); + + if (_.isPlainObject(opts.body)) { + opts.headers['content-type'] = 'application/json'; + opts.body = JSON.stringify(opts.body); + } + + const endpoint = path.startsWith('http') ? path : url.resolve(opts.domain, path); + + if (opts.stream) { + return got.stream(endpoint, opts); + } + + return got(endpoint, opts); +} + +thingiverse.stream = (url, opts) => thingiverse(url, _.assign({}, opts, { + json: false, + stream: true +})); + +thingiverse.getError = (res) => { + return _.get(res, ['headers', 'x-error']) + || _.get(res, 'body') + || null; +}; + +function assertParams(params, fields) { + params = _.isString(params) ? JSON.parse(params) : params; + _.each(fields, + field => assert(_.isString(params[field]), `Missing "${field}" field`) + ); +} + +thingiverse.getAuthorizeUrl = opts => { + if (_.isString(opts)) { + opts = { query: { client_id: opts } }; + } + assertParams(opts.query, ['client_id']); + return url.format(_.assign({}, defaultHost, { + hostname: 'www.thingiverse.com', + pathname: '/login/oauth/authorize' + }, opts)); +}; + +thingiverse.getAccessToken = opts => { + opts = _.assign({ + domain: 'www.thingiverse.com' + }, opts); + assertParams(opts.body, ['code', 'client_id', 'client_secret']); + return thingiverse.post('/login/oauth/access_token', opts); +}; + +const helpers = [ + 'get', + 'post', + 'put', + 'patch', + 'head', + 'delete' +]; +for (const x of helpers) { + const method = x.toUpperCase(); + thingiverse[x] = (url, opts) => thingiverse(url, _.assign({}, opts, {method})); + thingiverse.stream[x] = (url, opts) => thingiverse.stream(url, _.assign({}, opts, {method})); +} + +module.exports = thingiverse; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7c0c720 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "thingiverse-js", + "version": "0.0.0", + "description": "Easy Thingiverse API requests for Node.js", + "main": "lib/index.js", + "scripts": { + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/makerbot/thingiverse-js.git" + }, + "author": "Greg Walden ", + "license": "MIT", + "devDependencies": { + "chai": "^3.4.1", + "mocha": "^3.0.0", + "nock": "^9.0.9" + }, + "dependencies": { + "got": "^6.7.1", + "lodash": "^4.17.4" + }, + "keywords": [ + "thingiverse", + "api", + "makerbot", + "got" + ] +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..07cfc14 --- /dev/null +++ b/test/index.js @@ -0,0 +1,89 @@ +'use strict'; + +const chai = require('chai') + , expect = chai.expect + , should = chai.should() + , thingiverse = require('../lib') + , _ = require('lodash') + ; + +describe('thingiverse object tests', () => { + it('has all methods', () => { + thingiverse.should.contain.all.keys([ + 'get', + 'post', + 'put', + 'patch', + 'head', + 'delete' + ]); + }) + + it('has stream', () => { + expect(thingiverse.stream).to.exist; + }) + + it('has utility methods', () => { + thingiverse.should.contain.all.keys([ + 'getError', + 'getAuthorizeUrl', + 'getAccessToken', + ]); + }) +}) + +describe('getError tests', () => { + const res = { + headers: { 'x-error': 'Beefy-armed error message' }, + body: 'An error with barbie legs' + }; + + it('check the x-error header', () => { + thingiverse.getError(res).should.eq(res.headers['x-error']); + }) + + it('returns the body if x-error header empty', () => { + thingiverse.getError( + _.assign({}, res, { headers: { 'x-error': null } }) + ).should.eq(res.body); + }) + + it('returns null the body and x-error header are empty', () => { + expect(thingiverse.getError({})).to.be.null; + }) +}); + +describe('getAuthorizeUrl tests', () => { + it('returns expected URL (object param)', () => { + thingiverse.getAuthorizeUrl({ query: { client_id: 'abcdef123' } }).should.eq( + 'https://www.thingiverse.com/login/oauth/authorize?client_id=abcdef123' + ); + }) + + it('returns expected URL (string param)', () => { + thingiverse.getAuthorizeUrl('abcdef123').should.eq( + 'https://www.thingiverse.com/login/oauth/authorize?client_id=abcdef123' + ); + }) + + it('throws if missing client_id (object param)', () => { + const fn = _.partial(thingiverse.getAuthorizeUrl, {}); + expect(fn).to.throw; + }) + + it('throws if missing client_id (string param)', () => { + const fn = _.partial(thingiverse.getAuthorizeUrl, ''); + expect(fn).to.throw; + }) + + it('can override URL elements', () => { + thingiverse.getAuthorizeUrl({ + protocol: 'http:', + hostname: 'www.thingiverse.dev', + port: 8888, + query: { client_id: 'abcdef123' } + }).should.eq( + 'http://www.thingiverse.dev:8888/login/oauth/authorize?client_id=abcdef123' + ); + }) +});