Skip to content

Commit

Permalink
Merge pull request #312 from mdwheele/feature/custom-operation-resolvers
Browse files Browse the repository at this point in the history
Implement the idea of an "Operation Resolver"
  • Loading branch information
cdimascio committed Jun 13, 2020
2 parents 5d6d5de + 3534a46 commit 72a553e
Show file tree
Hide file tree
Showing 18 changed files with 6,440 additions and 41 deletions.
19 changes: 17 additions & 2 deletions README.md
Expand Up @@ -489,7 +489,7 @@ new OpenApiValidator(options).install({
}
}
},
operationHandlers: false | 'operations/base/path',
operationHandlers: false | 'operations/base/path' | { ... },
ignorePaths: /.*\/pets$/,
unknownFormats: ['phone-number', 'uuid'],
fileUploader: { ... } | true | false,
Expand Down Expand Up @@ -610,11 +610,26 @@ Defines how the validator should behave if an unknown or custom format is encoun

### 鈻笍 operationHandlers (optional)

Defines the base directory for operation handlers. This is used in conjunction with express-openapi-validator's OpenAPI vendor extensions.
Defines the base directory for operation handlers. This is used in conjunction with express-openapi-validator's OpenAPI vendor extensions, `x-eov-operation-id`, `x-eov-operation-handler` and OpenAPI's `operationId`.

