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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f3a2ed3cb6cea9416a6a745959519ccfdd9fd3a35667432939ece4291368266
4
- data.tar.gz: 6b0625c10d66661cff9b09959ddd7f1a0eae630c0a519fa34116855b38588377
3
+ metadata.gz: e280efd8ead44768d16e0ba663e25a40d6972b53e253281d1c7eac0735421b64
4
+ data.tar.gz: 78625a2a759ca5fda1e7d41e421db92ed721f2e2c343524f0580cd810d32ac7a
5
5
  SHA512:
6
- metadata.gz: c4b2ab3b62121f5409a0d11d498a094162f847027941deb4abb3c72dc0c5c623934c3aeee5a65c64149039fbf2c09d4e9f9a8e10da6818ad32e236545e592331
7
- data.tar.gz: 75211a88220e4328897ba0c36ebd55b04742bc253874f711cf14e644ff5a9d595455003facfb7ad9896346e63570d6dffb36fbeb947e34261a17e38ddb60aa80
6
+ metadata.gz: 406a83cc98deaa7fa3ba5f7ccb9a12486a903b6aef81bddd0a5f4f9c595ffa64e7414cb8a718d4a4fe96f987fcefaf34a8b201cbe16aab4db2721eea0bd19397
7
+ data.tar.gz: 7177530ba1a01fcf526c406a14f3bf64bfcf895ce5675f4acca5f24f28c57db90859ddab43866d314f70e723cfebda24bc5930d64ac66a01d29bc6246792c04a
data/.gitignore CHANGED
@@ -147,6 +147,11 @@ config/initializers/secret_token.rb
147
147
  ## Environment normalization:
148
148
  /.bundle
149
149
  /vendor/bundle
150
+ # Appraisal gemfiles vendor their own bundles; never commit those.
151
+ /gemfiles/.bundle/
152
+ /gemfiles/vendor/
153
+ # Local tooling logs.
154
+ /.setting/
150
155
 
151
156
  # these should all be checked in to normalize the environment:
152
157
  # Gemfile.lock, .ruby-version, .ruby-gemset
data/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  ## Change log
2
2
 
