eaco 0.6.1 → 0.7.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/features/authorization_parse_error.feature +157 -0
  4. data/features/enterprise_authorization.feature +159 -0
  5. data/features/rails_integration.feature +1 -1
  6. data/features/role_based_authorization.feature +30 -7
  7. data/features/step_definitions/actor_steps.rb +29 -25
  8. data/features/step_definitions/enterprise_steps.rb +81 -0
  9. data/features/step_definitions/error_steps.rb +49 -0
  10. data/features/step_definitions/fixture_steps.rb +14 -0
  11. data/features/step_definitions/resource_steps.rb +16 -24
  12. data/features/support/env.rb +4 -2
  13. data/lib/eaco.rb +2 -0
  14. data/lib/eaco/actor.rb +4 -3
  15. data/lib/eaco/adapters/active_record.rb +1 -1
  16. data/lib/eaco/adapters/active_record/compatibility.rb +19 -14
  17. data/lib/eaco/adapters/active_record/compatibility/scoped.rb +25 -0
  18. data/lib/eaco/adapters/active_record/compatibility/v40.rb +11 -3
  19. data/lib/eaco/adapters/active_record/compatibility/v41.rb +14 -3
  20. data/lib/eaco/adapters/active_record/compatibility/v42.rb +10 -2
  21. data/lib/eaco/controller.rb +16 -3
  22. data/lib/eaco/coverage.rb +83 -0
  23. data/lib/eaco/cucumber/active_record.rb +13 -18
  24. data/lib/eaco/cucumber/active_record/department.rb +4 -0
  25. data/lib/eaco/cucumber/active_record/position.rb +2 -0
  26. data/lib/eaco/cucumber/active_record/schema.rb +20 -2
  27. data/lib/eaco/cucumber/active_record/user.rb +9 -0
  28. data/lib/eaco/cucumber/active_record/user/designators.rb +4 -1
  29. data/lib/eaco/cucumber/active_record/user/designators/authenticated.rb +54 -0
  30. data/lib/eaco/cucumber/active_record/user/designators/department.rb +58 -0
  31. data/lib/eaco/cucumber/active_record/user/designators/position.rb +53 -0
  32. data/lib/eaco/cucumber/active_record/user/designators/user.rb +4 -0
  33. data/lib/eaco/cucumber/world.rb +115 -5
  34. data/lib/eaco/designator.rb +7 -2
  35. data/lib/eaco/dsl.rb +9 -1
  36. data/lib/eaco/dsl/acl.rb +2 -2
  37. data/lib/eaco/dsl/actor.rb +6 -3
  38. data/lib/eaco/dsl/base.rb +5 -0
  39. data/lib/eaco/error.rb +10 -1
  40. data/lib/eaco/rake.rb +1 -0
  41. data/lib/eaco/rake/default_task.rb +29 -9
  42. data/lib/eaco/rake/utils.rb +38 -0
  43. data/lib/eaco/version.rb +1 -1
  44. data/spec/eaco/acl_spec.rb +34 -0
  45. data/spec/spec_helper.rb +3 -3
  46. metadata +18 -2
@@ -16,6 +16,8 @@ module Eaco
16
16
  #
17
17
  class User < Eaco::Designator
18
18
  ##
19
+ # This {Designator} description
20
+ #
19
21
  # @return [String] the {User}'s name.
20
22
  #
21
23
  def describe(*)
@@ -23,6 +25,8 @@ module Eaco
23
25
  end
24
26
 
25
27
  ##
28
+ # {User}s matching this designator.
29
+ #
26
30
  # @return [Array] this very {User} wrapped in an +Array+.
27
31
  #
28
32
  def resolve
@@ -132,7 +132,7 @@ module Eaco
132
132
  class World
133
133
 
134
134
  ##
135
- # Set up the World:
135
+ # Sets up the World:
136
136
  #
137
137
  # * Connect to ActiveRecord
138
138
  #
