composable-form 0.0.12 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01b2fd833bdd18c409136253a91d70e2d19073097c1681f156e732b2a4e09ef3
4
- data.tar.gz: d640cda21e2e83b36b239333ad5f7954028e0b7ce7d99b02bf89be6ecbbd0954
3
+ metadata.gz: f24c4de19b32a5c86c1e1d27fcadda201048976bf7301adfb42277e6b02e5e52
4
+ data.tar.gz: 1c4390383698b86e1e233d5bd11b829e6651a1982de360abf6c00952298be6d8
5
5
  SHA512:
6
- metadata.gz: 9394e1aa23611b0344e5c52839745585cadb83d4ba8343bfedec143e3056969f45835a49310b995ddbcc4e97fc03da93eb604ac0eb6b59d28966c5c96fcbfa19
7
- data.tar.gz: a31af33ef9a8a3a10f34693537186a391d54e4ea2a9370de69f93a01f9a9980e2029ad3274dffb013e87efdafa654b7d5367c4bf2bc1f340b3e4a55537da0687
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
- # Composable::Form
1
+ # composable-form
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/composable/form`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
7
+ ## Map a form to a record
6
8
 
7
- ## Installation
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
- Add this line to your application's Gemfile:
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
- gem 'composable-form'
38
+ form = ProjectForm.call(
39
+ params.require(:project).permit(:name).to_h.merge(project: authorized_project)
40
+ )
13
41
  ```
14
42
 
15
- And then execute:
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
- $ bundle install
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
- Or install it yourself as:
66
+ ## Connect a controller and view
20
67
 
21
- $ gem install composable-form
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
- ## Usage
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
- TODO: Write usage instructions here
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
- ## Development
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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
108
+ <%= f.label :name %>
109
+ <%= f.text_field :name %>
110
+ <%= f.submit "Save" %>
111
+ <% end %>
112
+ ```
30
113
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
114
+ ## Rename mappings and conditionally write fields
32
115
 
33
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/jairovm/composables. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/jairovm/composables/tree/main/CODE_OF_CONDUCT.md).
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
- ## License
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
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
152
+ ## Normalize input before saving
40
153
 
41
- ## Code of Conduct
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
- Everyone interacting in the Composable::Form project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jairovm/composables/tree/main/CODE_OF_CONDUCT.md).
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
36
- save_composables { save }
31
+ completed = run_callbacks :save do
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?
@@ -10,7 +10,7 @@ module Composable
10
10
  module VERSION
11
11
  MAJOR = 0
12
12
  MINOR = 0
13
- TINY = 12
13
+ TINY = 14
14
14
  PRE = nil
15
15
 
16
16
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: composable-form
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.12
4
+ version: 0.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jairo Vazquez
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-03-02 00:00:00.000000000 Z
10
+ date: 2026-10-07 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: composable-core
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - '='
18
17
  - !ruby/object:Gem::Version
19
- version: 0.0.12
18
+ version: 0.0.14
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - '='
25
24
  - !ruby/object:Gem::Version
26
- version: 0.0.12
25
+ version: 0.0.14
27
26
  description: Form composable object to work with Rails ActionView forms
28
27
  email:
29
28
  - jairovm20@gmail.com
@@ -45,7 +44,6 @@ metadata:
45
44
  homepage_uri: https://github.com/jairovm/composables
46
45
  source_code_uri: https://github.com/jairovm/composables/tree/main/composable-form
47
46
  changelog_uri: https://github.com/jairovm/composables/tree/main/composable-form/CHANGELOG.md
48
- post_install_message:
49
47
  rdoc_options: []
50
48
  require_paths:
51
49
  - lib
@@ -53,15 +51,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
53
51
  requirements:
54
52
  - - ">="
55
53
  - !ruby/object:Gem::Version
56
- version: 2.7.0
54
+ version: 3.2.0
57
55
  required_rubygems_version: !ruby/object:Gem::Requirement
58
56
  requirements:
59
57
  - - ">="
60
58
  - !ruby/object:Gem::Version
61
59
  version: '0'
62
60
  requirements: []
63
- rubygems_version: 3.5.5
64
- signing_key:
61
+ rubygems_version: 3.6.2
65
62
  specification_version: 4
66
63
  summary: Form Composable Object
67
64
  test_files: []