wor-simple_crud 0.2.1 → 0.3.0

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/Gemfile +1 -0
  4. data/README.md +157 -2
  5. data/lib/simple_crud/authorization/adapter.rb +7 -0
  6. data/lib/simple_crud/authorization/pundit_adapter.rb +6 -0
  7. data/lib/simple_crud/config.rb +6 -1
  8. data/lib/simple_crud/controller_helpers.rb +119 -0
  9. data/lib/simple_crud/pagination/adapter.rb +7 -1
  10. data/lib/simple_crud/pagination/kaminari_adapter.rb +23 -2
  11. data/lib/simple_crud/pagination/pagy_adapter.rb +7 -2
  12. data/lib/simple_crud/pagination/will_paginate_adapter.rb +7 -3
  13. data/lib/simple_crud/pagination/wor_paginate_adapter.rb +7 -2
  14. data/lib/simple_crud/rspec/config.rb +61 -0
  15. data/lib/simple_crud/rspec/helpers.rb +147 -0
  16. data/lib/simple_crud/rspec.rb +34 -0
  17. data/lib/simple_crud/simple_crud_controller.rb +51 -36
  18. data/lib/simple_crud/version.rb +1 -1
  19. data/lib/spec/shared_contexts/authenticate_user.rb +1 -7
  20. data/lib/spec/shared_examples/authorization_adapter_authorize.rb +11 -0
  21. data/lib/spec/shared_examples/simple_crud_for_create.rb +43 -11
  22. data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +18 -0
  23. data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +15 -0
  24. data/lib/spec/shared_examples/simple_crud_for_destroy.rb +19 -8
  25. data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +13 -0
  26. data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +24 -0
  27. data/lib/spec/shared_examples/simple_crud_for_index.rb +8 -23
  28. data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +14 -0
  29. data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +21 -0
  30. data/lib/spec/shared_examples/simple_crud_for_new.rb +29 -0
  31. data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +9 -0
  32. data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +13 -0
  33. data/lib/spec/shared_examples/simple_crud_for_show.rb +14 -10
  34. data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +11 -0
  35. data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +24 -0
  36. data/lib/spec/shared_examples/simple_crud_for_update.rb +55 -15
  37. data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +29 -0
  38. data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +13 -0
  39. data/lib/spec/shared_examples/simple_crud_renders_template.rb +8 -0
  40. data/lib/spec/shared_examples/simple_crud_when_not_authorized.rb +18 -0
  41. data/lib/spec/shared_examples/simple_crud_without_authenticated_user.rb +9 -0
  42. data/lib/spec/shared_examples/unauthorized_when_not_logged_in.rb +2 -2
  43. data/spec/block/dummy_models_controller_spec.rb +8 -0
  44. data/spec/block_create/dummy_models_controller_spec.rb +7 -0
  45. data/spec/block_destroy/dummy_models_controller_spec.rb +7 -0
  46. data/spec/block_new/dummy_models_controller_spec.rb +7 -0
  47. data/spec/block_show/dummy_models_controller_spec.rb +7 -0
  48. data/spec/built/dummy_models_controller_spec.rb +8 -0
  49. data/spec/dummy/app/controllers/block/dummy_models_controller.rb +17 -0
  50. data/spec/dummy/app/controllers/block_create/dummy_models_controller.rb +13 -0
  51. data/spec/dummy/app/controllers/block_destroy/dummy_models_controller.rb +9 -0
  52. data/spec/dummy/app/controllers/block_modes/base_controller.rb +11 -0
  53. data/spec/dummy/app/controllers/block_new/dummy_models_controller.rb +9 -0
  54. data/spec/dummy/app/controllers/block_show/dummy_models_controller.rb +9 -0
  55. data/spec/dummy/app/controllers/built/dummy_models_controller.rb +21 -0
  56. data/spec/dummy/app/controllers/dummy_models_controller.rb +2 -1
  57. data/spec/dummy/app/controllers/finder/dummy_models_controller.rb +22 -0
  58. data/spec/dummy/app/controllers/html/dummy_models_controller.rb +7 -0
  59. data/spec/dummy/app/controllers/html_create/dummy_models_controller.rb +7 -0
  60. data/spec/dummy/app/controllers/html_destroy/dummy_models_controller.rb +7 -0
  61. data/spec/dummy/app/controllers/html_modes/base_controller.rb +17 -0
  62. data/spec/dummy/app/controllers/html_new/dummy_models_controller.rb +7 -0
  63. data/spec/dummy/app/controllers/html_scoped/dummy_models_controller.rb +8 -0
  64. data/spec/dummy/app/controllers/html_show/dummy_models_controller.rb +7 -0
  65. data/spec/dummy/app/controllers/html_update/dummy_models_controller.rb +7 -0
  66. data/spec/dummy/app/controllers/invalid_status/dummy_models_controller.rb +13 -0
  67. data/spec/dummy/app/controllers/nested/dummy_models_controller.rb +21 -0
  68. data/spec/dummy/app/controllers/nested_route/dummy_models_controller.rb +26 -0
  69. data/spec/dummy/app/controllers/redirect/dummy_models_controller.rb +10 -0
  70. data/spec/dummy/app/controllers/redirect_auth/dummy_models_controller.rb +28 -0
  71. data/spec/dummy/app/controllers/scoped/dummy_models_controller.rb +16 -0
  72. data/spec/dummy/app/controllers/scoped_params/dummy_models_controller.rb +16 -0
  73. data/spec/dummy/app/controllers/strict/dummy_models_controller.rb +21 -0
  74. data/spec/dummy/app/models/dummy_model.rb +4 -0
  75. data/spec/dummy/app/models/dummy_model_policy.rb +4 -0
  76. data/spec/dummy/app/views/block/dummy_models/index.html.erb +3 -0
  77. data/spec/dummy/app/views/block_new/dummy_models/new.html.erb +1 -0
  78. data/spec/dummy/app/views/block_show/dummy_models/show.html.erb +1 -0
  79. data/spec/dummy/app/views/html/dummy_models/index.html.erb +3 -0
  80. data/spec/dummy/app/views/html_create/dummy_models/new.html.erb +1 -0
  81. data/spec/dummy/app/views/html_destroy/dummy_models/show.html.erb +1 -0
  82. data/spec/dummy/app/views/html_new/dummy_models/new.html.erb +1 -0
  83. data/spec/dummy/app/views/html_scoped/dummy_models/index.html.erb +3 -0
  84. data/spec/dummy/app/views/html_show/dummy_models/show.html.erb +1 -0
  85. data/spec/dummy/app/views/html_update/dummy_models/edit.html.erb +1 -0
  86. data/spec/dummy/app/views/invalid_status/dummy_models/new.html.erb +1 -0
  87. data/spec/dummy/app/views/layouts/application.html.erb +4 -0
  88. data/spec/dummy/app/views/redirect/dummy_models/new.html.erb +1 -0
  89. data/spec/dummy/config/routes.rb +90 -0
  90. data/spec/dummy/db/migrate/20260806000000_add_slug_to_dummy_models.rb +7 -0
  91. data/spec/dummy/db/migrate/20260807000000_add_classroom_slug_to_dummy_models.rb +7 -0
  92. data/spec/dummy/db/schema.rb +3 -1
  93. data/spec/dummy/spec/factories/dummy_models.rb +1 -0
  94. data/spec/dummy_controller_spec.rb +1 -0
  95. data/spec/dummy_model_policy_spec.rb +8 -16
  96. data/spec/finder/dummy_models_controller_spec.rb +12 -0
  97. data/spec/html/dummy_models_controller_spec.rb +7 -0
  98. data/spec/html_create/dummy_models_controller_spec.rb +7 -0
  99. data/spec/html_destroy/dummy_models_controller_spec.rb +7 -0
  100. data/spec/html_new/dummy_models_controller_spec.rb +7 -0
  101. data/spec/html_scoped/dummy_models_controller_spec.rb +7 -0
  102. data/spec/html_show/dummy_models_controller_spec.rb +7 -0
  103. data/spec/html_update/dummy_models_controller_spec.rb +7 -0
  104. data/spec/invalid_status/dummy_models_controller_spec.rb +10 -0
  105. data/spec/nested/dummy_models_controller_spec.rb +12 -0
  106. data/spec/nested_route/dummy_models_controller_spec.rb +19 -0
  107. data/spec/redirect/dummy_models_controller_spec.rb +23 -0
  108. data/spec/redirect_auth/dummy_models_controller_spec.rb +11 -0
  109. data/spec/scoped/dummy_models_controller_spec.rb +7 -0
  110. data/spec/scoped_params/dummy_models_controller_spec.rb +11 -0
  111. data/spec/simple_crud/authorization/action_policy_adapter_spec.rb +1 -9
  112. data/spec/simple_crud/authorization/can_can_can_adapter_spec.rb +1 -7
  113. data/spec/simple_crud/authorization/pundit_adapter_spec.rb +41 -0
  114. data/spec/simple_crud/controller_helpers_spec.rb +49 -0
  115. data/spec/simple_crud/pagination/adapters_spec.rb +37 -13
  116. data/spec/simple_crud/rspec_config_spec.rb +67 -0
  117. data/spec/simple_crud_configuration_spec.rb +19 -16
  118. data/spec/simple_crud_controller_spec.rb +4 -4
  119. data/spec/spec_helper.rb +1 -0
  120. data/spec/strict/dummy_models_controller_spec.rb +23 -0
  121. metadata +87 -4
  122. data/lib/spec/shared_examples/helpers.rb +0 -50
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ module RSpec
5
+ # Helpers available to the shared examples (and to apps that include
6
+ # them). Everything app-specific is delegated to SimpleCrud::RSpec::Config
7
+ # so it can be overridden without editing the examples.
8
+ module Helpers
9
+ def config
10
+ SimpleCrud::RSpec::Config.instance
11
+ end
12
+
13
+ def get_option(method, option)
14
+ described_class.instance_variable_get(:@simple_crud_metadata)[method][option]
15
+ end
16
+
17
+ # :unprocessable_entity was renamed :unprocessable_content in Rack 3.1.
18
+ # Resolve the current Rack's canonical symbol for 422.
19
+ def unprocessable_status
20
+ Rack::Utils::SYMBOL_TO_STATUS_CODE.invert[422]
21
+ end
22
+
23
+ %i[paginate authorize authenticate serializer html finder scope build raise_on_invalid].each do |option|
24
+ define_method("check_#{option}") { |method| get_option(method, option) }
25
+ end
26
+
27
+ %i[owner_association required_attribute required_error finder_key params_key invalid_status
28
+ unauthenticated_status assert_html_template].each do |setting|
29
+ define_method(setting) { resolve(config.public_send(setting)) }
30
+ end
31
+
32
+ def model_class
33
+ described_class.to_s.split('::')
34
+ .last.sub('Controller', '').singularize.underscore
35
+ end
36
+
37
+ def model_class_object
38
+ model_class.classify.constantize
39
+ end
40
+
41
+ def model
42
+ @model ||= create_record(model_class, model_attributes)
43
+ end
44
+
45
+ def model_attributes
46
+ association = resolve(config.owner_association)
47
+ association ? { association => current_user } : {}
48
+ end
49
+
50
+ def create_record(klass, attributes)
51
+ instance_exec(klass, attributes, &config.create_record)
52
+ end
53
+
54
+ def create_records(klass, count, attributes)
55
+ instance_exec(klass, count, attributes, &config.create_records)
56
+ end
57
+
58
+ def params_for(klass)
59
+ instance_exec(klass, &config.params_for)
60
+ end
61
+
62
+ def model_params
63
+ @model_params ||= params_for(model_class)
64
+ end
65
+
66
+ def current_user
67
+ @current_user ||= instance_exec(&config.current_user)
68
+ end
69
+
70
+ def authenticate_request
71
+ instance_exec(&config.authenticate)
72
+ end
73
+
74
+ def owner_foreign_key
75
+ :"#{owner_association}_id"
76
+ end
77
+
78
+ # The owner association attributed to current_user, if the model has one.
79
+ def owner_params
80
+ owner_association ? { owner_foreign_key => current_user.id } : {}
81
+ end
82
+
83
+ # Extra params (e.g. a parent slug) added to every request, for nested
84
+ # resources like /classrooms/:classroom_slug/assignments.
85
+ def route_params
86
+ resolve(config.route_params)
87
+ end
88
+
89
+ def with_route_params(attrs)
90
+ route_params.merge(attrs)
91
+ end
92
+
93
+ # The params that identify a record for the given action, using the
94
+ # configured finder_key when the action has a custom finder.
95
+ def record_param(action, record, not_found: false)
96
+ key = check_finder(action) ? finder_key : :id
97
+ value = not_found ? "nonexistent-#{key}" : record.public_send(key)
98
+ { key => value }
99
+ end
100
+
101
+ # Wraps a request body under the model's strong-params key when
102
+ # params_key is configured (nested strong params), else passes it flat.
103
+ def body_params(attrs)
104
+ key = params_key
105
+ key ? { key => attrs } : attrs
106
+ end
107
+
108
+ def policy_class_object
109
+ instance_exec(model_class_object, &config.policy_class)
110
+ end
111
+
112
+ def model_serializer
113
+ defined?(serializer) ? serializer : instance_exec(model, &config.serializer_class)
114
+ end
115
+
116
+ def make_policies_fail(method)
117
+ allow(policy_class_object).to receive(:new)
118
+ .and_return(instance_double(policy_class_object, "#{method}?" => false))
119
+ end
120
+
121
+ def make_policies_succeed(method)
122
+ allow(policy_class_object).to receive(:new)
123
+ .and_return(instance_double(policy_class_object, "#{method}?" => true))
124
+ end
125
+
126
+ def request_format(action)
127
+ check_html(action) ? :html : :json
128
+ end
129
+
130
+ def resolve(value)
131
+ value.is_a?(Proc) ? instance_exec(&value) : value
132
+ end
133
+
134
+ # Runs the block with a config setting temporarily overridden, restoring
135
+ # the original value afterwards (even if the block raises). Useful when
136
+ # a controller's spec needs a different setting than the app-wide default.
137
+ def with_config_override(setting, value)
138
+ config_instance = SimpleCrud::RSpec::Config.instance
139
+ original = config_instance.public_send(setting)
140
+ SimpleCrud::RSpec.configure { |c| c.public_send("#{setting}=", value) }
141
+ yield
142
+ ensure
143
+ SimpleCrud::RSpec.configure { |c| c.public_send("#{setting}=", original) }
144
+ end
145
+ end
146
+ end
147
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'rspec/config'
4
+ require_relative 'rspec/helpers'
3
5
  require_relative '../spec/response_helper'
