iknow_view_models 3.6.1 → 3.6.4

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.
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