@@ -141,7 +141,117 @@ module Eaco
141
141
  end
142
142
 
143
143
  ##
144
- # @return [Class] a model in the {ActiveRecord} namespace.
144
+ # Authorizes model with the given {DSL}
145
+ #
146
+ # @param name [String] the model name
147
+ # @param definition [String] the {DSL} code
148
+ # @see {#find_model}
149
+ #
150
+ # @return [void]
151
+ #
152
+ def authorize_model(name, definition)
153
+ model = find_model(name)
154
+
155
+ eval_dsl definition, model
156
+ end
157
+
158
+ ##
159
+ # Registers and persists an {Actor} instance with the given +name+.
160
+ #
161
+ # @param model [String] the {Actor} model name
162
+ # @param name [String] the {Actor} instance name
163
+ # @param options [Boolean] the only supported one is +admin+, and
164
+ # specifies whether this {Actor} an admin
165
+ #
166
+ # @return [Actor] the newly created {Actor} instance.
167
+ #
168
+ def register_actor(model, name, options = {})
169
+ actor_model = find_model(model)
170
+
171
+ actors[name] = actor_model.new.tap do |actor|
172
+ actor.name = name
173
+ actor.admin = options.fetch(:admin, false)
174
+ actor.save!
175
+ end
176
+ end
177
+
178
+ ##
179
+ # Fetches an {Actor} instance by name.
180
+ #
181
+ # @param name [String] the actor name
182
+ # @return [Actor] the registered actor name
183
+ # @raise [RuntimeError] if the actor is not found in the registry
184
+ #
185
+ def fetch_actor(name)
186
+ actors.fetch(name)
187
+ rescue KeyError
188
+ # :nocov:
189
+ raise "Actor '#{name}' not found in registry"
190
+ # :nocov:
191
+ end
192
+
193
+ ##
194
+ # Registers and persists {Resource} instance with the given name.
195
+ #
196
+ # @param model [String] the {Resource} model name
197
+ # @param name [String] the {Resource} name
198
+ #
199
+ # @return [Resource] the newly instantiated {Resource}
200
+ #
201
+ def register_resource(model, name)
202
+ resource_model = find_model(model)
203
+
204
+ resource = resource_model.new.tap do |resource|
205
+ resource.name = name
206
+ resource.save!
207
+ end
208
+
209
+ resources[model] ||= {}
210
+ resources[model][name] = resource
211
+ end
212
+
213
+ ##
214
+ # Fetches a {Resource} instance by name.
215
+ #
216
+ # @param model [String] the {Resource} model name
217
+ # @param name [String] the {Resource} name
218
+ #
219
+ def fetch_resource(model, name)
220
+ resources.fetch(model).fetch(name)
221
+ rescue KeyError
222
+ # :nocov:
223
+ raise "Resource #{model} '#{resource}' not found in registry"
224
+ # :nocov:
225
+ end
226
+
227
+ ##
228
+ # All registered {Actor} instances.
229
+ #
230
+ # @return [Hash] actors keyed by name
231
+ #
232
+ def actors
233
+ @actors ||= {}
234
+ end
235
+
236
+ ##
237
+ # All registered {Resource} instances.
238
+ #
239
+ # @return [Hash] resources keyed by model name with +Hash+es
240
+ # as values keyed by resource name.
241
+ #
242
+ def resources
243
+ @resources ||= {}
244
+ end
245
+
246
+ ##
247
+ # Returns a model in the {ActiveRecord} namespace.
248
+ #
249
+ # Example:
250
+ #
251
+ # World.find_model('Document')
252
+ #
253
+ # @param model_name [String] the model name
254
+ # @return [Class]
145
255
  #
146
256
  def find_model(model_name)
147
257
  Eaco::Cucumber::ActiveRecord.const_get(model_name)
@@ -152,13 +262,13 @@ module Eaco
152
262
  # +$MODEL+ string with the given model name.
153
263
  #
154
264
  # @param code [String] the DSL code to eval
