wor-simple_crud 0.2.1 → 0.3.1

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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/CHANGELOG.md +46 -0
  4. data/Gemfile +1 -0
  5. data/README.md +200 -2
  6. data/lib/simple_crud/action_lambdas.rb +81 -0
  7. data/lib/simple_crud/authorization/adapter.rb +7 -0
  8. data/lib/simple_crud/authorization/pundit_adapter.rb +6 -0
  9. data/lib/simple_crud/config.rb +6 -1
  10. data/lib/simple_crud/controller_helpers.rb +128 -0
  11. data/lib/simple_crud/pagination/adapter.rb +7 -1
  12. data/lib/simple_crud/pagination/kaminari_adapter.rb +23 -2
  13. data/lib/simple_crud/pagination/pagy_adapter.rb +7 -2
  14. data/lib/simple_crud/pagination/will_paginate_adapter.rb +7 -3
  15. data/lib/simple_crud/pagination/wor_paginate_adapter.rb +7 -2
  16. data/lib/simple_crud/rspec/config.rb +74 -0
  17. data/lib/simple_crud/rspec/helpers/authentication.rb +32 -0
  18. data/lib/simple_crud/rspec/helpers/controller_options.rb +19 -0
  19. data/lib/simple_crud/rspec/helpers/models.rb +55 -0
  20. data/lib/simple_crud/rspec/helpers/policies.rb +25 -0
  21. data/lib/simple_crud/rspec/helpers/requests.rb +45 -0
  22. data/lib/simple_crud/rspec/helpers/settings.rb +49 -0
  23. data/lib/simple_crud/rspec/helpers.rb +25 -0
  24. data/lib/simple_crud/rspec.rb +35 -0
  25. data/lib/simple_crud/simple_crud_controller.rb +27 -75
  26. data/lib/simple_crud/version.rb +1 -1
  27. data/lib/spec/shared_contexts/authenticate_user.rb +1 -7
  28. data/lib/spec/shared_examples/authorization_adapter_authorize.rb +11 -0
  29. data/lib/spec/shared_examples/simple_crud_for_create.rb +46 -11
  30. data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +36 -0
  31. data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +15 -0
  32. data/lib/spec/shared_examples/simple_crud_for_destroy.rb +44 -8
  33. data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +21 -0
  34. data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +35 -0
  35. data/lib/spec/shared_examples/simple_crud_for_edit.rb +79 -0
  36. data/lib/spec/shared_examples/simple_crud_for_index.rb +9 -24
  37. data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +22 -0
  38. data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +33 -0
  39. data/lib/spec/shared_examples/simple_crud_for_new.rb +29 -0
  40. data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +20 -0
  41. data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +19 -0
  42. data/lib/spec/shared_examples/simple_crud_for_show.rb +14 -10
  43. data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +22 -0
  44. data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +33 -0
  45. data/lib/spec/shared_examples/simple_crud_for_update.rb +61 -14
  46. data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +39 -0
  47. data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +14 -0
  48. data/lib/spec/shared_examples/simple_crud_renders_template.rb +8 -0
  49. data/lib/spec/shared_examples/simple_crud_when_not_authorized.rb +18 -0
  50. data/lib/spec/shared_examples/simple_crud_without_authenticated_user.rb +9 -0
  51. data/lib/spec/shared_examples/unauthorized_when_not_logged_in.rb +2 -2
  52. data/spec/block/dummy_models_controller_spec.rb +8 -0
  53. data/spec/block_create/dummy_models_controller_spec.rb +7 -0
  54. data/spec/block_destroy/dummy_models_controller_spec.rb +7 -0
  55. data/spec/block_finder/dummy_models_controller_spec.rb +10 -0
  56. data/spec/block_json/dummy_models_controller_spec.rb +9 -0
  57. data/spec/block_new/dummy_models_controller_spec.rb +7 -0
  58. data/spec/block_redirect/dummy_models_controller_spec.rb +7 -0
  59. data/spec/block_show/dummy_models_controller_spec.rb +7 -0
  60. data/spec/built/dummy_models_controller_spec.rb +8 -0
  61. data/spec/dummy/app/controllers/block/dummy_models_controller.rb +17 -0
  62. data/spec/dummy/app/controllers/block_create/dummy_models_controller.rb +13 -0
  63. data/spec/dummy/app/controllers/block_destroy/dummy_models_controller.rb +9 -0
  64. data/spec/dummy/app/controllers/block_finder/dummy_models_controller.rb +25 -0
  65. data/spec/dummy/app/controllers/block_json/dummy_models_controller.rb +17 -0
  66. data/spec/dummy/app/controllers/block_modes/base_controller.rb +11 -0
  67. data/spec/dummy/app/controllers/block_new/dummy_models_controller.rb +9 -0
  68. data/spec/dummy/app/controllers/block_redirect/dummy_models_controller.rb +13 -0
  69. data/spec/dummy/app/controllers/block_show/dummy_models_controller.rb +9 -0
  70. data/spec/dummy/app/controllers/built/dummy_models_controller.rb +21 -0
  71. data/spec/dummy/app/controllers/dummy_models_controller.rb +2 -1
  72. data/spec/dummy/app/controllers/finder/dummy_models_controller.rb +23 -0
  73. data/spec/dummy/app/controllers/html/dummy_models_controller.rb +7 -0
  74. data/spec/dummy/app/controllers/html_create/dummy_models_controller.rb +7 -0
  75. data/spec/dummy/app/controllers/html_destroy/dummy_models_controller.rb +7 -0
  76. data/spec/dummy/app/controllers/html_finder/dummy_models_controller.rb +17 -0
  77. data/spec/dummy/app/controllers/html_modes/base_controller.rb +17 -0
  78. data/spec/dummy/app/controllers/html_new/dummy_models_controller.rb +7 -0
  79. data/spec/dummy/app/controllers/html_scoped/dummy_models_controller.rb +8 -0
  80. data/spec/dummy/app/controllers/html_show/dummy_models_controller.rb +7 -0
  81. data/spec/dummy/app/controllers/html_update/dummy_models_controller.rb +7 -0
  82. data/spec/dummy/app/controllers/invalid_status/dummy_models_controller.rb +13 -0
  83. data/spec/dummy/app/controllers/nested/dummy_models_controller.rb +21 -0
  84. data/spec/dummy/app/controllers/nested_route/dummy_models_controller.rb +26 -0
  85. data/spec/dummy/app/controllers/redirect/dummy_models_controller.rb +10 -0
  86. data/spec/dummy/app/controllers/redirect_auth/dummy_models_controller.rb +28 -0
  87. data/spec/dummy/app/controllers/scoped/dummy_models_controller.rb +16 -0
  88. data/spec/dummy/app/controllers/scoped_params/dummy_models_controller.rb +16 -0
  89. data/spec/dummy/app/controllers/strict/dummy_models_controller.rb +21 -0
  90. data/spec/dummy/app/controllers/unpaginated_scoped/dummy_models_controller.rb +16 -0
  91. data/spec/dummy/app/models/dummy_model.rb +4 -0
  92. data/spec/dummy/app/models/dummy_model_policy.rb +9 -1
  93. data/spec/dummy/app/views/block/dummy_models/index.html.erb +3 -0
  94. data/spec/dummy/app/views/block_new/dummy_models/new.html.erb +1 -0
  95. data/spec/dummy/app/views/block_show/dummy_models/show.html.erb +1 -0
  96. data/spec/dummy/app/views/html/dummy_models/index.html.erb +3 -0
  97. data/spec/dummy/app/views/html_create/dummy_models/new.html.erb +1 -0
  98. data/spec/dummy/app/views/html_destroy/dummy_models/show.html.erb +1 -0
  99. data/spec/dummy/app/views/html_finder/dummy_models/edit.html.erb +1 -0
  100. data/spec/dummy/app/views/html_finder/dummy_models/new.html.erb +1 -0
  101. data/spec/dummy/app/views/html_finder/dummy_models/show.html.erb +1 -0
  102. data/spec/dummy/app/views/html_new/dummy_models/new.html.erb +1 -0
  103. data/spec/dummy/app/views/html_scoped/dummy_models/index.html.erb +3 -0
  104. data/spec/dummy/app/views/html_show/dummy_models/show.html.erb +1 -0
  105. data/spec/dummy/app/views/html_update/dummy_models/edit.html.erb +1 -0
  106. data/spec/dummy/app/views/invalid_status/dummy_models/new.html.erb +1 -0
  107. data/spec/dummy/app/views/layouts/application.html.erb +4 -0
  108. data/spec/dummy/app/views/redirect/dummy_models/new.html.erb +1 -0
  109. data/spec/dummy/config/routes.rb +110 -0
  110. data/spec/dummy/db/migrate/20260806000000_add_slug_to_dummy_models.rb +7 -0
  111. data/spec/dummy/db/migrate/20260807000000_add_classroom_slug_to_dummy_models.rb +7 -0
  112. data/spec/dummy/db/schema.rb +3 -1
  113. data/spec/dummy/spec/factories/dummy_models.rb +1 -0
  114. data/spec/dummy_controller_spec.rb +1 -0
  115. data/spec/dummy_model_policy_spec.rb +6 -4
  116. data/spec/finder/dummy_models_controller_spec.rb +13 -0
  117. data/spec/html/dummy_models_controller_spec.rb +7 -0
  118. data/spec/html_create/dummy_models_controller_spec.rb +7 -0
  119. data/spec/html_destroy/dummy_models_controller_spec.rb +7 -0
  120. data/spec/html_finder/dummy_models_controller_spec.rb +11 -0
  121. data/spec/html_new/dummy_models_controller_spec.rb +7 -0
  122. data/spec/html_scoped/dummy_models_controller_spec.rb +7 -0
  123. data/spec/html_show/dummy_models_controller_spec.rb +7 -0
  124. data/spec/html_update/dummy_models_controller_spec.rb +7 -0
  125. data/spec/invalid_status/dummy_models_controller_spec.rb +10 -0
  126. data/spec/nested/dummy_models_controller_spec.rb +12 -0
  127. data/spec/nested_route/dummy_models_controller_spec.rb +19 -0
  128. data/spec/redirect/dummy_models_controller_spec.rb +23 -0
  129. data/spec/redirect_auth/dummy_models_controller_spec.rb +11 -0
  130. data/spec/scoped/dummy_models_controller_spec.rb +7 -0
  131. data/spec/scoped_params/dummy_models_controller_spec.rb +12 -0
  132. data/spec/simple_crud/authorization/action_policy_adapter_spec.rb +1 -9
  133. data/spec/simple_crud/authorization/can_can_can_adapter_spec.rb +1 -7
  134. data/spec/simple_crud/authorization/pundit_adapter_spec.rb +41 -0
  135. data/spec/simple_crud/controller_helpers_spec.rb +49 -0
  136. data/spec/simple_crud/pagination/adapters_spec.rb +37 -13
  137. data/spec/simple_crud/rspec_config_spec.rb +100 -0
  138. data/spec/simple_crud_configuration_spec.rb +19 -16
  139. data/spec/simple_crud_controller_spec.rb +4 -4
  140. data/spec/spec_helper.rb +1 -0
  141. data/spec/strict/dummy_models_controller_spec.rb +23 -0
  142. data/spec/unpaginated_scoped/dummy_models_controller_spec.rb +7 -0
  143. metadata +108 -4
  144. data/lib/spec/shared_examples/helpers.rb +0 -50
