api_presenter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4779e47afef65722b830ee6c85455f3f9853cf40
4
+ data.tar.gz: f5b677a9c48fcac592de8b9e7e631f06924e3dd5
5
+ SHA512:
6
+ metadata.gz: 5f3a9f458b103dbadea74c3844b129a53a9328af660e0bdca3215183c0ceccd92d97c9bc0a76050e1a0e6fcb13e331dcb9bc09a193d16041db20fa3b4baa181e
7
+ data.tar.gz: 91e99b5f3919c87cad9997a8d6341dd3cd613894c5f986e2a7b0feb1d47ca7a00c4f8d7bbfef3d5bf832fd3158b12f5415f4d6b4b3c59ac5e4248858862a3249
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
4
+ before_install: gem install bundler -v 1.10.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in api_presenter.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Yuval Kordov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # ApiPresenter
2
+
3
+ A much longer readme is coming, including best practices and cautions, but in the meantime lets keep it simple...
4
+
5
+ When creating RESTful APIs for web or mobile clients, there are a couple of common use cases that have emerged:
6
+
7
+ * Allow inclusion of associated data to mitigate number of requests
8
+ * Include permissions so that the client can intelligently draw its UI (ex: edit/delete buttons), while maintaining a single source of truth
9
+
10
+ ApiPresenter does both of these things, plus a bit more.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'api_presenter'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install api_presenter
27
+
28
+ ## Usage
29
+
30
+ We'll use a simple blog as the usage example for this gem. The blog has the following model structure:
31
+
32
+ ```ruby
33
+ class Category < ActiveRecord::Base
34
+ has_many :sub_categories
35
+ has_many :posts, through: :sub_categories
36
+ end
37
+
38
+ class SubCategory < ActiveRecord::Base
39
+ belongs_to :category
40
+ has_many :posts
41
+ end
42
+
43
+ class Post < ActiveRecord::Base
44
+ belongs_to :sub_category
45
+ belongs_to :creator, class_name: 'User'
46
+ belongs_to :publisher, class_name: 'User'
47
+ end
48
+
49
+ class User < ActiveRecord::Base
50
+ has_many :created_posts, class_name: 'Post', foreign_key: 'creator_id'
51
+ has_many :published_posts, class_name: 'Post', foreign_key: 'publisher_id'
52
+ end
53
+ ```
54
+
55
+ When clients request posts (the primary collection), they may want any or all of the above data for those posts.
56
+
57
+ ### Create your Presenter
58
+
59
+ ```ruby
60
+ class PostPresenter < ApiPresenter::Base
61
+ def associations_map
62
+ {
63
+ categories: { associations: { sub_category: :category } },
64
+ sub_categories: { associations: :sub_category },
65
+ users: { associations: [:creator, :publisher] }
66
+ }
67
+ end
68
+
69
+ def policy_methods
70
+ [:update, :destroy]
71
+ end
72
+
73
+ # def policy_associations
74
+ # :user_profile
75
+ # end
76
+ end
77
+ ```
78
+
79
+ Presenters can define up to three methods:
80
+
81
+ * `associations_map` The includable resources for the ActiveRecord model (Post, in this case). Consists of the model name as key and traversla required to preload/load them. In most cases, the value of `associations` will correspond directly to associations on the primary model.
82
+ * `policy_methods` A list of Pundit policy methods to resolve for the primary collection.
83
+ * `policy_associations` Additional records to preload in order to optimize policies that must traverse asscoiations.
84
+
85
+ ### Enable your controllers
86
+
87
+ ApiPresenter provides a controller concern that executes the Presenter. This process analyzes your params, preloads records as needed, and produces a `@presenter` object you can work with.
88
+
89
+ ```ruby
90
+ class ApplicationController
91
+ include ApiPresenter::Concerns::Presentable
92
+ end
93
+
94
+ class PostsCOntroller < ApplicationController
95
+ def index
96
+ posts = PostQuery.records(current_user, params)
97
+ present posts
98
+ end
99
+
100
+ def show
101
+ post = Post.find(params[:id])
102
+ present post
103
+ end
104
+ end
105
+ ```
106
+
107
+ ### Render the result
108
+
109
+ How you ultimately render the primary collection and the data produced by ApiPresenter is up to you. `@presenter` has the following properties:
110
+
111
+ * `collection` The primary collection that was passed into the presenter.
112
+ * `total_count` When using Kaminari or another pagination method that defines a `total_count` property, returns unpaginated count. If the primary collection is not an `ActiveRecord::Relation`, simply returns the number of records.
113
+ * `included_collection_names` Convenience method that returns an array of included collecton model names.
114
+ * `included_collections` A hash of included collections, consisting of the model name and corresponding records.
115
+ * `policies` An array of resolved policies for the primary collection.
116
+
117
+ Here's an example of how you might render this using JBduiler:
118
+
119
+ ### api/posts/index.json.jbuilder
120
+
121
+ ```ruby
122
+ json.posts(@presenter.collection) do |post|
123
+ json.partial!(post)
124
+ end
125
+ json.partial!("api/shared/included_collections_and_meta", presenter: @presenter)
126
+ ```
127
+
128
+ ### api/shared/included_collections_and_meta
129
+
130
+ ```ruby
131
+ presenter.included_collections.each do |collection_key, collection|
132
+ json.set!(collection_key, collection) do |record|
133
+ json.partial!(record)
134
+ end
135
+ end
136
+
137
+ json.meta do
138
+ json.total_count(presenter.total_count)
139
+ json.policies presenter.policies
140
+ end
141
+ ```
142
+
143
+ ## Advanced Usage
144
+
145
+ ### Conditional includes
146
+
147
+ There are a number of ways you can conditionally include resources, depending, for insatnce, on user type.
148
+
149
+ #### Add conditions inside `associations_map` method
150
+
151
+ ```ruby
152
+ class PostPresenter < ApiPresenter::Base
153
+ def associations_map
154
+ current_user.admin? ? admin_associations_map : user_associations_map
155
+ end
156
+
157
+ private
158
+
159
+ def user_associations_map
160
+ {
161
+ sub_categories: { associations: :sub_category },
162
+ users: { associations: [:creator, :publisher] }
163
+ }
164
+ end
165
+
166
+ def admin_associations_map
167
+ {
168
+ categories: { associations: { sub_category: :category } },
169
+ sub_categories: { associations: :sub_category },
170
+ users: { associations: [:creator, :publisher] }
171
+ }
172
+ end
173
+ end
174
+ ```
175
+
176
+ #### Use `condition` property within `association_map` definition
177
+
178
+ Via inline string:
179
+
180
+ ```ruby
181
+ class PostPresenter < ApiPresenter::Base
182
+ def associations_map
183
+ {
184
+ categories: { associations: { sub_category: :category }, condition: 'current_user.admin?' },
185
+ sub_categories: { associations: :sub_category },
186
+ users: { associations: [:creator, :publisher] }
187
+ }
188
+ end
189
+ end
190
+ ```
191
+
192
+ Via method call:
193
+
194
+ ```ruby
195
+ class PostPresenter < ApiPresenter::Base
196
+ def associations_map
197
+ {
198
+ categories: { associations: { sub_category: :category }, condition: :admin? },
199
+ sub_categories: { associations: :sub_category },
200
+ users: { associations: [:creator, :publisher] }
201
+ }
202
+ end
203
+
204
+ private
205
+
206
+ def admin?
207
+ current_user.admin?
208
+ end
209
+ end
210
+ ```
211
+
212
+ #### Control it from your policy
213
+
214
+ ```ruby
215
+ class CategoryPolicy < ApplicationPolicy
216
+ def index?
217
+ user.admin?
218
+ end
219
+ end
220
+ ```
221
+
222
+ ## TODO
223
+
224
+ * More doc
225
+ * Decouple from Pundit
226
+ * Make index policy checking on includes optional
227
+ * Allow custom collection names
228
+ * Add test helper to assert presenter was called for a given controller action
229
+
230
+ ## Development
231
+
232
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
233
+
234
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
235
+
236
+ ## Contributing
237
+
238
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/api_presenter.
239
+
240
+
241
+ ## License
242
+
243
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
244
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'api_presenter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "api_presenter"
8
+ spec.version = ApiPresenter::VERSION
9
+ spec.authors = ["Yuval Kordov", "Little Blimp"]
10
+ spec.email = ["yuval@littleblimp.com"]
11
+ spec.summary = "Return associations and policies with API responses"
12
+ spec.description = "Facilitates optimized side loading of associated resources and permission policies from RESTful endpoints"
13
+ spec.homepage = "http://github.com/uberllama/api_presenter"
14
+ spec.license = "MIT"
15
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16
+ spec.bindir = "exe"
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+ spec.add_dependency "activesupport", ">= 3.0.0"
20
+ spec.add_dependency "pundit"
21
+ spec.add_development_dependency "activerecord", ">= 4.2.3"
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "sqlite3"
26
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "api_presenter"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,3 @@
1
+ module ApiPresenter
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,204 @@
1
+ require 'pundit'
2
+ require 'api_presenter/parse_include_params'
3
+ require 'api_presenter/version'
4
+ require 'api_presenter/concerns/presentable'
5
+ require 'api_presenter/resolvers/base'
6
+ require 'api_presenter/resolvers/policies_resolver'
7
+ require 'api_presenter/resolvers/included_collections_resolver'
8
+
9
+ module ApiPresenter
10
+ class Base
11
+
12
+ attr_reader :current_user, :params, :relation
13
+
14
+ # @example
15
+ # @presenter = PostPresenter.call(
16
+ # current_user: current_user,
17
+ # relation: relation,
18
+ # params: params
19
+ # )
20
+ #
21
+ # @param (see #initialize)
22
+ #
23
+ # @return [ApiPresenter::Base]
24
+ #
25
+ def self.call(**kwargs)
26
+ new(kwargs).call
27
+ end
28
+
29
+ # @param current_user [User] Optional. current_user context.
30
+ # @param relation [ActiveRecord::Relation, Array] Relation or array-wrapped record(s) to present
31
+ # @param params [Hash] Controller params
32
+ # @option params [Boolean] :count Optional. If true, return count only.
33
+ # @option params [String, Array] :include Optional. Associated resources to include.
34
+ # @option params [Boolean] :policies Optional. If true, resolve polciies for relation.
35
+ #
36
+ def initialize(current_user: nil, relation:, params: {})
37
+ @current_user = current_user
38
+ @relation = relation
39
+ @params = params
40
+ end
41
+
42
+ # @return [ApiPresenter::Base]
43
+ #
44
+ def call
45
+ return self if count_only?
46
+ initialize_resolvers
47
+ call_resolvers
48
+ self
49
+ end
50
+
51
+ # Primary collection, empty if count requested
52
+ #
53
+ # @return [ActiveRecord::Relation, Array<ActiveRecord::Base>]
54
+ #
55
+ def collection
56
+ count_only? ? [] : relation
57
+ end
58
+
59
+ # Count of primary collection
60
+ #
61
+ # @note Delegate to Kaminari's `total_count` property, or regular count if not a paginated relation
62
+ #
63
+ # @return [Integer]
64
+ #
65
+ def total_count
66
+ relation.respond_to?(:total_count) ? relation.total_count : relation.count
67
+ end
68
+
69
+ # Policies for the primary collection
70
+ #
71
+ # @example
72
+ # [
73
+ # { post_id: 1, update: true, destroy: true },
74
+ # { post_id: 2, update: false, destroy: false }
75
+ # ]
76
+ #
77
+ # @return [<Array<Hash>]
78
+ #
79
+ def policies
80
+ @policies_resolver ? @policies_resolver.resolved_policies : {}
81
+ end
82
+
83
+ # Class names of included collections
84
+ #
85
+ # @example
86
+ # [:categories, :sub_categories, :users]
87
+ #
88
+ # @return [Array<Symbol>]
89
+ #
90
+ def included_collection_names
91
+ @included_collection_names ||= ParseIncludeParams.call(params[:include])
92
+ end
93
+
94
+ # Map of included collection names and loaded record
95
+ #
96
+ # @example
97
+ # {
98
+ # categories: [#<Category id:1>],
99
+ # sub_categories: [#<SubCategory id:1>],
100
+ # users: [#<User id:1>, #<User id:2]
101
+ # }
102
+ #
103
+ # @return [Hash]
104
+ #
105
+ def included_collections
106
+ @included_collections_resolver ? @included_collections_resolver.resolved_collections : {}
107
+ end
108
+
109
+ # Preload additional records with the relation
110
+ #
111
+ # @note Called by resolvers, but can also be called if additional data is required that does
112
+ # not need to be loaded as an included collection, and for some reason cannot be chained
113
+ # onto the original relation.
114
+ #
115
+ # @param associations [Symbol, Array<Symbol>]
116
+ #
117
+ def preload(associations)
118
+ @relation = @relation.preload(associations)
119
+ end
120
+
121
+ # Hash map that defines the sources for included collection names
122
+ #
123
+ # @example
124
+ # def associations_map
125
+ # {
126
+ # categories: { associations: { sub_category: :category } },
127
+ # sub_categories: { associations: :sub_category },
128
+ # users: { associations: [:creator, :publisher] }
129
+ # }
130
+ # end
131
+ #
132
+ # @abstract
133
+ #
134
+ # @return [Hash]
135
+ #
136
+ def associations_map
137
+ {}
138
+ end
139
+
140
+ # Policy methods to resolve for the primary relation
141
+ #
142
+ # @example Single
143
+ # def policy_methods
144
+ # :update
145
+ # end
146
+ #
147
+ # @example Multiple
148
+ # def policy_methods
149
+ # [:update, :destroy]
150
+ # end
151
+ #
152
+ # @abstract
153
+ #
154
+ # @return [Symbol, Array<Symbol>]
155
+ #
156
+ def policy_methods
157
+ []
158
+ end
159
+
160
+ # Policy associations to preload to optimize policy resolution
161
+ #
162
+ # @example Single
163
+ # def policy_associations
164
+ # :user_profile
165
+ # end
166
+ #
167
+ # @example Multiple
168
+ # def policy_associations
169
+ # [:user_profile, :company]
170
+ # end
171
+ #
172
+ # @abstract
173
+ #
174
+ # @return [Symbol, Array<Symbol>]
175
+ #
176
+ def policy_associations
177
+ []
178
+ end
179
+
180
+ private
181
+
182
+ def count_only?
183
+ @count_only ||= !!params[:count]
184
+ end
185
+
186
+ def resolve_policies?
187
+ @resolve_policies ||= current_user && !!params[:policies]
188
+ end
189
+
190
+ def resolve_included_collctions?
191
+ included_collection_names.any?
192
+ end
193
+
194
+ def initialize_resolvers
195
+ @policies_resolver = Resolvers::PoliciesResolver.new(self) if resolve_policies?
196
+ @included_collections_resolver = Resolvers::IncludedCollectionsResolver.new(self) if resolve_included_collctions?
197
+ end
198
+
199
+ def call_resolvers
200
+ @policies_resolver.call if @policies_resolver
201
+ @included_collections_resolver.call if @included_collections_resolver
202
+ end
203
+ end
204
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: api_presenter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yuval Kordov
8
+ - Little Blimp
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2016-10-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 3.0.0
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: 3.0.0
28
+ - !ruby/object:Gem::Dependency
29
+ name: pundit
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: activerecord
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 4.2.3
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 4.2.3
56
+ - !ruby/object:Gem::Dependency
57
+ name: bundler
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.10'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.10'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '10.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '10.0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rspec
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: sqlite3
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ description: Facilitates optimized side loading of associated resources and permission
113
+ policies from RESTful endpoints
114
+ email:
115
+ - yuval@littleblimp.com
116
+ executables: []
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - ".gitignore"
121
+ - ".rspec"
122
+ - ".travis.yml"
123
+ - Gemfile
124
+ - LICENSE.txt
125
+ - README.md
126
+ - Rakefile
127
+ - api_presenter.gemspec
128
+ - bin/console
129
+ - bin/setup
130
+ - lib/api_presenter.rb
131
+ - lib/api_presenter/version.rb
132
+ homepage: http://github.com/uberllama/api_presenter
133
+ licenses:
134
+ - MIT
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.4.6
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Return associations and policies with API responses
156
+ test_files: []