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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -0
  3. data/CHANGELOG.md +21 -0
  4. data/README.md +89 -12
  5. data/lib/simple_crud/action_context.rb +98 -0
  6. data/lib/simple_crud/action_lambdas.rb +4 -72
  7. data/lib/simple_crud/cache_helpers.rb +17 -0
  8. data/lib/simple_crud/create_context.rb +17 -0
  9. data/lib/simple_crud/destroy_context.rb +15 -0
  10. data/lib/simple_crud/edit_context.rb +13 -0
  11. data/lib/simple_crud/index_context.rb +64 -0
  12. data/lib/simple_crud/new_context.rb +13 -0
  13. data/lib/simple_crud/persistence_context.rb +33 -0
  14. data/lib/simple_crud/rspec/helpers/controller_options.rb +6 -1
  15. data/lib/simple_crud/rspec/helpers/models.rb +16 -1
  16. data/lib/simple_crud/show_context.rb +27 -0
  17. data/lib/simple_crud/simple_crud_controller.rb +35 -9
  18. data/lib/simple_crud/update_context.rb +16 -0
  19. data/lib/simple_crud/version.rb +1 -1
  20. data/lib/simple_crud.rb +1 -0
  21. data/lib/spec/shared_examples/authorization_adapter_authorize.rb +1 -1
  22. data/lib/spec/shared_examples/simple_crud_for_create.rb +1 -1
  23. data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +1 -1
  24. data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +1 -1
  25. data/lib/spec/shared_examples/simple_crud_for_destroy.rb +2 -2
  26. data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +1 -1
  27. data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +1 -1
  28. data/lib/spec/shared_examples/simple_crud_for_edit.rb +1 -1
  29. data/lib/spec/shared_examples/simple_crud_for_index.rb +1 -1
  30. data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +1 -1
  31. data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +1 -1
  32. data/lib/spec/shared_examples/simple_crud_for_new.rb +1 -1
  33. data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +1 -1
  34. data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +1 -1
  35. data/lib/spec/shared_examples/simple_crud_for_show.rb +1 -1
  36. data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +1 -1
  37. data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +1 -1
  38. data/lib/spec/shared_examples/simple_crud_for_update.rb +1 -1
  39. data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +1 -1
  40. data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +1 -1
  41. data/lib/spec/shared_examples/simple_crud_renders_template.rb +1 -1
  42. data/lib/spec/shared_examples/simple_crud_when_not_authorized.rb +1 -1
  43. data/lib/spec/shared_examples/simple_crud_without_authenticated_user.rb +1 -1
  44. data/lib/spec/shared_examples/unauthorized_when_not_logged_in.rb +1 -1
  45. data/spec/cached/dummy_models_controller_spec.rb +23 -0
  46. data/spec/cached_defaults/dummy_models_controller_spec.rb +34 -0
  47. data/spec/dummy/app/controllers/block/dummy_models_controller.rb +1 -6
  48. data/spec/dummy/app/controllers/block_new/dummy_models_controller.rb +1 -1
  49. data/spec/dummy/app/controllers/block_redirect/dummy_models_controller.rb +1 -1
  50. data/spec/dummy/app/controllers/block_show/dummy_models_controller.rb +1 -1
  51. data/spec/dummy/app/controllers/built/dummy_models_controller.rb +0 -5
  52. data/spec/dummy/app/controllers/cached/dummy_models_controller.rb +14 -0
  53. data/spec/dummy/app/controllers/cached_defaults/dummy_models_controller.rb +11 -0
  54. data/spec/dummy/app/controllers/dummy_models_controller.rb +0 -5
  55. data/spec/dummy/app/controllers/finder/dummy_models_controller.rb +0 -5
  56. data/spec/dummy/app/controllers/html_modes/base_controller.rb +0 -5
  57. data/spec/dummy/app/controllers/invalid_status/dummy_models_controller.rb +1 -1
  58. data/spec/dummy/app/controllers/nested/dummy_models_controller.rb +0 -5
  59. data/spec/dummy/app/controllers/nested_route/dummy_models_controller.rb +0 -5
  60. data/spec/dummy/app/controllers/redirect_auth/dummy_models_controller.rb +0 -5
  61. data/spec/dummy/app/controllers/scoped/dummy_models_controller.rb +0 -5
  62. data/spec/dummy/app/controllers/scoped_params/dummy_models_controller.rb +0 -5
  63. data/spec/dummy/app/controllers/strict/dummy_models_controller.rb +0 -5
  64. data/spec/dummy/app/controllers/unpaginated_scoped/dummy_models_controller.rb +0 -5
  65. data/spec/dummy/app/controllers/without_pagination/dummy_models_controller.rb +0 -5
  66. data/spec/dummy/config/routes.rb +8 -0
  67. data/spec/simple_crud/action_context_spec.rb +22 -0
  68. data/spec/simple_crud/index_context_spec.rb +31 -0
  69. data/spec/simple_crud/rspec_config_spec.rb +6 -0
  70. data/spec/simple_crud/show_context_spec.rb +14 -0
  71. data/spec/simple_crud_controller_spec.rb +38 -0
  72. metadata +19 -4
  73. data/lib/simple_crud/controller_helpers.rb +0 -128
  74. 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: e280efd8ead44768d16e0ba663e25a40d6972b53e253281d1c7eac0735421b64
4
- data.tar.gz: 78625a2a759ca5fda1e7d41e421db92ed721f2e2c343524f0580cd810d32ac7a
3
+ metadata.gz: b5b50548416811b034a75f72c96d4e94c41cdb9883d7112cdf720ad3febaeb11
4
+ data.tar.gz: 99ae1562bae38c2fa8ff1d697859f8643e2b818aaeb66f2e14220171fa9f28dc
5
5
  SHA512:
6
- metadata.gz: 406a83cc98deaa7fa3ba5f7ccb9a12486a903b6aef81bddd0a5f4f9c595ffa64e7414cb8a718d4a4fe96f987fcefaf34a8b201cbe16aab4db2721eea0bd19397
7
- data.tar.gz: 7177530ba1a01fcf526c406a14f3bf64bfcf895ce5675f4acca5f24f28c57db90859ddab43866d314f70e723cfebda24bc5930d64ac66a01d29bc6246792c04a
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 it should use Devise to check for a current_user
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
- SimpleCrud will assume a current_user method. Future versions will support a custom model. Defining a current_user method in ApplicationController should work if you're using a different model, as of now.
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`. Passing a block also implies `html: true` for the shared examples (so a server-rendered block is asserted as HTML); if your block renders JSON instead, pass `html: false` explicitly:
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
- simple_crud_for :index do |records|
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, just add `require 'simple_crud/rspec'` to your `rails_helper.rb` file and add the lines you need to your `*_spec.rb` files:
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(&parameters[: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
- 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)
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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ class EditContext < ActionContext
5
+ private
6
+
7
+ def run
8
+ record = find_record
9
+ maybe_authorize(record)
10
+ render_record(record, :edit)
11
+ end
12
+ end
13
+ 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCrud
4
+ class NewContext < ActionContext
5
+ private
6
+
7
+ def run
8
+ record = build_record
9
+ maybe_authorize(record)
10
+ render_record(record, :new)
11
+ end
12
+ end
13
+ 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].each do |option|
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 ||= create_record(model_class, model_attributes)
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