diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..8dc6807 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,21 @@ +{ + "rules": { + "no-console": "off", + "indent": [ "error", 2 ], + "quotes": [ "error", "single" ], + "semi": ["error", "always"], + "linebreak-style": [ "error", "unix" ] + }, + "env": { + "es6": true, + "node": true, + "mocha": true, + "jasmine": true + }, + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true, + "impliedStrict": true + }, + "extends": "eslint:recommended" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b34584 --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +# Created by https://www.gitignore.io/api/macos,node,vim,linux + +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon +# Thumbnails +._* +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Node ### +node_modules +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# 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 directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + + + +### Vim ### +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +sample-req.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..ded7799 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +## Hello and welcome to my Electric Vehicle API. + +In the last decade, Electric vehicles have exploded in popularity with the advent of new battery technologies and practical vehicles hitting the market, from flashy luxury models like Tesla's Model S, to affordable commuter/family models like Nissan's Leaf. As technology improves and better, more affordable models hit the market, Electric vehicles have the potential to make a serious dent in pollution from automotive transportation. This is especially true in markets with relatively green power grids. + +The purpose of this API is ultimately to be a resource for consumers interested in Electric Vehicles and green transportation in general, containing the most up-to-date information on Electric Vehicles. Info like range on a battery charge, MPGe, market availability, quick-charging options and standards will eventually be added as separate categories in terms of consumer info (object properties in terms of the API) + +For this API, we define "Electric Vehicles" more technically and specifically as "Battery-Electric Vehicles," or BEVs. Only vehicles that are powered exclusively by electricity from a battery are included. This designation excludes plug-in-hybrid vehicles (like the Chevrolet Volt) and hydrogen fuel cell vehicles. + +## How to use this API + +Currently, this API is configured for use in the command line. It is set up to run on port 3000 of your computer's local IP (this IP can be accessed with the identifier `localhost`). + +You will need a command line http tool installed. I recommend httpie, and I assume you have it installed for this example. + + * In the command line, making sure you're in the root directory of your local version of the API, start the node server by typing `node server.js` + * Let's add a sample vehicle to the API. In a **separate** window or pane of your command line interface (your first window is running the node server), making sure you're still in the root directory of your local version of the API, type `http POST localhost:3000/api/bev vehicle="Nissan Leaf" info="108 mile-range on a single charge"` + * Let's add a second sample vehicle. Type `http POST localhost:3000/api/bev vehicle="Tesla Model S" info="300 mile-range on a single charge"` + * Let's get the unique id for each vehicle. Each id is a random string of numbers, letter, and dashes autogenerated with node-uuid. type `http localhost:3000/api/bev` This should print an array of ids (for this example there should be two) in the command line. + * Let's take one of the ids and look up the vehicle information it references. Copy the text of one of the ids. Then in the command line, type `http localhost:3000/api/bev?id=` and paste the id before submitting the command. You should see an object printed to the command line with the vehicle info. + + Thanks for using my API. Check back soon for updates and improvements! diff --git a/data/bev/507c19c0-c745-11e6-b109-8b592aaa39eb.json b/data/bev/507c19c0-c745-11e6-b109-8b592aaa39eb.json new file mode 100644 index 0000000..8923f13 --- /dev/null +++ b/data/bev/507c19c0-c745-11e6-b109-8b592aaa39eb.json @@ -0,0 +1 @@ +{"id":"507c19c0-c745-11e6-b109-8b592aaa39eb","vehicle":"Nissan Leaf","info":"practical and affordable commuter and family hatchback","range":"120"} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..04eaa14 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,23 @@ +'use strict'; + +const gulp = require('gulp'); +const eslint = require('gulp-eslint'); +const mocha = require('gulp-mocha'); + +gulp.task('test', function() { + gulp.src('./test/*-test.js', { read: false }) + .pipe(mocha({ reporter: 'spec' })); +}); + +gulp.task('lint', function() { + gulp.src(['**/*.js', '!node_modules']) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +gulp.task('dev', function() { + gulp.watch(['**/*.js', '!node_modules'], ['lint', 'test']); +}); + +gulp.task('default', ['dev']); diff --git a/lib/storage.js b/lib/storage.js new file mode 100644 index 0000000..9b59b95 --- /dev/null +++ b/lib/storage.js @@ -0,0 +1,71 @@ +'use strict'; + +const Promise = require('bluebird'); +const fs = Promise.promisifyAll(require('fs'), { suffix: 'Prom' }); +const createError = require('http-errors'); +const debug = require('debug')('bev:storage'); + +module.exports = exports = {}; + +exports.createEntry = function(schemaName, entry) { + debug('createEntry'); + + if (!schemaName) return Promise.reject(createError(400, 'expected schema name')); + if (!entry) return Promise.reject(createError(400, 'expected entry data')); + + let json = JSON.stringify(entry); + return fs.writeFileProm(`${__dirname}/../data/${schemaName}/${entry.id}.json`, json) + .then( () => entry) + .catch( err => Promise.reject(createError(500, err.message))); +}; + +exports.fetchEntry = function(schemaName, id) { + debug('fetchEntry'); + + if (!schemaName) return Promise.reject(createError(400, 'expected schema name')); + if (!id) return Promise.reject(createError(400, 'expected id')); + + return fs.readFileProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .then( data => { + try { + let entry = JSON.parse(data.toString()); + return entry; + } catch (err) { + return Promise.reject(createError(500, err.message)); + }; + }) + .catch( err => Promise.reject(createError(404, err.message))); +}; + +exports.fetchAll = function(schemaName) { + debug('fetchAll'); + + if (!schemaName) return Promise.reject(createError(400, 'expected schema name')); + + // return array of saved filenames whose names (minus the '.json' filename extention) match vehicle IDs + return fs.readdirProm(`${__dirname}/../data/${schemaName}/`) + .then( fileNames => { + try { + var ids = []; + fileNames.forEach( fileName => { + // remove '.json' filename extention from array of IDs, push to new array to print to users' CLI + ids.push(fileName.replace('.json', '')); + }); + return ids; + + } catch (err) { + return Promise.reject(err); + }; + }) + .catch( err => Promise.reject(err)); +}; + +exports.deleteEntry = function(schemaName, id) { + debug('deleteEntry'); + + if (!schemaName) return Promise.reject(createError(400, 'expected schemaName')); + if (!id) return Promise.reject(createError(400, 'expected id')); + + return fs.unlinkProm(`${__dirname}/../data/${schemaName}/${id}.json`) + .catch( err => Promise.reject(createError(404, err.message))); +}; diff --git a/model/bevs.js b/model/bevs.js new file mode 100644 index 0000000..4ad7723 --- /dev/null +++ b/model/bevs.js @@ -0,0 +1,52 @@ +'use strict'; + +// Model for data on Battery-Electric Vehicles (BEVs) + +const uuid = require('node-uuid'); +const createError = require('http-errors'); +const debug = require('debug')('bev:bev'); +const storage = require('../lib/storage.js'); + +const BEV = module.exports = function(vehicle, info, range) { + debug('BEV constructor'); + + if (!vehicle) throw createError(400, 'expected vehicle model name'); + if (!info) throw createError(400, 'expected vehicle model info'); + if (!range) throw createError(400, 'expected vehicle model range'); + if (isNaN(range)) throw createError(400, 'expected range to be a number'); + + this.id = uuid.v1(); + // vehicle make, model + this.vehicle = vehicle; + // general vehicle description + this.info = info; + // EPA-tested range on a single battery charge, in miles + // range must be a number + this.range = range; +}; + +BEV.createVehicle = function(_vehicle) { + debug('createVehicle'); + + try { + let bev = new BEV(_vehicle.vehicle, _vehicle.info, _vehicle.range); + return storage.createEntry('bev', bev); + } catch (err) { + return Promise.reject(err); + }; +}; + +BEV.fetchVehicle = function(id) { + debug('fetchVehicle'); + return storage.fetchEntry('bev', id); +}; + +BEV.fetchAllVehicles = function() { + debug('fetchAllVehicles'); + return storage.fetchAll('bev'); +}; + +BEV.deleteVehicle = function(id) { + debug('deleteVehicle'); + return storage.deleteEntry('bev', id); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..7c93f2b --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "11-express_single_resource_api", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "DEBUG='bev*' mocha", + "start": "DEBUG='bev*' node server.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/maschigokae/11-express_single_resource_api.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/maschigokae/11-express_single_resource_api/issues" + }, + "homepage": "https://github.com/maschigokae/11-express_single_resource_api#readme", + "dependencies": { + "bluebird": "^3.4.6", + "body-parser": "^1.15.2", + "debug": "^2.4.5", + "express": "^4.14.0", + "http-errors": "^1.5.1", + "morgan": "^1.7.0", + "node-uuid": "^1.4.7" + }, + "devDependencies": { + "chai": "^3.5.0", + "gulp": "^3.9.1", + "gulp-eslint": "^3.0.1", + "gulp-mocha": "^3.0.1", + "mocha": "^3.2.0", + "superagent": "^3.3.1" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..eba1f6e --- /dev/null +++ b/server.js @@ -0,0 +1,48 @@ +'use strict'; + +const express = require('express'); +const morgan = require('morgan'); +const createError = require('http-errors'); +const jsonParser = require('body-parser').json(); +const debug = require('debug')('bev:server'); + +const app = express(); +const BEV = require('./model/bevs.js'); + +const PORT = 3000; + +app.use(morgan('dev')); + +app.post('/api/bev', jsonParser, function(req, res, next) { + debug('POST: api/bev'); + + BEV.createVehicle(req.body) + .then( vehicle => res.json(vehicle)) + .catch( err => next(err)); +}); + +app.get('/api/bev', function(req, res, next) { + debug('GET: api/bev'); + + if (req.query.id) { + BEV.fetchVehicle(req.query.id) + .then( vehicle => res.json(vehicle)) + .catch( err => next(err)); + }; + + if (!req.query.id) { + BEV.fetchAllVehicles() + .then( vehicles => res.json(vehicles)) + .catch( err => next(err)); + }; +}); + +app.delete('/api/bev', function(req, res, next) { + BEV.deleteVehicle(req.query.id) + .then( vehicle => res.json(vehicle)) + .catch( err => next(err)); +}); + +app.listen(PORT, () => { + console.log(`SERVER RUNNING ON PORT ${PORT}`); +}); diff --git a/test/bev-test.js b/test/bev-test.js new file mode 100644 index 0000000..5f2e797 --- /dev/null +++ b/test/bev-test.js @@ -0,0 +1,92 @@ +'use strict'; + +const request = require('superagent'); +const expect = require('chai').expect; +const BEV = require('../model/bevs.js'); + +require('../server.js'); + +describe('BEV Routes', function() { + var vehicle = null; + + var testVehicleEntry = { + vehicle: 'Test Vehicle', + info: 'Test vehicle info', + range: 100 + }; + + describe('POST: api/bev', function() { + it('should throw a 400 \'bad request\' error', function(done) { + request.post('http://localhost:3000/api/bev') + .send({}) + .end((err, res) => { + expect(res.status).to.equal(400); + done(); + }); + }); + + it('should return a vehicle info entry', function(done) { + request.post('http://localhost:3000/api/bev') + .send(testVehicleEntry) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.vehicle).to.equal('Test Vehicle'); + expect(res.body.info).to.equal('Test vehicle info'); + expect(res.body.range).to.equal(100); + expect(res.body.range).to.be.a('number'); + vehicle = res.body; + done(); + }); + }); + }); + + describe('GET: api/bev?id=test_id', function() { + it('should throw a 404 \'not found\' error', function(done) { + request.get('http://localhost:3000/api/bev?id=foo-bar') + .end((err, res) => { + expect(res.status).to.equal(404); + done(); + }); + }); + + it('should return a vehicle info entry', function(done) { + request.get(`http://localhost:3000/api/bev?id=${vehicle.id}`) + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body.vehicle).to.equal('Test Vehicle'); + expect(res.body.info).to.equal('Test vehicle info'); + done(); + }); + }); + }); + + describe('GET: api/bev', function() { + it('should return an array of entry ids', function(done) { + request.get('http://localhost:3000/api/bev') + .end((err, res) => { + if (err) return done(err); + expect(res.status).to.equal(200); + expect(res.body).to.be.an('array'); + // expect filenames to be sorted in alpha. order. + // thus, latest filename will be in random position in array + expect(res.body).to.include(vehicle.id); + done(); + }); + }); + }); + + describe('DELETE: api/bev', function() { + it('should delete the test vehicle info entry', function(done) { + request.delete(`http://localhost:3000/api/bev?id=${vehicle.id}`) + .end((err, res) => { + if (err) return done(err); + // not sure how to get a 204 with the refactor. But both 200 and 204 seem to be a common convention for successful DELETE requests. + expect(res.status).to.equal(200 || 204); + expect(res.body).to.be.empty; + done(); + }); + }); + }); +});