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,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'view_model/after_transaction_runner'
|
|
4
|
+
|
|
5
|
+
# Concern providing caching configuration and lookup on viewmodels.
|
|
6
|
+
module ViewModel::ActiveRecord::Cache::CacheableView
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
# Callback handler to participate in ActiveRecord::ConnectionAdapters::
|
|
10
|
+
# Transaction callbacks: invalidates a given cache member after the current
|
|
11
|
+
# transaction commits.
|
|
12
|
+
CacheClearer = Struct.new(:cache, :id) do
|
|
13
|
+
include ViewModel::AfterTransactionRunner
|
|
14
|
+
|
|
15
|
+
def after_transaction
|
|
16
|
+
cache.delete(id)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def connection
|
|
20
|
+
cache.viewmodel_class.model_class.connection
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class_methods do
|
|
25
|
+
def create_viewmodel_cache!(**opts)
|
|
26
|
+
@viewmodel_cache = ViewModel::ActiveRecord::Cache.new(self, **opts)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def viewmodel_cache
|
|
30
|
+
@viewmodel_cache
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def serialize_from_cache(views, serialize_context:)
|
|
34
|
+
plural = views.is_a?(Array)
|
|
35
|
+
views = Array.wrap(views)
|
|
36
|
+
json_views, json_refs = viewmodel_cache.fetch_by_viewmodel(views, serialize_context: serialize_context)
|
|
37
|
+
json_views = json_views.first unless plural
|
|
38
|
+
return json_views, json_refs
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Clear the cache if the view or its owned children were changed during
|
|
43
|
+
# deserialization
|
|
44
|
+
def after_deserialize(deserialize_context:, changes:)
|
|
45
|
+
super if defined?(super)
|
|
46
|
+
|
|
47
|
+
if !changes.new? && changes.changed_tree?
|
|
48
|
+
CacheClearer.new(self.class.viewmodel_cache, id).add_to_transaction
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Simple visitor for cloning models through the tree structure defined by
|
|
2
|
+
# ViewModel::ActiveRecord. Owned associations will be followed and cloned, while
|
|
3
|
+
# shared associations will be copied directly. Attributes (including association
|
|
4
|
+
# foreign keys not covered by ViewModel `association`s) will be copied from the
|
|
5
|
+
# original.
|
|
6
|
+
#
|
|
7
|
+
# To customize, subclasses may define methods `visit_x_view(node, new_model)`
|
|
8
|
+
# for each type they wish to affect. These callbacks may update attributes of
|
|
9
|
+
# the new model, and additionally can call `ignore!` or
|
|
10
|
+
# `ignore_association!(name)` to prune the current model or the target of the
|
|
11
|
+
# named association from the cloned tree.
|
|
12
|
+
class ViewModel::ActiveRecord::Cloner
|
|
13
|
+
def clone(node)
|
|
14
|
+
reset_state!
|
|
15
|
+
|
|
16
|
+
new_model = node.model.dup
|
|
17
|
+
|
|
18
|
+
pre_visit(node, new_model)
|
|
19
|
+
return nil if ignored?
|
|
20
|
+
|
|
21
|
+
if node.class.name
|
|
22
|
+
class_name = node.class.name.underscore.gsub('/', '__')
|
|
23
|
+
visit = :"visit_#{class_name}"
|
|
24
|
+
end_visit = :"end_visit_#{class_name}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if visit && respond_to?(visit, true)
|
|
28
|
+
self.send(visit, node, new_model)
|
|
29
|
+
return nil if ignored?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# visit the underlying viewmodel for each association, ignoring any
|
|
33
|
+
# customization
|
|
34
|
+
node.class._members.each do |name, association_data|
|
|
35
|
+
next unless association_data.is_a?(ViewModel::ActiveRecord::AssociationData)
|
|
36
|
+
|
|
37
|
+
reflection = association_data.direct_reflection
|
|
38
|
+
|
|
39
|
+
if association_ignored?(name)
|
|
40
|
+
new_associated = nil
|
|
41
|
+
else
|
|
42
|
+
# Load the record associated with the old model
|
|
43
|
+
associated = node.model.public_send(reflection.name)
|
|
44
|
+
|
|
45
|
+
if associated.nil?
|
|
46
|
+
new_associated = nil
|
|
47
|
+
elsif association_data.shared? && !association_data.through?
|
|
48
|
+
# simply attach the associated target to the new model
|
|
49
|
+
new_associated = associated
|
|
50
|
+
else
|
|
51
|
+
# Otherwise descend into the child, and attach the result
|
|
52
|
+
vm_class =
|
|
53
|
+
case
|
|
54
|
+
when association_data.through?
|
|
55
|
+
# descend into the synthetic join table viewmodel
|
|
56
|
+
association_data.direct_viewmodel
|
|
57
|
+
when association_data.collection?
|
|
58
|
+
association_data.viewmodel_class
|
|
59
|
+
else
|
|
60
|
+
association_data.viewmodel_class_for_model!(associated.class)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
new_associated =
|
|
64
|
+
if ViewModel::Utils.array_like?(associated)
|
|
65
|
+
associated.map { |m| clone(vm_class.new(m)) }.compact
|
|
66
|
+
else
|
|
67
|
+
clone(vm_class.new(associated))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
new_association = new_model.association(reflection.name)
|
|
73
|
+
new_association.writer(new_associated)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if end_visit && respond_to?(end_visit, true)
|
|
77
|
+
self.send(end_visit, node, new_model)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
post_visit(node, new_model)
|
|
81
|
+
|
|
82
|
+
new_model
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def pre_visit(node, new_model)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def post_visit(node, new_model)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def reset_state!
|
|
94
|
+
@ignored = false
|
|
95
|
+
@ignored_associations = Set.new
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def ignore!
|
|
99
|
+
@ignored = true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def ignore_association!(name)
|
|
103
|
+
@ignored_associations.add(name.to_s)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def ignored?
|
|
107
|
+
@ignored
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def association_ignored?(name)
|
|
111
|
+
@ignored_associations.include?(name.to_s)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'view_model/active_record/nested_controller_base'
|
|
4
|
+
|
|
5
|
+
# Controller mixin for accessing a root ViewModel which can be accessed in a
|
|
6
|
+
# collection by a parent model. Enabled by calling `nested_in :parent, as:
|
|
7
|
+
# :children` on the viewmodel controller
|
|
8
|
+
|
|
9
|
+
# Contributes the following routes:
|
|
10
|
+
# PUT /parents/:parent_id/children #append -- deserialize (possibly existing) children and append to collection
|
|
11
|
+
# POST /parents/:parent_id/children #replace -- deserialize (possibly existing) children, replacing existing collection
|
|
12
|
+
# GET /parents/:parent_id/children #index_associated -- list collection
|
|
13
|
+
# DELETE /parents/:parent_id/children/:child_id #disassociate -- delete relationship between parent/child (possibly garbage-collecting child)
|
|
14
|
+
# DELETE /parents/:parent_id/children #disassociate_all -- delete relationship from parent to all children
|
|
15
|
+
|
|
16
|
+
## Inherits the following routes to manipulate children directly:
|
|
17
|
+
# POST /children #create -- create or update without parent
|
|
18
|
+
# GET /children #index -- list all child models regardless of parent
|
|
19
|
+
# GET /children/:id #show
|
|
20
|
+
# DELETE /children/:id #destroy
|
|
21
|
+
module ViewModel::ActiveRecord::CollectionNestedController
|
|
22
|
+
extend ActiveSupport::Concern
|
|
23
|
+
include ViewModel::ActiveRecord::NestedControllerBase
|
|
24
|
+
|
|
25
|
+
def index_associated(scope: nil, serialize_context: new_serialize_context)
|
|
26
|
+
show_association(scope: scope, serialize_context: serialize_context)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Deserialize items of the associated type and associate them with the owner,
|
|
30
|
+
# replacing previously associated items.
|
|
31
|
+
def replace(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
|
32
|
+
write_association(serialize_context: serialize_context, deserialize_context: deserialize_context)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def disassociate_all(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
|
36
|
+
destroy_association(true, serialize_context: serialize_context, deserialize_context: deserialize_context)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Deserialize items of the associated type and append them to the owner's
|
|
40
|
+
# collection.
|
|
41
|
+
def append(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
|
42
|
+
assoc_view = nil
|
|
43
|
+
pre_rendered = owner_viewmodel.transaction do
|
|
44
|
+
update_hash, refs = parse_viewmodel_updates
|
|
45
|
+
|
|
46
|
+
before = parse_relative_position(:before)
|
|
47
|
+
after = parse_relative_position(:after)
|
|
48
|
+
|
|
49
|
+
if before && after
|
|
50
|
+
raise ViewModel::DeserializationError::InvalidSyntax.new("Can not provide both `before` and `after` anchors for a collection append")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, serialize_context: serialize_context)
|
|
54
|
+
|
|
55
|
+
assoc_view = owner_view.append_associated(association_name,
|
|
56
|
+
update_hash,
|
|
57
|
+
references: refs,
|
|
58
|
+
before: before,
|
|
59
|
+
after: after,
|
|
60
|
+
deserialize_context: deserialize_context)
|
|
61
|
+
ViewModel::Callbacks.wrap_serialize(owner_view, context: serialize_context) do
|
|
62
|
+
child_context = owner_view.context_for_child(association_name, context: serialize_context)
|
|
63
|
+
ViewModel.preload_for_serialization(assoc_view, serialize_context: child_context)
|
|
64
|
+
prerender_viewmodel(assoc_view, serialize_context: child_context)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
render_json_string(pre_rendered)
|
|
68
|
+
assoc_view
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def disassociate(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
|
72
|
+
owner_viewmodel.transaction do
|
|
73
|
+
owner_view = owner_viewmodel.find(owner_viewmodel_id, eager_include: false, serialize_context: serialize_context)
|
|
74
|
+
owner_view.delete_associated(association_name, viewmodel_id, type: viewmodel_class, deserialize_context: deserialize_context)
|
|
75
|
+
render_viewmodel(nil)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def parse_relative_position(name)
|
|
82
|
+
id = parse_uuid_param(name, default: nil)
|
|
83
|
+
|
|
84
|
+
if id
|
|
85
|
+
if association_data.polymorphic?
|
|
86
|
+
type_name = parse_param("#{name}_type")
|
|
87
|
+
type = association_data.viewmodel_class_for_name(type_name)
|
|
88
|
+
if type.nil?
|
|
89
|
+
raise ViewModel::DeserializationError::InvalidAssociationType.new(association_data.association_name.to_s, type_name)
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
type = owner_viewmodel.viewmodel_class
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
ViewModel::Reference.new(type, id)
|
|
96
|
+
else
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'view_model/active_record/controller_base'
|
|
4
|
+
require 'view_model/active_record/collection_nested_controller'
|
|
5
|
+
require 'view_model/active_record/singular_nested_controller'
|
|
6
|
+
|
|
7
|
+
# Controller for accessing an ViewModel::ActiveRecord
|
|
8
|
+
# Provides for the following routes:
|
|
9
|
+
# POST /models #create -- create or update one or more models
|
|
10
|
+
# GET /models #index
|
|
11
|
+
# GET /models/:id #show
|
|
12
|
+
# DELETE /models/:id #destroy
|
|
13
|
+
|
|
14
|
+
module ViewModel::ActiveRecord::Controller
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
include ViewModel::ActiveRecord::ControllerBase
|
|
17
|
+
include ViewModel::ActiveRecord::CollectionNestedController
|
|
18
|
+
include ViewModel::ActiveRecord::SingularNestedController
|
|
19
|
+
|
|
20
|
+
def show(scope: nil, viewmodel_class: self.viewmodel_class, serialize_context: new_serialize_context(viewmodel_class: viewmodel_class))
|
|
21
|
+
view = nil
|
|
22
|
+
pre_rendered = viewmodel_class.transaction do
|
|
23
|
+
view = viewmodel_class.find(viewmodel_id, scope: scope, serialize_context: serialize_context)
|
|
24
|
+
view = yield(view) if block_given?
|
|
25
|
+
prerender_viewmodel(view, serialize_context: serialize_context)
|
|
26
|
+
end
|
|
27
|
+
render_json_string(pre_rendered)
|
|
28
|
+
view
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def index(scope: nil, viewmodel_class: self.viewmodel_class, serialize_context: new_serialize_context(viewmodel_class: viewmodel_class))
|
|
32
|
+
views = nil
|
|
33
|
+
pre_rendered = viewmodel_class.transaction do
|
|
34
|
+
views = viewmodel_class.load(scope: scope, serialize_context: serialize_context)
|
|
35
|
+
views = yield(views) if block_given?
|
|
36
|
+
prerender_viewmodel(views, serialize_context: serialize_context)
|
|
37
|
+
end
|
|
38
|
+
render_json_string(pre_rendered)
|
|
39
|
+
views
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def create(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
|
43
|
+
update_hash, refs = parse_viewmodel_updates
|
|
44
|
+
|
|
45
|
+
view = nil
|
|
46
|
+
pre_rendered = viewmodel_class.transaction do
|
|
47
|
+
view = viewmodel_class.deserialize_from_view(update_hash, references: refs, deserialize_context: deserialize_context)
|
|
48
|
+
|
|
49
|
+
serialize_context.add_includes(deserialize_context.updated_associations)
|
|
50
|
+
|
|
51
|
+
view = yield(view) if block_given?
|
|
52
|
+
|
|
53
|
+
ViewModel.preload_for_serialization(view, serialize_context: serialize_context)
|
|
54
|
+
prerender_viewmodel(view, serialize_context: serialize_context)
|
|
55
|
+
end
|
|
56
|
+
render_json_string(pre_rendered)
|
|
57
|
+
view
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def destroy(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
|
|
61
|
+
viewmodel_class.transaction do
|
|
62
|
+
view = viewmodel_class.find(viewmodel_id, eager_include: false, serialize_context: serialize_context)
|
|
63
|
+
view.destroy!(deserialize_context: deserialize_context)
|
|
64
|
+
end
|
|
65
|
+
render_viewmodel(nil)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
included do
|
|
69
|
+
etag { self.viewmodel_class.deep_schema_version }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def viewmodel_id
|
|
75
|
+
parse_param(:id)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'iknow_params'
|
|
4
|
+
|
|
5
|
+
module ViewModel::ActiveRecord::ControllerBase
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
include IknowParams::Parser
|
|
8
|
+
include ViewModel::Controller
|
|
9
|
+
|
|
10
|
+
protected
|
|
11
|
+
|
|
12
|
+
# Override (pre)render_viewmodel to use the default serialization context from this controller.
|
|
13
|
+
def render_viewmodel(viewmodel, serialize_context: new_serialize_context, **args)
|
|
14
|
+
super(viewmodel, serialize_context: serialize_context, **args)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def prerender_viewmodel(viewmodel, serialize_context: new_serialize_context, **args)
|
|
18
|
+
super(viewmodel, serialize_context: serialize_context, **args)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def new_deserialize_context(viewmodel_class: self.viewmodel_class, access_control: self.access_control.new, **args)
|
|
22
|
+
viewmodel_class.new_deserialize_context(access_control: access_control, **args)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def new_serialize_context(viewmodel_class: self.viewmodel_class, access_control: self.access_control.new, **args)
|
|
26
|
+
viewmodel_class.new_serialize_context(access_control: access_control, **args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class_methods do
|
|
30
|
+
def viewmodel_class
|
|
31
|
+
unless instance_variable_defined?(:@viewmodel_class)
|
|
32
|
+
# try to autodetect the viewmodel based on our name
|
|
33
|
+
if (match = /(.*)Controller$/.match(self.name))
|
|
34
|
+
self.viewmodel_name = match[1].singularize
|
|
35
|
+
else
|
|
36
|
+
raise ArgumentError.new("Could not auto-determine ViewModel from Controller name '#{self.name}'") if match.nil?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
@viewmodel_class
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def access_control
|
|
43
|
+
unless instance_variable_defined?(:@access_control)
|
|
44
|
+
raise ArgumentError.new("AccessControl instance not set for Controller '#{self.name}'")
|
|
45
|
+
end
|
|
46
|
+
@access_control
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def model_class
|
|
50
|
+
viewmodel_class.model_class
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
protected
|
|
54
|
+
|
|
55
|
+
def viewmodel_name=(name)
|
|
56
|
+
self.viewmodel_class = ViewModel::Registry.for_view_name(name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def viewmodel_class=(type)
|
|
60
|
+
if instance_variable_defined?(:@viewmodel_class)
|
|
61
|
+
raise ArgumentError.new("ViewModel class for Controller '#{self.name}' already set")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
unless type < ViewModel
|
|
65
|
+
raise ArgumentError.new("'#{type.inspect}' is not a valid ViewModel")
|
|
66
|
+
end
|
|
67
|
+
@viewmodel_class = type
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def access_control=(access_control)
|
|
71
|
+
if instance_variable_defined?(:@access_control)
|
|
72
|
+
raise ArgumentError.new("AccessControl class for Controller '#{self.name}' already set")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
unless access_control.is_a?(Class) && access_control < ViewModel::AccessControl
|
|
76
|
+
raise ArgumentError.new("'#{access_control.inspect}' is not a valid AccessControl")
|
|
77
|
+
end
|
|
78
|
+
@access_control = access_control
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def viewmodel_class
|
|
83
|
+
self.class.viewmodel_class
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def model_class
|
|
87
|
+
self.class.model_class
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def access_control
|
|
91
|
+
self.class.access_control
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
module ActionDispatch
|
|
96
|
+
module Routing
|
|
97
|
+
class Mapper
|
|
98
|
+
module Resources
|
|
99
|
+
def arvm_resources(resource_name, options = {}, &block)
|
|
100
|
+
except = options.delete(:except) { [] }
|
|
101
|
+
add_shallow_routes = options.delete(:add_shallow_routes) { true }
|
|
102
|
+
|
|
103
|
+
nested = shallow_nesting_depth > 0
|
|
104
|
+
|
|
105
|
+
only_routes = []
|
|
106
|
+
only_routes += [:create] unless nested
|
|
107
|
+
only_routes += [:show, :destroy] if add_shallow_routes
|
|
108
|
+
only_routes -= except
|
|
109
|
+
|
|
110
|
+
resources resource_name, shallow: true, only: only_routes, **options do
|
|
111
|
+
instance_eval(&block) if block_given?
|
|
112
|
+
|
|
113
|
+
if nested
|
|
114
|
+
# Nested controllers also get :append and :disassociate, and alias a top level create.
|
|
115
|
+
collection do
|
|
116
|
+
name_route = { as: '' } # Only one route may take the name
|
|
117
|
+
get('', action: :index_associated, **name_route.extract!(:as)) unless except.include?(:index)
|
|
118
|
+
put('', action: :append, **name_route.extract!(:as)) unless except.include?(:append)
|
|
119
|
+
post('', action: :replace, **name_route.extract!(:as)) unless except.include?(:replace)
|
|
120
|
+
delete('', action: :disassociate_all, **name_route.extract!(:as)) unless except.include?(:disassociate_all)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
scope shallow: false do
|
|
124
|
+
member do
|
|
125
|
+
delete '', action: :disassociate, as: '' unless except.include?(:disassociate)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Add top level `create` route to manipulate existing viewmodels
|
|
130
|
+
# without providing parent context
|
|
131
|
+
shallow_scope do
|
|
132
|
+
collection do
|
|
133
|
+
name_route = { as: '' } # Only one route may take the name
|
|
134
|
+
post('', action: :create, **name_route.extract!(:as)) unless except.include?(:create) || !add_shallow_routes
|
|
135
|
+
get('', action: :index, **name_route.extract!(:as)) unless except.include?(:index) || !add_shallow_routes
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
else
|
|
139
|
+
collection do
|
|
140
|
+
get('', action: :index, as: '') unless except.include?(:index)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def arvm_resource(resource_name, options = {}, &block)
|
|
147
|
+
except = options.delete(:except) { [] }
|
|
148
|
+
add_shallow_routes = options.delete(:add_shallow_routes) { true }
|
|
149
|
+
|
|
150
|
+
only_routes = []
|
|
151
|
+
is_shallow = false
|
|
152
|
+
resource resource_name, shallow: true, only: only_routes, **options do
|
|
153
|
+
is_shallow = shallow_nesting_depth > 1
|
|
154
|
+
instance_eval(&block) if block_given?
|
|
155
|
+
|
|
156
|
+
name_route = { as: '' } # Only one route may take the name
|
|
157
|
+
|
|
158
|
+
if is_shallow
|
|
159
|
+
post('', action: :create_associated, **name_route.extract!(:as)) unless except.include?(:create)
|
|
160
|
+
get('', action: :show_associated, **name_route.extract!(:as)) unless except.include?(:show)
|
|
161
|
+
delete('', action: :destroy_associated, **name_route.extract!(:as)) unless except.include?(:destroy)
|
|
162
|
+
else
|
|
163
|
+
post('', action: :create, **name_route.extract!(:as)) unless except.include?(:create)
|
|
164
|
+
get('', action: :show, **name_route.extract!(:as)) unless except.include?(:show)
|
|
165
|
+
delete('', action: :destroy, **name_route.extract!(:as)) unless except.include?(:destroy)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# singularly nested resources provide collection accessors at the top level
|
|
170
|
+
if is_shallow && add_shallow_routes
|
|
171
|
+
resources resource_name.to_s.pluralize, shallow: true, only: [:show, :destroy] - except do
|
|
172
|
+
shallow_scope do
|
|
173
|
+
collection do
|
|
174
|
+
name_route = { as: '' } # Only one route may take the name
|
|
175
|
+
post('', action: :create, **name_route.extract!(:as)) unless except.include?(:create)
|
|
176
|
+
get('', action: :index, **name_route.extract!(:as)) unless except.include?(:index)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|