4
6
  require_relative '../spec/shared_contexts/authenticate_user'
5
7
  require_relative '../spec/shared_examples/simple_crud_for_update'
@@ -7,5 +9,37 @@ require_relative '../spec/shared_examples/simple_crud_for_show'
7
9
  require_relative '../spec/shared_examples/simple_crud_for_index'
8
10
  require_relative '../spec/shared_examples/simple_crud_for_destroy'
9
11
  require_relative '../spec/shared_examples/simple_crud_for_create'
12
+ require_relative '../spec/shared_examples/simple_crud_for_new'
10
13
  require_relative '../spec/shared_examples/unauthorized_when_not_logged_in'
14
+ require_relative '../spec/shared_examples/simple_crud_renders_template'
15
+ require_relative '../spec/shared_examples/simple_crud_without_authenticated_user'
16
+ require_relative '../spec/shared_examples/simple_crud_when_not_authorized'
17
+ require_relative '../spec/shared_examples/simple_crud_for_index_with_block'
18
+ require_relative '../spec/shared_examples/simple_crud_for_index_with_scope'
19
+ require_relative '../spec/shared_examples/simple_crud_for_show_with_block'
20
+ require_relative '../spec/shared_examples/simple_crud_for_new_with_block'
21
+ require_relative '../spec/shared_examples/simple_crud_for_create_with_block'
22
+ require_relative '../spec/shared_examples/simple_crud_for_destroy_with_block'
23
+ require_relative '../spec/shared_examples/simple_crud_for_show_with_finder'
24
+ require_relative '../spec/shared_examples/simple_crud_for_update_with_finder'
25
+ require_relative '../spec/shared_examples/simple_crud_for_destroy_with_finder'
26
+ require_relative '../spec/shared_examples/simple_crud_not_found_with_finder'
27
+ require_relative '../spec/shared_examples/simple_crud_for_new_with_build'
28
+ require_relative '../spec/shared_examples/simple_crud_for_create_with_build'
29
+ require_relative '../spec/shared_examples/authorization_adapter_authorize'
11
30
  require_relative '../spec/matchers/have_been_serialized_with'
