iknow_view_models 3.6.1 → 3.6.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f05051b924e9602a09da08b3aec5e5c4de224753d70d29523d66eb34db6947f
4
- data.tar.gz: aac9f7453c54bb216d3cf59b438070c2857f186138c8ff871abaf026a94e4ab4
3
+ metadata.gz: 31ecbd493e6716db0442e309c1566f38c13f676ef72a55ea9708f1537875331f
4
+ data.tar.gz: b03d5b1b1f0d3ba458f11f5575f379969eea72089f781ef65462ab47e181e158
5
5
  SHA512:
6
- metadata.gz: 3edbbb12ef310f58fea255589d2e2112cc62760518af31628c1121b0ea1db9634cb5ace9657087bceecd7f1bd38188fe9bec8b7e03c88613b773fafd2c6b7b6a
7
- data.tar.gz: b063d4884cf2846bef6baca22bbc7f8533ddb2c876bf5ffa5e456a5834d7729881b0d08aab69ff524b5cf0fd451599346cce022502334f2bba26673b753a8068
6
+ metadata.gz: 06e8384611d8fe7e93ac6065c11aca651f15b717be02a0818106198b769fe6ea59c843a83fd7cd457089a44a98cda499e7f5a2ae905d6c4b46c04e18c68cd4af
7
+ data.tar.gz: 3c20e972b8a5011aeae40494bf8d271ec3ef5092bbccb7495ea435a7e99dbf45b89f632e868c56bdf681722251f4432e6bd426dba2bfd9d2ef93cce7fc71c066
data/.circleci/config.yml CHANGED
@@ -8,7 +8,7 @@ executors:
8
8
  default: "2.7"
9
9
  pg-version:
10
10
  type: string
11
- default: "11"
11
+ default: "12.10"
12
12
  gemfile:
13
13
  type: string
14
14
  default: "Gemfile"
@@ -16,14 +16,14 @@ executors:
16
16
  PGHOST: 127.0.0.1
17
17
  PGUSER: eikaiwa
18
18
  docker:
19
- - image: circleci/ruby:<< parameters.ruby-version >>
19
+ - image: cimg/ruby:<< parameters.ruby-version >>
20
20
  environment:
21
21
  BUNDLE_JOBS: 3
22
22
  BUNDLE_RETRY: 3
23
23
  BUNDLE_PATH: vendor/bundle
24
24
  RAILS_ENV: test
25
25
  BUNDLE_GEMFILE: << parameters.gemfile >>
26
- - image: circleci/postgres:<< parameters.pg-version >>-alpine
26
+ - image: cimg/postgres:<< parameters.pg-version >>
27
27
  environment:
28
28
  POSTGRES_USER: eikaiwa
29
29
  POSTGRES_DB: iknow_view_models
@@ -104,27 +104,27 @@ workflows:
104
104
  - test:
105
105
  name: 'ruby 2.7 rails 5.2 pg 12'
106
106
  ruby-version: "2.7"
107
- pg-version: "12"
107
+ pg-version: "12.10"
108
108
  gemfile: gemfiles/rails_5_2.gemfile
109
109
  - test:
110
110
  name: 'ruby 2.7 rails 6.0 pg 12'
111
111
  ruby-version: "2.7"
112
- pg-version: "12"
112
+ pg-version: "12.10"
113
113
  gemfile: gemfiles/rails_6_0.gemfile
114
114
  - test:
115
115
  name: 'ruby 2.7 rails 6.1 pg 12'
116
116
  ruby-version: "2.7"
117
- pg-version: "12"
117
+ pg-version: "12.10"
118
118
  gemfile: gemfiles/rails_6_1.gemfile
119
119
  - test:
120
120
  name: 'ruby 3.0 rails 6.1 pg 12'
121
121
  ruby-version: "3.0"
122
- pg-version: "12"
122
+ pg-version: "12.10"
123
123
  gemfile: gemfiles/rails_6_1.gemfile
124
124
  - test:
125
- name: 'ruby 3.0 rails 7.0 pg 12'
126
- ruby-version: "3.0"
127
- pg-version: "12"
125
+ name: 'ruby 3.1 rails 7.0 pg 13'
126
+ ruby-version: "3.1"
127
+ pg-version: "13.6"
128
128
  gemfile: gemfiles/rails_7_0.gemfile
129
129
  - publish:
130
130
  filters:
data/Gemfile CHANGED
@@ -13,3 +13,4 @@ gem 'minitest-ci'
13
13
  # Override gemspec for development version preferences
14
14
  gem 'activerecord', '~> 7.0.0'
15
15
  gem 'activesupport', '~> 7.0.0'
16
+ gem 'actionpack', '~> 7.0.0'
@@ -5,5 +5,6 @@ source 'https://rubygems.org'
5
5
  gem 'minitest-ci'
