wor-simple_crud 0.4.0 → 0.6.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 +28 -0
- data/Gemfile +5 -1
- data/README.md +69 -3
- data/lib/simple_crud/action_context.rb +119 -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 +18 -0
- data/lib/simple_crud/destroy_context.rb +16 -0
- data/lib/simple_crud/edit_context.rb +13 -0
- data/lib/simple_crud/index_context.rb +71 -0
- data/lib/simple_crud/new_context.rb +13 -0
- data/lib/simple_crud/pagination/wor_paginate_adapter.rb +7 -1
- data/lib/simple_crud/persistence_context.rb +49 -0
- data/lib/simple_crud/rspec/config.rb +6 -1
- data/lib/simple_crud/rspec/helpers/authentication.rb +2 -1
- data/lib/simple_crud/rspec/helpers/controller_options.rb +2 -2
- data/lib/simple_crud/rspec/helpers/requests.rb +5 -2
- data/lib/simple_crud/serializer.rb +19 -0
- data/lib/simple_crud/show_context.rb +29 -0
- data/lib/simple_crud/simple_crud_controller.rb +17 -4
- data/lib/simple_crud/update_context.rb +17 -0
- data/lib/simple_crud/version.rb +1 -1
- data/lib/spec/matchers/have_been_serialized_with.rb +19 -2
- data/lib/spec/shared_examples/simple_crud_for_create.rb +6 -6
- 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 +1 -1
- data/lib/spec/shared_examples/simple_crud_for_destroy.rb +9 -9
- 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 +3 -3
- data/lib/spec/shared_examples/simple_crud_for_edit.rb +2 -2
- data/lib/spec/shared_examples/simple_crud_for_index.rb +3 -3
- 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 +2 -2
- 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 +7 -7
- 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/spec/blueprint/dummy_models_controller_spec.rb +25 -0
- data/spec/blueprint_unpaginated/dummy_models_controller_spec.rb +9 -0
- 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/blueprints/dummy_model_blueprint.rb +10 -0
- data/spec/dummy/app/controllers/blueprint/dummy_models_controller.rb +21 -0
- data/spec/dummy/app/controllers/blueprint_unpaginated/dummy_models_controller.rb +10 -0
- 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/config/routes.rb +16 -0
- data/spec/dummy_model_blueprint_spec.rb +16 -0
- data/spec/simple_crud/action_context_spec.rb +41 -0
- data/spec/simple_crud/index_context_spec.rb +31 -0
- data/spec/simple_crud/rspec_config_spec.rb +13 -0
- data/spec/simple_crud/serializer_spec.rb +28 -0
- data/spec/simple_crud/show_context_spec.rb +14 -0
- metadata +28 -5
- 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: b40c7867a484ffc3807b8dceac29ac1f6cc49ccca6d8a9bb067f93f839703e0c
|
|
4
|
+
data.tar.gz: 5a72d84147fc1f149695fe0244a11e7f99f2c9b4debb0b99a291e16439b873ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2354c67d6866dd82b70172279fd47a65bced5a950ce4385df97a1d3ab3373724d7f49a2ee7185e9cca3f86f403892e732afeade705c74337d7ae32ed53815632
|
|
7
|
+
data.tar.gz: 851104267914cd47c8312723af3361a13e5ee5fa2a323f73add55a0a09e04d965492f870c66ffc81f24c5851239afd35485b367633192c80ce8b3d8d90a7753a
|
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,33 @@
|
|
|
1
1
|
## Change log
|
|
2
2
|
|
|
3
|
+
### V0.6.0
|
|
4
|
+
|
|
5
|
+
Serializer support:
|
|
6
|
+
* Blueprinter: `serializer:` now accepts a [Blueprinter](https://github.com/blueprinter/blueprinter) blueprint on every action, which simple_crud renders with `render_as_hash(record, options)`, as well as an ActiveModelSerializers serializer. The shared examples' default `serializer_class` uses `"#{Model}Blueprint"` when no `"#{Model}Serializer"` constant exists, and `have_been_serialized_with` reads Blueprinter fields.
|
|
7
|
+
* `serializer_options:` on every action: a lambda that runs through `instance_exec`. simple_crud forwards its result to the serializer (`render_as_hash(record, options)` for Blueprinter, `new(record, options)` for AMS). Arity 0 runs with no arguments; arity ≥ 1 gets the record. On `:index` it runs once with no record, so arity-1 lambdas get `nil`. Paginated Blueprinter indexes need wor-paginate ≥ 0.5.
|
|
8
|
+
|
|
9
|
+
New options:
|
|
10
|
+
* `status:` on `:create`/`:update`/`:destroy` overrides the success status (`:created`/`:ok`/`:ok` by default). `status: :no_content` returns an empty body.
|
|
11
|
+
* `after_persist:` on `:create`/`:update`/`:destroy`: a `(record, saved)` lambda that runs after the write and before rendering. Use it for side effects like cache invalidation. It still runs when you pass a render block.
|
|
12
|
+
|
|
13
|
+
Behavior changes:
|
|
14
|
+
* `create`/`update` responses now honor `serializer:`. Before, they returned the raw record. AMS apps that never passed `serializer:` see no change.
|
|
15
|
+
* Cached `:show` now honors `serializer:`. Before, it returned `record.as_json`.
|
|
16
|
+
* Unpaginated `:index` now serializes through the same path as the other actions, so Blueprinter works there. Before, it passed `each_serializer` to `render`.
|
|
17
|
+
|
|
18
|
+
Fixes:
|
|
19
|
+
* `finder:` lambdas now run through `instance_exec` on the controller, so `current_user` and other controller methods are available, matching the README.
|
|
20
|
+
* `:index` resolves a private `current_user` method.
|
|
21
|
+
* Shared examples no longer inject `params[:format]`, which collided with model attributes named `format`.
|
|
22
|
+
* Shared-example `owner_foreign_key` honors custom `foreign_key:` declarations.
|
|
23
|
+
* The base `index` example compares ids with `match_array`; create/update/destroy examples honor a custom `status:`.
|
|
24
|
+
|
|
25
|
+
### V0.5.0
|
|
26
|
+
|
|
27
|
+
New options:
|
|
28
|
+
* `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.
|
|
29
|
+
* `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.
|
|
30
|
+
|
|
3
31
|
### V0.4.0
|
|
4
32
|
|
|
5
33
|
Breaking:
|
data/Gemfile
CHANGED
|
@@ -17,6 +17,7 @@ group :development, :test do
|
|
|
17
17
|
gem 'action_policy', '~> 0.7.6', require: false
|
|
18
18
|
gem 'active_model_serializers', '~> 0.10.16'
|
|
19
19
|
gem 'appraisal', '~> 2.5'
|
|
20
|
+
gem 'blueprinter', '~> 1.3'
|
|
20
21
|
gem 'byebug', '~> 13.0'
|
|
21
22
|
gem 'cancancan', '~> 3.6', require: false
|
|
22
23
|
gem 'database_cleaner-active_record', '~> 2.2', require: 'database_cleaner/active_record'
|
|
@@ -24,6 +25,9 @@ group :development, :test do
|
|
|
24
25
|
gem 'devise-jwt', '~> 0.13'
|
|
25
26
|
gem 'factory_bot_rails', '~> 6.5'
|
|
26
27
|
gem 'faker', '~> 3.8'
|
|
28
|
+
# json 3 dropped the `quirks_mode` keyword that ActionController::TestCase
|
|
29
|
+
# (and thus rails-controller-testing) still passes on Rails <= 8.0.
|
|
30
|
+
gem 'json', '< 3'
|
|
27
31
|
gem 'kaminari', '~> 1.2', require: false
|
|
28
32
|
gem 'overcommit', '~> 0.72'
|
|
29
33
|
gem 'pagy', '~> 43.0', require: false
|
|
@@ -39,5 +43,5 @@ group :development, :test do
|
|
|
39
43
|
# required range differs across supported versions.
|
|
40
44
|
gem 'webmock', '~> 3.26'
|
|
41
45
|
gem 'will_paginate', '~> 4.0', require: false
|
|
42
|
-
gem 'wor-paginate', '~> 0.
|
|
46
|
+
gem 'wor-paginate', '~> 0.5'
|
|
43
47
|
end
|
data/README.md
CHANGED
|
@@ -18,6 +18,7 @@ SimpleCrud
|
|
|
18
18
|
- [Serializer](#serializer)
|
|
19
19
|
- [HTML](#html)
|
|
20
20
|
- [Finder](#finder)
|
|
21
|
+
- [Cache](#cache)
|
|
21
22
|
- [Controller-level defaults](#controller-level-defaults)
|
|
22
23
|
- [Shared examples](#shared-examples)
|
|
23
24
|
- [Contributing](#contributing)
|
|
@@ -102,7 +103,10 @@ simple_crud_for :index, paginate: false, authorize: false, serializer: CustomSer
|
|
|
102
103
|
- Authorize: whether it should check authorization via the configured authorization adapter (Pundit by default)
|
|
103
104
|
- Authenticate: whether the generated action calls `authenticate_user!`. `true` (default) or `false`
|
|
104
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!`
|
|
105
|
-
- Serializer: specify a particular serializer you should use
|
|
106
|
+
- Serializer: specify a particular serializer you should use. Accepts an ActiveModelSerializers serializer or a Blueprinter blueprint (see [Serializer](#serializer))
|
|
107
|
+
- Serializer_options: a `Proc`/`lambda` that runs with the controller as `self`. Its result goes to the serializer: `render_as_hash(record, options)` for Blueprinter, `new(record, options)` for ActiveModelSerializers. Arity 0 runs without arguments; arity ≥ 1 receives the record. On `:index` it runs once with no record
|
|
108
|
+
- 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
|
+
- 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
|
|
106
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
|
|
107
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`)
|
|
108
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])`.
|
|
@@ -268,12 +272,22 @@ simple_crud_defaults authenticate: false, authenticate_headers: true
|
|
|
268
272
|
```
|
|
269
273
|
|
|
270
274
|
#### Serializer
|
|
271
|
-
|
|
275
|
+
`serializer:` picks the serializer for JSON responses on every action. Pass either an [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers) serializer class, which simple_crud builds per record with `serializer.new(record, serializer_options)`, or a [Blueprinter](https://github.com/blueprinter/blueprinter) blueprint class, which it renders with `blueprint.render_as_hash(record, serializer_options)`.
|
|
276
|
+
|
|
277
|
+
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.
|
|
272
278
|
|
|
273
279
|
```ruby
|
|
274
280
|
class AuthorSerializer < ActiveModel::Serializer
|
|
275
|
-
attributes :email, :first_name, :last_name, :
|
|
281
|
+
attributes :email, :first_name, :last_name, :id
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
class AuthorBlueprint < Blueprinter::Base
|
|
285
|
+
identifier :id
|
|
286
|
+
fields :email, :first_name, :last_name
|
|
276
287
|
end
|
|
288
|
+
|
|
289
|
+
simple_crud_for :show, serializer: AuthorBlueprint
|
|
290
|
+
simple_crud_for :index, serializer: AuthorBlueprint, serializer_options: -> { { current_user: current_user } }
|
|
277
291
|
```
|
|
278
292
|
|
|
279
293
|
#### HTML
|
|
@@ -343,6 +357,58 @@ simple_crud_for :destroy, finder: ->(params) { current_user.models.find(params[:
|
|
|
343
357
|
|
|
344
358
|
When omitted it defaults to `klass.find(params[:id])`, and `not_found` is still returned whenever the finder finds no record.
|
|
345
359
|
|
|
360
|
+
#### Cache
|
|
361
|
+
Pass `cache: { key:, ttl: }` on `:show` or `:index` to skip the DB on cache hits.
|
|
362
|
+
|
|
363
|
+
`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.
|
|
364
|
+
|
|
365
|
+
Controllers get a default `fetch_cached(key, ttl, &block)` backed by `Rails.cache`. Override to use a different store:
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# Override with a direct Redis connection
|
|
369
|
+
def fetch_cached(key, ttl)
|
|
370
|
+
cached = redis.get(key)
|
|
371
|
+
return JSON.parse(cached, symbolize_names: true) if cached
|
|
372
|
+
|
|
373
|
+
result = yield
|
|
374
|
+
redis.setex(key, ttl, result.to_json)
|
|
375
|
+
result
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# All options explicit
|
|
379
|
+
simple_crud_for :show,
|
|
380
|
+
finder: ->(p) { Article.includes(:author).find(p[:id]) },
|
|
381
|
+
cache: { key: ->(p) { "article:v1:#{p[:id]}" }, ttl: 900 } do |article|
|
|
382
|
+
article_payload(article)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# All defaults — key and TTL inferred from model name and params
|
|
386
|
+
simple_crud_for :index, paginate: false, cache: {} do |articles|
|
|
387
|
+
articles.map { |a| article_summary(a) }
|
|
388
|
+
end
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Invalidate on write. For the default key format, `expire_simple_crud_cache(action)` deletes it without constructing it by hand:
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
expire_simple_crud_cache(:show)
|
|
395
|
+
expire_simple_crud_cache(:index)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
For a custom `key:` lambda, delete the key yourself:
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
def update
|
|
402
|
+
article = current_user.articles.find(params[:id])
|
|
403
|
+
if article.update(article_params)
|
|
404
|
+
redis.del("article:v1:#{article.id}")
|
|
405
|
+
render json: article_payload(article)
|
|
406
|
+
else
|
|
407
|
+
render json: { errors: article.errors.full_messages }, status: :unprocessable_entity
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
```
|
|
411
|
+
|
|
346
412
|
### Shared examples
|
|
347
413
|
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:
|
|
348
414
|
```ruby
|
|
@@ -0,0 +1,119 @@
|
|
|
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 find_by_id if finder.nil?
|
|
54
|
+
return klass.send(finder, controller.params) unless finder.respond_to?(:call)
|
|
55
|
+
|
|
56
|
+
controller.instance_exec(controller.params, &finder)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find_by_id
|
|
60
|
+
klass.find(controller.params[:id])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_record
|
|
64
|
+
parameters[:build] ? controller.instance_exec(¶meters[:build]) : klass.new
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def permitted_params
|
|
68
|
+
controller.send("#{klass.model_name.singular}_params")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def serialize_opts(key)
|
|
72
|
+
opts = { key => parameters[:serializer] }.compact
|
|
73
|
+
options = serializer_options
|
|
74
|
+
opts[:serializer_options] = options if options.present?
|
|
75
|
+
opts
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def serializer_options(record = nil)
|
|
79
|
+
callable = parameters[:serializer_options]
|
|
80
|
+
return {} unless callable
|
|
81
|
+
|
|
82
|
+
callable.arity.zero? ? controller.instance_exec(&callable) : controller.instance_exec(record, &callable)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def render_record(record, template)
|
|
86
|
+
return render_html_record(record, template) if parameters[:html] || block
|
|
87
|
+
|
|
88
|
+
render_json_record(record)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render_html_record(record, template)
|
|
92
|
+
controller.instance_variable_set(:@record, record)
|
|
93
|
+
block ? controller.instance_exec(record, &block) : controller.render(template)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def render_json_record(record)
|
|
97
|
+
serializer = parameters[:serializer]
|
|
98
|
+
controller.render json: SimpleCrud::Serializer.render(serializer, record, serializer_options(record))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_cached(cache_opts, &fetch_block)
|
|
102
|
+
key = resolve_cache_key(cache_opts)
|
|
103
|
+
ttl = cache_opts[:ttl] || DEFAULT_CACHE_TTL
|
|
104
|
+
result = controller.send(:fetch_cached, key, ttl, &fetch_block)
|
|
105
|
+
controller.render json: result
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def resolve_cache_key(cache_opts)
|
|
109
|
+
k = cache_opts[:key]
|
|
110
|
+
return default_cache_key if k.nil?
|
|
111
|
+
|
|
112
|
+
k.respond_to?(:call) ? controller.instance_exec(controller.params, &k) : k
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def default_cache_key
|
|
116
|
+
self.class.cache_key_for(klass, action_name, controller.request.fullpath)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
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,18 @@
|
|
|
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: parameters[:status] || :created, failure_template: :new,
|
|
14
|
+
redirect: parameters[:redirect] || record }
|
|
15
|
+
persist_and_render(record, options, persist)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
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: parameters[:status] || :ok, failure_template: :show,
|
|
12
|
+
redirect: parameters[:redirect] || klass }
|
|
13
|
+
persist_and_render(record, options, persist)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
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) : serialize_records(records)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def serialize_records(records)
|
|
24
|
+
serializer = parameters[:serializer]
|
|
25
|
+
return records.as_json if serializer.nil?
|
|
26
|
+
|
|
27
|
+
records.map { |record| SimpleCrud::Serializer.render(serializer, record, serializer_options) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render_index
|
|
31
|
+
relation = index_relation
|
|
32
|
+
opts = serialize_opts(:each_serializer)
|
|
33
|
+
if parameters[:html] || block
|
|
34
|
+
render_records(relation, opts)
|
|
35
|
+
elsif parameters[:paginate]
|
|
36
|
+
SimpleCrud::Config.pagination_adapter.paginate(controller, relation, opts)
|
|
37
|
+
else
|
|
38
|
+
controller.render json: serialize_records(relation)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render_records(relation, opts)
|
|
43
|
+
records = index_records(relation, opts)
|
|
44
|
+
controller.instance_variable_set(:@records, records)
|
|
45
|
+
block ? controller.instance_exec(records, &block) : controller.render(:index)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def index_relation
|
|
49
|
+
if parameters[:scope]
|
|
50
|
+
call_scope
|
|
51
|
+
elsif parameters[:authorize]
|
|
52
|
+
SimpleCrud::Config.authorization_adapter.policy_scope(controller, klass)
|
|
53
|
+
else
|
|
54
|
+
klass.all
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def call_scope
|
|
59
|
+
user_method = SimpleCrud::Config.user_method
|
|
60
|
+
user = controller.respond_to?(user_method, true) ? controller.send(user_method) : nil
|
|
61
|
+
scope = parameters[:scope]
|
|
62
|
+
scope.arity == 1 ? scope.call(user) : scope.call(user, controller.params)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def index_records(relation, opts)
|
|
66
|
+
return relation unless parameters[:paginate]
|
|
67
|
+
|
|
68
|
+
SimpleCrud::Config.pagination_adapter.paginated_records(controller, relation, opts)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -10,7 +10,13 @@ module SimpleCrud
|
|
|
10
10
|
include Adapter
|
|
11
11
|
|
|
12
12
|
def paginate(controller, relation, options)
|
|
13
|
-
|
|
13
|
+
serializer = options[:each_serializer]
|
|
14
|
+
return controller.send(:render_paginated, relation, options) if serializer.nil?
|
|
15
|
+
|
|
16
|
+
serializer_options = options[:serializer_options] || {}
|
|
17
|
+
controller.send(:render_paginated, relation, options) do |record|
|
|
18
|
+
SimpleCrud::Serializer.render(serializer, record, serializer_options)
|
|
19
|
+
end
|
|
14
20
|
end
|
|
15
21
|
|
|
16
22
|
def paginated_records(controller, relation, options)
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
run_after_persist(record, saved)
|
|
10
|
+
return render_persisted(record, saved, options) unless block || parameters[:html]
|
|
11
|
+
|
|
12
|
+
controller.instance_variable_set(:@record, record)
|
|
13
|
+
block ? controller.instance_exec(record, saved, &block) : render_html_redirect(record, saved, options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run_after_persist(record, saved)
|
|
17
|
+
hook = parameters[:after_persist]
|
|
18
|
+
controller.instance_exec(record, saved, &hook) if hook
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def render_html_redirect(record, saved, options)
|
|
22
|
+
if saved
|
|
23
|
+
controller.redirect_to(redirect_target(record, options[:redirect]))
|
|
24
|
+
else
|
|
25
|
+
controller.render(options[:failure_template])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def redirect_target(record, target)
|
|
30
|
+
target.is_a?(Proc) ? controller.instance_exec(record, &target) : target
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render_persisted(record, saved, options)
|
|
34
|
+
return render_success(record, options[:status]) if saved
|
|
35
|
+
|
|
36
|
+
controller.render json: { errors: record.errors.full_messages }, status: 422
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render_success(record, status)
|
|
40
|
+
return controller.head(status) if status == :no_content
|
|
41
|
+
|
|
42
|
+
controller.render json: payload(record), status: status
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def payload(record)
|
|
46
|
+
SimpleCrud::Serializer.render(parameters[:serializer], record, serializer_options(record))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -51,7 +51,12 @@ module SimpleCrud
|
|
|
51
51
|
unauthenticated_status: :unauthorized,
|
|
52
52
|
assert_html_template: true,
|
|
53
53
|
policy_class: ->(klass) { "#{klass}Policy".constantize },
|
|
54
|
-
serializer_class:
|
|
54
|
+
serializer_class: lambda { |model|
|
|
55
|
+
klass = model.class
|
|
56
|
+
next "#{klass}Serializer".constantize if Kernel.const_defined?("#{klass}Serializer")
|
|
57
|
+
|
|
58
|
+
"#{klass}Blueprint".constantize
|
|
59
|
+
},
|
|
55
60
|
created_record_check: nil
|
|
56
61
|
}.freeze
|
|
57
62
|
|
|
@@ -19,7 +19,8 @@ module SimpleCrud
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def owner_foreign_key
|
|
22
|
-
|
|
22
|
+
reflection = model_class_object.reflect_on_association(owner_association)
|
|
23
|
+
reflection ? reflection.foreign_key : :"#{owner_association}_id"
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
# The owner association attributed to current_user, if the model has one.
|
|
@@ -10,8 +10,8 @@ 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
|
|
14
|
-
authenticate_headers].each do |option|
|
|
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|
|
|
15
15
|
define_method("check_#{option}") { |method| get_option(method, option) }
|
|
16
16
|
end
|
|
17
17
|
|