Permalink
Browse files

Merge pull request #833 from hobofan/dredd-hooks-rust

feat: add integration for Rust hooks
  • Loading branch information...
honzajavorek committed Aug 10, 2017
2 parents a0e999d + 1aa429b commit c2272e22ca984f288244b3f62489d5af856f4375
View
@@ -38,6 +38,7 @@ Dredd supports writing [hooks](https://dredd.readthedocs.io/en/latest/hooks/)
- [PHP](https://dredd.readthedocs.io/en/latest/hooks-php/)
- [Python](https://dredd.readthedocs.io/en/latest/hooks-python/)
- [Ruby](https://dredd.readthedocs.io/en/latest/hooks-ruby/)
- [Rust](https://dredd.readthedocs.io/en/latest/hooks-rust/)
- Didn't find your favorite language? _[Add a new one!](https://dredd.readthedocs.io/en/latest/hooks-new-language/)_
### Supported Systems
View
@@ -0,0 +1,180 @@
# Writing Dredd Hooks In Rust
[![Crates.io](https://img.shields.io/crates/v/dredd-hooks.svg)](https://crates.io/crates/dredd-hooks)
[GitHub repository](https://github.com/hobofan/dredd-hooks-rust)
Rust hooks are using [Dredd's hooks handler socket interface](hooks-new-language.md). For using Rust hooks in Dredd you have to have [Dredd already installed](quickstart.md). The Rust library is called `dredd-hooks` and the correspondig binary `dredd-hooks-rust`.
## Installation
```
$ cargo install dredd-hooks
```
## Usage
Using Dredd with Rust is slightly different to other languages, as a binary needs to be compiled for execution. The --hookfiles flags should point to compiled hook binaries. See below for an example hooks.rs file to get an idea of what the source file behind the Rust binary would look like.
```
$ dredd apiary.apib http://127.0.0.1:3000 --server=./rust-web-server-to-test --language=rust --hookfiles=./hook-file-binary
```
## API Reference
In order to get a general idea of how the Rust Hooks work, the main executable from the package `dredd-hooks` is an HTTP Server that Dredd communicates with and an RPC client. Each hookfile then acts as a corresponding RPC server. So when Dredd notifies the Hooks server what transaction event is occuring the hooks server will execute all registered hooks on each of the hookfiles RPC servers.
You’ll need to know a few things about the `HooksServer` type in the `dredd-hooks` package.
1. The `HooksServer` type is how you can define event callbacks such as `beforeEach`, `afterAll`, etc..
2. To get a `HooksServer` struct you must do the following;
```rust
extern crate dredd_hooks;
use dredd_hooks::{HooksServer};
fn main() {
let mut hooks = HooksServer::new();
// Define all your event callbacks here
// HooksServer::start_from_env will block and allow the RPC server
// to receive messages from the main `dredd-hooks-rust` process.
HooksServer::start_from_env(hooks);
}
```
3. Callbacks receive a `Transaction` instance, or an array of them.
### Runner Callback Events
The `HooksServer` type has the following callback methods.
1. `before_each`, `before_each_validation`, `after_each`
- accepts a function as a first argument passing a [Transaction object](data-structures.md#transaction) as a first argument
2. `before`, `before_validation`, `after`
- accepts [transaction name](hooks.md#getting-transaction-names) as a first argument
- accepts a function as a second argument passing a [Transaction object](data-structures.md#transaction) as a first argument of it
3. `before_all`, `after_all`
- accepts a function as a first argument passing a `Vec` of [Transaction objects](data-structures.md#transaction) as a first argument
Refer to [Dredd execution lifecycle](how-it-works.md#execution-life-cycle) to find when each hook callback is executed.
### Using the Rust API
Example usage of all methods.
```rust
extern crate dredd_hooks;
use dredd_hooks::{HooksServer};
fn main() {
let mut hooks = HooksServer::new();
hooks.before("/message > GET", Box::new(move |tr| {
println!("before hook handled");
tr
}));
hooks.after("/message > GET", Box::new(move |tr| {
println!("after hook handled");
tr
}));
hooks.before_validation("/message > GET", Box::new(move |tr| {
println!("before validation hook handled");
tr
}));
hooks.before_all(Box::new(move |tr| {
println!("before all hook handled");
tr
}));
hooks.after_all(Box::new(move |tr| {
println!("after all hook handled");
tr
}));
hooks.before_each(Box::new(move |tr| {
println!("before each hook handled");
tr
}));
hooks.before_each_validation(Box::new(move |tr| {
println!("before each validation hook handled");
tr
}));
hooks.after_each(Box::new(move |tr| {
println!("after each hook handled");
tr
}));
HooksServer::start_from_env(hooks);
}
```
## Examples
### How to Skip Tests
Any test step can be skipped by setting the value of the `skip` field of the `Transaction` instance to `true`.
```rust
extern crate dredd_hooks;
use dredd_hooks::{HooksServer};
fn main() {
let mut hooks = HooksServer::new();
// Runs only before the "/message > GET" test.
hooks.before("/message > GET", Box::new(|mut tr| {
// Set the skip flag on this test.
tr.insert("skip".to_owned(), true.into());
// Hooks must always return the (modified) Transaction(s) that were passed in.
tr
}));
HooksServer::start_from_env(hooks);
}
```
### Failing Tests Programmatically
You can fail any step by setting the value of the `fail` field of the `Transaction` instance to `true` or any string with a descriptive message.
```rust
extern crate dredd_hooks;
use dredd_hooks::{HooksServer};
fn main() {
let mut hooks = HooksServer::new();
hooks.before("/message > GET", Box::new(|mut tr| {
// .into() can be used as an easy way to convert
// your value into the desired Json type.
tr.insert("fail".to_owned(), "Yay! Failed!".into());
tr
}));
HooksServer::start_from_env(hooks);
}
```
### Modifying the Request Body Prior to Execution
```rust
extern crate dredd_hooks;
use dredd_hooks::{HooksServer};
fn main() {
let mut hooks = HooksServer::new();
hooks.before("/message > GET", Box::new(|mut tr| {
// Try to access the "request" key as an object.
// (This will panic should the "request" key not be present.)
tr["request"].as_object_mut().unwrap()
.insert("body".to_owned(), "Hello World!".into());
tr
}));
HooksServer::start_from_env(hooks);
}
```
View
@@ -35,6 +35,7 @@ Dredd supports writing [hooks](hooks.md) — a glue code for each test setup and
- [PHP](hooks-php.md)
- [Python](hooks-python.md)
- [Ruby](hooks-ruby.md)
- [Rust](hooks-rust.md)
- Didn't find your favorite language? _[Add a new one!](hooks-new-language.md)_
### Supported Systems
@@ -58,6 +59,7 @@ Dredd supports writing [hooks](hooks.md) — a glue code for each test setup and
- [Hooks: PHP](hooks-php.md)
- [Hooks: Python](hooks-python.md)
- [Hooks: Ruby](hooks-ruby.md)
- [Hooks: Rust](hooks-rust.md)
- [Hooks: Other Languages](hooks-new-language.md)
- [Data Structures](data-structures.md)
- [Contributing](contributing.md)
View
@@ -145,6 +145,8 @@ class DreddCommand
console.log " $ cpanm Dredd::Hooks"
else if config['language'] == 'go'
console.log " $ go get github.com/snikch/goodman/cmd/goodman"
else if config['language'] == 'rust'
console.log " $ cargo install dredd-hooks"
console.log " $ dredd"
console.log ""
@@ -74,6 +74,19 @@ class HooksWorkerClient
else
callback()
else if @language == 'rust'
@handlerCommand = 'dredd-hooks-rust'
@handlerCommandArgs = []
unless which.which @handlerCommand
msg = """\
Rust hooks handler command not found: #{@handlerCommand}
Install rust hooks handler by running:
$ cargo install dredd-hooks
"""
return callback(new Error(msg))
else
callback()
else if @language == 'python'
@handlerCommand = 'dredd-hooks-python'
@handlerCommandArgs = []
@@ -43,6 +43,7 @@ interactiveConfig.prompt = (config = {}, callback) ->
"php"
"perl"
"go"
"rust"
]
}
View
@@ -12,7 +12,7 @@ options =
language:
alias: "a"
description: "Language of hookfiles. Possible options are: nodejs, ruby, python, php, perl, go"
description: "Language of hookfiles. Possible options are: nodejs, ruby, python, php, perl, go, rust"
default: "nodejs"
sandbox:
@@ -364,6 +364,24 @@ describe "DreddCommand class", () ->
it 'should save configuration', ->
assert.isTrue configUtilsStub.save.called
describe '"init" (rust)', ->
before (done) ->
sinon.stub(interactiveConfigStub, 'run').callsFake (argv, cb) ->
cb({language: 'rust'})
sinon.stub configUtilsStub, 'save'
execCommand argv: ['init'], ->
done()
after () ->
interactiveConfigStub.run.restore()
configUtilsStub.save.restore()
it 'should run interactive config', ->
assert.isTrue interactiveConfigStub.run.called
it 'should save configuration', ->
assert.isTrue configUtilsStub.save.called
describe 'without argv', ->
before (done) ->
execCommand argv: [], ->
@@ -408,6 +408,69 @@ describe 'Hooks worker client', ->
assert.equal crossSpawnStub.spawn.getCall(0).args[1][0], 'gobinary'
done()
describe 'when --language=rust option is given and the worker is not installed', ->
beforeEach ->
sinon.stub(whichStub, 'which').callsFake (command) -> false
runner.hooks['configuration'] =
options:
language: 'rust'
hookfiles: 'rustbinary'
afterEach ->
whichStub.which.restore()
it 'should write a hint how to install', (done) ->
loadWorkerClient (err) ->
assert.isOk err
assert.include err.message, "cargo install dredd-hooks"
done()
describe 'when --language=rust option is given and the worker is installed', ->
beforeEach ->
sinon.stub(crossSpawnStub, 'spawn').callsFake( ->
emitter = new EventEmitter
emitter.stdout = new EventEmitter
emitter.stderr = new EventEmitter
emitter
)
runner.hooks['configuration'] =
options:
language: 'rust'
hookfiles: "rustbinary"
sinon.stub(whichStub, 'which').callsFake (command) -> true
sinon.stub(HooksWorkerClient.prototype, 'terminateHandler').callsFake (callback) ->
callback()
afterEach ->
crossSpawnStub.spawn.restore()
runner.hooks['configuration'] = undefined
whichStub.which.restore()
HooksWorkerClient.prototype.terminateHandler.restore()
it 'should spawn the server process with command "dredd-hooks-rust"', (done) ->
loadWorkerClient (err) ->
assert.isUndefined err
hooksWorkerClient.stop (err) ->
assert.isUndefined err
assert.isTrue crossSpawnStub.spawn.called
assert.equal crossSpawnStub.spawn.getCall(0).args[0], 'dredd-hooks-rust'
done()
it 'should pass --hookfiles option as an array of arguments', (done) ->
loadWorkerClient (err) ->
assert.isUndefined err
hooksWorkerClient.stop (err) ->
assert.isUndefined err
assert.equal crossSpawnStub.spawn.getCall(0).args[1][0], 'rustbinary'
done()
describe 'when --language=perl option is given and the worker is installed', ->
beforeEach ->
sinon.stub(crossSpawnStub, 'spawn').callsFake( ->

0 comments on commit c2272e2

Please sign in to comment.