@@ -10,10 +10,31 @@ module SimpleCrud
10
10
  class KaminariAdapter
11
11
  include Adapter
12
12
 
13
- def paginate(controller, klass, options)
14
- records = klass.page(controller.params[:page]).per(controller.params[:per_page])
13
+ def paginate(controller, relation, options)
14
+ records = paginated_records(controller, relation, options)
15
15
  controller.render({ json: records }.merge(options))
16
16
  end
17
+
18
+ # Manual page scope: will_paginate shadows Relation#page when both load.
19
+ def paginated_records(controller, relation, _options)
20
+ page = [controller.params[:page].to_i, 1].max
21
+ per_page = page_size(controller)
22
+
23
+ relation.limit(per_page).offset(per_page * (page - 1)).extending do
24
+ include Kaminari::ActiveRecordRelationMethods
25
+ include Kaminari::PageScopeMethods
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ # Params arrive as strings: coerce before arithmetic or "5" * 2 == "55".
32
+ def page_size(controller)
33
+ size = (controller.params[:per_page] || Kaminari.config.default_per_page).to_i
34
+ size = [size, 1].max
35
+ size = [size, Kaminari.config.max_per_page].min if Kaminari.config.max_per_page
36
+ size
37
+ end
17
38
  end
18
39
  end