155
- # @param model [Class] the model name to substitute
265
+ # @param model [Class] the model name to substitute (optional)
156
266
  #
157
267
  # @return [void]
158
268
  #
159
- def eval_dsl(code, model)
269
+ def eval_dsl(code, model = nil)
160
270
  # Sub in place to print final code when running cucumber
161
- code.sub! '$MODEL', model.name
271
+ code.sub! '$MODEL', model.name if model
162
272
  Eaco.eval! code, '(feature)'
163
273
  end
164
274
  end
@@ -63,7 +63,7 @@ module Eaco
63
63
  # @return [Array] resolved actors, application-dependant.
64
64
  #
65
65
  def resolve(designators)
66
- Array.new(designators||[]).inject([]) {|ret, d| ret.concat parse(d).resolve}
66
+ Array.new(designators||[]).inject(Set.new) {|ret, d| ret.merge parse(d).resolve}
67
67
  end
68
68
 
69
69
  ##
@@ -108,7 +108,9 @@ module Eaco
108
108
  # @see Actor
109
109
  #
110
110
  def harvest(actor)
111
- Array.new([actor.send(@method)].flatten).map! {|value| new(value) }
111
+ list = actor.send(@method)
112
+ list = list.to_a if list.respond_to?(:to_a)
113
+ Array.new([list]).flatten.map! {|value| new(value) }
112
114
  end
113
115
 
114
116
  ##
@@ -167,6 +169,9 @@ module Eaco
167
169
  # in a typeahead menu, for your Enterprise authorization management
168
170
  # UI... :-)
169
171
  #
172
+ # @param query [String] the query to search against
173
+ # @return [Enumerable] application {Actor}s collection
174
+ #
170
175
  # @raise [NotImplementedError]
171
176
  #
172
177
  def search(query)
data/lib/eaco/dsl.rb CHANGED
@@ -19,6 +19,10 @@ module Eaco
19
19
  ##
20
20
  # Entry point for the {Resource} authorization definition.
21
21
  #
22
+ # @param resource_class [Class] the application resource class
23
+ # @param options [Hash] options passed to {DSL::Resource} and
24
+ # and {DSL::ACL}.
25
+ #
22
26
  # @see DSL::Resource
23
27
  # @see DSL::ACL
24
28
  #
@@ -28,7 +32,11 @@ module Eaco
28
32
  end
29
33
 
30
34
  ##
31
- # Entry point for the {Actor} designators definition.
35
+ # Entry point for an {Actor} definition.
36
+ #
37
+ # @param actor_class [Class] the application actor class
38
+ # @param options [Hash] currently unused
39
+ # @param block [Proc] the DSL code to eval
32
40
  #
33
41
  # @see DSL::Actor
34
42
  #
data/lib/eaco/dsl/acl.rb CHANGED
@@ -50,7 +50,7 @@ module Eaco
50
50
  #
51
51
  def define_acl_subclass
52
52
  target_eval do
53
- remove_const(:ACL) if const_defined?(:ACL)
53
+ remove_const(:ACL) if constants.include?(:ACL)
54
54
 
55
55
  Class.new(Eaco::ACL).tap do |acl_class|
56
56
  define_singleton_method(:acl) { acl_class }
@@ -97,7 +97,7 @@ module Eaco
97
97
  target.send(:include, adapter)
98
98
  install_authorized_collection_strategy
99
99
 
100
- elsif target.respond_to?(:acl) && target.respond_to?(:acl=)
100
+ elsif (target.instance_methods & [:acl, :acl=]).size != 2
101
101
  raise Malformed, <<-EOF
102
102
  Don't know how to persist ACLs using <#{target}>'s ORM
