grape_ape_rails 0.5.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +255 -3
- data/grape_ape_rails.gemspec +1 -0
- data/lib/grape_ape_rails/api.rb +29 -10
- data/lib/grape_ape_rails/handlers/formatters.rb +11 -7
- data/lib/grape_ape_rails/version.rb +1 -1
- data/spec/dummy/app/controllers/api/v1/widgets.rb +4 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/requests/v1/widgets_spec.rb +8 -0
- data/spec/spec_helper.rb +3 -1
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e824e2537351f93db1a3ed5cfdce4ec40b81d0f2
|
4
|
+
data.tar.gz: 1a3e327e2214920119b74e73860de459de9bdc87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb81cbfa2a74492f1a6dd920c5465ff551a86f2af6d6341fdc770b80d1cdef3ea045320f0f0f6728b7c2ee261dbe7835d07d3ffb12cc5e1aa604a11a35a9e568
|
7
|
+
data.tar.gz: 670bcf477439e38f345e02b574136822087a039d0a2d8e7cbbd3d6e0de3c5969a78a7e4f925975e6434cb59c8e2e2bbf392ea4748ad3e5b2cbd7436fa4b5bc6e
|
data/README.md
CHANGED
@@ -1,6 +1,32 @@
|
|
1
1
|
# GrapeApeRails
|
2
2
|
|
3
|
-
|
3
|
+
[](http://badge.fury.io/rb/grape_ape_rails)
|
4
|
+
[](https://codeclimate.com/github/mepatterson/grape_ape_rails)
|
5
|
+
[](https://codeclimate.com/github/mepatterson/grape_ape_rails)
|
6
|
+
[](https://semaphoreapp.com/mepatterson/grape_ape_rails)
|
7
|
+
|
8
|
+
The general purpose of this gem is to wrap the various best practices of integrating GrapeAPI within the context of a Rails app
|
9
|
+
into an easy-to-use macro-framework and DSL, plus some opinionated value-adds and features. Basically, Grape and Rails play
|
10
|
+
together great, but can be tricky to integrate in a robust, clean way; GrapeApeRails (hopefully) makes your life easier.
|
11
|
+
|
12
|
+
## Features/Opinions
|
13
|
+
|
14
|
+
GrapeApeRails is opinionated. The goal is to make integration easier, so GrapeApeRails makes a number of integration decisions for you:
|
15
|
+
|
16
|
+
* API endpoints respond with JSON
|
17
|
+
* API endpoints expect serialized JSON strings for POST and PUT bodies
|
18
|
+
* JSON responses are wrapped in a structure that mostly resembles the JSON-RPC spec
|
19
|
+
* GrapeApeRails APIs are header-versioned using the 'Accept' HTTP header
|
20
|
+
* API endpoints automatically handle locale if provided (either via params[:locale] or the 'Accept-Language' header) and use the `http_accept_language` middleware
|
21
|
+
* GrapeApeRails provides an ActiveSupport::Notification that can be suscribed to for logging/injection into the Rails log
|
22
|
+
* Pagination support is already included for all endpoints via the [grape-kaminari gem](https://github.com/monterail/grape-kaminari)
|
23
|
+
* Rails cache support is already included for all endpoints via the [grape-rails-cache gem](https://github.com/monterail/grape-rails-cache)
|
24
|
+
* API endpoints are automagically documented into [Swagger API XML docs](https://helloreverb.com/developers/swagger), presented by your API via mounted endpoints
|
25
|
+
* Swagger-documented APIs are automatically separated into different URIs by API version
|
26
|
+
|
27
|
+
If these opinions and features align closely to what you're planning to build for your API, I think you'll find GrapeApeRails very useful.
|
28
|
+
If you're intending to build something very different from the above, you're probably better off integrating Grape on your own, or looking at alternate projects
|
29
|
+
like [Rails::API](https://github.com/rails-api/rails-api).
|
4
30
|
|
5
31
|
## Installation
|
6
32
|
|
@@ -16,13 +42,239 @@ Or install it yourself as:
|
|
16
42
|
|
17
43
|
$ gem install grape_ape_rails
|
18
44
|
|
45
|
+
## Setup
|
46
|
+
|
47
|
+
First, GrapeApeRails needs an initializer as `config/initializers/grape_ape_rails.rb`
|
48
|
+
|
49
|
+
The easiest way to do this is to run the handy rails generator command:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
rails g grape_ape_rails:setup
|
53
|
+
```
|
54
|
+
|
55
|
+
This will:
|
56
|
+
* create your initializer with some default settings, based on how you answer the questions
|
57
|
+
* add a `tilt.root` Rack config to your application.rb for use with Rabl templates
|
58
|
+
|
59
|
+
Next, you'll need to create your `API::Base` class, which serves as the starting point for all
|
60
|
+
of your API endpoints and all versions of your API. By default, GrapeApeRails looks for a
|
61
|
+
base.rb file in `app/controllers/api/base.rb`
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
# app/controllers/api/base.rb
|
65
|
+
module API
|
66
|
+
class Base < GrapeApeRails::API
|
67
|
+
grape_apis do
|
68
|
+
api "V1" do
|
69
|
+
# ... mounts go here
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
NOTE: the api name needs to look like a class name, with camelcase (e.g. "V1" or "AdminV1").
|
77
|
+
This actually _is_ the class that GrapeApeRails will then expect you to define for exposing your endpoints.
|
78
|
+
Internally, Grape will translate this to an under-dotted version name (e.g "v1" or "admin.v1")
|
79
|
+
and this is what will be expected in the Accept header string provided by the requestor.
|
80
|
+
|
19
81
|
## Usage
|
20
82
|
|
21
|
-
|
83
|
+
Now that you've got your base.rb ready, you need to mount your various resources/endpoints. This is
|
84
|
+
done via the `grape_mount` command. These should generally be mounted as pluralized resource names.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# inside the base.rb ...
|
88
|
+
api "V1" do
|
89
|
+
grape_mount :widgets
|
90
|
+
grape_mount :robots
|
91
|
+
grape_mount :pirates
|
92
|
+
# ... etc
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
If you try to spin up your app now, it will complain that you need to actually create the class files
|
97
|
+
for each of the resources you've mounted. So let's do that in a 'v1' subfolder...
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# app/controllers/api/v1/widgets.rb
|
101
|
+
module API
|
102
|
+
module V1
|
103
|
+
class Widgets < GrapeApeRails::API
|
104
|
+
include GrapeApeRails::Handlers::All
|
105
|
+
|
106
|
+
resource :widgets do
|
107
|
+
desc "Get a single Widget"
|
108
|
+
get ':id', rabl: 'v1/widget' do
|
109
|
+
@widget = Widget.find(params[:id])
|
110
|
+
@widget
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
In this case, I've simply created a `/widgets/:id` endpoint, but you can define whatever endpoints you want.
|
119
|
+
Because you've defined this within the :widgets resource, the API will assume all of these endpoints begin
|
120
|
+
with `/widgets`
|
121
|
+
|
122
|
+
### Rabl
|
123
|
+
|
124
|
+
In my example, I'm using the default Rabl-based templating. The `rabl` parameter expects a path to the .rabl template file,
|
125
|
+
as found within /app/views/api/ ... all Rabl functionality should work as expected.
|
126
|
+
|
127
|
+
NOTE: Unlike the ActiveModel Serializers, the Rabl formatter doesn't impose any sort of resource-keyed hash for the response.
|
128
|
+
This is your responsibility to define in your Rabl template if you want to use the "plural key, always-array" standard.
|
129
|
+
|
130
|
+
Example Rabl templates:
|
131
|
+
```ruby
|
132
|
+
# v1/base_widget.rabl
|
133
|
+
attributes *Widget.column_names
|
134
|
+
|
135
|
+
# single widget (v1/widget.rabl)
|
136
|
+
collection [@widget] => :widgets
|
137
|
+
extends "v1/base_widget"
|
138
|
+
|
139
|
+
# array of multiple widgets (v1/widgets.rabl)
|
140
|
+
collection @widgets => :widgets
|
141
|
+
extends "v1/base_widget"
|
142
|
+
```
|
143
|
+
|
144
|
+
### ActiveModel Serializers
|
145
|
+
|
146
|
+
If you'd prefer ActiveModel Serializers over Rabl, GrapeApeRails supports that as well via a custom Grape formatter
|
147
|
+
called `Grape::Formatter::GarActiveModelSerializers`. To use it, just override the `formatter` within each of your API
|
148
|
+
classes.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
module API
|
152
|
+
module V1
|
153
|
+
class Monkeys < GrapeApeRails::API
|
154
|
+
include GrapeApeRails::Handlers::All
|
155
|
+
|
156
|
+
formatter :json, Grape::Formatter::GarActiveModelSerializers
|
157
|
+
|
158
|
+
resource :monkeys do
|
159
|
+
desc "Get a single Monkey"
|
160
|
+
get ':id' do
|
161
|
+
@monkey = Monkey.find(params[:id])
|
162
|
+
@monkey
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
In this case, it's expected that you've defined a MonkeySerializer class in your models directory, as usual with ActiveModelSerializers.
|
171
|
+
|
172
|
+
IMPORTANT: For ActiveModel Serializers, when defining the response structure, I made the decision (based on lots of research) to go with a plural resource key and an _always-array_ approach in the
|
173
|
+
response hash. To put it another way:
|
174
|
+
|
175
|
+
If you ask for /widgets/1 you will get
|
176
|
+
|
177
|
+
`{ "result" : { "widgets" : [ {<widget>} ] } }`
|
178
|
+
|
179
|
+
and if you ask for /widgets you will get
|
180
|
+
|
181
|
+
`{ "result" : { "widgets" : [ {widget1}, {widget2}, {widget3}, ... ] } }`
|
182
|
+
|
183
|
+
#### Overriding the ActiveModel Serializer resource key
|
184
|
+
|
185
|
+
If you need, for some reason, to provide a different key in your response hash, you can
|
186
|
+
override the resource key for the plural form of your resource.
|
187
|
+
|
188
|
+
Let's say, for example, that you've created a `UserSerializer` but you want the JSON to use 'people'...
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
class UserSerializer < ActiveModel::Serializer
|
192
|
+
attributes :name, :age, :hair_color
|
193
|
+
|
194
|
+
def resource_plural
|
195
|
+
'people'
|
196
|
+
end
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
... GrapeApeRails will now always key the hash with `"people" : [...one or more people hashes...]` for both single (1-element array) and multiple (n-element array) records.
|
201
|
+
|
202
|
+
### JSON Response Structures
|
203
|
+
|
204
|
+
Similar to the JSON-RPC spec, endpoints exposed using GrapeApeRails will present either a `result` hash or an `error` hash.
|
205
|
+
The error hash will be composed of a `code` (a machine-friendly enum-like uppercase string) and a
|
206
|
+
message (human-friendly, user-presentable message that includes the code inside brackets).
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
# Successful response
|
210
|
+
{
|
211
|
+
"result" : {
|
212
|
+
"widgets" : [
|
213
|
+
{ "id" : 1, "name" : "Fancy Widget" },
|
214
|
+
{ "id" : 2, "name" : "Other Thing" },
|
215
|
+
...
|
216
|
+
]
|
217
|
+
}
|
218
|
+
}
|
219
|
+
|
220
|
+
# Error response
|
221
|
+
{
|
222
|
+
"error" : {
|
223
|
+
"code" : "UNAUTHORIZED",
|
224
|
+
"message" : "[UNAUTHORIZED] Requires a valid user authorization"
|
225
|
+
}
|
226
|
+
}
|
227
|
+
```
|
228
|
+
|
229
|
+
In the case of a validation error on a resource, there will additionally be a `data` key inside the error hash that includes Rails-style validation errors.
|
230
|
+
|
231
|
+
### Pagination
|
232
|
+
|
233
|
+
GrapeApeRails uses Kaminari for pagination via the [grape-kaminari](https://github.com/monterail/grape-kaminari) gem.
|
234
|
+
|
235
|
+
Enabling pagination on a resource is super-simple:
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
# inside your API resource class...
|
239
|
+
desc "Return a list of Widgets"
|
240
|
+
params :pagination do
|
241
|
+
optional :page, type: Integer
|
242
|
+
optional :per_page, type: Integer
|
243
|
+
optional :offset, type: Integer
|
244
|
+
end
|
245
|
+
paginate per_page: 30
|
246
|
+
get '/', rabl: 'v1/widgets' do
|
247
|
+
@widgets = paginate(@widgets)
|
248
|
+
end
|
249
|
+
```
|
250
|
+
|
251
|
+
_From the grape-kaminari docs:_
|
252
|
+
|
253
|
+
Now you can make a HTTP request to your endpoint with the following parameters
|
254
|
+
|
255
|
+
- `page`: your current page (default: 1)
|
256
|
+
- `per_page`: how many to record in a page (default: 10)
|
257
|
+
- `offset`: the offset to start from (default: 0)
|
258
|
+
|
259
|
+
```
|
260
|
+
curl -v http://host.dev/widgets?page=3&offset=10
|
261
|
+
```
|
262
|
+
|
263
|
+
and the response will be paginated and also will include pagination headers
|
264
|
+
|
265
|
+
```
|
266
|
+
X-Total: 42
|
267
|
+
X-Total-Pages: 5
|
268
|
+
X-Page: 3
|
269
|
+
X-Per-Page: 10
|
270
|
+
X-Next-Page: 4
|
271
|
+
X-Prev-Page: 2
|
272
|
+
X-Offset: 10
|
273
|
+
```
|
22
274
|
|
23
275
|
## Contributing
|
24
276
|
|
25
|
-
1. Fork it
|
277
|
+
1. Fork it
|
26
278
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
279
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
280
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/grape_ape_rails.gemspec
CHANGED
data/lib/grape_ape_rails/api.rb
CHANGED
@@ -42,17 +42,10 @@ module GrapeApeRails
|
|
42
42
|
@gar_api_version = name
|
43
43
|
mounts_klass = Class.new(GrapeApeRails::API)
|
44
44
|
klass = GrapeApeRails.const_set("#{name}Base", mounts_klass)
|
45
|
-
api_key =
|
46
|
-
api_key = args[0] if args[0].present? && args[0].is_a?(String)
|
47
|
-
|
45
|
+
api_key = api_key_from(args)
|
48
46
|
api_version = name.underscore.gsub('_','.')
|
49
|
-
|
50
|
-
|
51
|
-
elsif args[1].present? && args[1].is_a?(Array)
|
52
|
-
GrapeApeRails::API.api_version_cascades_map.merge!({ api_version => args[1].map{ |v| v.underscore.gsub('_','.') } })
|
53
|
-
end
|
54
|
-
dotname = name.underscore.gsub('_','.')
|
55
|
-
GrapeApeRails::API.api_keys_map.merge!({ dotname => api_key })
|
47
|
+
update_api_version_cascades_map(api_version, args)
|
48
|
+
update_api_keys_map(name, api_key)
|
56
49
|
yield
|
57
50
|
mount mounts_klass
|
58
51
|
ensure
|
@@ -72,6 +65,32 @@ module GrapeApeRails
|
|
72
65
|
puts " [ERROR] Could not instantiate API Endpoints at '#{name}'. Maybe you still need to create the class file?"
|
73
66
|
end
|
74
67
|
|
68
|
+
private
|
69
|
+
|
70
|
+
class << self
|
71
|
+
def api_key_from(args)
|
72
|
+
api_key = GrapeApeRails.configuration.api_secret_key
|
73
|
+
api_key = args[0] if args[0].present? && args[0].is_a?(String)
|
74
|
+
api_key
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_api_keys_map(name, api_key)
|
78
|
+
GrapeApeRails::API.api_keys_map.merge!({ name.underscore.gsub('_','.') => api_key })
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_api_version_cascades_map(api_version, args)
|
82
|
+
return unless arr = cascades_array_from(args)
|
83
|
+
cascades = arr.map{ |v| v.underscore.gsub('_','.') }
|
84
|
+
GrapeApeRails::API.api_version_cascades_map.merge!({ api_version => cascades })
|
85
|
+
end
|
86
|
+
|
87
|
+
def cascades_array_from(args)
|
88
|
+
return args[0] if args[0].present? && args[0].is_a?(Array)
|
89
|
+
return args[1] if args[1].present? && args[1].is_a?(Array)
|
90
|
+
return nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
75
94
|
end
|
76
95
|
end
|
77
96
|
|
@@ -34,10 +34,17 @@ module Grape
|
|
34
34
|
module GarActiveModelSerializers
|
35
35
|
def self.call(resource, env)
|
36
36
|
if serializer = Grape::Formatter::ActiveModelSerializers.fetch_serializer(resource, env)
|
37
|
-
single = serializer.try(:resource_singular) || serializer.object.class.name.underscore
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
# single = serializer.try(:resource_singular) || serializer.object.class.name.underscore
|
38
|
+
# default_plural = default_plural = serializer.object.class.name.underscore.pluralize
|
39
|
+
# if serializer.object.is_a?(Array)
|
40
|
+
# single = serializer.object.first.class.name.underscore
|
41
|
+
# default_plural = serializer.object.first.class.name.underscore.pluralize
|
42
|
+
# end
|
43
|
+
# plural = serializer.try(:resource_plural) || default_plural
|
44
|
+
single = serializer.try(:resource_singular) || serializer.instance_variable_get(:@resource_name).try(:singularize) || serializer.instance_variable_get(:@object).class.name.underscore
|
45
|
+
plural = serializer.try(:resource_plural) || serializer.instance_variable_get(:@resource_name) || single.pluralize
|
46
|
+
hash = serializer.as_json(root: false)
|
47
|
+
output = if hash.is_a?(Hash) && hash[single].present?
|
41
48
|
hash[single].is_a?(Array) ? hash[single] : [hash[single]]
|
42
49
|
else
|
43
50
|
hash.is_a?(Array) ? hash : [hash]
|
@@ -77,14 +84,11 @@ module Grape
|
|
77
84
|
private
|
78
85
|
|
79
86
|
def wrap_output(output)
|
80
|
-
# root_resource = endpoint.route.route_namespace.gsub('/','').pluralize
|
81
87
|
template = MultiJson.dump({ result: "***" })
|
82
|
-
# output = "[#{output}]" if output[0] != '[' && output[-1] != ']'
|
83
88
|
template.gsub(%Q("***"), output)
|
84
89
|
end
|
85
90
|
|
86
91
|
def view_path(template)
|
87
|
-
# api_version = endpoint.route.route_version || '.'
|
88
92
|
if template.split('.')[-1] == 'rabl'
|
89
93
|
File.join(env['api.tilt.root'], template)
|
90
94
|
else
|
data/spec/dummy/db/test.sqlite3
CHANGED
Binary file
|
@@ -9,6 +9,14 @@ describe API::V1::Widgets do
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
+
describe 'GET /thing' do
|
13
|
+
it "returns the expected json hash for a non-resource endpoint" do
|
14
|
+
req :get, "/thing"
|
15
|
+
json = MultiJson.load(response.body, symbolize_keys: true)
|
16
|
+
expect(json).to eql({ result: { foo: 42 } })
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
12
20
|
describe "GET /widgets/:id/:name" do
|
13
21
|
let(:widget) { FactoryGirl.create(:widget) }
|
14
22
|
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
1
3
|
ENV["RAILS_ENV"] = "test"
|
2
4
|
|
3
5
|
require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
@@ -9,7 +11,7 @@ require 'factory_girl'
|
|
9
11
|
require 'timecop'
|
10
12
|
require 'pry'
|
11
13
|
require 'webmock/rspec'
|
12
|
-
WebMock.disable_net_connect!(allow_localhost: true)
|
14
|
+
WebMock.disable_net_connect!(allow_localhost: true, allow: "codeclimate.com")
|
13
15
|
|
14
16
|
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
|
15
17
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: grape_ape_rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt E. Patterson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -346,6 +346,20 @@ dependencies:
|
|
346
346
|
- - ">="
|
347
347
|
- !ruby/object:Gem::Version
|
348
348
|
version: '0'
|
349
|
+
- !ruby/object:Gem::Dependency
|
350
|
+
name: codeclimate-test-reporter
|
351
|
+
requirement: !ruby/object:Gem::Requirement
|
352
|
+
requirements:
|
353
|
+
- - ">="
|
354
|
+
- !ruby/object:Gem::Version
|
355
|
+
version: '0'
|
356
|
+
type: :development
|
357
|
+
prerelease: false
|
358
|
+
version_requirements: !ruby/object:Gem::Requirement
|
359
|
+
requirements:
|
360
|
+
- - ">="
|
361
|
+
- !ruby/object:Gem::Version
|
362
|
+
version: '0'
|
349
363
|
description: Provides customized Grape API functionality inside Rails
|
350
364
|
email:
|
351
365
|
- madraziel@gmail.com
|