diff --git a/lib/index.js b/lib/index.js index e467278..968ffb2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -27,6 +27,7 @@ const optionsSchema = Joi.object({ cors: Joi.alternatives().try(Joi.object(), Joi.boolean()).default(true), vhost: Joi.string().allow(null), handlers: Joi.alternatives().try(Joi.string().default(Path.join(CALLER_DIR, 'routes')), Joi.object()).allow(null), + handlersPath: Joi.string().allow(null), extensions: Joi.array().items(Joi.string()).default(['js']), outputvalidation: Joi.boolean().default(false) }).required(); @@ -78,7 +79,7 @@ const register = async function (server, options, next) { Hoek.assert(!validation.error, validation.error); - const { api, cors, vhost, handlers, extensions, outputvalidation } = validation.value; + const { api, cors, vhost, handlers, handlersPath, extensions, outputvalidation } = validation.value; let { docs, docspath } = validation.value; const spec = await Parser.validate(api); @@ -104,7 +105,7 @@ const register = async function (server, options, next) { if (typeof api === 'string') { apiDocument = requireApi(api); - basedir = Path.dirname(Path.resolve(api)); + basedir = handlersPath ? Path.resolve(handlersPath) : Path.dirname(Path.resolve(api)); } else { apiDocument = api; diff --git a/lib/routes.js b/lib/routes.js index e4cb2b0..c9f756a 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -36,10 +36,21 @@ const create = async function (server, { api, basedir, cors, vhost, handlers, ex for (const [path, operations] of Object.entries(api.paths)) { const pathnames = Utils.unsuffix(path, '/').split('/').slice(1).join(SEPARATOR); + const hapiHandler = operations['x-hapi-handler']; for (const [method, operation] of Object.entries(operations)) { - const pathsearch = `${pathnames}${SEPARATOR}${method}`; - const handler = Hoek.reach(handlers, pathsearch, { separator: SEPARATOR }); + const operationId = operation.operationId; + const hapiHandlerMethod = `${hapiHandler}${SEPARATOR}${method}`; + const hapiHandlerOperationId = `${hapiHandler}${SEPARATOR}${operationId}`; + const pathsearchMethod = `${pathnames}${SEPARATOR}${method}`; + const pathSearchOperationId = `${pathnames}${SEPARATOR}${operationId}`; + // Give precedence to operationId over method and give precedence to hapiHandler over path. + const handler = + Hoek.reach(handlers, hapiHandlerOperationId, { separator: SEPARATOR }) || + Hoek.reach(handlers, pathSearchOperationId, { separator: SEPARATOR }) || + Hoek.reach(handlers, hapiHandlerMethod, { separator: SEPARATOR }) || + Hoek.reach(handlers, pathsearchMethod, { separator: SEPARATOR }); + const xoptions = operation['x-hapi-options'] || {}; if (!handler) { diff --git a/test/fixtures/defs/pets_xhandlers_handlersPath.json b/test/fixtures/defs/pets_xhandlers_handlersPath.json new file mode 100644 index 0000000..d75837d --- /dev/null +++ b/test/fixtures/defs/pets_xhandlers_handlersPath.json @@ -0,0 +1,258 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore (Simple)", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://helloreverb.com/terms/", + "contact": { + "name": "Swagger API team", + "email": "foo@example.com", + "url": "http://swagger.io" + }, + "license": { + "name": "MIT", + "url": "http://opensource.org/licenses/MIT" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v1/petstore", + "schemes": [ + "http" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/firstPet": { + "x-hapi-handler": "pets", + "get": { + "description": "Returns all pets from the system that the user has access to", + "operationId": "getFirstPet", + "produces": [ + "application/json", + "application/xml", + "text/xml", + "text/html" + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "$ref": "#/definitions/pet" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/errorModel" + } + } + } + } + }, + "/pets": { + "x-hapi-handler": "pets", + "get": { + "description": "Returns all pets from the system that the user has access to", + "operationId": "findPets", + "produces": [ + "application/json", + "application/xml", + "text/xml", + "text/html" + ], + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "tags to filter by", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "limit", + "in": "query", + "description": "maximum number of results to return", + "required": false, + "type": "integer", + "format": "int32" + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/pet" + } + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/errorModel" + } + } + } + }, + "post": { + "description": "Creates a new pet in the store. Duplicates are allowed", + "operationId": "addPet", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "pet", + "in": "body", + "description": "Pet to add to the store", + "required": true, + "schema": { + "$ref": "#/definitions/newPet" + } + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "$ref": "#/definitions/pet" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/errorModel" + } + } + } + } + }, + "/pets/{id}": { + "x-hapi-handler": "pets", + "get": { + "description": "Returns a user based on a single ID, if the user does not have access to the pet", + "operationId": "findPetById", + "produces": [ + "application/json", + "application/xml", + "text/xml", + "text/html" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to fetch", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "200": { + "description": "pet response", + "schema": { + "$ref": "#/definitions/pet" + } + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/errorModel" + } + } + } + }, + "delete": { + "description": "deletes a single pet based on the ID supplied", + "operationId": "deletePet", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "ID of pet to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "204": { + "description": "pet deleted" + }, + "default": { + "description": "unexpected error", + "schema": { + "$ref": "#/definitions/errorModel" + } + } + } + } + } + }, + "definitions": { + "pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "newPet": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "errorModel": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/handlers/pets.js b/test/fixtures/handlers/pets.js index 346e1b9..81cb54f 100644 --- a/test/fixtures/handlers/pets.js +++ b/test/fixtures/handlers/pets.js @@ -8,5 +8,20 @@ module.exports = { }, post: function (req, h) { return Store.get(Store.put(req.payload)); + }, + getFirstPet: function (req, h) { + return Store.first(); + }, + findPetById: [ + function (req, h) { + return Store.get(req.params.id); + }, + function handler(req, h) { + return req.pre.p1; + } + ], + delete: function (req, h) { + Store.delete(req.params.id); + return Store.all(); } }; diff --git a/test/fixtures/lib/store.js b/test/fixtures/lib/store.js index 5b54f3c..529a80a 100644 --- a/test/fixtures/lib/store.js +++ b/test/fixtures/lib/store.js @@ -1,6 +1,6 @@ 'use strict'; -var store = []; +const store = []; module.exports = { put: function (data) { @@ -15,5 +15,8 @@ module.exports = { }, all: function () { return store; + }, + first: function () { + return {}; } }; diff --git a/test/test-hapi-openapi.js b/test/test-hapi-openapi.js index 59260c7..de8dd17 100644 --- a/test/test-hapi-openapi.js +++ b/test/test-hapi-openapi.js @@ -720,6 +720,66 @@ Test('test plugin', function (t) { }); + t.test('routes x-handler handlersPath', async function (t) { + t.plan(5); + + const server = new Hapi.Server(); + + try { + await server.register({ + plugin: OpenAPI, + options: { + api: Path.join(__dirname, './fixtures/defs/pets_xhandlers_handlersPath.json'), + handlersPath: Path.join(__dirname, './fixtures/handlers') + } + }); + + let response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets' + }); + + t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + + response = await server.inject({ + method: 'POST', + url: '/v1/petstore/pets', + payload: { + id: '0', + name: 'Cat' + } + }); + + t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + + response = await server.inject({ + method: 'GET', + url: '/v1/petstore/pets/0' + }); + + t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + + response = await server.inject({ + method: 'DELETE', + url: '/v1/petstore/pets/0' + }); + + t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + + response = await server.inject({ + method: 'GET', + url: '/v1/petstore/firstPet' + }); + + t.strictEqual(response.statusCode, 200, `${response.request.path} OK.`); + + } + catch (error) { + t.fail(error.message); + } + + }); + t.test('query validation', async function (t) {