31
+
32
+ module SimpleCrud
33
+ # Opt-in wiring for the shared examples: see .install!
34
+ module RSpec
35
+ # Opt-in wiring: call from your spec support after requiring this file.
36
+ # extend wires definition-time helpers (check_html...), include runtime ones (model...).
37
+ def self.install!
38
+ ::RSpec.configure do |config|
39
+ config.extend Helpers
40
+ config.include Helpers
41
+ config.include Response::JSONParser, type: :controller
42
+ end
43
+ end
44
+ end
45
+ end
@@ -2,9 +2,12 @@
2
2
 
3
3
  require 'active_support/all'
4
4
  require_relative 'config'
5
+ require_relative 'controller_helpers'
5
6
 
6
7
  # Extended onto a controller for CRUD actions.
7
8
  module SimpleCrudController
9
+ extend SimpleCrud::ControllerHelpers
10
+
8
11
  cattr_accessor :params, :permitted
9
12
 
10
13
  # Possible options:
@@ -12,21 +15,30 @@ module SimpleCrudController
12
15
  ### paginate: paginate the list via Config.pagination_adapter (wor-paginate by default)
13
16
  ### authenticate: use devise to authenticate
14
17
  ### serializer: use a particular serializer (both each_serializer and serializer)
15
- def simple_crud_for(method, parameters = {})
18
+ ### html: render the action's ERB template instead of JSON (index/show/new/create/update/destroy)
19
+ ### finder: custom record lookup (Proc/lambda or Symbol) for :show/:update/:destroy
20
+ ### scope: custom index scope (Proc/lambda taking current_user and optional params), overrides policy_scope
21
+ ### build: custom record builder (Proc/lambda) for :new/:create, invoked with the controller as self
22
+ ### raise_on_invalid: use strict create!/update! semantics instead of returning 422
23
+ ### A block given to simple_crud_for renders explicitly: it receives the records for :index,
24
+ ### the record for :show/:new, or the record and a saved flag for :create/:update/:destroy
25
+ def simple_crud_for(method, parameters = {}, &block)
26
+ parameters[:html] = true if block && !parameters.key?(:html)
16
27
  parameters = parameters_with_defaults(parameters)