19
40
  end
@@ -13,10 +13,15 @@ module SimpleCrud
13
13
  # the pre-rename 43.4.x, so pick whichever key the loaded Pagy expects.
14
14
  MAX_LIMIT_KEY = Gem::Version.new(Pagy::VERSION) >= Gem::Version.new('43.5.0') ? :max_limit : :client_max_limit
15
15
 
16
- def paginate(controller, klass, options)
17
- _pagy, records = controller.send(:pagy, klass.all, MAX_LIMIT_KEY => 100)
16
+ def paginate(controller, relation, options)
17
+ records = paginated_records(controller, relation, options)
18
18
  controller.render({ json: records }.merge(options))
19
19
  end
20
+
21
+ def paginated_records(controller, relation, _options)
22
+ _pagy, records = controller.send(:pagy, relation, MAX_LIMIT_KEY => 100)
23
+ records
24
+ end
20
25
  end
21
26
  end
22
27
  end
@@ -11,11 +11,15 @@ module SimpleCrud
11
11
  class WillPaginateAdapter
12
12
  include Adapter
13
13
 
14
- def paginate(controller, klass, options)
14
+ def paginate(controller, relation, options)
15
+ records = paginated_records(controller, relation, options)
16
+ controller.render({ json: records }.merge(options))
17
+ end
18
+
19
+ def paginated_records(controller, relation, _options)
15
20
  page = controller.params[:page] || 1
