Commits

David Hessing committed c391f32

Initial working version.

Comments (0)

Files changed (29)

+*.log
+*.csv
+*.md
+*.example
+*.zip
+.*
+test/
+localconfig.js
+/node_modules
+localconfig.js
+manifest.yml
+*.log
+*.txt
+*.zip
+Copyright (c) 2013, TopCoder Inc
+All rights reserved.
+
+Auto API
+========
+
+## Overview ##
+
+This is reference implementation for a RESTful API centered on entity CRUD operations. The CRUD operations are all automatically generated from the entity definitions. Additionally, the API has swagger built in so it supports active docs through a swagger ui.
+
+To use as is all you need to do is set your configuration values as described below, deploy and start. 
+
+## Demo ##
+You can see the reference implementation in action [here](http://apipoc.aws.af.cm/activedocs).
+**NOTE:** For the demo the create, update and delete methods are disabled and will _always_ simply return success.
+
+## Usage and Configuration ##
+### Environment Setup ###
+
+You need Nodejs installed on your system and in your path. This reference implementation was built using Nodejs version 0.8.16, and is currently updated to and tested against version 0.10.12. See the [downloads](http://nodejs.org/download/) page for versions and install information.
+
+### Dependencies ###
+
+See the npm-shrinkwrap.json file for the full list of packages that are used. That said, the main pieces are:
+
+* [Nodejs](http://nodejs.org) 
+* [Express](http://expressjs.com)
+* [MongoDB](http://www.mongodb.org/)
+* [Mongoose](http://mongoosejs.com/)
+* [Swagger](https://github.com/wordnik/swagger-node-express)
+
+From the directory where this reference implementation exists, use `npm install` to setup the dependencies from the shrinkwrap file.
+
+### Configuration Setup ###
+
+In your dev environment, create a "localconfig.js" file, and copy in the contents from the "localconfig.js.example" file. There are only two required configuration values:
+
+* _dbconnstring_ - The database connection information. You can either setup mongodb locally, or use a hosted solution. For example, [mongolab](http://mongolab.com) has a free developer tier that works well.
+* _publicUrl_ - The base URL for the deployment. This defaults to 'http://localhost', so it only needs to be explicitly set for production.
+
+There are many other configuration settings, but they all use reasonable defaults. See the "config.js" file for full details. 
+
+For production use, set the process.env.NODE_ENV variable to 'production'. This will cause the app to load all configuration values from environment variables rather than from the "localconfig.js" file.
+
+### Customizing the Entity Definitions ###
+
+The API is code is designed to be very easy to use and to customize - to change the entities you only need to modify the schemas.js file. All RESTful CRUD operations (GET, PUT, POST, DELETE) will be automatically generated along with Swagger documentation for active docs.
+
+### Run the API ###
+
+Run `node server` to run the API. Note if you're in development mode the application will pre-populate the DB with some test data on the first startup.
+
+## Active Docs ##
+
+This reference implementation uses [swagger](https://github.com/wordnik/swagger-node-express) on all api methods, and therefore automatically provides active docs from [any](http://petstore.swagger.wordnik.com/) accessible swagger ui. Just replace URL with the discovery URL of this API.
+
+The discoveryURL is built from the config values in the config.js file. It is: `[publicUrl]/[apiSubpath]/[apiDocsSubpath]`. For example, in development using all default config values the discover URL is: `http://localhost:1337/api/api-docs`
+
+For convenience, this implementation includes its own swagger-ui pulled from the 'dist' folder of the [swagger-ui git repository](https://github.com/wordnik/swagger-ui), and is located under `static/activedocs`. This means that if you keep all default configuration values then during dev the swagger-ui is located at `http://localhost:1337/activedocs`
+
+This swagger UI instance has been only very minorly modified as follows:
+
+* Turned on header parameters as described [here](https://github.com/wordnik/swagger-ui#header-parameters).
+* Allowed 'delete' invocation as described [here](https://github.com/wordnik/swagger-ui#http-methods-and-api-invocation).
+* Set the discoveryURL to be `/api/api-docs`. For prod deployment you may want to change this to a fully qualified URL just so it's a little clearer, but this is not required.
+* Commented out the links in the header to the petstore API and the wordnik API.
+
+## License ##
+
+Starter App is licensed under by TopCoder. See the accompanying LICENSE.md file.
+
+The swagger-ui found under `static/activedocs` folder remains under [Apache License](http://www.apache.org/licenses/LICENSE-2.0) as stated [here](https://github.com/wordnik/swagger-ui#license).
+
+## Future Work ##
+
+* Adding in API authorization/authentication.
+'use strict';
+var mongoose = require('mongoose');
+var async = require('async');
+var schemas = require('./schemas');
+var winston = require('winston');
+var localconfig = null;
+var entityNames = Object.keys(schemas.swaggerModels.models);
+
+//default to development
+var environment = (process.env.NODE_ENV !== undefined) ? process.env.NODE_ENV : 'development';
+if (environment === 'development') {
+	localconfig = require("./localconfig").localconfig;
+}
+
+//Helper function for setting config values
+function getConfigValue(configName) {
+	if (environment === 'development') {
+		return localconfig[configName];
+	}
+	if (environment === 'production') {
+		return process.env[configName];
+	}
+	throw new Error('Cannot load configuration values since the environment "' + environment + '" is unknown.');
+}
+
+//All application wide values are parsed into a 'config' object.
+var config =	{
+		//MUST BE SET TO CUSTOM VALUE FOR PRODUCTION AND DEVELOPMENT. FOR DEV, SET IT IN YOUR localconfig.js FILE.
+		dbconnstring: getConfigValue('dbconnstring'),
+
+		//MUST BE SET TO CUSTOM VALUE FOR PRODUCTION USE. FEEL FREE TO OVERRIDE DEFAULT DEV VALUES IN YOUR localconfig.js FILE.
+		publicUrl   : getConfigValue('publicUrl') || 'http://localhost', //used only by swagger for the active api docs
+		VMC_APP_PORT: getConfigValue('VMC_APP_PORT') || 1337, //Note if you're using a PAAS service like appfog for prod this environment variables is likely set for you automatically. 
+
+		//ALL REMAINING CONFIG VALUES ARE OPTIONAL FOR BOTH DEV AND PROD AND HAVE SENSIBLE DEFAULTS.
+		apiSubpath  : getConfigValue('apiSubpath') || 'api',
+		apiDocsSubpath : getConfigValue('apiDocsSubpath') || 'api-docs',
+		apiVersion : '0.1',
+		logger: new (winston.Logger)({
+			transports: [
+				new (winston.transports.Console)({ json: true, colorize: true, prettyPrint: true}),
+				new (winston.transports.File)({ filename: getConfigValue('logFileName') || 'logfile.log' })
+			]
+		})
+	};
+
+//do some config cleanup
+config.VMC_APP_PORT = parseInt(String(config.VMC_APP_PORT).replace(/:/, ''), 10); //remove the colon if it was put in the config value
+config.apiSubpath = config.apiSubpath.replace(/\//g, ''); //remove slashes if they were put in the api subpath
+config.apiDocsSubpath = config.apiDocsSubpath.replace(/\//g, ''); //remove slashes if they were put in the api subpath
+config.publicUrl = config.publicUrl.replace(/\/$/, ''); //remove trailing slash if it was put in the public url
+
+//For dev environment only, assume the port has to be part of the public url
+if (environment === 'development' && config.VMC_APP_PORT !== 80) {
+	config.publicUrl = config.publicUrl + ':' + config.VMC_APP_PORT;
+}
+
+// Add api response codes. This is a subset of codes following this convention: https://dev.twitter.com/docs/error-codes-responses
+config.apiCodes = {
+	OK           : {name : 'OK', value : 200, description : 'Success' },
+	notModified  : {name : 'Not Modified', value: 304, description : 'There was no new data to return.' },
+	badRequest   : {name : 'Bad Request', value: 400, description : 'The request was invalid. An accompanying message will explain why.' },
+	unauthorized : {name : 'Unauthorized', value: 401, description : 'Authentication credentials were missing or incorrect.' },
+	forbidden    : {name : 'Forbidden', value: 403, description : 'The request is understood, but it has been refused or access is not allowed.' },
+	notFound     : {name : 'Not Found', value: 404, description : 'The URI requested is invalid or the requested resource does not exist.' },
+	serverError  : {name : 'Internal Server Error', value: 500, description : 'Something is broken. Please contact support.' }
+};
+
+// Setup application wide DB usage. 
+var db = mongoose.createConnection(config.dbconnstring);
+config.models = {};
+
+//TODO: db connection held open forever?
+db.on('error', function (err) {
+	config.logger.error('Failed to connect to DB: ' + err);
+});
+db.once('open', function () {
+	async.each(
+		entityNames,
+		function (entityName, callback) {
+			try {
+				config.models[entityName] = db.model(entityName, schemas[entityName]);
+			} catch (err) {
+				callback(err);
+			}
+			callback(null);
+		},
+		function (err) {
+			if (err) { config.logger.error(err); }
+		}
+	);
+
+	config.logger.info('Successfully connected to DB and created the document models.');
+
+	//In Dev mode, if DB is empty, populate some test data
+	if (environment === 'development') {
+		schemas.populateDB(config.models, config.logger, function (err) {
+			if (err) {
+				config.logger.error('Failed to populate db with test data.' + err);
+			}
+		});
+	}
+
+});
+
+
+
+module.exports.config = config;

localconfig.js.example

+'use strict';
+
+var localconfig =	{
+		//PUT THE CONNECTION INFO HERE. FOR EXAMPLE: mongodb://USERNAME:PASSWORD@SERVER:PORT/DBNAME
+		dbconnstring: 'YOUR_CONNECTION_STRING'
+		//SEE config.js FOR OTHER VALUES THAT CAN BE OVERRIDDEN HERE
+	};
+
+
+module.exports.localconfig = localconfig;

npm-shrinkwrap.json

+{
+  "name": "auto-api",
+  "version": "0.0.1",
+  "dependencies": {
+    "async": {
+      "version": "0.2.9",
+      "from": "async@"
+    },
+    "express": {
+      "version": "3.3.0",
+      "from": "express@",
+      "dependencies": {
+        "connect": {
+          "version": "2.8.0",
+          "from": "connect@2.8.0",
+          "dependencies": {
+            "qs": {
+              "version": "0.6.5",
+              "from": "qs@0.6.5"
+            },
+            "formidable": {
+              "version": "1.0.14",
+              "from": "formidable@1.0.14"
+            },
+            "cookie": {
+              "version": "0.0.5",
+              "from": "cookie@0.0.5"
+            },
+            "bytes": {
+              "version": "0.2.0",
+              "from": "bytes@0.2.0"
+            },
+            "pause": {
+              "version": "0.0.1",
+              "from": "pause@0.0.1"
+            },
+            "uid2": {
+              "version": "0.0.2",
+              "from": "uid2@0.0.2"
+            }
+          }
+        },
+        "commander": {
+          "version": "0.6.1",
+          "from": "commander@0.6.1"
+        },
+        "range-parser": {
+          "version": "0.0.4",
+          "from": "range-parser@0.0.4"
+        },
+        "mkdirp": {
+          "version": "0.3.4",
+          "from": "mkdirp@0.3.4"
+        },
+        "cookie": {
+          "version": "0.1.0",
+          "from": "cookie@0.1.0"
+        },
+        "buffer-crc32": {
+          "version": "0.2.1",
+          "from": "buffer-crc32@0.2.1"
+        },
+        "fresh": {
+          "version": "0.1.0",
+          "from": "fresh@0.1.0"
+        },
+        "methods": {
+          "version": "0.0.1",
+          "from": "methods@0.0.1"
+        },
+        "send": {
+          "version": "0.1.1",
+          "from": "send@0.1.1",
+          "dependencies": {
+            "mime": {
+              "version": "1.2.9",
+              "from": "mime@~1.2.9"
+            }
+          }
+        },
+        "cookie-signature": {
+          "version": "1.0.1",
+          "from": "cookie-signature@1.0.1"
+        },
+        "debug": {
+          "version": "0.7.2",
+          "from": "debug@*"
+        }
+      }
+    },
+    "JSON": {
+      "version": "1.0.0",
+      "from": "JSON@"
+    },
+    "mongodb": {
+      "version": "1.3.10",
+      "from": "mongodb@",
+      "dependencies": {
+        "bson": {
+          "version": "0.1.9",
+          "from": "bson@0.1.9"
+        },
+        "kerberos": {
+          "version": "0.0.2",
+          "from": "kerberos@latest"
+        }
+      }
+    },
+    "mongoose": {
+      "version": "3.6.12",
+      "from": "mongoose@",
+      "dependencies": {
+        "hooks": {
+          "version": "0.2.1",
+          "from": "hooks@0.2.1"
+        },
+        "ms": {
+          "version": "0.1.0",
+          "from": "ms@0.1.0"
+        },
+        "sliced": {
+          "version": "0.0.3",
+          "from": "sliced@0.0.3"
+        },
+        "muri": {
+          "version": "0.3.1",
+          "from": "muri@0.3.1"
+        },
+        "mpromise": {
+          "version": "0.2.1",
+          "from": "mpromise@0.2.1",
+          "dependencies": {
+            "sliced": {
+              "version": "0.0.4",
+              "from": "sliced@0.0.4"
+            }
+          }
+        },
+        "mpath": {
+          "version": "0.1.1",
+          "from": "mpath@0.1.1"
+        },
+        "regexp-clone": {
+          "version": "0.0.1",
+          "from": "regexp-clone@0.0.1"
+        }
+      }
+    },
+    "swagger-node-express": {
+      "version": "1.2.3",
+      "from": "swagger-node-express@",
+      "dependencies": {
+        "connect": {
+          "version": "2.8.0",
+          "from": "connect@>= 1.8.x",
+          "dependencies": {
+            "qs": {
+              "version": "0.6.5",
+              "from": "qs@0.6.5"
+            },
+            "formidable": {
+              "version": "1.0.14",
+              "from": "formidable@1.0.14"
+            },
+            "cookie-signature": {
+              "version": "1.0.1",
+              "from": "cookie-signature@1.0.1"
+            },
+            "buffer-crc32": {
+              "version": "0.2.1",
+              "from": "buffer-crc32@0.2.1"
+            },
+            "cookie": {
+              "version": "0.0.5",
+              "from": "cookie@0.0.5"
+            },
+            "send": {
+              "version": "0.1.1",
+              "from": "send@0.1.1",
+              "dependencies": {
+                "mime": {
+                  "version": "1.2.9",
+                  "from": "mime@~1.2.9"
+                },
+                "range-parser": {
+                  "version": "0.0.4",
+                  "from": "range-parser@0.0.4"
+                }
+              }
+            },
+            "bytes": {
+              "version": "0.2.0",
+              "from": "bytes@0.2.0"
+            },
+            "fresh": {
+              "version": "0.1.0",
+              "from": "fresh@0.1.0"
+            },
+            "pause": {
+              "version": "0.0.1",
+              "from": "pause@0.0.1"
+            },
+            "uid2": {
+              "version": "0.0.2",
+              "from": "uid2@0.0.2"
+            },
+            "debug": {
+              "version": "0.7.2",
+              "from": "debug@*"
+            }
+          }
+        },
+        "docco": {
+          "version": "0.4.0",
+          "from": "docco@0.4.x",
+          "dependencies": {
+            "commander": {
+              "version": "1.2.0",
+              "from": "commander@>=0.5.2",
+              "dependencies": {
+                "keypress": {
+                  "version": "0.1.0",
+                  "from": "keypress@0.1.x"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "winston": {
+      "version": "0.7.2",
+      "from": "winston@",
+      "dependencies": {
+        "colors": {
+          "version": "0.6.0-1",
+          "from": "colors@0.6.x"
+        },
+        "cycle": {
+          "version": "1.0.2",
+          "from": "cycle@1.0.x"
+        },
+        "eyes": {
+          "version": "0.1.8",
+          "from": "eyes@0.1.x"
+        },
+        "pkginfo": {
+          "version": "0.3.0",
+          "from": "pkginfo@0.3.x"
+        },
+        "request": {
+          "version": "2.16.6",
+          "from": "request@2.16.x",
+          "dependencies": {
+            "form-data": {
+              "version": "0.0.10",
+              "from": "form-data@~0.0.3",
+              "dependencies": {
+                "combined-stream": {
+                  "version": "0.0.4",
+                  "from": "combined-stream@~0.0.4",
+                  "dependencies": {
+                    "delayed-stream": {
+                      "version": "0.0.5",
+                      "from": "delayed-stream@0.0.5"
+                    }
+                  }
+                }
+              }
+            },
+            "mime": {
+              "version": "1.2.9",
+              "from": "mime@~1.2.7"
+            },
+            "hawk": {
+              "version": "0.10.2",
+              "from": "hawk@~0.10.2",
+              "dependencies": {
+                "hoek": {
+                  "version": "0.7.6",
+                  "from": "hoek@0.7.x"
+                },
+                "boom": {
+                  "version": "0.3.8",
+                  "from": "boom@0.3.x"
+                },
+                "cryptiles": {
+                  "version": "0.1.3",
+                  "from": "cryptiles@0.1.x"
+                },
+                "sntp": {
+                  "version": "0.1.4",
+                  "from": "sntp@0.1.x"
+                }
+              }
+            },
+            "node-uuid": {
+              "version": "1.4.0",
+              "from": "node-uuid@~1.4.0"
+            },
+            "cookie-jar": {
+              "version": "0.2.0",
+              "from": "cookie-jar@~0.2.0"
+            },
+            "aws-sign": {
+              "version": "0.2.0",
+              "from": "aws-sign@~0.2.0"
+            },
+            "oauth-sign": {
+              "version": "0.2.0",
+              "from": "oauth-sign@~0.2.0"
+            },
+            "forever-agent": {
+              "version": "0.2.0",
+              "from": "forever-agent@~0.2.0"
+            },
+            "tunnel-agent": {
+              "version": "0.2.0",
+              "from": "tunnel-agent@~0.2.0"
+            },
+            "json-stringify-safe": {
+              "version": "3.0.0",
+              "from": "json-stringify-safe@~3.0.0"
+            },
+            "qs": {
+              "version": "0.5.6",
+              "from": "qs@~0.5.4"
+            }
+          }
+        },
+        "stack-trace": {
+          "version": "0.0.6",
+          "from": "stack-trace@0.0.x"
+        }
+      }
+    }
+  }
+}
+{
+  "name": "auto-api",
+  "description": "Reference implementation for API that, using the entity definitions, auto-generates all CRUD REST operations and active Swagger docs.",
+  "version": "0.0.1",
+  "keywords": [
+    "api",
+    "REST",
+    "CRUD",
+    "swagger",
+    "active docs"
+  ],
+  "maintainers": [
+    {
+      "name": "David Hessing",
+      "email": "dhessing@topcoder.com",
+      "web": "http://www.topcoder.com"
+    }
+  ],
+  "dependencies": {
+    "async": "~0.2.9",
+    "express": "~3.3.0",
+    "JSON": "~1.0.0",
+    "mongodb": "~1.3.10",
+    "mongoose": "~3.6.12",
+    "swagger-node-express": "~1.2.3",
+    "winston": "~0.7.2"
+  }
+}
+'use strict';
+var mongoose = require('mongoose');
+var swagger = require('swagger-node-express');
+var JSON = require('JSON');
+var schemas = require('./../schemas');
+var config = require("./../config").config;
+
+var logger = config.logger,
+	models = config.models;
+
+
+//Generic helper methods
+var parseObjectId = function (id) {
+	var objectId = null;
+	try {
+		objectId = mongoose.Types.ObjectId(id);
+	} catch (err) { }
+	return objectId;
+};
+
+//swagger implemented errors poorly. See swagger.js line 515. This works better and provides more info in the response.
+var sendErrorResponse = function (res, errorCode, moreInfo) {
+	errorCode.details = moreInfo || 'No additional information available.';
+	res.send(errorCode, errorCode.value);
+};
+
+//Generalized find by ID method with swagger support. The entity name must match the name in the swagger models and the name in the config.models
+module.exports.findEntityById = function (entityName) {
+	var cappedName = entityName.charAt(0).toUpperCase() + entityName.slice(1);
+	var specAndFunction = {
+		spec: {
+			description: 'Operations on a ' + cappedName,
+			path: '/' + entityName + 's/{id}',
+			notes: 'Returns a ' + entityName + ' based on an id. Note the id is a MongoDB ObjectID.',
+			summary: 'Find ' + entityName + ' by ID',
+			method: 'GET',
+			params: [swagger.pathParam('id', 'ObjectId of the ' + entityName + ' that needs to be retrieved.', 'string')],
+			responseClass: entityName,
+			nickname : 'get' + cappedName + 'ById'
+		},
+		action: function (req, res) {
+			var id = parseObjectId(req.params.id);
+			if (!id) {
+				sendErrorResponse(res, config.apiCodes.badRequest, 'Value \'' + req.params.id + '\' is not a valid MongoDB ObjectID');
+				return;
+			}
+			logger.debug('Retrieving ' + cappedName + ' with id: ' + id);
+			models[entityName].findById(id, function (err, entityDoc) {
+				if (err) {
+					logger.error(err);
+					sendErrorResponse(res, config.apiCodes.serverError, 'MongoDB returned an error. An admin will need to investigate.');
+					return;
+				}
+				if (!entityDoc) {
+					sendErrorResponse(res, config.apiCodes.notFound, 'No ' + entityName + ' found with id \'' + id + '\'.');
+					return;
+				}
+				res.send(entityDoc);
+			});
+		}
+	};
+	return specAndFunction;
+};
+
+//Generalized find all with swagger support. The entity name must match the name in the swagger models and the name in the config.models
+module.exports.getAllEntities = function (entityName) {
+	var cappedName = entityName.charAt(0).toUpperCase() + entityName.slice(1);
+	var specAndFunction = {
+		spec: {
+			description: 'Operations on a ' + cappedName,
+			path: '/' + entityName + 's',
+			notes: 'Returns all ' + entityName + 's.',
+			summary: 'Find all ' + entityName + 's',
+			method: 'GET',
+			params: [],
+			responseClass: entityName,
+			nickname : 'getAll' + cappedName
+		},
+		action: function (req, res) {
+			logger.debug('Retrieving all ' + cappedName + 's.');
+			models[entityName].find({}, function (err, entityDocs) {
+				if (err) {
+					logger.error(err);
+					sendErrorResponse(res, config.apiCodes.serverError, 'MongoDB returned an error. An admin will need to investigate.');
+					return;
+				}
+				if (!entityDocs) {
+					sendErrorResponse(res, config.apiCodes.notFound, 'No ' + entityName + 's were found.');
+					return;
+				}
+				res.send(entityDocs);
+			});
+		}
+	};
+	return specAndFunction;
+};
+
+//Generalized insert method with swagger support. The entity name must match the name in the swagger models and the name in the config.models
+module.exports.addEntity = function (entityName) {
+	var cappedName = entityName.charAt(0).toUpperCase() + entityName.slice(1);
+	var specAndFunction = {
+		spec: {
+			description: 'Operations on a ' + cappedName,
+			path: '/' + entityName + 's',
+			notes: 'Creates a new ' + entityName + '. Enclose all parameter names, strings, and date strings in "double-quotes". Leave out the "_id" parameter since it will be automtically assigned by the DB.',
+			summary: 'Creates a new ' + entityName,
+			method: 'POST',
+			params: [swagger.postParam(entityName, 'JSON of the new ' + entityName + ' to create.')],
+			nickname : 'create' + cappedName
+		},
+		action: function (req, res) {
+			var newEntity = req.body;
+			if (!newEntity || Object.keys(newEntity).length === 0) {
+				sendErrorResponse(res, config.apiCodes.badRequest, 'JSON for the ' + entityName + ' to create is missing from the body of the request.');
+				return;
+			}
+			logger.debug('About to create: ' + newEntity);
+			models[entityName].create(newEntity, function (err, newDoc) {
+				if (err) {
+					logger.error(err);
+					sendErrorResponse(res, config.apiCodes.serverError, 'MongoDB returned an error. An admin will need to investigate.');
+					return;
+				}
+				res.send(newDoc._id);
+			});
+		}
+	};
+	return specAndFunction;
+};
+
+//Generalized update method with swagger support. The entity name must match the name in the swagger models and the name in the config.models
+//NOTE that per mongoose docs, schema validation rules are not applied when using the model.update method!
+module.exports.updateEntity = function (entityName) {
+	var cappedName = entityName.charAt(0).toUpperCase() + entityName.slice(1),
+		conditions = {};
+
+	var specAndFunction = {
+		spec: {
+			description: 'Operations on a ' + cappedName,
+			path: '/' + entityName + 's',
+			notes: 'Updates an existing ' + entityName + '. Enclose all parameter names, strings, and date strings in "double-quotes".',
+			summary: 'Updates an existing ' + entityName,
+			method: 'PUT',
+			params: [swagger.postParam(entityName, 'JSON of the ' + entityName + ' to update. "_id" must be specified and exist.')],
+			nickname : 'edit' + cappedName
+		},
+		action: function (req, res) {
+			var modifiedEntity = req.body;
+			if (!modifiedEntity || Object.keys(modifiedEntity).length === 0) {
+				sendErrorResponse(res, config.apiCodes.badRequest, 'JSON for the ' + entityName + ' to update is missing from the body of the request.');
+				return;
+			}
+			if (!modifiedEntity._id || !parseObjectId(modifiedEntity._id)) {
+				sendErrorResponse(res, config.apiCodes.badRequest, 'The _id for the ' + entityName + ' to update is missing or is not a valid MongoDB ObjectID.');
+				return;
+			}
+			logger.debug('About to update: ' + modifiedEntity);
+			conditions._id = modifiedEntity._id;
+			delete modifiedEntity._id;
+			models[entityName].update(conditions, modifiedEntity, function (err, numberAffected) {
+				if (err || numberAffected > 1) {
+					logger.error(err);
+					sendErrorResponse(res, config.apiCodes.serverError, 'MongoDB returned an error. An admin will need to investigate.');
+					return;
+				}
+				if (numberAffected === 0) {
+					sendErrorResponse(res, config.apiCodes.notFound, 'No ' + entityName + ' was found with the specified _id.');
+					return;
+				}
+				res.send(config.apiCodes.OK);
+			});
+		}
+	};
+	return specAndFunction;
+};
+
+
+//Generalized delete by ID method with swagger support. The entity name must match the name in the swagger models and the name in the config.models
+module.exports.deleteEntityById = function (entityName) {
+	var cappedName = entityName.charAt(0).toUpperCase() + entityName.slice(1);
+	var specAndFunction = {
+		spec: {
+			description: 'Operations on a ' + cappedName,
+			path: '/' + entityName + 's/{id}',
+			notes: 'Deletes a ' + entityName + ' based on an id. Note the id is a MongoDB ObjectID.',
+			summary: 'Delete ' + entityName + ' by ID',
+			method: 'DELETE',
+			params: [swagger.pathParam('id', 'ObjectId of the ' + entityName + ' that needs to be deleted.', 'string')],
+			responseClass: entityName,
+			nickname : 'remove' + cappedName + 'ById'
+		},
+		action: function (req, res) {
+			var id = parseObjectId(req.params.id);
+			if (!id) {
+				sendErrorResponse(res, config.apiCodes.badRequest, 'Value \'' + req.params.id + '\' is not a valid MongoDB ObjectID');
+				return;
+			}
+			logger.debug('Deleting ' + cappedName + ' with id: ' + id);
+			models[entityName].remove({_id : id}, function (err) {
+				if (err) {
+					logger.error(err);
+					sendErrorResponse(res, config.apiCodes.serverError, 'MongoDB returned an error. An admin will need to investigate.');
+					return;
+				}
+				res.send(config.apiCodes.OK);
+			});
+		}
+	};
+	return specAndFunction;
+};
+'use strict';
+var mongoose = require('mongoose');
+var async = require('async');
+
+var validateEmail = function (email) {
+    return (/^[a-zA-Z0-9._\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,4}$/).test(email);
+};
+
+//NOTE: mongoose schema names and swagger model names must match exactly!
+//Define Mongoose Schemas. See http://mongoosejs.com/docs/guide.html
+var user = new mongoose.Schema({
+	firstName: String,
+	lastName: String,
+    email: {type : String, validate: [validateEmail, 'Email address is not valid.'] }
+});
+
+var asset = new mongoose.Schema({
+	name: String,
+	typeCode: String,
+	typeDescription: String,
+	purchaseDate: { type: Date, default: Date.now },
+	units: Number,
+	active: Boolean
+
+
+});
+
+
+//Define swagger models. See swagger model spec here: https://github.com/wordnik/swagger-core/wiki/Datatypes
+//Maybe a mongoose plugin could auto-generate this off the schema or model? That would be much cleaner then having to define everything twice.
+var swaggerModels = {
+	models: {
+		user: {
+			id: 'user',
+			properties: {
+				_id: {
+					type: 'string',
+					description: 'MongoDB unique ObjectID.'
+				},
+				firstName: { type: 'string' },
+				lastName: { type: 'string' },
+				email: { type: 'string' }
+			}
+		},
+
+		asset: {
+			id: 'asset',
+			properties: {
+				_id: {
+					type: 'string',
+					description: 'MongoDB unique ObjectID.'
+				},
+				name: { type: 'string' },
+				typeCode: {
+					type: 'string',
+					description: 'Code for this asset determined by the business.'
+				},
+				typeDescription: { type: 'string' },
+				purchaseDate: { type: 'date' },
+				units: { type: 'int' },
+				active: { type: 'boolean' }
+			}
+		}
+	}
+};
+
+
+//called in dev mode on startup to populate data if db is empty. It's here so it can be easily updated if models are changed.
+var populateDB = function (models, logger, callback) {
+	logger.info('Attempting to populate DB with test data. Checking whether collections are empty...');
+	var saves = [];
+
+	var createUserData = function (cb) {
+		models.user.count({}, function (err, count) {
+			if (err) { cb(err); return; }
+			if (count === 0) {
+				logger.info('No users found in dev DB. Creating some sample users.');
+				var user0 = new models.user({ firstName: 'Joel', lastName: 'Test0', email: 'test0@domain.com' }),
+					user1 = new models.user({ firstName: 'Amos', lastName: 'Test1', email: 'test1@domain.com' }),
+					user2 = new models.user({ firstName: 'Micah', lastName: 'Test2', email: 'test2@domain.com' });
+				saves.push(user0);
+				saves.push(user1);
+				saves.push(user2);
+			}
+			cb(null);
+		});
+	};
+
+	var createAssetData = function (cb) {
+		models.asset.count({}, function (err, count) {
+			if (err) { cb(err); return; }
+			if (count === 0) {
+				logger.info('No assets found in dev DB. Creating some sample assets.');
+				var asset0 = new models.asset({ name: 'Asset0', typeCode: 'Code0', typeDescription: 'Asset of type code0', purchaseDate: new Date(), active: true  }),
+					asset1 = new models.asset({ name: 'Asset1', typeCode: 'Code1', typeDescription: 'Asset of type code1', purchaseDate: new Date(), active: false  }),
+					asset2 = new models.asset({ name: 'Asset1', typeCode: 'Code1', typeDescription: 'Asset of type code2', active: true, units: 3 });
+				saves.push(asset0);
+				saves.push(asset1);
+				saves.push(asset2);
+			}
+			cb(null);
+		});
+	};
+
+	async.parallel([createUserData, createAssetData], function (err) {
+		if (err) { callback(err); return; }
+		if (saves.length === 0) {
+			logger.info('All collections are already populated. No test data was added.');
+			return;
+		}
+		async.eachSeries(
+			saves,
+			function (doc, cb) {
+				doc.save(cb);
+			},
+			function (err) {
+				if (err) { callback(err); return; }
+				logger.info('Successfully populated DB with ' + saves.length + ' test documents.');
+				callback(null);
+			}
+		);
+	});
+};
+
+
+
+
+
+module.exports.user = user;
+module.exports.asset = asset;
+
+module.exports.populateDB = populateDB;
+module.exports.swaggerModels = swaggerModels;
+'use strict';
+var path = require('path');
+var url = require('url');
+var async = require('async');
+var express = require('express');
+var swagger = require('swagger-node-express');
+var api = require('./routes/api');
+var schemas = require('./schemas');
+var config = require('./config').config;
+var logger = config.logger;
+
+
+var app = express(),
+	apiApp = express(),
+	entityNames = Object.keys(schemas.swaggerModels.models);
+
+app.set('port', config.VMC_APP_PORT);
+app.use(express.bodyParser());
+app.use('/' + config.apiSubpath, apiApp); //put the api under the configured subpath. 
+
+
+app.use(express.static(path.join(__dirname, 'static')));
+//logger.debug(__dirname);
+
+
+swagger.setAppHandler(apiApp);
+swagger.addModels(schemas.swaggerModels);
+
+async.each(
+	entityNames,
+	function (entityName, callback) {
+		try {
+			swagger.addGet(api.getAllEntities(entityName));
+			swagger.addGet(api.findEntityById(entityName));
+			swagger.addPost(api.addEntity(entityName));
+			swagger.addPut(api.updateEntity(entityName));
+			swagger.addDelete(api.deleteEntityById(entityName));
+		} catch (err) {
+			callback(err);
+		}
+		callback(null);
+	},
+	function (err) {
+		if (err) { logger.error(err); }
+	}
+);
+
+/* Alternatively, add routes by individual entity	
+swagger.addGet(api.getAllEntities('user'));
+swagger.addGet(api.getAllEntities('asset'));
+swagger.addGet(api.findEntityById('user'));
+swagger.addGet(api.findEntityById('asset'));
+swagger.addPost(api.addEntity('user'));
+swagger.addPost(api.addEntity('asset'));
+*/
+
+swagger.configureSwaggerPaths('', '/' + config.apiDocsSubpath, '');
+swagger.configure(url.resolve(config.publicUrl, config.apiSubpath), config.apiVersion);
+
+
+
+app.listen(config.VMC_APP_PORT);
+logger.info('Server listening on port ' + app.get('port'));
+

static/activedocs/css/hightlight.default.css

+/*
+
+Original style from softwaremaniacs.org (c) Ivan Sagalaev <Maniac@SoftwareManiacs.Org>
+
+*/
+
+pre code {
+  display: block; padding: 0.5em;
+  background: #F0F0F0;
+}
+
+pre code,
+pre .subst,
+pre .tag .title,
+pre .lisp .title,
+pre .clojure .built_in,
+pre .nginx .title {
+  color: black;
+}
+
+pre .string,
+pre .title,
+pre .constant,
+pre .parent,
+pre .tag .value,
+pre .rules .value,
+pre .rules .value .number,
+pre .preprocessor,
+pre .ruby .symbol,
+pre .ruby .symbol .string,
+pre .aggregate,
+pre .template_tag,
+pre .django .variable,
+pre .smalltalk .class,
+pre .addition,
+pre .flow,
+pre .stream,
+pre .bash .variable,
+pre .apache .tag,
+pre .apache .cbracket,
+pre .tex .command,
+pre .tex .special,
+pre .erlang_repl .function_or_atom,
+pre .markdown .header {
+  color: #800;
+}
+
+pre .comment,
+pre .annotation,
+pre .template_comment,
+pre .diff .header,
+pre .chunk,
+pre .markdown .blockquote {
+  color: #888;
+}
+
+pre .number,
+pre .date,
+pre .regexp,
+pre .literal,
+pre .smalltalk .symbol,
+pre .smalltalk .char,
+pre .go .constant,
+pre .change,
+pre .markdown .bullet,
+pre .markdown .link_url {
+  color: #080;
+}
+
+pre .label,
+pre .javadoc,
+pre .ruby .string,
+pre .decorator,
+pre .filter .argument,
+pre .localvars,
+pre .array,
+pre .attr_selector,
+pre .important,
+pre .pseudo,
+pre .pi,
+pre .doctype,
+pre .deletion,
+pre .envvar,
+pre .shebang,
+pre .apache .sqbracket,
+pre .nginx .built_in,
+pre .tex .formula,
+pre .erlang_repl .reserved,
+pre .prompt,
+pre .markdown .link_label,
+pre .vhdl .attribute,
+pre .clojure .attribute,
+pre .coffeescript .property {
+  color: #88F
+}
+
+pre .keyword,
+pre .id,
+pre .phpdoc,
+pre .title,
+pre .built_in,
+pre .aggregate,
+pre .css .tag,
+pre .javadoctag,
+pre .phpdoc,
+pre .yardoctag,
+pre .smalltalk .class,
+pre .winutils,
+pre .bash .variable,
+pre .apache .tag,
+pre .go .typename,
+pre .tex .command,
+pre .markdown .strong,
+pre .request,
+pre .status {
+  font-weight: bold;
+}
+
+pre .markdown .emphasis {
+  font-style: italic;
+}
+
+pre .nginx .built_in {
+  font-weight: normal;
+}
+
+pre .coffeescript .javascript,
+pre .javascript .xml,
+pre .tex .formula,
+pre .xml .javascript,
+pre .xml .vbscript,
+pre .xml .css,
+pre .xml .cdata {
+  opacity: 0.5;
+}

static/activedocs/css/screen.css

+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    font-size: 100%;
+    font: inherit;
+    vertical-align: baseline;
+}
+
+body {
+    line-height: 1;
+}
+
+ol, ul {
+    list-style: none;
+}
+
+table {
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+
+caption, th, td {
+    text-align: left;
+    font-weight: normal;
+    vertical-align: middle;
+}
+
+q, blockquote {
+    quotes: none;
+}
+
+q:before, q:after, blockquote:before, blockquote:after {
+    content: "";
+    content: none;
+}
+
+a img {
+    border: none;
+}
+
+article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary {
+    display: block;
+}
+
+h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
+    text-decoration: none;
+}
+
+h1 a:hover, h2 a:hover, h3 a:hover, h4 a:hover, h5 a:hover, h6 a:hover {
+    text-decoration: underline;
+}
+
+h1 span.divider, h2 span.divider, h3 span.divider, h4 span.divider, h5 span.divider, h6 span.divider {
+    color: #aaaaaa;
+}
+
+h1 {
+    color: black;
+    font-size: 1.5em;
+    line-height: 1.3em;
+    padding: 10px 0 10px 0;
+    font-family: "Droid Sans", sans-serif;
+    font-weight: bold;
+}
+
+h2 {
+    color: black;
+    font-size: 1.3em;
+    padding: 10px 0 10px 0;
+}
+
+h2 a {
+    color: black;
+}
+
+h2 span.sub {
+    font-size: 0.7em;
+    color: #999999;
+    font-style: italic;
+}
+
+h2 span.sub a {
+    color: #777777;
+}
+
+h3 {
+    color: black;
+    font-size: 1.1em;
+    padding: 10px 0 10px 0;
+}
+
+.heading_with_menu {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+}
+
+.heading_with_menu h1, .heading_with_menu h2, .heading_with_menu h3, .heading_with_menu h4, .heading_with_menu h5, .heading_with_menu h6 {
+    display: block;
+    clear: none;
+    float: left;
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+    box-sizing: border-box;
+    width: 60%;
+}
+
+.heading_with_menu ul {
+    display: block;
+    clear: none;
+    float: right;
+    -moz-box-sizing: border-box;
+    -webkit-box-sizing: border-box;
+    -ms-box-sizing: border-box;
+    box-sizing: border-box;
+    margin-top: 10px;
+}
+
+input.parameter {
+    width: 300px;
+    border: 1px solid #aaa;
+}
+
+.body-textarea {
+    width: 300px;
+    height: 100px;
+    border: 1px solid #aaa;
+}
+
+p {
+    line-height: 1.4em;
+    padding: 0 0 10px;
+    color: #333333;
+}
+
+ol {
+    margin: 0px 0 10px;
+    padding: 0 0 0 18px;
+    list-style-type: decimal;
+}
+
+ol li {
+    padding: 5px 0px;
+    font-size: 0.9em;
+    color: #333333;
+}
+
+.markdown h3 {
+    color: #547f00;
+}
+
+.markdown h4 {
+    color: #666666;
+}
+
+.markdown pre {
+    font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
+    background-color: #fcf6db;
+    border: 1px solid #e5e0c6;
+    padding: 10px;
+    margin: 0 0 10px 0;
+}
+
+.markdown pre code {
+    line-height: 1.6em;
+}
+
+.markdown p code, .markdown li code {
+    font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
+    background-color: #f0f0f0;
+    color: black;
+    padding: 1px 3px;
+}
+
+.markdown ol, .markdown ul {
+    font-family: "Droid Sans", sans-serif;
+    margin: 5px 0 10px;
+    padding: 0 0 0 18px;
+    list-style-type: disc;
+}
+
+.markdown ol li, .markdown ul li {
+    padding: 3px 0px;
+    line-height: 1.4em;
+    color: #333333;
+}
+
+div.gist {
+    margin: 20px 0 25px 0 !important;
+}
+
+p.big, div.big p {
+    font-size: 1em;
+    margin-bottom: 10px;
+}
+
+span.weak {
+    color: #666666;
+}
+
+span.blank, span.empty {
+    color: #888888;
+    font-style: italic;
+}
+
+a {
+    color: #547f00;
+}
+
+b, strong {
+    font-family: "Droid Sans", sans-serif;
+    font-weight: bold;
+}
+
+.code {
+    font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
+}
+
+pre {
+    font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
+    background-color: #fcf6db;
+    border: 1px solid #e5e0c6;
+    padding: 10px;
+}
+
+pre code {
+    line-height: 1.6em;
+}
+
+.required {
+    font-weight: bold;
+}
+
+table.fullwidth {
+    width: 100%;
+}
+
+table thead tr th {
+    padding: 5px;
+    font-size: 0.9em;
+    color: #666666;
+    border-bottom: 1px solid #999999;
+}
+
+table tbody tr.offset {
+    background-color: #f5f5f5;
+}
+
+table tbody tr td {
+    padding: 6px;
+    font-size: 0.9em;
+    border-bottom: 1px solid #cccccc;
+    vertical-align: top;
+    line-height: 1.3em;
+}
+
+table tbody tr:last-child td {
+    border-bottom: none;
+}
+
+table tbody tr.offset {
+    background-color: #f0f0f0;
+}
+
+form.form_box {
+    background-color: #ebf3f9;
+    border: 1px solid #c3d9ec;
+    padding: 10px;
+}
+
+form.form_box label {
+    color: #0f6ab4 !important;
+}
+
+form.form_box input[type=submit] {
+    display: block;
+    padding: 10px;
+}
+
+form.form_box p {
+    font-size: 0.9em;
+    padding: 0 0 15px;
+    color: #7e7b6d;
+}
+
+form.form_box p a {
+    color: #646257;
+}
+
+form.form_box p strong {
+    color: black;
+}
+
+form.form_box p.weak {
+    font-size: 0.8em;
+}
+
+form.formtastic fieldset.inputs ol li p.inline-hints {
+    margin-left: 0;
+    font-style: italic;
+    font-size: 0.9em;
+    margin: 0;
+}
+
+form.formtastic fieldset.inputs ol li label {
+    display: block;
+    clear: both;
+    width: auto;
+    padding: 0 0 3px;
+    color: #666666;
+}
+
+form.formtastic fieldset.inputs ol li label abbr {
+    padding-left: 3px;
+    color: #888888;
+}
+
+form.formtastic fieldset.inputs ol li.required label {
+    color: black;
+}
+
+form.formtastic fieldset.inputs ol li.string input, form.formtastic fieldset.inputs ol li.url input, form.formtastic fieldset.inputs ol li.numeric input {
+    display: block;
+    padding: 4px;
+    width: auto;
+    clear: both;
+}
+
+form.formtastic fieldset.inputs ol li.string input.title, form.formtastic fieldset.inputs ol li.url input.title, form.formtastic fieldset.inputs ol li.numeric input.title {
+    font-size: 1.3em;
+}
+
+form.formtastic fieldset.inputs ol li.text textarea {
+    font-family: "Droid Sans", sans-serif;
+    height: 250px;
+    padding: 4px;
+    display: block;
+    clear: both;
+}
+
+form.formtastic fieldset.inputs ol li.select select {
+    display: block;
+    clear: both;
+}
+
+form.formtastic fieldset.inputs ol li.boolean {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+}
+
+form.formtastic fieldset.inputs ol li.boolean input {
+    display: block;
+    float: left;
+    clear: none;
+    margin: 0 5px 0 0;
+}
+
+form.formtastic fieldset.inputs ol li.boolean label {
+    display: block;
+    float: left;
+    clear: none;
+    margin: 0;
+    padding: 0;
+}
+
+form.formtastic fieldset.buttons {
+    margin: 0;
+    padding: 0;
+}
+
+form.fullwidth ol li.string input, form.fullwidth ol li.url input, form.fullwidth ol li.text textarea, form.fullwidth ol li.numeric input {
+    width: 500px !important;
+}
+
+body {
+    font-family: "Droid Sans", sans-serif;
+}
+
+body #content_message {
+    margin: 10px 15px;
+    font-style: italic;
+    color: #999999;
+}
+
+body #header {
+    background-color: #89bf04;
+    padding: 14px;
+}
+
+body #header a#logo {
+    font-size: 1.5em;
+    font-weight: bold;
+    text-decoration: none;
+    background: transparent url(../images/logo_small.png) no-repeat left center;
+    padding: 20px 0 20px 40px;
+    color: white;
+}
+
+body #header form#api_selector {
+    display: block;
+    clear: none;
+    float: right;
+}
+
+body #header form#api_selector .input {
+    display: block;
+    clear: none;
+    float: left;
+    margin: 0 10px 0 0;
+}
+
+body #header form#api_selector .input input {
+    font-size: 0.9em;
+    padding: 3px;
+    margin: 0;
+}
+
+body #header form#api_selector .input input#input_baseUrl {
+    width: 400px;
+}
+
+body #header form#api_selector .input input#input_apiKey {
+    width: 200px;
+}
+
+body #header form#api_selector .input a#explore {
+    display: block;
+    text-decoration: none;
+    font-weight: bold;
+    padding: 6px 8px;
+    font-size: 0.9em;
+    color: white;
+    background-color: #547f00;
+    -moz-border-radius: 4px;
+    -webkit-border-radius: 4px;
+    -o-border-radius: 4px;
+    -ms-border-radius: 4px;
+    -khtml-border-radius: 4px;
+    border-radius: 4px;
+}
+
+body #header form#api_selector .input a#explore:hover {
+    background-color: #547f00;
+}
+
+body p#colophon {
+    margin: 0 15px 40px 15px;
+    padding: 10px 0;
+    font-size: 0.8em;
+    border-top: 1px solid #dddddd;
+    font-family: "Droid Sans", sans-serif;
+    color: #999999;
+    font-style: italic;
+}
+
+body p#colophon a {
+    text-decoration: none;
+    color: #547f00;
+}
+
+body ul#resources {
+    font-family: "Droid Sans", sans-serif;
+    font-size: 0.9em;
+}
+
+body ul#resources li.resource {
+    border-bottom: 1px solid #dddddd;
+}
+
+body ul#resources li.resource:last-child {
+    border-bottom: none;
+}
+
+body ul#resources li.resource div.heading {
+    border: 1px solid transparent;
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+}
+
+body ul#resources li.resource div.heading h2 {
+    color: #999999;
+    padding-left: 0;
+    display: block;
+    clear: none;
+    float: left;
+    font-family: "Droid Sans", sans-serif;
+    font-weight: bold;
+}
+
+body ul#resources li.resource div.heading h2 a {
+    color: #999999;
+}
+
+body ul#resources li.resource div.heading h2 a:hover {
+    color: black;
+}
+
+body ul#resources li.resource div.heading ul.options {
+    overflow: hidden;
+    padding: 0;
+    display: block;
+    clear: none;
+    float: right;
+    margin: 14px 10px 0 0;
+}
+
+body ul#resources li.resource div.heading ul.options li {
+    float: left;
+    clear: none;
+    margin: 0;
+    padding: 2px 10px;
+    border-right: 1px solid #dddddd;
+}
+
+body ul#resources li.resource div.heading ul.options li:first-child, body ul#resources li.resource div.heading ul.options li.first {
+    padding-left: 0;
+}
+
+body ul#resources li.resource div.heading ul.options li:last-child, body ul#resources li.resource div.heading ul.options li.last {
+    padding-right: 0;
+    border-right: none;
+}
+
+body ul#resources li.resource div.heading ul.options li {
+    color: #666666;
+    font-size: 0.9em;
+}
+
+body ul#resources li.resource div.heading ul.options li a {
+    color: #aaaaaa;
+    text-decoration: none;
+}
+
+body ul#resources li.resource div.heading ul.options li a:hover {
+    text-decoration: underline;
+    color: black;
+}
+
+body ul#resources li.resource:hover div.heading h2 a, body ul#resources li.resource.active div.heading h2 a {
+    color: black;
+}
+
+body ul#resources li.resource:hover div.heading ul.options li a, body ul#resources li.resource.active div.heading ul.options li a {
+    color: #555555;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+    margin: 0 0 10px;
+    padding: 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+    margin: 0;
+    padding: 0;
+    background-color: #e7f0f7;
+    border: 1px solid #c3d9ec;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 {
+    display: block;
+    clear: none;
+    float: left;
+    width: auto;
+    margin: 0;
+    padding: 0;
+    line-height: 1.1em;
+    color: black;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span {
+    margin: 0;
+    padding: 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.http_method a {
+    text-transform: uppercase;
+    background-color: #0f6ab4;
+    text-decoration: none;
+    color: white;
+    display: inline-block;
+    width: 50px;
+    font-size: 0.7em;
+    text-align: center;
+    padding: 7px 0 4px 0;
+    -moz-border-radius: 2px;
+    -webkit-border-radius: 2px;
+    -o-border-radius: 2px;
+    -ms-border-radius: 2px;
+    -khtml-border-radius: 2px;
+    border-radius: 2px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path {
+    padding-left: 10px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path a {
+    color: black;
+    text-decoration: none;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading h3 span.path a:hover {
+    text-decoration: underline;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options {
+    overflow: hidden;
+    padding: 0;
+    display: block;
+    clear: none;
+    float: right;
+    margin: 6px 10px 0 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li {
+    float: left;
+    clear: none;
+    margin: 0;
+    padding: 2px 10px;
+    border-right: 1px solid #dddddd;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.first {
+    padding-left: 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li.last {
+    padding-right: 0;
+    border-right: none;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li {
+    border-right-color: #c3d9ec;
+    color: #0f6ab4;
+    font-size: 0.9em;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a {
+    color: #0f6ab4;
+    text-decoration: none;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.heading ul.options li a.active {
+    text-decoration: underline;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content {
+    background-color: #ebf3f9;
+    border: 1px solid #c3d9ec;
+    border-top: none;
+    padding: 10px;
+    -moz-border-radius-bottomleft: 6px;
+    -webkit-border-bottom-left-radius: 6px;
+    -o-border-bottom-left-radius: 6px;
+    -ms-border-bottom-left-radius: 6px;
+    -khtml-border-bottom-left-radius: 6px;
+    border-bottom-left-radius: 6px;
+    -moz-border-radius-bottomright: 6px;
+    -webkit-border-bottom-right-radius: 6px;
+    -o-border-bottom-right-radius: 6px;
+    -ms-border-bottom-right-radius: 6px;
+    -khtml-border-bottom-right-radius: 6px;
+    border-bottom-right-radius: 6px;
+    margin: 0 0 20px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content h4 {
+    color: #0f6ab4;
+    font-size: 1.1em;
+    margin: 0;
+    padding: 15px 0 5px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content form input[type='text'].error {
+    outline: 2px solid black;
+    outline-color: #cc0000;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header input.submit {
+    display: block;
+    clear: none;
+    float: left;
+    padding: 6px 8px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header img {
+    display: block;
+    clear: none;
+    float: right;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.sandbox_header a {
+    padding: 4px 0 0 10px;
+    color: #6fa5d2;
+    display: inline-block;
+    font-size: 0.9em;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.get div.content div.response div.block pre {
+    font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
+    padding: 10px;
+    font-size: 0.9em;
+    max-height: 400px;
+    overflow-y: auto;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+    margin: 0 0 10px;
+    padding: 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+    margin: 0;
+    padding: 0;
+    background-color: #e7f6ec;
+    border: 1px solid #c3e8d1;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 {
+    display: block;
+    clear: none;
+    float: left;
+    width: auto;
+    margin: 0;
+    padding: 0;
+    line-height: 1.1em;
+    color: black;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span {
+    margin: 0;
+    padding: 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.http_method a {
+    text-transform: uppercase;
+    background-color: #10a54a;
+    text-decoration: none;
+    color: white;
+    display: inline-block;
+    width: 50px;
+    font-size: 0.7em;
+    text-align: center;
+    padding: 7px 0 4px 0;
+    -moz-border-radius: 2px;
+    -webkit-border-radius: 2px;
+    -o-border-radius: 2px;
+    -ms-border-radius: 2px;
+    -khtml-border-radius: 2px;
+    border-radius: 2px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path {
+    padding-left: 10px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path a {
+    color: black;
+    text-decoration: none;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading h3 span.path a:hover {
+    text-decoration: underline;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options {
+    overflow: hidden;
+    padding: 0;
+    display: block;
+    clear: none;
+    float: right;
+    margin: 6px 10px 0 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li {
+    float: left;
+    clear: none;
+    margin: 0;
+    padding: 2px 10px;
+    border-right: 1px solid #dddddd;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.first {
+    padding-left: 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li.last {
+    padding-right: 0;
+    border-right: none;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li {
+    border-right-color: #c3e8d1;
+    color: #10a54a;
+    font-size: 0.9em;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a {
+    color: #10a54a;
+    text-decoration: none;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.heading ul.options li a.active {
+    text-decoration: underline;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content {
+    background-color: #ebf7f0;
+    border: 1px solid #c3e8d1;
+    border-top: none;
+    padding: 10px;
+    -moz-border-radius-bottomleft: 6px;
+    -webkit-border-bottom-left-radius: 6px;
+    -o-border-bottom-left-radius: 6px;
+    -ms-border-bottom-left-radius: 6px;
+    -khtml-border-bottom-left-radius: 6px;
+    border-bottom-left-radius: 6px;
+    -moz-border-radius-bottomright: 6px;
+    -webkit-border-bottom-right-radius: 6px;
+    -o-border-bottom-right-radius: 6px;
+    -ms-border-bottom-right-radius: 6px;
+    -khtml-border-bottom-right-radius: 6px;
+    border-bottom-right-radius: 6px;
+    margin: 0 0 20px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content h4 {
+    color: #10a54a;
+    font-size: 1.1em;
+    margin: 0;
+    padding: 15px 0 5px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content form input[type='text'].error {
+    outline: 2px solid black;
+    outline-color: #cc0000;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header input.submit {
+    display: block;
+    clear: none;
+    float: left;
+    padding: 6px 8px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header img {
+    display: block;
+    clear: none;
+    float: right;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.sandbox_header a {
+    padding: 4px 0 0 10px;
+    color: #6fc992;
+    display: inline-block;
+    font-size: 0.9em;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.post div.content div.response div.block pre {
+    font-family: "Anonymous Pro", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
+    padding: 10px;
+    font-size: 0.9em;
+    max-height: 400px;
+    overflow-y: auto;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+    margin: 0 0 10px;
+    padding: 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading {
+    float: none;
+    clear: both;
+    overflow: hidden;
+    display: block;
+    margin: 0;
+    padding: 0;
+    background-color: #f9f2e9;
+    border: 1px solid #f0e0ca;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 {
+    display: block;
+    clear: none;
+    float: left;
+    width: auto;
+    margin: 0;
+    padding: 0;
+    line-height: 1.1em;
+    color: black;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span {
+    margin: 0;
+    padding: 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.http_method a {
+    text-transform: uppercase;
+    background-color: #c5862b;
+    text-decoration: none;
+    color: white;
+    display: inline-block;
+    width: 50px;
+    font-size: 0.7em;
+    text-align: center;
+    padding: 7px 0 4px;
+    -moz-border-radius: 2px;
+    -webkit-border-radius: 2px;
+    -o-border-radius: 2px;
+    -ms-border-radius: 2px;
+    -khtml-border-radius: 2px;
+    border-radius: 2px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path {
+    padding-left: 10px;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path a {
+    color: black;
+    text-decoration: none;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading h3 span.path a:hover {
+    text-decoration: underline;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options {
+    overflow: hidden;
+    padding: 0;
+    display: block;
+    clear: none;
+    float: right;
+    margin: 6px 10px 0 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li {
+    float: left;
+    clear: none;
+    margin: 0;
+    padding: 2px 10px;
+    border-right: 1px solid #dddddd;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:first-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.first {
+    padding-left: 0;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li:last-child, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li.last {
+    padding-right: 0;
+    border-right: none;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li {
+    border-right-color: #f0e0ca;
+    color: #c5862b;
+    font-size: 0.9em;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a {
+    color: #c5862b;
+    text-decoration: none;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a:hover, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a:active, body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.heading ul.options li a.active {
+    text-decoration: underline;
+}
+
+body ul#resources li.resource ul.endpoints li.endpoint ul.operations li.operation.put div.content {