6
6
  gem 'activerecord', '~> 5.2.0'
7
7
  gem 'activesupport', '~> 5.2.0'
8
+ gem 'actionpack', '~> 5.2.0'
8
9
 
9
10
  gemspec path: '../'
@@ -5,5 +5,6 @@ source 'https://rubygems.org'
5
5
  gem 'minitest-ci'
6
6
  gem 'activerecord', '~> 6.0.0'
7
7
  gem 'activesupport', '~> 6.0.0'
8
+ gem 'actionpack', '~> 6.0.0'
8
9
 
9
10
  gemspec path: '../'
@@ -5,5 +5,6 @@ source 'https://rubygems.org'
5
5
  gem 'minitest-ci'
6
6
  gem 'activerecord', '~> 6.1.0'
7
7
  gem 'activesupport', '~> 6.1.0'
8
+ gem 'actionpack', '~> 6.1.0'
8
9
 
9
10
  gemspec path: '../'
@@ -5,5 +5,6 @@ source "https://rubygems.org"
5
5
  gem 'minitest-ci'
6
6
  gem "activerecord", "~> 7.0.0"
7
7
  gem "activesupport", "~> 7.0.0"
8
+ gem "actionpack", "~> 7.0.0"
8
9
 
9
10
  gemspec path: '../'
@@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.required_ruby_version = '>= 2.7'
23
23
 
24
+ spec.add_dependency 'actionpack', '>= 5.0'
24
25
  spec.add_dependency 'activerecord', '>= 5.0'
25
26
  spec.add_dependency 'activesupport', '>= 5.0'
26
27
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.6.1'
4
+ VERSION = '3.6.4'
5
5
  end
@@ -39,15 +39,31 @@ class ViewModel::AccessControl::Composed < ViewModel::AccessControl
39
39
  case
40
40
  when new_allow
41
41
  nil
42
- when self.allow_error && other.allow_error
43
- self.allow_error.merge(other.allow_error)
42
+ when self.allow_error.nil?
43
+ other.allow_error
44
+ when other.allow_error.nil?
45
+ self.allow_error
46
+ # Mergeable (standard) errors should be merged if possible; if not
47
+ # possible, we should take the first non-standard error.
48
+ when mergeable_error?(self.allow_error)
49
+ if mergeable_error?(other.allow_error)
50
+ self.allow_error.merge(other.allow_error)
51
+ else
52
+ other.allow_error
53
+ end
44
54
  else
45
- self.allow_error || other.allow_error
55
+ self.allow_error
46
56
  end
47
57
 
48
58
  ComposedResult.new(new_allow, other.veto, new_allow_error, other.veto_error)
49
59
  end
50
60
  end
61
+
62
+ private
63
+
64
+ def mergeable_error?(err)
65
+ err.is_a?(NoRequiredConditionsError)
66
+ end
51
67
  end
52
68
 
53
69
  PermissionsCheck = Struct.new(:location, :reason, :error_type, :checker) do
@@ -159,11 +175,10 @@ class ViewModel::AccessControl::Composed < ViewModel::AccessControl
159
175
  end
160
176
 
161
177
  def inspect
162
- s = super + '('
163
- s += inspect_checks.join(', ')
164
- s += " includes checkers: #{@included_checkers.inspect}" if @included_checkers.present?
165
- s += ')'
166
- s
178
+ checks = inspect_checks
179
+ checks << "includes checkers: #{@included_checkers.inspect}" if @included_checkers.present?
180
+
181
+ super + '(' + checks.join(', ') + ')'
167
182
  end
168
183
 
169
184
  def inspect_checks
@@ -196,22 +211,54 @@ class ViewModel::AccessControl::Composed < ViewModel::AccessControl
196
211
  protected
197
212
 
198
213
  def check_delegates(env, ifs, unlesses)
199
- vetoed_checker = unlesses.detect { |checker| checker.check(env) }
214
+ veto, veto_error = detect_veto(env, unlesses)
215
+ allow, allow_error = detect_allow(env, ifs)
216
+
217
+ ComposedResult.new(allow, veto, allow_error, veto_error)
218
+ end
219
+
220
+ private
221
+
222
+ def detect_veto(env, checkers)
223
+ checkers.each do |checker|
224
+ result = checker.check(env)
225
+ next unless result
200
226
 
201
- veto = vetoed_checker.present?
202
- if veto
203
- veto_error = vetoed_checker.error_type.new('Action not permitted because: ' +
204
- vetoed_checker.reason,
205
- env.view.blame_reference)
227
+ error =
228
+ if result.is_a?(StandardError)
229
+ result
230
+ else
231
+ checker.error_type.new('Action not permitted because: ' +
232
+ checker.reason,
233
+ env.view.blame_reference)
234
+ end
235
+
236
+ # short-circuit exit with failure
237
+ return true, error
206
238
  end
