diff --git a/DEVGUIDE.md b/DEVGUIDE.md index e67bac7..eda01c2 100644 --- a/DEVGUIDE.md +++ b/DEVGUIDE.md @@ -127,3 +127,15 @@ npm run docs This task will generate all documentation to the `docs` directory. All the source script files will be parsed via ESDoc and all RAML files in `raml/*.raml` will create API docs. + +# Generating Swagger + +``` +npm run raml2swagger +``` + +This task will process the /raml/api.v1.raml and convert it to /swagger/api.v1.yaml. + +This task should be run whenever RAML files are modified. + +Read more about this in [/swagger/README.md](swagger/README.md) diff --git a/package.json b/package.json index e4c9752..4906c32 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "predocs-raml": "mkdir docs/raml ; exit 0", "docs-raml": "\"node_modules/.bin/raml2html\" \"raml/api.v1.raml\" > \"docs/raml/index.html\"", "prescaffold": "cd scaffold && npm install", - "scaffold": "node scaffold \"config/scaffold.config.js\"" + "scaffold": "node scaffold \"config/scaffold.config.js\"", + "raml2swagger": "node node_modules/api-spec-converter/bin/api-spec-converter raml/api.v1.raml --from=raml --to=swagger_2 --check | node node_modules/json2yaml/CLI > swagger/api.v1.yaml" }, "author": "", "license": "", @@ -65,6 +66,7 @@ "zone.js": "^0.6.21" }, "devDependencies": { + "api-spec-converter": "^2.0.1", "babel-eslint": "^6.1.2", "babel-loader": "^6.2.5", "babel-plugin-angular2-annotations": "^5.1.0", @@ -91,6 +93,7 @@ "istanbul": "^0.4.5", "jasmine-reporters": "^2.2.0", "json-loader": "^0.5.4", + "json2yaml": "^1.1.0", "karma": "^1.2.0", "karma-babel-preprocessor": "^6.0.1", "karma-chrome-launcher": "^2.0.0", diff --git a/raml/api.v1.raml b/raml/api.v1.raml index 144df12..42693ef 100644 --- a/raml/api.v1.raml +++ b/raml/api.v1.raml @@ -1,7 +1,7 @@ #%RAML 0.8 title: WebStart Example Services version: 1 -baseUri: /api/v1 +baseUri: http://calproc.website/api/v1 mediaType: application/json protocols: - HTTP @@ -10,23 +10,16 @@ documentation: - title: TODO content: Put your stuff here! +securitySchemes: [] + schemas: - + Empty: !include "api/v1/common/empty.schema.json" Error: !include "api/v1/errors/error.schema.json" User: !include "api/v1/users/user.schema.json" UserCollection: !include "api/v1/users/user-collection.schema.json" Login: !include "api/v1/login/login.schema.json" -securitySchemes: - - JWT: - type: x-JWT - describedBy: - headers: - Authorization: - description: | - JWT token containing claims for 'exp' (expiration), 'roles', and 'permissions'. TODO: define the format for each of those - required: true - traits: !include api/traits.raml resourceTypes: !include api/types.raml diff --git a/raml/api/traits.raml b/raml/api/traits.raml index 9956aaa..b362510 100644 --- a/raml/api/traits.raml +++ b/raml/api/traits.raml @@ -1,10 +1,4 @@ - secured: - #securedBy: [JWT] - #headers: - # Authorization: - # description: | - # JWT token containing claims for 'exp' (expiration), 'roles', and 'permissions'. - # required: true responses: 401: description: Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen. @@ -15,6 +9,8 @@ responses: 204: description: The request was successfully handled + body: + schema: Empty - paged: queryParameters: @@ -44,7 +40,7 @@ - mock: responses: - 299: + 200: description: This is a mock service that has not yet been implemented - member: diff --git a/raml/api/v1/common/empty.schema.json b/raml/api/v1/common/empty.schema.json new file mode 100644 index 0000000..c71f76f --- /dev/null +++ b/raml/api/v1/common/empty.schema.json @@ -0,0 +1,8 @@ +{ + "id": "http://kpmg/webstart/Empty", + "type": "object", + "$schema": "http://json-schema.org/draft-03/schema", + "additionalProperties": false, + "properties": { + } +} diff --git a/raml/api/v1/errors/error.schema.json b/raml/api/v1/errors/error.schema.json index 70a5411..cb936f8 100644 --- a/raml/api/v1/errors/error.schema.json +++ b/raml/api/v1/errors/error.schema.json @@ -10,8 +10,7 @@ "type": "string" }, "errorCode": { - "type": "integer", - "format": "int32" + "type": "integer" }, "message": { "type": "string", diff --git a/raml/api/v1/login/login.raml b/raml/api/v1/login/login.raml index ab681bc..b924cb7 100644 --- a/raml/api/v1/login/login.raml +++ b/raml/api/v1/login/login.raml @@ -23,6 +23,8 @@ post: "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJJc3N1ZXIiOiJodHRwOi8vd3d3Lnd5bnlhcmRncm91cC5jb20iLCJBdWRpZW5jZSI6IkFDQSIsIlByaW5jaXBhbCI6eyJTZXNzaW9uSWQiOiI3ZDZjN2ZjMC1lNzkzLTQyNjMtOTQ3OC01MmQzMmQyYzYzNjEiLCJVc2VyS2V5IjoiNCIsIlVzZXJOYW1lIjoia2NsaWZmZSIsIkNsYWltcyI6WyJBZG1pbiJdLCJMb2NhbGUiOiJlbi1OWiIsIlNlc3Npb25UaW1lT3V0IjoiXC9EYXRlKDE0NTA3OTQ1OTczNjIpXC8iLCJJc3N1ZWRUbyI6bnVsbCwiSWRlbnRpdHkiOnsiTmFtZSI6ImtjbGlmZmUiLCJBdXRoZW50aWNhdGlvblR5cGUiOiJXeW55YXJkIiwiSXNBdXRoZW50aWNhdGVkIjp0cnVlfX0sIkV4cGlyeSI6IlwvRGF0ZSgxKVwvIn0.0GZlnA-mdDQqSfSKvBlWsUehtVCRkNK8DA9siyeVLQ0" } 401: + body: + schema: Empty description: | Invalid username or password diff --git a/raml/api/v1/users/user.schema.json b/raml/api/v1/users/user.schema.json index 3ad23ad..2869ccd 100644 --- a/raml/api/v1/users/user.schema.json +++ b/raml/api/v1/users/user.schema.json @@ -7,11 +7,7 @@ "properties": { "userId": { "description": "The identifier of the object", - "type": "string", - "x-kpmg-dotnet-attributes": [ - "//[CustomAttribute]", - "//[CustomAttribute2(param=\"value\")]" - ] + "type": "string" }, "firstName": { "description": "The users first name", @@ -37,24 +33,9 @@ "format": "uri" }, "role": { - "description": "The associated user role", + "description": "The associated user role. 0 = Standard user, 1 = Account admin, 2 = System admin", "enum": [0,1,2], - "options": [ - { - "value": 0, - "label": "Standard" - }, - { - "value": 1, - "label": "AccountAdmin" - }, - { - "value": 2, - "label": "SystemAdmin" - } - ], - "type": "integer", - "x-kpmg-enumName": "UserRole" + "type": "integer" } } } diff --git a/server/api.js b/server/api.js index 1fb5cab..39c44e1 100644 --- a/server/api.js +++ b/server/api.js @@ -11,6 +11,7 @@ module.exports = function(app) { let osprey = require('osprey'), + url = require('url'), path = require('path'), ospreyMock = require('osprey-mock-service'), fs = require('fs'), @@ -21,7 +22,8 @@ module.exports = function(app) { return Promise.all(files.map(file => { return raml.loadFile(path.join(dir, file)).then(raml => { - console.log(`registering ${file} on ${raml.baseUri}`); + let baseUri = url.parse(raml.baseUri).path; + console.log(`registering ${file} on ${baseUri}`); try { app.use(raml.baseUri, osprey.server(raml)); try { @@ -33,8 +35,8 @@ module.exports = function(app) { console.log(e); } finally { - app.use(raml.baseUri, ospreyMock(raml)); - console.log(`registered ${file} on ${raml.baseUri}`); + app.use(baseUri, ospreyMock(raml)); + console.log(`registered ${file} on ${baseUri}`); } } catch (e) { diff --git a/swagger/README.md b/swagger/README.md new file mode 100644 index 0000000..cdaa496 --- /dev/null +++ b/swagger/README.md @@ -0,0 +1,25 @@ +# Note on Swagger & RAML + +KPMG has been an early adopter in the area of documenting API specs in standardized, machine-readable, and easily editable formats. This has significantly enhanced our development process and drastically reduced integration issues. + +After using Swagger on a few projects, we found that the process of writing an maintaining Swagger specs was cumbersome, tedious, and error prone because the Swagger syntax is not tailored towards the software engineering best practices of composability and reuse. + +A few years ago, we switched to recommending [RAML](http://raml.org) instead of Swagger as the API definition syntax on our projects. + +# What we do with RAML + +Leveraging RAML, our "accelerator" (code our teams can fork from when creating new projects for clients) provides several useful capabilities out of the box. + +1. Documentation generation. Human-friendly API docs are generated as static HTML files based on RAML specs. +1. Client-side API wrapper. As part of the UI build process, a JavaScript module is generated with function, parameter, and attribute names based on the documentation. This makes it simpler for UI developers to code against the defined REST APIs. +1. Static mocks. One of our goals in project management is ensuring that nobody on the team is ever blocked from making forward progress. A common issue we encounter is backend development and UI development not being 100% aligned on schedules. Some things are simply harder to do on one side or the other. To enable different sides of the stack to progress independently, our Express server identifies calls to APIs that have not yet been implemented and, instead of returning a 404, returns a response based on the *example* defined for that endpoint in the RAML spec. UI development can move forward coding against services and the example data they return. As individual services are implemented for real, the UI that's been built against these mocks often works perfect without any additional integration work being needed. +1. Static schema validation. As part of the build process, RAML files are validated for compliance with the RAML spec. Additionally, examples for request/response bodies in the RAML files are validated against the corresponding JSON schemas in the RAML files. This prevents the common problem of schemas and examples not being kept up to date with each other. +1. Runtime validation proxy. When running the Express server in non-production mode, all request & response bodies will be validated against the schemas defined for their endpoints in RAML. This enables us to quickly find and identify issues where the client or server is not sending data in the expected structure. +1. Server-side codegen. While not demonstrated in this prototype, we also have acclerators for doing codegen of Java & C# service interfaces & DTO classes. These help expedite service implementation and further help ensure that the implementation of the interface matches the spec. Additionally, because these are strongly typed languages, many types of breaking changes to RAML specs will result in the Java/C# projects failing to build - a good thing because it forces the team to have a discussion & remediation rather than let the issue linger. + +# For this prototype + +Each of these capabilities could certainly be implemented with Swagger as well. However, in the interest of time, we have decided to sitck with using RAML as the "source of truth" for this project and to leverage our existing RAML-based versions of these capabilities. To meet the requirement of providing a Swagger spec, we have created a script that converts RAML to Swagger and validates the result against the Swagger schema. + +The `api.v1.yaml` file in this folder is a Swagger spec generated from `./raml/api.v1.raml` +by running `npm run raml2swagger`. \ No newline at end of file diff --git a/swagger/api.v1.yaml b/swagger/api.v1.yaml new file mode 100644 index 0000000..a306546 --- /dev/null +++ b/swagger/api.v1.yaml @@ -0,0 +1,340 @@ +--- + basePath: "/api/v1" + consumes: + - "application/json" + definitions: + Empty: + additionalProperties: false + properties: {} + type: "object" + Error: + additionalProperties: false + properties: + errorCode: + type: "integer" + errorId: + description: "The identifier of the object" + type: "string" + logHistory: + description: "most recent client-side logging messages" + items: + type: "string" + type: "array" + message: + description: "diagnostic error message" + type: "string" + url: + description: "full URL the user was viewing when the error occurred" + type: "string" + required: + - "errorId" + - "errorCode" + - "message" + - "url" + - "logHistory" + type: "object" + Login: + additionalProperties: false + properties: + password: + description: "The password of the authenticated user (not returned in response)" + type: "string" + token: + description: "JWT token containing claims for exp, roles, and permissions (not part of request, only response)" + type: "string" + userName: + description: "The userName of the authenticated user" + type: "string" + required: + - "userName" + type: "object" + User: + additionalProperties: false + properties: + email: + description: "The users email address" + format: "email" + type: "string" + firstName: + description: "The users first name" + type: "string" + homepage: + description: "The users web page" + format: "uri" + type: "string" + lastName: + description: "The users last name" + type: "string" + role: + description: "The associated user role. 0 = Standard user, 1 = Account admin, 2 = System admin" + enum: + - 0 + - 1 + - 2 + type: "integer" + userId: + description: "The identifier of the object" + type: "string" + userName: + description: "The users login user name" + minLength: 8 + type: "string" + required: + - "userId" + - "firstName" + - "lastName" + - "userName" + type: "object" + UserCollection: + items: + $ref: "#/definitions/User" + type: "array" + host: "calproc.website" + info: + title: "WebStart Example Services" + version: "1" + paths: + /errors: + post: + description: "Add a new error to the collection\n" + parameters: + - + in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/Error" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + 204: + description: "The request was successfully handled" + schema: + $ref: "#/definitions/Empty" + 409: + description: "Unable to process request to do conflict in state of object\n" + /login: + delete: + description: "Clears current session and logs user out of the application.\n" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + 204: + description: "The request was successfully handled" + schema: + $ref: "#/definitions/Empty" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + post: + description: "Authenticate the user via username & password\n" + parameters: + - + in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/Login" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + 201: + description: "Created" + schema: + $ref: "#/definitions/Login" + 401: + description: "Invalid username or password\n" + schema: + $ref: "#/definitions/Empty" + put: + description: "Returns an updated token with an extended expiration.\nThis will be invoked by the client when the user has been actively using the client but not in ways that have resulted in a other service calls. It allows the client to prevent the user from being timed out unnecessarily.\n" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + 201: + description: "Created" + schema: + $ref: "#/definitions/Login" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + /users: + get: + description: "Get a list of user\n" + parameters: + - + default: 1 + description: "The page number of the collection to return" + in: "query" + name: "page" + required: false + type: "number" + - + default: 20 + description: "The number of items to display per page" + in: "query" + name: "pageSize" + required: false + type: "number" + - + description: "A field to sort the paged collection by" + in: "query" + name: "sortBy" + required: false + type: "string" + - + default: false + description: "Sort results in descending order rather than ascending" + in: "query" + name: "sortDescending" + required: false + type: "boolean" + - + description: "A string of terms to query for users by" + in: "query" + name: "terms" + required: false + type: "string" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + schema: + $ref: "#/definitions/UserCollection" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + post: + description: "Add a new user to the collection\n" + parameters: + - + in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/User" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + 201: + description: "Created" + schema: + $ref: "#/definitions/User" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + 409: + description: "Unable to process request to do conflict in state of object. Any specific details included in body.\n" + /users/current: + delete: + description: "Deletes a specific current\n" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + 204: + description: "Successfully deleted the current" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + get: + description: "Get a specific current\n" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + schema: + $ref: "#/definitions/User" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + put: + description: "Update a specific current\n" + parameters: + - + in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/User" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + schema: + $ref: "#/definitions/User" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + 409: + description: "Unable to process request to do conflict in state of object. Any specific details included in body.\n" + /users/{userId}: + delete: + description: "Deletes a specific user\n" + parameters: + - + in: "path" + name: "userId" + required: true + type: "string" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + 204: + description: "Successfully deleted the user" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + get: + description: "Get a specific user\n" + parameters: + - + in: "path" + name: "userId" + required: true + type: "string" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + schema: + $ref: "#/definitions/User" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + put: + description: "Update a specific user\n" + parameters: + - + in: "path" + name: "userId" + required: true + type: "string" + - + in: "body" + name: "body" + required: true + schema: + $ref: "#/definitions/User" + responses: + 200: + description: "This is a mock service that has not yet been implemented" + schema: + $ref: "#/definitions/User" + 401: + description: "Authentication token is invalid or has expired. Client should display a message to the user and return them user to the login screen." + 403: + description: "Authentication token is valid, but client does not have access to perform this action." + 409: + description: "Unable to process request to do conflict in state of object. Any specific details included in body.\n" + produces: + - "application/json" + schemes: + - "http" + securityDefinitions: {} + swagger: "2.0" +