Feature Proposal: Middleware API for event handlers
See original GitHub issueHi 😄
First, I love the probot framework, thanks to the whole team for developing such an awesome framework and making it opensource!
Feature Proposal
Recently I build a probot app for my team for posting pull request notifications to our team’s slack channel. In the process, I wanted to see if I could have middleware for github event handlers which allows defining event/context related transformations at an app level scale and not per individual event handler. Less code duplication and less errors. I’m proposing a middleware implementation for probot event handlers. The middleware API I’m proposing works similar to the redux
middleware API.
Middleware API
The following shows the event handler middleware api.
next
: the next middleware in the chain. The last next
is going to be the original event handler. E.g) app.on(event, handler)
context
: the context for the current github event being processed
next => context => {
// do something with `context` or `context.payload`.
// forward context to the next middleware in the chain
return next(context);
};
// async middleware
next => async context => {
// fetch some data from another api.
const data = await fetchData();
// forward context to the next middleware in the chain
return next({
...context,
payload: {
...context.payload,
...data
}
});
};
I’ll start with some middleware examples, followed by how I implemented it for my probot application.
To begin, assume I have a probot application subscribed to pull_request
events from github.
Middleware 1 - Metadata
A middleware can transform the context.payload
object and include parsed metadata. Similar to probot-metadata
. However, compared to probot-metadata
, reduces the code duplication involved in having each event handler parse the metadata. It’s all defined in a single place now.
const parseMetadataFromBody = require('../utils/parse-metadata-from-body');
const parseMetadata = next => (context) => {
if (!context.payload) {
return next(context);
}
const { payload } = context;
if (!payload.pull_request) {
return next(context);
}
const {
pull_request: { body },
} = payload;
const { metadata, bodyWithoutMetadata } = parseMetadataFromBody(body);
return next({
...context,
payload: {
...context.payload,
pull_request: {
...context.payload.pull_request,
body: bodyWithoutMetadata,
},
myApp: {
metadata,
},
},
});
};
module.exports = parseMetadata;
Middleware 2 - Error Reporting
Another middleware implemented was one which can capture any errors and log them. No more need to capture errors at the event
handler level. Any error thrown by an event handler will be captured by this middleware. This middleware also is asynchronous!
const slack = require('../notification-clients/slack');
const errorReporting = next => async (context) => {
try {
// capture errors thrown by any middleware down the chain
// and event handlers
const result = await next(context);
return result;
} catch (error) {
await slack.dumpLogs([`${error.message}\n${error.stack}`]);
return 'Failed to handle event';
}
};
module.exports = errorReporting;
Middleware 3 - Appending Additional Pull Request Information
The following middleware will detect pull_request
events and automatically append additional information returned by the github api for that pull request and also the files included in the pull request. Now any event handler will have all this information immediately!
const path = require('path');
const addPullRequestInfo = next => async (context) => {
const { payload } = context;
if (payload.pull_request && payload.pull_request.number) {
const {
pull_request: { number },
repository: {
name,
owner: { login },
},
} = payload;
const { data: pullRequestInfo } = await context.github.pullRequests.get({
owner: login,
repo: name,
number,
});
// for example purposes
const { data: files } = await context.github.pullRequests.getFiles({
owner: login,
repo: name,
number,
per_page: 100,
page: 1,
});
const { fileTypes, fileTypesPretty } = getFileTypes(files);
return next({
...context,
payload: {
...context.payload,
pull_request: {
...pullRequestInfo,
files,
},
},
});
}
return next(context);
};
module.exports = addPullRequestInfo;
The possibilities are endless. Applying these transformations and data retrieval at the application scale removes a lot of cruft away from individual event handlers.
Implementation Of The Middleware API
Currently, here’s how I implemented the middleware api in my probot application. It takes the app
object and patches app.on
to add the middleware functionality.
const compose = require('./utils/compose');
const applyEventMiddleware = app => (middlewares) => {
const originalAppOn = app.on.bind(app);
app.on = (event, handler) => {
// build chain. a->b->c
const handlerWithMiddlewares = compose(...middlewares)(handler);
originalAppOn(event, handlerWithMiddlewares);
};
};
module.exports = applyEventMiddleware;
Composition of the middleware chain
// ./utils/compose
const compose = (...functions) => {
if (functions.length === 0) {
return arg => arg;
}
if (functions.length === 1) {
return functions[0];
}
return functions.reduce((a, b) => (...args) => a(b(...args)));
};
module.exports = compose;
If we could have the middleware api integrated into Probot, it would definitely clean this patching up.
Hence, I’m proposing a middleware API for event handlers. This reduces code duplication and errors by defining necessary transforms and data retrieval operations at the application level and not the event handler level.
If this proposal is approved, I would be happy to help contribute for it. Thank you! 😄
Issue Analytics
- State:
- Created 5 years ago
- Reactions:3
- Comments:7 (5 by maintainers)
Top GitHub Comments
In Probot, you’ll probably do something like this:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.