composable-form 0.0.13 → 0.0.14
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/CHANGELOG.md +4 -0
- data/README.md +144 -21
- data/lib/composable/form/command.rb +4 -6
- data/lib/composable/form/gem_version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f24c4de19b32a5c86c1e1d27fcadda201048976bf7301adfb42277e6b02e5e52
|
|
4
|
+
data.tar.gz: 1c4390383698b86e1e233d5bd11b829e6651a1982de360abf6c00952298be6d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05f8b36572ae20f88eca269941ccc8cb9e82efba73e8927d3e310e69c494def537366810c7dee9429ec696915b29b09b330a9be8a1599a4fb3192c7620b1114e
|
|
7
|
+
data.tar.gz: 03c6522b0db1b8bd1836e5e9d0a247bab3198a885ff78471b7e7f608755562992ccd4531c6a9b47bf6ce3747d0a30fcf8937df1e282b20c0925cbe96d9fd3e82
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Prepend the command wrapper once to preserve results in nested subclasses.
|
|
4
|
+
- Mark aborted callbacks as failures and document persistence/model naming.
|
|
5
|
+
- Add behavioral tests with complete line/branch coverage.
|
|
6
|
+
|
|
3
7
|
## [0.0.1] - 2022-01-24
|
|
4
8
|
|
|
5
9
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,43 +1,166 @@
|
|
|
1
|
-
#
|
|
1
|
+
# composable-form
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rails form objects combining ActiveModel validation, composed record persistence,
|
|
4
|
+
callbacks and model naming. Requires Ruby 3.2+ and ActiveModel/ActiveSupport 7.2+.
|
|
5
|
+
Install `gem "composable-form"`; require `composable/form`.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Map a form to a record
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
```ruby
|
|
10
|
+
require "composable/form"
|
|
11
|
+
|
|
12
|
+
class ProjectForm < Composable::Form::Command
|
|
13
|
+
attribute :name, type: :string
|
|
14
|
+
validates :name, presence: true
|
|
15
|
+
|
|
16
|
+
composable :project, model: Project do
|
|
17
|
+
sync :name
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
project = Project.new(name: "Initial")
|
|
22
|
+
form = ProjectForm.new(project: project, name: "Updated")
|
|
23
|
+
form.call
|
|
24
|
+
form.success? # => true
|
|
25
|
+
form.project.name # => "Updated"
|
|
26
|
+
```
|
|
8
27
|
|
|
9
|
-
|
|
28
|
+
The application provides the `Project` ActiveRecord model and its table.
|
|
29
|
+
`model: Project` supplies model naming and delegates `id`, `persisted?` and
|
|
30
|
+
`new_record?` to `project`. Forms without a model mapping report `persisted?` as
|
|
31
|
+
false and `new_record?` as true. This integration does not delegate `public_id` or
|
|
32
|
+
`to_param`, or authorize the underlying record.
|
|
33
|
+
|
|
34
|
+
In a controller, load an authorized record first, then pass explicit strong
|
|
35
|
+
parameters:
|
|
10
36
|
|
|
11
37
|
```ruby
|
|
12
|
-
|
|
38
|
+
form = ProjectForm.call(
|
|
39
|
+
params.require(:project).permit(:name).to_h.merge(project: authorized_project)
|
|
40
|
+
)
|
|
13
41
|
```
|
|
14
42
|
|
|
15
|
-
|
|
43
|
+
Use `form_with model: form` in the Rails view. On failure, render the form with
|
|
44
|
+
status `:unprocessable_entity` and display its errors.
|
|
45
|
+
|
|
46
|
+
## Persistence and callbacks
|
|
47
|
+
|
|
48
|
+
The order is: validate form, enter `save` callbacks, synchronize and save eligible
|
|
49
|
+
records, execute the form's `save`, finish callbacks. Override private `save` to
|
|
50
|
+
return a result or perform additional work. Without an override its result is
|
|
51
|
+
nil. `before_save`, `around_save` and `after_save` use the ActiveModel callback API.
|
|
52
|
+
An aborted chain or a `false` result records failure.
|
|
53
|
+
|
|
54
|
+
`.call` returns the form; `.call!` raises on errors. Record validation errors are
|
|
55
|
+
copied without modifying the record's errors. Unhandled exceptions propagate.
|
|
56
|
+
`rescue_from` may translate expected exceptions into form errors.
|
|
57
|
+
|
|
58
|
+
Shared-connection record writes roll back when an exception escapes the
|
|
59
|
+
transaction. Adding errors alone does not roll back writes; callbacks finishing
|
|
60
|
+
after the transaction are not part of that transaction. Separate database
|
|
61
|
+
connections do not guarantee distributed atomicity.
|
|
16
62
|
|
|
17
|
-
|
|
63
|
+
See [core](../composable-core/README.md) for conditional composition, input
|
|
64
|
+
precedence and typing, and [testing](../docs/testing.md) for coverage requirements.
|
|
18
65
|
|
|
19
|
-
|
|
66
|
+
## Connect a controller and view
|
|
20
67
|
|
|
21
|
-
|
|
68
|
+
Assume the application provides `current_account.projects` as an authorized
|
|
69
|
+
association. Load the record through that association before building the form:
|
|
22
70
|
|
|
23
|
-
|
|
71
|
+
```ruby
|
|
72
|
+
class ProjectsController < ApplicationController
|
|
73
|
+
def edit
|
|
74
|
+
project = current_account.projects.find(params[:id])
|
|
75
|
+
@form = ProjectForm.new(project: project)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def update
|
|
79
|
+
project = current_account.projects.find(params[:id])
|
|
80
|
+
@form = ProjectForm.call(
|
|
81
|
+
project_params.to_h.merge(project: project)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if @form.success?
|
|
85
|
+
redirect_to project_path(project), notice: "Project updated"
|
|
86
|
+
else
|
|
87
|
+
render :edit, status: :unprocessable_entity
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def project_params
|
|
94
|
+
params.require(:project).permit(:name)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
```
|
|
24
98
|
|
|
25
|
-
|
|
99
|
+
Because `model: Project` gives the form the model's naming, the generated fields
|
|
100
|
+
use the `project` parameter key. Specify the route explicitly:
|
|
26
101
|
|
|
27
|
-
|
|
102
|
+
```erb
|
|
103
|
+
<%= form_with model: @form, url: project_path(@form.project), method: :patch do |f| %>
|
|
104
|
+
<% @form.errors.full_messages.each do |message| %>
|
|
105
|
+
<p><%= message %></p>
|
|
106
|
+
<% end %>
|
|
28
107
|
|
|
29
|
-
|
|
108
|
+
<%= f.label :name %>
|
|
109
|
+
<%= f.text_field :name %>
|
|
110
|
+
<%= f.submit "Save" %>
|
|
111
|
+
<% end %>
|
|
112
|
+
```
|
|
30
113
|
|
|
31
|
-
|
|
114
|
+
## Rename mappings and conditionally write fields
|
|
32
115
|
|
|
33
|
-
|
|
116
|
+
Assume `Project` also has a `description` column. This form exposes a different
|
|
117
|
+
name for that field and writes it only when the caller explicitly opts in:
|
|
34
118
|
|
|
35
|
-
|
|
119
|
+
```ruby
|
|
120
|
+
class ProjectDetailsForm < Composable::Form::Command
|
|
121
|
+
attribute :name, :summary, type: :string
|
|
122
|
+
attribute :update_summary, type: :boolean, default: false
|
|
123
|
+
validates :name, presence: true
|
|
124
|
+
|
|
125
|
+
composable :project, model: Project do
|
|
126
|
+
sync :name
|
|
127
|
+
sync :summary, to: :description, if: :update_summary?
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def save
|
|
133
|
+
project
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
form = ProjectDetailsForm.new(project: project)
|
|
138
|
+
form.summary # => the project's current description
|
|
139
|
+
|
|
140
|
+
operation = ProjectDetailsForm.call(
|
|
141
|
+
project: project, name: "Revised", summary: "New description",
|
|
142
|
+
update_summary: true
|
|
143
|
+
)
|
|
144
|
+
operation.result # => project, when persistence succeeds
|
|
145
|
+
```
|
|
36
146
|
|
|
37
|
-
|
|
147
|
+
The condition controls writes, not the initial read from the record. Treat
|
|
148
|
+
`update_summary` as application policy input: do not permit it from a browser
|
|
149
|
+
unless the user is allowed to make that decision. Implement `save` rather than
|
|
150
|
+
redefining `call`, so validation, composition and command wrapping remain active.
|
|
38
151
|
|
|
39
|
-
|
|
152
|
+
## Normalize input before saving
|
|
40
153
|
|
|
41
|
-
|
|
154
|
+
Callbacks run after form validation. For a value that must be normalized before
|
|
155
|
+
validation, normalize in its setter rather than a `before_save` callback:
|
|
42
156
|
|
|
43
|
-
|
|
157
|
+
```ruby
|
|
158
|
+
class NormalizedProjectForm < ProjectForm
|
|
159
|
+
def name=(value)
|
|
160
|
+
super(value&.strip)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
form = NormalizedProjectForm.new(project: project, name: " Example ")
|
|
165
|
+
form.name # => "Example"
|
|
166
|
+
```
|
|
@@ -8,14 +8,10 @@ module Composable
|
|
|
8
8
|
include Core::AttributeDSL
|
|
9
9
|
include Core::ComposableDSL
|
|
10
10
|
include Core::Callbacks
|
|
11
|
+
prepend Core::Command
|
|
11
12
|
include ActiveModel::Conversion
|
|
12
13
|
|
|
13
14
|
class << self
|
|
14
|
-
def inherited(subclass)
|
|
15
|
-
super
|
|
16
|
-
subclass.prepend(Core::Command)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
15
|
def composable(attribute, model: nil, **options, &block)
|
|
20
16
|
super(attribute, **options, &block)
|
|
21
17
|
|
|
@@ -32,9 +28,11 @@ module Composable
|
|
|
32
28
|
def call
|
|
33
29
|
return unless valid?
|
|
34
30
|
|
|
35
|
-
run_callbacks :save do
|
|
31
|
+
completed = run_callbacks :save do
|
|
36
32
|
@result = save_composables { save }
|
|
37
33
|
end
|
|
34
|
+
errors.add(:base, :invalid) if completed == false
|
|
35
|
+
@result
|
|
38
36
|
end
|
|
39
37
|
|
|
40
38
|
def persisted?
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: composable-form
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jairo Vazquez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: composable-core
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.14
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.14
|
|
26
26
|
description: Form composable object to work with Rails ActionView forms
|
|
27
27
|
email:
|
|
28
28
|
- jairovm20@gmail.com
|
|
@@ -51,7 +51,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
51
51
|
requirements:
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 2.
|
|
54
|
+
version: 3.2.0
|
|
55
55
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
56
|
requirements:
|
|
57
57
|
- - ">="
|