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 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