wor-simple_crud 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +116 -8
- data/lib/simple_crud/action_context.rb +16 -2
- data/lib/simple_crud/index_context.rb +21 -10
- data/lib/simple_crud/persistence_context.rb +16 -5
- data/lib/simple_crud/relation_source.rb +107 -0
- data/lib/simple_crud/rspec/config.rb +2 -0
- data/lib/simple_crud/rspec/helpers/controller_options.rb +1 -1
- data/lib/simple_crud/rspec/helpers/policies.rb +3 -3
- data/lib/simple_crud/rspec/helpers/requests.rb +4 -1
- data/lib/simple_crud/rspec/helpers/settings.rb +10 -1
- data/lib/simple_crud/rspec.rb +6 -0
- data/lib/simple_crud/serializer.rb +2 -7
- data/lib/simple_crud/simple_crud_controller.rb +28 -7
- data/lib/simple_crud/version.rb +1 -1
- data/lib/spec/matchers/render_or_redirect.rb +7 -0
- data/lib/spec/shared_examples/simple_crud_for_create.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +2 -2
- data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +8 -2
- data/lib/spec/shared_examples/simple_crud_for_destroy.rb +11 -9
- data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_edit.rb +16 -14
- data/lib/spec/shared_examples/simple_crud_for_edit_with_block.rb +22 -0
- data/lib/spec/shared_examples/simple_crud_for_edit_with_finder.rb +34 -0
- data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +15 -12
- data/lib/spec/shared_examples/simple_crud_for_nested_resource.rb +21 -0
- data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +11 -5
- data/lib/spec/shared_examples/simple_crud_for_owned_resource.rb +45 -0
- data/lib/spec/shared_examples/simple_crud_for_show.rb +10 -8
- data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_update.rb +14 -12
- data/lib/spec/shared_examples/simple_crud_for_update_with_block.rb +40 -0
- data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +7 -4
- data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +9 -7
- data/spec/block_create_redirect/dummy_models_controller_spec.rb +11 -0
- data/spec/block_edit/dummy_models_controller_spec.rb +7 -0
- data/spec/block_finder/dummy_models_controller_spec.rb +2 -0
- data/spec/block_policy_show/dummy_models_controller_spec.rb +7 -0
- data/spec/block_redirect/dummy_models_controller_spec.rb +1 -0
- data/spec/block_update/dummy_models_controller_spec.rb +11 -0
- data/spec/dummy/app/controllers/block_create_redirect/dummy_models_controller.rb +13 -0
- data/spec/dummy/app/controllers/block_edit/dummy_models_controller.rb +9 -0
- data/spec/dummy/app/controllers/block_policy_show/dummy_models_controller.rb +12 -0
- data/spec/dummy/app/controllers/block_update/dummy_models_controller.rb +9 -0
- data/spec/dummy/app/controllers/html_notice/dummy_models_controller.rb +11 -0
- data/spec/dummy/app/controllers/owned/dummy_models_controller.rb +21 -0
- data/spec/dummy/app/controllers/parented/dummy_models_controller.rb +24 -0
- data/spec/dummy/app/controllers/scoped_order/dummy_models_controller.rb +11 -0
- data/spec/dummy/app/controllers/scoped_symbol/dummy_models_controller.rb +11 -0
- data/spec/dummy/app/controllers/singular/dummy_models_controller.rb +12 -0
- data/spec/dummy/app/models/dummy_model.rb +4 -0
- data/spec/dummy/app/serializers/dummy_model_plain_serializer.rb +8 -0
- data/spec/dummy/app/views/block_edit/dummy_models/edit.html.erb +1 -0
- data/spec/dummy/app/views/block_policy_show/dummy_models/show.html.erb +1 -0
- data/spec/dummy/app/views/html_notice/dummy_models/edit.html.erb +1 -0
- data/spec/dummy/config/routes.rb +40 -0
- data/spec/finder/dummy_models_controller_spec.rb +6 -0
- data/spec/html_finder/dummy_models_controller_spec.rb +1 -0
- data/spec/html_notice/dummy_models_controller_spec.rb +38 -0
- data/spec/owned/dummy_models_controller_spec.rb +12 -0
- data/spec/parented/dummy_models_controller_spec.rb +39 -0
- data/spec/scoped_order/dummy_models_controller_spec.rb +10 -0
- data/spec/scoped_symbol/dummy_models_controller_spec.rb +7 -0
- data/spec/simple_crud/action_context_spec.rb +15 -0
- data/spec/simple_crud/index_context_spec.rb +49 -0
- data/spec/simple_crud/relation_source_spec.rb +125 -0
- data/spec/simple_crud/serializer_spec.rb +21 -0
- data/spec/simple_crud/show_context_spec.rb +5 -0
- data/spec/simple_crud_controller_spec.rb +15 -0
- data/spec/singular/dummy_models_controller_spec.rb +10 -0
- metadata +34 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31af4e1bcfb9f45e6bcbe33a2454c96b27b2adab13ff87674a9bde462d21d2f4
|
|
4
|
+
data.tar.gz: 7fadec0262d373a35c02cd015e566a97d312c5afb110abe34f58e3ca99d88d2e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 56aa1239c57e89eaa7a3944bf2c6303033806815e9671357249958b0eb954b431a9798898fdffa4e8d70432ee650ecad9a3ab6888aff38f0398ea69454302cb3
|
|
7
|
+
data.tar.gz: 69f04e8600f4e4d10b7687c94a14a3f06b4434d12572ab969ce5f67bbb9b886e4e6ffe1d572e9a7e89f936fe999ffc435bbcc358eda12fefb0108d4f2756e67e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## Change log
|
|
2
2
|
|
|
3
|
+
### V0.8.0
|
|
4
|
+
|
|
5
|
+
Shared examples:
|
|
6
|
+
* New examples: `update with block`, `edit with finder` and `edit with block`.
|
|
7
|
+
* `required_attribute: nil` skips the invalid-attribute cases in `create`, `update` and their block variants, for models with nothing to validate. `update with finder` and `owned resource` skip their `required_attribute` assertions as well.
|
|
8
|
+
* `singular: true` covers singular resources. Requests carry no id, the "does not exist" examples are skipped, and the controller's own finder loads the record.
|
|
9
|
+
* `index with scope` handles scopes that exclude nothing. With `other_scoped_attributes: nil` it asserts every scoped record comes back, and `scoped_order: ->(records) { ... }` asserts their order.
|
|
10
|
+
* `create with block` and `destroy with block` accept a redirect or a 2xx render, so a block that redirects on success (the README's HTML example) passes. `edit` and `update with finder` accept the same when they use a block.
|
|
11
|
+
* `new with build` and `create with build` assert ownership by the current user only when an `owner_association` is configured. For `parent:`-built records they check that the hook built the record.
|
|
12
|
+
* `make_policies_succeed` takes `permissive: true`. The `show`, `new` and `edit` "with block" examples use it, so a block can ask the policy about other actions, such as `update?`, without failing the strict double.
|
|
13
|
+
|
|
14
|
+
### V0.7.0
|
|
15
|
+
|
|
16
|
+
Serializer support:
|
|
17
|
+
* `serializer:` accepts a plain class exposing a `.render` class method, with no base class or gem dependency. simple_crud calls `serializer.render(record, **options)`, so a `def self.render(record, key: default)` signature works untouched. It checks this after Blueprinter's `render_as_hash` and before the ActiveModelSerializers fallback, leaving existing Blueprinter and AMS apps unaffected.
|
|
18
|
+
|
|
19
|
+
New options:
|
|
20
|
+
* `owned_by:` (Symbol): names an association on `Config.user_method` (`current_user.posts` for `owned_by: :posts`) and supplies the default `finder:`/`build:`/index `scope:` from it, so an owned resource stops spelling out `finder: ->(params) { current_user.posts.find(params[:id]) }` and `build: -> { current_user.posts.build }` on every action. An explicit `finder:`/`build:`/`scope:` overrides the default for that option alone. On `:index`, `owned_by:` takes precedence over the Pundit `policy_scope`, and an explicit `scope:` overrides both. With no current user, an owned `:index` returns no records and an owned `:show`/`:update`/`:destroy` returns `not_found`.
|
|
21
|
+
* `parent:` names a nested route's parent and derives the same defaults from `parent.public_send(parent_association)`. A Symbol resolves a controller method first, then an `@ivar`, so a conventional `before_action` works unchanged; a `Proc` runs in controller context. `parent_association:` defaults to the controller model's plural (`ExamsController` gives `@category.exams`). An explicit `finder:`/`build:`/`scope:` overrides the default, and a nil parent fails closed (404 on find/build, empty index). `parent:` and `owned_by:` both resolve a relation from an owner, so setting both raises `ArgumentError`.
|
|
22
|
+
|
|
23
|
+
Behavior changes:
|
|
24
|
+
* `scope:` on `:index` accepts a `Symbol` naming a class method on the model, called as `klass.send(scope, user, params)`. Query composition can live on the model instead of inline in the controller.
|
|
25
|
+
* `scope:` lambdas run with the controller as `self`, so a scope can read `@ivars` and route-parent state. Arity 0 receives no arguments (it previously errored by being passed two), arity 1 the user, anything else the user and params.
|
|
26
|
+
* `simple_crud_for` accepts an Array of actions to declare several at once: `simple_crud_for %i[show create update destroy], owned_by: :tasks, serializer: TaskSerializer`.
|
|
27
|
+
* `notice:`/`alert:` on HTML `:create`/`:update`/`:destroy`: set a flash message on the success redirect (`notice`) or the failure re-render (`alert`).
|
|
28
|
+
|
|
3
29
|
### V0.6.0
|
|
4
30
|
|
|
5
31
|
Serializer support:
|
data/README.md
CHANGED
|
@@ -18,6 +18,8 @@ SimpleCrud
|
|
|
18
18
|
- [Serializer](#serializer)
|
|
19
19
|
- [HTML](#html)
|
|
20
20
|
- [Finder](#finder)
|
|
21
|
+
- [Owned by](#owned-by)
|
|
22
|
+
- [Parent](#parent)
|
|
21
23
|
- [Cache](#cache)
|
|
22
24
|
- [Controller-level defaults](#controller-level-defaults)
|
|
23
25
|
- [Shared examples](#shared-examples)
|
|
@@ -94,6 +96,12 @@ simple_crud_for :new
|
|
|
94
96
|
simple_crud_for :edit
|
|
95
97
|
```
|
|
96
98
|
|
|
99
|
+
Declare several actions with the same options at once by passing an Array:
|
|
100
|
+
```ruby
|
|
101
|
+
simple_crud_for %i[show create update destroy], owned_by: :tasks, serializer: TaskSerializer
|
|
102
|
+
simple_crud_for :destroy, status: :no_content # per-action override on top of the batch
|
|
103
|
+
```
|
|
104
|
+
|
|
97
105
|
Each method supports different options, as in:
|
|
98
106
|
```
|
|
99
107
|
simple_crud_for :index, paginate: false, authorize: false, serializer: CustomSerializer
|
|
@@ -108,11 +116,13 @@ simple_crud_for :index, paginate: false, authorize: false, serializer: CustomSer
|
|
|
108
116
|
- Status: only valid for `:create`, `:update` and `:destroy`. Overrides the success status (`:created`, `:ok` and `:ok` by default). `status: :no_content` responds with an empty body
|
|
109
117
|
- After_persist: only valid for `:create`, `:update` and `:destroy`. A `Proc`/`lambda` `->(record, saved) { ... }` that runs with the controller as `self`, after the write and before rendering. Use it for side effects like cache invalidation. It still runs when you pass a render block
|
|
110
118
|
- 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`
|
|
119
|
+
- Scope: only valid for `:index`. A `Proc`/`lambda` that runs with the controller as `self` (so `@ivars` and route-parent state are available) and returns the relation to list, overriding the default `policy_scope`. It takes `current_user` (plus the controller's `params` when it takes a second argument); an arity-0 lambda gets no arguments. The user is resolved via `SimpleCrud::Config.user_method` (`:current_user` by default; set it to e.g. `:current_admin`)
|
|
112
120
|
- 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
121
|
- 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
122
|
- 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
123
|
- 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`)
|
|
124
|
+
- Notice: only valid for HTML-mode `:create`, `:update` and `:destroy`. A flash message set on the success redirect. No-op when the controller has no `flash`
|
|
125
|
+
- Alert: only valid for HTML-mode `:create`, `:update` and `:destroy`. A `flash.now` message set before re-rendering on failure. No-op when the controller has no `flash`
|
|
116
126
|
|
|
117
127
|
#### Controller-level defaults
|
|
118
128
|
|
|
@@ -203,12 +213,19 @@ end
|
|
|
203
213
|
|
|
204
214
|
```
|
|
205
215
|
|
|
206
|
-
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
|
|
216
|
+
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 `scope:`: 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), or a `Symbol` naming a class method on the model so query composition lives on the model:
|
|
207
217
|
|
|
208
218
|
```ruby
|
|
209
219
|
SimpleCrud.configure { |c| c.user_method = :current_admin } # non-Devise naming conventions
|
|
210
220
|
simple_crud_for :index, scope: ->(user) { Model.visible_to(user) }
|
|
211
221
|
simple_crud_for :index, scope: ->(user, params) { Model.where(status: params[:status]).visible_to(user) }
|
|
222
|
+
simple_crud_for :index, scope: :visible_to # calls Model.visible_to(user, params)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
A `scope:` lambda runs with the controller as `self`, so it can reach `@ivars` and route-parent context. Arity 0 receives no arguments, arity 1 the user, anything else the user and params:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
simple_crud_for :index, scope: -> { @category.exams.includes(:questions).order(:title) }
|
|
212
229
|
```
|
|
213
230
|
|
|
214
231
|
Prefer CanCanCan or Action Policy instead? Both have adapters ready to go:
|
|
@@ -272,7 +289,7 @@ simple_crud_defaults authenticate: false, authenticate_headers: true
|
|
|
272
289
|
```
|
|
273
290
|
|
|
274
291
|
#### Serializer
|
|
275
|
-
`serializer:` picks the serializer for JSON responses on every action. Pass
|
|
292
|
+
`serializer:` picks the serializer for JSON responses on every action. Pass an [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers) serializer class, which simple_crud builds per record with `serializer.new(record, serializer_options)`; a [Blueprinter](https://github.com/blueprinter/blueprinter) blueprint class, rendered with `blueprint.render_as_hash(record, serializer_options)`; or any plain class of your own that responds to `.render`, called as `serializer.render(record, **serializer_options)`, with no base class or extra gem required.
|
|
276
293
|
|
|
277
294
|
Without `serializer:`, the response uses the record's `as_json`. The shared examples expect an `:id` attribute. Paginated `:index` with Blueprinter needs wor-paginate ≥ 0.5.
|
|
278
295
|
|
|
@@ -286,6 +303,13 @@ class AuthorBlueprint < Blueprinter::Base
|
|
|
286
303
|
fields :email, :first_name, :last_name
|
|
287
304
|
end
|
|
288
305
|
|
|
306
|
+
# A plain class works too, with no dependency on either gem above:
|
|
307
|
+
class AuthorPlainSerializer
|
|
308
|
+
def self.render(author, **)
|
|
309
|
+
{ id: author.id, email: author.email, first_name: author.first_name, last_name: author.last_name }
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
289
313
|
simple_crud_for :show, serializer: AuthorBlueprint
|
|
290
314
|
simple_crud_for :index, serializer: AuthorBlueprint, serializer_options: -> { { current_user: current_user } }
|
|
291
315
|
```
|
|
@@ -306,11 +330,13 @@ simple_crud_for :index, html: true
|
|
|
306
330
|
simple_crud_for :show, html: true
|
|
307
331
|
simple_crud_for :new, html: true
|
|
308
332
|
simple_crud_for :edit, html: true
|
|
309
|
-
simple_crud_for :create, html: true
|
|
310
|
-
simple_crud_for :update, html: true
|
|
311
|
-
simple_crud_for :destroy, html: true
|
|
333
|
+
simple_crud_for :create, html: true, notice: 'Created!', alert: 'Could not create.'
|
|
334
|
+
simple_crud_for :update, html: true, redirect: ->(record) { record_path(record) }, notice: 'Saved!'
|
|
335
|
+
simple_crud_for :destroy, html: true, notice: 'Removed.'
|
|
312
336
|
```
|
|
313
337
|
|
|
338
|
+
`notice:` sets a flash message on the success redirect; `alert:` sets a `flash.now` message before the failure re-render. Both are skipped when the controller has no `flash` (e.g. `ActionController::API`).
|
|
339
|
+
|
|
314
340
|
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`. Blocks do not change the `html:` default; pass it explicitly so the shared examples know which request format to use:
|
|
315
341
|
|
|
316
342
|
```ruby
|
|
@@ -357,6 +383,63 @@ simple_crud_for :destroy, finder: ->(params) { current_user.models.find(params[:
|
|
|
357
383
|
|
|
358
384
|
When omitted it defaults to `klass.find(params[:id])`, and `not_found` is still returned whenever the finder finds no record.
|
|
359
385
|
|
|
386
|
+
#### Owned by
|
|
387
|
+
`owned_by:` covers a resource scoped to the current user's own records. It names an association on `Config.user_method` (`current_user` by default) and derives `finder:`, `build:` and the index `scope:` from it:
|
|
388
|
+
|
|
389
|
+
```ruby
|
|
390
|
+
class PostsController < ApplicationController
|
|
391
|
+
simple_crud_defaults owned_by: :posts
|
|
392
|
+
simple_crud_for :index
|
|
393
|
+
simple_crud_for :show
|
|
394
|
+
simple_crud_for :create
|
|
395
|
+
simple_crud_for :update
|
|
396
|
+
simple_crud_for :destroy
|
|
397
|
+
end
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
This is equivalent to writing, on every action:
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
simple_crud_for :index, scope: ->(user) { user.posts }
|
|
404
|
+
simple_crud_for :show, finder: ->(params) { current_user.posts.find(params[:id]) }
|
|
405
|
+
simple_crud_for :create, build: -> { current_user.posts.build }
|
|
406
|
+
simple_crud_for :update, finder: ->(params) { current_user.posts.find(params[:id]) }
|
|
407
|
+
simple_crud_for :destroy, finder: ->(params) { current_user.posts.find(params[:id]) }
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
An explicit `finder:`, `build:` or `scope:` on an action overrides the `owned_by:` default for that option alone, so an owned `:show`/`:update`/`:destroy` can use a hand-written `:index` scope that needs extra filtering or eager loading. On `:index`, `owned_by:` takes precedence over the Pundit `policy_scope`; an explicit `scope:` overrides both. With no current user, an owned `:index` returns no records and an owned `:show`/`:update`/`:destroy` returns `not_found`, instead of falling back to the unscoped relation.
|
|
411
|
+
|
|
412
|
+
Keep the owner's foreign key out of strong params. If `owned_by: :posts` sets the owner through the built association, `params.permit(:user_id)` lets a request override it.
|
|
413
|
+
|
|
414
|
+
#### Parent
|
|
415
|
+
`parent:` covers a nested route. It derives the same `finder:`, `build:` and index `scope:` from an association on a parent resource instead of on the current user. Both options resolve an association on some owner, so setting them together raises `ArgumentError`.
|
|
416
|
+
|
|
417
|
+
`parent:` takes a `Symbol` that resolves a controller method first, then an `@ivar`. A `before_action` that sets `@category` works unchanged:
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
class ExamsController < ApplicationController
|
|
421
|
+
before_action :set_category
|
|
422
|
+
|
|
423
|
+
simple_crud_defaults parent: :category # reads the category method, else @category
|
|
424
|
+
simple_crud_for %i[index show new create], serializer: ExamSerializer, html: true
|
|
425
|
+
|
|
426
|
+
private
|
|
427
|
+
|
|
428
|
+
def set_category = @category = Category.find_by!(slug: params.expect(:category_slug))
|
|
429
|
+
end
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
The association comes from the controller's model, so `ExamsController` gets `@category.exams`. Pass `parent_association:` when the name differs, or a `Proc` (run in controller context) for a parent that isn't an ivar or method:
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
simple_crud_for :show, parent: :category, parent_association: :published_exams
|
|
436
|
+
simple_crud_for :show, parent: -> { @listing.interview }
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
As with `owned_by:`, an explicit `finder:`/`build:`/`scope:` overrides the derived default for that option, and a nil parent fails closed: `:show`/`:update`/`:destroy` return `not_found` and `:index` returns no records.
|
|
440
|
+
|
|
441
|
+
The same strong-params caveat applies: `:create` builds through the parent association and then assigns the permitted params, so never permit the parent's foreign key (for a `@category.exams` controller, don't permit `:category_id`).
|
|
442
|
+
|
|
360
443
|
#### Cache
|
|
361
444
|
Pass `cache: { key:, ttl: }` on `:show` or `:index` to skip the DB on cache hits.
|
|
362
445
|
|
|
@@ -428,11 +511,14 @@ The `create` and `update` examples cover the `422` response with validation erro
|
|
|
428
511
|
```ruby
|
|
429
512
|
include_examples 'simple crud for new' # the :new action
|
|
430
513
|
include_examples 'simple crud for edit' # the :edit action
|
|
514
|
+
include_examples 'simple crud for edit with finder' # finder on :edit
|
|
515
|
+
include_examples 'simple crud for edit with block' # render block on :edit
|
|
431
516
|
include_examples 'simple crud for index with block' # render block
|
|
432
517
|
include_examples 'simple crud for index with scope' # scope: ->(user) { ... }
|
|
433
518
|
include_examples 'simple crud for show with block' # render block on :show
|
|
434
519
|
include_examples 'simple crud for new with block' # render block on :new
|
|
435
520
|
include_examples 'simple crud for create with block' # render block on :create
|
|
521
|
+
include_examples 'simple crud for update with block' # render block on :update
|
|
436
522
|
include_examples 'simple crud for destroy with block' # render block on :destroy
|
|
437
523
|
include_examples 'simple crud for show with finder' # finder on :show
|
|
438
524
|
include_examples 'simple crud for update with finder' # finder on :update
|
|
@@ -441,6 +527,8 @@ include_examples 'simple crud for new with build' # build: -> { ... } on
|
|
|
441
527
|
include_examples 'simple crud for create with build' # build: -> { ... } on :create
|
|
442
528
|
```
|
|
443
529
|
|
|
530
|
+
The `create with block`, `destroy with block`, `edit` and `update with finder` examples accept a 2xx render or a redirect. The `show`, `new` and `edit` block examples let the block ask the policy about other actions, such as `update?`. `new with build` and `create with build` check ownership by the current user only when you configure an `owner_association`.
|
|
531
|
+
|
|
444
532
|
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
|
|
445
533
|
|
|
446
534
|
#### Adopting the shared examples
|
|
@@ -465,7 +553,7 @@ SimpleCrud::RSpec.configure do |config|
|
|
|
465
553
|
# override model_attributes directly for multi-key or non-user-owned models).
|
|
466
554
|
config.owner_association = :owner
|
|
467
555
|
config.model_attributes = -> { { owner: current_user } }
|
|
468
|
-
config.required_attribute = :title
|
|
556
|
+
config.required_attribute = :title # nil skips the invalid-attribute cases
|
|
469
557
|
config.required_error = "Title can't be blank"
|
|
470
558
|
|
|
471
559
|
# Redirect-based apps: unauthenticated requests get a redirect, not a 401.
|
|
@@ -504,7 +592,7 @@ SimpleCrud::RSpec.configure do |config|
|
|
|
504
592
|
end
|
|
505
593
|
```
|
|
506
594
|
|
|
507
|
-
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.
|
|
595
|
+
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 and `scoped_order` the created records. 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.
|
|
508
596
|
|
|
509
597
|
#### Per-controller overrides via metadata
|
|
510
598
|
|
|
@@ -523,6 +611,26 @@ end
|
|
|
523
611
|
|
|
524
612
|
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.
|
|
525
613
|
|
|
614
|
+
A scope that only orders excludes nothing. Set `other_scoped_attributes: nil` and `simple crud for index with scope` asserts that every scoped record comes back. `scoped_order` also asserts the order. It takes the created records and returns them in the expected order:
|
|
615
|
+
|
|
616
|
+
```ruby
|
|
617
|
+
RSpec.describe UsersController, type: :controller, simple_crud: {
|
|
618
|
+
other_scoped_attributes: nil,
|
|
619
|
+
scoped_order: ->(records) { records.sort_by(&:email) }
|
|
620
|
+
} do
|
|
621
|
+
include_examples 'simple crud for index with scope'
|
|
622
|
+
end
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
`singular: true` is a per-controller setting for singular resources (`resource :profile`). Requests carry no id, the "model does not exist" examples are skipped, and the controller's own finder, such as `finder: ->(_params) { current_user.profile }`, loads the record:
|
|
626
|
+
|
|
627
|
+
```ruby
|
|
628
|
+
RSpec.describe ProfilesController, type: :controller, simple_crud: { singular: true } do
|
|
629
|
+
include_examples 'simple crud for show'
|
|
630
|
+
include_examples 'simple crud for update'
|
|
631
|
+
end
|
|
632
|
+
```
|
|
633
|
+
|
|
526
634
|
## Contributing
|
|
527
635
|
|
|
528
636
|
1. Fork it
|
|
@@ -57,11 +57,25 @@ module SimpleCrud
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def find_by_id
|
|
60
|
-
|
|
60
|
+
finder_scope.find(controller.params[:id])
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def build_record
|
|
64
|
-
|
|
64
|
+
return controller.instance_exec(¶meters[:build]) if parameters[:build]
|
|
65
|
+
return relation_source.relation!.build if relation_source.configured?
|
|
66
|
+
|
|
67
|
+
klass.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Falls back to klass when owned_by:/parent: is unset.
|
|
71
|
+
def finder_scope
|
|
72
|
+
return klass unless relation_source.configured?
|
|
73
|
+
|
|
74
|
+
relation_source.relation!
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def relation_source
|
|
78
|
+
@relation_source ||= SimpleCrud::RelationSource.new(controller, klass, parameters)
|
|
65
79
|
end
|
|
66
80
|
|
|
67
81
|
def permitted_params
|
|
@@ -46,20 +46,31 @@ module SimpleCrud
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def index_relation
|
|
49
|
-
if parameters[:scope]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
return call_scope if parameters[:scope]
|
|
50
|
+
return relation_source.relation || klass.none if relation_source.configured?
|
|
51
|
+
|
|
52
|
+
default_index_relation
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def default_index_relation
|
|
56
|
+
return SimpleCrud::Config.authorization_adapter.policy_scope(controller, klass) if parameters[:authorize]
|
|
57
|
+
|
|
58
|
+
klass.all
|
|
56
59
|
end
|
|
57
60
|
|
|
58
61
|
def call_scope
|
|
59
|
-
user_method = SimpleCrud::Config.user_method
|
|
60
|
-
user = controller.respond_to?(user_method, true) ? controller.send(user_method) : nil
|
|
61
62
|
scope = parameters[:scope]
|
|
62
|
-
|
|
63
|
+
return klass.send(scope, relation_source.user, controller.params) unless scope.respond_to?(:call)
|
|
64
|
+
|
|
65
|
+
controller.instance_exec(*scope_arguments(scope), &scope)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def scope_arguments(scope)
|
|
69
|
+
case scope.arity
|
|
70
|
+
when 0 then []
|
|
71
|
+
when 1 then [relation_source.user]
|
|
72
|
+
else [relation_source.user, controller.params]
|
|
73
|
+
end
|
|
63
74
|
end
|
|
64
75
|
|
|
65
76
|
def index_records(relation, opts)
|
|
@@ -19,11 +19,22 @@ module SimpleCrud
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def render_html_redirect(record, saved, options)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
return render_html_failure(options) unless saved
|
|
23
|
+
|
|
24
|
+
set_flash(:notice, parameters[:notice])
|
|
25
|
+
controller.redirect_to(redirect_target(record, options[:redirect]))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render_html_failure(options)
|
|
29
|
+
set_flash(:alert, parameters[:alert], now: true)
|
|
30
|
+
controller.render(options[:failure_template])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def set_flash(key, message, now: false)
|
|
34
|
+
return if message.nil? || !controller.respond_to?(:flash)
|
|
35
|
+
|
|
36
|
+
target = now ? controller.flash.now : controller.flash
|
|
37
|
+
target[key] = message
|
|
27
38
|
end
|
|
28
39
|
|
|
29
40
|
def redirect_target(record, target)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
# Resolves the owner shared by owned_by: and parent:.
|
|
5
|
+
class RelationSource
|
|
6
|
+
def initialize(controller, klass, parameters)
|
|
7
|
+
@controller = controller
|
|
8
|
+
@klass = klass
|
|
9
|
+
@parameters = parameters
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def configured?
|
|
13
|
+
!owned_by.nil? || !parent.nil?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# nil when the owner can't be resolved.
|
|
17
|
+
def relation
|
|
18
|
+
return owner.relation if owned_by
|
|
19
|
+
return parent_source.relation if parent
|
|
20
|
+
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def relation!
|
|
25
|
+
relation || raise(ActiveRecord::RecordNotFound, "no #{description} to scope the record to")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def user
|
|
29
|
+
Source.user(controller)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :controller, :klass, :parameters
|
|
35
|
+
|
|
36
|
+
def owned_by = parameters[:owned_by]
|
|
37
|
+
|
|
38
|
+
def parent = parameters[:parent]
|
|
39
|
+
|
|
40
|
+
def description
|
|
41
|
+
owned_by ? "owner for #{owned_by}" : "parent for #{parent}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def owner
|
|
45
|
+
OwnerSource.new(controller, parameters[:owned_by])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parent_source
|
|
49
|
+
ParentSource.new(controller, klass, parameters)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Shared by both relation sources.
|
|
53
|
+
module Source
|
|
54
|
+
def self.user(controller)
|
|
55
|
+
user_method = SimpleCrud::Config.user_method
|
|
56
|
+
controller.respond_to?(user_method, true) ? controller.send(user_method) : nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# nil without a user.
|
|
61
|
+
class OwnerSource
|
|
62
|
+
def initialize(controller, association)
|
|
63
|
+
@controller = controller
|
|
64
|
+
@association = association
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def relation
|
|
68
|
+
Source.user(@controller)&.public_send(@association)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A Symbol resolves method then @ivar.
|
|
73
|
+
class ParentSource
|
|
74
|
+
def initialize(controller, klass, parameters)
|
|
75
|
+
@controller = controller
|
|
76
|
+
@klass = klass
|
|
77
|
+
@parameters = parameters
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def relation
|
|
81
|
+
resolved&.public_send(association)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def resolved
|
|
87
|
+
reference = @parameters[:parent]
|
|
88
|
+
return @controller.instance_exec(&reference) if reference.respond_to?(:call)
|
|
89
|
+
|
|
90
|
+
by_method_or_ivar(reference)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def by_method_or_ivar(reference)
|
|
94
|
+
return @controller.send(reference) if @controller.respond_to?(reference, true)
|
|
95
|
+
|
|
96
|
+
ivar = :"@#{reference}"
|
|
97
|
+
return @controller.instance_variable_get(ivar) if @controller.instance_variable_defined?(ivar)
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, "parent: #{reference} is neither a controller method nor an @#{reference} ivar"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def association
|
|
103
|
+
@parameters[:parent_association] || @klass.model_name.plural.to_sym
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -11,7 +11,7 @@ module SimpleCrud
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
%i[paginate authorize authenticate serializer serializer_options status after_persist html finder
|
|
14
|
-
scope build raise_on_invalid block authenticate_headers].each do |option|
|
|
14
|
+
scope build raise_on_invalid block authenticate_headers owned_by parent].each do |option|
|
|
15
15
|
define_method("check_#{option}") { |method| get_option(method, option) }
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -15,9 +15,9 @@ module SimpleCrud
|
|
|
15
15
|
.and_return(instance_double(policy_class_object, "#{method}?" => false))
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def make_policies_succeed(method)
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
def make_policies_succeed(method, permissive: false)
|
|
19
|
+
policy = instance_double(policy_class_object, "#{method}?" => true)
|
|
20
|
+
allow(policy_class_object).to receive(:new).and_return(permissive ? policy.as_null_object : policy)
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -29,8 +29,11 @@ module SimpleCrud
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# The params that identify a record for the given action, using the
|
|
32
|
-
# configured finder_key when the action has a custom finder.
|
|
32
|
+
# configured finder_key when the action has a custom finder. A singular
|
|
33
|
+
# resource has no id, so it identifies nothing.
|
|
33
34
|
def record_param(action, record, not_found: false)
|
|
35
|
+
return {} if singular
|
|
36
|
+
|
|
34
37
|
key = check_finder(action) ? finder_key : :id
|
|
35
38
|
value = not_found ? "nonexistent-#{key}" : record.public_send(key)
|
|
36
39
|
{ key => value }
|
|
@@ -24,10 +24,19 @@ module SimpleCrud
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
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
|
|
27
|
+
unauthenticated_status assert_html_template scoped_attributes other_scoped_attributes
|
|
28
|
+
singular].each do |name|
|
|
28
29
|
define_method(name) { resolve(setting(name)) }
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
def invalid_case?(action)
|
|
33
|
+
!required_attribute.nil? && !check_raise_on_invalid(action)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def scoped_order
|
|
37
|
+
setting(:scoped_order)
|
|
38
|
+
end
|
|
39
|
+
|
|
31
40
|
def created_record_check
|
|
32
41
|
setting(:created_record_check)
|
|
33
42
|
end
|
data/lib/simple_crud/rspec.rb
CHANGED
|
@@ -11,6 +11,9 @@ require_relative '../spec/shared_examples/simple_crud_for_destroy'
|
|
|
11
11
|
require_relative '../spec/shared_examples/simple_crud_for_create'
|
|
12
12
|
require_relative '../spec/shared_examples/simple_crud_for_new'
|
|
13
13
|
require_relative '../spec/shared_examples/simple_crud_for_edit'
|
|
14
|
+
require_relative '../spec/shared_examples/simple_crud_for_edit_with_finder'
|
|
15
|
+
require_relative '../spec/shared_examples/simple_crud_for_edit_with_block'
|
|
16
|
+
require_relative '../spec/shared_examples/simple_crud_for_update_with_block'
|
|
14
17
|
require_relative '../spec/shared_examples/unauthorized_when_not_logged_in'
|
|
15
18
|
require_relative '../spec/shared_examples/simple_crud_renders_template'
|
|
16
19
|
require_relative '../spec/shared_examples/simple_crud_without_authenticated_user'
|
|
@@ -27,8 +30,11 @@ require_relative '../spec/shared_examples/simple_crud_for_destroy_with_finder'
|
|
|
27
30
|
require_relative '../spec/shared_examples/simple_crud_not_found_with_finder'
|
|
28
31
|
require_relative '../spec/shared_examples/simple_crud_for_new_with_build'
|
|
29
32
|
require_relative '../spec/shared_examples/simple_crud_for_create_with_build'
|
|
33
|
+
require_relative '../spec/shared_examples/simple_crud_for_owned_resource'
|
|
34
|
+
require_relative '../spec/shared_examples/simple_crud_for_nested_resource'
|
|
30
35
|
require_relative '../spec/shared_examples/authorization_adapter_authorize'
|
|
31
36
|
require_relative '../spec/matchers/have_been_serialized_with'
|
|
37
|
+
require_relative '../spec/matchers/render_or_redirect'
|
|
32
38
|
|
|
33
39
|
module SimpleCrud
|
|
34
40
|
# Opt-in wiring for the shared examples: see .install!
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SimpleCrud
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# Blueprinter blueprints respond to `render_as_hash` and receive the
|
|
7
|
-
# render options, so a blueprint can read `url_builder:` (or any other
|
|
8
|
-
# option) from them. For anything else, simple_crud assumes an
|
|
9
|
-
# ActiveModelSerializers-style serializer and calls `new(record, options)`.
|
|
10
|
-
# A `nil` serializer renders `record.as_json`.
|
|
4
|
+
# Precedence: Blueprinter, then a plain class .render, then ActiveModelSerializers.
|
|
11
5
|
module Serializer
|
|
12
6
|
def self.render(serializer, record, options = {})
|
|
13
7
|
return record.as_json if serializer.nil?
|
|
14
8
|
return serializer.render_as_hash(record, options) if serializer.respond_to?(:render_as_hash)
|
|
9
|
+
return serializer.render(record, **options) if serializer.is_a?(Class) && serializer.respond_to?(:render)
|
|
15
10
|
|
|
16
11
|
serializer.new(record, options).as_json
|
|
17
12
|
end
|