17
28
  klass = simple_crud_controller_model
18
29
  check_valid_method(method)
19
30
  check_policies(parameters)
20
31
  check_serializer(parameters)
21
- define_method(method, send("crud_lambda_for_#{method}", klass, parameters))
32
+ define_method(method, send("crud_lambda_for_#{method}", klass, parameters, &block))
22
33
  write_metadata(method, parameters)
23
34
  end
24
35
 
25
36
  def parameters_with_defaults(parameters)
26
- defaults = { authorize: true, paginate: true, authenticate: true, serializer: nil }
27
- defaults.each do |key, value|
28
- parameters[key] = value unless parameters.key?(key)
29
- end
37
+ defaults = {
38
+ authorize: true, paginate: true, authenticate: true, serializer: nil,
39
+ html: false, finder: nil, scope: nil, build: nil, raise_on_invalid: false
40
+ }
41
+ defaults.each { |key, value| parameters[key] = value unless parameters.key?(key) }
30
42
  parameters
31
43
  end
32
44
 
@@ -35,65 +47,68 @@ module SimpleCrudController
35
47
  @simple_crud_metadata[method] = parameters
36
48
  end
37
49
 
38
- def self.maybe_authorize(controller, record, parameters)
39
- return unless parameters[:authorize] && parameters[:authenticate]
40
-
41
- SimpleCrud::Config.authorization_adapter.authorize(controller, record)
42
- end
43
-
44
- def crud_lambda_for_show(klass, parameters = {})
50
+ def crud_lambda_for_show(klass, parameters = {}, &block)
45
51
  lambda do