16
21
  per_page = controller.params[:per_page] || WillPaginate.per_page
17
- records = klass.paginate(page: page, per_page: per_page)
18
- controller.render({ json: records }.merge(options))
22
+ relation.paginate(page: page, per_page: per_page)
19
23
  end
20
24
  end
21
25
  end
@@ -9,8 +9,13 @@ module SimpleCrud
9
9
  class WorPaginateAdapter
10
10
  include Adapter
11
11
 
12
- def paginate(controller, klass, options)
13
- controller.send(:render_paginated, klass, options)
12
+ def paginate(controller, relation, options)
13
+ controller.send(:render_paginated, relation, options)
14
+ end
15
+
16
+ def paginated_records(controller, relation, options)
17
+ adapter = controller.send(:find_adapter_for_content, relation, options)
18
+ adapter.paginated_content
14
19
  end
15
20
  end
16
21
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ # Test helpers: SimpleCrud::RSpec.configure exposes the shared examples'
5
+ # configuration for apps on a different stack than the gem's.
6
+ module RSpec
7
+ def self.configure
8
+ yield Config.instance
9
+ end
10
+
11
+ # Configures how the shared examples create records, authenticate
12
+ # requests and introspect policies/serializers. The defaults match the
13
+ # gem's own stack (Devise-JWT + FactoryBot + Pundit + ActiveModel
14
+ # Serializers), so apps on a different stack can override them via
15
+ # SimpleCrud::RSpec.configure instead of editing the examples.
16
+ #
17
+ # Callable settings (authenticate, current_user, other_user,
18
+ # create_record, create_records, params_for, model_attributes,
19
+ # policy_class, serializer_class) run in the example-group context, so
20
+ # they can call helpers such as `create`, `request` and `current_user`.
21
+ class Config
22
+ DEFAULTS = {
23
+ authenticate: lambda {
24
+ headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/json' }
25
+ request.headers.merge!(Devise::JWT::TestHelpers.auth_headers(headers, current_user))
26
+ },
27
+ current_user: -> { create(:user) },
28
+ other_user: -> { create(:user) },
29
+ create_record: ->(klass, attributes) { create(klass, **attributes) },
30
+ create_records: ->(klass, count, attributes) { create_list(klass, count, **attributes) },
31
+ params_for: ->(klass) { attributes_for(klass) },
32
+ owner_association: :user,
33
+ model_attributes: -> { owner_association ? { owner_association => current_user } : {} },
34
+ scoped_attributes: -> { model_attributes },
35
+ other_scoped_attributes: lambda {
36
+ attributes = model_attributes
37
+ if owner_association && !attributes.is_a?(Hash)
38
+ raise ArgumentError,
39
+ 'model_attributes must resolve to a Hash when owner_association is set; ' \
40
+ 'override other_scoped_attributes instead'
41
+ end
42
+
43
+ owner_association ? attributes.merge(owner_association => other_user) : {}
44
+ },
45
+ required_attribute: :name,
46
+ required_error: "Name can't be blank",
47
+ finder_key: :slug,
48
+ params_key: nil,
49
+ route_params: -> { {} },
50
+ invalid_status: :ok,
51
+ unauthenticated_status: :unauthorized,
52
+ assert_html_template: true,
53
+ policy_class: ->(klass) { "#{klass}Policy".constantize },
54
+ serializer_class: ->(model) { "#{model.class}_serializer".classify.constantize },
55
+ created_record_check: nil
56
+ }.freeze
57
+
58
+ class << self
59
+ def instance
60
+ @instance ||= new
61
+ end
62
+ end
63
+
64
+ def initialize
65
+ @values = {}
66
+ end
67
+
68
+ DEFAULTS.each_key do |key|
69
+ define_method(key) { @values.fetch(key, DEFAULTS[key]) }
70
+ define_method("#{key}=") { |value| @values[key] = value }
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ module RSpec
5
+ module Helpers
6
+ # Current/other users, signing requests in and the owner association
7
+ # attributed to the current user.
8
+ module Authentication
9
+ def current_user
10
+ @current_user ||= instance_exec(&setting(:current_user))
11
+ end
12
+
13
+ def other_user
14
+ @other_user ||= instance_exec(&setting(:other_user))
15
+ end
16
+
17
+ def authenticate_request
18
+ instance_exec(&setting(:authenticate))
19
+ end
20
+
21
+ def owner_foreign_key
22
+ :"#{owner_association}_id"
23
+ end
24
+
25
+ # The owner association attributed to current_user, if the model has one.
26
+ def owner_params
27
+ owner_association ? { owner_foreign_key => current_user.id } : {}
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ module RSpec
5
+ module Helpers
6
+ # Definition-time introspection of the options each action was declared
7
+ # with via simple_crud_for (read from the controller's metadata).
8
+ module ControllerOptions
9
+ def get_option(method, option)
10
+ described_class.instance_variable_get(:@simple_crud_metadata)[method][option]
11
+ end
12
+
13
+ %i[paginate authorize authenticate serializer html finder scope build raise_on_invalid block].each do |option|
14
+ define_method("check_#{option}") { |method| get_option(method, option) }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ module RSpec
5
+ module Helpers
6
+ # Building the record under test and the params used to persist it.
7
+ module Models
8
+ def model_class
9
+ described_class.to_s.split('::')
10
+ .last.sub('Controller', '').singularize.underscore
11
+ end
12
+
13
+ def model_class_object
14
+ model_class.classify.constantize
15
+ end
16
+
17
+ def model
18
+ @model ||= create_record(model_class, model_attributes)
19
+ end
20
+
21
+ def model_attributes
22
+ resolve(setting(:model_attributes))
23
+ end
24
+
25
+ def create_record(klass, attributes)
26
+ instance_exec(klass, attributes, &setting(:create_record))
27
+ end
28
+
29
+ def create_records(klass, count, attributes)
30
+ instance_exec(klass, count, attributes, &setting(:create_records))
31
+ end
32
+
33
+ def params_for(klass)
34
+ instance_exec(klass, &setting(:params_for))
35
+ end
36
+
37
+ def model_params
38
+ @model_params ||= params_for(model_class)
39
+ end
40
+
41
+ def model_serializer
42
+ defined?(serializer) ? serializer : instance_exec(model, &setting(:serializer_class))
43
+ end
44
+
45
+ def rendered_record
46
+ controller.instance_variable_get(:@record)
47
+ end
48
+
49
+ def rendered_records
50
+ controller.instance_variable_get(:@records)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ module RSpec
5
+ module Helpers
6
+ # Locating the policy object for the model under test and stubbing it
7
+ # to allow or deny.
8
+ module Policies
9
+ def policy_class_object
10
+ instance_exec(model_class_object, &setting(:policy_class))
11
+ end
12
+
13
+ def make_policies_fail(method)
14
+ allow(policy_class_object).to receive(:new)
15
+ .and_return(instance_double(policy_class_object, "#{method}?" => false))
16
+ end
17
+
18
+ def make_policies_succeed(method)
19
+ allow(policy_class_object).to receive(:new)
20
+ .and_return(instance_double(policy_class_object, "#{method}?" => true))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ module RSpec
5
+ module Helpers
6
+ # Request params and format helpers shared by every action example.
7
+ module Requests
8
+ # :unprocessable_entity was renamed :unprocessable_content in Rack 3.1.
9
+ # Resolve the current Rack's canonical symbol for 422.
10
+ def unprocessable_status
11
+ Rack::Utils::SYMBOL_TO_STATUS_CODE.invert[422]
12
+ end
13
+
14
+ def request_format(action)
15
+ check_html(action) ? :html : :json
16
+ end
17
+
18
+ # Extra params (e.g. a parent slug) added to every request, for nested
19
+ # resources like /projects/:project_slug/tasks.
20
+ def route_params
21
+ resolve(setting(:route_params))
22
+ end
23
+
24
+ def with_route_params(attrs)
25
+ route_params.merge(attrs)
26
+ end
27
+
28
+ # The params that identify a record for the given action, using the
29
+ # configured finder_key when the action has a custom finder.
30
+ def record_param(action, record, not_found: false)
31
+ key = check_finder(action) ? finder_key : :id
32
+ value = not_found ? "nonexistent-#{key}" : record.public_send(key)
33
+ { key => value }
34
+ end
35
+
36
+ # Wraps a request body under the model's strong-params key when
37
+ # params_key is configured (nested strong params), else passes it flat.
38
+ def body_params(attrs)
39
+ key = params_key
40
+ key ? { key => attrs } : attrs
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ module RSpec
5
+ module Helpers
6
+ # Settings resolution: `simple_crud:` example-group metadata first,
7
+ # falling back to the app-wide SimpleCrud::RSpec::Config singleton.
8
+ module Settings
9
+ def config
10
+ SimpleCrud::RSpec::Config.instance
11
+ end
12
+
13
+ def simple_crud_settings
14
+ source = ::RSpec.current_example ? ::RSpec.current_example.metadata : metadata
15
+ source[:simple_crud] || {}
16
+ end
17
+
18
+ def setting(name)
19
+ simple_crud_settings.fetch(name) { config.public_send(name) }
20
+ end
21
+
22
+ def resolve(value)
23
+ value.is_a?(Proc) ? instance_exec(&value) : value
24
+ end
25
+
26
+ %i[owner_association required_attribute required_error finder_key params_key invalid_status
27
+ unauthenticated_status assert_html_template scoped_attributes other_scoped_attributes].each do |name|
28
+ define_method(name) { resolve(setting(name)) }
29
+ end
30
+
31
+ def created_record_check
32
+ setting(:created_record_check)
33
+ end
34
+
35
+ # Runs the block with a config setting temporarily overridden, restoring
36
+ # the original value afterwards (even if the block raises). Useful when
37
+ # a controller's spec needs a different setting than the app-wide default.
38
+ def with_config_override(setting, value)
39
+ config_instance = SimpleCrud::RSpec::Config.instance
40
+ original = config_instance.public_send(setting)
41
+ SimpleCrud::RSpec.configure { |c| c.public_send("#{setting}=", value) }
42
+ yield
43
+ ensure
44
+ SimpleCrud::RSpec.configure { |c| c.public_send("#{setting}=", original) }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helpers/settings'
4
+ require_relative 'helpers/controller_options'
5
+ require_relative 'helpers/models'
6
+ require_relative 'helpers/authentication'
7
+ require_relative 'helpers/requests'
8
+ require_relative 'helpers/policies'
9
+
10
+ module SimpleCrud
11
+ module RSpec
12
+ # Helpers available to the shared examples (and to apps that include
13
+ # them), composed from focused sub-modules: settings resolution,
14
+ # controller-option introspection, record building, authentication,
15
+ # request params and policy access.
16
+ module Helpers
17
+ include Settings
18
+ include ControllerOptions
19
+ include Models
20
+ include Authentication
21
+ include Requests
22
+ include Policies
23
+ end
24
+ end
25
+ 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,38 @@ 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'
13
+ require_relative '../spec/shared_examples/simple_crud_for_edit'
10
14
  require_relative '../spec/shared_examples/unauthorized_when_not_logged_in'
