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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -0
  3. data/CHANGELOG.md +28 -0
  4. data/Gemfile +5 -1
  5. data/README.md +69 -3
  6. data/lib/simple_crud/action_context.rb +119 -0
  7. data/lib/simple_crud/action_lambdas.rb +4 -72
  8. data/lib/simple_crud/cache_helpers.rb +17 -0
  9. data/lib/simple_crud/create_context.rb +18 -0
  10. data/lib/simple_crud/destroy_context.rb +16 -0
  11. data/lib/simple_crud/edit_context.rb +13 -0
  12. data/lib/simple_crud/index_context.rb +71 -0
  13. data/lib/simple_crud/new_context.rb +13 -0
  14. data/lib/simple_crud/pagination/wor_paginate_adapter.rb +7 -1
  15. data/lib/simple_crud/persistence_context.rb +49 -0
  16. data/lib/simple_crud/rspec/config.rb +6 -1
  17. data/lib/simple_crud/rspec/helpers/authentication.rb +2 -1
  18. data/lib/simple_crud/rspec/helpers/controller_options.rb +2 -2
  19. data/lib/simple_crud/rspec/helpers/requests.rb +5 -2
  20. data/lib/simple_crud/serializer.rb +19 -0
  21. data/lib/simple_crud/show_context.rb +29 -0
  22. data/lib/simple_crud/simple_crud_controller.rb +17 -4
  23. data/lib/simple_crud/update_context.rb +17 -0
  24. data/lib/simple_crud/version.rb +1 -1
  25. data/lib/spec/matchers/have_been_serialized_with.rb +19 -2
  26. data/lib/spec/shared_examples/simple_crud_for_create.rb +6 -6
  27. data/lib/spec/shared_examples/simple_crud_for_create_with_block.rb +2 -2
  28. data/lib/spec/shared_examples/simple_crud_for_create_with_build.rb +1 -1
  29. data/lib/spec/shared_examples/simple_crud_for_destroy.rb +9 -9
  30. data/lib/spec/shared_examples/simple_crud_for_destroy_with_block.rb +1 -1
  31. data/lib/spec/shared_examples/simple_crud_for_destroy_with_finder.rb +3 -3
  32. data/lib/spec/shared_examples/simple_crud_for_edit.rb +2 -2
  33. data/lib/spec/shared_examples/simple_crud_for_index.rb +3 -3
  34. data/lib/spec/shared_examples/simple_crud_for_index_with_block.rb +1 -1
  35. data/lib/spec/shared_examples/simple_crud_for_index_with_scope.rb +1 -1
  36. data/lib/spec/shared_examples/simple_crud_for_new.rb +1 -1
  37. data/lib/spec/shared_examples/simple_crud_for_new_with_block.rb +1 -1
  38. data/lib/spec/shared_examples/simple_crud_for_new_with_build.rb +1 -1
  39. data/lib/spec/shared_examples/simple_crud_for_show.rb +2 -2
  40. data/lib/spec/shared_examples/simple_crud_for_show_with_block.rb +1 -1
  41. data/lib/spec/shared_examples/simple_crud_for_show_with_finder.rb +1 -1
  42. data/lib/spec/shared_examples/simple_crud_for_update.rb +7 -7
  43. data/lib/spec/shared_examples/simple_crud_for_update_with_finder.rb +1 -1
  44. data/lib/spec/shared_examples/simple_crud_not_found_with_finder.rb +1 -1
  45. data/spec/blueprint/dummy_models_controller_spec.rb +25 -0
  46. data/spec/blueprint_unpaginated/dummy_models_controller_spec.rb +9 -0
  47. data/spec/cached/dummy_models_controller_spec.rb +23 -0
  48. data/spec/cached_defaults/dummy_models_controller_spec.rb +34 -0
  49. data/spec/dummy/app/blueprints/dummy_model_blueprint.rb +10 -0
  50. data/spec/dummy/app/controllers/blueprint/dummy_models_controller.rb +21 -0
  51. data/spec/dummy/app/controllers/blueprint_unpaginated/dummy_models_controller.rb +10 -0
  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/config/routes.rb +16 -0
  55. data/spec/dummy_model_blueprint_spec.rb +16 -0
  56. data/spec/simple_crud/action_context_spec.rb +41 -0
  57. data/spec/simple_crud/index_context_spec.rb +31 -0
  58. data/spec/simple_crud/rspec_config_spec.rb +13 -0
  59. data/spec/simple_crud/serializer_spec.rb +28 -0
  60. data/spec/simple_crud/show_context_spec.rb +14 -0
  61. metadata +28 -5
  62. data/lib/simple_crud/controller_helpers.rb +0 -128
  63. 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: dbcb6881c359fa3b5c4ce518871a4f378d92e0698ab7d57dc489315f2f25b80b
4
- data.tar.gz: 62cf8cbd6df845cd23a388e9b2a1f8ebfdae784ff91f4e57268688bce0c0bf87
3
+ metadata.gz: b40c7867a484ffc3807b8dceac29ac1f6cc49ccca6d8a9bb067f93f839703e0c
4
+ data.tar.gz: 5a72d84147fc1f149695fe0244a11e7f99f2c9b4debb0b99a291e16439b873ac
5
5
  SHA512:
6
- metadata.gz: 4406326eab40876599274e927df02c9c3470d3f753dcd5f0f150b67a7b3087e2a965ec0fd18ad1e007bada514954f9a665d573d18da98bf7c93aee3ffebc1637
7
- data.tar.gz: 1deddc698fb6008531d59e5656675086a309dbc26177b403c023e7743463425626b8e977785f6ef2cbe122d4f251ad697944d1c71f665e498666feecedbae792
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.4'
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
- 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.
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, :institution, :role, :id
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(&parameters[: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
- 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,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,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,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
@@ -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
@@ -10,7 +10,13 @@ module SimpleCrud
10
10
  include Adapter
11
11
 
12
12
  def paginate(controller, relation, options)
13
- controller.send(:render_paginated, relation, options)
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: ->(model) { "#{model.class}_serializer".classify.constantize },
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
- :"#{owner_association}_id"
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 html finder scope build raise_on_invalid block
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