207
239
 
208
- allow = ifs.any? { |checker| checker.check(env) }
240
+ return false, nil
241
+ end
242
+
243
+ def detect_allow(env, checkers)
244
+ error = nil
245
+
246
+ checkers.each do |checker|
247
+ result = checker.check(env)
248
+ next unless result
209
249
 
210
- unless allow
211
- allow_error = NoRequiredConditionsError.new(env.view.blame_reference,
212
- ifs.map(&:name))
250
+ if result.is_a?(StandardError)
251
+ error ||= result
252
+ else
253
+ # short-circuit exit with success
254
+ return true, nil
255
+ end
213
256
  end
214
257
 
215
- ComposedResult.new(allow, veto, allow_error, veto_error)
258
+ error ||= NoRequiredConditionsError.new(
259
+ env.view.blame_reference,
260
+ checkers.map(&:name))
261
+
262
+ return false, error
216
263
  end
217
264
  end
@@ -192,8 +192,6 @@ class ViewModel::AccessControl::Tree < ViewModel::AccessControl
192
192
  def inspect_checks
193
193
  checks = super
194
194
  if root?
195
- checks << 'no root checks'
196
- else
197
195
  checks << "root_children_visible_if: #{root_children_visible_ifs.map(&:reason)}" if root_children_visible_ifs.present?
198
196
  checks << "root_children_visible_unless: #{root_children_visible_unlesses.map(&:reason)}" if root_children_visible_unlesses.present?
199
197
  checks << "root_children_editable_if: #{root_children_editable_ifs.map(&:reason)}" if root_children_editable_ifs.present?
@@ -252,16 +250,16 @@ class ViewModel::AccessControl::Tree < ViewModel::AccessControl
252
250
 
253
251
  def save_root_visibility!(traversal_env)
254
252
  result = check_delegates(traversal_env,
255
- self.class.each_check(:root_children_visible_ifs, ->(a) { a.is_a?(Node) }),
256
- self.class.each_check(:root_children_visible_unlesses, ->(a) { a.is_a?(Node) }))
253
+ self.class.each_check(:root_children_visible_ifs, ->(a) { a < Node }),
254
+ self.class.each_check(:root_children_visible_unlesses, ->(a) { a < Node }))
257
255
 
258
256
  store_descendent_visibility(traversal_env.view, result)
259
257
  end
260
258
 
261
259
  def save_root_editability!(traversal_env)
262
260
  result = check_delegates(traversal_env,
263
- self.class.each_check(:root_children_editable_ifs, ->(a) { a.is_a?(Node) }),
264
- self.class.each_check(:root_children_editable_unlesses, ->(a) { a.is_a?(Node) }))
261
+ self.class.each_check(:root_children_editable_ifs, ->(a) { a < Node }),
262
+ self.class.each_check(:root_children_editable_unlesses, ->(a) { a < Node }))
265
263
 
266
264
  store_descendent_editability(traversal_env.view, result)
267
265
  end
@@ -149,6 +149,14 @@ class ViewModel::AccessControl
149
149
  def raise_if_error!(result)
150
150
  raise (result.error || yield) unless result.permit?
151
151
  end
152
+
153
+ # Called from composed access controls via the `env`, this is used to make the
154
+ # if/unless DSL more readable when returning a custom failure error.
155
+ def failure(err)
156
+ raise ArgumentError.new("Unexpected failure type: #{err}") unless err.is_a?(StandardError)
157
+
158
+ err
159
+ end
152
160
  end
153
161
 
154
162
  require 'view_model/access_control/open'
@@ -14,7 +14,7 @@ class ViewModel::Migration::NoPathError < ViewModel::AbstractError
14
14
  end
15
15
 
16
16
  def detail
17
- "No migration path for #{vm_name} from #{from} to #{to}"
17
+ "No migration path for #{vm_name} between client version #{from} and server version #{to}"
18
18
  end
19
19
 
20
20
  def meta
data/lib/view_model.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  # A ViewModel encapsulates a particular aggregation of data calculated via the
4
4
  # underlying models and provides a means of serializing it into views.
5
5
  require 'jbuilder'
6
+ require 'base64'
6
7
  require 'deep_preloader'
7
8
 
8
9
  class ViewModel
@@ -156,6 +156,54 @@ class ViewModel::AccessControlTest < ActiveSupport::TestCase
156
156
  assert_equal(2, ex.reasons.count)
157
157
  end
158
158
 
