iknow_view_models 2.8.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|