iknow_view_models 2.8.4

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