well_formed 0.1.0 → 0.1.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 +4 -4
- data/.rubocop.yml +6 -0
- data/README.md +14 -4
- data/gems/well_formed-dry_types/.rspec +3 -0
- data/gems/well_formed-dry_types/Gemfile +12 -0
- data/gems/well_formed-dry_types/README.md +107 -0
- data/gems/well_formed-dry_types/Rakefile +10 -0
- data/gems/well_formed-dry_types/lib/well_formed/dry_types/version.rb +7 -0
- data/gems/well_formed-dry_types/lib/well_formed/dry_types.rb +57 -0
- data/gems/well_formed-dry_types/lib/well_formed-dry_types.rb +8 -0
- data/gems/well_formed-dry_types/spec/spec_helper.rb +17 -0
- data/gems/well_formed-dry_types/spec/well_formed/dry_types_spec.rb +215 -0
- data/gems/well_formed-dry_types/well_formed-dry_types.gemspec +28 -0
- data/gems/well_formed-paper_trail/.rspec +3 -0
- data/gems/well_formed-paper_trail/Gemfile +12 -0
- data/gems/well_formed-paper_trail/README.md +106 -0
- data/gems/well_formed-paper_trail/Rakefile +10 -0
- data/gems/well_formed-paper_trail/lib/well_formed/paper_trail/version.rb +7 -0
- data/gems/well_formed-paper_trail/lib/well_formed/paper_trail.rb +55 -0
- data/gems/well_formed-paper_trail/lib/well_formed-paper_trail.rb +8 -0
- data/gems/well_formed-paper_trail/spec/spec_helper.rb +13 -0
- data/gems/well_formed-paper_trail/spec/well_formed/paper_trail_spec.rb +261 -0
- data/gems/well_formed-paper_trail/well_formed-paper_trail.gemspec +28 -0
- data/gems/well_formed-pundit/Gemfile +0 -1
- data/lib/well_formed/extensions.rb +18 -0
- data/lib/well_formed/simple_action.rb +1 -0
- data/lib/well_formed/simple_resource.rb +1 -0
- data/lib/well_formed/simple_struct.rb +1 -0
- data/lib/well_formed/transactional.rb +13 -4
- data/lib/well_formed/version.rb +1 -1
- data/lib/well_formed.rb +2 -0
- data/sig/well_formed.rbs +0 -6
- metadata +22 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76f1e5f45582808eddd2fd54df5c806ff9fdf7c98b2d48a733e0918c99404056
|
|
4
|
+
data.tar.gz: 868cb13f7192a529027b9809638f1b1b3e61bd9e53e647589be5917c2714ef25
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05ea83dd04cce981118f8b3154d55a81b26919054cbee2877918630937c4f7540aa672a41a0ce9eabd8e8062d1387f6c8bab23421020d6007fc1923a38aa3cc4
|
|
7
|
+
data.tar.gz: 201011a76f89b5a84f689f91eddff8504ebb6e40b45cd5fd1d9bb28eb64718bd394e19c3d6f070607e6932d8055586e5cf4fe5e37b22e6074eb6ee575bb9da8c
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
|
@@ -6,18 +6,16 @@ Form objects are compatible with `form_with` and Rails view helpers anywhere an
|
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
|
10
|
-
|
|
11
9
|
Install the gem and add to the application's Gemfile by executing:
|
|
12
10
|
|
|
13
11
|
```bash
|
|
14
|
-
bundle add
|
|
12
|
+
bundle add well_formed
|
|
15
13
|
```
|
|
16
14
|
|
|
17
15
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
18
16
|
|
|
19
17
|
```bash
|
|
20
|
-
gem install
|
|
18
|
+
gem install well_formed
|
|
21
19
|
```
|
|
22
20
|
|
|
23
21
|
## Usage
|
|
@@ -850,6 +848,18 @@ form.user # => current_user
|
|
|
850
848
|
|
|
851
849
|
`ResourceForm`, `ActionForm`, and `Struct` are themselves implemented this way — they inherit from their Simple counterpart and prepend `WithUser`.
|
|
852
850
|
|
|
851
|
+
## Plugins
|
|
852
|
+
|
|
853
|
+
| Gem | Description |
|
|
854
|
+
|-----|-------------|
|
|
855
|
+
| [well_formed-pundit](gems/well_formed-pundit/README.md) | [Pundit](https://github.com/varvet/pundit) authorization — adds `authorize!`, `policy`, and `policy_scope` helpers to any WellFormed form |
|
|
856
|
+
| [well_formed-paper_trail](gems/well_formed-paper_trail/README.md) | [PaperTrail](https://github.com/paper-trail-gem/paper_trail) versioning — automatically sets `whodunnit` to the form's `user` around every `save` and `perform`, with a `paper_trail_whodunnit` macro for custom values |
|
|
857
|
+
| [well_formed-dry_types](gems/well_formed-dry_types/README.md) | [dry-types](https://dry-rb.org/gems/dry-types) coercion — declare typed attributes with `dry_attribute`; coercion runs before validation with configurable error messages |
|
|
858
|
+
|
|
859
|
+
## See Also
|
|
860
|
+
|
|
861
|
+
- **[Halitosis](https://github.com/bmorrall/halitosis)** — JSON serialization library for Rails APIs. Declare attributes, HAL-style links, permissions, and nested relationships on serializer classes; render resources and collections with built-in support for sorting, filtering, and pagination. Use `Halitosis::ErrorsSerializer` to serialize `ActiveModel::Errors` from a WellFormed form directly into a JSON:API-style errors response.
|
|
862
|
+
|
|
853
863
|
## Development
|
|
854
864
|
|
|
855
865
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# well_formed-dry_types
|
|
2
|
+
|
|
3
|
+
[dry-types](https://dry-rb.org/gems/dry-types) coercion integration for [WellFormed](https://github.com/bmorrall/well_formed) form objects.
|
|
4
|
+
|
|
5
|
+
Declare typed attributes with `dry_attribute` and coercion runs automatically before validation — keeping your forms free of manual casting logic.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add well_formed-dry_types
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Require the gem in your application:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require "well_formed-dry_types"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Include `WellFormed::DryTypes` in any form, then use `dry_attribute` in place of `attribute` for fields that need type coercion:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
module Types
|
|
25
|
+
include Dry.Types()
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class CreateOrderForm < WellFormed::ResourceForm
|
|
29
|
+
include WellFormed::DryTypes
|
|
30
|
+
|
|
31
|
+
resource_alias :order
|
|
32
|
+
|
|
33
|
+
dry_attribute :quantity, Types::Params::Integer
|
|
34
|
+
dry_attribute :amount, Types::Params::Decimal
|
|
35
|
+
dry_attribute :status, Types::String.enum("pending", "confirmed")
|
|
36
|
+
|
|
37
|
+
validates :quantity, presence: true, numericality: {greater_than: 0}
|
|
38
|
+
validates :amount, presence: true
|
|
39
|
+
validates :status, presence: true
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Coercion runs before validation. If coercion fails, an error is added to the attribute and validation is skipped for that field:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
form = CreateOrderForm.new(order, current_user, {quantity: "abc", amount: "9.99", status: "pending"})
|
|
47
|
+
form.valid?
|
|
48
|
+
# => false
|
|
49
|
+
form.errors[:quantity]
|
|
50
|
+
# => ["must be Params::Integer"] # dry-types error message
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Custom error messages
|
|
54
|
+
|
|
55
|
+
Pass `message:` to override the default dry-types error message.
|
|
56
|
+
|
|
57
|
+
#### I18n key
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
dry_attribute :status, Types::String.enum("pending", "confirmed"), message: :inclusion
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Passes the symbol to `errors.add`, which resolves it through your I18n translations.
|
|
64
|
+
|
|
65
|
+
#### Literal string
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
dry_attribute :amount, Types::Params::Decimal, message: "must be a number"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Default (no message option)
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
dry_attribute :quantity, Types::Params::Integer
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Falls back to the error message from dry-types itself.
|
|
78
|
+
|
|
79
|
+
## Inheritance
|
|
80
|
+
|
|
81
|
+
`_dry_attributes` merges up the superclass chain, so subclass forms inherit parent coercions and can add their own:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class BaseOrderForm < WellFormed::ResourceForm
|
|
85
|
+
include WellFormed::DryTypes
|
|
86
|
+
dry_attribute :amount, Types::Params::Decimal
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class CreateOrderForm < BaseOrderForm
|
|
90
|
+
dry_attribute :quantity, Types::Params::Integer
|
|
91
|
+
# inherits :amount coercion from BaseOrderForm
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
| Class macro | Description |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `dry_attribute(name, type, message: nil)` | Declares a coerced attribute. Coercion runs before validation via a `before_validate` callback. |
|
|
100
|
+
|
|
101
|
+
## How it works
|
|
102
|
+
|
|
103
|
+
When `WellFormed::DryTypes` is included, a `before_validate` callback (`_coerce_dry_attributes`) is registered. Before each validation run, every `dry_attribute` is coerced in declaration order. On `Dry::Types::CoercionError`, an error is added and the raw value is left in place. Subsequent validators can still run against the failing attribute (e.g. a `presence` check on a nil optional).
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WellFormed
|
|
4
|
+
module DryTypes
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
base.set_callback(:validate, :before, :_coerce_dry_attributes)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
# Declares an attribute whose value is coerced via a dry-types type before
|
|
12
|
+
# validation runs. Any dry-types type is accepted, including constrained,
|
|
13
|
+
# optional, and sum types.
|
|
14
|
+
#
|
|
15
|
+
# dry_attribute :amount, Types::Params::Decimal
|
|
16
|
+
# dry_attribute :status, Types::String.enum("draft", "published")
|
|
17
|
+
# dry_attribute :tags, Types::Strict::Array.of(Types::Strict::String)
|
|
18
|
+
# dry_attribute :score, Types::Params::Integer.optional
|
|
19
|
+
#
|
|
20
|
+
# If coercion fails, an error is added to the attribute. Pass +message:+ to
|
|
21
|
+
# use a specific error message or I18n key instead of the dry-types error:
|
|
22
|
+
#
|
|
23
|
+
# dry_attribute :status, Types::String.enum("draft", "published"), message: :inclusion
|
|
24
|
+
# dry_attribute :amount, Types::Params::Decimal, message: "must be a number"
|
|
25
|
+
#
|
|
26
|
+
# Coercion runs before validation, so +validates :amount, presence: true+ will
|
|
27
|
+
# still fire for nil values on optional attributes.
|
|
28
|
+
def dry_attribute(name, type, message: nil)
|
|
29
|
+
_dry_attribute_registry[name] = {type: type, message: message}
|
|
30
|
+
attribute name
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the merged dry-types attribute registry for this class and all ancestors.
|
|
34
|
+
def _dry_attributes
|
|
35
|
+
parent = superclass.respond_to?(:_dry_attributes) ? superclass._dry_attributes : {}
|
|
36
|
+
parent.merge(_dry_attribute_registry)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def _dry_attribute_registry
|
|
42
|
+
@_dry_attribute_registry ||= {}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def _coerce_dry_attributes
|
|
49
|
+
self.class._dry_attributes.each do |name, options|
|
|
50
|
+
coerced = options[:type].call(public_send(name))
|
|
51
|
+
public_send(:"#{name}=", coerced)
|
|
52
|
+
rescue ::Dry::Types::CoercionError => e
|
|
53
|
+
errors.add(name, options[:message] || e.message)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "well_formed-dry_types"
|
|
4
|
+
|
|
5
|
+
module Types
|
|
6
|
+
include Dry.Types()
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
RSpec.configure do |config|
|
|
10
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
|
11
|
+
|
|
12
|
+
config.disable_monkey_patching!
|
|
13
|
+
|
|
14
|
+
config.expect_with :rspec do |c|
|
|
15
|
+
c.syntax = :expect
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe WellFormed::DryTypes do
|
|
4
|
+
it "has a version number" do
|
|
5
|
+
expect(WellFormed::DryTypes::VERSION).not_to be_nil
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe "auto-include via Extensions" do
|
|
9
|
+
let(:form_class) do
|
|
10
|
+
stub_const("TestForm", Class.new(WellFormed::SimpleResource))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "is included automatically in SimpleResource subclasses" do
|
|
14
|
+
expect(form_class.ancestors).to include(WellFormed::DryTypes)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe "#dry_attribute coercion" do
|
|
19
|
+
let(:resource) { double("resource", save: true) }
|
|
20
|
+
let(:user) { double("user") }
|
|
21
|
+
|
|
22
|
+
let(:form_class) do
|
|
23
|
+
stub_const("CoercionForm", Class.new(WellFormed::ResourceForm) do
|
|
24
|
+
dry_attribute :amount, Types::Params::Decimal
|
|
25
|
+
dry_attribute :count, Types::Params::Integer
|
|
26
|
+
end)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "coerces string params to the declared type before validation" do
|
|
30
|
+
form = form_class.new(resource, user, {amount: "42.5", count: "3"})
|
|
31
|
+
form.valid?
|
|
32
|
+
|
|
33
|
+
expect(form.amount).to eq(BigDecimal("42.5"))
|
|
34
|
+
expect(form.count).to eq(3)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "stores the coerced value, not the raw string" do
|
|
38
|
+
form = form_class.new(resource, user, {amount: "10.00"})
|
|
39
|
+
form.valid?
|
|
40
|
+
|
|
41
|
+
expect(form.amount).to be_a(BigDecimal)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "is valid when all coercions succeed" do
|
|
45
|
+
form = form_class.new(resource, user, {amount: "9.99", count: "1"})
|
|
46
|
+
|
|
47
|
+
expect(form).to be_valid
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe "coercion failures" do
|
|
52
|
+
let(:resource) { double("resource") }
|
|
53
|
+
let(:user) { double("user") }
|
|
54
|
+
|
|
55
|
+
let(:form_class) do
|
|
56
|
+
stub_const("FailureForm", Class.new(WellFormed::ResourceForm) do
|
|
57
|
+
dry_attribute :amount, Types::Params::Decimal
|
|
58
|
+
dry_attribute :count, Types::Params::Integer
|
|
59
|
+
end)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "adds an error on the attribute when coercion fails" do
|
|
63
|
+
form = form_class.new(resource, user, {amount: "not-a-number"})
|
|
64
|
+
form.valid?
|
|
65
|
+
|
|
66
|
+
expect(form.errors[:amount]).to include('"not-a-number" cannot be coerced to decimal')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "collects all coercion errors in a single valid? call" do
|
|
70
|
+
form = form_class.new(resource, user, {amount: "bad", count: "also-bad"})
|
|
71
|
+
form.valid?
|
|
72
|
+
|
|
73
|
+
expect(form.errors[:amount]).to include('"bad" cannot be coerced to decimal')
|
|
74
|
+
expect(form.errors[:count]).to include('invalid value for Integer(): "also-bad"')
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "leaves the raw value on the attribute when coercion fails" do
|
|
78
|
+
form = form_class.new(resource, user, {amount: "bad"})
|
|
79
|
+
form.valid?
|
|
80
|
+
|
|
81
|
+
expect(form.amount).to eq("bad")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe "nil and optional values" do
|
|
86
|
+
let(:resource) { double("resource") }
|
|
87
|
+
let(:user) { double("user") }
|
|
88
|
+
|
|
89
|
+
it "passes nil through a strict type and raises a coercion error" do
|
|
90
|
+
form_class = stub_const("StrictNilForm", Class.new(WellFormed::ResourceForm) do
|
|
91
|
+
dry_attribute :amount, Types::Strict::Decimal
|
|
92
|
+
end)
|
|
93
|
+
|
|
94
|
+
form = form_class.new(resource, user, {amount: nil})
|
|
95
|
+
form.valid?
|
|
96
|
+
|
|
97
|
+
expect(form.errors[:amount]).not_to be_empty
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "accepts nil when the type is optional" do
|
|
101
|
+
form_class = stub_const("OptionalForm", Class.new(WellFormed::ResourceForm) do
|
|
102
|
+
dry_attribute :amount, Types::Params::Decimal.optional
|
|
103
|
+
end)
|
|
104
|
+
|
|
105
|
+
form = form_class.new(resource, user, {amount: nil})
|
|
106
|
+
form.valid?
|
|
107
|
+
|
|
108
|
+
expect(form.errors[:amount]).to be_empty
|
|
109
|
+
expect(form.amount).to be_nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
describe "message: option" do
|
|
114
|
+
let(:resource) { double("resource") }
|
|
115
|
+
let(:user) { double("user") }
|
|
116
|
+
|
|
117
|
+
it "uses an I18n symbol message on coercion failure" do
|
|
118
|
+
form_class = stub_const("SymbolMessageForm", Class.new(WellFormed::ResourceForm) do
|
|
119
|
+
dry_attribute :amount, Types::Params::Decimal, message: :invalid
|
|
120
|
+
end)
|
|
121
|
+
|
|
122
|
+
form = form_class.new(resource, user, {amount: "bad"})
|
|
123
|
+
form.valid?
|
|
124
|
+
|
|
125
|
+
expect(form.errors[:amount]).to include("is invalid")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "uses a string message on coercion failure" do
|
|
129
|
+
form_class = stub_const("StringMessageForm", Class.new(WellFormed::ResourceForm) do
|
|
130
|
+
dry_attribute :amount, Types::Params::Decimal, message: "must be a number"
|
|
131
|
+
end)
|
|
132
|
+
|
|
133
|
+
form = form_class.new(resource, user, {amount: "bad"})
|
|
134
|
+
form.valid?
|
|
135
|
+
|
|
136
|
+
expect(form.errors[:amount]).to include("must be a number")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "falls back to the dry-types error message when no message is given" do
|
|
140
|
+
form_class = stub_const("DefaultMessageForm", Class.new(WellFormed::ResourceForm) do
|
|
141
|
+
dry_attribute :amount, Types::Params::Decimal
|
|
142
|
+
end)
|
|
143
|
+
|
|
144
|
+
form = form_class.new(resource, user, {amount: "bad"})
|
|
145
|
+
form.valid?
|
|
146
|
+
|
|
147
|
+
expect(form.errors[:amount].first).not_to be_empty
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
describe "subclass inheritance" do
|
|
152
|
+
let(:resource) { double("resource", save: true) }
|
|
153
|
+
let(:user) { double("user") }
|
|
154
|
+
|
|
155
|
+
let(:parent_class) do
|
|
156
|
+
stub_const("DryParentForm", Class.new(WellFormed::ResourceForm) do
|
|
157
|
+
dry_attribute :amount, Types::Params::Decimal
|
|
158
|
+
end)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
let(:child_class) do
|
|
162
|
+
stub_const("DryChildForm", Class.new(parent_class) do
|
|
163
|
+
dry_attribute :count, Types::Params::Integer
|
|
164
|
+
end)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it "child inherits the parent's dry_attribute declarations" do
|
|
168
|
+
form = child_class.new(resource, user, {amount: "5.0", count: "3"})
|
|
169
|
+
form.valid?
|
|
170
|
+
|
|
171
|
+
expect(form.amount).to eq(BigDecimal("5.0"))
|
|
172
|
+
expect(form.count).to eq(3)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "parent is unaffected by the child's dry_attribute declarations" do
|
|
176
|
+
form = parent_class.new(resource, user, {amount: "5.0"})
|
|
177
|
+
|
|
178
|
+
expect(form).not_to respond_to(:count)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it "collects errors from both parent and child attributes" do
|
|
182
|
+
form = child_class.new(resource, user, {amount: "bad", count: "bad"})
|
|
183
|
+
form.valid?
|
|
184
|
+
|
|
185
|
+
expect(form.errors[:amount]).not_to be_empty
|
|
186
|
+
expect(form.errors[:count]).not_to be_empty
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
describe "constrained types" do
|
|
191
|
+
let(:resource) { double("resource") }
|
|
192
|
+
let(:user) { double("user") }
|
|
193
|
+
|
|
194
|
+
let(:form_class) do
|
|
195
|
+
stub_const("ConstrainedForm", Class.new(WellFormed::ResourceForm) do
|
|
196
|
+
dry_attribute :score, Types::Params::Integer.constrained(gteq: 0, lteq: 100)
|
|
197
|
+
end)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "accepts a value within the constraint" do
|
|
201
|
+
form = form_class.new(resource, user, {score: "50"})
|
|
202
|
+
form.valid?
|
|
203
|
+
|
|
204
|
+
expect(form.errors[:score]).to be_empty
|
|
205
|
+
expect(form.score).to eq(50)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it "adds an error when the constraint is violated" do
|
|
209
|
+
form = form_class.new(resource, user, {score: "150"})
|
|
210
|
+
form.valid?
|
|
211
|
+
|
|
212
|
+
expect(form.errors[:score]).not_to be_empty
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/well_formed/dry_types/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "well_formed-dry_types"
|
|
7
|
+
spec.version = WellFormed::DryTypes::VERSION
|
|
8
|
+
spec.authors = ["Ben Morrall"]
|
|
9
|
+
spec.email = ["bemo56@hotmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "dry-types attribute coercion integration for well_formed"
|
|
12
|
+
spec.homepage = "https://github.com/bmorrall/well_formed"
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
15
|
+
|
|
16
|
+
gemspec = File.basename(__FILE__)
|
|
17
|
+
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
|
|
18
|
+
ls.readlines("\x0", chomp: true).reject do |f|
|
|
19
|
+
(f == gemspec) ||
|
|
20
|
+
f.start_with?(*%w[spec/ .git Gemfile])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
spec.require_paths = ["lib"]
|
|
25
|
+
|
|
26
|
+
spec.add_dependency "well_formed", ">= 0.1.0"
|
|
27
|
+
spec.add_dependency "dry-types", ">= 1.0"
|
|
28
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# well_formed-paper_trail
|
|
2
|
+
|
|
3
|
+
[PaperTrail](https://github.com/paper-trail-gem/paper_trail) versioning integration for [WellFormed](https://github.com/bmorrall/well_formed) form objects.
|
|
4
|
+
|
|
5
|
+
Automatically sets `PaperTrail.request.whodunnit` to the form's `user` around every `save` and `perform`, so version records are attributed to the correct user without any controller wiring.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add well_formed-paper_trail
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Require the gem in your application:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
require "well_formed-paper_trail"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`WellFormed::PaperTrail` is automatically included into all WellFormed forms — no `include` required. With PaperTrail configured on your models, versions are immediately attributed to the form's `user`:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
class UpdateArticleForm < WellFormed::ResourceForm
|
|
25
|
+
resource_alias :article
|
|
26
|
+
|
|
27
|
+
attribute :title, :string
|
|
28
|
+
attribute :body, :string
|
|
29
|
+
|
|
30
|
+
validates :title, presence: true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
form = UpdateArticleForm.new(article, current_user, article_params)
|
|
34
|
+
form.save
|
|
35
|
+
# => PaperTrail::Version created with whodunnit: current_user.id.to_s
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Works for `ActionForm` too — any model changes made inside `perform` are attributed correctly:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
class PublishArticleForm < WellFormed::ActionForm
|
|
42
|
+
resource_alias :article
|
|
43
|
+
|
|
44
|
+
def perform
|
|
45
|
+
article.publish! # triggers a PaperTrail version attributed to user
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Custom whodunnit
|
|
51
|
+
|
|
52
|
+
By default, `whodunnit` is set to `user&.id&.to_s`. There are two ways to override this.
|
|
53
|
+
|
|
54
|
+
#### Global default
|
|
55
|
+
|
|
56
|
+
Set a global proc in an initializer. It receives the form's `user` as its argument:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# config/initializers/well_formed_paper_trail.rb
|
|
60
|
+
WellFormed::PaperTrail.whodunnit = ->(user) { user&.email }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Per-form override
|
|
64
|
+
|
|
65
|
+
Use `paper_trail_whodunnit` to override on a specific form class. The block is evaluated in the context of the form instance, so all form attributes and helpers (including `user`) are available:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
class UpdateArticleForm < WellFormed::ResourceForm
|
|
69
|
+
paper_trail_whodunnit { user.email }
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
class AdminUpdateForm < WellFormed::ResourceForm
|
|
75
|
+
paper_trail_whodunnit { "admin:#{user.id}" }
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The per-form macro takes precedence over the global config. It is also inherited by subclasses and can be overridden per subclass:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
class BaseForm < WellFormed::ResourceForm
|
|
83
|
+
paper_trail_whodunnit { user.email }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class AuditedForm < BaseForm
|
|
87
|
+
paper_trail_whodunnit { "#{user.role}:#{user.id}" } # overrides parent
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The priority order is: **per-form macro → global config → `user&.id&.to_s`**.
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
| Method | Description |
|
|
96
|
+
|--------|-------------|
|
|
97
|
+
| `WellFormed::PaperTrail.whodunnit = ->(user) { ... }` | Global default — proc receives `user`, return value used as `whodunnit` |
|
|
98
|
+
| `paper_trail_whodunnit { ... }` | Per-form macro — block evaluated on the form instance, takes precedence over global config. Inherits from superclass. |
|
|
99
|
+
|
|
100
|
+
## How it works
|
|
101
|
+
|
|
102
|
+
`whodunnit` is set via `PaperTrail.request(whodunnit:) { ... }` inside an `around_save` callback (for `ResourceForm` and `Struct`) or an `around_perform` callback (for `ActionForm`). This is the thread-safe, block-scoped API recommended by PaperTrail — the previous `whodunnit` value is always restored after the block exits, even if an exception is raised.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WellFormed
|
|
4
|
+
module PaperTrail
|
|
5
|
+
@whodunnit = nil
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
# Global default whodunnit proc, used when no per-form +paper_trail_whodunnit+ is set.
|
|
9
|
+
# The proc receives the form's +user+ as its argument.
|
|
10
|
+
#
|
|
11
|
+
# WellFormed::PaperTrail.whodunnit = ->(user) { user&.email }
|
|
12
|
+
#
|
|
13
|
+
attr_accessor :whodunnit
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.included(base)
|
|
17
|
+
base.extend(ClassMethods)
|
|
18
|
+
base.set_callback(:save, :around, :_with_paper_trail) if base.respond_to?(:_save_callbacks)
|
|
19
|
+
base.set_callback(:perform, :around, :_with_paper_trail) if base.respond_to?(:_perform_callbacks)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
module ClassMethods
|
|
23
|
+
# Override the whodunnit value set on PaperTrail.request during save/perform.
|
|
24
|
+
# The block is evaluated in the context of the form instance, so form attributes
|
|
25
|
+
# and helpers (including +user+) are available.
|
|
26
|
+
#
|
|
27
|
+
# paper_trail_whodunnit { user.email }
|
|
28
|
+
# paper_trail_whodunnit { "admin:#{user.id}" }
|
|
29
|
+
#
|
|
30
|
+
# Defaults to +user&.id&.to_s+ when not set.
|
|
31
|
+
def paper_trail_whodunnit(&block)
|
|
32
|
+
@_paper_trail_whodunnit = block
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def _paper_trail_whodunnit_proc
|
|
36
|
+
return @_paper_trail_whodunnit if defined?(@_paper_trail_whodunnit)
|
|
37
|
+
|
|
38
|
+
superclass._paper_trail_whodunnit_proc if superclass.respond_to?(:_paper_trail_whodunnit_proc)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def _with_paper_trail
|
|
45
|
+
whodunnit = if (proc = self.class._paper_trail_whodunnit_proc)
|
|
46
|
+
instance_exec(&proc)
|
|
47
|
+
elsif (global = WellFormed::PaperTrail.whodunnit)
|
|
48
|
+
global.call(user)
|
|
49
|
+
else
|
|
50
|
+
user&.id&.to_s
|
|
51
|
+
end
|
|
52
|
+
::PaperTrail.request(whodunnit: whodunnit) { yield }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "well_formed-paper_trail"
|
|
4
|
+
|
|
5
|
+
RSpec.configure do |config|
|
|
6
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
|
7
|
+
|
|
8
|
+
config.disable_monkey_patching!
|
|
9
|
+
|
|
10
|
+
config.expect_with :rspec do |c|
|
|
11
|
+
c.syntax = :expect
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe WellFormed::PaperTrail do
|
|
4
|
+
it "has a version number" do
|
|
5
|
+
expect(WellFormed::PaperTrail::VERSION).not_to be_nil
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe "auto-include via WithUser" do
|
|
9
|
+
let(:form_class) do
|
|
10
|
+
stub_const("TestForm", Class.new(WellFormed::ResourceForm) do
|
|
11
|
+
attribute :title, :string
|
|
12
|
+
end)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "is included automatically when WithUser is prepended" do
|
|
16
|
+
expect(form_class.ancestors).to include(WellFormed::PaperTrail)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "ResourceForm — around save" do
|
|
21
|
+
let(:resource) { double("resource", save: true) }
|
|
22
|
+
let(:user) { double("user", id: 42) }
|
|
23
|
+
|
|
24
|
+
let(:form_class) do
|
|
25
|
+
stub_const("TestResourceForm", Class.new(WellFormed::ResourceForm) do
|
|
26
|
+
attribute :title, :string
|
|
27
|
+
end)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
subject(:form) { form_class.new(resource, user) }
|
|
31
|
+
|
|
32
|
+
it "calls PaperTrail.request with whodunnit set to user.id.to_s" do
|
|
33
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "42") do |**, &block|
|
|
34
|
+
block.call
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
form.save
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "wraps around resource.save so whodunnit is active during the save" do
|
|
41
|
+
order = []
|
|
42
|
+
|
|
43
|
+
allow(::PaperTrail).to receive(:request) do |whodunnit:, &block|
|
|
44
|
+
order << :paper_trail_start
|
|
45
|
+
block.call
|
|
46
|
+
order << :paper_trail_end
|
|
47
|
+
end
|
|
48
|
+
allow(resource).to receive(:save) do
|
|
49
|
+
order << :resource_save
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
form.save
|
|
54
|
+
|
|
55
|
+
expect(order).to eq([:paper_trail_start, :resource_save, :paper_trail_end])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context "when resource.save fails" do
|
|
59
|
+
before { allow(resource).to receive(:save).and_return(false) }
|
|
60
|
+
|
|
61
|
+
it "still calls PaperTrail.request" do
|
|
62
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "42") do |**, &block|
|
|
63
|
+
block.call
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
form.save
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe "ActionForm — around perform" do
|
|
72
|
+
let(:user) { double("user", id: 7) }
|
|
73
|
+
|
|
74
|
+
let(:form_class) do
|
|
75
|
+
stub_const("TestActionForm", Class.new(WellFormed::ActionForm) do
|
|
76
|
+
def perform
|
|
77
|
+
# represents a transition, event dispatch, etc.
|
|
78
|
+
end
|
|
79
|
+
end)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
subject(:form) { form_class.new(double("resource"), user) }
|
|
83
|
+
|
|
84
|
+
it "calls PaperTrail.request with whodunnit set to user.id.to_s" do
|
|
85
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "7") do |**, &block|
|
|
86
|
+
block.call
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
form.submit
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "wraps around perform so whodunnit is active during the action" do
|
|
93
|
+
order = []
|
|
94
|
+
|
|
95
|
+
allow(::PaperTrail).to receive(:request) do |whodunnit:, &block|
|
|
96
|
+
order << :paper_trail_start
|
|
97
|
+
block.call
|
|
98
|
+
order << :paper_trail_end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
form_class.before_perform { order << :before_perform }
|
|
102
|
+
form_class.after_perform { order << :after_perform }
|
|
103
|
+
|
|
104
|
+
form.submit
|
|
105
|
+
|
|
106
|
+
expect(order).to eq([:paper_trail_start, :before_perform, :after_perform, :paper_trail_end])
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "global whodunnit config" do
|
|
111
|
+
let(:resource) { double("resource", save: true) }
|
|
112
|
+
let(:user) { double("user", id: 42, email: "alice@example.com") }
|
|
113
|
+
|
|
114
|
+
let(:form_class) do
|
|
115
|
+
stub_const("GlobalConfigForm", Class.new(WellFormed::ResourceForm) do
|
|
116
|
+
attribute :title, :string
|
|
117
|
+
end)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
subject(:form) { form_class.new(resource, user) }
|
|
121
|
+
|
|
122
|
+
around do |example|
|
|
123
|
+
original = WellFormed::PaperTrail.whodunnit
|
|
124
|
+
example.run
|
|
125
|
+
ensure
|
|
126
|
+
WellFormed::PaperTrail.whodunnit = original
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "uses the global proc when no per-form macro is set" do
|
|
130
|
+
WellFormed::PaperTrail.whodunnit = ->(u) { u.email }
|
|
131
|
+
|
|
132
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "alice@example.com") do |**, &block|
|
|
133
|
+
block.call
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
form.save
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "per-form macro takes precedence over the global config" do
|
|
140
|
+
WellFormed::PaperTrail.whodunnit = ->(u) { u.email }
|
|
141
|
+
|
|
142
|
+
form_class.paper_trail_whodunnit { "override" }
|
|
143
|
+
|
|
144
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "override") do |**, &block|
|
|
145
|
+
block.call
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
form.save
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "falls back to user.id.to_s when global config is nil" do
|
|
152
|
+
WellFormed::PaperTrail.whodunnit = nil
|
|
153
|
+
|
|
154
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "42") do |**, &block|
|
|
155
|
+
block.call
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
form.save
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe "#paper_trail_whodunnit macro" do
|
|
163
|
+
let(:resource) { double("resource", save: true) }
|
|
164
|
+
let(:user) { double("user", id: 42, email: "alice@example.com") }
|
|
165
|
+
|
|
166
|
+
context "with a custom whodunnit block" do
|
|
167
|
+
let(:form_class) do
|
|
168
|
+
stub_const("CustomWhodunnitForm", Class.new(WellFormed::ResourceForm) do
|
|
169
|
+
attribute :title, :string
|
|
170
|
+
paper_trail_whodunnit { user.email }
|
|
171
|
+
end)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
subject(:form) { form_class.new(resource, user) }
|
|
175
|
+
|
|
176
|
+
it "uses the block's return value as whodunnit" do
|
|
177
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "alice@example.com") do |**, &block|
|
|
178
|
+
block.call
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
form.save
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
context "when nil user" do
|
|
186
|
+
let(:form_class) do
|
|
187
|
+
stub_const("NilUserForm", Class.new(WellFormed::ResourceForm) do
|
|
188
|
+
attribute :title, :string
|
|
189
|
+
end)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
subject(:form) { form_class.new(resource, nil) }
|
|
193
|
+
|
|
194
|
+
it "passes nil as whodunnit when user is nil" do
|
|
195
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: nil) do |**, &block|
|
|
196
|
+
block.call
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
form.save
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
context "with a subclass overriding the whodunnit" do
|
|
204
|
+
let(:parent_class) do
|
|
205
|
+
stub_const("ParentForm", Class.new(WellFormed::ResourceForm) do
|
|
206
|
+
attribute :title, :string
|
|
207
|
+
paper_trail_whodunnit { user.email }
|
|
208
|
+
end)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
let(:child_class) do
|
|
212
|
+
stub_const("ChildForm", Class.new(parent_class) do
|
|
213
|
+
paper_trail_whodunnit { "admin:#{user.id}" }
|
|
214
|
+
end)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "child uses its own whodunnit" do
|
|
218
|
+
form = child_class.new(resource, user)
|
|
219
|
+
|
|
220
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "admin:42") do |**, &block|
|
|
221
|
+
block.call
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
form.save
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "parent still uses its own whodunnit" do
|
|
228
|
+
form = parent_class.new(resource, user)
|
|
229
|
+
|
|
230
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "alice@example.com") do |**, &block|
|
|
231
|
+
block.call
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
form.save
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
context "with a subclass inheriting the parent whodunnit" do
|
|
239
|
+
let(:parent_class) do
|
|
240
|
+
stub_const("InheritParentForm", Class.new(WellFormed::ResourceForm) do
|
|
241
|
+
attribute :title, :string
|
|
242
|
+
paper_trail_whodunnit { user.email }
|
|
243
|
+
end)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
let(:child_class) do
|
|
247
|
+
stub_const("InheritChildForm", Class.new(parent_class))
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
it "inherits the parent's whodunnit proc" do
|
|
251
|
+
form = child_class.new(resource, user)
|
|
252
|
+
|
|
253
|
+
expect(::PaperTrail).to receive(:request).with(whodunnit: "alice@example.com") do |**, &block|
|
|
254
|
+
block.call
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
form.save
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/well_formed/paper_trail/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "well_formed-paper_trail"
|
|
7
|
+
spec.version = WellFormed::PaperTrail::VERSION
|
|
8
|
+
spec.authors = ["Ben Morrall"]
|
|
9
|
+
spec.email = ["bemo56@hotmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "PaperTrail versioning integration for well_formed"
|
|
12
|
+
spec.homepage = "https://github.com/bmorrall/well_formed"
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.required_ruby_version = ">= 3.1.0"
|
|
15
|
+
|
|
16
|
+
gemspec = File.basename(__FILE__)
|
|
17
|
+
spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
|
|
18
|
+
ls.readlines("\x0", chomp: true).reject do |f|
|
|
19
|
+
(f == gemspec) ||
|
|
20
|
+
f.start_with?(*%w[spec/ .git Gemfile])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
spec.require_paths = ["lib"]
|
|
25
|
+
|
|
26
|
+
spec.add_dependency "well_formed", ">= 0.1.0"
|
|
27
|
+
spec.add_dependency "paper_trail", ">= 12.0"
|
|
28
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WellFormed
|
|
4
|
+
module Extensions
|
|
5
|
+
@extensions = []
|
|
6
|
+
@bases = []
|
|
7
|
+
|
|
8
|
+
def self.register_extension(mod)
|
|
9
|
+
@extensions << mod
|
|
10
|
+
@bases.each { |base| base.include(mod) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.included(base)
|
|
14
|
+
@bases << base
|
|
15
|
+
@extensions.each { |mod| base.include(mod) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "active_record"
|
|
4
|
-
|
|
5
3
|
module WellFormed
|
|
6
4
|
module Transactional
|
|
7
5
|
def self.included(base)
|
|
@@ -11,10 +9,21 @@ module WellFormed
|
|
|
11
9
|
|
|
12
10
|
module ClassMethods
|
|
13
11
|
def after_save_commit(*args, &block)
|
|
14
|
-
|
|
12
|
+
require "active_record"
|
|
13
|
+
if block
|
|
14
|
+
set_callback(:save_commit, :after) do
|
|
15
|
+
::ActiveRecord.after_all_transactions_commit { instance_exec(&block) }
|
|
16
|
+
end
|
|
17
|
+
else
|
|
18
|
+
method_name = args.shift
|
|
19
|
+
set_callback(:save_commit, :after, *args) do
|
|
20
|
+
::ActiveRecord.after_all_transactions_commit { send(method_name) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
15
23
|
end
|
|
16
24
|
|
|
17
25
|
def save_within_transaction
|
|
26
|
+
require "active_record"
|
|
18
27
|
set_callback(:save, :around) do |form, block|
|
|
19
28
|
saved = false
|
|
20
29
|
form.resource.class.transaction do
|
|
@@ -31,7 +40,7 @@ module WellFormed
|
|
|
31
40
|
|
|
32
41
|
def save
|
|
33
42
|
result = super
|
|
34
|
-
|
|
43
|
+
run_callbacks(:save_commit) if result
|
|
35
44
|
result || false
|
|
36
45
|
end
|
|
37
46
|
end
|
data/lib/well_formed/version.rb
CHANGED
data/lib/well_formed.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "well_formed/errors"
|
|
|
6
6
|
require_relative "well_formed/railtie" if defined?(Rails::Railtie)
|
|
7
7
|
|
|
8
8
|
module WellFormed
|
|
9
|
+
autoload :Extensions, "well_formed/extensions"
|
|
9
10
|
autoload :Initializer, "well_formed/initializer"
|
|
10
11
|
autoload :WithUser, "well_formed/with_user"
|
|
11
12
|
autoload :AttributeAssignment, "well_formed/attribute_assignment"
|
|
@@ -34,5 +35,6 @@ module WellFormed
|
|
|
34
35
|
base.include Collections
|
|
35
36
|
base.include NestedAttributes
|
|
36
37
|
base.prepend Initializer
|
|
38
|
+
base.include Extensions
|
|
37
39
|
end
|
|
38
40
|
end
|
data/sig/well_formed.rbs
CHANGED
|
@@ -38,12 +38,9 @@ module WellFormed
|
|
|
38
38
|
|
|
39
39
|
attr_reader resource: ::WellFormed::_ActiveModelLike
|
|
40
40
|
|
|
41
|
-
def id: () -> untyped
|
|
42
|
-
def persisted?: () -> bool
|
|
43
41
|
def new_record?: () -> bool
|
|
44
42
|
def to_param: () -> ::String?
|
|
45
43
|
|
|
46
|
-
def save: () -> bool
|
|
47
44
|
def save!: () -> bool
|
|
48
45
|
def submit: () -> (::WellFormed::_ActiveModelLike | false)
|
|
49
46
|
def submit!: () -> ::WellFormed::_ActiveModelLike
|
|
@@ -106,12 +103,9 @@ module WellFormed
|
|
|
106
103
|
|
|
107
104
|
attr_reader resource: ::WellFormed::_ActiveModelLike
|
|
108
105
|
|
|
109
|
-
def id: () -> untyped
|
|
110
|
-
def persisted?: () -> bool
|
|
111
106
|
def new_record?: () -> bool
|
|
112
107
|
def to_param: () -> ::String?
|
|
113
108
|
|
|
114
|
-
def save: () -> bool
|
|
115
109
|
def save!: () -> bool
|
|
116
110
|
def submit: () -> (::WellFormed::_ActiveModelLike | false)
|
|
117
111
|
def submit!: () -> ::WellFormed::_ActiveModelLike
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: well_formed
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ben Morrall
|
|
@@ -41,6 +41,26 @@ files:
|
|
|
41
41
|
- README.md
|
|
42
42
|
- Rakefile
|
|
43
43
|
- Steepfile
|
|
44
|
+
- gems/well_formed-dry_types/.rspec
|
|
45
|
+
- gems/well_formed-dry_types/Gemfile
|
|
46
|
+
- gems/well_formed-dry_types/README.md
|
|
47
|
+
- gems/well_formed-dry_types/Rakefile
|
|
48
|
+
- gems/well_formed-dry_types/lib/well_formed-dry_types.rb
|
|
49
|
+
- gems/well_formed-dry_types/lib/well_formed/dry_types.rb
|
|
50
|
+
- gems/well_formed-dry_types/lib/well_formed/dry_types/version.rb
|
|
51
|
+
- gems/well_formed-dry_types/spec/spec_helper.rb
|
|
52
|
+
- gems/well_formed-dry_types/spec/well_formed/dry_types_spec.rb
|
|
53
|
+
- gems/well_formed-dry_types/well_formed-dry_types.gemspec
|
|
54
|
+
- gems/well_formed-paper_trail/.rspec
|
|
55
|
+
- gems/well_formed-paper_trail/Gemfile
|
|
56
|
+
- gems/well_formed-paper_trail/README.md
|
|
57
|
+
- gems/well_formed-paper_trail/Rakefile
|
|
58
|
+
- gems/well_formed-paper_trail/lib/well_formed-paper_trail.rb
|
|
59
|
+
- gems/well_formed-paper_trail/lib/well_formed/paper_trail.rb
|
|
60
|
+
- gems/well_formed-paper_trail/lib/well_formed/paper_trail/version.rb
|
|
61
|
+
- gems/well_formed-paper_trail/spec/spec_helper.rb
|
|
62
|
+
- gems/well_formed-paper_trail/spec/well_formed/paper_trail_spec.rb
|
|
63
|
+
- gems/well_formed-paper_trail/well_formed-paper_trail.gemspec
|
|
44
64
|
- gems/well_formed-pundit/.rspec
|
|
45
65
|
- gems/well_formed-pundit/Gemfile
|
|
46
66
|
- gems/well_formed-pundit/README.md
|
|
@@ -59,6 +79,7 @@ files:
|
|
|
59
79
|
- lib/well_formed/attribute_assignment.rb
|
|
60
80
|
- lib/well_formed/collections.rb
|
|
61
81
|
- lib/well_formed/errors.rb
|
|
82
|
+
- lib/well_formed/extensions.rb
|
|
62
83
|
- lib/well_formed/initializer.rb
|
|
63
84
|
- lib/well_formed/nested_attributes.rb
|
|
64
85
|
- lib/well_formed/nested_form.rb
|