algol 0.1.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.
@@ -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