15
+ require_relative '../spec/shared_examples/simple_crud_renders_template'
16
+ require_relative '../spec/shared_examples/simple_crud_without_authenticated_user'
17
+ require_relative '../spec/shared_examples/simple_crud_when_not_authorized'
18
+ require_relative '../spec/shared_examples/simple_crud_for_index_with_block'
19
+ require_relative '../spec/shared_examples/simple_crud_for_index_with_scope'
20
+ require_relative '../spec/shared_examples/simple_crud_for_show_with_block'
21
+ require_relative '../spec/shared_examples/simple_crud_for_new_with_block'
22
+ require_relative '../spec/shared_examples/simple_crud_for_create_with_block'
23
+ require_relative '../spec/shared_examples/simple_crud_for_destroy_with_block'
24
+ require_relative '../spec/shared_examples/simple_crud_for_show_with_finder'
25
+ require_relative '../spec/shared_examples/simple_crud_for_update_with_finder'
26
+ require_relative '../spec/shared_examples/simple_crud_for_destroy_with_finder'
27
+ require_relative '../spec/shared_examples/simple_crud_not_found_with_finder'
28
+ require_relative '../spec/shared_examples/simple_crud_for_new_with_build'
29
+ require_relative '../spec/shared_examples/simple_crud_for_create_with_build'
30
+ require_relative '../spec/shared_examples/authorization_adapter_authorize'
11
31
  require_relative '../spec/matchers/have_been_serialized_with'
