wor-simple_crud 0.3.0 → 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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/CHANGELOG.md +16 -2
  4. data/README.md +57 -14
  5. data/lib/simple_crud/action_lambdas.rb +81 -0
  6. data/lib/simple_crud/controller_helpers.rb +11 -2
  7. data/lib/simple_crud/rspec/config.rb +18 -5
  8. data/lib/simple_crud/rspec/helpers/authentication.rb +32 -0
  9. data/lib/simple_crud/rspec/helpers/controller_options.rb +19 -0
  10. data/lib/simple_crud/rspec/helpers/models.rb +55 -0
  11. data/lib/simple_crud/rspec/helpers/policies.rb +25 -0
  12. data/lib/simple_crud/rspec/helpers/requests.rb +45 -0
  13. data/lib/simple_crud/rspec/helpers/settings.rb +49 -0
  14. data/lib/simple_crud/rspec/helpers.rb +16 -138
  15. data/lib/simple_crud/rspec.rb +1 -0
  16. data/lib/simple_crud/simple_crud_controller.rb +11 -74
  17. data/lib/simple_crud/version.rb +1 -1
  18. data/lib/spec/shared_examples/simple_crud_for_create.rb +15 -12
  19. data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +25 -7
  20. data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +2 -2
  21. data/lib/spec/shared_examples/simple_crud_for_destroy.rb +25 -0
  22. data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +10 -2
  23. data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +17 -6
  24. data/lib/spec/shared_examples/simple_crud_for_edit.rb +79 -0
  25. data/lib/spec/shared_examples/simple_crud_for_index.rb +1 -1
  26. data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +11 -3
  27. data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +25 -13
  28. data/lib/spec/shared_examples/simple_crud_for_new.rb +1 -1
  29. data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +13 -2
  30. data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +10 -4
  31. data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +13 -2
  32. data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +16 -7
  33. data/lib/spec/shared_examples/simple_crud_for_update.rb +16 -9
  34. data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +16 -6
  35. data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +2 -1
  36. data/spec/block_finder/dummy_models_controller_spec.rb +10 -0
  37. data/spec/block_json/dummy_models_controller_spec.rb +9 -0
  38. data/spec/block_redirect/dummy_models_controller_spec.rb +7 -0
  39. data/spec/dummy/app/controllers/block_finder/dummy_models_controller.rb +25 -0
  40. data/spec/dummy/app/controllers/block_json/dummy_models_controller.rb +17 -0
  41. data/spec/dummy/app/controllers/block_redirect/dummy_models_controller.rb +13 -0
  42. data/spec/dummy/app/controllers/finder/dummy_models_controller.rb +1 -0
  43. data/spec/dummy/app/controllers/html_finder/dummy_models_controller.rb +17 -0
  44. data/spec/dummy/app/controllers/html_scoped/dummy_models_controller.rb +1 -1
  45. data/spec/dummy/app/controllers/unpaginated_scoped/dummy_models_controller.rb +16 -0
  46. data/spec/dummy/app/models/dummy_model_policy.rb +5 -1
  47. data/spec/dummy/app/views/html_finder/dummy_models/edit.html.erb +1 -0
  48. data/spec/dummy/app/views/html_finder/dummy_models/new.html.erb +1 -0
  49. data/spec/dummy/app/views/html_finder/dummy_models/show.html.erb +1 -0
  50. data/spec/dummy/config/routes.rb +21 -1
  51. data/spec/dummy_model_policy_spec.rb +18 -8
  52. data/spec/finder/dummy_models_controller_spec.rb +1 -0
  53. data/spec/html_finder/dummy_models_controller_spec.rb +11 -0
  54. data/spec/html_scoped/dummy_models_controller_spec.rb +1 -1
  55. data/spec/scoped_params/dummy_models_controller_spec.rb +4 -3
  56. data/spec/simple_crud/rspec_config_spec.rb +33 -0
  57. data/spec/unpaginated_scoped/dummy_models_controller_spec.rb +7 -0
  58. metadata +23 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 667672176b1c817bdcf1d9c5248e1b3701253d52f289a85bc2e4b3e632c47188
4
- data.tar.gz: f03748bda6004b670a8989fed13d82e0c6e0e0ec6bd9323a1c4e43e786da4dfe
3
+ metadata.gz: e280efd8ead44768d16e0ba663e25a40d6972b53e253281d1c7eac0735421b64
4
+ data.tar.gz: 78625a2a759ca5fda1e7d41e421db92ed721f2e2c343524f0580cd810d32ac7a
5
5
  SHA512:
6
- metadata.gz: '04257318d37af96ef5df3d17d2561dfad551cf3eaca51dc4630d7e7d9a7d19cd05852a4b31e00902ab245610d0e35379e3314586f18646afe3ddb246a17a1756'
7
- data.tar.gz: 81b0fda087fc2723f8f7a6789f7f2542037b7054b36e9db8aed05eba03f5cdbcb0c38fdf45da85f60e9b3e67c58332118d73be257e5d16b108559eade35adba7
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,17 @@
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
+
3
15
  ### V0.3.0
4
16
 
5
17
  Server-rendered (HTML) support:
@@ -7,8 +19,9 @@ Server-rendered (HTML) support:
7
19
  * New `:new` action: builds a record, authorizes it, and renders `new.html.erb` (or returns JSON in API mode).
8
20
 
9
21
  Custom lookups, scoping and building:
10
- * `finder:` on `:show`/`:update`/`:destroy`: 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])`.
11
- * `build:` on `:new`/`:create` for owner-scoped or nested builds (`current_user.classrooms.build`); runs with the controller as `self`, and `:create` assigns the permitted params to the built record before saving.
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.
12
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.
13
26
  * `redirect:` on HTML-mode `:create`/`:update`/`:destroy`: a `Proc` called with the record, or a literal path, overriding the default success redirect.
14
27
 
@@ -17,6 +30,7 @@ Validation errors:
17
30
 
18
31
  Authorization changes:
19
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.
20
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.
21
35
 
22
36
  Adapter interfaces:
data/README.md CHANGED
@@ -95,6 +95,7 @@ simple_crud_for :index
95
95
  simple_crud_for :create
96
96
  simple_crud_for :destroy
97
97
  simple_crud_for :new
98
+ simple_crud_for :edit
98
99
  ```
99
100
 
100
101
  Each method supports different options, as in:
@@ -106,10 +107,10 @@ simple_crud_for :index, paginate: false, authorize: false, serializer: CustomSer
106
107
  - Authorize: whether it should check authorization via the configured authorization adapter (Pundit by default)
107
108
  - Authenticate: whether it should use Devise to check for a current_user
108
109
  - Serializer: specify a particular serializer you should use
109
- - Html: renders the action's ERB template instead of JSON (valid for `:index`, `:show`, `:new`, `:create`, `:update` and `:destroy`). Only meaningful in controllers that render templates
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
110
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`)
111
- - Finder: only valid for `:show`, `:update` and `:destroy`. 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])`.
112
- - 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.classrooms.build`. `:create` then assigns the permitted params to the built record before saving
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
113
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
114
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`)
115
116
 
@@ -263,6 +264,7 @@ For server-rendered apps, `html: true` renders the action's ERB template instead
263
264
  - `:index` renders `index.html.erb` with the paginated records exposed as `@records` (pagination still applies, or use `paginate: false`).
264
265
  - `:show` renders `show.html.erb` with the record exposed as `@record` (custom `finder:` still applies).
265
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`.
266
268
  - `:create` saves and redirects to the created record on success, or re-renders `new.html.erb` (with `@record` and its errors) on failure.
267
269
  - `:update` saves and redirects to the record on success, or re-renders `edit.html.erb` on failure.
268
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,6 +273,7 @@ For server-rendered apps, `html: true` renders the action's ERB template instead
271
273
  simple_crud_for :index, html: true
272
274
  simple_crud_for :show, html: true
273
275
  simple_crud_for :new, html: true
276
+ simple_crud_for :edit, html: true
274
277
  simple_crud_for :create, html: true
275
278
  simple_crud_for :update, html: true
276
279
  simple_crud_for :destroy, html: true
@@ -289,15 +292,22 @@ end
289
292
  ```
290
293
 
291
294
  #### Build
292
- `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.classrooms.build`, `@classroom.assignments.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:
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:
293
296
 
294
297
  ```ruby
295
- simple_crud_for :new, build: -> { current_user.classrooms.build }
296
- simple_crud_for :create, build: -> { current_user.classrooms.build }
298
+ simple_crud_for :new, build: -> { current_user.projects.build }
299
+ simple_crud_for :create, build: -> { @project.tasks.build }
297
300
  ```
298
301
 
299
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:`.
300
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
+
301
311
  #### Finder
302
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:
303
313
 
@@ -323,10 +333,11 @@ describe V1::Backoffice::AuthorsController do
323
333
  end
324
334
  ```
325
335
 
326
- The `create` and `update` examples cover the `422` response with validation errors (skipped when `raise_on_invalid: true`), 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:
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:
327
337
 
328
338
  ```ruby
