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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +115 -0
  3. data/.gitignore +36 -0
  4. data/.travis.yml +31 -0
  5. data/Appraisals +9 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +19 -0
  9. data/Rakefile +21 -0
  10. data/appveyor.yml +22 -0
  11. data/gemfiles/rails_5_2.gemfile +15 -0
  12. data/gemfiles/rails_6_0_beta.gemfile +15 -0
  13. data/iknow_view_models.gemspec +49 -0
  14. data/lib/iknow_view_models.rb +12 -0
  15. data/lib/iknow_view_models/railtie.rb +8 -0
  16. data/lib/iknow_view_models/version.rb +5 -0
  17. data/lib/view_model.rb +333 -0
  18. data/lib/view_model/access_control.rb +154 -0
  19. data/lib/view_model/access_control/composed.rb +216 -0
  20. data/lib/view_model/access_control/open.rb +13 -0
  21. data/lib/view_model/access_control/read_only.rb +13 -0
  22. data/lib/view_model/access_control/tree.rb +264 -0
  23. data/lib/view_model/access_control_error.rb +10 -0
  24. data/lib/view_model/active_record.rb +383 -0
  25. data/lib/view_model/active_record/association_data.rb +178 -0
  26. data/lib/view_model/active_record/association_manipulation.rb +389 -0
  27. data/lib/view_model/active_record/cache.rb +265 -0
  28. data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
  29. data/lib/view_model/active_record/cloner.rb +113 -0
  30. data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
  31. data/lib/view_model/active_record/controller.rb +77 -0
  32. data/lib/view_model/active_record/controller_base.rb +185 -0
  33. data/lib/view_model/active_record/nested_controller_base.rb +93 -0
  34. data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
  35. data/lib/view_model/active_record/update_context.rb +252 -0
  36. data/lib/view_model/active_record/update_data.rb +749 -0
  37. data/lib/view_model/active_record/update_operation.rb +810 -0
  38. data/lib/view_model/active_record/visitor.rb +77 -0
  39. data/lib/view_model/after_transaction_runner.rb +29 -0
  40. data/lib/view_model/callbacks.rb +219 -0
  41. data/lib/view_model/changes.rb +62 -0
  42. data/lib/view_model/config.rb +29 -0
  43. data/lib/view_model/controller.rb +142 -0
  44. data/lib/view_model/deserialization_error.rb +437 -0
  45. data/lib/view_model/deserialize_context.rb +16 -0
  46. data/lib/view_model/error.rb +191 -0
  47. data/lib/view_model/error_view.rb +35 -0
  48. data/lib/view_model/record.rb +367 -0
  49. data/lib/view_model/record/attribute_data.rb +48 -0
  50. data/lib/view_model/reference.rb +31 -0
  51. data/lib/view_model/references.rb +48 -0
  52. data/lib/view_model/registry.rb +73 -0
  53. data/lib/view_model/schemas.rb +45 -0
  54. data/lib/view_model/serialization_error.rb +10 -0
  55. data/lib/view_model/serialize_context.rb +118 -0
  56. data/lib/view_model/test_helpers.rb +103 -0
  57. data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
  58. data/lib/view_model/traversal_context.rb +126 -0
  59. data/lib/view_model/utils.rb +24 -0
  60. data/lib/view_model/utils/collections.rb +49 -0
  61. data/test/helpers/arvm_test_models.rb +59 -0
  62. data/test/helpers/arvm_test_utilities.rb +187 -0
  63. data/test/helpers/callback_tracer.rb +27 -0
  64. data/test/helpers/controller_test_helpers.rb +270 -0
  65. data/test/helpers/match_enumerator.rb +58 -0
  66. data/test/helpers/query_logging.rb +71 -0
  67. data/test/helpers/test_access_control.rb +56 -0
  68. data/test/helpers/viewmodel_spec_helpers.rb +326 -0
  69. data/test/unit/view_model/access_control_test.rb +769 -0
  70. data/test/unit/view_model/active_record/alias_test.rb +35 -0
  71. data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
  72. data/test/unit/view_model/active_record/cache_test.rb +351 -0
  73. data/test/unit/view_model/active_record/cloner_test.rb +313 -0
  74. data/test/unit/view_model/active_record/controller_test.rb +561 -0
  75. data/test/unit/view_model/active_record/counter_test.rb +80 -0
  76. data/test/unit/view_model/active_record/customization_test.rb +388 -0
  77. data/test/unit/view_model/active_record/has_many_test.rb +957 -0
  78. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
  79. data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
  80. data/test/unit/view_model/active_record/has_one_test.rb +334 -0
  81. data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
  82. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
  83. data/test/unit/view_model/active_record/poly_test.rb +320 -0
  84. data/test/unit/view_model/active_record/shared_test.rb +285 -0
  85. data/test/unit/view_model/active_record/version_test.rb +121 -0
  86. data/test/unit/view_model/active_record_test.rb +542 -0
  87. data/test/unit/view_model/callbacks_test.rb +582 -0
  88. data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
  89. data/test/unit/view_model/record_test.rb +524 -0
  90. data/test/unit/view_model/traversal_context_test.rb +371 -0
  91. data/test/unit/view_model_test.rb +62 -0
  92. 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