32
+
33
+ module SimpleCrud
34
+ # Opt-in wiring for the shared examples: see .install!
35
+ module RSpec
36
+ # Opt-in wiring: call from your spec support after requiring this file.
37
+ # extend wires definition-time helpers (check_html...), include runtime ones (model...).
38
+ def self.install!
39
+ ::RSpec.configure do |config|
40
+ config.extend Helpers
41
+ config.include Helpers
42
+ config.include Response::JSONParser, type: :controller
43
+ end
44
+ end
45
+ end
46
+ end
@@ -2,9 +2,15 @@
2
2
 
3
3
  require 'active_support/all'
4
4
  require_relative 'config'
5
+ require_relative 'controller_helpers'
6
+ require_relative 'action_lambdas'
5
7
 
6
8
  # Extended onto a controller for CRUD actions.
7
9
  module SimpleCrudController
10
+ extend SimpleCrud::ControllerHelpers
11
+ # Include, not extend: controllers gain these by extending SimpleCrudController.
12
+ include SimpleCrud::ActionLambdas
13
+
8
14
  cattr_accessor :params, :permitted
9
15
 
10
16
  # Possible options:
@@ -12,21 +18,31 @@ module SimpleCrudController
12
18
  ### paginate: paginate the list via Config.pagination_adapter (wor-paginate by default)