329
339
  include_examples 'simple crud for new' # the :new action
340
+ include_examples 'simple crud for edit' # the :edit action
330
341
  include_examples 'simple crud for index with block' # render block
331
342
  include_examples 'simple crud for index with scope' # scope: ->(user) { ... }
332
343
  include_examples 'simple crud for show with block' # render block on :show
@@ -359,8 +370,11 @@ SimpleCrud::RSpec.configure do |config|
359
370
  config.create_records = ->(klass, count, attributes) { count.times.map { klass.create!(attributes) } }
360
371
  config.params_for = ->(klass) { klass.new.attributes.slice('title') }
361
372
 
362
- # The owner association and the validation this app's models enforce.
363
- config.owner_association = :instructor
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 } }
364
378
  config.required_attribute = :title
365
379
  config.required_error = "Title can't be blank"
366
380
 
@@ -371,24 +385,53 @@ SimpleCrud::RSpec.configure do |config|
371
385
  config.assert_html_template = false
372
386
 
373
387
  # Server-rendered apps usually use nested strong params
374
- # (params.require(:classroom).permit(:name)); wrap request bodies under the
388
+ # (params.require(:task).permit(:name)); wrap request bodies under the
375
389
  # model's params key instead of posting flat params.
376
- config.params_key = :classroom
390
+ config.params_key = :task
377
391
 
378
- # Nested resources (/classrooms/:classroom_slug/assignments): extra params
392
+ # Nested resources (/projects/:project_slug/tasks): extra params
379
393
  # (e.g. the parent slug) added to every request.
380
- config.route_params = -> { { classroom_slug: model.classroom_slug } }
394
+ config.route_params = -> { { project_slug: project.slug } }
381
395
 
382
396
  # Re-render the form with a 422 on validation failure (instead of 200).
383
397
  config.invalid_status = :unprocessable_entity
384
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
+
385
407
  # Pundit policy and serializer class lookup.
386
408
  config.policy_class = ->(klass) { "#{klass}Policy".constantize }
387
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'
388
431
  end
389
432
  ```
390
433
 
391
- Each setting has a sensible default, so you only override what differs. Callable settings (`current_user`, `authenticate`, `create_record`, `create_records`, `params_for`, `policy_class`, `serializer_class`) run in the example-group context, so they can call `request`, `create`, `current_user`, etc. 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.
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.
392
435
 
393
436
  ## Contributing
394
437
 
@@ -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
@@ -33,11 +33,20 @@ module SimpleCrud
33
33
  end
34
34
 
35
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)
36
44
  if parameters[:html] || block
37
45
  controller.instance_variable_set(:@record, record)
38
- block ? controller.instance_exec(record, &block) : controller.render(:new)
46
+ block ? controller.instance_exec(record, &block) : controller.render(template)
39
47
  else
40
- controller.render json: record
48
+ options = {}.merge(serializer: parameters[:serializer]).compact
49
+ controller.render({ json: record }.merge(options))
41
50
  end
42
51
  end
43
52
 
@@ -14,10 +14,10 @@ module SimpleCrud
14
14
  # Serializers), so apps on a different stack can override them via
15
15
  # SimpleCrud::RSpec.configure instead of editing the examples.
16
16
  #
17
- # Callable settings (authenticate, current_user, create_record,
18
- # create_records, attributes_for, policy_class, serializer_class) run in
19
- # the example-group context, so they can call helpers such as `create`,
20
- # `request` and `current_user`.
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
21
  class Config
22
22
  DEFAULTS = {
23
23
  authenticate: lambda {
@@ -30,6 +30,18 @@ module SimpleCrud
30
30
  create_records: ->(klass, count, attributes) { create_list(klass, count, **attributes) },
31
31
  params_for: ->(klass) { attributes_for(klass) },
32
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
+ },
33
45
  required_attribute: :name,
34
46
  required_error: "Name can't be blank",
35
47
  finder_key: :slug,
@@ -39,7 +51,8 @@ module SimpleCrud
39
51
  unauthenticated_status: :unauthorized,
40
52
  assert_html_template: true,
41
53
  policy_class: ->(klass) { "#{klass}Policy".constantize },
42
- serializer_class: ->(model) { "#{model.class}_serializer".classify.constantize }
54
+ serializer_class: ->(model) { "#{model.class}_serializer".classify.constantize },
55
+ created_record_check: nil
43
56
  }.freeze
44
57
 
45
58
  class << self
@@ -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