algol 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ require 'algol/version'
@@ -0,0 +1,235 @@
1
+ module Sinatra
2
+
3
+ # TODO: accept nested parameters
4
+ module API
5
+ module Helpers
6
+ def api_call?
7
+ (request.accept || '').to_s.include?('json')
8
+ end
9
+
10
+ # Define the required API arguments map. Any item defined
11
+ # not found in the supplied parameters of the API call will
12
+ # result in a 400 RC with a proper message marking the missing
13
+ # field.
14
+ #
15
+ # The map is a Hash of parameter keys and optional validator blocks.
16
+ #
17
+ # @example A map of required API call arguments
18
+ # api_required!({ title: nil, user_id: nil })
19
+ #
20
+ # Each entry can be optionally mapped to a validation proc that will
21
+ # be invoked *if* the field was supplied. The proc will be passed
22
+ # the value of the field.
23
+ #
24
+ # If the value is invalid and you need to suspend the request, you
25
+ # must return a String object with an appropriate error message.
26
+ #
27
+ # @example Rejecting a title if it's rude
28
+ # api_required!({
29
+ # :title => lambda { |t| return "Don't be rude" if t && t =~ /rude/ }
30
+ # })
31
+ #
32
+ # @note
33
+ # The supplied value passed to validation blocks is not pre-processed,
34
+ # so you must make sure that you check for nils or bad values in validator blocks!
35
+ def api_required!(args, h = params)
36
+ args.each_pair { |name, cnd|
37
+ if cnd.is_a?(Hash)
38
+ api_required!(cnd, h[name])
39
+ next
40
+ end
41
+
42
+ parse_api_argument(h, name, cnd, :required)
43
+ }
44
+ end
45
+
46
+ # Same as #api_required! except that fields defined in this map
47
+ # are optional and will be used only if they're supplied.
48
+ #
49
+ # @see #api_required!
50
+ def api_optional!(args, h = params)
51
+ args.each_pair { |name, cnd|
52
+ if cnd.is_a?(Hash)
53
+ api_optional!(cnd, h[name])
54
+ next
55
+ end
56
+
57
+ parse_api_argument(h, name, cnd, :optional)
58
+ }
59
+ end
60
+
61
+ # Consumes supplied parameters with the given keys from the API
62
+ # parameter map, and yields the consumed values for processing by
63
+ # the supplied block (if any).
64
+ #
65
+ # This is useful if:
66
+ # 1. a certain parameter does not correspond to a model attribute
67
+ # and needs to be renamed, or is used in a validation context
68
+ # 2. the data needs special treatment
69
+ # 3. the data needs to be (re)formatted
70
+ #
71
+ def api_consume!(keys)
72
+ out = nil
73
+
74
+ keys = [ keys ] unless keys.is_a?(Array)
75
+ keys.each do |k|
76
+ if val = @api[:required].delete(k.to_sym)
77
+ out = val
78
+ out = yield(val) if block_given?
79
+ end
80
+
81
+ if val = @api[:optional].delete(k.to_sym)
82
+ out = val
83
+ out = yield(val) if block_given?
84
+ end
85
+ end
86
+
87
+ out
88
+ end
89
+
90
+ def api_transform!(key, &handler)
91
+ if val = @api[:required][key.to_sym]
92
+ @api[:required][key.to_sym] = yield(val) if block_given?
93
+ end
94
+
95
+ if val = @api[:optional][key.to_sym]
96
+ @api[:optional][key.to_sym] = yield(val) if block_given?
97
+ end
98
+ end
99
+
100
+ def api_has_param?(key)
101
+ @api[:optional].has_key?(key)
102
+ end
103
+
104
+ def api_param(key)
105
+ @api[:optional][key.to_sym] || @api[:required][key.to_sym]
106
+ end
107
+
108
+ # Returns a Hash of the *supplied* request parameters. Rejects
109
+ # any parameter that was not defined in the REQUIRED or OPTIONAL
110
+ # maps (or was consumed).
111
+ #
112
+ # @param Hash q A Hash of attributes to merge with the parameters,
113
+ # useful for defining defaults
114
+ def api_params(q = {})
115
+ @api[:optional].deep_merge(@api[:required]).deep_merge(q)
116
+ end
117
+
118
+ def api_clear!()
119
+ @api = { required: {}, optional: {} }
120
+ end
121
+
122
+ alias_method :api_reset!, :api_clear!
123
+
124
+ # Attempt to locate a resource based on an ID supplied in a request parameter.
125
+ #
126
+ # If the param map contains a resource id (ie, :folder_id),
127
+ # we attempt to locate and expose it to the route.
128
+ #
129
+ # A 404 is raised if:
130
+ # 1. the scope is missing (@space for folder, @space or @folder for page)
131
+ # 2. the resource couldn't be identified in its scope (@space or @folder)
132
+ #
133
+ # If the resources were located, they're accessible using @folder or @page.
134
+ #
135
+ # The route can be halted using the :requires => [] condition when it expects
136
+ # a resource.
137
+ #
138
+ # @example using :requires to reject a request with an invalid @page
139
+ # get '/folders/:folder_id/pages/:page_id', :requires => [ :page ] do
140
+ # @page.show # page is good
141
+ # @folder.show # so is its folder
142
+ # end
143
+ #
144
+ def __api_locate_resource(r, container = nil)
145
+
146
+ resource_id = params[r + '_id'].to_i
147
+ rklass = r.capitalize
148
+
149
+ collection = case
150
+ when container.nil?; eval "#{rklass}"
151
+ else; container.send("#{r.to_plural}")
152
+ end
153
+
154
+ # puts "locating resource #{r} with id #{resource_id} from #{collection} [#{container}]"
155
+
156
+ resource = collection.get(resource_id)
157
+
158
+ if !resource
159
+ m = "No such resource: #{rklass}##{resource_id}"
160
+ if container
161
+ m << " in #{container.class.name.to_s}##{container.id}"
162
+ end
163
+
164
+ halt 404, m
165
+ end
166
+
167
+ unless can? :access, resource
168
+ halt 403, "You do not have access to this #{rklass} resource."
169
+ end
170
+
171
+ instance_variable_set('@'+r, resource)
172
+
173
+ resource
174
+ end
175
+
176
+ private
177
+
178
+ def parse_api_argument(h = params, name, cnd, type)
179
+ cnd ||= lambda { |*_| true }
180
+ name = name.to_s
181
+
182
+ unless [:required, :optional].include?(type)
183
+ raise ArgumentError, 'API Argument type must be either :required or :optional'
184
+ end
185
+
186
+ if !h.has_key?(name)
187
+ if type == :required
188
+ halt 400, "Missing required parameter :#{name}"
189
+ end
190
+ else
191
+ if cnd.respond_to?(:call)
192
+ errmsg = cnd.call(h[name])
193
+ halt 400, { :"#{name}" => errmsg } if errmsg && errmsg.is_a?(String)
194
+ end
195
+
196
+ @api[type][name.to_sym] = h[name]
197
+ end
198
+ end
199
+ end
200
+
201
+ def self.registered(app)
202
+ app.helpers Helpers
203
+ app.before do
204
+ @api = { required: {}, optional: {} }
205
+ @parent_resource = nil
206
+
207
+ if api_call?
208
+ request.body.rewind
209
+ body = request.body.read.to_s || ''
210
+ unless body.empty?
211
+ begin;
212
+ params.merge!(::JSON.parse(body))
213
+ # puts params.inspect
214
+ # puts request.path
215
+ rescue ::JSON::ParserError => e
216
+ puts e.message
217
+ puts e.backtrace
218
+ halt 400, "Malformed JSON content"
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ app.set(:requires) do |*resources|
225
+ condition do
226
+ @required = resources.collect { |r| r.to_s }
227
+ @required.each { |r| @parent_resource = __api_locate_resource(r, @parent_resource) }
228
+ end
229
+ end
230
+
231
+ end
232
+ end
233
+
234
+ register API
235
+ end
@@ -0,0 +1,3 @@
1
+ module Algol
2
+ VERSION = '0.1.0'
3
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: algol
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ahmad Amireh
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-07-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sinatra
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.4.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.4.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description:
47
+ email: ahmad@algollabs.com
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - lib/algol.rb
53
+ - lib/algol/version.rb
54
+ - lib/algol/sinatra/api_helpers.rb
55
+ homepage: https://github.com/amireh/algol.rb
56
+ licenses:
57
+ - MIT
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 1.8.23
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: Logic bundle.
80
+ test_files: []
81
+ has_rdoc: yard