159
+ def test_veto_ordering
160
+ TestAccessControl.visible_if!('always') { true }
161
+
162
+ TestAccessControl.visible_unless!('car starts with i') do
163
+ view.car =~ /^i/
164
+ end
165
+
166
+ TestAccessControl.visible_unless!('car ends with e') do
167
+ view.car =~ /e$/
168
+ end
169
+
170
+ assert_serializes(ListView, List.create!(car: 'ok'))
171
+ refute_serializes(ListView, List.create!(car: 'invisible'), /not permitted.*car starts with i/)
172
+ end
173
+
174
+ def test_custom_error_if
175
+ TestAccessControl.visible_if!('car is visible1') do
176
+ view.car == 'visible1' ||
177
+ # In principle a failure() may return any error, but by returning an
178
+ # AccessControlError we make it possible to test with refute_serializes
179
+ failure(ViewModel::AccessControlError.new('Custom Error Message', view.blame_reference))
180
+ end
181
+
182
+ TestAccessControl.visible_if!('car is visible2') do
183
+ view.car == 'visible2' ||
184
+ # Only the first failure() recorded by a failed if check will be
185
+ # raised as the error.
186
+ failure(ViewModel::AccessControlError.new('Should not be seen', view.blame_reference))
187
+ end
188
+
189
+ assert_serializes(ListView, List.create!(car: 'visible1'))
190
+ assert_serializes(ListView, List.create!(car: 'visible2'))
191
+ refute_serializes(ListView, List.create!(car: 'bad'), /Custom Error Message/)
192
+ end
193
+
194
+ def test_custom_error_unless
195
+ TestAccessControl.visible_if!('always') { true }
196
+
197
+ TestAccessControl.visible_unless!('car is invisible') do
198
+ if view.car == 'invisible'
199
+ failure(ViewModel::AccessControlError.new('Custom Error Message', view.blame_reference))
200
+ end
201
+ end
202
+
203
+ assert_serializes(ListView, List.create!(car: 'ok'))
204
+ refute_serializes(ListView, List.create!(car: 'invisible'), /Custom Error Message/)
205
+ end
206
+
159
207
  def test_inheritance
160
208
  child_access_control = Class.new(ViewModel::AccessControl::Composed)
161
209
  child_access_control.include_from(TestAccessControl)
@@ -271,6 +319,26 @@ class ViewModel::AccessControlTest < ActiveSupport::TestCase
271
319
  assert_serializes(Tree1View, make_tree('rule:visible_children', 'visible child', 'rule:visible_children', 'visible child'))
272
320
  end
273
321
 
322
+ def test_root_inheritance
323
+ parent_access_control = Class.new(ViewModel::AccessControl::Tree)
324
+ parent_access_control.view 'Tree1' do
325
+ visible_if!('true') { true }
326
+
327
+ root_children_visible_if!('root children visible') do
328
+ view.val == 'rule:visible_children'
329
+ end
330
+ end
331
+
332
+ TestAccessControl.include_from(parent_access_control)
333
+
334
+ refute_serializes(Tree1View, make_tree('arbitrary parent', 'invisible child'))
335
+ assert_serializes(Tree1View, make_tree('rule:visible_children', 'visible child'))
336
+
337
+ # nested root
338
+ refute_serializes(Tree1View, make_tree('rule:visible_children', 'visible child', 'arbitrary parent', 'invisible child'))
339
+ assert_serializes(Tree1View, make_tree('rule:visible_children', 'visible child', 'rule:visible_children', 'visible child'))
340
+ end
341
+
274
342
  def test_visibility_veto_from_root
275
343
  TestAccessControl.view 'Tree1' do
276
344
  root_children_visible_unless!('root children invisible') do
@@ -15,16 +15,11 @@ require 'view_model/active_record'
15
15
 
16
16
  DUMMY_RAILS_CACHE = ActiveSupport::Cache::MemoryStore.new
17
17
 
18
- module RailsDummyCache
19
- def cache
20
- DUMMY_RAILS_CACHE
21
- end
18
+ IknowCache.configure! do
19
+ logger ::ActiveRecord::Base.logger
20
+ cache DUMMY_RAILS_CACHE
22
21
  end
23
22
 
24
- # Ensure we have a dummy Rails, and then prepend our dummy cache
25
- module Rails; end
26
- Rails.singleton_class.prepend(RailsDummyCache)
27
-
28
23
  class ViewModel::ActiveRecord
29
24
  class CacheTest < ActiveSupport::TestCase
30
25
  using ViewModel::Utils::Collections
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iknow_view_models
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.1
4
+ version: 3.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - iKnow Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-12 00:00:00.000000000 Z
11
+ date: 2022-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: actionpack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: activerecord
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -495,7 +509,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
495
509
  - !ruby/object:Gem::Version
496
510
  version: '0'
497
511
  requirements: []
498
- rubygems_version: 3.1.6
512
+ rubygems_version: 3.3.11
499
513
  signing_key:
500
514
  specification_version: 4
501
515
  summary: ViewModels provide a means of encapsulating a collection of related data