46
52
  authenticate_user! if parameters[:authenticate]
47
- requested = klass.find(params[:id])
53
+ requested = SimpleCrudController.find_record(klass, self, parameters)
48
54
 
49
55
  options = {}.merge(serializer: parameters[:serializer]).compact
50
56
  SimpleCrudController.maybe_authorize(self, requested, parameters)
51
- render({ json: requested }.merge(options))
57
+ SimpleCrudController.render_show(self, requested, options, parameters, &block)
52
58
  end
53
59
  end
54
60
 
55
- def crud_lambda_for_index(klass, parameters = {})
61
+ def crud_lambda_for_new(klass, parameters = {}, &block)
56
62
  lambda do
57
63
  authenticate_user! if parameters[:authenticate]
58
- SimpleCrudController.maybe_authorize(self, klass.new, parameters)
59
- options = {}.merge(each_serializer: parameters[:serializer]).compact
60
- SimpleCrudController.render_index(self, klass, options, parameters)
64
+ record = SimpleCrudController.build_record(self, klass, parameters)
65
+ SimpleCrudController.maybe_authorize(self, record, parameters)
66
+ SimpleCrudController.render_new(self, record, parameters, &block)
61
67
  end
62
68
  end
63
69
 
64
- def self.render_index(controller, klass, options, parameters)
65
- if parameters[:paginate]
66
- SimpleCrud::Config.pagination_adapter.paginate(controller, klass, options)
67
- else
68
- controller.render({ json: klass.all }.merge(options))
70
+ def crud_lambda_for_index(klass, parameters = {}, &block)
71
+ lambda do
72
+ authenticate_user! if parameters[:authenticate]
73
+ SimpleCrudController.maybe_authorize(self, klass.new, parameters)
74
+ options = {}.merge(each_serializer: parameters[:serializer]).compact
75
+ SimpleCrudController.render_index(self, klass, options, parameters, &block)
69
76
  end
