the_garage 2.0.0

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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +162 -0
  4. data/Rakefile +33 -0
  5. data/app/assets/javascripts/garage/application.js +16 -0
  6. data/app/assets/javascripts/garage/docs/console.js.coffee +90 -0
  7. data/app/assets/javascripts/garage/docs/jquery.colorbox.js +1026 -0
  8. data/app/assets/stylesheets/garage/application.css +14 -0
  9. data/app/assets/stylesheets/garage/colorbox.scss +62 -0
  10. data/app/assets/stylesheets/garage/style.scss +59 -0
  11. data/app/assets/stylesheets/vendor/bootstrap.min.css +9 -0
  12. data/app/controllers/garage/application_controller.rb +4 -0
  13. data/app/controllers/garage/docs/resources_controller.rb +103 -0
  14. data/app/controllers/garage/meta/docs_controller.rb +20 -0
  15. data/app/controllers/garage/meta/services_controller.rb +20 -0
  16. data/app/helpers/garage/application_helper.rb +4 -0
  17. data/app/helpers/garage/docs/resources_helper.rb +24 -0
  18. data/app/models/garage/hash_representer.rb +11 -0
  19. data/app/views/garage/docs/resources/_layout_navigation.html.haml +5 -0
  20. data/app/views/garage/docs/resources/_navigation.html.haml +6 -0
  21. data/app/views/garage/docs/resources/callback.html.haml +5 -0
  22. data/app/views/garage/docs/resources/console.html.haml +45 -0
  23. data/app/views/garage/docs/resources/index.html.haml +2 -0
  24. data/app/views/garage/docs/resources/show.html.haml +16 -0
  25. data/app/views/layouts/garage/application.html.haml +26 -0
  26. data/config/routes.rb +0 -0
  27. data/lib/garage/app_responder.rb +22 -0
  28. data/lib/garage/authorizable.rb +26 -0
  29. data/lib/garage/config.rb +76 -0
  30. data/lib/garage/controller_helper.rb +110 -0
  31. data/lib/garage/docs/anchor_building.rb +28 -0
  32. data/lib/garage/docs/application.rb +24 -0
  33. data/lib/garage/docs/config.rb +61 -0
  34. data/lib/garage/docs/console_link_building.rb +14 -0
  35. data/lib/garage/docs/document.rb +141 -0
  36. data/lib/garage/docs/engine.rb +35 -0
  37. data/lib/garage/docs/example.rb +26 -0
  38. data/lib/garage/docs/renderer.rb +17 -0
  39. data/lib/garage/docs/toc_renderer.rb +14 -0
  40. data/lib/garage/docs.rb +9 -0
  41. data/lib/garage/exceptions.rb +49 -0
  42. data/lib/garage/hypermedia_filter.rb +44 -0
  43. data/lib/garage/hypermedia_responder.rb +120 -0
  44. data/lib/garage/meta/engine.rb +16 -0
  45. data/lib/garage/meta/remote_service.rb +78 -0
  46. data/lib/garage/meta.rb +6 -0
  47. data/lib/garage/meta_resource.rb +17 -0
  48. data/lib/garage/nested_field_query.rb +183 -0
  49. data/lib/garage/optional_response_body_responder.rb +16 -0
  50. data/lib/garage/paginating_responder.rb +113 -0
  51. data/lib/garage/permission.rb +13 -0
  52. data/lib/garage/permissions.rb +75 -0
  53. data/lib/garage/representer.rb +214 -0
  54. data/lib/garage/resource_casting_responder.rb +13 -0
  55. data/lib/garage/restful_actions.rb +219 -0
  56. data/lib/garage/strategy/access_token.rb +57 -0
  57. data/lib/garage/strategy/auth_server.rb +200 -0
  58. data/lib/garage/strategy/no_authentication.rb +13 -0
  59. data/lib/garage/strategy/test.rb +44 -0
  60. data/lib/garage/strategy.rb +4 -0
  61. data/lib/garage/test/migrator.rb +31 -0
  62. data/lib/garage/token_scope.rb +134 -0
  63. data/lib/garage/utils.rb +28 -0
  64. data/lib/garage/version.rb +3 -0
  65. data/lib/garage.rb +23 -0
  66. metadata +275 -0
