zeiv-declarative_authorization 1.0.0.pre

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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +189 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +632 -0
  5. data/Rakefile +53 -0
  6. data/app/controllers/authorization_rules_controller.rb +258 -0
  7. data/app/controllers/authorization_usages_controller.rb +22 -0
  8. data/app/helpers/authorization_rules_helper.rb +218 -0
  9. data/app/views/authorization_rules/_change.erb +58 -0
  10. data/app/views/authorization_rules/_show_graph.erb +44 -0
  11. data/app/views/authorization_rules/_suggestions.erb +48 -0
  12. data/app/views/authorization_rules/change.html.erb +169 -0
  13. data/app/views/authorization_rules/graph.dot.erb +68 -0
  14. data/app/views/authorization_rules/graph.html.erb +47 -0
  15. data/app/views/authorization_rules/index.html.erb +17 -0
  16. data/app/views/authorization_usages/index.html.erb +36 -0
  17. data/authorization_rules.dist.rb +20 -0
  18. data/config/routes.rb +20 -0
  19. data/garlic_example.rb +20 -0
  20. data/init.rb +5 -0
  21. data/lib/declarative_authorization.rb +19 -0
  22. data/lib/declarative_authorization/adapters/active_record.rb +13 -0
  23. data/lib/declarative_authorization/adapters/active_record/base_extensions.rb +0 -0
  24. data/lib/declarative_authorization/adapters/active_record/obligation_scope_builder.rb +0 -0
  25. data/lib/declarative_authorization/authorization.rb +798 -0
  26. data/lib/declarative_authorization/development_support/analyzer.rb +261 -0
  27. data/lib/declarative_authorization/development_support/change_analyzer.rb +253 -0
  28. data/lib/declarative_authorization/development_support/change_supporter.rb +620 -0
  29. data/lib/declarative_authorization/development_support/development_support.rb +243 -0
  30. data/lib/declarative_authorization/helper.rb +68 -0
  31. data/lib/declarative_authorization/in_controller.rb +703 -0
  32. data/lib/declarative_authorization/in_model.rb +188 -0
  33. data/lib/declarative_authorization/maintenance.rb +210 -0
  34. data/lib/declarative_authorization/obligation_scope.rb +361 -0
  35. data/lib/declarative_authorization/rails_legacy.rb +22 -0
  36. data/lib/declarative_authorization/railsengine.rb +6 -0
  37. data/lib/declarative_authorization/reader.rb +546 -0
  38. data/lib/generators/authorization/install/install_generator.rb +77 -0
  39. data/lib/generators/authorization/rules/rules_generator.rb +14 -0
  40. data/lib/generators/authorization/rules/templates/authorization_rules.rb +27 -0
  41. data/lib/tasks/authorization_tasks.rake +89 -0
  42. data/test/authorization_test.rb +1124 -0
  43. data/test/controller_filter_resource_access_test.rb +575 -0
  44. data/test/controller_test.rb +480 -0
  45. data/test/database.yml +3 -0
  46. data/test/dsl_reader_test.rb +178 -0
  47. data/test/helper_test.rb +247 -0
  48. data/test/maintenance_test.rb +46 -0
  49. data/test/model_test.rb +2008 -0
  50. data/test/schema.sql +56 -0
  51. data/test/test_helper.rb +255 -0
  52. metadata +95 -0