70
77
  end
71
78
 
72
- def crud_lambda_for_create(klass, parameters = {})
79
+ def crud_lambda_for_create(klass, parameters = {}, &block)
73
80
  lambda do
74
81
  authenticate_user! if parameters[:authenticate]
75
82
  permitted_params = send("#{self.class.simple_crud_controller_model.to_s.underscore}_params")
76
- SimpleCrudController.maybe_authorize(self, klass.new(permitted_params), parameters)
77
- render json: klass.create!(permitted_params), status: :created
83
+ record = SimpleCrudController.build_record(self, klass, parameters)
84
+ record.assign_attributes(permitted_params)
85
+ SimpleCrudController.maybe_authorize(self, record, parameters)
86
+ persist = ->(bang:) { bang ? record.save! : record.save }
87
+ options = { status: :created, failure_template: :new }
88
+ SimpleCrudController.save_and_render(self, record, parameters, options, persist, &block)
78
89
  end
79
90
  end
80
91
 
81
- def crud_lambda_for_update(klass, parameters = {})
92
+ def crud_lambda_for_update(klass, parameters = {}, &block)
82
93
  lambda do
83
94
  authenticate_user! if parameters[:authenticate]
84
- requested = klass.find(params[:id])
95
+ requested = SimpleCrudController.find_record(klass, self, parameters)
85
96
  SimpleCrudController.maybe_authorize(self, requested, parameters)
86
97
  permitted_params = send("#{self.class.simple_crud_controller_model.to_s.underscore}_params")
87
- render json: requested.update!(permitted_params)
98
+ persist = ->(bang:) { bang ? requested.update!(permitted_params) : requested.update(permitted_params) }
99
+ options = { status: :ok, failure_template: :edit }
100
+ SimpleCrudController.save_and_render(self, requested, parameters, options, persist, &block)
88
101
  end
89
102
  end
90
103
 
91
- def crud_lambda_for_destroy(klass, parameters = {})
104
+ def crud_lambda_for_destroy(klass, parameters = {}, &block)
92
105
  lambda do
93
106
  authenticate_user! if parameters[:authenticate]
94
- requested = klass.find(params[:id])
107
+ requested = SimpleCrudController.find_record(klass, self, parameters)
95
108
  SimpleCrudController.maybe_authorize(self, requested, parameters)
96
- render json: klass.find(params[:id]).destroy
109
+ options = { status: :ok, failure_template: :show, redirect: parameters[:redirect] || klass }
110
+ persist = ->(bang:) { bang ? requested.destroy! : requested.destroy }
111
+ SimpleCrudController.persist_and_render(self, requested, parameters, options, persist, &block)
97
112
  end
98
113
  end
99
114
 
@@ -102,7 +117,7 @@ module SimpleCrudController
102
117
  end
103
118
 
104
119
  def check_valid_method(method)
105
- throw 'invalid method' unless %i[show index create update destroy].include? method
120
+ raise ArgumentError, 'invalid method' unless %i[show index create update destroy new].include? method
106
121
  end
107
122
 
108
123
  def check_policies(parameters)
@@ -111,7 +126,7 @@ module SimpleCrudController
111
126
  model = simple_crud_controller_model
112
127
  return if SimpleCrud::Config.authorization_adapter.policy_defined?(model)
113
128
 
114
- throw "no authorization policy configured for #{model}"
129
+ raise ArgumentError, "no authorization policy configured for #{model}"
115
130
  end
116
131
 
117
132
  def check_serializer(parameters)
@@ -120,6 +135,6 @@ module SimpleCrudController
120
135
  serializer_name = parameters[:serializer].to_s
121
136
  return if Kernel.const_defined?(serializer_name)
122
137
 
