wor-simple_crud 0.3.1 → 0.5.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/.rubocop.yml +15 -0
- data/CHANGELOG.md +21 -0
- data/README.md +89 -12
- data/lib/simple_crud/action_context.rb +98 -0
- data/lib/simple_crud/action_lambdas.rb +4 -72
- data/lib/simple_crud/cache_helpers.rb +17 -0
- data/lib/simple_crud/create_context.rb +17 -0
- data/lib/simple_crud/destroy_context.rb +15 -0
- data/lib/simple_crud/edit_context.rb +13 -0
- data/lib/simple_crud/index_context.rb +64 -0
- data/lib/simple_crud/new_context.rb +13 -0
- data/lib/simple_crud/persistence_context.rb +33 -0
- data/lib/simple_crud/rspec/helpers/controller_options.rb +6 -1
- data/lib/simple_crud/rspec/helpers/models.rb +16 -1
- data/lib/simple_crud/show_context.rb +27 -0
- data/lib/simple_crud/simple_crud_controller.rb +35 -9
- data/lib/simple_crud/update_context.rb +16 -0
- data/lib/simple_crud/version.rb +1 -1
- data/lib/simple_crud.rb +1 -0
- data/lib/spec/shared_examples/authorization_adapter_authorize.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_create.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_destroy.rb +2 -2
- data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_edit.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_index.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_new.rb +1 -1
- 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 +1 -1
- data/lib/spec/shared_examples/simple_crud_for_show.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_update.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_renders_template.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_when_not_authorized.rb +1 -1
- data/lib/spec/shared_examples/simple_crud_without_authenticated_user.rb +1 -1
- data/lib/spec/shared_examples/unauthorized_when_not_logged_in.rb +1 -1
- data/spec/cached/dummy_models_controller_spec.rb +23 -0
- data/spec/cached_defaults/dummy_models_controller_spec.rb +34 -0
- data/spec/dummy/app/controllers/block/dummy_models_controller.rb +1 -6
- data/spec/dummy/app/controllers/block_new/dummy_models_controller.rb +1 -1
- data/spec/dummy/app/controllers/block_redirect/dummy_models_controller.rb +1 -1
- data/spec/dummy/app/controllers/block_show/dummy_models_controller.rb +1 -1
- data/spec/dummy/app/controllers/built/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/cached/dummy_models_controller.rb +14 -0
- data/spec/dummy/app/controllers/cached_defaults/dummy_models_controller.rb +11 -0
- data/spec/dummy/app/controllers/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/finder/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/html_modes/base_controller.rb +0 -5
- data/spec/dummy/app/controllers/invalid_status/dummy_models_controller.rb +1 -1
- data/spec/dummy/app/controllers/nested/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/nested_route/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/redirect_auth/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/scoped/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/scoped_params/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/strict/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/unpaginated_scoped/dummy_models_controller.rb +0 -5
- data/spec/dummy/app/controllers/without_pagination/dummy_models_controller.rb +0 -5
- data/spec/dummy/config/routes.rb +8 -0
- data/spec/simple_crud/action_context_spec.rb +22 -0
- data/spec/simple_crud/index_context_spec.rb +31 -0
- data/spec/simple_crud/rspec_config_spec.rb +6 -0
- data/spec/simple_crud/show_context_spec.rb +14 -0
- data/spec/simple_crud_controller_spec.rb +38 -0
- metadata +19 -4
- data/lib/simple_crud/controller_helpers.rb +0 -128
- data/spec/simple_crud/controller_helpers_spec.rb +0 -49
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5b50548416811b034a75f72c96d4e94c41cdb9883d7112cdf720ad3febaeb11
|
|
4
|
+
data.tar.gz: 99ae1562bae38c2fa8ff1d697859f8643e2b818aaeb66f2e14220171fa9f28dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef7dd6acb64f882534e9951a9adf2d8deab27f2264451cb1dee2613c78e4842e38d10a638a2ed9ec623984d0d571aee558e2a3626f7bf8f016eca65dc3174ae3
|
|
7
|
+
data.tar.gz: 43a9b8fb46febb90d101814a33d48049ba0a5c2df3a3b14568c5423816ebd55325966fabe8673320ef3ed89b1a7bfe0ac10f4940c16b0e412649b100dd08e051
|
data/.rubocop.yml
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
plugins:
|
|
2
2
|
- rubocop-rspec
|
|
3
3
|
|
|
4
|
+
inherit_mode:
|
|
5
|
+
merge:
|
|
6
|
+
- Exclude
|
|
7
|
+
|
|
4
8
|
AllCops:
|
|
5
9
|
TargetRubyVersion: 3.2
|
|
6
10
|
NewCops: disable
|
|
@@ -14,3 +18,14 @@ RSpec/SpecFilePathFormat:
|
|
|
14
18
|
RSpec/SpecFilePathSuffix:
|
|
15
19
|
Exclude:
|
|
16
20
|
- spec/dummy_controller_spec.rb
|
|
21
|
+
|
|
22
|
+
# Context class names are self-documenting; comments would be noise.
|
|
23
|
+
Style/Documentation:
|
|
24
|
+
Exclude:
|
|
25
|
+
- lib/simple_crud/*_context.rb
|
|
26
|
+
|
|
27
|
+
# The destroy failure spec must stub find_record on the instance to intercept
|
|
28
|
+
# both default (klass.find) and custom-finder (lambda) lookup paths.
|
|
29
|
+
RSpec/AnyInstance:
|
|
30
|
+
Exclude:
|
|
31
|
+
- lib/spec/shared_examples/simple_crud_for_destroy.rb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
## Change log
|
|
2
2
|
|
|
3
|
+
### V0.5.0
|
|
4
|
+
|
|
5
|
+
New options:
|
|
6
|
+
* `cache: { key:, ttl: }` on `simple_crud_for :show` and `:index` — skips the DB on cache hits. On a miss: find/query, authorize, run block, cache, render. `key` is a lambda receiving `params` or a plain string; both default when omitted (`"#{model}:#{action}:v1:#{request.fullpath}"` and 300 s). Block must return the payload hash, not call `render`. `@record`/`@records` is set before the block, so shared examples work unchanged. Controllers get a default `fetch_cached(key, ttl, &block)` backed by `Rails.cache`; override to use a different store.
|
|
7
|
+
* `expire_simple_crud_cache(action, path: request.fullpath)` on controllers — deletes the default cache key for an action without constructing it by hand. Only works for the default key format; custom `key:` lambdas must be deleted directly.
|
|
8
|
+
|
|
9
|
+
### V0.4.0
|
|
10
|
+
|
|
11
|
+
Breaking:
|
|
12
|
+
* Blocks given to `simple_crud_for` no longer imply `html: true`. HTML controllers with render blocks must pass `html: true` explicitly.
|
|
13
|
+
|
|
14
|
+
New options:
|
|
15
|
+
* `authenticate_headers:` controls whether shared examples set auth headers and run the unauthorized test, independent of `authenticate:`. Defaults to `authenticate:`. Set `authenticate: false, authenticate_headers: true` when a base-controller `before_action` handles auth so the lambda skips the call but tests still cover the auth path.
|
|
16
|
+
* `simple_crud_defaults(options)` sets option defaults at the controller level. Subclasses inherit. Per-action options override. Precedence: gem defaults < controller defaults < per-action options.
|
|
17
|
+
|
|
18
|
+
Fixes:
|
|
19
|
+
* Use `RSpec.shared_examples` instead of the top-level alias, which is unavailable with `config.disable_monkey_patching!`.
|
|
20
|
+
* Auto-require `SimpleCrud::Authorization::Adapter` on `require 'simple_crud'`.
|
|
21
|
+
* Remove `cattr_accessor :params, :permitted` (thread-unsafe, never read by the gem). Remove the `set_params` boilerplate from host controllers.
|
|
22
|
+
* Raise a clear error when `owner_association` names an attribute the model doesn't have, instead of a bare `NoMethodError` from FactoryBot.
|
|
23
|
+
|
|
3
24
|
### V0.3.1
|
|
4
25
|
|
|
5
26
|
Shared examples configuration:
|
data/README.md
CHANGED
|
@@ -18,6 +18,8 @@ SimpleCrud
|
|
|
18
18
|
- [Serializer](#serializer)
|
|
19
19
|
- [HTML](#html)
|
|
20
20
|
- [Finder](#finder)
|
|
21
|
+
- [Cache](#cache)
|
|
22
|
+
- [Controller-level defaults](#controller-level-defaults)
|
|
21
23
|
- [Shared examples](#shared-examples)
|
|
22
24
|
- [Contributing](#contributing)
|
|
23
25
|
- [Releases](#releases)
|
|
@@ -68,12 +70,6 @@ Before SimpleCrud can be used, some boilerplate is needed. Add the following to
|
|
|
68
70
|
include Pundit::Authorization
|
|
69
71
|
include Wor::Paginate
|
|
70
72
|
extend SimpleCrudController
|
|
71
|
-
|
|
72
|
-
before_action :set_params
|
|
73
|
-
|
|
74
|
-
def set_params
|
|
75
|
-
SimpleCrudController.params = params
|
|
76
|
-
end
|
|
77
73
|
```
|
|
78
74
|
|
|
79
75
|
(Skip `include Pundit::Authorization` / `include Wor::Paginate` if you're using a different adapter, or `authorize: false` / `paginate: false` everywhere.)
|
|
@@ -105,7 +101,8 @@ simple_crud_for :index, paginate: false, authorize: false, serializer: CustomSer
|
|
|
105
101
|
|
|
106
102
|
- Paginate: whether it should paginate or not. `true` paginates via the configured pagination adapter (wor-paginate by default), `false` doesn't paginate
|
|
107
103
|
- Authorize: whether it should check authorization via the configured authorization adapter (Pundit by default)
|
|
108
|
-
- Authenticate: whether
|
|
104
|
+
- Authenticate: whether the generated action calls `authenticate_user!`. `true` (default) or `false`
|
|
105
|
+
- Authenticate_headers: whether shared examples set auth headers and run the unauthorized test. Defaults to `authenticate:`. Set independently when a base-controller `before_action` handles auth and the lambda should skip `authenticate_user!`
|
|
109
106
|
- Serializer: specify a particular serializer you should use
|
|
110
107
|
- 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
108
|
- 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`)
|
|
@@ -114,6 +111,22 @@ simple_crud_for :index, paginate: false, authorize: false, serializer: CustomSer
|
|
|
114
111
|
- 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
112
|
- 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`)
|
|
116
113
|
|
|
114
|
+
#### Controller-level defaults
|
|
115
|
+
|
|
116
|
+
`simple_crud_defaults` sets option defaults for every `simple_crud_for` in the controller. Subclasses inherit; per-action options override.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class Api::BaseController < ActionController::API
|
|
120
|
+
extend SimpleCrudController
|
|
121
|
+
simple_crud_defaults authorize: false, authenticate: false, authenticate_headers: true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class Api::PostsController < Api::BaseController
|
|
125
|
+
simple_crud_for :show # inherits defaults
|
|
126
|
+
simple_crud_for :create, authorize: true # overrides for this action only
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
117
130
|
You'll need a few things so they work correctly:
|
|
118
131
|
|
|
119
132
|
### Options
|
|
@@ -247,7 +260,13 @@ end
|
|
|
247
260
|
```
|
|
248
261
|
|
|
249
262
|
#### Authenticate
|
|
250
|
-
|
|
263
|
+
`authenticate: true` (default) calls `authenticate_user!` inside the generated action. `authenticate: false` skips it.
|
|
264
|
+
|
|
265
|
+
If your base controller already runs `before_action :authenticate_user!`, set `authenticate: false` to avoid a double call and `authenticate_headers: true` so shared examples still cover the auth path:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
simple_crud_defaults authenticate: false, authenticate_headers: true
|
|
269
|
+
```
|
|
251
270
|
|
|
252
271
|
#### Serializer
|
|
253
272
|
The name of the serializer, by default, is the name of the model followed by Serializer, as is the standard for [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers). It's possible to just pass a custom serializer class though. As for the serializer itself, it's a standard serializer, with the gotcha that you need to include `:id` for the SimpleCrud examples to work.
|
|
@@ -279,16 +298,22 @@ simple_crud_for :update, html: true
|
|
|
279
298
|
simple_crud_for :destroy, html: true
|
|
280
299
|
```
|
|
281
300
|
|
|
282
|
-
Or pass a block that renders explicitly, overriding the auto-render. The block receives the records for `:index`, the record for `:show`/`:new`, or the record plus a saved flag for `:create`/`:update`/`:destroy`.
|
|
301
|
+
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:
|
|
283
302
|
|
|
284
303
|
```ruby
|
|
285
|
-
|
|
304
|
+
# Server-rendered block: pass html: true so shared examples send HTML requests
|
|
305
|
+
simple_crud_for :index, html: true do |records|
|
|
286
306
|
render :index, locals: { models: records }
|
|
287
307
|
end
|
|
288
308
|
|
|
289
|
-
simple_crud_for :create do |record, saved|
|
|
309
|
+
simple_crud_for :create, html: true do |record, saved|
|
|
290
310
|
saved ? redirect_to(record) : render(:new, locals: { model: record })
|
|
291
311
|
end
|
|
312
|
+
|
|
313
|
+
# API block: html: false is the default
|
|
314
|
+
simple_crud_for :show, html: false do |record|
|
|
315
|
+
render json: record.as_json(only: %i[id name])
|
|
316
|
+
end
|
|
292
317
|
```
|
|
293
318
|
|
|
294
319
|
#### Build
|
|
@@ -319,8 +344,60 @@ simple_crud_for :destroy, finder: ->(params) { current_user.models.find(params[:
|
|
|
319
344
|
|
|
320
345
|
When omitted it defaults to `klass.find(params[:id])`, and `not_found` is still returned whenever the finder finds no record.
|
|
321
346
|
|
|
347
|
+
#### Cache
|
|
348
|
+
Pass `cache: { key:, ttl: }` on `:show` or `:index` to skip the DB on cache hits.
|
|
349
|
+
|
|
350
|
+
`key` is a lambda receiving `params`, or a plain string. Both `key` and `ttl` are optional — defaults are `"#{model}:#{action}:v1:#{request.fullpath}"` and 300 seconds. With `cache:` the block must return the payload hash, not call `render`. `@record`/`@records` is set before the block, so shared examples work unchanged.
|
|
351
|
+
|
|
352
|
+
Controllers get a default `fetch_cached(key, ttl, &block)` backed by `Rails.cache`. Override to use a different store:
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
# Override with a direct Redis connection
|
|
356
|
+
def fetch_cached(key, ttl)
|
|
357
|
+
cached = redis.get(key)
|
|
358
|
+
return JSON.parse(cached, symbolize_names: true) if cached
|
|
359
|
+
|
|
360
|
+
result = yield
|
|
361
|
+
redis.setex(key, ttl, result.to_json)
|
|
362
|
+
result
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# All options explicit
|
|
366
|
+
simple_crud_for :show,
|
|
367
|
+
finder: ->(p) { Article.includes(:author).find(p[:id]) },
|
|
368
|
+
cache: { key: ->(p) { "article:v1:#{p[:id]}" }, ttl: 900 } do |article|
|
|
369
|
+
article_payload(article)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# All defaults — key and TTL inferred from model name and params
|
|
373
|
+
simple_crud_for :index, paginate: false, cache: {} do |articles|
|
|
374
|
+
articles.map { |a| article_summary(a) }
|
|
375
|
+
end
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Invalidate on write. For the default key format, `expire_simple_crud_cache(action)` deletes it without constructing it by hand:
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
expire_simple_crud_cache(:show)
|
|
382
|
+
expire_simple_crud_cache(:index)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
For a custom `key:` lambda, delete the key yourself:
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
def update
|
|
389
|
+
article = current_user.articles.find(params[:id])
|
|
390
|
+
if article.update(article_params)
|
|
391
|
+
redis.del("article:v1:#{article.id}")
|
|
392
|
+
render json: article_payload(article)
|
|
393
|
+
else
|
|
394
|
+
render json: { errors: article.errors.full_messages }, status: :unprocessable_entity
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
```
|
|
398
|
+
|
|
322
399
|
### Shared examples
|
|
323
|
-
While optional, using the included shared examples saves you from writing the standard test cases for the methods. You can even use them if you didn't use `simple_crud_for`, as a set of basic tests. To include them,
|
|
400
|
+
While optional, using the included shared examples saves you from writing the standard test cases for the methods. You can even use them if you didn't use `simple_crud_for`, as a set of basic tests. To include them, add `require 'simple_crud/rspec'` to your `rails_helper.rb` **after** `require "rspec/rails"`, then add the lines you need to your `*_spec.rb` files:
|
|
324
401
|
```ruby
|
|
325
402
|
require 'rails_helper'
|
|
326
403
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
class ActionContext
|
|
5
|
+
DEFAULT_CACHE_TTL = 300
|
|
6
|
+
|
|
7
|
+
attr_reader :controller, :klass, :parameters, :block
|
|
8
|
+
|
|
9
|
+
def self.cache_key_for(model, action, path)
|
|
10
|
+
"#{model.model_name.singular}:#{action}:v1:#{path}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(controller, klass, parameters, &block)
|
|
14
|
+
@controller = controller
|
|
15
|
+
@klass = klass
|
|
16
|
+
@parameters = parameters
|
|
17
|
+
@block = block
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
controller.authenticate_user! if parameters[:authenticate]
|
|
22
|
+
run
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
raise NotImplementedError, "#{self.class} must implement run"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def action_name
|
|
32
|
+
raise NotImplementedError, "#{self.class} must implement action_name to support caching"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def maybe_authorize(object)
|
|
36
|
+
return unless parameters[:authorize]
|
|
37
|
+
|
|
38
|
+
SimpleCrud::Config.authorization_adapter.authorize(controller, object)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find_record
|
|
42
|
+
record = lookup_record
|
|
43
|
+
raise ActiveRecord::RecordNotFound, "couldn't find #{klass}" if record.nil?
|
|
44
|
+
unless record.is_a?(ActiveRecord::Base)
|
|
45
|
+
raise ActiveRecord::RecordNotFound, "#{klass} finder must return a single record"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
record
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def lookup_record
|
|
52
|
+
finder = parameters[:finder]
|
|
53
|
+
return klass.find(controller.params[:id]) if finder.nil?
|
|
54
|
+
return klass.send(finder, controller.params) unless finder.respond_to?(:call)
|
|
55
|
+
|
|
56
|
+
finder.call(controller.params)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_record
|
|
60
|
+
parameters[:build] ? controller.instance_exec(¶meters[:build]) : klass.new
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def permitted_params
|
|
64
|
+
controller.send("#{klass.model_name.singular}_params")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def serialize_opts(key)
|
|
68
|
+
{ key => parameters[:serializer] }.compact
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def render_record(record, template)
|
|
72
|
+
if parameters[:html] || block
|
|
73
|
+
controller.instance_variable_set(:@record, record)
|
|
74
|
+
block ? controller.instance_exec(record, &block) : controller.render(template)
|
|
75
|
+
else
|
|
76
|
+
controller.render({ json: record }.merge(serialize_opts(:serializer)))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_cached(cache_opts, &fetch_block)
|
|
81
|
+
key = resolve_cache_key(cache_opts)
|
|
82
|
+
ttl = cache_opts[:ttl] || DEFAULT_CACHE_TTL
|
|
83
|
+
result = controller.send(:fetch_cached, key, ttl, &fetch_block)
|
|
84
|
+
controller.render json: result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def resolve_cache_key(cache_opts)
|
|
88
|
+
k = cache_opts[:key]
|
|
89
|
+
return default_cache_key if k.nil?
|
|
90
|
+
|
|
91
|
+
k.respond_to?(:call) ? controller.instance_exec(controller.params, &k) : k
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def default_cache_key
|
|
95
|
+
self.class.cache_key_for(klass, action_name, controller.request.fullpath)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -3,78 +3,10 @@
|
|
|
3
3
|
module SimpleCrud
|
|
4
4
|
# Builds the lambda installed as each action declared with simple_crud_for.
|
|
5
5
|
module ActionLambdas
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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)
|
|
6
|
+
%i[show new edit index create update destroy].each do |action|
|
|
7
|
+
ctx_class = SimpleCrud.const_get(:"#{action.to_s.capitalize}Context")
|
|
8
|
+
define_method(:"crud_lambda_for_#{action}") do |klass, parameters = {}, &block|
|
|
9
|
+
-> { ctx_class.new(self, klass, parameters, &block).call }
|
|
78
10
|
end
|
|
79
11
|
end
|
|
80
12
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
# Default fetch_cached included into controllers that extend SimpleCrudController.
|
|
5
|
+
# Override in the controller to use a different store.
|
|
6
|
+
module CacheHelpers
|
|
7
|
+
def fetch_cached(key, ttl, &block)
|
|
8
|
+
Rails.cache.fetch(key, expires_in: ttl, &block)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def expire_simple_crud_cache(action, path: request.fullpath)
|
|
12
|
+
Rails.cache.delete(
|
|
13
|
+
SimpleCrud::ActionContext.cache_key_for(self.class.simple_crud_controller_model, action, path)
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
class CreateContext < PersistenceContext
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def run
|
|
8
|
+
attrs = permitted_params
|
|
9
|
+
record = build_record
|
|
10
|
+
record.assign_attributes(attrs)
|
|
11
|
+
maybe_authorize(record)
|
|
12
|
+
persist = ->(bang:) { bang ? record.save! : record.save }
|
|
13
|
+
options = { status: :created, failure_template: :new, redirect: parameters[:redirect] || record }
|
|
14
|
+
persist_and_render(record, options, persist)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
class DestroyContext < PersistenceContext
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def run
|
|
8
|
+
record = find_record
|
|
9
|
+
maybe_authorize(record)
|
|
10
|
+
persist = ->(bang:) { bang ? record.destroy! : record.destroy }
|
|
11
|
+
options = { status: :ok, failure_template: :show, redirect: parameters[:redirect] || klass }
|
|
12
|
+
persist_and_render(record, options, persist)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
class IndexContext < ActionContext
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def action_name = :index
|
|
8
|
+
|
|
9
|
+
def run
|
|
10
|
+
maybe_authorize(klass)
|
|
11
|
+
cache_opts = parameters[:cache]
|
|
12
|
+
cache_opts ? render_cached(cache_opts) { payload } : render_index
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def payload
|
|
16
|
+
opts = serialize_opts(:each_serializer)
|
|
17
|
+
relation = index_relation
|
|
18
|
+
records = index_records(relation, opts)
|
|
19
|
+
controller.instance_variable_set(:@records, records)
|
|
20
|
+
block ? controller.instance_exec(records, &block) : records.as_json
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def render_index
|
|
24
|
+
relation = index_relation
|
|
25
|
+
opts = serialize_opts(:each_serializer)
|
|
26
|
+
if parameters[:html] || block
|
|
27
|
+
render_records(relation, opts)
|
|
28
|
+
elsif parameters[:paginate]
|
|
29
|
+
SimpleCrud::Config.pagination_adapter.paginate(controller, relation, opts)
|
|
30
|
+
else
|
|
31
|
+
controller.render({ json: relation }.merge(opts))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def render_records(relation, opts)
|
|
36
|
+
records = index_records(relation, opts)
|
|
37
|
+
controller.instance_variable_set(:@records, records)
|
|
38
|
+
block ? controller.instance_exec(records, &block) : controller.render(:index)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def index_relation
|
|
42
|
+
if parameters[:scope]
|
|
43
|
+
call_scope
|
|
44
|
+
elsif parameters[:authorize]
|
|
45
|
+
SimpleCrud::Config.authorization_adapter.policy_scope(controller, klass)
|
|
46
|
+
else
|
|
47
|
+
klass.all
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def call_scope
|
|
52
|
+
user_method = SimpleCrud::Config.user_method
|
|
53
|
+
user = controller.respond_to?(user_method) ? controller.public_send(user_method) : nil
|
|
54
|
+
scope = parameters[:scope]
|
|
55
|
+
scope.arity == 1 ? scope.call(user) : scope.call(user, controller.params)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def index_records(relation, opts)
|
|
59
|
+
return relation unless parameters[:paginate]
|
|
60
|
+
|
|
61
|
+
SimpleCrud::Config.pagination_adapter.paginated_records(controller, relation, opts)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
class PersistenceContext < ActionContext
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def persist_and_render(record, options, persist)
|
|
8
|
+
saved = parameters[:raise_on_invalid] ? persist.call(bang: true) : persist.call(bang: false)
|
|
9
|
+
return render_persisted(record, saved, options) unless block || parameters[:html]
|
|
10
|
+
|
|
11
|
+
controller.instance_variable_set(:@record, record)
|
|
12
|
+
block ? controller.instance_exec(record, saved, &block) : render_html_redirect(record, saved, options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render_html_redirect(record, saved, options)
|
|
16
|
+
if saved
|
|
17
|
+
controller.redirect_to(redirect_target(record, options[:redirect]))
|
|
18
|
+
else
|
|
19
|
+
controller.render(options[:failure_template])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def redirect_target(record, target)
|
|
24
|
+
target.is_a?(Proc) ? controller.instance_exec(record, &target) : target
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def render_persisted(record, saved, options)
|
|
28
|
+
return controller.render(json: record, status: options[:status]) if saved
|
|
29
|
+
|
|
30
|
+
controller.render json: { errors: record.errors.full_messages }, status: 422
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -10,9 +10,14 @@ module SimpleCrud
|
|
|
10
10
|
described_class.instance_variable_get(:@simple_crud_metadata)[method][option]
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
%i[paginate authorize authenticate serializer html finder scope build raise_on_invalid block
|
|
13
|
+
%i[paginate authorize authenticate serializer html finder scope build raise_on_invalid block
|
|
14
|
+
authenticate_headers].each do |option|
|
|
14
15
|
define_method("check_#{option}") { |method| get_option(method, option) }
|
|
15
16
|
end
|
|
17
|
+
|
|
18
|
+
def check_authenticate(method)
|
|
19
|
+
check_authenticate_headers(method)
|
|
20
|
+
end
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
end
|
|
@@ -15,7 +15,22 @@ module SimpleCrud
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def model
|
|
18
|
-
@model ||=
|
|
18
|
+
@model ||= begin
|
|
19
|
+
validate_owner_association!
|
|
20
|
+
create_record(model_class, model_attributes)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate_owner_association!
|
|
25
|
+
assoc = owner_association
|
|
26
|
+
return unless assoc
|
|
27
|
+
|
|
28
|
+
klass = model_class_object
|
|
29
|
+
return if klass.method_defined?("#{assoc}=") || klass.column_names.include?(assoc.to_s)
|
|
30
|
+
|
|
31
|
+
raise "simple_crud: owner_association is :#{assoc} but #{klass} has no #{assoc}= setter. " \
|
|
32
|
+
'Set `simple_crud: { owner_association: nil }` in your describe block or ' \
|
|
33
|
+
'override model_attributes directly.'
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
def model_attributes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCrud
|
|
4
|
+
class ShowContext < ActionContext
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def action_name = :show
|
|
8
|
+
|
|
9
|
+
def run
|
|
10
|
+
cache_opts = parameters[:cache]
|
|
11
|
+
if cache_opts
|
|
12
|
+
render_cached(cache_opts) { payload }
|
|
13
|
+
else
|
|
14
|
+
record = find_record
|
|
15
|
+
maybe_authorize(record)
|
|
16
|
+
render_record(record, :show)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def payload
|
|
21
|
+
record = find_record
|
|
22
|
+
maybe_authorize(record)
|
|
23
|
+
controller.instance_variable_set(:@record, record)
|
|
24
|
+
block ? controller.instance_exec(record, &block) : record.as_json
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|