@@ -0,0 +1,22 @@
1
+ # Groups methods and additions that were used but are not readily available in
2
+ # all supported Rails versions.
3
+ class Hash
4
+ unless {}.respond_to?(:deep_merge)
5
+ # Imported from Rails 2.2
6
+ def deep_merge(other_hash)
7
+ self.merge(other_hash) do |key, oldval, newval|
8
+ oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
9
+ newval = newval.to_hash if newval.respond_to?(:to_hash)
10
+ oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ class String
17
+ unless "".respond_to?(:html_safe)
18
+ def html_safe
19
+ self
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ require 'rails'
2
+
3
+ module Authorization
4
+ class RailsEngine < Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,546 @@
1
+ # Authorization::Reader
2
+
3
+ require File.dirname(__FILE__) + '/authorization.rb'
4
+
5
+ module Authorization
6
+ # Parses an authorization configuration file in the authorization DSL and
7
+ # constructs a data model of its contents.
8
+ #
9
+ # For examples and the modeled data model, see the
10
+ # README[link:files/README_rdoc.html].
11
+ #
12
+ # Also, see role definition methods
13
+ # * AuthorizationRulesReader#role,
14
+ # * AuthorizationRulesReader#includes,
15
+ # * AuthorizationRulesReader#title,
16
+ # * AuthorizationRulesReader#description
17
+ #
18
+ # Methods for rule definition in roles
19
+ # * AuthorizationRulesReader#has_permission_on,
20
+ # * AuthorizationRulesReader#to,
21
+ # * AuthorizationRulesReader#if_attribute,
22
+ # * AuthorizationRulesReader#if_permitted_to
23
+ #
24
+ # Methods to be used in if_attribute statements
25
+ # * AuthorizationRulesReader#contains,
26
+ # * AuthorizationRulesReader#does_not_contain,
27
+ # * AuthorizationRulesReader#intersects_with,
28
+ # * AuthorizationRulesReader#is,
29
+ # * AuthorizationRulesReader#is_not,
30
+ # * AuthorizationRulesReader#is_in,
31
+ # * AuthorizationRulesReader#is_not_in,
32
+ # * AuthorizationRulesReader#lt,
33
+ # * AuthorizationRulesReader#lte,
34
+ # * AuthorizationRulesReader#gt,
35
+ # * AuthorizationRulesReader#gte
36
+ #
37
+ # And privilege definition methods
38
+ # * PrivilegesReader#privilege,
39
+ # * PrivilegesReader#includes
40
+ #
41
+ module Reader
42
+ # Signals that the specified file to load was not found.
43
+ class DSLFileNotFoundError < Exception; end
44
+ # Signals errors that occur while reading and parsing an authorization DSL
45
+ class DSLError < Exception; end
46
+ # Signals errors in the syntax of an authorization DSL.
47
+ class DSLSyntaxError < DSLError; end
48
+
49
+ # Top-level reader, parses the methods +privileges+ and +authorization+.
50
+ # +authorization+ takes a block with authorization rules as described in
51
+ # AuthorizationRulesReader. The block to +privileges+ defines privilege
52
+ # hierarchies, as described in PrivilegesReader.
53
+ #
54
+ class DSLReader
55
+ attr_reader :privileges_reader, :auth_rules_reader # :nodoc:
56
+
57
+ def initialize ()
58
+ @privileges_reader = PrivilegesReader.new
59
+ @auth_rules_reader = AuthorizationRulesReader.new
60
+ end
61
+
62
+ def initialize_copy (from) # :nodoc:
63
+ @privileges_reader = from.privileges_reader.clone
64
+ @auth_rules_reader = from.auth_rules_reader.clone
65
+ end
66
+
67
+ # ensures you get back a DSLReader
68
+ # if you provide a:
69
+ # DSLReader - you will get it back.
70
+ # String or Array - it will treat it as if you have passed a path or an array of paths and attempt to load those.
71
+ def self.factory(obj)
72
+ case obj
73
+ when Reader::DSLReader
74
+ obj
75
+ when String, Array
76
+ load(obj)
77
+ end
78
+ end
79
+
80
+ # Parses a authorization DSL specification from the string given
81
+ # in +dsl_data+. Raises DSLSyntaxError if errors occur on parsing.
82
+ def parse (dsl_data, file_name = nil)
83
+ if file_name
84
+ DSLMethods.new(self).instance_eval(dsl_data, file_name)
85
+ else
86
+ DSLMethods.new(self).instance_eval(dsl_data)
87
+ end
88
+ rescue SyntaxError, NoMethodError, NameError => e
89
+ raise DSLSyntaxError, "Illegal DSL syntax: #{e}"
90
+ end
91
+
92
+ # Load and parse a DSL from the given file name.
93
+ def load (dsl_file)
94
+ parse(File.read(dsl_file), dsl_file) if File.exist?(dsl_file)
95
+ end
96
+
97
+ # Load and parse a DSL from the given file name. Raises Authorization::Reader::DSLFileNotFoundError
98
+ # if the file cannot be found.
99
+ def load! (dsl_file)
100
+ raise ::Authorization::Reader::DSLFileNotFoundError, "Error reading authorization rules file with path '#{dsl_file}'! Please ensure it exists and that it is accessible." unless File.exist?(dsl_file)
101
+ load(dsl_file)
102
+ end
103
+
104
+ # Loads and parses DSL files and returns a new reader
105
+ def self.load (dsl_files)
106
+ # TODO cache reader in production mode?
107
+ reader = new
108
+ dsl_files = [dsl_files].flatten
109
+ dsl_files.each do |file|
110
+ reader.load(file)
111
+ end
112
+ reader
113
+ end
114
+
115
+ # DSL methods
116
+ class DSLMethods # :nodoc:
117
+ def initialize (parent)
118
+ @parent = parent
119
+ end
120
+
121
+ def privileges (&block)
122
+ @parent.privileges_reader.instance_eval(&block)
123
+ end
124
+
125
+ def contexts (&block)
126
+ # Not implemented
127
+ end
128
+
129
+ def authorization (&block)
130
+ @parent.auth_rules_reader.instance_eval(&block)
131
+ end
132
+ end
133
+ end
134
+
135
+ # The PrivilegeReader handles the part of the authorization DSL in
136
+ # a +privileges+ block. Here, privilege hierarchies are defined.
137
+ class PrivilegesReader
138
+ # TODO handle privileges with separated context
139
+ attr_reader :privileges, :privilege_hierarchy # :nodoc:
140
+
141
+ def initialize # :nodoc:
142
+ @current_priv = nil
143
+ @current_context = nil
144
+ @privileges = []
145
+ # {priv => [[priv,ctx], ...]}
146
+ @privilege_hierarchy = {}
147
+ end
148
+
149
+ def initialize_copy (from) # :nodoc:
150
+ @privileges = from.privileges.clone
151
+ @privilege_hierarchy = from.privilege_hierarchy.clone
152
+ end
153
+
154
+ def append_privilege (priv) # :nodoc:
155
+ @privileges << priv unless @privileges.include?(priv)
156
+ end
157
+
158
+ # Defines part of a privilege hierarchy. For the given +privilege+,
159
+ # included privileges may be defined in the block (through includes)
160
+ # or as option :+includes+. If the optional context is given,
161
+ # the privilege hierarchy is limited to that context.
162
+ #
163
+ def privilege (privilege, context = nil, options = {}, &block)
164
+ if context.is_a?(Hash)
165
+ options = context
166
+ context = nil
167
+ end
168
+ @current_priv = privilege
169
+ @current_context = context
170
+ append_privilege privilege
171
+ instance_eval(&block) if block
172
+ includes(*options[:includes]) if options[:includes]
173
+ ensure
174
+ @current_priv = nil
175
+ @current_context = nil
176
+ end
177
+
178
+ # Specifies +privileges+ that are to be assigned as lower ones. Only to
179
+ # be used inside a privilege block.
180
+ def includes (*privileges)
181
+ raise DSLError, "includes only in privilege block" if @current_priv.nil?
182
+ privileges.each do |priv|
183
+ append_privilege priv
184
+ @privilege_hierarchy[@current_priv] ||= []
185
+ @privilege_hierarchy[@current_priv] << [priv, @current_context]
186
+ end
187
+ end
188
+ end
189
+
190
+ class AuthorizationRulesReader
191
+ attr_reader :roles, :role_hierarchy, :auth_rules,
192
+ :role_descriptions, :role_titles, :omnipotent_roles # :nodoc:
193
+
194
+ def initialize # :nodoc:
195
+ @current_role = nil
196
+ @current_rule = nil
197
+ @roles = []
198
+ @omnipotent_roles = []
199
+ # higher_role => [lower_roles]
200
+ @role_hierarchy = {}
201
+ @role_titles = {}
202
+ @role_descriptions = {}
203
+ @auth_rules = AuthorizationRuleSet.new
204
+ end
205
+
206
+ def initialize_copy (from) # :nodoc:
207
+ [:roles, :role_hierarchy, :auth_rules,
208
+ :role_descriptions, :role_titles, :omnipotent_roles].each do |attribute|
209
+ instance_variable_set(:"@#{attribute}", from.send(attribute).clone)
210
+ end
211
+ end
212
+
213
+ def append_role (role, options = {}) # :nodoc:
214
+ @roles << role unless @roles.include? role
215
+ @role_titles[role] = options[:title] if options[:title]
216
+ @role_descriptions[role] = options[:description] if options[:description]
217
+ end
218
+
219
+ # Defines the authorization rules for the given +role+ in the
220
+ # following block.
221
+ # role :admin do
222
+ # has_permissions_on ...
223
+ # end
224
+ #
225
+ def role (role, options = {}, &block)
226
+ append_role role, options
227
+ @current_role = role
228
+ yield
229
+ ensure
230
+ @current_role = nil
231
+ end
232
+
233
+ # Roles may inherit all the rights from subroles. The given +roles+
234
+ # become subroles of the current block's role.
235
+ # role :admin do
236
+ # includes :user
237
+ # has_permission_on :employees, :to => [:update, :create]
238
+ # end
239
+ # role :user do
240
+ # has_permission_on :employees, :to => :read
241
+ # end
242
+ #
243
+ def includes (*roles)
244
+ raise DSLError, "includes only in role blocks" if @current_role.nil?
245
+ @role_hierarchy[@current_role] ||= []
246
+ @role_hierarchy[@current_role] += roles.flatten
247
+ end
248
+
249
+ # Allows the definition of privileges to be allowed for the current role,
250
+ # either in a has_permission_on block or directly in one call.
251
+ # role :admin
252
+ # has_permission_on :employees, :to => :read
253
+ # has_permission_on [:employees, :orders], :to => :read
254
+ # has_permission_on :employees do
255
+ # to :create
256
+ # if_attribute ...
257
+ # end
258
+ # has_permission_on :employees, :to => :delete do
259
+ # if_attribute ...
260
+ # end
261
+ # end
262
+ # The block form allows to describe restrictions on the permissions
263
+ # using if_attribute. Multiple has_permission_on statements are
264
+ # OR'ed when evaluating the permissions. Also, multiple if_attribute
265
+ # statements in one block are OR'ed if no :+join_by+ option is given
266
+ # (see below). To AND conditions, either set :+join_by+ to :and or place
267
+ # them in one if_attribute statement.
268
+ #
269
+ # Available options
270
+ # [:+to+]
271
+ # A symbol or an array of symbols representing the privileges that
272
+ # should be granted in this statement.
273
+ # [:+join_by+]
274
+ # Join operator to logically connect the constraint statements inside
275
+ # of the has_permission_on block. May be :+and+ or :+or+. Defaults to :+or+.
276
+ #
277
+ def has_permission_on (*args, &block)
278
+ options = args.extract_options!
279
+ context = args.flatten
280
+
281
+ raise DSLError, "has_permission_on only allowed in role blocks" if @current_role.nil?
282
+ options = {:to => [], :join_by => :or}.merge(options)
283
+
284
+ privs = options[:to]
285
+ privs = [privs] unless privs.is_a?(Array)
286
+ raise DSLError, "has_permission_on either needs a block or :to option" if !block_given? and privs.empty?
287
+
288
+ file, line = file_and_line_number_from_call_stack
289
+ rule = AuthorizationRule.new(@current_role, privs, context, options[:join_by],
290
+ :source_file => file, :source_line => line)
291
+ @auth_rules << rule
292
+ if block_given?
293
+ @current_rule = rule
294
+ yield
295
+ raise DSLError, "has_permission_on block content specifies no privileges" if rule.privileges.empty?
296
+ # TODO ensure?
297
+ @current_rule = nil
298
+ end
299
+ end
300
+
301
+ # Removes any permission checks for the current role.
302
+ # role :admin
303
+ # has_omnipotence
304
+ # end
305
+ def has_omnipotence
306
+ raise DSLError, "has_omnipotence only allowed in role blocks" if @current_role.nil?
307
+ @omnipotent_roles << @current_role
308
+ end
309
+
310
+ # Sets a description for the current role. E.g.
311
+ # role :admin
312
+ # description "To be assigned to administrative personnel"
313
+ # has_permission_on ...
314
+ # end
315
+ def description (text)
316
+ raise DSLError, "description only allowed in role blocks" if @current_role.nil?
317
+ role_descriptions[@current_role] = text
318
+ end
319
+
320
+ # Sets a human-readable title for the current role. E.g.
321
+ # role :admin
322
+ # title "Administrator"
323
+ # has_permission_on ...
324
+ # end
325
+ def title (text)
326
+ raise DSLError, "title only allowed in role blocks" if @current_role.nil?
327
+ role_titles[@current_role] = text
328
+ end
329
+
330
+ # Used in a has_permission_on block, to may be used to specify privileges
331
+ # to be assigned to the current role under the conditions specified in
332
+ # the current block.
333
+ # role :admin
334
+ # has_permission_on :employees do
335
+ # to :create, :read, :update, :delete
336
+ # end
337
+ # end
338
+ def to (*privs)
339
+ raise DSLError, "to only allowed in has_permission_on blocks" if @current_rule.nil?
340
+ @current_rule.append_privileges(privs.flatten)
341
+ end
342
+
343
+ # In a has_permission_on block, if_attribute specifies conditions
344
+ # of dynamic parameters that have to be met for the user to meet the
345
+ # privileges in this block. Conditions are evaluated on the context
346
+ # object. Thus, the following allows CRUD for branch admins only on
347
+ # employees that belong to the same branch as the current user.
348
+ # role :branch_admin
349
+ # has_permission_on :employees do
350
+ # to :create, :read, :update, :delete
351
+ # if_attribute :branch => is { user.branch }
352
+ # end
353
+ # end
354
+ # In this case, is is the operator for evaluating the condition. Another
355
+ # operator is contains for collections. In the block supplied to the
356
+ # operator, +user+ specifies the current user for whom the condition
357
+ # is evaluated.
358
+ #
359
+ # Conditions may be nested:
360
+ # role :company_admin
361
+ # has_permission_on :employees do
362
+ # to :create, :read, :update, :delete
363
+ # if_attribute :branch => { :company => is {user.branch.company} }
364
+ # end
365
+ # end
366
+ #
367
+ # has_many and has_many through associations may also be nested.
368
+ # Then, at least one item in the association needs to fulfill the
369
+ # subsequent condition:
370
+ # if_attribute :company => { :branches => { :manager => { :last_name => is { user.last_name } } }
371
+ # Beware of possible performance issues when using has_many associations in
372
+ # permitted_to? checks. For
373
+ # permitted_to? :read, object
374
+ # a check like
375
+ # object.company.branches.any? { |branch| branch.manager ... }
376
+ # will be executed. with_permission_to scopes construct efficient SQL
377
+ # joins, though.
378
+ #
379
+ # Multiple attributes in one :if_attribute statement are AND'ed.
380
+ # Multiple if_attribute statements are OR'ed if the join operator for the
381
+ # has_permission_on block isn't explicitly set. Thus, the following would
382
+ # require the current user either to be of the same branch AND the employee
383
+ # to be "changeable_by_coworker". OR the current user has to be the
384
+ # employee in question.
385
+ # has_permission_on :employees, :to => :manage do
386
+ # if_attribute :branch => is {user.branch}, :changeable_by_coworker => true
387
+ # if_attribute :id => is {user.id}
388
+ # end
389
+ # The join operator for if_attribute rules can explicitly set to AND, though.
390
+ # See has_permission_on for details.
391
+ #
392
+ # Arrays and fixed values may be used directly as hash values:
393
+ # if_attribute :id => 1
394
+ # if_attribute :type => "special"
395
+ # if_attribute :id => [1,2]
396
+ #
397
+ def if_attribute (attr_conditions_hash)
398
+ raise DSLError, "if_attribute only in has_permission blocks" if @current_rule.nil?
399
+ parse_attribute_conditions_hash!(attr_conditions_hash)
400
+ @current_rule.append_attribute Attribute.new(attr_conditions_hash)
401
+ end
402
+
403
+ # if_permitted_to allows the has_permission_on block to depend on
404
+ # permissions on associated objects. By using it, the authorization
405
+ # rules may be a lot DRYer. E.g.:
406
+ #
407
+ # role :branch_manager
408
+ # has_permission_on :branches, :to => :manage do
409
+ # if_attribute :employees => contains { user }
410
+ # end
411
+ # has_permission_on :employees, :to => :read do
412
+ # if_permitted_to :read, :branch
413
+ # # instead of
414
+ # # if_attribute :branch => { :employees => contains { user } }
415
+ # end
416
+ # end
417
+ #
418
+ # if_permitted_to associations may be nested as well:
419
+ # if_permitted_to :read, :branch => :company
420
+ #
421
+ # You can even use has_many associations as target. Then, it is checked
422
+ # if the current user has the required privilege on *any* of the target objects.
423
+ # if_permitted_to :read, :branch => :employees
424
+ # Beware of performance issues with permission checks. In the current implementation,
425
+ # all employees are checked until the first permitted is found.
426
+ # with_permissions_to, on the other hand, constructs more efficient SQL
427
+ # instead.
428
+ #
429
+ # To check permissions based on the current object, the attribute has to
430
+ # be left out:
431
+ # has_permission_on :branches, :to => :manage do
432
+ # if_attribute :employees => contains { user }
433
+ # end
434
+ # has_permission_on :branches, :to => :paint_green do
435
+ # if_permitted_to :update
436
+ # end
437
+ # Normally, one would merge those rules into one. Dividing makes sense
438
+ # if additional if_attribute are used in the second rule or those rules
439
+ # are applied to different roles.
440
+ #
441
+ # Options:
442
+ # [:+context+]
443
+ # When using with_permissions_to, the target context of the if_permitted_to
444
+ # statement is inferred from the last reflections target class. Still,
445
+ # you may override this algorithm by setting the context explicitly.
446
+ # if_permitted_to :read, :home_branch, :context => :branches
447
+ # if_permitted_to :read, :branch => :main_company, :context => :companies
448
+ #
449
+ def if_permitted_to (privilege, attr_or_hash = nil, options = {})
450
+ raise DSLError, "if_permitted_to only in has_permission blocks" if @current_rule.nil?
451
+ options[:context] ||= attr_or_hash.delete(:context) if attr_or_hash.is_a?(Hash)
452
+ # only :context option in attr_or_hash:
453
+ attr_or_hash = nil if attr_or_hash.is_a?(Hash) and attr_or_hash.empty?
454
+ @current_rule.append_attribute AttributeWithPermission.new(privilege,
455
+ attr_or_hash, options[:context])
456
+ end
457
+
458
+ # In an if_attribute statement, is says that the value has to be
459
+ # met exactly by the if_attribute attribute. For information on the block
460
+ # argument, see if_attribute.
461
+ def is (&block)
462
+ [:is, block]
463
+ end
464
+
465
+ # The negation of is.
466
+ def is_not (&block)
467
+ [:is_not, block]
468
+ end
469
+
470
+ # In an if_attribute statement, contains says that the value has to be
471
+ # part of the collection specified by the if_attribute attribute.
472
+ # For information on the block argument, see if_attribute.
473
+ def contains (&block)
474
+ [:contains, block]
475
+ end
476
+
477
+ # The negation of contains. Currently, query rewriting is disabled
478
+ # for does_not_contain.
479
+ def does_not_contain (&block)
480
+ [:does_not_contain, block]
481
+ end
482
+
483
+ # In an if_attribute statement, intersects_with requires that at least
484
+ # one of the values has to be part of the collection specified by the
485
+ # if_attribute attribute. The value block needs to evaluate to an
486
+ # Enumerable. For information on the block argument, see if_attribute.
487
+ def intersects_with (&block)
488
+ [:intersects_with, block]
489
+ end
490
+
491
+ # In an if_attribute statement, is_in says that the value has to
492
+ # contain the attribute value.
493
+ # For information on the block argument, see if_attribute.
494
+ def is_in (&block)
495
+ [:is_in, block]
496
+ end
497
+
498
+ # The negation of is_in.
499
+ def is_not_in (&block)
500
+ [:is_not_in, block]
501
+ end
502
+
503
+ # Less than
504
+ def lt (&block)
505
+ [:lt, block]
506
+ end
507
+
508
+ # Less than or equal to
509
+ def lte (&block)
510
+ [:lte, block]
511
+ end
512
+
513
+ # Greater than
514
+ def gt (&block)
515
+ [:gt, block]
516
+ end
517
+
518
+ # Greater than or equal to
519
+ def gte (&block)
520
+ [:gte, block]
521
+ end
522
+
523
+ private
524
+ def parse_attribute_conditions_hash! (hash)
525
+ merge_hash = {}
526
+ hash.each do |key, value|
527
+ if value.is_a?(Hash)
528
+ parse_attribute_conditions_hash!(value)
529
+ elsif !value.is_a?(Array)
530
+ merge_hash[key] = [:is, proc { value }]
531
+ elsif value.is_a?(Array) and !value[0].is_a?(Symbol)
532
+ merge_hash[key] = [:is_in, proc { value }]
533
+ end
534
+ end
535
+ hash.merge!(merge_hash)
536
+ end
537
+
538
+ def file_and_line_number_from_call_stack
539
+ caller_parts = caller(2).first.split(':')
540
+ [caller_parts[0] == "(eval)" ? nil : caller_parts[0],
541
+ caller_parts[1] && caller_parts[1].to_i]
542
+ end
543
+ end
544
+ end
545
+ end
546
+