Additionally, if you want to change how modules are resolved e.g. use dot deliminted operation ids e.g. `path.to.module.myFunction`, you may optionally add a custom `resolver`. See [documentation and example](https://github.com/cdimascio/express-openapi-validator/tree/master/examples/5-eov-operations)

- `string` - the base directory containing operation handlers
- `false` - (default) disable auto wired operation handlers
- `{ ... }` - specifies a base directory and optionally a custom resolver

**handlers:**

For example:

```javascript
operationHandlers: {
basePath: __dirname,
resolver: function (modulePath, route): express.RequestHandler {
///...
}
}
```
```
operationHandlers: 'operations/base/path'
```
Expand Down
76 changes: 76 additions & 0 deletions examples/5-custom-operation-resolver/README.md
@@ -0,0 +1,76 @@
# example

example using express-openapi-validator with custom operation resolver

## Install

```shell
npm i && npm run deps
```

## Run

From this `5-custom-operation-resolver` directory, run:

```shell
npm start
```

## Try

```shell
## call ping
curl http://localhost:3000/v1/ping

## call pets
## the call below should return 400 since it requires additional parameters
curl http://localhost:3000/v1/pets
```

## [Example Express API Server: with custom operation resolver](https://github.com/cdimascio/express-openapi-validator/tree/master/examples/5-custom-operation-resolver)

By default, when you configure `operationHandlers` to be the base path to your operation handler files, we use `operationId`, `x-eov-operation-id` and `x-eov-operation-handler` to determine what request handler should be used during routing.

If you ever want _FULL_ control over how that resolution happens (e.g. you want to use your own extended attributes or simply rely on `operationId`), then here's how you can accomplish that following an example where our `operationId` becomes a template that follows `{module}.{function}`.

- First, specifiy the `operationHandlers` option to be an object with a `basePath` and `resolver` properties. `basePath` is the path to where all your handler files are located. `resolver` is a function that **MUST** return an Express `RequestHandler` given `basePath` and `route` (which gives you access to the OpenAPI schema for a specific Operation)

```javascript
new OpenApiValidator({
apiSpec,
operationHandlers: {
basePath: path.join(__dirname, 'routes'),
resolver: (basePath, route) => {
// Pluck controller and function names from operationId
const [controllerName, functionName] = route.schema['operationId'].split('.')
// Get path to module and attempt to require it
const modulePath = path.join(basePath, controllerName);
const handler = require(modulePath)
// Simplistic error checking to make sure the function actually exists
// on the handler module
if (handler[functionName] === undefined) {
throw new Error(
`Could not find a [${functionName}] function in ${modulePath} when trying to route [${route.method} ${route.expressRoute}].`
)
}
// Finally return our function
return handler[functionName]
}
});
```
- Next, use `operationId` to specify the id of opeartion handler to invoke.
```yaml
/pets:
get:
# This means our resolver will look for a file named "pets.js" at our
# configured base path and will return an export named "list" from
# that module as the Express RequestHandler.
operationId: pets.list
```
- Finally, create the express handler module e.g. `routes/pets.js`
```javascript
module.exports = {
// the express handler implementation for the pets collection
list: (req, res) => res.json(/* ... */),
};
```
229 changes: 229 additions & 0 deletions examples/5-custom-operation-resolver/api.yaml
@@ -0,0 +1,229 @@
openapi: '3.0.0'
info:
version: 1.0.0
title: Swagger Petstore
description: A sample API
termsOfService: http://swagger.io/terms/
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: /v1
paths:
/ping:
get:
description: |
ping then pong!
operationId: ping.ping
responses:
'200':
description: OK
content:
text/plain:
schema:
type: string
example: pong
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/pets:
get:
description: |
Returns all pets
operationId: pets.list
parameters:
- name: type
in: query
description: maximum number of results to return
required: true
schema:
type: string
enum:
- dog
- cat
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: true
schema:
type: integer
format: int32
minimum: 1
maximum: 20
responses:
'200':
description: pet response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

post:
description: Creates a new pet in the store.
operationId: pets.create
security:
- ApiKeyAuth: []
requestBody:
description: Pet to add to the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

/pets/{id}:
get:
description: Returns a user based on a single ID, if the user does not have access to the pet
operationId: pets.show
parameters:
- name: id
in: path
description: ID of pet to fetch
required: true
schema:
type: integer
format: int64
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes a single pet based on the ID supplied
operationId: pets.destroy
parameters:
- name: id
in: path
description: ID of pet to delete
required: true
schema:
type: integer
format: int64
responses:
'204':
description: pet deleted
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

/pets/{id}/photos:
post:
description: upload a photo of the pet
operationId: pets.photos
parameters:
- name: id
in: path
description: ID of pet to fetch
required: true
schema:
type: integer
format: int64
requestBody:
content:
multipart/form-data:
schema:
# $ref: '#/components/schemas/NewPhoto'
type: object
required:
- file
properties:
file:
description: The photo
type: string
format: binary
required: true
responses:
201:
description: Created
content:
application/json:
schema:
type: object
properties:
success:
type: boolean

components:
schemas:
Pet:
required:
- id
- name
- type
properties:
id:
readOnly: true
type: number
name:
type: string
tag:
type: string
type:
$ref: '#/components/schemas/PetType'

PetType:
type: string
enum:
- dog
- cat

Error:
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string

securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
51 changes: 51 additions & 0 deletions examples/5-custom-operation-resolver/app.js
@@ -0,0 +1,51 @@
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const logger = require('morgan');
const http = require('http');
const { OpenApiValidator, resolvers } = require('express-openapi-validator');

const port = 3000;
const app = express();
const apiSpec = path.join(__dirname, 'api.yaml');

// 1. Install bodyParsers for the request types your API will support
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.text());
app.use(bodyParser.json());

app.use(logger('dev'));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/spec', express.static(apiSpec));

// 2. Install the OpenApiValidator on your express app
new OpenApiValidator({
apiSpec,
validateResponses: true, // default false
operationHandlers: {
// 3. Provide the path to the controllers directory
basePath: path.join(__dirname, 'routes'),
// 4. Provide a function responsible for resolving an Express RequestHandler
// function from the current OpenAPI Route object.
resolver: resolvers.modulePathResolver
}
})
.install(app)
.then(() => {
// 5. Create a custom error handler
app.use((err, req, res, next) => {
// format errors
res.status(err.status || 500).json({
message: err.message,
errors: err.errors,
});
});

http.createServer(app).listen(port);
console.log(`Listening on port ${port}`);
});

module.exports = app;

0 comments on commit 72a553e

Please sign in to comment.