13
19
  ### authenticate: use devise to authenticate
14
20
  ### serializer: use a particular serializer (both each_serializer and serializer)
15
- def simple_crud_for(method, parameters = {})
21
+ ### html: render the action's ERB template instead of JSON (index/show/new/edit/create/update/destroy)
22
+ ### finder: custom record lookup (Proc/lambda or Symbol) for :show/:update/:destroy/:edit
23
+ ### scope: custom index scope (Proc/lambda taking current_user and optional params), overrides policy_scope
24
+ ### build: custom record builder (Proc/lambda) for :new/:create, invoked with the controller as self
25
+ ### raise_on_invalid: use strict create!/update! semantics instead of returning 422
26
+ ### A block given to simple_crud_for renders explicitly: it receives the records for :index,
27
+ ### the record for :show/:new/:edit, or the record and a saved flag for :create/:update/:destroy
28
+ def simple_crud_for(method, parameters = {}, &block)
29
+ parameters[:html] = true if block && !parameters.key?(:html)
30
+ parameters[:block] = true if block
16
31
  parameters = parameters_with_defaults(parameters)
17
32
  klass = simple_crud_controller_model
18
33
  check_valid_method(method)
19
34
  check_policies(parameters)
20
35
  check_serializer(parameters)
21
- define_method(method, send("crud_lambda_for_#{method}", klass, parameters))
36
+ define_method(method, send("crud_lambda_for_#{method}", klass, parameters, &block))
22
37
  write_metadata(method, parameters)
