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.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/CHANGELOG.md +16 -2
- data/README.md +57 -14
- data/lib/simple_crud/action_lambdas.rb +81 -0
- data/lib/simple_crud/controller_helpers.rb +11 -2
- data/lib/simple_crud/rspec/config.rb +18 -5
- data/lib/simple_crud/rspec/helpers/authentication.rb +32 -0
- data/lib/simple_crud/rspec/helpers/controller_options.rb +19 -0
- data/lib/simple_crud/rspec/helpers/models.rb +55 -0
- data/lib/simple_crud/rspec/helpers/policies.rb +25 -0
- data/lib/simple_crud/rspec/helpers/requests.rb +45 -0
- data/lib/simple_crud/rspec/helpers/settings.rb +49 -0
- data/lib/simple_crud/rspec/helpers.rb +16 -138
- data/lib/simple_crud/rspec.rb +1 -0
- data/lib/simple_crud/simple_crud_controller.rb +11 -74
- data/lib/simple_crud/version.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_create.rb +15 -12
- data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +25 -7
- data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +2 -2
- data/lib/spec/shared_examples/simple_crud_for_destroy.rb +25 -0
- data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +10 -2
- data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +17 -6
- data/lib/spec/shared_examples/simple_crud_for_edit.rb +79 -0
- data/lib/spec/shared_examples/simple_crud_for_index.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +11 -3
- data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +25 -13
- data/lib/spec/shared_examples/simple_crud_for_new.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +13 -2
- data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +10 -4
- data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +13 -2
- data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +16 -7
- data/lib/spec/shared_examples/simple_crud_for_update.rb +16 -9
- data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +16 -6
- data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +2 -1
- data/spec/block_finder/dummy_models_controller_spec.rb +10 -0
- data/spec/block_json/dummy_models_controller_spec.rb +9 -0
- data/spec/block_redirect/dummy_models_controller_spec.rb +7 -0
- data/spec/dummy/app/controllers/block_finder/dummy_models_controller.rb +25 -0
- data/spec/dummy/app/controllers/block_json/dummy_models_controller.rb +17 -0
- data/spec/dummy/app/controllers/block_redirect/dummy_models_controller.rb +13 -0
- data/spec/dummy/app/controllers/finder/dummy_models_controller.rb +1 -0
- data/spec/dummy/app/controllers/html_finder/dummy_models_controller.rb +17 -0
- data/spec/dummy/app/controllers/html_scoped/dummy_models_controller.rb +1 -1
- data/spec/dummy/app/controllers/unpaginated_scoped/dummy_models_controller.rb +16 -0
- data/spec/dummy/app/models/dummy_model_policy.rb +5 -1
- data/spec/dummy/app/views/html_finder/dummy_models/edit.html.erb +1 -0
- data/spec/dummy/app/views/html_finder/dummy_models/new.html.erb +1 -0
- data/spec/dummy/app/views/html_finder/dummy_models/show.html.erb +1 -0
- data/spec/dummy/config/routes.rb +21 -1
- data/spec/dummy_model_policy_spec.rb +18 -8
- data/spec/finder/dummy_models_controller_spec.rb +1 -0
- data/spec/html_finder/dummy_models_controller_spec.rb +11 -0
- data/spec/html_scoped/dummy_models_controller_spec.rb +1 -1
- data/spec/scoped_params/dummy_models_controller_spec.rb +4 -3
- data/spec/simple_crud/rspec_config_spec.rb +33 -0
- data/spec/unpaginated_scoped/dummy_models_controller_spec.rb +7 -0
- metadata +23 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e280efd8ead44768d16e0ba663e25a40d6972b53e253281d1c7eac0735421b64
|
|
4
|
+
data.tar.gz: 78625a2a759ca5fda1e7d41e421db92ed721f2e2c343524f0580cd810d32ac7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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 `:
|
|
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.
|
|
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.
|
|
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.
|
|
296
|
-
simple_crud_for :create, 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
|
|
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
|
|
363
|
-
|
|
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(:
|
|
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 = :
|
|
390
|
+
config.params_key = :task
|
|
377
391
|
|
|
378
|
-
# Nested resources (/
|
|
392
|
+
# Nested resources (/projects/:project_slug/tasks): extra params
|
|
379
393
|
# (e.g. the parent slug) added to every request.
|
|
380
|
-
config.route_params = -> { {
|
|
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
|
-
|
|
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(
|
|
46
|
+
block ? controller.instance_exec(record, &block) : controller.render(template)
|
|
39
47
|
else
|
|
40
|
-
|
|
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,
|
|
18
|
-
# create_records,
|
|
19
|
-
# the example-group context, so
|
|
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
|