3
+ ### V0.3.1
4
+
5
+ Shared examples configuration:
6
+ * Every setting can be overridden per controller or per example via `simple_crud:` RSpec metadata. Metadata takes precedence over `SimpleCrud::RSpec.configure` and the defaults, and all reads happen at example runtime, so per-resource tweaks need no around hooks, no global mutation and no restore logic.
7
+ * New `model_attributes` setting (default: `{ owner_association => current_user }`) decides how records are built for the examples, so multi-key (`{ user:, project: }`), non-user-owned (`{ project: project }`) or owner-less models work without editing the shared examples.
8
+ * Dedicated examples (`*_with_block`, `*_with_build`, `*_with_scope`) no longer assume the gem's dummy app: request params derive from `params_for`/`owner_params`, authentication wraps conditionally on the action's `authenticate:` option, and render-block actions are asserted on status and persistence effects only (a block's response body is app-defined by definition).
9
+ * The finder/build dedicated families (`show/update/destroy with finder`, `not found with finder`, `new with build`) adapt to each action's declared options too: render-block actions are asserted success-only, `html: true` actions assert the rendered template or redirect, and plain JSON keeps the status/body assertions. Server-rendered apps no longer need to skip these families.
10
+ * `index with scope` builds its records through new `scoped_attributes` (default: `model_attributes`) and `other_scoped_attributes` (default: `model_attributes.merge(owner_association => other_user)`) settings instead of hardcoding `owner_association => user`, so factories without that attribute keep working and multi-key models express their scope via metadata. The assertion follows the controller: `assigns(:records)` ids for `html: true`, the wor-paginate envelope for paginated JSON, and the bare serialized array otherwise.
11
+ * Actions declared with a render block expose `block: true` in the controller metadata, and the shared examples treat them accordingly: failure/invalid paths assert persistence effects only (the block decides how the response is rendered), while default flows keep their pinned statuses/templates.
12
+ * The base destroy example covers the failure path (record always kept; non-block flows respond 422 as JSON or re-render `show` as HTML).
13
+ * New optional `created_record_check` setting: a lambda receiving the persisted record in the create/update success paths, to assert ownership/scope that `count == 1` alone can't catch.
14
+
15
+ ### V0.3.0
16
+
17
+ Server-rendered (HTML) support:
18
+ * `html: true` or a render block on every action: `:index` renders `index.html.erb` with the paginated records as `@records`; `:show`/`:new` expose `@record`; `:create`/`:update` redirect to the record on success and re-render the form with errors on failure; `:destroy` redirects to the collection (re-rendering `show.html.erb` when a callback aborts). Blocks receive the records (`:index`), the record (`:show`/`:new`) or `(record, saved)` (`:create`/`:update`/`:destroy`) and override the auto-render.
19
+ * New `:new` action: builds a record, authorizes it, and renders `new.html.erb` (or returns JSON in API mode).
20
+
21
+ Custom lookups, scoping and building:
22
+ * `finder:` on `:show`/`:update`/`:destroy`/`:edit`: a `Proc`/lambda invoked with the controller's params, or a `Symbol` naming a class method on the model. Defaults to `klass.find(params[:id])`.
23
+ * New `simple_crud_for :edit`: the find-instead-of-build twin of `:new`. It looks the record up (honoring `finder:`), authorizes it and renders `edit.html.erb` with `@record` in HTML mode, returning the record as JSON otherwise. A matching `simple crud for edit` shared example covers the authorized/denied, not-found and html/json/block paths.
24
+ * `build:` on `:new`/`:create` for owner-scoped or nested builds (`current_user.projects.build`); runs with the controller as `self`, and `:create` assigns the permitted params to the built record before saving.
25
+ * With `authorize: true`, `:index` paginates the Pundit `policy_scope` of the model (falling back to the full relation when no `Scope` is defined) instead of `klass.all`, with a per-action `scope: ->(user[, params]) { ... }` override.
26
+ * `redirect:` on HTML-mode `:create`/`:update`/`:destroy`: a `Proc` called with the record, or a literal path, overriding the default success redirect.
27
+
28
+ Validation errors:
29
+ * `:create` returns `201` / `422 { errors: [...] }` and `:update` returns `200` / `422` instead of raising; `raise_on_invalid: true` keeps strict `create!`/`update!` semantics.
30
+
31
+ Authorization changes:
32
+ * Breaking: `authorize: true` now always enforces policy checks, including for `authenticate: false` actions, which reach policies with a `nil` user. Policies must tolerate a `nil` user.
33
+ * Breaking: `:index` authorizes the model class instead of an unsaved instance, matching the Pundit/CanCanCan collection conventions. `index?` policies can no longer inspect record state (there is none), so write them against the user.
34
+ * `SimpleCrud::Config.user_method` (default `:current_user`) selects the controller method providing the user to policies and scope lambdas, so apps with other conventions (`current_admin`, ...) need no shims.
35
+
36
+ Adapter interfaces:
37
+ * Authorization adapters gain `policy_scope`; pagination adapters gain `paginated_records`, so HTML-mode `:index` can fetch the current page without rendering. Both are implemented by every shipped adapter (Pundit, CanCanCan, Action Policy / wor-paginate, Kaminari, will_paginate, Pagy).
38
+
39
+ Fixes:
40
+ * Kaminari adapter: string query params corrupted offsets (`?page=3&per_page=10` resolved to `OFFSET 1010`), and `per_page` now respects `Kaminari.config.max_per_page`.
41
+ * Custom finders returning anything but a single record raise `ActiveRecord::RecordNotFound` instead of failing later with an obscure error.
42
+ * Invalid `simple_crud_for` usage (unknown action, missing policy, missing serializer) raises `ArgumentError` instead of throwing uncatchable string tags.
43
+
44
+ Testing library:
45
+ * The shared examples are a reusable library: configure via `SimpleCrud::RSpec.configure`, wire up explicitly with `SimpleCrud::RSpec.install!`. Every app-specific assumption has an overridable default matching the gem's own stack (Devise-JWT + FactoryBot + Pundit + AMS): `authenticate`/`current_user`/`other_user`, `create_record`/`create_records`/`params_for`, `owner_association`/`required_attribute`/`required_error`, `finder_key`, `params_key`, `route_params`, `invalid_status`, `unauthenticated_status`, `assert_html_template`, `policy_class`, `serializer_class`.
46
+ * Examples are HTML-aware (branching on the `html:` option, inferring it for block-form actions) and issue requests by action name, so one set covers API and server-rendered controllers, namespaced and nested routes alike.
47
+ * Refactor `SimpleCrudController` helpers into `SimpleCrud::ControllerHelpers`, dedupe repeated test code, and require `rails-controller-testing` for template assertions. Tested across Rails 6.1-8.1.
48
+
3
49
  ### V0.2.1
4
50
  * Rename the published gem from `simple_crud` to `wor-simple_crud`. `simple_crud` is already taken on RubyGems by an unrelated gem, and RubyGems also rejects names that differ from an existing one only by hyphen/underscore (so `simple-crud` was rejected too). Install with `gem 'wor-simple_crud', require: 'simple_crud'` (the require path is unchanged).
5
51
  * Drop the last Wolox references now that this gem lives at icoluccio/simple-crud, and point the wor-paginate link and CI/gem badges at their new homes.
data/Gemfile CHANGED
@@ -28,6 +28,7 @@ group :development, :test do
28
28
  gem 'overcommit', '~> 0.72'
29
29
  gem 'pagy', '~> 43.0', require: false
30
30
  gem 'pundit', '~> 2.5'
31
+ gem 'rails-controller-testing', '~> 1.0'
31
32
  gem 'rake', '~> 13.4'
32
33
  gem 'rspec', '~> 3.13'
33
34
  gem 'rspec-rails', '>= 6.0', '< 9'
data/README.md CHANGED
@@ -14,7 +14,10 @@ SimpleCrud
14
14
  - [Options](#options)
15
15
  - [Paginate](#paginate)
16
16
  - [Authorize](#authorize)
17
+ - [Authenticate](#authenticate)
17
18
  - [Serializer](#serializer)
19
+ - [HTML](#html)
20
+ - [Finder](#finder)
18
21
  - [Shared examples](#shared-examples)
19
22
  - [Contributing](#contributing)
20
23
  - [Releases](#releases)
@@ -64,7 +67,7 @@ Before SimpleCrud can be used, some boilerplate is needed. Add the following to
64
67
  ```ruby
65
68
  include Pundit::Authorization
66
69
  include Wor::Paginate
67
- extend SimpleCrud
70
+ extend SimpleCrudController
68
71
 
69
72
  before_action :set_params
70
73
 
@@ -91,6 +94,8 @@ simple_crud_for :show
91
94
  simple_crud_for :index
92
95
  simple_crud_for :create
93
96
  simple_crud_for :destroy
97
+ simple_crud_for :new
98
+ simple_crud_for :edit
94
99
  ```
95
100
 
96
101
  Each method supports different options, as in:
@@ -102,6 +107,12 @@ simple_crud_for :index, paginate: false, authorize: false, serializer: CustomSer
102
107
  - Authorize: whether it should check authorization via the configured authorization adapter (Pundit by default)
103
108
  - Authenticate: whether it should use Devise to check for a current_user
104
109
  - Serializer: specify a particular serializer you should use
110
+ - Html: renders the action's ERB template instead of JSON (valid for `:index`, `:show`, `:new`, `:edit`, `:create`, `:update` and `:destroy`). Only meaningful in controllers that render templates
111
+ - Scope: only valid for `:index`. A `Proc`/`lambda` taking `current_user` (plus the controller's `params` if it takes a second argument) that returns the relation to list, overriding the default `policy_scope`. The user is resolved via `SimpleCrud::Config.user_method` (`:current_user` by default; set it to e.g. `:current_admin`)
112
+ - Finder: only valid for `:show`, `:update`, `:destroy` and `:edit`. A `Proc`/`lambda` (invoked with the controller's params) or a `Symbol` naming a class method on the model, used to look up the record instead of `klass.find(params[:id])`.
113
+ - Build: only valid for `:new` and `:create`. A `Proc`/`lambda` that builds the record (invoked with the controller as `self`, so `current_user`, `params` and any instance variables are available), for building nested or owner-scoped records like `current_user.projects.build`. `:create` then assigns the permitted params to the built record before saving
114
+ - Raise_on_invalid: only valid for `:create` and `:update`. Keeps the strict `create!`/`update!` semantics (raising on invalid input) instead of returning `422` with the validation errors
115
+ - Redirect: only valid for HTML-mode `:create`, `:update` and `:destroy`. A `Proc`/`lambda` (called with the record) or a literal path used as the success redirect target. Defaults to the record (`:create`/`:update`) or the model's collection path (`:destroy`)
105
116
 
106
117
  You'll need a few things so they work correctly:
107
118
 
@@ -143,10 +154,11 @@ end
143
154
 
144
155
  SimpleCrud.configure { |config| config.pagination_adapter = MyPaginationAdapter.new }
145
156
  ```
146
-
147
157
  #### Authorize
148
158
  Authorization checks go through [Pundit](https://github.com/varvet/pundit) by default. Name the policy after the model plus `Policy`, e.g. `AuthorPolicy`, written as a regular Pundit policy:
149
159
 
160
+ > **Note:** authorization runs whenever `authorize: true`, independently of `authenticate`. With no signed-in user the policy receives `nil` as the user, so write policies that tolerate it (e.g. `user.nil? ? false : ...`). Devise provides `authenticate_user!` and `current_user` for you; non-Devise apps must define a `current_user` (or point `SimpleCrud::Config.user_method` at their own method) for policies and `scope:` lambdas to receive the user.
161
+
150
162
  ```ruby
151
163
  class AuthorPolicy
152
164
  attr_reader :user, :author
@@ -159,10 +171,30 @@ class AuthorPolicy
159
171
  def show?
160
172
  user.present?
161
173
  end
174
+
175
+ class Scope
176
+ def initialize(user, scope)
177
+ @user = user
178
+ @scope = scope
179
+ end
180
+
181
+ # Used by :index to scope the listed records to the current user.
182
+ def resolve
183
+ @scope.where(user: @user)
184
+ end
185
+ end
162
186
  end
163
187
 
164
188
  ```
165
189
 
190
+ When `authorize: true`, the `:index` action paginates the Pundit `policy_scope` of the model (falling back to the full relation when no `Scope` is defined) instead of `klass.all`, so "only my records" scoping works out of the box. Override the scope per action with the `scope:` option, a callable that receives the user resolved via `SimpleCrud::Config.user_method` (`nil` when there is none, and `params` too when it takes a second argument):
191
+
192
+ ```ruby
193
+ SimpleCrud.configure { |c| c.user_method = :current_admin } # non-Devise naming conventions
194
+ simple_crud_for :index, scope: ->(user) { Model.visible_to(user) }
195
+ simple_crud_for :index, scope: ->(user, params) { Model.where(status: params[:status]).visible_to(user) }
196
+ ```
197
+
166
198
  Prefer CanCanCan or Action Policy instead? Both have adapters ready to go:
167
199
 
168
200
  ```ruby
@@ -200,6 +232,13 @@ class MyAuthorizationAdapter
200
232
  def policy_defined?(model_class)
201
233
  Kernel.const_defined?("#{model_class}Policy")
202
234
  end
235
+
236
+ # Called when rendering :index with authorize: true. Must return the
237
+ # relation the current user is allowed to list. The default returns
238
+ # the full relation.
239
+ def policy_scope(controller, model_class)
240
+ model_class.all
241
+ end
203
242
  end
204
243
 
205
244
  SimpleCrud.configure do |config|
@@ -219,6 +258,67 @@ class AuthorSerializer < ActiveModel::Serializer
219
258
  end
220
259
  ```
221
260
 
261
+ #### HTML
262
+ For server-rendered apps, `html: true` renders the action's ERB template instead of JSON, so be sure the controller can render templates (e.g. `ActionController::Base`). Behavior per action:
263
+
264
+ - `:index` renders `index.html.erb` with the paginated records exposed as `@records` (pagination still applies, or use `paginate: false`).
265
+ - `:show` renders `show.html.erb` with the record exposed as `@record` (custom `finder:` still applies).
266
+ - `:new` builds a new record, authorizes it, and renders `new.html.erb` with it exposed as `@record`.
267
+ - `:edit` looks the record up (honoring `finder:`), authorizes it, and renders `edit.html.erb` with it exposed as `@record`.
268
+ - `:create` saves and redirects to the created record on success, or re-renders `new.html.erb` (with `@record` and its errors) on failure.
269
+ - `:update` saves and redirects to the record on success, or re-renders `edit.html.erb` on failure.
270
+ - `:destroy` destroys and redirects to the collection (`redirect_to Model`) on success, or re-renders `show.html.erb` if a callback aborts the destroy.
271
+
272
+ ```ruby
273
+ simple_crud_for :index, html: true
274
+ simple_crud_for :show, html: true
275
+ simple_crud_for :new, html: true
276
+ simple_crud_for :edit, html: true
277
+ simple_crud_for :create, html: true
278
+ simple_crud_for :update, html: true
279
+ simple_crud_for :destroy, html: true
280
+ ```
281
+
282
+ Or pass a block that renders explicitly, overriding the auto-render. The block receives the records for `:index`, the record for `:show`/`:new`, or the record plus a saved flag for `:create`/`:update`/`:destroy`. Passing a block also implies `html: true` for the shared examples (so a server-rendered block is asserted as HTML); if your block renders JSON instead, pass `html: false` explicitly:
283
+
284
+ ```ruby
285
+ simple_crud_for :index do |records|
286
+ render :index, locals: { models: records }
287
+ end
288
+
289
+ simple_crud_for :create do |record, saved|
290
+ saved ? redirect_to(record) : render(:new, locals: { model: record })
291
+ end
292
+ ```
293
+
294
+ #### Build
295
+ `simple_crud_for :new` and `simple_crud_for :create` build the record with `klass.new`, which can't express owner-scoped or nested records (`current_user.projects.build`, `@project.tasks.build`). Pass a `build:` lambda; it runs with the controller as `self`, so `current_user`, `params` and any instance variables set by a `before_action` are available:
296
+
297
+ ```ruby
298
+ simple_crud_for :new, build: -> { current_user.projects.build }
299
+ simple_crud_for :create, build: -> { @project.tasks.build }
300
+ ```
301
+
302
+ `:create` assigns the permitted params to the built record before saving, so the owner/parent association survives. `:update` keeps finding the record via the `finder:`.
303
+
304
+ #### Edit
305
+ `simple_crud_for :edit` is the find-instead-of-build twin of `:new`, the last hand-written CRUD action in server-rendered apps. It looks the record up (honoring `finder:` exactly like `:show`), authorizes it, and renders `edit.html.erb` with the record exposed as `@record`; in JSON mode it returns the record:
306
+
307
+ ```ruby
308
+ simple_crud_for :edit, finder: ->(params) { Project.find_by!(slug: params[:slug]) }
309
+ ```
310
+
311
+ #### Finder
312
+ By default records are looked up by primary key via `klass.find(params[:id])`. The `finder:` option on `:show`, `:update` and `:destroy` replaces that with any lookup you want: pretty URLs (`resources :posts, param: :slug`), tokens, composite keys, or scoping by a parent resource. Pass a `Proc`/`lambda` that maps the controller's `params` to a record, or a `Symbol` naming a class method on the model that takes the params:
313
+
314
+ ```ruby
315
+ simple_crud_for :show, finder: ->(params) { Model.find_by!(token: params[:token]) }
316
+ simple_crud_for :update, finder: :find_by_slug
317
+ simple_crud_for :destroy, finder: ->(params) { current_user.models.find(params[:id]) }
318
+ ```
319
+
320
+ When omitted it defaults to `klass.find(params[:id])`, and `not_found` is still returned whenever the finder finds no record.
321
+
222
322
  ### Shared examples
223
323
  While optional, using the included shared examples saves you from writing the standard test cases for the methods. You can even use them if you didn't use `simple_crud_for`, as a set of basic tests. To include them, just add `require 'simple_crud/rspec'` to your `rails_helper.rb` file and add the lines you need to your `*_spec.rb` files:
224
324
  ```ruby
@@ -233,8 +333,106 @@ describe V1::Backoffice::AuthorsController do
233
333
  end
234
334
  ```
235
335
 
336
+ The `create` and `update` examples cover the `422` response with validation errors (skipped when `raise_on_invalid: true`, or whenever the action uses a render block, since the block decides how the response is rendered), and all base examples adapt to `html: true` controllers (asserting the rendered template/redirect instead of JSON). Controllers using the extra options can include their dedicated examples too:
337
+
338
+ ```ruby
339
+ include_examples 'simple crud for new' # the :new action
340
+ include_examples 'simple crud for edit' # the :edit action
341
+ include_examples 'simple crud for index with block' # render block
342
+ include_examples 'simple crud for index with scope' # scope: ->(user) { ... }
343
+ include_examples 'simple crud for show with block' # render block on :show
344
+ include_examples 'simple crud for new with block' # render block on :new
345
+ include_examples 'simple crud for create with block' # render block on :create
346
+ include_examples 'simple crud for destroy with block' # render block on :destroy
347
+ include_examples 'simple crud for show with finder' # finder on :show
348
+ include_examples 'simple crud for update with finder' # finder on :update
349
+ include_examples 'simple crud for destroy with finder' # finder on :destroy
350
+ include_examples 'simple crud for new with build' # build: -> { ... } on :new
351
+ include_examples 'simple crud for create with build' # build: -> { ... } on :create
352
+ ```
353
+
236
354
  It's not needed to specify paginate: true and such, since the shared examples will use the configuration that was originally passed to simple_crud_for
237
355
 
356
+ #### Adopting the shared examples
357
+ The shared examples assume the gem's own stack by default (Devise-JWT authentication, FactoryBot, Pundit, ActiveModel Serializers). Wiring is opt-in: require the file and call `SimpleCrud::RSpec.install!` in `spec/spec_helper.rb` (or `rails_helper.rb`), then configure anything your app differs on:
358
+
359
+ ```ruby
360
+ SimpleCrud::RSpec.configure do |config|
361
+ # How the current user (and a secondary "other user") is built.
362
+ config.current_user = -> { User.create!(email: 'user@example.com', password: 'secret') }
363
+ config.other_user = -> { User.create!(email: 'other@example.com', password: 'secret') }
364
+
365
+ # How to sign the current user in for a request (no Devise-JWT here).
366
+ config.authenticate = -> { request.session[:user_id] = current_user.id }
367
+
368
+ # How records and create/update params are built (no FactoryBot here).
369
+ config.create_record = ->(klass, attributes) { klass.create!(attributes) }
370
+ config.create_records = ->(klass, count, attributes) { count.times.map { klass.create!(attributes) } }
371
+ config.params_for = ->(klass) { klass.new.attributes.slice('title') }
372
+
373
+ # The owner association used when building records for the examples
374
+ # (model_attributes defaults to `{ owner_association => current_user }`;
375
+ # override model_attributes directly for multi-key or non-user-owned models).
376
+ config.owner_association = :owner
377
+ config.model_attributes = -> { { owner: current_user } }
378
+ config.required_attribute = :title
379
+ config.required_error = "Title can't be blank"
380
+
381
+ # Redirect-based apps: unauthenticated requests get a redirect, not a 401.
382
+ # And if the rendered template name doesn't match the action, drop the
383
+ # render_template assertion.
384
+ config.unauthenticated_status = :found
385
+ config.assert_html_template = false
386
+
387
+ # Server-rendered apps usually use nested strong params
388
+ # (params.require(:task).permit(:name)); wrap request bodies under the
389
+ # model's params key instead of posting flat params.
390
+ config.params_key = :task
391
+
392
+ # Nested resources (/projects/:project_slug/tasks): extra params
393
+ # (e.g. the parent slug) added to every request.
394
+ config.route_params = -> { { project_slug: project.slug } }
395
+
396
+ # Re-render the form with a 422 on validation failure (instead of 200).
397
+ config.invalid_status = :unprocessable_entity
398
+
399
+ # How the 'simple crud for index with scope' example builds the records
400
+ # that match the controller's scope and the ones that must be excluded.
401
+ # Defaults derive from model_attributes (mine) plus an owner_association
402
+ # => other_user variant (theirs); override both when your scope keys on
403
+ # something other than the owner association.
404
+ config.scoped_attributes = -> { { project: project } }
405
+ config.other_scoped_attributes = -> { { project: other_project } }
406
+
407
+ # Pundit policy and serializer class lookup.
408
+ config.policy_class = ->(klass) { "#{klass}Policy".constantize }
409
+ config.serializer_class = ->(model) { "#{model.class}Serializer".constantize }
410
+
411
+ # Optional extra assertions on the persisted record in the create/update
412
+ # success paths (catches regressions that save into the wrong parent).
413
+ config.created_record_check = ->(record) { expect(record.project).to eq(project) }
414
+ end
415
+ ```
416
+
417
+ Each setting has a sensible default, so you only override what differs. Callable settings (`current_user`, `authenticate`, `create_record`, `create_records`, `params_for`, `model_attributes`, `scoped_attributes`, `other_scoped_attributes`, `policy_class`, `serializer_class`) run in the example-group context, so they can call `request`, `create`, `current_user`, etc.; `created_record_check` receives the persisted record. The examples are controller-agnostic (they issue requests by action name, not hardcoded paths), so they work for namespaced and nested controllers alike. If you keep `assert_html_template` on (the default), add `gem 'rails-controller-testing'` for the `render_template` matcher.
418
+
419
+ #### Per-controller overrides via metadata
420
+
421
+ Any setting can also be overridden per controller (or per example) with `simple_crud:` metadata instead of globally, useful when the app matches the defaults except for one resource. Metadata wins over global config, and every read happens at example runtime, so there are no around hooks or state to restore:
422
+
423
+ ```ruby
424
+ RSpec.describe TasksController, type: :controller, simple_crud: {
425
+ model_attributes: -> { { project: project } },
426
+ route_params: -> { { project_slug: project.slug } }
427
+ } do
428
+ let(:project) { create(:project, owner: current_user) }
429
+
430
+ include_examples 'simple crud for show'
431
+ end
432
+ ```
433
+
434
+ Callable settings resolve in the example context, so their lambdas can close over the spec's `let`s; that's how records land in the same project the route params point at.
435
+
238
436
  ## Contributing
239
437
 
240
438
  1. Fork it
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ # Builds the lambda installed as each action declared with simple_crud_for.
5
+ module ActionLambdas
6
+ def crud_lambda_for_show(klass, parameters = {}, &block)
7
+ lambda do
8
+ authenticate_user! if parameters[:authenticate]
9
+ requested = SimpleCrudController.find_record(klass, self, parameters)
10
+
11
+ options = {}.merge(serializer: parameters[:serializer]).compact
12
+ SimpleCrudController.maybe_authorize(self, requested, parameters)
13
+ SimpleCrudController.render_show(self, requested, options, parameters, &block)
14
+ end
15
+ end
16
+
17
+ def crud_lambda_for_new(klass, parameters = {}, &block)
18
+ lambda do
19
+ authenticate_user! if parameters[:authenticate]
20
+ record = SimpleCrudController.build_record(self, klass, parameters)
21
+ SimpleCrudController.maybe_authorize(self, record, parameters)
22
+ SimpleCrudController.render_new(self, record, parameters, &block)
23
+ end
24
+ end
25
+
26
+ # The find-instead-of-build twin of :new.
27
+ def crud_lambda_for_edit(klass, parameters = {}, &block)
28
+ lambda do
29
+ authenticate_user! if parameters[:authenticate]
30
+ requested = SimpleCrudController.find_record(klass, self, parameters)
31
+ SimpleCrudController.maybe_authorize(self, requested, parameters)
32
+ SimpleCrudController.render_edit(self, requested, parameters, &block)
33
+ end
34
+ end
35
+
36
+ def crud_lambda_for_index(klass, parameters = {}, &block)
37
+ lambda do
38
+ authenticate_user! if parameters[:authenticate]
39
+ SimpleCrudController.maybe_authorize(self, klass, parameters)
40
+ options = {}.merge(each_serializer: parameters[:serializer]).compact
41
+ SimpleCrudController.render_index(self, klass, options, parameters, &block)
42
+ end
43
+ end
44
+
45
+ def crud_lambda_for_create(klass, parameters = {}, &block)
46
+ lambda do
47
+ authenticate_user! if parameters[:authenticate]
48
+ permitted_params = send("#{self.class.simple_crud_controller_model.to_s.underscore}_params")
49
+ record = SimpleCrudController.build_record(self, klass, parameters)
50
+ record.assign_attributes(permitted_params)
51
+ SimpleCrudController.maybe_authorize(self, record, parameters)
52
+ persist = ->(bang:) { bang ? record.save! : record.save }
53
+ options = { status: :created, failure_template: :new }
54
+ SimpleCrudController.save_and_render(self, record, parameters, options, persist, &block)
55
+ end
56
+ end
57
+
58
+ def crud_lambda_for_update(klass, parameters = {}, &block)
59
+ lambda do
60
+ authenticate_user! if parameters[:authenticate]
61
+ requested = SimpleCrudController.find_record(klass, self, parameters)
62
+ SimpleCrudController.maybe_authorize(self, requested, parameters)
63
+ permitted_params = send("#{self.class.simple_crud_controller_model.to_s.underscore}_params")
64
+ persist = ->(bang:) { bang ? requested.update!(permitted_params) : requested.update(permitted_params) }
65
+ options = { status: :ok, failure_template: :edit }
66
+ SimpleCrudController.save_and_render(self, requested, parameters, options, persist, &block)
67
+ end
68
+ end
69
+
70
+ def crud_lambda_for_destroy(klass, parameters = {}, &block)
71
+ lambda do
72
+ authenticate_user! if parameters[:authenticate]
73
+ requested = SimpleCrudController.find_record(klass, self, parameters)
74
+ SimpleCrudController.maybe_authorize(self, requested, parameters)
75
+ options = { status: :ok, failure_template: :show, redirect: parameters[:redirect] || klass }
76
+ persist = ->(bang:) { bang ? requested.destroy! : requested.destroy }
77
+ SimpleCrudController.persist_and_render(self, requested, parameters, options, persist, &block)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -15,6 +15,13 @@ module SimpleCrud
15
15
  def policy_defined?(model_class)
16
16
  raise NotImplementedError
17
17
  end
18
+
19
+ # Called when rendering :index with authorize: true. Must return the
20
+ # relation the current user is allowed to list. Defaults to the full
21
+ # relation so no scoping dependency is required.
22
+ def policy_scope(_controller, model_class)
23
+ model_class.all
24
+ end
18
25
  end
19
26
  end
20
27
  end
@@ -16,6 +16,12 @@ module SimpleCrud
16
16
  def policy_defined?(model_class)
17
17
  Kernel.const_defined?("#{model_class}Policy")
18
18
  end
19
+
20
+ def policy_scope(controller, model_class)
21
+ controller.send(:policy_scope, model_class)
22
+ rescue Pundit::NotDefinedError
23
+ model_class.all
24
+ end
19
25
  end
20
26
  end
21
27
  end
@@ -4,7 +4,7 @@ module SimpleCrud
4
4
  # Holds simple_crud's configurable pieces, set via SimpleCrud.configure.
5
5
  class Config
6
6
  class << self
7
- attr_writer :authorization_adapter, :pagination_adapter
7
+ attr_writer :authorization_adapter, :pagination_adapter, :user_method
8
8
 
9
9
  def authorization_adapter
10
10
  @authorization_adapter ||= begin
@@ -19,6 +19,11 @@ module SimpleCrud
19
19
  Pagination::WorPaginateAdapter.new
20
20
  end
21
21
  end
22
+
23
+ # Controller method providing the current user; override for non-Devise apps.
24
+ def user_method
25
+ @user_method ||= :current_user
26
+ end
22
27
  end
23
28
  end
24
29
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ # Class-level helpers shared by the CRUD lambdas defined in SimpleCrudController.
5
+ module ControllerHelpers
6
+ def maybe_authorize(controller, record, parameters)
7
+ return unless parameters[:authorize]
8
+
9
+ SimpleCrud::Config.authorization_adapter.authorize(controller, record)
10
+ end
11
+
12
+ def render_index(controller, klass, options, parameters, &block)
13
+ relation = index_relation(controller, klass, parameters)
14
+
15
+ if parameters[:html] || block
16
+ records = index_records(controller, relation, options, parameters)
17
+ controller.instance_variable_set(:@records, records)
18
+ block ? controller.instance_exec(records, &block) : controller.render(:index)
19
+ elsif parameters[:paginate]
20
+ SimpleCrud::Config.pagination_adapter.paginate(controller, relation, options)
21
+ else
22
+ controller.render({ json: relation }.merge(options))
23
+ end
24
+ end
25
+
26
+ def render_show(controller, record, options, parameters, &block)
27
+ if parameters[:html] || block
28
+ controller.instance_variable_set(:@record, record)
29
+ block ? controller.instance_exec(record, &block) : controller.render(:show)
30
+ else
31
+ controller.render({ json: record }.merge(options))
32
+ end
33
+ end
34
+
35
+ def render_new(controller, record, parameters, &block)
36
+ render_form(controller, record, :new, parameters, &block)
37
+ end
38
+
39
+ def render_edit(controller, record, parameters, &block)
40
+ render_form(controller, record, :edit, parameters, &block)
41
+ end
42
+
43
+ def render_form(controller, record, template, parameters, &block)
44
+ if parameters[:html] || block
45
+ controller.instance_variable_set(:@record, record)
46
+ block ? controller.instance_exec(record, &block) : controller.render(template)
47
+ else
48
+ options = {}.merge(serializer: parameters[:serializer]).compact
49
+ controller.render({ json: record }.merge(options))
50
+ end
51
+ end
52
+
53
+ def build_record(controller, klass, parameters)
54
+ parameters[:build] ? controller.instance_exec(&parameters[:build]) : klass.new
55
+ end
56
+
57
+ def index_relation(controller, klass, parameters)
58
+ if parameters[:scope]
59
+ call_scope(parameters[:scope], controller)
60
+ elsif parameters[:authorize]
61
+ SimpleCrud::Config.authorization_adapter.policy_scope(controller, klass)
62
+ else
63
+ klass.all
64
+ end
65
+ end
66
+
67
+ def call_scope(scope, controller)
68
+ user_method = SimpleCrud::Config.user_method
69
+ user = controller.respond_to?(user_method) ? controller.public_send(user_method) : nil
70
+ return scope.call(user) if scope.arity == 1
71
+
72
+ scope.call(user, controller.params)
73
+ end
74
+
75
+ def index_records(controller, relation, options, parameters)
76
+ return relation unless parameters[:paginate]
77
+
78
+ SimpleCrud::Config.pagination_adapter.paginated_records(controller, relation, options)
79
+ end
80
+
81
+ def find_record(klass, controller, parameters)
82
+ record = lookup_record(klass, controller, parameters)
83
+ raise ActiveRecord::RecordNotFound, "couldn't find #{klass}" if record.nil?
84
+ unless record.is_a?(ActiveRecord::Base)
85
+ raise ActiveRecord::RecordNotFound, "#{klass} finder must return a single record"
86
+ end
87
+
88
+ record
89
+ end
90
+
91
+ def lookup_record(klass, controller, parameters)
92
+ finder = parameters[:finder]
93
+ return klass.find(controller.params[:id]) if finder.nil?
94
+ return klass.send(finder, controller.params) unless finder.respond_to?(:call)
95
+
96
+ finder.call(controller.params)
97
+ end
98
+
99
+ def persist_and_render(controller, record, parameters, options, persist, &block)
100
+ saved = parameters[:raise_on_invalid] ? persist.call(bang: true) : persist.call(bang: false)
101
+ return render_persisted(controller, record, saved, options) unless block || parameters[:html]
102
+
103
+ controller.instance_variable_set(:@record, record)
104
+ return controller.instance_exec(record, saved, &block) if block
105
+
106
+ if saved
107
+ controller.redirect_to(redirect_target(controller, record, options[:redirect]))
108
+ else
109
+ controller.render(options[:failure_template])
110
+ end
111
+ end
112
+
113
+ def save_and_render(controller, record, parameters, options, persist, &block)
114
+ persist_and_render(controller, record, parameters,
115
+ options.merge(redirect: parameters[:redirect] || record), persist, &block)
116
+ end
117
+
118
+ def redirect_target(controller, record, target)
119
+ target.is_a?(Proc) ? controller.instance_exec(record, &target) : target
120
+ end
121
+
122
+ def render_persisted(controller, record, saved, options)
123
+ return controller.render(json: record, status: options[:status]) if saved
124
+
125
+ controller.render json: { errors: record.errors.full_messages }, status: 422
126
+ end
127
+ end
128
+ end
@@ -6,7 +6,13 @@ module SimpleCrud
6
6
  # (the default, see WorPaginateAdapter).
7
7
  module Adapter
8
8
  # Called when paginate: true. Must render the response itself.
9
- def paginate(controller, klass, options)
9
+ def paginate(controller, relation, options)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ # Called when rendering :index as HTML (html: true or a block given) and
14
+ # paginate: true. Must return the current page's records without rendering.
15
+ def paginated_records(controller, relation, options)
10
16
  raise NotImplementedError
11
17
  end
12
18
  end