axn 0.1.0.pre.alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +54 -0
- data/CHANGELOG.md +5 -0
- data/CONTRIBUTING.md +37 -0
- data/LICENSE.txt +22 -0
- data/README.md +30 -0
- data/Rakefile +12 -0
- data/axn.gemspec +45 -0
- data/docs/.vitepress/config.mjs +55 -0
- data/docs/about/index.md +46 -0
- data/docs/advanced/rough.md +12 -0
- data/docs/advanced/validating-user-input.md +11 -0
- data/docs/guide/index.md +157 -0
- data/docs/index.md +26 -0
- data/docs/reference/action-result.md +12 -0
- data/docs/reference/class.md +18 -0
- data/docs/reference/configuration.md +90 -0
- data/docs/reference/instance.md +17 -0
- data/docs/usage/conventions.md +38 -0
- data/docs/usage/setup.md +26 -0
- data/docs/usage/testing.md +13 -0
- data/docs/usage/using.md +65 -0
- data/docs/usage/writing.md +118 -0
- data/lib/action/configuration.rb +46 -0
- data/lib/action/context_facade.rb +201 -0
- data/lib/action/contract.rb +190 -0
- data/lib/action/contract_validator.rb +66 -0
- data/lib/action/enqueueable.rb +74 -0
- data/lib/action/exceptions.rb +65 -0
- data/lib/action/hoist_errors.rb +51 -0
- data/lib/action/logging.rb +41 -0
- data/lib/action/organizer.rb +41 -0
- data/lib/action/swallow_exceptions.rb +104 -0
- data/lib/action/top_level_around_hook.rb +63 -0
- data/lib/axn/version.rb +5 -0
- data/lib/axn.rb +54 -0
- data/package.json +10 -0
- data/yarn.lock +1166 -0
- metadata +128 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
::: danger ALPHA
|
2
|
+
* TODO: convert this rough outline into actual documentation
|
3
|
+
:::
|
4
|
+
|
5
|
+
|
6
|
+
While coding:
|
7
|
+
* `expose`
|
8
|
+
* `fail!`
|
9
|
+
* `log`
|
10
|
+
* `try` - any exceptions raised by the block will trigger the on_exception handler, but then will be swallowed (the action is _not_ failed)
|
11
|
+
* Edge case: explicit `fail!` calls _will_ still fail the action
|
12
|
+
* `hoist_errors`
|
13
|
+
* Edge case: intent is a single action call in the block -- if there are multiple calls, only the last one will be checked (anything explicitly _raised_ will still be handled).
|
14
|
+
<!-- TODO: is there difference between `SubAction.call!` and `hoist_errors { SubAction.call }`?? -->
|
15
|
+
* `context_for_logging` (and decent #inspect support)
|
16
|
+
|
17
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
---
|
2
|
+
outline: deep
|
3
|
+
---
|
4
|
+
|
5
|
+
# Conventions
|
6
|
+
|
7
|
+
This page serves as a repository for various softly-held opinions about _how_ it makes sense to use the library.
|
8
|
+
|
9
|
+
::: warning DRAFT
|
10
|
+
These conventions are still in flux as the library is solidified and we gain more experience using it in production. Take these notes with a grain of salt.
|
11
|
+
:::
|
12
|
+
|
13
|
+
## Organizing Actions (Rails)
|
14
|
+
|
15
|
+
You _can_ `include Action` into _any_ Ruby class, but to keep track of things we've found it helpful to:
|
16
|
+
|
17
|
+
* Create a new `app/actions` folder for our actions
|
18
|
+
* Name them `Actions::[DOMAIN]::[VERB]` where `[DOMAIN]` is a (possibly nested) identifier and `[VERB]` is the action to be taken.
|
19
|
+
|
20
|
+
Examples:
|
21
|
+
* `Actions::User::Create`
|
22
|
+
* `Actions::Slack::Notify`
|
23
|
+
|
24
|
+
For us, we've found the maintenance benefits of knowing roughly how the class will behave just by glancing at the name has been worth being a bit pedantic about the naming.
|
25
|
+
|
26
|
+
## Naming conventions
|
27
|
+
|
28
|
+
### The responsible user
|
29
|
+
When tracking _who_ is responsible for the action being taken, one option is to inject it globally via `Current.user` (see: [Current Attributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html)), but that only works if you're _sure_ you're never going to want to enqueue the job on a background processor.
|
30
|
+
|
31
|
+
More generally, we've adopted the convention of passing in the responsible user as `actor`:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class Foo
|
35
|
+
include Action
|
36
|
+
expects :actor, type: User # [!code focus]
|
37
|
+
end
|
38
|
+
```
|
data/docs/usage/setup.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
---
|
2
|
+
outline: deep
|
3
|
+
---
|
4
|
+
# Getting Started
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Adding `axn` to your Gemfile is enough to start using `include Action`.
|
9
|
+
<!-- todo bundler -->
|
10
|
+
|
11
|
+
|
12
|
+
## Global Configuration
|
13
|
+
|
14
|
+
A few configuration steps are _highly_ recommended to get the full benefits (e.g. making sure all your swallowed exceptions are getting reported to your error tracking service).
|
15
|
+
|
16
|
+
The full set of available configuration settings is documented [over here](/reference/configuration), but there are two worth calling out specifically:
|
17
|
+
|
18
|
+
### Error Tracking
|
19
|
+
|
20
|
+
By default any swallowed errors are noted in the logs, but it's _highly recommended_ to [wire up an `on_exception` handler](/reference/configuration#on-exception).
|
21
|
+
|
22
|
+
### Metrics / Tracing
|
23
|
+
|
24
|
+
If you're using an APM provider, observability can be greatly enhanced by [configuring a `top_level_around_hook`](/reference/configuration#top-level-around-hook).
|
25
|
+
|
26
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Testing
|
2
|
+
|
3
|
+
::: danger ALPHA
|
4
|
+
* TODO: document testing patterns
|
5
|
+
:::
|
6
|
+
|
7
|
+
Configuring rspec to treat files in spec/actions as service specs:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
config.define_derived_metadata(file_path: %r{spec/actions}) do |metadata|
|
11
|
+
metadata[:type] = :service
|
12
|
+
end
|
13
|
+
```
|
data/docs/usage/using.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
---
|
2
|
+
outline: deep
|
3
|
+
---
|
4
|
+
|
5
|
+
|
6
|
+
# How to _use_ an Action
|
7
|
+
|
8
|
+
## Common Case
|
9
|
+
|
10
|
+
An action is usually executed via `#call`, and _always_ returns an instance of the `Action::Result` class.
|
11
|
+
|
12
|
+
This means the result _always_ implements a consistent interface, including `ok?` and `error` (see [full details](/reference/action-result)) as well as any variables that it `exposes`. Remember any exceptions have been swallowed.
|
13
|
+
|
14
|
+
As a consumer, you usually want a conditional that surfaces `error` unless the result is `ok?`, and otherwise takes whatever success action is relevant.
|
15
|
+
|
16
|
+
For example:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
class MessagesController < ApplicationController
|
20
|
+
def create
|
21
|
+
result = Actions::Slack::Post.call( # [!code focus]
|
22
|
+
channel: "#engineering",
|
23
|
+
message: params[:message],
|
24
|
+
)
|
25
|
+
|
26
|
+
if result.ok? # [!code focus:2]
|
27
|
+
@thread_id = result.thread_id # Because `thread_id` was explicitly exposed
|
28
|
+
flash.now[:success] = "Sent the Slack message"
|
29
|
+
else
|
30
|
+
flash[:alert] = result.error # [!code focus]
|
31
|
+
redirect_to action: :new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
<!-- TODO: replace manual flash success with result.success (here and in guide?) -->
|
38
|
+
|
39
|
+
|
40
|
+
## Advanced Usage
|
41
|
+
|
42
|
+
### `#call!`
|
43
|
+
|
44
|
+
::: danger ALPHA
|
45
|
+
* TODO - flesh out this section
|
46
|
+
:::
|
47
|
+
|
48
|
+
|
49
|
+
* `call!`
|
50
|
+
* call! -- will raise any exceptions OR our own Action::Failure if user-facing error occurred (otherwise non-bang will never raise)
|
51
|
+
* note call! still logs completion even if failure (from configuration's on_exception)
|
52
|
+
|
53
|
+
|
54
|
+
### `#enqueue`
|
55
|
+
|
56
|
+
Before adopting this library, our code was littered with one-line workers whose only job was to fire off a service on a background job. We were able to remove that entire glue layer by directly supporting enqueueing sidekiq jobs from the Action itself.
|
57
|
+
|
58
|
+
::: danger ALPHA
|
59
|
+
Sidekiq integration is NOT YET TESTED/NOT YET USED IN OUR APP, and naming will VERY LIKELY change to make it clearer which actions will be retried!
|
60
|
+
:::
|
61
|
+
|
62
|
+
* enqueue vs enqueue!
|
63
|
+
* enqueue will not retry even if fails
|
64
|
+
* enqueue! will go through normal sidekiq retries on any failure (including user-facing `fail!`)
|
65
|
+
* Note implicit GlobalID support (if not serializable, will get ArgumentError at callsite)
|
@@ -0,0 +1,118 @@
|
|
1
|
+
---
|
2
|
+
outline: deep
|
3
|
+
---
|
4
|
+
|
5
|
+
# How to _build_ an Action
|
6
|
+
|
7
|
+
The core boilerplate is pretty minimal:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class Foo
|
11
|
+
include Action
|
12
|
+
|
13
|
+
def call
|
14
|
+
# ... do some stuff here?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
## Declare the interface
|
20
|
+
|
21
|
+
The first step is to determine what arguments you expect to be passed into `call`. These are declared via the `expects` keyword.
|
22
|
+
|
23
|
+
If you want to expose any results to the caller, declare that via the `exposes` keyword.
|
24
|
+
|
25
|
+
Both of these optionally accept `type:`, `allow_blank:`, and any other ActiveModel validation (see: [reference](/reference/class)).
|
26
|
+
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class Foo
|
30
|
+
include Action
|
31
|
+
|
32
|
+
expects :name, type: String # [!code focus:2]
|
33
|
+
exposes :meaning_of_life
|
34
|
+
|
35
|
+
def call
|
36
|
+
# ... do some stuff here?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
## Implement the action
|
42
|
+
|
43
|
+
Once the interface is defined, you're primarily focused on defining the `call` method.
|
44
|
+
|
45
|
+
To abort execution with a specific error message, call `fail!`.
|
46
|
+
|
47
|
+
If you declare that your action `exposes` anything, you need to actually `expose` it.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class Foo
|
51
|
+
include Action
|
52
|
+
|
53
|
+
expects :name, type: String
|
54
|
+
exposes :meaning_of_life
|
55
|
+
|
56
|
+
def call
|
57
|
+
fail! "Douglas already knows the meaning" if name == "Doug" # [!code focus]
|
58
|
+
|
59
|
+
msg = "Hello #{name}, the meaning of life is 42"
|
60
|
+
expose meaning_of_life: msg # [!code focus]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
See [the reference doc](/reference/instance) for a few more handy helper methods (e.g. `#log`).
|
66
|
+
|
67
|
+
## Customizing messages
|
68
|
+
|
69
|
+
::: danger ALPHA
|
70
|
+
* TODO: document `messages` setup
|
71
|
+
:::
|
72
|
+
|
73
|
+
|
74
|
+
## Lifecycle methods
|
75
|
+
|
76
|
+
In addition to `#call`, there are a few additional pieces to be aware of:
|
77
|
+
|
78
|
+
### `#rollback`
|
79
|
+
|
80
|
+
If you define a `#rollback` method, it'll be called (_before_ returning an `Action::Result` to the caller) whenever your action fails.
|
81
|
+
|
82
|
+
### Hooks
|
83
|
+
|
84
|
+
`before`, `after`, and `around` hooks are also supported.
|
85
|
+
|
86
|
+
### Concrete example
|
87
|
+
|
88
|
+
Given this series of methods and hooks:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class Foo
|
92
|
+
include Action
|
93
|
+
|
94
|
+
before { log("before hook") }
|
95
|
+
after { log("after hook") }
|
96
|
+
|
97
|
+
def call
|
98
|
+
log("in call")
|
99
|
+
raise "oh no something borked"
|
100
|
+
end
|
101
|
+
|
102
|
+
def rollback
|
103
|
+
log("rolling back")
|
104
|
+
end
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
`Foo.call` would fail (because of the raise), but along the way would end up logging:
|
109
|
+
|
110
|
+
```text
|
111
|
+
before hook
|
112
|
+
in call
|
113
|
+
after hook
|
114
|
+
rolling back
|
115
|
+
```
|
116
|
+
|
117
|
+
## Debugging
|
118
|
+
Remember you can [enable debug logging](/reference/configuration.html#global-debug-logging) to print log lines before and after each action is executed.
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Action
|
4
|
+
class Configuration
|
5
|
+
include Action::Logging
|
6
|
+
attr_accessor :global_debug_logging, :top_level_around_hook
|
7
|
+
attr_writer :logger, :env, :on_exception, :additional_includes
|
8
|
+
|
9
|
+
def global_debug_logging? = !!global_debug_logging
|
10
|
+
|
11
|
+
def additional_includes = @additional_includes ||= []
|
12
|
+
|
13
|
+
def on_exception(e, action:, context: {})
|
14
|
+
if @on_exception
|
15
|
+
# TODO: only pass action: or context: if requested
|
16
|
+
@on_exception.call(e, action:, context:)
|
17
|
+
else
|
18
|
+
log("[#{action.class.name.presence || "Anonymous Action"}] Exception swallowed: #{e.class.name} - #{e.message}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def logger
|
23
|
+
@logger ||= begin
|
24
|
+
Rails.logger
|
25
|
+
rescue NameError
|
26
|
+
Logger.new($stdout).tap do |l|
|
27
|
+
l.level = Logger::INFO
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def env
|
33
|
+
@env ||= ENV["RACK_ENV"].presence || ENV["RAILS_ENV"].presence || "development"
|
34
|
+
ActiveSupport::StringInquirer.new(@env)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def config = @config ||= Configuration.new
|
40
|
+
|
41
|
+
def configure
|
42
|
+
self.config ||= Configuration.new
|
43
|
+
yield(config) if block_given?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/parameter_filter"
|
4
|
+
|
5
|
+
module Action
|
6
|
+
class ContextFacade
|
7
|
+
def initialize(action:, context:, declared_fields:, implicitly_allowed_fields: nil)
|
8
|
+
if self.class.name == "Action::ContextFacade" # rubocop:disable Style/ClassEqualityComparison
|
9
|
+
raise "Action::ContextFacade is an abstract class and should not be instantiated directly"
|
10
|
+
end
|
11
|
+
|
12
|
+
@context = context
|
13
|
+
@action = action
|
14
|
+
@declared_fields = declared_fields
|
15
|
+
|
16
|
+
(@declared_fields + Array(implicitly_allowed_fields)).each do |field|
|
17
|
+
singleton_class.define_method(field) { @context.public_send(field) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :declared_fields
|
22
|
+
|
23
|
+
def inspect = Inspector.new(facade: self, action:, context:).call
|
24
|
+
|
25
|
+
def fail!(...)
|
26
|
+
raise Action::ContractViolation::MethodNotAllowed, "Call fail! directly rather than on the context"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :action, :context
|
32
|
+
|
33
|
+
def exposure_method_name = raise NotImplementedError
|
34
|
+
|
35
|
+
# Add nice error message for missing methods
|
36
|
+
def method_missing(method_name, ...) # rubocop:disable Style/MissingRespondToMissing (because we're not actually responding to anything additional)
|
37
|
+
if context.respond_to?(method_name)
|
38
|
+
msg = <<~MSG
|
39
|
+
Method ##{method_name} is not available on #{self.class.name}!
|
40
|
+
|
41
|
+
#{@action.class.name || "The action"} may be missing a line like:
|
42
|
+
#{exposure_method_name} :#{method_name}
|
43
|
+
MSG
|
44
|
+
|
45
|
+
raise Action::ContractViolation::MethodNotAllowed, msg
|
46
|
+
end
|
47
|
+
|
48
|
+
super
|
49
|
+
end
|
50
|
+
|
51
|
+
def determine_error_message(only_default: false)
|
52
|
+
return @context.error_from_user if @context.error_from_user.present?
|
53
|
+
|
54
|
+
unless only_default
|
55
|
+
msg = message_from_rescues
|
56
|
+
return msg if msg.present?
|
57
|
+
end
|
58
|
+
|
59
|
+
the_exception = @context.exception || (only_default ? Action::Failure.new(@context) : nil)
|
60
|
+
stringified(action._error_msg, exception: the_exception).presence || "Something went wrong"
|
61
|
+
end
|
62
|
+
|
63
|
+
def message_from_rescues
|
64
|
+
Array(action._error_rescues).each do |(matcher, value)|
|
65
|
+
matches = if matcher.respond_to?(:call)
|
66
|
+
if matcher.arity == 1
|
67
|
+
!!action.instance_exec(exception, &matcher)
|
68
|
+
else
|
69
|
+
!!action.instance_exec(&matcher)
|
70
|
+
end
|
71
|
+
elsif matcher.is_a?(String) || matcher.is_a?(Symbol)
|
72
|
+
klass = Object.const_get(matcher.to_s)
|
73
|
+
klass && exception.is_a?(klass)
|
74
|
+
elsif matcher < Exception
|
75
|
+
exception.is_a?(matcher)
|
76
|
+
else
|
77
|
+
action.warn("Ignoring matcher #{matcher.inspect} in rescues command")
|
78
|
+
end
|
79
|
+
|
80
|
+
return stringified(value, exception:) if matches
|
81
|
+
end
|
82
|
+
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# Allow for callable OR string messages
|
87
|
+
def stringified(msg, exception: nil)
|
88
|
+
return msg.presence unless msg.respond_to?(:call)
|
89
|
+
|
90
|
+
# The error message callable can take the exception as an argument
|
91
|
+
if exception && msg.arity == 1
|
92
|
+
action.instance_exec(exception, &msg)
|
93
|
+
else
|
94
|
+
action.instance_exec(&msg)
|
95
|
+
end
|
96
|
+
rescue StandardError => e
|
97
|
+
action.warn("Ignoring #{e.class.name} raised while determining message callable: #{e.message}")
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Inbound / Internal ContextFacade
|
103
|
+
class InternalContext < ContextFacade
|
104
|
+
# So can be referenced from within e.g. rescues callables
|
105
|
+
def default_error
|
106
|
+
[@context.error_prefix, determine_error_message(only_default: true)].compact.join(" ").squeeze(" ")
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def exposure_method_name = :gets
|
112
|
+
end
|
113
|
+
|
114
|
+
# Outbound / External ContextFacade
|
115
|
+
class Result < ContextFacade
|
116
|
+
# Poke some holes for necessary internal control methods
|
117
|
+
delegate :called!, :rollback!, :each_pair, to: :context
|
118
|
+
|
119
|
+
# External interface
|
120
|
+
delegate :success?, :failure?, :exception, to: :context
|
121
|
+
def ok? = success?
|
122
|
+
|
123
|
+
def error
|
124
|
+
return if ok?
|
125
|
+
|
126
|
+
[@context.error_prefix, determine_error_message].compact.join(" ").squeeze(" ")
|
127
|
+
end
|
128
|
+
|
129
|
+
def success
|
130
|
+
return unless ok?
|
131
|
+
|
132
|
+
stringified(action._success_msg).presence || "Action completed successfully"
|
133
|
+
end
|
134
|
+
|
135
|
+
def ok = success
|
136
|
+
|
137
|
+
def message = error || success
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def exposure_method_name = :sets
|
142
|
+
end
|
143
|
+
|
144
|
+
class Inspector
|
145
|
+
def initialize(action:, facade:, context:)
|
146
|
+
@action = action
|
147
|
+
@facade = facade
|
148
|
+
@context = context
|
149
|
+
end
|
150
|
+
|
151
|
+
def call
|
152
|
+
str = [status, visible_fields].compact_blank.join(" ")
|
153
|
+
|
154
|
+
"#<#{class_name} #{str}>"
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
attr_reader :action, :facade, :context
|
160
|
+
|
161
|
+
def status
|
162
|
+
return unless facade.is_a?(Action::Result)
|
163
|
+
|
164
|
+
return "[OK]" if context.success?
|
165
|
+
return "[failed with '#{context.error_from_user}']" unless context.exception
|
166
|
+
|
167
|
+
%([failed with #{context.exception.class.name}: '#{context.exception.message}'])
|
168
|
+
end
|
169
|
+
|
170
|
+
def visible_fields
|
171
|
+
declared_fields.map do |field|
|
172
|
+
value = facade.public_send(field)
|
173
|
+
|
174
|
+
"#{field}: #{format_for_inspect(field, value)}"
|
175
|
+
end.join(", ")
|
176
|
+
end
|
177
|
+
|
178
|
+
def class_name = facade.class.name
|
179
|
+
def declared_fields = facade.send(:declared_fields)
|
180
|
+
|
181
|
+
def format_for_inspect(field, value)
|
182
|
+
return value.inspect if value.nil?
|
183
|
+
|
184
|
+
# Initially based on https://github.com/rails/rails/blob/800976975253be2912d09a80757ee70a2bb1e984/activerecord/lib/active_record/attribute_methods.rb#L527
|
185
|
+
inspected_value = if value.is_a?(String) && value.length > 50
|
186
|
+
"#{value[0, 50]}...".inspect
|
187
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
188
|
+
%("#{value.to_fs(:inspect)}")
|
189
|
+
elsif defined?(::ActiveRecord::Relation) && value.instance_of?(::ActiveRecord::Relation)
|
190
|
+
# Avoid hydrating full AR relation (i.e. avoid loading records just to report an error)
|
191
|
+
"#{value.name}::ActiveRecord_Relation"
|
192
|
+
else
|
193
|
+
value.inspect
|
194
|
+
end
|
195
|
+
|
196
|
+
inspection_filter.filter_param(field, inspected_value)
|
197
|
+
end
|
198
|
+
|
199
|
+
def inspection_filter = action.send(:inspection_filter)
|
200
|
+
end
|
201
|
+
end
|