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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -0
  3. data/README.md +14 -4
  4. data/gems/well_formed-dry_types/.rspec +3 -0
  5. data/gems/well_formed-dry_types/Gemfile +12 -0
  6. data/gems/well_formed-dry_types/README.md +107 -0
  7. data/gems/well_formed-dry_types/Rakefile +10 -0
  8. data/gems/well_formed-dry_types/lib/well_formed/dry_types/version.rb +7 -0
  9. data/gems/well_formed-dry_types/lib/well_formed/dry_types.rb +57 -0
  10. data/gems/well_formed-dry_types/lib/well_formed-dry_types.rb +8 -0
  11. data/gems/well_formed-dry_types/spec/spec_helper.rb +17 -0
  12. data/gems/well_formed-dry_types/spec/well_formed/dry_types_spec.rb +215 -0
  13. data/gems/well_formed-dry_types/well_formed-dry_types.gemspec +28 -0
  14. data/gems/well_formed-paper_trail/.rspec +3 -0
  15. data/gems/well_formed-paper_trail/Gemfile +12 -0
  16. data/gems/well_formed-paper_trail/README.md +106 -0
  17. data/gems/well_formed-paper_trail/Rakefile +10 -0
  18. data/gems/well_formed-paper_trail/lib/well_formed/paper_trail/version.rb +7 -0
  19. data/gems/well_formed-paper_trail/lib/well_formed/paper_trail.rb +55 -0
  20. data/gems/well_formed-paper_trail/lib/well_formed-paper_trail.rb +8 -0
  21. data/gems/well_formed-paper_trail/spec/spec_helper.rb +13 -0
  22. data/gems/well_formed-paper_trail/spec/well_formed/paper_trail_spec.rb +261 -0
  23. data/gems/well_formed-paper_trail/well_formed-paper_trail.gemspec +28 -0
  24. data/gems/well_formed-pundit/Gemfile +0 -1
  25. data/lib/well_formed/extensions.rb +18 -0
  26. data/lib/well_formed/simple_action.rb +1 -0
  27. data/lib/well_formed/simple_resource.rb +1 -0
  28. data/lib/well_formed/simple_struct.rb +1 -0
  29. data/lib/well_formed/transactional.rb +13 -4
  30. data/lib/well_formed/version.rb +1 -1
  31. data/lib/well_formed.rb +2 -0
  32. data/sig/well_formed.rbs +0 -6
  33. metadata +22 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a0261a88ec791fe146575065a052505be38ddf4b899eaec1ea6f69648ad9445
4
- data.tar.gz: d5c21350691a1ad78f34b94aa4ff59d11c496d4a9d774c9b5ffda24c3c08ce84
3
+ metadata.gz: 76f1e5f45582808eddd2fd54df5c806ff9fdf7c98b2d48a733e0918c99404056
4
+ data.tar.gz: 868cb13f7192a529027b9809638f1b1b3e61bd9e53e647589be5917c2714ef25
5
5
  SHA512:
6
- metadata.gz: b791b764fe96a550f25e6308a0e238016024e6edf2fe20c6085b5817b3212add43b45ec40296b65df5a2f3bbc14af523633f4537f7c4eaff7c61b85399c58f00
7
- data.tar.gz: 45b73ded6c9643af05effd8e21926f64278185a128df497140d8aeb0384980ff24cdc9a9309f9e2066055d5f964be27005a80f990baa219db2adda9b2a1964da
6
+ metadata.gz: 05ea83dd04cce981118f8b3154d55a81b26919054cbee2877918630937c4f7540aa672a41a0ce9eabd8e8062d1387f6c8bab23421020d6007fc1923a38aa3cc4
7
+ data.tar.gz: 201011a76f89b5a84f689f91eddff8504ebb6e40b45cd5fd1d9bb28eb64718bd394e19c3d6f070607e6932d8055586e5cf4fe5e37b22e6074eb6ee575bb9da8c
data/.rubocop.yml CHANGED
@@ -10,3 +10,9 @@ inherit_gem:
10
10
  AllCops:
11
11
  NewCops: enable
12
12
  SuggestExtensions: false
13
+
14
+ Rails:
15
+ TargetRailsVersion: 7.2
16
+
17
+ Rails/StrongParametersExpect:
18
+ Enabled: false
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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 UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
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,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "well_formed", path: "../../"
8
+
9
+ gem "rake", "~> 13.0"
10
+ gem "rspec", "~> 3.0"
11
+ gem "standard", "~> 1.3"
12
+ gem "rubocop-rails", require: false
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new(:rubocop)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WellFormed
4
+ module DryTypes
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "well_formed"
4
+ require "dry-types"
5
+ require_relative "well_formed/dry_types/version"
6
+ require_relative "well_formed/dry_types"
7
+
8
+ WellFormed::Extensions.register_extension(WellFormed::DryTypes)
@@ -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,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ gem "well_formed", path: "../../"
8
+
9
+ gem "rake", "~> 13.0"
10
+ gem "rspec", "~> 3.0"
11
+ gem "standard", "~> 1.3"
12
+ gem "rubocop-rails", require: false
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new(:rubocop)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WellFormed
4
+ module PaperTrail
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "well_formed"
4
+ require "paper_trail"
5
+ require_relative "well_formed/paper_trail/version"
6
+ require_relative "well_formed/paper_trail"
7
+
8
+ WellFormed::WithUser.register_extension(WellFormed::PaperTrail)
@@ -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
@@ -8,6 +8,5 @@ gem "well_formed", path: "../../"
8
8
 
9
9
  gem "rake", "~> 13.0"
10
10
  gem "rspec", "~> 3.0"
11
- gem "activerecord", require: false
12
11
  gem "standard", "~> 1.3"
13
12
  gem "rubocop-rails", require: false
@@ -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
@@ -8,6 +8,7 @@ module WellFormed
8
8
  include AttributeAssignment
9
9
  include Performer
10
10
  include NestedAttributes
11
+ include Extensions
11
12
  prepend Initializer
12
13
  include RecordIdentity
13
14
  end
@@ -9,6 +9,7 @@ module WellFormed
9
9
  include Transactional
10
10
  include Collections
11
11
  include NestedAttributes
12
+ include Extensions
12
13
  prepend Initializer
13
14
  end
14
15
  end
@@ -17,6 +17,7 @@ module WellFormed
17
17
  include Translations
18
18
  include Persistence
19
19
  include NestedAttributes
20
+ include Extensions
20
21
  prepend Initializer
21
22
  prepend PoroInterface
22
23
 
@@ -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
- set_callback(:save_commit, :after, *args, &block)
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
- ::ActiveRecord.after_all_transactions_commit { run_callbacks(:save_commit) } if result
43
+ run_callbacks(:save_commit) if result
35
44
  result || false
36
45
  end
37
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WellFormed
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
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.0
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