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,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ViewModel::Record::AttributeData
|
|
4
|
+
attr_reader :name, :model_attr_name, :attribute_viewmodel, :attribute_serializer
|
|
5
|
+
|
|
6
|
+
def initialize(name, model_attr_name, attribute_viewmodel, attribute_serializer, array, optional, read_only, write_once)
|
|
7
|
+
@name = name
|
|
8
|
+
@model_attr_name = model_attr_name
|
|
9
|
+
@attribute_viewmodel = attribute_viewmodel
|
|
10
|
+
@attribute_serializer = attribute_serializer
|
|
11
|
+
@array = array
|
|
12
|
+
@optional = optional
|
|
13
|
+
@read_only = read_only
|
|
14
|
+
@write_once = write_once
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def array?
|
|
18
|
+
@array
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def optional?
|
|
22
|
+
@optional
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def read_only?
|
|
26
|
+
@read_only
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def write_once?
|
|
30
|
+
@write_once
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def using_serializer?
|
|
34
|
+
!@attribute_serializer.nil?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def using_viewmodel?
|
|
38
|
+
!@attribute_viewmodel.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def map_value(value)
|
|
42
|
+
if array?
|
|
43
|
+
value.map { |v| yield(v) }
|
|
44
|
+
else
|
|
45
|
+
yield(value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class ViewModel
|
|
2
|
+
# Key to identify a viewmodel with some kind of inherent ID (e.g. an ViewModel::ActiveRecord)
|
|
3
|
+
class Reference
|
|
4
|
+
attr_accessor :viewmodel_class, :model_id
|
|
5
|
+
|
|
6
|
+
def initialize(viewmodel_class, model_id)
|
|
7
|
+
@viewmodel_class = viewmodel_class
|
|
8
|
+
@model_id = model_id
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
"'#{viewmodel_class.view_name}(id=#{model_id})'"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inspect
|
|
16
|
+
"<Ref:#{self}>"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
other.class == self.class &&
|
|
21
|
+
other.viewmodel_class == viewmodel_class &&
|
|
22
|
+
other.model_id == model_id
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
alias :eql? :==
|
|
26
|
+
|
|
27
|
+
def hash
|
|
28
|
+
[viewmodel_class, model_id].hash
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class ViewModel
|
|
2
|
+
# A bucket for configuration, used for serializing and deserializing.
|
|
3
|
+
class References
|
|
4
|
+
delegate :each, :size, :present?, to: :@value_by_ref
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@last_ref = 0
|
|
8
|
+
@ref_by_value = {}
|
|
9
|
+
@value_by_ref = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def has_references?
|
|
13
|
+
@ref_by_value.present?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Takes a reference to a thing that is to be shared, and returns the id
|
|
17
|
+
# under which the data is stored. If the data is not present, will compute
|
|
18
|
+
# it by calling the given block.
|
|
19
|
+
def add_reference(value)
|
|
20
|
+
if (ref = @ref_by_value[value]).present?
|
|
21
|
+
ref
|
|
22
|
+
else
|
|
23
|
+
ref = new_ref!(value)
|
|
24
|
+
@ref_by_value[value] = ref
|
|
25
|
+
@value_by_ref[ref] = value
|
|
26
|
+
ref
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def clear!
|
|
31
|
+
@ref_by_value.clear
|
|
32
|
+
@value_by_ref.clear
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Ensure stable reference ids for the same (persisted) viewmodels.
|
|
38
|
+
def new_ref!(viewmodel)
|
|
39
|
+
vm_ref = viewmodel.to_reference
|
|
40
|
+
if vm_ref.model_id
|
|
41
|
+
hash = Digest::SHA256.base64digest("#{vm_ref.viewmodel_class.name}.#{vm_ref.model_id}")
|
|
42
|
+
"ref:h:#{hash}"
|
|
43
|
+
else
|
|
44
|
+
'ref:i:%06d' % (@last_ref += 1)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ViewModel::Registry
|
|
4
|
+
include Singleton
|
|
5
|
+
|
|
6
|
+
DEFERRED_NAME = Object.new
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
delegate :for_view_name, :register, :default_view_name, :infer_model_class_name, :clear_removed_classes!,
|
|
10
|
+
to: :instance
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@lock = Monitor.new
|
|
15
|
+
@viewmodel_classes_by_name = {}
|
|
16
|
+
@deferred_viewmodel_classes = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def for_view_name(name)
|
|
20
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new('ViewModel name cannot be nil') if name.nil?
|
|
21
|
+
|
|
22
|
+
@lock.synchronize do
|
|
23
|
+
# Resolve names for any deferred viewmodel classes
|
|
24
|
+
resolve_deferred_classes
|
|
25
|
+
|
|
26
|
+
viewmodel_class = @viewmodel_classes_by_name[name]
|
|
27
|
+
|
|
28
|
+
if viewmodel_class.nil? || !(viewmodel_class < ViewModel)
|
|
29
|
+
raise ViewModel::DeserializationError::UnknownView.new(name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
viewmodel_class
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def register(viewmodel, as: DEFERRED_NAME)
|
|
37
|
+
@lock.synchronize do
|
|
38
|
+
@deferred_viewmodel_classes << [viewmodel, as]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def default_view_name(model_class_name)
|
|
43
|
+
model_class_name.gsub('::', '.')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def infer_model_class_name(view_name)
|
|
47
|
+
view_name.gsub('.', '::')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# For Rails hot code loading: ditch any classes that are not longer present at
|
|
51
|
+
# their constant
|
|
52
|
+
def clear_removed_classes!
|
|
53
|
+
@lock.synchronize do
|
|
54
|
+
resolve_deferred_classes
|
|
55
|
+
@viewmodel_classes_by_name.delete_if do |_name, klass|
|
|
56
|
+
!Kernel.const_defined?(klass.name)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def resolve_deferred_classes
|
|
64
|
+
until @deferred_viewmodel_classes.empty?
|
|
65
|
+
vm, view_name = @deferred_viewmodel_classes.pop
|
|
66
|
+
|
|
67
|
+
if vm.should_register?
|
|
68
|
+
view_name = vm.view_name if view_name == DEFERRED_NAME
|
|
69
|
+
@viewmodel_classes_by_name[view_name] = vm
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'json_schema'
|
|
3
|
+
|
|
4
|
+
class ViewModel::Schemas
|
|
5
|
+
JsonSchema.configure do |c|
|
|
6
|
+
uuid_format = /\A[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\Z/
|
|
7
|
+
c.register_format('uuid', ->(value) { uuid_format.match(value) })
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
ID_SCHEMA =
|
|
11
|
+
{ 'oneOf' => [{ 'type' => 'integer' },
|
|
12
|
+
{ 'type' => 'string', 'format' => 'uuid' }] }
|
|
13
|
+
ID = JsonSchema.parse!(ID_SCHEMA)
|
|
14
|
+
|
|
15
|
+
VIEWMODEL_UPDATE_SCHEMA =
|
|
16
|
+
{
|
|
17
|
+
'type' => 'object',
|
|
18
|
+
'description' => 'viewmodel update',
|
|
19
|
+
'properties' => { ViewModel::TYPE_ATTRIBUTE => { 'type' => 'string' },
|
|
20
|
+
ViewModel::ID_ATTRIBUTE => ID_SCHEMA,
|
|
21
|
+
ViewModel::NEW_ATTRIBUTE => { 'type' => 'boolean' },
|
|
22
|
+
ViewModel::VERSION_ATTRIBUTE => { 'type' => 'integer' } },
|
|
23
|
+
'required' => [ViewModel::TYPE_ATTRIBUTE]
|
|
24
|
+
}
|
|
25
|
+
VIEWMODEL_UPDATE = JsonSchema.parse!(VIEWMODEL_UPDATE_SCHEMA)
|
|
26
|
+
|
|
27
|
+
VIEWMODEL_REFERENCE_SCHEMA =
|
|
28
|
+
{
|
|
29
|
+
'type' => 'object',
|
|
30
|
+
'description' => 'viewmodel shared reference',
|
|
31
|
+
'properties' => { ViewModel::REFERENCE_ATTRIBUTE => { 'type' => 'string' } },
|
|
32
|
+
'additionalProperties' => false,
|
|
33
|
+
'required' => [ViewModel::REFERENCE_ATTRIBUTE],
|
|
34
|
+
}
|
|
35
|
+
VIEWMODEL_REFERENCE = JsonSchema.parse!(VIEWMODEL_REFERENCE_SCHEMA)
|
|
36
|
+
|
|
37
|
+
def self.verify_schema!(schema, value)
|
|
38
|
+
valid, errors = schema.validate(value)
|
|
39
|
+
unless valid
|
|
40
|
+
error_list = errors.map { |e| "#{e.pointer}: #{e.message}" }.join("\n")
|
|
41
|
+
errors = 'Error'.pluralize(errors.length)
|
|
42
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new("#{errors} parsing #{schema.description}:\n#{error_list}")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require 'active_support/core_ext'
|
|
2
|
+
require 'view_model/traversal_context'
|
|
3
|
+
|
|
4
|
+
class ViewModel::SerializeContext < ViewModel::TraversalContext
|
|
5
|
+
class SharedContext < ViewModel::TraversalContext::SharedContext
|
|
6
|
+
attr_reader :references, :flatten_references
|
|
7
|
+
|
|
8
|
+
def initialize(flatten_references: false, **rest)
|
|
9
|
+
super(**rest)
|
|
10
|
+
@references = ViewModel::References.new
|
|
11
|
+
@flatten_references = flatten_references
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.shared_context_class
|
|
16
|
+
SharedContext
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
delegate :references, :flatten_references, to: :shared_context
|
|
20
|
+
|
|
21
|
+
attr_reader :include, :prune
|
|
22
|
+
|
|
23
|
+
def initialize(include: nil, prune: nil, **rest)
|
|
24
|
+
super(**rest)
|
|
25
|
+
@include = self.class.normalize_includes(include)
|
|
26
|
+
@prune = self.class.normalize_includes(prune)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize_as_child(include:, prune:, **rest)
|
|
30
|
+
super(**rest)
|
|
31
|
+
@include = include
|
|
32
|
+
@prune = prune
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def for_child(parent_viewmodel, association_name:, **rest)
|
|
36
|
+
super(parent_viewmodel,
|
|
37
|
+
association_name: association_name,
|
|
38
|
+
include: @include.try { |i| i[association_name] },
|
|
39
|
+
prune: @prune.try { |p| p[association_name] },
|
|
40
|
+
**rest)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def includes_member?(member_name, default)
|
|
44
|
+
member_name = member_name.to_s
|
|
45
|
+
|
|
46
|
+
# Every node in the include tree is to be included
|
|
47
|
+
included = @include.try { |is| is.has_key?(member_name) }
|
|
48
|
+
# whereas only the leaves of the prune tree are to be removed
|
|
49
|
+
pruned = @prune.try { |ps| ps.fetch(member_name, :sentinel).nil? }
|
|
50
|
+
|
|
51
|
+
(default || included) && !pruned
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def add_includes(includes)
|
|
55
|
+
return if includes.blank?
|
|
56
|
+
@include ||= {}
|
|
57
|
+
@include.deep_merge!(self.class.normalize_includes(includes))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def add_prunes(prunes)
|
|
61
|
+
return if prunes.blank?
|
|
62
|
+
@prune ||= {}
|
|
63
|
+
@prune.deep_merge!(self.class.normalize_includes(prunes))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
delegate :add_reference, :has_references?, to: :references
|
|
67
|
+
|
|
68
|
+
# Return viewmodels referenced during serialization and clear @references.
|
|
69
|
+
def extract_referenced_views!
|
|
70
|
+
refs = references.each.to_h
|
|
71
|
+
references.clear!
|
|
72
|
+
refs
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def serialize_references(json)
|
|
76
|
+
reference_context = self.for_references
|
|
77
|
+
|
|
78
|
+
# References should be serialized in a stable order to improve caching via
|
|
79
|
+
# naive response hash.
|
|
80
|
+
|
|
81
|
+
serialized_refs = {}
|
|
82
|
+
|
|
83
|
+
while references.present?
|
|
84
|
+
extract_referenced_views!.each do |ref, value|
|
|
85
|
+
unless serialized_refs.has_key?(ref)
|
|
86
|
+
serialized_refs[ref] = Jbuilder.new do |j|
|
|
87
|
+
ViewModel.serialize(value, j, serialize_context: reference_context)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
serialized_refs.sort.each do |ref, value|
|
|
94
|
+
json.set!(ref, value)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def serialize_references_to_hash
|
|
99
|
+
Jbuilder.new { |json| serialize_references(json) }.attributes!
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.normalize_includes(includes)
|
|
103
|
+
case includes
|
|
104
|
+
when Array
|
|
105
|
+
includes.each_with_object({}) do |v, new_includes|
|
|
106
|
+
new_includes.merge!(normalize_includes(v))
|
|
107
|
+
end
|
|
108
|
+
when Hash
|
|
109
|
+
includes.each_with_object({}) do |(k,v), new_includes|
|
|
110
|
+
new_includes[k.to_s] = normalize_includes(v)
|
|
111
|
+
end
|
|
112
|
+
when nil
|
|
113
|
+
nil
|
|
114
|
+
else
|
|
115
|
+
{ includes.to_s => nil }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Helpers useful for writing tests for viewmodel implementations
|
|
3
|
+
module ViewModel::TestHelpers
|
|
4
|
+
require 'view_model/test_helpers/arvm_builder'
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
using ViewModel::Utils::Collections
|
|
7
|
+
|
|
8
|
+
def serialize_with_references(serializable, serialize_context: ViewModel.new_serialize_context)
|
|
9
|
+
data = ViewModel.serialize_to_hash(serializable, serialize_context: serialize_context)
|
|
10
|
+
references = serialize_context.serialize_references_to_hash
|
|
11
|
+
return data, references
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serialize(serializable, serialize_context: ViewModel.new_serialize_context)
|
|
15
|
+
data, _ = serialize_with_references(serializable, serialize_context: serialize_context)
|
|
16
|
+
data
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Test helper: update a model by manipulating the full view hash
|
|
20
|
+
def alter_by_view!(viewmodel_class, model,
|
|
21
|
+
serialize_context: viewmodel_class.new_serialize_context,
|
|
22
|
+
deserialize_context: viewmodel_class.new_deserialize_context)
|
|
23
|
+
|
|
24
|
+
models = Array.wrap(model)
|
|
25
|
+
|
|
26
|
+
data, refs = serialize_with_references(models.map { |m| viewmodel_class.new(m) }, serialize_context: serialize_context)
|
|
27
|
+
|
|
28
|
+
if model.is_a?(Array)
|
|
29
|
+
yield(data, refs)
|
|
30
|
+
else
|
|
31
|
+
yield(data.first, refs)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
result = viewmodel_class.deserialize_from_view(
|
|
36
|
+
data, references: refs, deserialize_context: deserialize_context)
|
|
37
|
+
|
|
38
|
+
result.each do |vm|
|
|
39
|
+
assert_consistent_record(vm)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
result = result.first unless model.is_a?(Array)
|
|
43
|
+
|
|
44
|
+
models.each { |m| m.reload }
|
|
45
|
+
|
|
46
|
+
return result, deserialize_context
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def assert_consistent_record(viewmodel, been_there: Set.new)
|
|
53
|
+
return if been_there.include?(viewmodel.model)
|
|
54
|
+
been_there << viewmodel.model
|
|
55
|
+
|
|
56
|
+
if viewmodel.is_a?(ViewModel::ActiveRecord)
|
|
57
|
+
assert_model_represents_database(viewmodel.model, been_there: been_there)
|
|
58
|
+
elsif viewmodel.is_a?(ViewModel::Record)
|
|
59
|
+
viewmodel.class._members.each do |name, attribute_data|
|
|
60
|
+
if attribute_data.attribute_viewmodel
|
|
61
|
+
assert_consistent_record(viewmodel.send(name), been_there: been_there)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def assert_model_represents_database(model, been_there: Set.new)
|
|
68
|
+
return if been_there.include?(model)
|
|
69
|
+
been_there << model
|
|
70
|
+
|
|
71
|
+
refute(model.new_record?, 'model represents database entity')
|
|
72
|
+
refute(model.changed?, 'model is fully persisted')
|
|
73
|
+
|
|
74
|
+
database_model = model.class.find(model.id)
|
|
75
|
+
|
|
76
|
+
assert_equal(database_model.attributes,
|
|
77
|
+
model.attributes,
|
|
78
|
+
'in memory attributes match database attributes')
|
|
79
|
+
|
|
80
|
+
model.class.reflections.each do |_, reflection|
|
|
81
|
+
association = model.association(reflection.name)
|
|
82
|
+
|
|
83
|
+
next unless association.loaded?
|
|
84
|
+
|
|
85
|
+
case
|
|
86
|
+
when association.target == nil
|
|
87
|
+
assert_nil(database_model.association(reflection.name).target,
|
|
88
|
+
'in memory nil association matches database')
|
|
89
|
+
when reflection.collection?
|
|
90
|
+
association.target.each do |associated_model|
|
|
91
|
+
assert_model_represents_database(associated_model, been_there: been_there)
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
assert_model_represents_database(association.target, been_there: been_there)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def assert_contains_exactly(expected, actual, msg = nil)
|
|
100
|
+
msg ||= diff(expected, actual)
|
|
101
|
+
assert(expected.contains_exactly?(actual), msg)
|
|
102
|
+
end
|
|
103
|
+
end
|