grape 0.6.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- checksums.yaml +9 -9
- data/.gitignore +7 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +3 -3
- data/CHANGELOG.md +42 -3
- data/CONTRIBUTING.md +118 -0
- data/Gemfile +4 -4
- data/README.md +312 -52
- data/Rakefile +6 -1
- data/UPGRADING.md +124 -0
- data/lib/grape.rb +2 -0
- data/lib/grape/api.rb +95 -44
- data/lib/grape/cookies.rb +0 -2
- data/lib/grape/endpoint.rb +63 -39
- data/lib/grape/error_formatter/base.rb +0 -3
- data/lib/grape/error_formatter/json.rb +0 -2
- data/lib/grape/error_formatter/txt.rb +0 -2
- data/lib/grape/error_formatter/xml.rb +0 -2
- data/lib/grape/exceptions/base.rb +0 -2
- data/lib/grape/exceptions/incompatible_option_values.rb +0 -3
- data/lib/grape/exceptions/invalid_formatter.rb +0 -3
- data/lib/grape/exceptions/invalid_versioner_option.rb +0 -4
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +0 -2
- data/lib/grape/exceptions/missing_mime_type.rb +0 -4
- data/lib/grape/exceptions/missing_option.rb +0 -3
- data/lib/grape/exceptions/missing_vendor_option.rb +0 -3
- data/lib/grape/exceptions/unknown_options.rb +0 -4
- data/lib/grape/exceptions/unknown_validator.rb +0 -2
- data/lib/grape/exceptions/validation_errors.rb +6 -5
- data/lib/grape/formatter/base.rb +0 -3
- data/lib/grape/formatter/json.rb +0 -2
- data/lib/grape/formatter/serializable_hash.rb +15 -16
- data/lib/grape/formatter/txt.rb +0 -2
- data/lib/grape/formatter/xml.rb +0 -2
- data/lib/grape/http/request.rb +2 -4
- data/lib/grape/locale/en.yml +1 -1
- data/lib/grape/middleware/auth/oauth2.rb +15 -6
- data/lib/grape/middleware/base.rb +7 -7
- data/lib/grape/middleware/error.rb +11 -6
- data/lib/grape/middleware/formatter.rb +80 -78
- data/lib/grape/middleware/globals.rb +13 -0
- data/lib/grape/middleware/versioner/accept_version_header.rb +0 -2
- data/lib/grape/middleware/versioner/header.rb +5 -3
- data/lib/grape/middleware/versioner/param.rb +2 -4
- data/lib/grape/middleware/versioner/path.rb +3 -4
- data/lib/grape/namespace.rb +0 -1
- data/lib/grape/parser/base.rb +0 -3
- data/lib/grape/parser/json.rb +0 -2
- data/lib/grape/parser/xml.rb +0 -2
- data/lib/grape/path.rb +1 -3
- data/lib/grape/route.rb +0 -3
- data/lib/grape/util/hash_stack.rb +1 -1
- data/lib/grape/validations.rb +72 -22
- data/lib/grape/validations/coerce.rb +5 -4
- data/lib/grape/validations/default.rb +5 -3
- data/lib/grape/validations/presence.rb +1 -1
- data/lib/grape/validations/regexp.rb +0 -2
- data/lib/grape/validations/values.rb +2 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +385 -96
- data/spec/grape/endpoint_spec.rb +162 -15
- data/spec/grape/entity_spec.rb +25 -0
- data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
- data/spec/grape/middleware/auth/oauth2_spec.rb +60 -15
- data/spec/grape/middleware/base_spec.rb +3 -8
- data/spec/grape/middleware/error_spec.rb +2 -2
- data/spec/grape/middleware/exception_spec.rb +4 -4
- data/spec/grape/middleware/formatter_spec.rb +7 -4
- data/spec/grape/middleware/versioner/param_spec.rb +8 -7
- data/spec/grape/path_spec.rb +24 -14
- data/spec/grape/util/hash_stack_spec.rb +8 -8
- data/spec/grape/validations/coerce_spec.rb +75 -33
- data/spec/grape/validations/default_spec.rb +57 -0
- data/spec/grape/validations/presence_spec.rb +13 -11
- data/spec/grape/validations/values_spec.rb +76 -2
- data/spec/grape/validations_spec.rb +443 -20
- data/spec/spec_helper.rb +2 -2
- data/spec/support/content_type_helpers.rb +11 -0
- metadata +9 -38
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NzU0ZDEzNjI0ZDhlMzg5M2YxOTMzMmU2NmUxMWIyZDYxMGM3ODBiNQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
7
|
-
|
6
|
+
NmMwYmIxODY2ZmI0YzY5MzljYTM0NWE5MDM4MzAwZDQ3M2Y4MmYwNg==
|
7
|
+
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ODgwYzYzMzlkNzJjMTgyYzkyMmQ3ZDhlNWEyNDRmMGNiZDMxZDI4NDIzZGRk
|
10
|
+
NzEwNjdjYjI0MzkzNzE1M2QwM2ExZGM2NDRlMmFlNDJlZWY3NjljMGMxN2Zm
|
11
|
+
OWNkMGM0ZGFkMDQwYzk1MDBlNDZkNjI4ZmJmZjM4M2Y3MmU4NDI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
N2Y5Y2NhNWQxMjNmYmZmMzJhZDQyMzE2NDY1ODRlNjVhNDc2ZjQxNTE4NjRl
|
14
|
+
YzczZTk5NGUwZGQ1ZmJiNTUwNmQ5MTM1ZWVkNzA0M2JlNDZhNGVkM2MyNTM5
|
15
|
+
Zjg2MzhkNGI5YjMxNzRlNWUyZDg1YzA2NjY4MjdkMWE4MTU1N2Y=
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,44 @@
|
|
1
|
-
0.
|
2
|
-
|
1
|
+
0.7.0 (4/2/2013)
|
2
|
+
=================
|
3
|
+
|
4
|
+
#### Features
|
5
|
+
* [#558](https://github.com/intridea/grape/pull/558): Support lambda-based values for params - [@wpschallenger](https://github.com/wpschallenger).
|
6
|
+
* [#510](https://github.com/intridea/grape/pull/510): Support lambda-based default values for params - [@myitcv](https://github.com/myitcv).
|
7
|
+
* [#511](https://github.com/intridea/grape/pull/511): Added `required` option for OAuth2 middleware - [@bcm](https://github.com/bcm).
|
8
|
+
* [#520](https://github.com/intridea/grape/pull/520): Use `default_error_status` to specify the default status code returned from `error!` - [@salimane](https://github.com/salimane).
|
9
|
+
* [#525](https://github.com/intridea/grape/pull/525): The default status code returned from `error!` has been changed from 403 to 500 - [@dblock](https://github.com/dblock).
|
10
|
+
* [#526](https://github.com/intridea/grape/pull/526): Allowed specifying headers in `error!` - [@dblock](https://github.com/dblock).
|
11
|
+
* [#527](https://github.com/intridea/grape/pull/527): The `before_validation` callback is now a distinct one - [@myitcv](https://github.com/myitcv).
|
12
|
+
* [#530](https://github.com/intridea/grape/pull/530): Added ability to restrict `declared(params)` to the local endpoint with `include_parent_namespaces: false` - [@myitcv](https://github.com/myitcv).
|
13
|
+
* [#531](https://github.com/intridea/grape/pull/531): Helpers are now available to auth middleware, executing in the context of the endpoint - [@joelvh](https://github.com/joelvh).
|
14
|
+
* [#540](https://github.com/intridea/grape/pull/540): Ruby 2.1.0 is now supported - [@salimane](https://github.com/salimane).
|
15
|
+
* [#544](https://github.com/intridea/grape/pull/544): The `rescue_from` keyword now handles subclasses of exceptions by default - [@xevix](https://github.com/xevix).
|
16
|
+
* [#545](https://github.com/intridea/grape/pull/545): Added `type` (`Array` or `Hash`) support to `requires`, `optional` and `group` - [@bwalex](https://github.com/bwalex).
|
17
|
+
* [#550](https://github.com/intridea/grape/pull/550): Added possibility to define reusable params - [@dm1try](https://github.com/dm1try).
|
18
|
+
* [#560](https://github.com/intridea/grape/pull/560): Use `Grape::Entity` documentation to define required and optional parameters with `requires using:` - [@reynardmh](https://github.com/reynardmh).
|
19
|
+
* [#572](https://github.com/intridea/grape/pull/572): Added `documentation` support to `requires`, `optional` and `group` parameters - [@johnallen3d](https://github.com/johnallen3d).
|
20
|
+
|
21
|
+
#### Fixes
|
22
|
+
|
23
|
+
* [#600](https://github.com/intridea/grape/pull/600): Don't use an `Entity` constant that is available in the namespace as presenter - [@fuksito](https://github.com/fuksito).
|
24
|
+
* [#590](https://github.com/intridea/grape/pull/590): Fix issue where endpoint param of type `Integer` cannot set values array - [@xevix](https://github.com/xevix).
|
25
|
+
* [#586](https://github.com/intridea/grape/pull/586): Do not repeat the same validation error messages - [@kiela](https://github.com/kiela).
|
26
|
+
* [#508](https://github.com/intridea/grape/pull/508): Allow parameters, such as content encoding, in `content_type` - [@dm1try](https://github.com/dm1try).
|
27
|
+
* [#492](https://github.com/intridea/grape/pull/492): Don't allow to have nil value when a param is required and has a list of allowed values - [@Antti](https://github.com/Antti).
|
28
|
+
* [#495](https://github.com/intridea/grape/pull/495): Fixed `ParamsScope#params` for parameters nested inside arrays - [@asross](https://github.com/asross).
|
29
|
+
* [#498](https://github.com/intridea/grape/pull/498): Dry'ed up options and headers logic, allow headers to be passed to OPTIONS requests - [@karlfreeman](https://github.com/karlfreeman).
|
30
|
+
* [#500](https://github.com/intridea/grape/pull/500): Skip entity auto-detection when explicitely passed - [@yaneq](https://github.com/yaneq).
|
31
|
+
* [#503](https://github.com/intridea/grape/pull/503): Calling declared(params) from child namespace fails to include parent namespace defined params - [@myitcv](https://github.com/myitcv).
|
32
|
+
* [#512](https://github.com/intridea/grape/pull/512): Don't create `Grape::Request` multiple times - [@dblock](https://github.com/dblock).
|
33
|
+
* [#538](https://github.com/intridea/grape/pull/538): Fixed default values for grouped params - [@dm1try](https://github.com/dm1try).
|
34
|
+
* [#549](https://github.com/intridea/grape/pull/549): Fixed handling of invalid version headers to return 406 if a header cannot be parsed - [@bwalex](https://github.com/bwalex).
|
35
|
+
* [#557](https://github.com/intridea/grape/pull/557): Pass `content_types` option to `Grape::Middleware::Error` to fix the content-type header for custom formats. - [@bernd](https://github.com/bernd).
|
36
|
+
* [#585](https://github.com/intridea/grape/pull/585): Fix after boot thread-safety issue - [@etehtsea](https://github.com/etehtsea).
|
37
|
+
* [#587](https://github.com/intridea/grape/pull/587): Fix oauth2 middleware compatibility with [draft-ietf-oauth-v2-31](http://tools.ietf.org/html/draft-ietf-oauth-v2-31) spec - [@etehtsea](https://github.com/etehtsea).
|
38
|
+
* [#610](https://github.com/intridea/grape/pull/610): Fixed group keyword was not working with type parameter - [@klausmeyer](https://github.com/klausmeyer/).
|
39
|
+
|
40
|
+
0.6.1 (10/19/2013)
|
41
|
+
==================
|
3
42
|
|
4
43
|
#### Features
|
5
44
|
|
@@ -28,7 +67,7 @@
|
|
28
67
|
* [#450](https://github.com/intridea/grape/pull/450): Added option to pass an exception handler lambda as an argument to `rescue_from` - [@robertopedroso](https://github.com/robertopedroso).
|
29
68
|
* [#443](https://github.com/intridea/grape/pull/443): Let `requires` and `optional` take blocks that initialize new scopes - [@asross](https://github.com/asross).
|
30
69
|
* [#452](https://github.com/intridea/grape/pull/452): Added `with` as a hash option to specify handlers for `rescue_from` and `error_formatter` [@robertopedroso](https://github.com/robertopedroso).
|
31
|
-
* [#433](https://github.com/intridea/grape/issues/433), [#462](https://github.com/intridea/grape/issues/462):
|
70
|
+
* [#433](https://github.com/intridea/grape/issues/433), [#462](https://github.com/intridea/grape/issues/462): Validation errors are now collected and `Grape::Exceptions::ValidationErrors` is raised - [@stevschmid](https://github.com/stevschmid).
|
32
71
|
|
33
72
|
#### Fixes
|
34
73
|
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
Contributing to Grape
|
2
|
+
=====================
|
3
|
+
|
4
|
+
Grape is work of [hundreds of contributors](https://github.com/intridea/grape/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/intridea/grape/pulls), [propose features and discuss issues](https://github.com/intridea/grape/issues). When in doubt, ask a question in the [Grape Google Group](http://groups.google.com/group/ruby-grape).
|
5
|
+
|
6
|
+
#### Fork the Project
|
7
|
+
|
8
|
+
Fork the [project on Github](https://github.com/intridea/grape) and check out your copy.
|
9
|
+
|
10
|
+
```
|
11
|
+
git clone https://github.com/contributor/grape.git
|
12
|
+
cd grape
|
13
|
+
git remote add upstream https://github.com/intridea/grape.git
|
14
|
+
```
|
15
|
+
|
16
|
+
#### Create a Topic Branch
|
17
|
+
|
18
|
+
Make sure your fork is up-to-date and create a topic branch for your feature or bug fix.
|
19
|
+
|
20
|
+
```
|
21
|
+
git checkout master
|
22
|
+
git pull upstream master
|
23
|
+
git checkout -b my-feature-branch
|
24
|
+
```
|
25
|
+
|
26
|
+
#### Bundle Install and Test
|
27
|
+
|
28
|
+
Ensure that you can build the project and run tests.
|
29
|
+
|
30
|
+
```
|
31
|
+
bundle install
|
32
|
+
bundle exec rake
|
33
|
+
```
|
34
|
+
|
35
|
+
#### Write Tests
|
36
|
+
|
37
|
+
Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [spec/grape](spec/grape).
|
38
|
+
|
39
|
+
We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix.
|
40
|
+
|
41
|
+
#### Write Code
|
42
|
+
|
43
|
+
Implement your feature or bug fix.
|
44
|
+
|
45
|
+
Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop), run `bundle exec rubocop` and fix any style issues highlighted.
|
46
|
+
|
47
|
+
Make sure that `bundle exec rake` completes without errors.
|
48
|
+
|
49
|
+
#### Write Documentation
|
50
|
+
|
51
|
+
Document any external behavior in the [README](README.md).
|
52
|
+
|
53
|
+
#### Update Changelog
|
54
|
+
|
55
|
+
Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Make it look like every other line, including your name and link to your Github account.
|
56
|
+
|
57
|
+
#### Commit Changes
|
58
|
+
|
59
|
+
Make sure git knows your name and email address:
|
60
|
+
|
61
|
+
```
|
62
|
+
git config --global user.name "Your Name"
|
63
|
+
git config --global user.email "contributor@example.com"
|
64
|
+
```
|
65
|
+
|
66
|
+
Writing good commit logs is important. A commit log should describe what changed and why.
|
67
|
+
|
68
|
+
```
|
69
|
+
git add ...
|
70
|
+
git commit
|
71
|
+
```
|
72
|
+
|
73
|
+
#### Push
|
74
|
+
|
75
|
+
```
|
76
|
+
git push origin my-feature-branch
|
77
|
+
```
|
78
|
+
|
79
|
+
#### Make a Pull Request
|
80
|
+
|
81
|
+
Go to https://github.com/contributor/grape and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days.
|
82
|
+
|
83
|
+
#### Rebase
|
84
|
+
|
85
|
+
If you've been working on a change for a while, rebase with upstream/master.
|
86
|
+
|
87
|
+
```
|
88
|
+
git fetch upstream
|
89
|
+
git rebase upstream/master
|
90
|
+
git push origin my-feature-branch -f
|
91
|
+
```
|
92
|
+
|
93
|
+
#### Update CHANGELOG Again
|
94
|
+
|
95
|
+
Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows.
|
96
|
+
|
97
|
+
```
|
98
|
+
* [#123](https://github.com/intridea/grape/pull/123): Reticulated splines - [@contributor](https://github.com/contributor).
|
99
|
+
```
|
100
|
+
|
101
|
+
Amend your previous commit and force push the changes.
|
102
|
+
|
103
|
+
```
|
104
|
+
git commit --amend
|
105
|
+
git push origin my-feature-branch -f
|
106
|
+
```
|
107
|
+
|
108
|
+
#### Check on Your Pull Request
|
109
|
+
|
110
|
+
Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above.
|
111
|
+
|
112
|
+
#### Be Patient
|
113
|
+
|
114
|
+
It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there!
|
115
|
+
|
116
|
+
#### Thank You
|
117
|
+
|
118
|
+
Please do know that we really appreciate and value your time and work. We love you, really.
|
data/Gemfile
CHANGED
@@ -10,11 +10,11 @@ group :development, :test do
|
|
10
10
|
gem 'rb-fsevent'
|
11
11
|
gem 'growl'
|
12
12
|
gem 'json'
|
13
|
-
gem 'rspec'
|
14
|
-
gem 'rack-test',
|
13
|
+
gem 'rspec', '~> 2.14.1'
|
14
|
+
gem 'rack-test', '~> 0.6.2', require: 'rack/test'
|
15
15
|
gem 'github-markup'
|
16
16
|
gem 'cookiejar'
|
17
17
|
gem 'rack-contrib'
|
18
|
-
gem 'redcarpet', :
|
19
|
-
gem 'rubocop', '~> 0.
|
18
|
+
gem 'redcarpet', platforms: :ruby
|
19
|
+
gem 'rubocop', '~> 0.15.0'
|
20
20
|
end
|
data/README.md
CHANGED
@@ -8,11 +8,11 @@ providing a simple DSL to easily develop RESTful APIs. It has built-in support
|
|
8
8
|
for common conventions, including multiple formats, subdomain/prefix restriction,
|
9
9
|
content negotiation, versioning and much more.
|
10
10
|
|
11
|
-
[![Build Status](https://travis-ci.org/intridea/grape.png?branch=master)](http://travis-ci.org/intridea/grape) [![Code Climate](https://codeclimate.com/github/intridea/grape.png)](https://codeclimate.com/github/intridea/grape)
|
11
|
+
[![Build Status](https://travis-ci.org/intridea/grape.png?branch=master)](http://travis-ci.org/intridea/grape) [![Code Climate](https://codeclimate.com/github/intridea/grape.png)](https://codeclimate.com/github/intridea/grape) [![Inline docs](http://inch-pages.github.io/github/intridea/grape.png)](http://inch-pages.github.io/github/intridea/grape) [![Dependency Status](https://www.versioneye.com/ruby/grape/0.6.1/badge.png)](https://www.versioneye.com/ruby/grape/0.6.1)
|
12
12
|
|
13
13
|
## Stable Release
|
14
14
|
|
15
|
-
You're reading the documentation for the stable release of Grape, 0.
|
15
|
+
You're reading the documentation for the stable release of Grape, 0.7.0.
|
16
16
|
|
17
17
|
## Project Resources
|
18
18
|
|
@@ -40,7 +40,6 @@ the context of recreating parts of the Twitter API.
|
|
40
40
|
```ruby
|
41
41
|
module Twitter
|
42
42
|
class API < Grape::API
|
43
|
-
|
44
43
|
version 'v1', using: :header, vendor: 'twitter'
|
45
44
|
format :json
|
46
45
|
|
@@ -55,7 +54,6 @@ module Twitter
|
|
55
54
|
end
|
56
55
|
|
57
56
|
resource :statuses do
|
58
|
-
|
59
57
|
desc "Return a public timeline."
|
60
58
|
get :public_timeline do
|
61
59
|
Status.limit(20)
|
@@ -110,7 +108,6 @@ module Twitter
|
|
110
108
|
authenticate!
|
111
109
|
current_user.statuses.find(params[:id]).destroy
|
112
110
|
end
|
113
|
-
|
114
111
|
end
|
115
112
|
end
|
116
113
|
end
|
@@ -151,7 +148,7 @@ require 'grape'
|
|
151
148
|
|
152
149
|
class API < Grape::API
|
153
150
|
get :hello do
|
154
|
-
{hello: "world"}
|
151
|
+
{ hello: "world" }
|
155
152
|
end
|
156
153
|
end
|
157
154
|
|
@@ -170,8 +167,8 @@ run Rack::Cascade.new [API, Web]
|
|
170
167
|
Place API files into `app/api` and modify `application.rb`.
|
171
168
|
|
172
169
|
```ruby
|
173
|
-
config.paths.add
|
174
|
-
config.autoload_paths += Dir[
|
170
|
+
config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')
|
171
|
+
config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')]
|
175
172
|
```
|
176
173
|
|
177
174
|
Modify `config/routes`:
|
@@ -232,6 +229,10 @@ supplied. This behavior is similar to routing in Rails. To circumvent this defau
|
|
232
229
|
one could use the `:strict` option. When this option is set to `true`, a `406 Not Acceptable` error
|
233
230
|
is returned when no correct `Accept` header is supplied.
|
234
231
|
|
232
|
+
When an invalid `Accept` header is supplied, a `406 Not Acceptable` error is returned if the `:cascade`
|
233
|
+
option is set to `false`. Otherwise a `404 Not Found` error is returned by Rack if no other route
|
234
|
+
matches.
|
235
|
+
|
235
236
|
### Accept-Version Header
|
236
237
|
|
237
238
|
```ruby
|
@@ -289,7 +290,7 @@ get :public_timeline do
|
|
289
290
|
end
|
290
291
|
```
|
291
292
|
|
292
|
-
Parameters are automatically populated from the request body on POST and PUT for form input, JSON and
|
293
|
+
Parameters are automatically populated from the request body on `POST` and `PUT` for form input, JSON and
|
293
294
|
XML content-types.
|
294
295
|
|
295
296
|
The request:
|
@@ -302,7 +303,7 @@ The Grape endpoint:
|
|
302
303
|
|
303
304
|
```ruby
|
304
305
|
post '/statuses' do
|
305
|
-
Status.create!(
|
306
|
+
Status.create!(text: params[:text])
|
306
307
|
end
|
307
308
|
```
|
308
309
|
|
@@ -311,7 +312,7 @@ Multipart POSTs and PUTs are supported as well.
|
|
311
312
|
The request:
|
312
313
|
|
313
314
|
```
|
314
|
-
curl --form image_file
|
315
|
+
curl --form image_file=@image.jpg http://localhost:9292/upload
|
315
316
|
```
|
316
317
|
|
317
318
|
The Grape endpoint:
|
@@ -322,6 +323,14 @@ post "upload" do
|
|
322
323
|
end
|
323
324
|
```
|
324
325
|
|
326
|
+
In the case of conflict between either of:
|
327
|
+
|
328
|
+
* route string parameters
|
329
|
+
* `GET`, `POST` and `PUT` parameters
|
330
|
+
* the contents of the request body on `POST` and `PUT`
|
331
|
+
|
332
|
+
route string parameters will have precedence.
|
333
|
+
|
325
334
|
## Parameter Validation and Coercion
|
326
335
|
|
327
336
|
You can define validations and coercion options for your parameters using a `params` block.
|
@@ -350,20 +359,52 @@ Optional parameters can have a default value.
|
|
350
359
|
```ruby
|
351
360
|
params do
|
352
361
|
optional :color, type: String, default: 'blue'
|
362
|
+
optional :random_number, type: Integer, default: -> { Random.rand(1..100) }
|
363
|
+
optional :non_random_number, type: Integer, default: Random.rand(1..100)
|
353
364
|
end
|
354
365
|
```
|
355
366
|
|
356
367
|
Parameters can be restricted to a specific set of values with the `:values` option.
|
357
368
|
|
369
|
+
Default values are eagerly evaluated. Above `:non_random_number` will evaluate to the same
|
370
|
+
number for each call to the endpoint of this `params` block. To have the default evaluate
|
371
|
+
at calltime use a lambda, like `:random_number` above.
|
372
|
+
|
358
373
|
```ruby
|
359
374
|
params do
|
360
375
|
requires :status, type: Symbol, values: [:not_started, :processing, :done]
|
361
376
|
end
|
362
377
|
```
|
363
378
|
|
379
|
+
The :values option can also be supplied with a `Proc` to be evalutated at runtime. For example, given a status
|
380
|
+
model you may want to restrict by hashtags that you have previously defined in the `HashTag` model.
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
params do
|
384
|
+
required :hashtag, type: String, values: -> { Hashtag.all.map(&:tag) }
|
385
|
+
end
|
386
|
+
```
|
387
|
+
|
364
388
|
Parameters can be nested using `group` or by calling `requires` or `optional` with a block.
|
365
389
|
In the above example, this means `params[:media][:url]` is required along with `params[:id]`,
|
366
|
-
and `params[:audio][:
|
390
|
+
and `params[:audio][:format]` is required only if `params[:audio]` is present.
|
391
|
+
With a block, `group`, `requires` and `optional` accept an additional option `type` which can
|
392
|
+
be either `Array` or `Hash`, and defaults to `Array`. Depending on the value, the nested
|
393
|
+
parameters will be treated either as values of a hash or as values of hashes in an array.
|
394
|
+
|
395
|
+
```ruby
|
396
|
+
params do
|
397
|
+
optional :preferences, type: Array do
|
398
|
+
requires :key
|
399
|
+
requires :value
|
400
|
+
end
|
401
|
+
|
402
|
+
requires :name, type: Hash do
|
403
|
+
requires :first_name
|
404
|
+
requires :last_name
|
405
|
+
end
|
406
|
+
end
|
407
|
+
```
|
367
408
|
|
368
409
|
### Namespace Validation and Coercion
|
369
410
|
|
@@ -389,6 +430,23 @@ end
|
|
389
430
|
The `namespace` method has a number of aliases, including: `group`, `resource`,
|
390
431
|
`resources`, and `segment`. Use whichever reads the best for your API.
|
391
432
|
|
433
|
+
You can conveniently define a route parameter as a namespace using `route_param`.
|
434
|
+
|
435
|
+
```ruby
|
436
|
+
namespace :statuses do
|
437
|
+
route_param :id do
|
438
|
+
desc "Returns all replies for a status."
|
439
|
+
get 'replies' do
|
440
|
+
Status.find(params[:id]).replies
|
441
|
+
end
|
442
|
+
desc "Returns a status."
|
443
|
+
get do
|
444
|
+
Status.find(params[:id])
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
448
|
+
```
|
449
|
+
|
392
450
|
### Custom Validators
|
393
451
|
|
394
452
|
```ruby
|
@@ -434,9 +492,9 @@ You can rescue a `Grape::Exceptions::ValidationErrors` and respond with a custom
|
|
434
492
|
```ruby
|
435
493
|
rescue_from Grape::Exceptions::ValidationErrors do |e|
|
436
494
|
Rack::Response.new({
|
437
|
-
|
438
|
-
|
439
|
-
|
495
|
+
status: e.status,
|
496
|
+
message: e.message,
|
497
|
+
errors: e.errors
|
440
498
|
}.to_json, e.status)
|
441
499
|
end
|
442
500
|
```
|
@@ -448,7 +506,6 @@ The validation errors are grouped by parameter name and can be accessed via ``Gr
|
|
448
506
|
Grape supports I18n for parameter-related error messages, but will fallback to English if
|
449
507
|
translations for the default locale have not been provided. See [en.yml](lib/grape/locale/en.yml) for message keys.
|
450
508
|
|
451
|
-
|
452
509
|
## Headers
|
453
510
|
|
454
511
|
Request headers are available through the `headers` helper or from `env` in their original form.
|
@@ -468,7 +525,13 @@ end
|
|
468
525
|
You can set a response header with `header` inside an API.
|
469
526
|
|
470
527
|
```ruby
|
471
|
-
header
|
528
|
+
header 'X-Robots-Tag', 'noindex'
|
529
|
+
```
|
530
|
+
|
531
|
+
When raising `error!`, pass additional headers as arguments.
|
532
|
+
|
533
|
+
```ruby
|
534
|
+
error! 'Unauthorized', 401, 'X-Error-Detail' => 'Invalid token.'
|
472
535
|
```
|
473
536
|
|
474
537
|
## Routes
|
@@ -520,13 +583,75 @@ class API < Grape::API
|
|
520
583
|
end
|
521
584
|
```
|
522
585
|
|
586
|
+
You can define reusable `params` using `helpers`.
|
587
|
+
|
588
|
+
```ruby
|
589
|
+
class API < Grape::API
|
590
|
+
helpers do
|
591
|
+
params :pagination do
|
592
|
+
optional :page, type: Integer
|
593
|
+
optional :per_page, type: Integer
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
desc "Get collection"
|
598
|
+
params do
|
599
|
+
use :pagination # aliases: includes, use_scope
|
600
|
+
end
|
601
|
+
get do
|
602
|
+
Collection.page(params[:page]).per(params[:per_page])
|
603
|
+
end
|
604
|
+
end
|
605
|
+
```
|
606
|
+
|
607
|
+
You can also define reusable `params` using shared helpers.
|
608
|
+
|
609
|
+
```ruby
|
610
|
+
module SharedParams
|
611
|
+
extend Grape::API::Helpers
|
612
|
+
|
613
|
+
params :period do
|
614
|
+
optional :start_date
|
615
|
+
optional :end_date
|
616
|
+
end
|
617
|
+
|
618
|
+
params :pagination do
|
619
|
+
optional :page, type: Integer
|
620
|
+
optional :per_page, type: Integer
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
class API < Grape::API
|
625
|
+
helpers SharedParams
|
626
|
+
|
627
|
+
desc "Get collection"
|
628
|
+
params do
|
629
|
+
use :period, :pagination
|
630
|
+
end
|
631
|
+
get do
|
632
|
+
Collection.from(params[:start_date]).to(params[:end_date])
|
633
|
+
.page(params[:page]).per(params[:per_page])
|
634
|
+
end
|
635
|
+
end
|
636
|
+
```
|
637
|
+
|
638
|
+
## Parameter Documentation
|
639
|
+
|
640
|
+
You can attach additional documentation to `params` using a `documentation` hash.
|
641
|
+
|
642
|
+
```ruby
|
643
|
+
params do
|
644
|
+
optional :first_name, type: String, documentation: { example: 'Jim' }
|
645
|
+
requires :last_name, type: String, documentation: { example: 'Smith' }
|
646
|
+
end
|
647
|
+
```
|
648
|
+
|
523
649
|
## Cookies
|
524
650
|
|
525
651
|
You can set, get and delete your cookies very simply using `cookies` method.
|
526
652
|
|
527
653
|
```ruby
|
528
654
|
class API < Grape::API
|
529
|
-
|
530
655
|
get 'status_count' do
|
531
656
|
cookies[:status_count] ||= 0
|
532
657
|
cookies[:status_count] += 1
|
@@ -536,7 +661,6 @@ class API < Grape::API
|
|
536
661
|
delete 'status_count' do
|
537
662
|
{ status_count: cookies.delete(:status_count) }
|
538
663
|
end
|
539
|
-
|
540
664
|
end
|
541
665
|
```
|
542
666
|
|
@@ -544,10 +668,10 @@ Use a hash-based syntax to set more than one value.
|
|
544
668
|
|
545
669
|
```ruby
|
546
670
|
cookies[:status_count] = {
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
671
|
+
value: 0,
|
672
|
+
expires: Time.tomorrow,
|
673
|
+
domain: '.twitter.com',
|
674
|
+
path: '/'
|
551
675
|
}
|
552
676
|
|
553
677
|
cookies[:status_count][:value] +=1
|
@@ -570,11 +694,11 @@ cookies.delete :status_count, path: '/'
|
|
570
694
|
You can redirect to a new url temporarily (302) or permanently (301).
|
571
695
|
|
572
696
|
```ruby
|
573
|
-
redirect
|
697
|
+
redirect '/statuses'
|
574
698
|
```
|
575
699
|
|
576
700
|
```ruby
|
577
|
-
redirect
|
701
|
+
redirect '/statuses', permanent: true
|
578
702
|
```
|
579
703
|
|
580
704
|
## Allowed Methods
|
@@ -585,13 +709,11 @@ behavior with `do_not_route_head!`.
|
|
585
709
|
|
586
710
|
``` ruby
|
587
711
|
class API < Grape::API
|
588
|
-
|
589
712
|
do_not_route_head!
|
590
713
|
|
591
714
|
get '/example' do
|
592
715
|
# only responds to GET
|
593
716
|
end
|
594
|
-
|
595
717
|
end
|
596
718
|
```
|
597
719
|
|
@@ -601,7 +723,6 @@ include an "Allow" header listing the supported methods.
|
|
601
723
|
|
602
724
|
```ruby
|
603
725
|
class API < Grape::API
|
604
|
-
|
605
726
|
get '/rt_count' do
|
606
727
|
{ rt_count: current_user.rt_count }
|
607
728
|
end
|
@@ -613,7 +734,6 @@ class API < Grape::API
|
|
613
734
|
current_user.rt_count += params[:value].to_i
|
614
735
|
{ rt_count: current_user.rt_count }
|
615
736
|
end
|
616
|
-
|
617
737
|
end
|
618
738
|
```
|
619
739
|
|
@@ -646,14 +766,27 @@ curl -X DELETE -v http://localhost:3000/rt_count/
|
|
646
766
|
You can abort the execution of an API method by raising errors with `error!`.
|
647
767
|
|
648
768
|
```ruby
|
649
|
-
error!
|
769
|
+
error! 'Access Denied', 401
|
650
770
|
```
|
651
771
|
|
652
772
|
You can also return JSON formatted objects by raising error! and passing a hash
|
653
773
|
instead of a message.
|
654
774
|
|
655
775
|
```ruby
|
656
|
-
error!({
|
776
|
+
error!({ error: "unexpected error", detail: "missing widget" }, 500)
|
777
|
+
```
|
778
|
+
|
779
|
+
### Default Error HTTP Status Code
|
780
|
+
|
781
|
+
By default Grape returns a 500 status code from `error!`. You can change this with `default_error_status`.
|
782
|
+
|
783
|
+
``` ruby
|
784
|
+
class API < Grape::API
|
785
|
+
default_error_status 400
|
786
|
+
get '/example' do
|
787
|
+
error! "This should have http status code 400"
|
788
|
+
end
|
789
|
+
end
|
657
790
|
```
|
658
791
|
|
659
792
|
### Handling 404
|
@@ -684,10 +817,12 @@ You can also rescue specific exceptions.
|
|
684
817
|
|
685
818
|
```ruby
|
686
819
|
class Twitter::API < Grape::API
|
687
|
-
rescue_from ArgumentError,
|
820
|
+
rescue_from ArgumentError, UserDefinedError
|
688
821
|
end
|
689
822
|
```
|
690
823
|
|
824
|
+
In this case ```UserDefinedError``` must be inherited from ```StandardError```.
|
825
|
+
|
691
826
|
The error format will match the request format. See "Content-Types" below.
|
692
827
|
|
693
828
|
Custom error formatters for existing and additional types can be defined with a proc.
|
@@ -741,14 +876,49 @@ Or rescue specific exceptions.
|
|
741
876
|
```ruby
|
742
877
|
class Twitter::API < Grape::API
|
743
878
|
rescue_from ArgumentError do |e|
|
744
|
-
Rack::Response.new([ "ArgumentError: #{e.message}" ], 500)
|
879
|
+
Rack::Response.new([ "ArgumentError: #{e.message}" ], 500).finish
|
745
880
|
end
|
746
881
|
rescue_from NotImplementedError do |e|
|
747
|
-
Rack::Response.new([ "NotImplementedError: #{e.message}" ], 500)
|
882
|
+
Rack::Response.new([ "NotImplementedError: #{e.message}" ], 500).finish
|
748
883
|
end
|
749
884
|
end
|
750
885
|
```
|
751
886
|
|
887
|
+
By default, `rescue_from` will rescue the exceptions listed and all their subclasses.
|
888
|
+
|
889
|
+
Assume you have the following exception classes defined.
|
890
|
+
|
891
|
+
```ruby
|
892
|
+
module APIErrors
|
893
|
+
class ParentError < StandardError; end
|
894
|
+
class ChildError < ParentError; end
|
895
|
+
end
|
896
|
+
```
|
897
|
+
|
898
|
+
Then the following `rescue_from` clause will rescue exceptions of type `APIErrors::ParentError` and its subclasses (in this case `APIErrors::ChildError`).
|
899
|
+
|
900
|
+
```ruby
|
901
|
+
rescue_from APIErrors::ParentError do |e|
|
902
|
+
Rack::Response.new({
|
903
|
+
error: "#{e.class} error",
|
904
|
+
message: e.message
|
905
|
+
}.to_json, e.status).finish
|
906
|
+
end
|
907
|
+
```
|
908
|
+
|
909
|
+
To only rescue the base exception class, set `rescue_subclasses: false`.
|
910
|
+
The code below will rescue exceptions of type `RuntimeError` but _not_ its subclasses.
|
911
|
+
|
912
|
+
```ruby
|
913
|
+
rescue_from RuntimeError, rescue_subclasses: false do |e|
|
914
|
+
Rack::Response.new(
|
915
|
+
status: e.status,
|
916
|
+
message: e.message,
|
917
|
+
errors: e.errors
|
918
|
+
}.to_json, e.status).finish
|
919
|
+
end
|
920
|
+
```
|
921
|
+
|
752
922
|
#### Rails 3.x
|
753
923
|
|
754
924
|
When mounted inside containers, such as Rails 3.x, errors like "404 Not Found" or
|
@@ -1008,7 +1178,6 @@ The following example exposes statuses.
|
|
1008
1178
|
|
1009
1179
|
```ruby
|
1010
1180
|
module API
|
1011
|
-
|
1012
1181
|
module Entities
|
1013
1182
|
class Status < Grape::Entity
|
1014
1183
|
expose :user_name
|
@@ -1024,7 +1193,7 @@ module API
|
|
1024
1193
|
version 'v1'
|
1025
1194
|
|
1026
1195
|
desc 'Statuses index', {
|
1027
|
-
|
1196
|
+
params: API::Entities::Status.documentation
|
1028
1197
|
}
|
1029
1198
|
get '/statuses' do
|
1030
1199
|
statuses = Status.all
|
@@ -1035,6 +1204,23 @@ module API
|
|
1035
1204
|
end
|
1036
1205
|
```
|
1037
1206
|
|
1207
|
+
You can use entity documentation directly in the params block with `using: Entity.documentation`.
|
1208
|
+
|
1209
|
+
```ruby
|
1210
|
+
module API
|
1211
|
+
class Statuses < Grape::API
|
1212
|
+
version 'v1'
|
1213
|
+
|
1214
|
+
desc 'Create a status', {
|
1215
|
+
requires :all, except: [:ip], using: API::Entities::Status.documentation.except(:id)
|
1216
|
+
}
|
1217
|
+
post '/status' do
|
1218
|
+
Status.create! params
|
1219
|
+
end
|
1220
|
+
end
|
1221
|
+
end
|
1222
|
+
```
|
1223
|
+
|
1038
1224
|
You can present with multiple entities using an optional Symbol argument.
|
1039
1225
|
|
1040
1226
|
```ruby
|
@@ -1062,7 +1248,7 @@ classes underneath the model they represent.
|
|
1062
1248
|
```ruby
|
1063
1249
|
class Status
|
1064
1250
|
def entity
|
1065
|
-
|
1251
|
+
Entity.new(self)
|
1066
1252
|
end
|
1067
1253
|
|
1068
1254
|
class Entity < Grape::Entity
|
@@ -1083,14 +1269,19 @@ You can use any Hypermedia representer, including [Roar](https://github.com/apot
|
|
1083
1269
|
Roar renders JSON and works with the built-in Grape JSON formatter. Add `Roar::Representer::JSON`
|
1084
1270
|
into your models or call `to_json` explicitly in your API implementation.
|
1085
1271
|
|
1086
|
-
Other alternatives include `ActiveModel::Serializers` via [grape-active_model_serializers](https://github.com/jrhe/grape-active_model_serializers).
|
1087
|
-
|
1088
1272
|
### Rabl
|
1089
1273
|
|
1090
1274
|
You can use [Rabl](https://github.com/nesquena/rabl) templates with the help of the
|
1091
1275
|
[grape-rabl](https://github.com/LTe/grape-rabl) gem, which defines a custom Grape Rabl
|
1092
1276
|
formatter.
|
1093
1277
|
|
1278
|
+
### Active Model Serializers
|
1279
|
+
|
1280
|
+
You can use [Active Model Serializers](https://github.com/rails-api/active_model_serializers) serializers with the help of the
|
1281
|
+
[grape-active_model_serializers](https://github.com/jrhe/grape-active_model_serializers) gem, which defines a custom Grape AMS
|
1282
|
+
formatter.
|
1283
|
+
|
1284
|
+
|
1094
1285
|
## Authentication
|
1095
1286
|
|
1096
1287
|
### Basic and Digest Auth
|
@@ -1165,7 +1356,21 @@ end
|
|
1165
1356
|
|
1166
1357
|
## Before and After
|
1167
1358
|
|
1168
|
-
|
1359
|
+
Blocks can be executed before or after every API call, using `before`, `after`,
|
1360
|
+
`before_validation` and `after_validation`.
|
1361
|
+
|
1362
|
+
Before and after callbacks execute in the following order:
|
1363
|
+
|
1364
|
+
1. `before`
|
1365
|
+
2. `before_validation`
|
1366
|
+
3. _validations_
|
1367
|
+
4. `after_validation`
|
1368
|
+
5. _the API call_
|
1369
|
+
6. `after`
|
1370
|
+
|
1371
|
+
Steps 4, 5 and 6 only happen if validation succeeds.
|
1372
|
+
|
1373
|
+
E.g. using `before`:
|
1169
1374
|
|
1170
1375
|
```ruby
|
1171
1376
|
before do
|
@@ -1173,6 +1378,68 @@ before do
|
|
1173
1378
|
end
|
1174
1379
|
```
|
1175
1380
|
|
1381
|
+
The block applies to every API call within and below the current namespace:
|
1382
|
+
|
1383
|
+
```ruby
|
1384
|
+
class MyAPI < Grape::API
|
1385
|
+
get '/' do
|
1386
|
+
"root - #{@blah}"
|
1387
|
+
end
|
1388
|
+
|
1389
|
+
namespace :foo do
|
1390
|
+
before do
|
1391
|
+
@blah = 'blah'
|
1392
|
+
end
|
1393
|
+
|
1394
|
+
get '/' do
|
1395
|
+
"root - foo - #{@blah}"
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
namespace :bar do
|
1399
|
+
get '/' do
|
1400
|
+
"root - foo - bar - #{@blah}"
|
1401
|
+
end
|
1402
|
+
end
|
1403
|
+
end
|
1404
|
+
end
|
1405
|
+
```
|
1406
|
+
|
1407
|
+
The behaviour is then:
|
1408
|
+
|
1409
|
+
```bash
|
1410
|
+
GET / # 'root - '
|
1411
|
+
GET /foo # 'root - foo - blah'
|
1412
|
+
GET /foo/bar # 'root - foo - bar - blah'
|
1413
|
+
```
|
1414
|
+
|
1415
|
+
Params on a `namespace` (or whatever alias you are using) also work when using
|
1416
|
+
`before_validation` or `after_validation`:
|
1417
|
+
|
1418
|
+
```ruby
|
1419
|
+
class MyAPI < Grape::API
|
1420
|
+
params do
|
1421
|
+
requires :blah, type: Integer
|
1422
|
+
end
|
1423
|
+
resource ':blah' do
|
1424
|
+
after_validation do
|
1425
|
+
# if we reach this point validations will have passed
|
1426
|
+
@blah = declared(params, include_missing: false)[:blah]
|
1427
|
+
end
|
1428
|
+
|
1429
|
+
get '/' do
|
1430
|
+
@blah.class
|
1431
|
+
end
|
1432
|
+
end
|
1433
|
+
end
|
1434
|
+
```
|
1435
|
+
|
1436
|
+
The behaviour is then:
|
1437
|
+
|
1438
|
+
```bash
|
1439
|
+
GET /123 # 'Fixnum'
|
1440
|
+
GET /foo # 400 error - 'blah is invalid'
|
1441
|
+
```
|
1442
|
+
|
1176
1443
|
## Anchoring
|
1177
1444
|
|
1178
1445
|
Grape by default anchors all request paths, which means that the request URL
|
@@ -1281,31 +1548,28 @@ Add API paths to `config/application.rb`.
|
|
1281
1548
|
|
1282
1549
|
```ruby
|
1283
1550
|
# Auto-load API and its subdirectories
|
1284
|
-
config.paths.add "app
|
1285
|
-
config.autoload_paths += Dir[
|
1551
|
+
config.paths.add File.join("app", "api"), glob: File.join("**", "*.rb")
|
1552
|
+
config.autoload_paths += Dir[Rails.root.join("app", "api", "*")]
|
1286
1553
|
```
|
1287
1554
|
|
1288
1555
|
Create `config/initializers/reload_api.rb`.
|
1289
1556
|
|
1290
1557
|
```ruby
|
1291
1558
|
if Rails.env.development?
|
1292
|
-
|
1293
1559
|
ActiveSupport::Dependencies.explicitly_unloadable_constants << "Twitter::API"
|
1294
1560
|
|
1295
|
-
api_files = Dir[
|
1561
|
+
api_files = Dir[Rails.root.join('app', 'api', '**', '*.rb')]
|
1296
1562
|
api_reloader = ActiveSupport::FileUpdateChecker.new(api_files) do
|
1297
1563
|
Rails.application.reload_routes!
|
1298
1564
|
end
|
1299
1565
|
ActionDispatch::Callbacks.to_prepare do
|
1300
1566
|
api_reloader.execute_if_updated
|
1301
1567
|
end
|
1302
|
-
|
1303
1568
|
end
|
1304
1569
|
```
|
1305
1570
|
|
1306
1571
|
See [StackOverflow #3282655](http://stackoverflow.com/questions/3282655/ruby-on-rails-3-reload-lib-directory-for-each-request/4368838#4368838) for more information.
|
1307
1572
|
|
1308
|
-
|
1309
1573
|
## Performance Monitoring
|
1310
1574
|
|
1311
1575
|
Grape integrates with NewRelic via the
|
@@ -1314,14 +1578,10 @@ with Librato Metrics with the [grape-librato](https://github.com/seanmoon/grape-
|
|
1314
1578
|
|
1315
1579
|
## Contributing to Grape
|
1316
1580
|
|
1317
|
-
Grape is work of
|
1581
|
+
Grape is work of hundreds of contributors. You're encouraged to submit pull requests, propose
|
1318
1582
|
features and discuss issues.
|
1319
1583
|
|
1320
|
-
|
1321
|
-
* Write tests for your new feature or a test that reproduces a bug
|
1322
|
-
* Implement your feature or make a bug fix
|
1323
|
-
* Add a line to `CHANGELOG.md` describing your change
|
1324
|
-
* Commit, push and make a pull request. Bonus points for topic branches.
|
1584
|
+
See [CONTRIBUTING](CONTRIBUTING.md).
|
1325
1585
|
|
1326
1586
|
## License
|
1327
1587
|
|