123
- throw "create a valid serializer with name #{serializer_name}"
138
+ raise ArgumentError, "create a valid serializer with name #{serializer_name}"
124
139
  end
125
140
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCrud
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.shared_context 'with authenticated user' do
4
- let(:current_user) { create(:user) }
5
-
6
- before do
7
- headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
8
- auth_headers = Devise::JWT::TestHelpers.auth_headers(headers, current_user)
9
- request.headers.merge!(auth_headers)
10
- end
4
+ before { authenticate_request }
11
5
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'authorization adapter #authorize' do |record_class, denied_error|
4
+ it 'passes for a record the adapter allows' do
5
+ expect { adapter.authorize(controller, record_class.new(1)) }.not_to raise_error
6
+ end
7
+
8
+ it 'raises for a record the adapter denies' do
9
+ expect { adapter.authorize(controller, record_class.new(2)) }.to raise_error(denied_error)
10
+ end
11
+ end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'helpers'
4
-
5
3
  shared_examples 'simple crud for create' do
6
4
  describe 'POST #create' do
7
5
  context 'without authenticated user' do
8
- subject!(:req) { post :create, params: attributes_for(model_class) }
6
+ subject!(:req) do
7
+ post :create, params: with_route_params(body_params(params_for(model_class))), format: request_format(:create)
8
+ end
9
9
 
10
10
  include_examples 'unauthorized when not logged in' if check_authenticate(:create)
11
11
  end
@@ -16,7 +16,7 @@ shared_examples 'simple crud for create' do
16
16
 
17
17
  before do
18
18
  make_policies_fail(:create)
19
- post :create, params: model_params
19
+ post :create, params: with_route_params(body_params(model_params)), format: request_format(:create)
20
20
  end
21
21
 
22
22
  it 'fails with forbidden' do
@@ -27,22 +27,54 @@ shared_examples 'simple crud for create' do
27
27
 
28
28
  context 'when successfully creating an article' do
29
29
  include_context 'with authenticated user' if check_authenticate(:create)
30
- let(:create_params) { model_params.merge(user_id: current_user.id) }
30
+ let(:create_params) { model_params.merge(owner_params) }
31
31
 
32
32
  before do
33
- post :create, params: create_params
33
+ post :create, params: with_route_params(body_params(create_params)), format: request_format(:create)
34
34
  end
35
35
 
36
- it 'response with 200 status code' do
37
- expect(response).to have_http_status(:created)
36
+ if check_html(:create)
37
+ it 'redirects to the created record' do
38
+ expect(response).to have_http_status(:found)
39
+ end
40
+ else
41
+ it 'response with 201 status code' do
42
+ expect(response).to have_http_status(:created)
43
+ end
44
+
45
+ it 'creates an article with valid attributes' do
46
+ expect(model_class_object.last).to have_attributes(create_params)
47
+ end
38
48
  end
39
49
 
40
- it 'creates an article' do
50
+ it 'creates the record' do
41
51
  expect(model_class_object.count).to be 1
42
52
  end
53
+ end
43
54
 
44
- it 'creates an article with valid attributes' do
45
- expect(model_class_object.last).to have_attributes(create_params)
55
+ unless check_raise_on_invalid(:create)
56
+ context 'when creating with invalid attributes' do
57
+ include_context 'with authenticated user' if check_authenticate(:create)
58
+
59
+ before do
60
+ post :create, params: with_route_params(body_params(owner_params)), format: request_format(:create)
61
+ end
62
+
63
+ if check_html(:create)
64
+ include_examples 'simple crud renders template', :new, -> { invalid_status }
65
+ else
66
+ it 'responds with unprocessable entity' do
67
+ expect(response).to have_http_status(unprocessable_status)
68
+ end
69
+
70
+ it 'returns the validation errors' do
71
+ expect(response_body['errors']).to include(required_error)
72
+ end
73
+
74
+ it 'does not create a model' do
75
+ expect(model_class_object.count).to be 0
76
+ end
77
+ end
46
78
  end
47
79
  end
