iknow_view_models 2.8.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 +7 -0
- data/.circleci/config.yml +115 -0
- data/.gitignore +36 -0
- data/.travis.yml +31 -0
- data/Appraisals +9 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +19 -0
- data/Rakefile +21 -0
- data/appveyor.yml +22 -0
- data/gemfiles/rails_5_2.gemfile +15 -0
- data/gemfiles/rails_6_0_beta.gemfile +15 -0
- data/iknow_view_models.gemspec +49 -0
- data/lib/iknow_view_models.rb +12 -0
- data/lib/iknow_view_models/railtie.rb +8 -0
- data/lib/iknow_view_models/version.rb +5 -0
- data/lib/view_model.rb +333 -0
- data/lib/view_model/access_control.rb +154 -0
- data/lib/view_model/access_control/composed.rb +216 -0
- data/lib/view_model/access_control/open.rb +13 -0
- data/lib/view_model/access_control/read_only.rb +13 -0
- data/lib/view_model/access_control/tree.rb +264 -0
- data/lib/view_model/access_control_error.rb +10 -0
- data/lib/view_model/active_record.rb +383 -0
- data/lib/view_model/active_record/association_data.rb +178 -0
- data/lib/view_model/active_record/association_manipulation.rb +389 -0
- data/lib/view_model/active_record/cache.rb +265 -0
- data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
- data/lib/view_model/active_record/cloner.rb +113 -0
- data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
- data/lib/view_model/active_record/controller.rb +77 -0
- data/lib/view_model/active_record/controller_base.rb +185 -0
- data/lib/view_model/active_record/nested_controller_base.rb +93 -0
- data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
- data/lib/view_model/active_record/update_context.rb +252 -0
- data/lib/view_model/active_record/update_data.rb +749 -0
- data/lib/view_model/active_record/update_operation.rb +810 -0
- data/lib/view_model/active_record/visitor.rb +77 -0
- data/lib/view_model/after_transaction_runner.rb +29 -0
- data/lib/view_model/callbacks.rb +219 -0
- data/lib/view_model/changes.rb +62 -0
- data/lib/view_model/config.rb +29 -0
- data/lib/view_model/controller.rb +142 -0
- data/lib/view_model/deserialization_error.rb +437 -0
- data/lib/view_model/deserialize_context.rb +16 -0
- data/lib/view_model/error.rb +191 -0
- data/lib/view_model/error_view.rb +35 -0
- data/lib/view_model/record.rb +367 -0
- data/lib/view_model/record/attribute_data.rb +48 -0
- data/lib/view_model/reference.rb +31 -0
- data/lib/view_model/references.rb +48 -0
- data/lib/view_model/registry.rb +73 -0
- data/lib/view_model/schemas.rb +45 -0
- data/lib/view_model/serialization_error.rb +10 -0
- data/lib/view_model/serialize_context.rb +118 -0
- data/lib/view_model/test_helpers.rb +103 -0
- data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
- data/lib/view_model/traversal_context.rb +126 -0
- data/lib/view_model/utils.rb +24 -0
- data/lib/view_model/utils/collections.rb +49 -0
- data/test/helpers/arvm_test_models.rb +59 -0
- data/test/helpers/arvm_test_utilities.rb +187 -0
- data/test/helpers/callback_tracer.rb +27 -0
- data/test/helpers/controller_test_helpers.rb +270 -0
- data/test/helpers/match_enumerator.rb +58 -0
- data/test/helpers/query_logging.rb +71 -0
- data/test/helpers/test_access_control.rb +56 -0
- data/test/helpers/viewmodel_spec_helpers.rb +326 -0
- data/test/unit/view_model/access_control_test.rb +769 -0
- data/test/unit/view_model/active_record/alias_test.rb +35 -0
- data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
- data/test/unit/view_model/active_record/cache_test.rb +351 -0
- data/test/unit/view_model/active_record/cloner_test.rb +313 -0
- data/test/unit/view_model/active_record/controller_test.rb +561 -0
- data/test/unit/view_model/active_record/counter_test.rb +80 -0
- data/test/unit/view_model/active_record/customization_test.rb +388 -0
- data/test/unit/view_model/active_record/has_many_test.rb +957 -0
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
- data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
- data/test/unit/view_model/active_record/has_one_test.rb +334 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
- data/test/unit/view_model/active_record/poly_test.rb +320 -0
- data/test/unit/view_model/active_record/shared_test.rb +285 -0
- data/test/unit/view_model/active_record/version_test.rb +121 -0
- data/test/unit/view_model/active_record_test.rb +542 -0
- data/test/unit/view_model/callbacks_test.rb +582 -0
- data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
- data/test/unit/view_model/record_test.rb +524 -0
- data/test/unit/view_model/traversal_context_test.rb +371 -0
- data/test/unit/view_model_test.rb +62 -0
- metadata +490 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
class ViewModel::TestHelpers::ARVMBuilder
|
|
2
|
+
attr_reader :name, :model, :viewmodel, :namespace
|
|
3
|
+
|
|
4
|
+
# Building an ARVM requires three blocks, to define schema, model and
|
|
5
|
+
# viewmodel. Support providing these either in an spec argument or as a
|
|
6
|
+
# dsl-style builder.
|
|
7
|
+
Spec = Struct.new(:schema, :model, :viewmodel) do
|
|
8
|
+
def initialize(schema:, model:, viewmodel:)
|
|
9
|
+
super(schema, model, viewmodel)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def merge(schema: nil, model: nil, viewmodel: nil)
|
|
13
|
+
this_schema = self.schema
|
|
14
|
+
this_model = self.model
|
|
15
|
+
this_viewmodel = self.viewmodel
|
|
16
|
+
|
|
17
|
+
Spec.new(
|
|
18
|
+
schema: ->(t) do
|
|
19
|
+
this_schema.(t)
|
|
20
|
+
schema&.(t)
|
|
21
|
+
end,
|
|
22
|
+
model: ->(m) do
|
|
23
|
+
m.class_eval(&this_model)
|
|
24
|
+
model.try { |b| m.class_eval(&b) }
|
|
25
|
+
end,
|
|
26
|
+
viewmodel: ->(v) do
|
|
27
|
+
v.class_eval(&this_viewmodel)
|
|
28
|
+
viewmodel.try { |b| v.class_eval(&b) }
|
|
29
|
+
end)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(name, model_base: ApplicationRecord, viewmodel_base: ViewModelBase, namespace: Object, spec: nil, &block)
|
|
34
|
+
@model_base = model_base
|
|
35
|
+
@viewmodel_base = viewmodel_base
|
|
36
|
+
@namespace = namespace
|
|
37
|
+
@name = name.to_s.camelize
|
|
38
|
+
@no_viewmodel = false
|
|
39
|
+
|
|
40
|
+
if spec
|
|
41
|
+
define_schema(&spec.schema)
|
|
42
|
+
define_model(&spec.model)
|
|
43
|
+
define_viewmodel(&spec.viewmodel)
|
|
44
|
+
else
|
|
45
|
+
instance_eval(&block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
raise "Model not created in ARVMBuilder" unless model
|
|
49
|
+
raise "Schema not created in ARVMBuilder" unless model.table_exists?
|
|
50
|
+
raise "ViewModel not created in ARVMBuilder" unless (viewmodel || @no_viewmodel)
|
|
51
|
+
|
|
52
|
+
# Force the realization of the view model into the library's lookup
|
|
53
|
+
# table. If this doesn't happen the library may have conflicting entries in
|
|
54
|
+
# the deferred table, and will allow viewmodels to leak between tests.
|
|
55
|
+
unless @no_viewmodel || !(@viewmodel < ViewModel::Record)
|
|
56
|
+
resolved = ViewModel::Registry.for_view_name(viewmodel.view_name)
|
|
57
|
+
raise "Failed to register expected new class!" unless resolved == @viewmodel
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def teardown
|
|
62
|
+
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{name.underscore.pluralize} CASCADE")
|
|
63
|
+
namespace.send(:remove_const, name)
|
|
64
|
+
namespace.send(:remove_const, viewmodel_name) if viewmodel
|
|
65
|
+
# prevent cached old class from being used to resolve associations
|
|
66
|
+
ActiveSupport::Dependencies::Reference.clear!
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def viewmodel_name
|
|
72
|
+
self.name + "View"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def define_schema(&block)
|
|
76
|
+
table_name = name.underscore.pluralize
|
|
77
|
+
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{table_name} CASCADE")
|
|
78
|
+
ActiveRecord::Schema.define do
|
|
79
|
+
self.verbose = false
|
|
80
|
+
create_table(table_name, &block)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def define_model(&block)
|
|
85
|
+
model_name = name
|
|
86
|
+
_namespace = namespace
|
|
87
|
+
@model = Class.new(@model_base) do |c|
|
|
88
|
+
raise "Model already defined: #{model_name}" if _namespace.const_defined?(model_name, false)
|
|
89
|
+
_namespace.const_set(model_name, self)
|
|
90
|
+
class_eval(&block)
|
|
91
|
+
reset_column_information
|
|
92
|
+
end
|
|
93
|
+
@model
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def define_viewmodel(&block)
|
|
97
|
+
vm_name = viewmodel_name
|
|
98
|
+
_namespace = namespace
|
|
99
|
+
@viewmodel = Class.new(@viewmodel_base) do |c|
|
|
100
|
+
raise "Viewmodel alreay defined: #{vm_name}" if _namespace.const_defined?(vm_name, false)
|
|
101
|
+
_namespace.const_set(vm_name, self)
|
|
102
|
+
class_eval(&block)
|
|
103
|
+
end
|
|
104
|
+
raise "help help" if @viewmodel.name.nil?
|
|
105
|
+
@viewmodel
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def no_viewmodel
|
|
109
|
+
@no_viewmodel = true
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'view_model/access_control/tree'
|
|
4
|
+
|
|
5
|
+
# Abstract base for Serialize and DeserializeContexts.
|
|
6
|
+
class ViewModel::TraversalContext
|
|
7
|
+
class SharedContext
|
|
8
|
+
attr_reader :access_control, :callbacks
|
|
9
|
+
|
|
10
|
+
def initialize(access_control: ViewModel::AccessControl::Open.new, callbacks: [])
|
|
11
|
+
@access_control = access_control
|
|
12
|
+
# Access control is guaranteed to be run after callbacks that may have
|
|
13
|
+
# side-effects on the view.
|
|
14
|
+
pre_callbacks, post_callbacks = callbacks.partition { |c| c.class.updates_view? }
|
|
15
|
+
@callbacks = pre_callbacks + [access_control] + post_callbacks
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.shared_context_class
|
|
20
|
+
SharedContext
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :shared_context
|
|
24
|
+
delegate :access_control, :callbacks, to: :shared_context
|
|
25
|
+
|
|
26
|
+
def self.new_child(*args)
|
|
27
|
+
self.allocate.tap { |c| c.initialize_as_child(*args) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(shared_context: nil, **shared_context_params)
|
|
31
|
+
super()
|
|
32
|
+
@shared_context = shared_context || self.class.shared_context_class.new(**shared_context_params)
|
|
33
|
+
@parent_context = nil
|
|
34
|
+
@parent_viewmodel = nil
|
|
35
|
+
@parent_association = nil
|
|
36
|
+
@root = true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Overloaded constructor for initialization of descendent node contexts.
|
|
40
|
+
# Shared context is the same, ancestry is established, and subclasses can
|
|
41
|
+
# override to maintain other node-specific state.
|
|
42
|
+
def initialize_as_child(shared_context:, parent_context:, parent_viewmodel:, parent_association:)
|
|
43
|
+
@shared_context = shared_context
|
|
44
|
+
@parent_context = parent_context
|
|
45
|
+
@parent_viewmodel = parent_viewmodel
|
|
46
|
+
@parent_association = parent_association
|
|
47
|
+
@root = false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def for_child(parent_viewmodel, association_name:, **rest)
|
|
51
|
+
self.class.new_child(
|
|
52
|
+
shared_context: shared_context,
|
|
53
|
+
parent_context: self,
|
|
54
|
+
parent_viewmodel: parent_viewmodel,
|
|
55
|
+
parent_association: association_name,
|
|
56
|
+
**rest)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Obtain a semi-independent context for descending through a shared reference:
|
|
60
|
+
# keep the same shared context, but drop any tree location specific local
|
|
61
|
+
# context (since a shared reference could equally have been reached via any
|
|
62
|
+
# parent)
|
|
63
|
+
def for_references
|
|
64
|
+
self.class.new(shared_context: shared_context)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def parent_context(idx = 0)
|
|
68
|
+
if idx == 0
|
|
69
|
+
@parent_context
|
|
70
|
+
else
|
|
71
|
+
@parent_context&.parent_context(idx - 1)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parent_viewmodel(idx = 0)
|
|
76
|
+
if idx == 0
|
|
77
|
+
@parent_viewmodel
|
|
78
|
+
else
|
|
79
|
+
parent_context(idx - 1)&.parent_viewmodel
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parent_association(idx = 0)
|
|
84
|
+
if idx == 0
|
|
85
|
+
@parent_association
|
|
86
|
+
else
|
|
87
|
+
parent_context(idx - 1)&.parent_association
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parent_ref(idx = 0)
|
|
92
|
+
parent_viewmodel(idx)&.to_reference
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def run_callback(hook, view, **args)
|
|
96
|
+
callbacks.each do |callback|
|
|
97
|
+
callback.run_callback(hook, view, self, **args)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if view.respond_to?(hook.dsl_viewmodel_callback_method)
|
|
101
|
+
view.public_send(hook.dsl_viewmodel_callback_method, hook.context_name => self, **args)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def root?
|
|
106
|
+
@root
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def nearest_root
|
|
110
|
+
if root?
|
|
111
|
+
self
|
|
112
|
+
else
|
|
113
|
+
parent_context&.nearest_root
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def nearest_root_viewmodel
|
|
118
|
+
if root?
|
|
119
|
+
raise RuntimeError.new("Attempted to find nearest root from a root context. This is probably not what you wanted.")
|
|
120
|
+
elsif parent_context.root?
|
|
121
|
+
parent_viewmodel
|
|
122
|
+
else
|
|
123
|
+
parent_context.nearest_root_viewmodel
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ViewModel::Utils
|
|
4
|
+
class << self
|
|
5
|
+
def wrap_one_or_many(obj)
|
|
6
|
+
return_array = array_like?(obj)
|
|
7
|
+
results = yield(Array.wrap(obj))
|
|
8
|
+
return_array ? results : results.first
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def map_one_or_many(obj)
|
|
12
|
+
if array_like?(obj)
|
|
13
|
+
obj.map { |x| yield(x) }
|
|
14
|
+
else
|
|
15
|
+
yield(obj)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Cover arrays and also Rails' array-like types.
|
|
20
|
+
def array_like?(obj)
|
|
21
|
+
obj.respond_to?(:to_ary)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ViewModel::Utils
|
|
4
|
+
module Collections
|
|
5
|
+
def self.count_by(enumerable)
|
|
6
|
+
enumerable.each_with_object({}) do |el, counts|
|
|
7
|
+
key = yield(el)
|
|
8
|
+
|
|
9
|
+
unless key.nil?
|
|
10
|
+
counts[key] = (counts[key] || 0) + 1
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
refine Array do
|
|
16
|
+
def contains_exactly?(other)
|
|
17
|
+
mine = count_by { |x| x }
|
|
18
|
+
theirs = other.count_by { |x| x }
|
|
19
|
+
mine == theirs
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def count_by(&by)
|
|
23
|
+
Collections.count_by(self, &by)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def duplicates_by(&by)
|
|
27
|
+
count_by(&by).delete_if { |_, count| count == 1 }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def duplicates
|
|
31
|
+
duplicates_by { |x| x }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
refine Hash do
|
|
36
|
+
def count_by(&by)
|
|
37
|
+
Collections.count_by(self, &by)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def duplicates_by(&by)
|
|
41
|
+
count_by(&by).delete_if { |_, count| count == 1 }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def duplicates
|
|
45
|
+
duplicates_by { |x| x }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require_relative "test_access_control.rb"
|
|
2
|
+
|
|
3
|
+
require "iknow_view_models"
|
|
4
|
+
require "view_model/active_record"
|
|
5
|
+
require "view_model/active_record/controller"
|
|
6
|
+
|
|
7
|
+
require "acts_as_manual_list"
|
|
8
|
+
|
|
9
|
+
db_config_path = File.join(File.dirname(__FILE__), '../config/database.yml')
|
|
10
|
+
db_config = YAML.load(File.open(db_config_path))
|
|
11
|
+
raise "Test database configuration missing" unless db_config["test"]
|
|
12
|
+
ActiveRecord::Base.establish_connection(db_config["test"])
|
|
13
|
+
|
|
14
|
+
# Remove test tables if any exist
|
|
15
|
+
%w[labels parents children targets poly_ones poly_twos owners
|
|
16
|
+
grand_parents categories tags parents_tags].each do |t|
|
|
17
|
+
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{t} CASCADE")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Set up transactional tests
|
|
21
|
+
class ActiveSupport::TestCase
|
|
22
|
+
include ActiveRecord::TestFixtures
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Base class for models
|
|
26
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
27
|
+
self.abstract_class = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# base class for viewmodels
|
|
31
|
+
class ViewModelBase < ViewModel::ActiveRecord
|
|
32
|
+
self.abstract_class = true
|
|
33
|
+
|
|
34
|
+
class DeserializeContext < ViewModel::DeserializeContext
|
|
35
|
+
def initialize(can_view: true, can_edit: true, can_change: true, **params)
|
|
36
|
+
params[:access_control] ||= TestAccessControl.new(can_view, can_edit, can_change)
|
|
37
|
+
super(**params)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
delegate :visible_checks, :editable_checks, :valid_edit_refs, :valid_edit_changes, :all_valid_edit_changes, :was_edited?, to: :access_control
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.deserialize_context_class
|
|
44
|
+
DeserializeContext
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class SerializeContext < ViewModel::SerializeContext
|
|
48
|
+
def initialize(can_view: true, **params)
|
|
49
|
+
params[:access_control] ||= TestAccessControl.new(can_view, false, false)
|
|
50
|
+
super(**params)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
delegate :visible_checks, :editable_checks, :valid_edit_refs, :valid_edit_changes, :all_valid_edit_changes, to: :access_control
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.serialize_context_class
|
|
57
|
+
SerializeContext
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require 'active_support'
|
|
2
|
+
require 'minitest/hooks'
|
|
3
|
+
|
|
4
|
+
require 'view_model'
|
|
5
|
+
require 'view_model/test_helpers'
|
|
6
|
+
|
|
7
|
+
require_relative 'query_logging.rb'
|
|
8
|
+
|
|
9
|
+
ActiveSupport::TestCase.include(Minitest::Hooks)
|
|
10
|
+
|
|
11
|
+
module ARVMTestUtilities
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
include ViewModel::TestHelpers
|
|
14
|
+
using ViewModel::Utils::Collections
|
|
15
|
+
|
|
16
|
+
def self.included(klass)
|
|
17
|
+
klass.include(QueryLogging)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(*)
|
|
21
|
+
@viewmodels = []
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def after_all
|
|
26
|
+
@viewmodels.each(&:teardown)
|
|
27
|
+
@viewmodels.clear
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def teardown
|
|
32
|
+
ActiveRecord::Base.logger = nil
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def build_viewmodel(name, &block)
|
|
37
|
+
@viewmodels << ViewModel::TestHelpers::ARVMBuilder.new(name, &block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def serialize_with_references(serializable, serialize_context: ViewModelBase.new_serialize_context)
|
|
41
|
+
super(serializable, serialize_context: serialize_context)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def serialize(serializable, serialize_context: ViewModelBase.new_serialize_context)
|
|
45
|
+
super(serializable, serialize_context: serialize_context)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Construct an update hash that references an existing model. Does not include
|
|
49
|
+
# any of the model's attributes or association.
|
|
50
|
+
def update_hash_for(viewmodel_class, model)
|
|
51
|
+
refhash = { '_type' => viewmodel_class.view_name, 'id' => model.id }
|
|
52
|
+
yield(refhash) if block_given?
|
|
53
|
+
refhash
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Test helper: update a model by constructing a new view hash
|
|
57
|
+
# TODO the body of this is growing longer and is mostly the same as by `alter_by_view!`.
|
|
58
|
+
def set_by_view!(viewmodel_class, model)
|
|
59
|
+
models = Array.wrap(model)
|
|
60
|
+
|
|
61
|
+
data = models.map { |m| update_hash_for(viewmodel_class, m) }
|
|
62
|
+
refs = {}
|
|
63
|
+
|
|
64
|
+
if model.is_a?(Array)
|
|
65
|
+
yield(data, refs)
|
|
66
|
+
else
|
|
67
|
+
yield(data.first, refs)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
deserialize_context = ViewModelBase::DeserializeContext.new
|
|
72
|
+
|
|
73
|
+
viewmodel_class.deserialize_from_view(
|
|
74
|
+
data, references: refs, deserialize_context: ViewModelBase::DeserializeContext.new)
|
|
75
|
+
|
|
76
|
+
deserialize_context
|
|
77
|
+
ensure
|
|
78
|
+
models.each { |m| m.reload }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def count_all(enum)
|
|
83
|
+
enum.count_by { |x| x }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def enable_logging!
|
|
87
|
+
if ENV['DEBUG']
|
|
88
|
+
ActiveRecord::Base.logger = Logger.new(STDERR)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def assert_serializes(vm, model, serialize_context: vm.new_serialize_context)
|
|
93
|
+
h = vm.new(model).to_hash(serialize_context: serialize_context)
|
|
94
|
+
assert_kind_of(Hash, h)
|
|
95
|
+
refs = serialize_context.serialize_references_to_hash
|
|
96
|
+
assert_kind_of(Hash, refs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def refute_serializes(vm, model, message = nil, serialize_context: vm.new_serialize_context)
|
|
100
|
+
ex = assert_raises(ViewModel::AccessControlError) do
|
|
101
|
+
vm.new(model).to_hash(serialize_context: serialize_context)
|
|
102
|
+
serialize_context.serialize_references_to_hash
|
|
103
|
+
end
|
|
104
|
+
assert_match(message, ex.message) if message
|
|
105
|
+
ex
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def assert_deserializes(vm, model,
|
|
109
|
+
deserialize_context: vm.new_deserialize_context,
|
|
110
|
+
serialize_context: vm.new_serialize_context,
|
|
111
|
+
&block)
|
|
112
|
+
alter_by_view!(vm, model,
|
|
113
|
+
deserialize_context: deserialize_context,
|
|
114
|
+
serialize_context: serialize_context,
|
|
115
|
+
&block)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def refute_deserializes(vm, model, message = nil,
|
|
119
|
+
deserialize_context: vm.new_deserialize_context,
|
|
120
|
+
serialize_context: vm.new_serialize_context,
|
|
121
|
+
&block)
|
|
122
|
+
ex = assert_raises(ViewModel::AccessControlError) do
|
|
123
|
+
alter_by_view!(vm, model,
|
|
124
|
+
deserialize_context: deserialize_context,
|
|
125
|
+
serialize_context: serialize_context,
|
|
126
|
+
&block)
|
|
127
|
+
end
|
|
128
|
+
assert_match(message, ex.message) if message
|
|
129
|
+
ex
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
class FupdateBuilder
|
|
133
|
+
class DSL
|
|
134
|
+
def initialize(builder)
|
|
135
|
+
@builder = builder
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def append(hashes, **rest)
|
|
139
|
+
@builder.append_action(
|
|
140
|
+
type: ViewModel::ActiveRecord::FunctionalUpdate::Append,
|
|
141
|
+
values: hashes,
|
|
142
|
+
**rest)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def remove(hashes)
|
|
146
|
+
@builder.append_action(
|
|
147
|
+
type: ViewModel::ActiveRecord::FunctionalUpdate::Remove,
|
|
148
|
+
values: hashes)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def update(hashes)
|
|
152
|
+
@builder.append_action(
|
|
153
|
+
type: ViewModel::ActiveRecord::FunctionalUpdate::Update,
|
|
154
|
+
values: hashes)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def initialize
|
|
159
|
+
@actions = []
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def append_action(type:, values:, **rest)
|
|
163
|
+
@actions.push(
|
|
164
|
+
{
|
|
165
|
+
ViewModel::ActiveRecord::TYPE_ATTRIBUTE => type::NAME,
|
|
166
|
+
ViewModel::ActiveRecord::VALUES_ATTRIBUTE => values,
|
|
167
|
+
}.merge(rest.transform_keys(&:to_s))
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def build!(&block)
|
|
172
|
+
DSL.new(self).instance_eval(&block)
|
|
173
|
+
|
|
174
|
+
{
|
|
175
|
+
ViewModel::ActiveRecord::TYPE_ATTRIBUTE =>
|
|
176
|
+
ViewModel::ActiveRecord::FUNCTIONAL_UPDATE_TYPE,
|
|
177
|
+
|
|
178
|
+
ViewModel::ActiveRecord::ACTIONS_ATTRIBUTE =>
|
|
179
|
+
@actions,
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def build_fupdate(attrs = {}, &block)
|
|
185
|
+
FupdateBuilder.new.build!(&block).merge(attrs)
|
|
186
|
+
end
|
|
187
|
+
end
|