grape_ape_rails 0.5.1 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 20211d0d61dba9ad806f88f8ea9e787d6ae9ac50
4
- data.tar.gz: fb3ad6d3d737ce6dcf2881c6d6979e627dc79840
3
+ metadata.gz: e824e2537351f93db1a3ed5cfdce4ec40b81d0f2
4
+ data.tar.gz: 1a3e327e2214920119b74e73860de459de9bdc87
5
5
  SHA512:
6
- metadata.gz: f61b17889217ccd1ae707af14b481039c49638d84d496a3669c9a4125be178a9571e87410203ca0129a394fb87dced1cd81e6b883d46590c6e64ce297dd0261b
7
- data.tar.gz: dab69feaac7a11ab08e4b3a65716bebc93bec60f60727317866fbc4e612a8e5cd76b1143ff96fc2815752a67784f30c5cfbc3b87f49c4934ec9695d5af004f4d
6
+ metadata.gz: cb81cbfa2a74492f1a6dd920c5465ff551a86f2af6d6341fdc770b80d1cdef3ea045320f0f0f6728b7c2ee261dbe7835d07d3ffb12cc5e1aa604a11a35a9e568
7
+ data.tar.gz: 670bcf477439e38f345e02b574136822087a039d0a2d8e7cbbd3d6e0de3c5969a78a7e4f925975e6434cb59c8e2e2bbf392ea4748ad3e5b2cbd7436fa4b5bc6e
data/README.md CHANGED
@@ -1,6 +1,32 @@
1
1
  # GrapeApeRails
2
2
 
3
- TODO: Write a gem description
3
+ [![Gem Version](https://badge.fury.io/rb/grape_ape_rails.svg)](http://badge.fury.io/rb/grape_ape_rails)
4
+ [![Code Climate](https://codeclimate.com/github/mepatterson/grape_ape_rails/badges/gpa.svg)](https://codeclimate.com/github/mepatterson/grape_ape_rails)
5
+ [![Test Coverage](https://codeclimate.com/github/mepatterson/grape_ape_rails/badges/coverage.svg)](https://codeclimate.com/github/mepatterson/grape_ape_rails)
6
+ [![Build Status](https://semaphoreapp.com/api/v1/projects/dbb9cbd7-0767-4215-b3f8-faa25510b708/231133/shields_badge.png)](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
- TODO: Write usage instructions here
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 ( http://github.com/<my-github-username>/grape_ape_rails/fork )
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`)
@@ -43,4 +43,5 @@ Gem::Specification.new do |spec|
43
43
  spec.add_development_dependency 'timecop'
44
44
  spec.add_development_dependency 'pry'
45
45
  spec.add_development_dependency 'webmock'
46
+ spec.add_development_dependency 'codeclimate-test-reporter'
46
47
  end
@@ -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 = GrapeApeRails.configuration.api_secret_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
- if args[0].present? && args[0].is_a?(Array)
50
- GrapeApeRails::API.api_version_cascades_map.merge!({ api_version => args[0].map{ |v| v.underscore.gsub('_','.') } })
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
- plural = serializer.try(:resource_plural) || serializer.object.class.name.underscore.pluralize
39
- hash = serializer.as_json
40
- output = if hash[single].present?
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
@@ -1,3 +1,3 @@
1
1
  module GrapeApeRails
2
- VERSION = "0.5.1"
2
+ VERSION = "0.9.1"
3
3
  end
@@ -3,6 +3,10 @@ module API
3
3
  class Widgets < GrapeApeRails::API
4
4
  include GrapeApeRails::Handlers::All
5
5
 
6
+ get "/thing" do
7
+ { foo: 42 }
8
+ end
9
+
6
10
  resource :widgets do
7
11
  desc "Get a single Widget"
8
12
  get ':id', rabl: 'v1/widget' do
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.5.1
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: 2014-08-16 00:00:00.000000000 Z
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