@@ -0,0 +1,183 @@
1
+ module Garage
2
+ module NestedFieldQuery
3
+ class InvalidQuery < StandardError; end
4
+ class InvalidData < StandardError; end
5
+
6
+ class Parser
7
+ def self.parse(*args)
8
+ new.parse(*args)
9
+ end
10
+
11
+ def parse(given_query, indent = nil)
12
+ parse_recurse(given_query.to_s.dup, indent)
13
+ end
14
+
15
+ private
16
+
17
+ def parse_recurse(query, indent)
18
+ result = []
19
+ current = nil
20
+ while query.sub!(/^(?:\s*([\w\.\*]+)|(,)|(\[)|(\]))/, '')
21
+ if $1
22
+ current = $1.to_s
23
+ elsif $2
24
+ if current
25
+ result << current
26
+ current = nil
27
+ else
28
+ raise InvalidQuery, "Expected field name: #{query}"
29
+ end
30
+ elsif $3
31
+ if current
32
+ current = { current => parse_recurse(query, 1) }
33
+ else
34
+ raise InvalidQuery, "'[' should come after field: #{query}"
35
+ end
36
+ elsif $4
37
+ if indent
38
+ if current
39
+ result << current
40
+ return merge(result)
41
+ else
42
+ raise InvalidQuery, "']' should be after '[field': #{query}"
43
+ end
44
+ else
45
+ raise InvalidQuery, "']' should be after '[field': #{query}"
46
+ end
47
+ end
48
+ end
49
+
50
+ if current
51
+ result << current
52
+ else
53
+ raise InvalidQuery, "premature end of query"
54
+ end
55
+
56
+ merge(result)
57
+ end
58
+
59
+ def merge(result)
60
+ hash = Hash.new
61
+ result.each do |res|
62
+ if res.is_a?(Hash)
63
+ hash.merge!(res)
64
+ else
65
+ hash[res] = nil
66
+ end
67
+ end
68
+ hash
69
+ end
70
+ end
71
+
72
+ class Builder
73
+ def self.build(*args)
74
+ new.build(*args)
75
+ end
76
+
77
+ def build(arg)
78
+ val = ''
79
+
80
+ case arg
81
+ when Hash
82
+ val << arg.map { |key, value|
83
+ if value.nil?
84
+ key
85
+ else
86
+ "#{key}[#{build(value)}]"
87
+ end
88
+ }.join(',')
89
+ when Symbol, String
90
+ val << arg.to_s
91
+ else
92
+ raise InvalidData, "Can't encode data type: #{arg.class}"
93
+ end
94
+
95
+ val
96
+ end
97
+ end
98
+
99
+ class DefaultSelector
100
+ # kinda NullObject pattern
101
+
102
+ # Doesn't specify anything - includes/excludes returns both false :)
103
+
104
+ def includes?(field)
105
+ false
106
+ end
107
+
108
+ def excludes?(field)
109
+ false
110
+ end
111
+
112
+ def [](name)
113
+ DefaultSelector.new
114
+ end
115
+
116
+ def canonical
117
+ ''
118
+ end
119
+ end
120
+
121
+ class FullSelector < DefaultSelector
122
+ def includes?(field)
123
+ true
124
+ end
125
+
126
+ def excludes?(field)
127
+ false
128
+ end
129
+
130
+ def [](name)
131
+ FullSelector.new
132
+ end
133
+
134
+ def canonical
135
+ '*'
136
+ end
137
+ end
138
+
139
+ class Selector
140
+ # includes eager loading
141
+
142
+ def self.build(fields)
143
+ if fields.present?
144
+ build_parsed(Parser.parse(fields))
145
+ else
146
+ NestedFieldQuery::DefaultSelector.new
147
+ end
148
+ end
149
+
150
+ def self.build_parsed(fields)
151
+ if fields.key? '*'
152
+ FullSelector.new
153
+ else
154
+ self.new(fields)
155
+ end
156
+ end
157
+
158
+ def initialize(fields = {})
159
+ @fields = fields
160
+ end
161
+
162
+ def [](name)
163
+ if @fields[name].nil?
164
+ DefaultSelector.new
165
+ else
166
+ Selector.build_parsed(@fields[name])
167
+ end
168
+ end
169
+
170
+ def canonical
171
+ Builder.build(@fields)
172
+ end
173
+
174
+ def includes?(field)
175
+ @fields.has_key?(field)
176
+ end
177
+
178
+ def excludes?(field)
179
+ !@fields.has_key?('__default__') && !@fields.has_key?(field)
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,16 @@
1
+ module Garage::OptionalResponseBodyResponder
2
+ protected
3
+
4
+ def api_behavior(*)
5
+ case
6
+ when put? && options[:put] && options[:put][:body]
7
+ display resource, status: options[:put][:status] || :ok
8
+ when patch? && options[:patch] && options[:patch][:body]
9
+ display resource, status: options[:patch][:status] || :ok
10
+ when delete? && options[:delete] && options[:delete][:body]
11
+ display resource, status: options[:delete][:status] || :ok
12
+ else
13
+ super
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,113 @@
1
+ module Garage
2
+ module PaginatingResponder
3
+ def display(resource, *args)
4
+ if @options[:paginate]
5
+ resource = paginate resource
6
+ end
7
+ super(resource, *args)
8
+ end
9
+
10
+ def max_per_page=(count)
11
+ @max_per_page = count
12
+ end
13
+
14
+ def reveal_total!
15
+ @options.delete(:hard_limit)
16
+ end
17
+
18
+ private
19
+
20
+ def distinct?
21
+ !!@options[:distinct_by]
22
+ end
23
+
24
+ def hide_total?
25
+ !!@options[:hard_limit]
26
+ end
27
+
28
+ def hard_limit
29
+ @options[:hard_limit]
30
+ end
31
+
32
+ def max_per_page
33
+ @options[:max_per_page] || @max_per_page || 100
34
+ end
35
+
36
+ def set_total_count(rs, per_page)
37
+ if hard_limit
38
+ limit = hard_limit
39
+ rs.instance_variable_set(:@total_count, limit)
40
+ end
41
+ end
42
+
43
+ def total_count(rs)
44
+ if distinct?
45
+ rs.total_count(@options[:distinct_by], distinct: true)
46
+ else
47
+ rs.total_count
48
+ end
49
+ end
50
+
51
+ def paginate(rs)
52
+ @options[:hard_limit] ||= 1000 if @options[:hide_total] # backward compat for hide_total
53
+
54
+ per_page = [ max_per_page, (controller.params[:per_page] || @options[:per_page] || 20).to_i ].min
55
+
56
+ rs = rs.page(controller.params[:page] || 1).per(per_page)
57
+
58
+ set_total_count(rs, per_page)
59
+
60
+ unless hide_total?
61
+ controller.response.headers['X-List-TotalCount'] = total_count(rs).to_s
62
+ end
63
+
64
+ # FIXME construct_links must be called after calling rs.total_count to avoid invalid count cache
65
+ construct_links(rs, per_page)
66
+
67
+ if hide_total?
68
+ if rs.offset_value > hard_limit
69
+ rs = []
70
+ elsif rs.offset_value + per_page > hard_limit
71
+ rs = rs.slice 0, (hard_limit - rs.offset_value) # becomes Array here, and hope it's ok
72
+ end
73
+ end
74
+
75
+ rs
76
+ end
77
+
78
+ def construct_links(rs, per_page)
79
+ build_link_hash(rs, links={})
80
+ add_link_header(links, per_page) unless links.empty?
81
+ end
82
+
83
+ def build_link_hash(rs, links)
84
+ unless rs.first_page?
85
+ links[:first] = 1
86
+ links[:prev] = rs.current_page - 1
87
+ end
88
+
89
+ if rs.current_page < rs.total_pages
90
+ links[:next] = rs.current_page + 1
91
+ end
92
+
93
+ unless rs.last_page? || hide_total?
94
+ links[:last] = rs.total_pages
95
+ end
96
+ end
97
+
98
+ def build_path_for(params)
99
+ parameters = controller.request.query_parameters.merge(params).tap {|p| p.delete(:access_token) }
100
+ "#{controller.request.path}?#{parameters.to_query}"
101
+ end
102
+
103
+ def add_link_header(links, per_page)
104
+ headers = []
105
+ links.each do |rel, page|
106
+ url = build_path_for(:page => page, :per_page => per_page)
107
+ headers << "<#{url}>; rel=\"#{rel}\"; page=\"#{page}\""
108
+ end
109
+ controller.response.headers['Link'] = headers.join ', '
110
+ end
111
+ end
112
+ end
113
+
@@ -0,0 +1,13 @@
1
+ module Garage
2
+ class Permission
3
+ attr_accessor :user, :action
4
+
5
+ def initialize(*args)
6
+ @user, @action, @perm = *args
7
+ end
8
+
9
+ def allowed?
10
+ @perm == :ok
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,75 @@
1
+ # Public: represents permissions of the current request user against
2
+ # the resource and resource class.
3
+ #
4
+ # Examples
5
+ #
6
+ # class Post
7
+ # include Garage::Authorizable
8
+ #
9
+ # def build_permissions(perms, other)
10
+ # perms.permits! :read
11
+ # perms.permits! :write if owner == other
12
+ # end
13
+ #
14
+ # def self.build_permissions(perms, other, target)
15
+ # if target[:user]
16
+ # perms.permits! :read, :write if target[:user] == other
17
+ # else
18
+ # perms.permits! :read, :write
19
+ # end
20
+ # end
21
+ # end
22
+ require "garage/permission"
23
+
24
+ module Garage
25
+ class Permissions
26
+ attr_accessor :user, :resource_class
27
+
28
+ def initialize(user, resource_class, permissions = { read: :forbidden, write: :forbidden })
29
+ @user = user
30
+ @resource_class = resource_class
31
+ @perms = permissions
32
+ end
33
+
34
+ def authorize!(action)
35
+ exists? or raise PermissionError.new(user, action, resource_class, :not_found)
36
+ permits?(action) or raise PermissionError.new(user, action, resource_class, :forbidden)
37
+ end
38
+
39
+ def for(action)
40
+ Permission.new(@user, action, @perms[action])
41
+ end
42
+
43
+ def deleted!
44
+ @perms[:deleted] = true
45
+ end
46
+
47
+ def exists?
48
+ !@perms[:deleted]
49
+ end
50
+
51
+ def permits!(*actions)
52
+ actions.each do |action|
53
+ @perms[action] = :ok
54
+ end
55
+ end
56
+
57
+ def forbids!(*actions)
58
+ actions.each do |action|
59
+ @perms[action] = :forbidden
60
+ end
61
+ end
62
+
63
+ def permits?(action)
64
+ self.for(action).allowed?
65
+ end
66
+
67
+ def readable?
68
+ permits? :read
69
+ end
70
+
71
+ def writable?
72
+ permits? :write
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,214 @@
1
+ module Garage::Representer
2
+ attr_accessor :params, :representer_attrs, :partial, :selector
3
+
4
+ def partial?
5
+ @partial
6
+ end
7
+
8
+ def render_hash(options={})
9
+ obj = {}
10
+ representer_attrs.each do |definition|
11
+ if definition.options[:if]
12
+ next unless definition.options[:if].call(self, options[:responder])
13
+ end
14
+
15
+ if definition.respond_to?(:encode)
16
+ next unless handle_definition?(selector, definition, options)
17
+ obj[definition.name] = definition.encode(self, options[:responder], selector[definition.name])
18
+ else
19
+ next if selector.excludes?('_links')
20
+ obj['_links'] ||= {}
21
+ obj['_links'][definition.rel.to_s] = { 'href' => definition.pathify(self) }
22
+ end
23
+ end
24
+ obj
25
+ end
26
+
27
+ def handle_definition?(selector, definition, options)
28
+ if definition.requires_select?
29
+ # definition is not selected by default - it's opt-in
30
+ selector.includes?(definition.name) && definition.selectable?(self, options[:responder])
31
+ else
32
+ # definition is selected by default - it's opt-out
33
+ !selector.excludes?(definition.name)
34
+ end
35
+ end
36
+
37
+ def default_url_options
38
+ @default_url_options ||= {}
39
+ end
40
+
41
+ def represent!
42
+ self.representer_attrs ||= []
43
+ self.representer_attrs += self.class.representer_attrs
44
+ end
45
+
46
+ def self.representers
47
+ @representers ||= []
48
+ end
49
+
50
+ def resource_class
51
+ self.class
52
+ end
53
+
54
+ def to_resource
55
+ self
56
+ end
57
+
58
+ def link_path_for(rel)
59
+ represent! unless representer_attrs
60
+ representer_attrs.grep(Link).find { |link| link.rel === rel }.try(:pathify, self)
61
+ end
62
+
63
+ def self.included(base)
64
+ self.representers << base
65
+
66
+ base.class_eval do
67
+ if Rails.application
68
+ include Rails.application.routes.url_helpers
69
+ end
70
+ extend ClassMethods
71
+ end
72
+ end
73
+
74
+ module ClassMethods
75
+ attr_writer :representer_attrs
76
+
77
+ def inherited(child)
78
+ super
79
+ child.representer_attrs = self.representer_attrs.clone
80
+ end
81
+
82
+ def representer_attrs
83
+ @representer_attrs ||= []
84
+ end
85
+
86
+ def property(name, options={})
87
+ representer_attrs << Definition.new(name, options)
88
+ end
89
+
90
+ def link(rel, options={}, &block)
91
+ representer_attrs << Link.new(rel, options, block)
92
+ end
93
+
94
+ def collection(name, options={})
95
+ representer_attrs << Collection.new(name, options)
96
+ end
97
+
98
+ def oauth_scope(scope)
99
+ ->(resource, responder){
100
+ # FIXME: this only works with User resource for now
101
+ # partial representation will not render request scope-specific fields for better caching
102
+ !resource.partial? && responder.controller.requested_by?(resource) && responder.controller.has_scope?(scope)
103
+ }
104
+ end
105
+
106
+ def accessible(*args)
107
+ ->(resource, responder){
108
+ responder.controller.allow_access?(*args)
109
+ }
110
+ end
111
+
112
+ # represents the representer's schema in JSON format
113
+ def metadata
114
+ {:definitions => representer_attrs.grep(Definition).map {|definition| definition.name},
115
+ :links => representer_attrs.grep(Link).map {|link| link.options[:as] ? {link.rel => {'as' => link.options[:as]}} : link.rel}
116
+ }
117
+ end
118
+
119
+ def param(*keys)
120
+ keys.each {|key| params << key }
121
+ end
122
+
123
+ def params
124
+ @params ||= []
125
+ end
126
+ end
127
+
128
+ class NonEncodableValue < StandardError; end
129
+
130
+ class Definition
131
+ attr_reader :options
132
+
133
+ def initialize(name, options={})
134
+ @name = name
135
+ @options = options
136
+ end
137
+
138
+ def requires_select?
139
+ @options[:selectable]
140
+ end
141
+
142
+ def selectable?(*args)
143
+ if boolean?(@options[:selectable])
144
+ @options[:selectable]
145
+ else
146
+ @options[:selectable].call(*args)
147
+ end
148
+ end
149
+
150
+ def name
151
+ (@options[:as] || @name).to_s
152
+ end
153
+
154
+ def encode(object, responder, selector = nil)
155
+ value = object.send(@name)
156
+ encode_value(value, responder, selector)
157
+ end
158
+
159
+ def encode_value(value, responder, selector)
160
+ if !value.nil? && value.respond_to?(:represent!)
161
+ responder.encode_to_hash(value, partial: true, selector: selector)
162
+ elsif primitive?(value)
163
+ value
164
+ else
165
+ raise NonEncodableValue, "#{value.class} can not be encoded directly. Forgot to include Garage::Representer?"
166
+ end
167
+ end
168
+
169
+ def primitive?(value)
170
+ [
171
+ ActiveSupport::TimeWithZone,
172
+ Date,
173
+ Time,
174
+ Bignum,
175
+ Fixnum,
176
+ Float,
177
+ Hash,
178
+ Array,
179
+ String,
180
+ NilClass,
181
+ TrueClass,
182
+ FalseClass,
183
+ Symbol,
184
+ ].any? {|k| value.is_a?(k) }
185
+ end
186
+
187
+ private
188
+
189
+ def boolean?(value)
190
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
191
+ end
192
+ end
193
+
194
+ class Collection < Definition
195
+ def encode(object, responder, selector = nil)
196
+ value = object.send(@name)
197
+ value.map do |item|
198
+ encode_value(item, responder, selector)
199
+ end
200
+ end
201
+ end
202
+
203
+ class Link
204
+ attr_reader :rel, :options, :block
205
+
206
+ def initialize(rel, options, block)
207
+ @rel, @options, @block = rel, options, block
208
+ end
209
+
210
+ def pathify(representer)
211
+ representer.instance_exec(&@block)
212
+ end
213
+ end
214
+ end