103
103
  (identified as <#{orm}>). Please define an `acl' instance
@@ -21,7 +21,7 @@ module Eaco
21
21
  autoload :Designators, 'eaco/dsl/actor/designators'
22
22
 
23
23
  ##
24
- # Initializes an Actor class.
24
+ # Makes an application model a valid {Eaco::Actor}.
25
25
  #
26
26
  # @see Eaco::Actor
27
27
  #
@@ -79,7 +79,7 @@ module Eaco
79
79
 
80
80
  ##
81
81
  # Defines the boolean logic that determines whether an {Actor} is an
82
- # admin. Usually you'll have an +admin+ method on your model, that you
82
+ # admin. Usually you'll have an +admin?+ method on your model, that you
83
83
  # can call from here. Or, feel free to just return +false+ to disable
84
84
  # this functionality.
85
85
  #
@@ -91,9 +91,12 @@ module Eaco
91
91
  # end
92
92
  # end
93
93
  #
94
+ # @param block [Proc]
95
+ # @return [void]
96
+ #
94
97
  def admin(&block)
95
98
  target_eval do
96
- @admin_logic = block
99
+ @_admin_logic = block
97
100
  end
98
101
  end
99
102
 
data/lib/eaco/dsl/base.rb CHANGED
@@ -32,6 +32,8 @@ module Eaco
32
32
  attr_reader :options
33
33
 
34
34
  ##
35
+ # Sets up the DSL instance target class and the options.
36
+ #
35
37
  # @param target [Class]
36
38
  # @param options [Hash]
37
39
  #
@@ -43,6 +45,9 @@ module Eaco
43
45
  ##
44
46
  # Evaluates the given block in the context of the target class
45
47
  #
48
+ # @param block [Proc]
49
+ # @return [void]
50
+ #
46
51
  def target_eval(&block)
47
52
  target.instance_eval(&block)
48
53
  end
data/lib/eaco/error.rb CHANGED
@@ -5,7 +5,16 @@ module Eaco
5
5
  #
6
6
  class Error < StandardError
7
7
  # As we make use of heredoc for long error messages, squeeze subsequent
8
- # spaces and remove newlines.
8
+ # spaces and remove newlines. If the message looks like an internal error
9
+ # though, newlines are preserved.
10
+ #
11
+ # Example:
12
+ #
13
+ # raise Eaco::Error, <<-EOF
14
+ # Some fancy message
15
+ # end
16
+ #
17
+ # @param message [String]
9
18
  #
10
19
  def initialize(message)
11
20
  unless message =~ %r{EACO.+Error}
data/lib/eaco/rake.rb CHANGED
@@ -4,6 +4,7 @@ module Eaco
4
4
  # Namespace to all rake-related functionality.
5
5
  #
6
6
  module Rake
7
+ autoload :Utils, 'eaco/rake/utils.rb'
7
8
  autoload :DefaultTask, 'eaco/rake/default_task.rb'
8
9
  end
9
10
 
@@ -1,3 +1,5 @@
1
+ require 'eaco/coverage'
2
+
1
3
  module Eaco
2
4
  module Rake
3
5
 
@@ -30,12 +32,14 @@ module Eaco
30
32
  task :default do
31
33
  run_specs
32
34
  run_cucumber
35
+ output_coverage
33
36
  end
34
37
 
35
38
  elsif running_in_travis?
36
39
  task :default do
37
40
  run_specs
38
41
  run_cucumber
42
+ report_coverage
39
43
  generate_documentation
40
44
  end
41
45
 
@@ -114,6 +118,25 @@ module Eaco
114
118
  ::Rake::Task[task].invoke
115
119
  end
116
120
 
121
+ ##
122
+ # Reports coverage data
123
+ #
124
+ # @return [void]
125
+ #
126
+ def report_coverage
127
+ Eaco::Coverage.report!
128
+ end
129
+
130
+ ##
131
+ # Formats code coverage results and prints a summary
132
+ #
133
+ # @return [void]
134
+ #
135
+ def output_coverage
136
+ summary = Eaco::Coverage.format!
137
+ croak summary
138
+ end
139
+
117
140
  ##
118
141
  # Fancily logs the given +msg+ to +$stderr+.
119
142
  #
@@ -142,8 +165,8 @@ module Eaco
142
165
  # @return [String]
143
166
  #
144
167
  def with_appraisal(msg)
145
- if appraisal
146
- msg = "%s \033[1;31m[%s]" % [msg, appraisal]
168
+ if gemfile
169
+ msg = "%s \033[1;31m[%s]" % [msg, gemfile]
147
170
  end
148
171
 
149
172
  return msg
@@ -166,14 +189,11 @@ module Eaco
166
189
  end
167
190
 
168
191
  ##
169
- # @return [String] the current appraisal name, or nil
192
+ # @see {Rake::Utils.gemfile}
193
+ # @private
170
194
  #
171
- def appraisal
172
- return unless running_appraisals?
173
-
174
- gemfile = ENV['BUNDLE_GEMFILE']
175
-
176
- File.basename(gemfile, '.*') if gemfile
195
+ def gemfile
196
+ Rake::Utils.gemfile
177
197
  end
178
198
 
179
199
  ##
@@ -0,0 +1,38 @@
1
+ module Eaco
2
+ module Rake
3
+
4
+ ##
5
+ # Assorted utilities.
6
+ #
7
+ module Utils
8
+ extend self
9
+
10
+ ##
11
+ # Captures the stdout emitted by the given +block+
12
+ #
13
+ # @param block [Proc]
14
+ # @return [String] the captured output
15
+ #
16
+ def capture_stdout(&block)
17
+ stdout, string = $stdout, StringIO.new
18
+ $stdout = string
19
+
20
+ yield
21
+
22
+ string.tap(&:rewind).read
23
+ ensure
24
+ $stdout = stdout
25
+ end
26
+
27
+ ##
28
+ # @return [String] the current gemfile name
29
+ #
30
+ def gemfile
31
+ gemfile = ENV['BUNDLE_GEMFILE']
32
+
33
+ File.basename(gemfile, '.*') if gemfile
34
+ end
35
+ end
36
+
37
+ end
38
+ end
data/lib/eaco/version.rb CHANGED
@@ -2,6 +2,6 @@ module Eaco
2
2
 
3
3
  # Current version
4
4
  #
5
- VERSION = '0.6.1'
5
+ VERSION = '0.7.0'
6
6
 
7
7
  end
@@ -31,6 +31,24 @@ RSpec.describe Eaco::ACL do
31
31
  it { expect(subject).to be_a(described_class) }
32
32
  it { expect(subject).to include({designator => :reader}) }
33
33
  end
34
+
35
+ context 'when looking up designators' do
36
+ after { acl.add(:reader, :foo, 1) }
37
+
38
+ let(:acl) { described_class.new }
39
+
40
+ it { expect(Eaco::Designator).to receive(:make).with(:foo, 1) }
41
+ end
42
+
43
+ context 'when giving rubbish' do
44
+ subject { acl.add(:reader, 'rubbish') }
45
+
46
+ let(:acl) { described_class.new }
47
+
48
+ it { expect { subject }.to \
49
+ raise_error(Eaco::Error).
50
+ with_message(/Cannot infer designator from "rubbish"/) }
51
+ end
34
52
  end
35
53
 
36
54
  describe '#del' do
@@ -144,4 +162,20 @@ RSpec.describe Eaco::ACL do
144
162
  end
145
163
  end
146
164
 
165
+ describe '#inspect' do
166
+ let(:acl) { described_class.new('foo' => :bar) }
167
+
168
+ subject { acl.inspect }
169
+
170
+ it { expect(subject).to eq('#<Eaco::ACL: {"foo"=>:bar}>') }
171
+ end
172
+
173
+ describe '#pretty_inspect' do
174
+ let(:acl) { described_class.new('foo' => :bar) }
175
+
176
+ subject { acl.pretty_inspect }
177
+
178
+ it { expect(subject).to eq("Eaco::ACL\n{\"foo\"=>:bar}\n") }
179
+ end
180
+
147
181
  end