yaaf 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 897bfdef28cdce5ee405763d213d28c7b2e011a661c4a7106f52100786110f6d
4
+ data.tar.gz: 3bd98b7cc8a9fcd78e982844a311ea56b7dd1dacbebc0d8c3c0d5f879a1c13fd
5
+ SHA512:
6
+ metadata.gz: f073c7fa7d30bd7bfdafd4169dedc1af11cb4ad186c22f25237011cd23a7f19e628c1d5a6ac33d4b2318456800398a65e039a0cc8d74b517194c3852716b545c
7
+ data.tar.gz: 94851b76f07a37e0c8f339e9e76e3a0576fadd0797ce0b5c53ddf307b9650016728e6ac99efb8adfcb1cf9857c0c14b851e66627159b7c1c36c3720887c1a0c1
@@ -0,0 +1,42 @@
1
+ name: CI
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ build:
7
+ if: "! contains(toJSON(github.event.commits.*.message), '[skip-ci]')"
8
+ runs-on: ubuntu-latest
9
+ strategy:
10
+ matrix:
11
+ gemfile: [rails_4.0.0.gemfile, rails_5.0.0.gemfile, rails_5.2.3.gemfile, rails_6.0.0.gemfile, rails_master.gemfile]
12
+ ruby_version: [2.4.x, 2.5.x, 2.6.x, 2.7.x]
13
+ exclude:
14
+ - gemfile: rails_master.gemfile
15
+ ruby_version: 2.4.x
16
+ - gemfile: rails_6.0.0.gemfile
17
+ ruby_version: 2.4.x
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - name: Setup Ruby
21
+ uses: actions/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby_version }}
24
+ - name: Update rubygems when testing with Ruby 2.4.x
25
+ if: startsWith(matrix.ruby_version, '2.4')
26
+ run: |
27
+ gem update --system --no-document
28
+ - name: Before build
29
+ run: |
30
+ sudo apt-get install libsqlite3-dev
31
+ curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
32
+ chmod +x ./cc-test-reporter
33
+ ./cc-test-reporter before-build
34
+ env:
35
+ CC_TEST_REPORTER_ID: aff2c7b9e07e54d5fc9e5588d2e2a8bab4f69950d35000edc2b6250bbaba477d
36
+ - name: Build and test with Rake
37
+ run: |
38
+ gem install bundler:1.17.3
39
+ bundle _1.17.3_ update
40
+ bundle _1.17.3_ install --gemfile spec/gemfiles/${{ matrix.gemfile }} --jobs 4 --retry 3
41
+ bundle _1.17.3_ exec rake code_analysis
42
+ bundle _1.17.3_ exec rspec
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ Gemfile.lock
14
+ .irb_history
@@ -0,0 +1,128 @@
1
+ detectors:
2
+ Attribute:
3
+ enabled: false
4
+ exclude: []
5
+ BooleanParameter:
6
+ enabled: true
7
+ exclude: []
8
+ ClassVariable:
9
+ enabled: false
10
+ exclude: []
11
+ ControlParameter:
12
+ enabled: true
13
+ exclude: []
14
+ DataClump:
15
+ enabled: true
16
+ exclude: []
17
+ max_copies: 2
18
+ min_clump_size: 2
19
+ DuplicateMethodCall:
20
+ enabled: true
21
+ exclude: []
22
+ max_calls: 1
23
+ allow_calls: []
24
+ FeatureEnvy:
25
+ enabled: true
26
+ exclude: []
27
+ InstanceVariableAssumption:
28
+ enabled: false
29
+ IrresponsibleModule:
30
+ enabled: false
31
+ exclude: []
32
+ LongParameterList:
33
+ enabled: true
34
+ exclude: []
35
+ max_params: 4
36
+ overrides:
37
+ initialize:
38
+ max_params: 5
39
+ LongYieldList:
40
+ enabled: true
41
+ exclude: []
42
+ max_params: 3
43
+ ManualDispatch:
44
+ enabled: true
45
+ exclude: []
46
+ MissingSafeMethod:
47
+ enabled: false
48
+ exclude: []
49
+ ModuleInitialize:
50
+ enabled: true
51
+ exclude: []
52
+ NestedIterators:
53
+ enabled: true
54
+ exclude: []
55
+ max_allowed_nesting: 2
56
+ ignore_iterators: []
57
+ NilCheck:
58
+ enabled: false
59
+ exclude: []
60
+ RepeatedConditional:
61
+ enabled: true
62
+ exclude: []
63
+ max_ifs: 3
64
+ SubclassedFromCoreClass:
65
+ enabled: true
66
+ exclude: []
67
+ TooManyConstants:
68
+ enabled: true
69
+ exclude: []
70
+ max_constants: 5
71
+ TooManyInstanceVariables:
72
+ enabled: true
73
+ exclude: []
74
+ max_instance_variables: 9
75
+ TooManyMethods:
76
+ enabled: true
77
+ exclude: []
78
+ max_methods: 25
79
+ TooManyStatements:
80
+ enabled: true
81
+ exclude:
82
+ - initialize
83
+ max_statements: 12
84
+ UncommunicativeMethodName:
85
+ enabled: true
86
+ exclude: []
87
+ reject:
88
+ - "/^[a-z]$/"
89
+ - "/[0-9]$/"
90
+ - "/[A-Z]/"
91
+ accept: []
92
+ UncommunicativeModuleName:
93
+ enabled: true
94
+ exclude: []
95
+ reject:
96
+ - "/^.$/"
97
+ - "/[0-9]$/"
98
+ accept:
99
+ - Inline::C
100
+ - "/V[0-9]/"
101
+ UncommunicativeParameterName:
102
+ enabled: true
103
+ exclude: []
104
+ reject:
105
+ - "/^.$/"
106
+ - "/[0-9]$/"
107
+ - "/[A-Z]/"
108
+ accept: []
109
+ UncommunicativeVariableName:
110
+ enabled: true
111
+ exclude:
112
+ - YAAF::Form#save_in_transaction
113
+ reject:
114
+ - "/^.$/"
115
+ - "/[0-9]$/"
116
+ - "/[A-Z]/"
117
+ accept:
118
+ - _
119
+ UnusedParameters:
120
+ enabled: true
121
+ exclude: []
122
+ UnusedPrivateMethod:
123
+ enabled: false
124
+ UtilityFunction:
125
+ enabled: false
126
+
127
+ exclude_paths:
128
+ - config
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --color
3
+ --order rand
4
+ --require spec_helper
@@ -0,0 +1,85 @@
1
+ Layout/LineLength:
2
+ Max: 100
3
+ # To make it possible to copy or click on URIs in the code, we allow lines
4
+ # containing a URI to be longer than Max.
5
+ AllowURI: true
6
+ URISchemes:
7
+ - http
8
+ - https
9
+
10
+ Layout/SpaceBeforeFirstArg:
11
+ Exclude:
12
+
13
+ Lint/AmbiguousBlockAssociation:
14
+ Exclude:
15
+ - spec/**/*
16
+
17
+ Lint/RescueException:
18
+ Exclude:
19
+ - lib/yaaf/form.rb
20
+
21
+ Metrics/AbcSize:
22
+ # The ABC size is a calculated magnitude, so this number can be an Integer or
23
+ # a Float.
24
+ Max: 15
25
+
26
+ Metrics/BlockLength:
27
+ CountComments: false # count full line comments?
28
+ Max: 25
29
+ Exclude:
30
+ - 'yaaf.gemspec'
31
+ - config/**/*
32
+ - spec/**/*
33
+ ExcludedMethods:
34
+ - class_methods
35
+
36
+ Metrics/BlockNesting:
37
+ Max: 4
38
+
39
+ Metrics/ClassLength:
40
+ CountComments: false # count full line comments?
41
+ Max: 200
42
+
43
+ # Avoid complex methods.
44
+ Metrics/CyclomaticComplexity:
45
+ Max: 7
46
+
47
+ Metrics/MethodLength:
48
+ CountComments: false # count full line comments?
49
+ Max: 24
50
+
51
+ Metrics/ModuleLength:
52
+ CountComments: false # count full line comments?
53
+ Max: 200
54
+
55
+ Metrics/ParameterLists:
56
+ Max: 5
57
+ CountKeywordArgs: true
58
+
59
+ Metrics/PerceivedComplexity:
60
+ Max: 12
61
+
62
+ Style/Documentation:
63
+ Enabled: false
64
+
65
+ Style/FrozenStringLiteralComment:
66
+ Enabled: false
67
+
68
+ Style/HashEachMethods:
69
+ Enabled: true
70
+
71
+ Style/HashTransformKeys:
72
+ Enabled: true
73
+
74
+ Style/HashTransformValues:
75
+ Enabled: true
76
+
77
+ Style/ModuleFunction:
78
+ Enabled: false
79
+
80
+ Style/RescueModifier:
81
+ Exclude:
82
+ - spec/**/*
83
+
84
+ Naming/PredicateName:
85
+ Enabled: false
@@ -0,0 +1 @@
1
+ 2.7.0
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at juan.ramallo[at]rootstrap.com or santiago.bartesaghi[at]rootstrap.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
@@ -0,0 +1,42 @@
1
+ ## Contributing ##
2
+
3
+ You can contribute to this repo if you have an issue, found a bug or think there's some functionality required that would add value to the gem. To do so, please check if there's not already an [issue](https://github.com/rootstrap/yaaf/issues) for that, if you find there's not, create a new one with as much detail as possible.
4
+
5
+ If you want to contribute with code as well, please follow the next steps:
6
+
7
+ 1. Read, understand and agree to our [code of conduct](https://github.com/rootstrap/yaaf/blob/master/CODE_OF_CONDUCT.md)
8
+ 2. [Fork the repo](https://help.github.com/articles/about-forks/)
9
+ 3. Clone the project into your machine:
10
+ `$ git clone git@github.com:rootstrap/yaaf.git`
11
+ 4. Access the repo:
12
+ `$ cd yaaf`
13
+ 5. Create your feature/bugfix branch:
14
+ `$ git checkout -b your_new_feature`
15
+ or
16
+ `$ git checkout -b fix/your_fix` in case of a bug fix
17
+ (if your PR is to address an existing issue, it would be good to name the branch after the issue, for example: if you are trying to solve issue 182, then a good idea for the branch name would be `182_your_new_feature`)
18
+ 6. Write tests for your changes (feature/bug)
19
+ 7. Code your (feature/bugfix)
20
+ 8. Run the code analysis tool by doing:
21
+ `$ rake code_analysis`
22
+ 9. Run the tests:
23
+ `$ bundle exec rspec`
24
+ All tests must pass. If all tests (both code analysis and rspec) do pass, then you are ready to go to the next step:
25
+ 10. Commit your changes:
26
+ `$ git commit -m 'Your feature or bugfix title'`
27
+ 11. Push to the branch `$ git push origin your_new_feature`
28
+ 12. Create a new [pull request](https://help.github.com/articles/creating-a-pull-request/)
29
+
30
+ Some helpful guides that will help you know how we work:
31
+ 1. [Code review](https://github.com/rootstrap/tech-guides/tree/master/code-review)
32
+ 2. [GIT workflow](https://github.com/rootstrap/tech-guides/tree/master/git)
33
+ 3. [Ruby style guide](https://github.com/rootstrap/tech-guides/tree/master/ruby)
34
+ 4. [Rails style guide](https://github.com/rootstrap/tech-guides/blob/master/ruby/rails.md)
35
+ 5. [RSpec style guide](https://github.com/rootstrap/tech-guides/blob/master/ruby/rspec/README.md)
36
+
37
+ For more information or guides like the ones mentioned above, please check our [tech guides](https://github.com/rootstrap/tech-guides). Keep in mind that the more you know about these guides, the easier it will be for your code to get approved and merged.
38
+
39
+ Note: We work with one commit per pull request, so if you make your commit and realize you were missing something or want to add something more to it, don't create a new commit with the changes, but use `$ git commit --amend` instead. This same principle also applies for when changes are requested on an open pull request.
40
+
41
+
42
+ Thank you very much for your time and for considering helping in this project.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Rootstrap
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,272 @@
1
+ # YAAF
2
+
3
+ ![CI](https://github.com/rootstrap/yaaf/workflows/CI/badge.svg)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/c3dea064e1003b700260/maintainability)](https://codeclimate.com/github/rootstrap/yaaf/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/c3dea064e1003b700260/test_coverage)](https://codeclimate.com/github/rootstrap/yaaf/test_coverage)
6
+
7
+ YAAF (Yet Another Active Form) is a gem that let you create form objects in an easy and Rails friendly way. It makes use of `ActiveRecord` and `ActiveModel` features in order to provide you with a form object that behaves pretty much like a Rails model, and still be completely configurable.
8
+
9
+ We were going to name this gem `ActiveForm` to follow Rails naming conventions but given there are a lot of form object gems named like that we preferred to go with `YAAF`.
10
+
11
+ ## Table of Contents
12
+
13
+ - [Motivation](#motivation)
14
+ - [Installation](#installation)
15
+ - [Usage](#usage)
16
+ - [Setting up a form object](#setting-up-a-form-object)
17
+ - [#initialize](#initialize)
18
+ - [#valid?](#valid?)
19
+ - [#invalid?](#invalid?)
20
+ - [#errors](#errors)
21
+ - [#save](#save)
22
+ - [#save!](#save!)
23
+ - [Validations](#validations)
24
+ - [Callbacks](#callbacks)
25
+ - [Sample app](#sample-app)
26
+ - [Links](#links)
27
+ - [Development](#development)
28
+ - [Contributing](#contributing)
29
+ - [License](#license)
30
+ - [Code of Conduct](#code-of-conduct)
31
+ - [Credits](#credits)
32
+
33
+ ## Motivation
34
+
35
+ Form Objects is a design pattern that allows us to:
36
+ 1. Keep views, models and controllers clean
37
+ 2. Create/update multiple models at the same time
38
+ 3. Keep business logic validations out of models
39
+
40
+ There are some other form objects gems but we felt none of them provided us all the features that we expected:
41
+ 1. Form objects that behaves like Rails models
42
+ 2. Simple to use and to understand the implementation (no magic)
43
+ 3. Easy to customize
44
+ 4. Gem is well tested and maintained
45
+
46
+ For this reason we decided to build our own Form Object implementation. After several months in production without issues we decided to extract it into a gem to share it with the community.
47
+
48
+ If you want to learn more about Form Objects you can check out [these great articles](#links).
49
+
50
+ ## Installation
51
+
52
+ Add this line to your application's Gemfile:
53
+
54
+ ```ruby
55
+ gem 'yaaf'
56
+ ```
57
+
58
+ And then execute:
59
+
60
+ $ bundle install
61
+
62
+ Or install it yourself as:
63
+
64
+ $ gem install yaaf
65
+
66
+ ## Usage
67
+
68
+ In the following sections we explain some basic usage and the API provided by the gem. You can also find some recipes [here](https://rootstrap.github.io/yaaf).
69
+
70
+ ### Setting up a form object
71
+
72
+ In order to use a `YAAF` form object, you need to inherit from `YAAF::Form` and define the `@models` of the form, for example:
73
+ ```ruby
74
+ # app/forms/registration_form.rb
75
+
76
+ class RegistrationForm < YAAF::Form
77
+ attr_accessor :user_attributes
78
+
79
+ def initialize(attributes)
80
+ super(attributes)
81
+ @models = [user]
82
+ end
83
+
84
+ def user
85
+ @user ||= User.new(user_attributes)
86
+ end
87
+ end
88
+ ```
89
+
90
+ By doing that you can work with your form object in your controller such as you'd do with a model.
91
+ ```ruby
92
+ # app/controllers/registrations_controller.rb
93
+
94
+ class RegistrationsController < ApplicationController
95
+ def create
96
+ registration_form = RegistrationForm.new(user_attributes: user_params)
97
+
98
+ if registration_form.save
99
+ redirect_to registration.user
100
+ else
101
+ render :new
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def user_params
108
+ params.require(:user).permit(:email, :password, :password_confirmation)
109
+ end
110
+ end
111
+ ```
112
+
113
+ Form objects supports calls to [valid?](#valid?), [invalid?](#invalid?), [errors](#errors), [save](#save), [save!](#save!), such as any `ActiveModel` model. The return values match the corresponding `ActiveModel` methods.
114
+
115
+ When saving or validating a form object, it will automatically validate all its models and promote the error to the form object itself, so they are accessible to you directly from the form object.
116
+
117
+ Form objects can also define validations like:
118
+ ```ruby
119
+ # app/forms/registration_form.rb
120
+
121
+ class RegistrationForm < YAAF::Form
122
+ validates :phone, presence: true
123
+ validate :a_custom_validation
124
+
125
+ # ...
126
+
127
+ def a_custom_validation
128
+ # ...
129
+ end
130
+ end
131
+ ```
132
+
133
+ Validations can be skipped the same way as for `ActiveModel` models:
134
+ ```ruby
135
+ # app/controllers/registrations_controller.rb
136
+
137
+ class RegistrationsController < ApplicationController
138
+ def create
139
+ registration_form = RegistrationForm.new(user_attributes: user_params)
140
+
141
+ registration_form.save!(validate: false)
142
+ end
143
+
144
+ private
145
+
146
+ def user_params
147
+ params.require(:user).permit(:email, :password, :password_confirmation)
148
+ end
149
+ end
150
+ ```
151
+
152
+ Form objects support the saving of multiple models at the same time, to prevent leaving the system in a bad state all the models are saved within a DB transaction.
153
+
154
+ A good practice would be to create an empty `ApplicationForm` and make your form objects inherit from it. This way you have a centralized place to customize any `YAAF` default behavior you would like.
155
+
156
+ ```ruby
157
+ class ApplicationForm < YAAF::Form
158
+ # Customized behavior
159
+ end
160
+ ```
161
+
162
+ ### #initialize
163
+
164
+ The `.new` method should be called with the arguments that the form object needs.
165
+
166
+ When initializing a `YAAF` form object, there are two things to keep in mind
167
+ 1. You need to define the `@models` instance variables to be an array of all the models that you want to be validated/saved within the form object.
168
+ 2. To leverage `ActiveModel`'s features, you can call `super` to automatically make the attributes be stored in instance variables. If you use it, make sure to also add `attr_accessor`s, otherwise `ActiveModel` will fail.
169
+
170
+ ### #valid?
171
+
172
+ The `#valid?` method will perform both the form object validations and the models validations. It will return `true` or `false` and store the errors in the form object.
173
+
174
+ By default `YAAF` form objects will store model errors in the form object under the same key. For example if a model has an `email` attribute that had an error, the form object will provide an error under the `email` key (e.g. `form_object.errors[:email]`).
175
+
176
+ ### #invalid?
177
+
178
+ The `#invalid?` method is exactly the same as the `.valid?` method but will return the opposite boolean value.
179
+
180
+ ### #errors
181
+
182
+ The `#errors` method will return an `ActiveModel::Errors` object such as any other `ActiveModel` model.
183
+
184
+ ### #save
185
+
186
+ The `#save` method will run validations. If it's invalid it will return `false`, otherwise it will save all the models within a DB transaction and return `true`.
187
+
188
+ Defined callbacks will be called in the following order:
189
+ - `before_validation`
190
+ - `after_validation`
191
+ - `before_save`
192
+ - `after_save`
193
+ - `after_commit/after_rollback`
194
+
195
+ Options:
196
+ - If `validate: false` is send as options to the `save` call, it will skip validations.
197
+
198
+ ### #save!
199
+
200
+ The `#save!` method is exactly the same as the `.save` method, just that if it is invalid it will raise an exception.
201
+
202
+ ### Validations
203
+
204
+ `YAAF` form objects support validations the same way as `ActiveModel` models. For example:
205
+
206
+ ```ruby
207
+ class RegistrationForm < YAAF::Form
208
+ validates :email, presence: true
209
+ validate :some_custom_validation
210
+
211
+ # ...
212
+ end
213
+ ```
214
+
215
+ ### Callbacks
216
+
217
+ `YAAF` form objects support validations the same way as `ActiveModel` models. For example:
218
+
219
+ ```ruby
220
+ class RegistrationForm < YAAF::Form
221
+ before_validation :normalize_attributes
222
+ after_commit :send_confirmation_email
223
+
224
+ # ...
225
+ end
226
+ ```
227
+
228
+ Available callbacks are (listed in execution order):
229
+ - `before_validation`
230
+ - `after_validation`
231
+ - `before_save`
232
+ - `after_save`
233
+ - `after_commit/after_rollback`
234
+
235
+ ## Sample app
236
+
237
+ You can find a sample app making use of the gem [here](https://yaaf-examples.herokuapp.com). Its code is also open source, and you can find it [here](https://github.com/rootstrap/yaaf-examples).
238
+
239
+ ## Links
240
+
241
+ - [How to improve maintainability in Rails applications using patterns. Part I](https://www.rootstrap.com/blog/2020/02/14/how-to-improve-maintainability-in-rails-applications-using-patterns-part-i/)
242
+ - [7 Patterns to Refactor Fat ActiveRecord Models](https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models/)
243
+ - [ActiveModel Form Objects](https://thoughtbot.com/blog/activemodel-form-objects)
244
+ - [Form Objects Design Pattern](https://gorails.com/episodes/form-objects-design-pattern)
245
+ - [Form Object from Railscasts](https://makandracards.com/alexander-m/42271-form-object-from-railscasts)
246
+ - [Validating Form Objects](https://revs.runtime-revolution.com/validating-form-objects-8058fefc7b89)
247
+ - [Disciplined Rails: Form Object Techniques & Patterns — Part 1](https://medium.com/@jaryl/disciplined-rails-form-object-techniques-patterns-part-1-23cfffcaf429)
248
+ - [Complex form objects with Rails](https://www.codementor.io/@victor_hazbun/complex-form-objects-in-rails-qval6b8kt)
249
+
250
+ ## Development
251
+
252
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
253
+
254
+ 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
255
+
256
+ ## Contributing
257
+
258
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rootstrap/yaaf. 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/rootstrap/yaaf/blob/master/CODE_OF_CONDUCT.md).
259
+
260
+ ## License
261
+
262
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
263
+
264
+ ## Code of Conduct
265
+
266
+ Everyone interacting in the YAAF project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/rootstrap/yaaf/blob/master/CODE_OF_CONDUCT.md).
267
+
268
+ ## Credits
269
+
270
+ YAAF is maintained by [Rootstrap](http://www.rootstrap.com) with the help of our [contributors](https://github.com/rootstrap/yaaf/contributors).
271
+
272
+ [<img src="https://s3-us-west-1.amazonaws.com/rootstrap.com/img/rs.png" width="100"/>](http://www.rootstrap.com)
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ task :code_analysis do
4
+ sh 'bundle exec rubocop lib spec'
5
+ sh 'bundle exec reek lib'
6
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'yaaf'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,15 @@
1
+ # 📖 Documentation
2
+
3
+ ## Usage
4
+
5
+ [Check usage section in the main README](https://github.com/rootstrap/yaaf/blob/master/README.md#usage)
6
+
7
+ ## Recipes
8
+
9
+ - [YAAF with Simple Form](recipes/simple_form.md)
10
+
11
+ ## Credits
12
+
13
+ YAAF is maintained by [Rootstrap](http://www.rootstrap.com) with the help of our [contributors](https://github.com/rootstrap/yaaf/contributors).
14
+
15
+ [<img src="https://s3-us-west-1.amazonaws.com/rootstrap.com/img/rs.png" width="100"/>](http://www.rootstrap.com)
@@ -0,0 +1,117 @@
1
+ # Using YAAF with Simple Form
2
+
3
+ [Simple Form](https://github.com/heartcombo/simple_form) is a gem to create visual forms in an easy and flexible manner.
4
+
5
+ ## Usage
6
+
7
+ Pass a `YAAF::Form` object instance to the `simple_form_for` helper method.
8
+
9
+ For example, in a library management system (a pretty miminal one), the form to create books will look like this:
10
+
11
+ ```ruby
12
+ # app/forms/book_form.rb
13
+
14
+ class BookForm < YAAF::Form
15
+ attr_accessor :name, :isbn
16
+
17
+ def initialize(attributes)
18
+ super(attributes)
19
+
20
+ @models = [book]
21
+ end
22
+
23
+ def book
24
+ @book ||= Book.new(name: name, isbn: isbn)
25
+ end
26
+ end
27
+ ```
28
+
29
+
30
+ ```ruby
31
+ # app/controllers/books_controller.rb
32
+
33
+ class BooksController < ApplicationController
34
+ def new
35
+ @book = BookForm.new
36
+ end
37
+ end
38
+ ```
39
+
40
+ ```erb
41
+ <%# app/views/books/new.html.erb %>
42
+
43
+ <%= simple_form_for(@book, method: :post, url: books_path) do |f| %>
44
+ <%= f.input :name %>
45
+ <%= f.input :isbn %>
46
+
47
+ <%= f.submit 'Create Book' %>
48
+ <% end %>
49
+ ```
50
+
51
+ ## I18n
52
+
53
+ In order to make use of translations correctly, we should pass an `as` value to the `simple_form_for` helper method.
54
+
55
+ ```erb
56
+ <%# app/views/books/new.html.erb %>
57
+
58
+ <%= simple_form_for(@book, as: :book, ...) do |f| %>
59
+ ...
60
+ ```
61
+
62
+ So you now can write your translations as:
63
+
64
+ ```yaml
65
+ # app/config/locales/simple_form.en.yml
66
+
67
+ en:
68
+ simple_form:
69
+ ...
70
+
71
+ labels:
72
+ book:
73
+ name: Book Name
74
+ isbn: International Standard Book Number
75
+ ```
76
+
77
+ When using the `f.submit` helper method from Simple Form, the method `model_name` will be used to display the
78
+ button's text. If this is not defined it will display: "Create Book Form".
79
+ You can use a translation directly in the view to avoid overriding this method.
80
+
81
+ ```ruby
82
+ # app/forms/book_form.rb
83
+
84
+ ...
85
+
86
+ def model_name
87
+ Book.model_name
88
+ end
89
+
90
+ ...
91
+ ```
92
+
93
+ More info about how Simple Form translates forms [here](https://github.com/heartcombo/simple_form#i18n).
94
+
95
+ ## Persisted?
96
+
97
+ Define the `persisted?` method in your form object to let Simple Form know if the object is being created or upated.
98
+
99
+ ```ruby
100
+ # app/forms/book_form.rb
101
+
102
+ ...
103
+
104
+ def persisted?
105
+ book.persisted? # Or you can directly make it return false
106
+ end
107
+
108
+ ...
109
+ ```
110
+
111
+ ## Got questions?
112
+
113
+ Feel free to [create an issue](https://github.com/rootstrap/yaaf/issues) and we'll discuss about it.
114
+
115
+ YAAF is maintained by [Rootstrap](http://www.rootstrap.com) with the help of our [contributors](https://github.com/rootstrap/yaaf/contributors).
116
+
117
+ [<img src="https://s3-us-west-1.amazonaws.com/rootstrap.com/img/rs.png" width="100"/>](http://www.rootstrap.com)
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'active_record'
5
+ require 'yaaf/form'
6
+ require 'yaaf/version'
7
+
8
+ # Namespace for all objects in YAAF
9
+ module YAAF
10
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YAAF
4
+ # Parent class for form objects
5
+ class Form
6
+ include ::ActiveModel::Model
7
+ include ::ActiveModel::Validations::Callbacks
8
+ include ::ActiveRecord::Transactions
9
+ define_model_callbacks :save
10
+
11
+ validate :validate_models
12
+
13
+ def save(options = {})
14
+ unless options[:validate] == false
15
+ return false if invalid?
16
+ end
17
+
18
+ run_callbacks :commit do
19
+ save_in_transaction(options)
20
+ end
21
+
22
+ true
23
+ end
24
+
25
+ def save!(options = {})
26
+ save(options) || raise(ActiveRecord::RecordNotSaved.new('Failed to save the form', self))
27
+ end
28
+
29
+ private
30
+
31
+ attr_accessor :models
32
+
33
+ def promote_errors(model)
34
+ model.errors.each do |attribute, message|
35
+ errors.add(attribute, message)
36
+ end
37
+ end
38
+
39
+ def save_in_transaction(options)
40
+ ::ActiveRecord::Base.transaction do
41
+ run_callbacks :save do
42
+ save_models(options)
43
+ end
44
+ end
45
+ rescue Exception => e
46
+ handle_transaction_rollback(e)
47
+ end
48
+
49
+ def save_models(options)
50
+ models.map { |model| model.save!(options) }
51
+ end
52
+
53
+ def validate_models
54
+ models.each do |model|
55
+ promote_errors(model) if model.invalid?
56
+ end
57
+ end
58
+
59
+ def handle_transaction_rollback(exception)
60
+ run_callbacks :rollback
61
+ raise exception
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YAAF
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/yaaf/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'yaaf'
7
+ spec.version = YAAF::VERSION
8
+ spec.authors = ['Juan Manuel Ramallo', 'Santiago Bartesaghi']
9
+ spec.email = ['juan.ramallo@rootstrap.com']
10
+
11
+ spec.summary = 'Easing the form object pattern in Rails applications.'
12
+ spec.homepage = 'https://github.com/rootstrap/yaaf'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = 'https://github.com/rootstrap/yaaf'
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = 'exe'
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ['lib']
27
+
28
+ # # Production dependencies
29
+ spec.add_dependency 'activemodel', ['>= 4', '< 7']
30
+ spec.add_dependency 'activerecord', ['>= 4', '< 7']
31
+
32
+ spec.add_development_dependency 'database_cleaner-active_record', '~> 1.8.0'
33
+ spec.add_development_dependency 'rake', '~> 13.0.1'
34
+ spec.add_development_dependency 'reek', '~> 5.6.0'
35
+ spec.add_development_dependency 'rspec', '~> 3.9.0'
36
+ spec.add_development_dependency 'rubocop', '~> 0.80.0'
37
+ spec.add_development_dependency 'simplecov', '~> 0.17.1'
38
+ spec.add_development_dependency 'sqlite3', '~> 1.4.2'
39
+ end
metadata ADDED
@@ -0,0 +1,204 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yaaf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Juan Manuel Ramallo
8
+ - Santiago Bartesaghi
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2020-04-07 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activemodel
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '4'
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: '7'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: '4'
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: '7'
34
+ - !ruby/object:Gem::Dependency
35
+ name: activerecord
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4'
41
+ - - "<"
42
+ - !ruby/object:Gem::Version
43
+ version: '7'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '4'
51
+ - - "<"
52
+ - !ruby/object:Gem::Version
53
+ version: '7'
54
+ - !ruby/object:Gem::Dependency
55
+ name: database_cleaner-active_record
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 1.8.0
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 1.8.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 13.0.1
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 13.0.1
82
+ - !ruby/object:Gem::Dependency
83
+ name: reek
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 5.6.0
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 5.6.0
96
+ - !ruby/object:Gem::Dependency
97
+ name: rspec
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 3.9.0
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 3.9.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: rubocop
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 0.80.0
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 0.80.0
124
+ - !ruby/object:Gem::Dependency
125
+ name: simplecov
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 0.17.1
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: 0.17.1
138
+ - !ruby/object:Gem::Dependency
139
+ name: sqlite3
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: 1.4.2
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 1.4.2
152
+ description:
153
+ email:
154
+ - juan.ramallo@rootstrap.com
155
+ executables: []
156
+ extensions: []
157
+ extra_rdoc_files: []
158
+ files:
159
+ - ".github/workflows/ci.yml"
160
+ - ".gitignore"
161
+ - ".reek.yml"
162
+ - ".rspec"
163
+ - ".rubocop.yml"
164
+ - ".ruby-version"
165
+ - CODE_OF_CONDUCT.md
166
+ - CONTRIBUTING.md
167
+ - Gemfile
168
+ - LICENSE.txt
169
+ - README.md
170
+ - Rakefile
171
+ - bin/console
172
+ - bin/setup
173
+ - docs/README.md
174
+ - docs/recipes/simple_form.md
175
+ - lib/yaaf.rb
176
+ - lib/yaaf/form.rb
177
+ - lib/yaaf/version.rb
178
+ - yaaf.gemspec
179
+ homepage: https://github.com/rootstrap/yaaf
180
+ licenses:
181
+ - MIT
182
+ metadata:
183
+ homepage_uri: https://github.com/rootstrap/yaaf
184
+ source_code_uri: https://github.com/rootstrap/yaaf
185
+ post_install_message:
186
+ rdoc_options: []
187
+ require_paths:
188
+ - lib
189
+ required_ruby_version: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: 2.3.0
194
+ required_rubygems_version: !ruby/object:Gem::Requirement
195
+ requirements:
196
+ - - ">="
197
+ - !ruby/object:Gem::Version
198
+ version: '0'
199
+ requirements: []
200
+ rubygems_version: 3.0.3
201
+ signing_key:
202
+ specification_version: 4
203
+ summary: Easing the form object pattern in Rails applications.
204
+ test_files: []