48
80
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'simple crud for create with block' do
4
+ describe 'POST #create with a render block' do
5
+ it 'passes the saved record to the block' do
6
+ post :create, params: with_route_params(body_params({ name: 'Blocked' }.merge(owner_params)))
7
+
8
+ expect(response.parsed_body).to include('saved' => true)
9
+ end
10
+
11
+ it 'passes the failed record to the block', :aggregate_failures do
12
+ post :create, params: with_route_params(body_params(owner_params))
13
+
14
+ expect(response).to have_http_status(422)
15
+ expect(response.parsed_body).to include('saved' => false)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'simple crud for create with build' do
4
+ describe 'POST #create with a build hook' do
5
+ include_context 'with authenticated user'
6
+
7
+ before do
8
+ post :create, params: with_route_params(body_params({ name: 'Built' }))
9
+ end
10
+
11
+ it 'creates the record owned by the current user' do
12
+ expect(model_class_object.last.public_send(owner_association)).to eq(current_user)
13
+ end
14
+ end
15
+ end
@@ -3,7 +3,10 @@
3
3
  shared_examples 'simple crud for destroy' do
4
4
  describe 'DELETE #destroy' do
5
5
  context 'without authenticated user' do
6
- subject!(:req) { delete :destroy, params: { id: 1 } }
6
+ subject!(:req) do
7
+ delete :destroy, params: with_route_params(record_param(:destroy, nil, not_found: true)),
8
+ format: request_format(:destroy)
9
+ end
7
10
 
8
11
  include_examples 'unauthorized when not logged in' if check_authenticate(:destroy)
9
12
  end
@@ -14,7 +17,7 @@ shared_examples 'simple crud for destroy' do
14
17
 
15
18
  before do
16
19
  make_policies_fail(:destroy)
17
- delete :destroy, params: { id: model.id }
20
+ delete :destroy, params: with_route_params(record_param(:destroy, model)), format: request_format(:destroy)
18
21
  end
19
22
 
20
23
  it 'fails with forbidden' do
@@ -28,11 +31,18 @@ shared_examples 'simple crud for destroy' do
28
31
 
29
32
  before do
30
33
  model
31
- delete :destroy, params: { id: model.id }
34
+ delete :destroy, params: with_route_params(record_param(:destroy, model)), format: request_format(:destroy)
32
35
  end
33
36
 
34
- it 'response with 200 status code' do
35
- expect(response).to have_http_status(:ok)
37
+ if check_html(:destroy)
38
+ it 'redirects to the collection and destroys the record', :aggregate_failures do
39
+ expect(response).to have_http_status(:found)
40
+ expect(model_class_object.exists?(model.id)).to be false
41
+ end
42
+ else
43
+ it 'response with 200 status code' do
44
+ expect(response).to have_http_status(:ok)
45
+ end
36
46
  end
37
47
  end
38
48
 
@@ -41,7 +51,8 @@ shared_examples 'simple crud for destroy' do
41
51
 
42
52
  before do
43
53
  model
44
- delete :destroy, params: { id: model.id + 13 }
54
+ delete :destroy, params: with_route_params(record_param(:destroy, nil, not_found: true)),
55
+ format: request_format(:destroy)
45
56
  end
46
57
 
47
58
  it 'responds with not found status' do
@@ -56,10 +67,10 @@ shared_examples 'simple crud for destroy' do
56
67
  before do
57
68
  model
58
69
  make_policies_fail(:destroy)
59
- delete :destroy, params: { id: model.id }
70
+ delete :destroy, params: with_route_params(record_param(:destroy, model)), format: request_format(:destroy)
60
71
  end
61
72
 
62
- it 'response with 200 status code' do
73
+ it 'responds with forbidden status' do
63
74
  expect(response).to have_http_status(:forbidden)
64
75
  end
65
76
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'simple crud for destroy with block' do
4
+ describe 'DELETE #destroy with a render block' do
5
+ let!(:record) { model }
6
+
7
+ before { delete :destroy, params: with_route_params({ id: record.id }) }
8
+
9
+ it 'passes the destroyed record to the block' do
10
+ expect(response.parsed_body).to eq('destroyed' => true)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ shared_examples 'simple crud for destroy with finder' do
4
+ describe 'DELETE #destroy with a custom finder' do
5
+ include_context 'with authenticated user'
6
+
7
+ context 'when the model exists' do
8
+ before do
9
+ model
10
+ delete :destroy, params: with_route_params(finder_key => model.public_send(finder_key))
11
+ end
12
+
13
+ it 'responds with ok status' do
14
+ expect(response).to have_http_status(:ok)
15
+ end
16
+
17
+ it 'destroys the model' do
18
+ expect(model_class_object.exists?(model.id)).to be false
19
+ end
20
+ end
21
+
22
+ include_examples 'simple crud not found with finder', :delete, :destroy
23
+ end
24
+ end