23
38
  end
24
39
 
25
40
  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
41
+ defaults = {
42
+ authorize: true, paginate: true, authenticate: true, serializer: nil,
43
+ html: false, finder: nil, scope: nil, build: nil, raise_on_invalid: false
44
+ }
45
+ defaults.each { |key, value| parameters[key] = value unless parameters.key?(key) }
30
46
  parameters
31
47
  end
32
48
 
@@ -35,74 +51,12 @@ module SimpleCrudController
35
51
  @simple_crud_metadata[method] = parameters
36
52
  end
37
53
 
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 = {})
45
- lambda do
46
- authenticate_user! if parameters[:authenticate]
47
- requested = klass.find(params[:id])
48
-
49
- options = {}.merge(serializer: parameters[:serializer]).compact
50
- SimpleCrudController.maybe_authorize(self, requested, parameters)
51
- render({ json: requested }.merge(options))
52
- end
53
- end
54
-
55
- def crud_lambda_for_index(klass, parameters = {})
56
- lambda do
57
- 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)
61
- end
62
- end
63
-
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))
69
- end
70
- end
71
-
72
- def crud_lambda_for_create(klass, parameters = {})
73
- lambda do
74
- authenticate_user! if parameters[:authenticate]
75
- 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
78
- end
79
- end
80
-
81
- def crud_lambda_for_update(klass, parameters = {})
82
- lambda do
83
- authenticate_user! if parameters[:authenticate]
84
- requested = klass.find(params[:id])
85
- SimpleCrudController.maybe_authorize(self, requested, parameters)
86
- permitted_params = send("#{self.class.simple_crud_controller_model.to_s.underscore}_params")
87
- render json: requested.update!(permitted_params)
88
- end
89
- end
90
-
91
- def crud_lambda_for_destroy(klass, parameters = {})
92
- lambda do
93
- authenticate_user! if parameters[:authenticate]
94
- requested = klass.find(params[:id])
95
- SimpleCrudController.maybe_authorize(self, requested, parameters)
96
- render json: klass.find(params[:id]).destroy
97
- end
98
- end
99
-
100
54
  def simple_crud_controller_model
101
55
  to_s.split('::').last.sub('Controller', '').singularize.classify.constantize
102
56
  end
103
57
 
104
58
  def check_valid_method(method)
105
- throw 'invalid method' unless %i[show index create update destroy].include? method
59
+ raise ArgumentError, 'invalid method' unless %i[show index create update destroy new edit].include? method
106
60
  end
107
61
 
108
62
  def check_policies(parameters)
@@ -111,15 +65,13 @@ module SimpleCrudController
111
65
  model = simple_crud_controller_model
112
66
  return if SimpleCrud::Config.authorization_adapter.policy_defined?(model)
113
67
 
114
- throw "no authorization policy configured for #{model}"
68
+ raise ArgumentError, "no authorization policy configured for #{model}"
115
69
  end
116
70
 
117
71
  def check_serializer(parameters)
118
- return if parameters[:serializer].blank?
119
-
120
- serializer_name = parameters[:serializer].to_s
121
- return if Kernel.const_defined?(serializer_name)
72
+ name = parameters[:serializer].to_s
73
+ return if name.blank? || Kernel.const_defined?(name)
122
74
 
123
- throw "create a valid serializer with name #{serializer_name}"
75
+ raise ArgumentError, "create a valid serializer with name #{name}"
124
76
  end
125
77
  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.1'
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