trans_forms 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +88 -0
- data/Rakefile +27 -0
- data/lib/generators/rails/setup_generator.rb +14 -0
- data/lib/generators/rails/templates/application_trans_form.rb +13 -0
- data/lib/generators/rails/templates/trans_form.rb +31 -0
- data/lib/generators/rails/trans_form_generator.rb +36 -0
- data/lib/generators/rspec/setup_generator.rb +9 -0
- data/lib/generators/rspec/templates/application_trans_form_spec.rb +4 -0
- data/lib/generators/rspec/templates/trans_form_spec.rb +4 -0
- data/lib/generators/rspec/trans_form_generator.rb +9 -0
- data/lib/trans_forms.rb +9 -0
- data/lib/trans_forms/callbacks.rb +30 -0
- data/lib/trans_forms/errors.rb +13 -0
- data/lib/trans_forms/form_base.rb +47 -0
- data/lib/trans_forms/form_errors.rb +38 -0
- data/lib/trans_forms/main_model.rb +115 -0
- data/lib/trans_forms/main_model/proxy.rb +38 -0
- data/lib/trans_forms/nested_forms.rb +69 -0
- data/lib/trans_forms/version.rb +3 -0
- data/spec/generators/trans_form/trans_form_generator_spec.rb +8 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/models.rb +12 -0
- data/spec/support/schema.rb +23 -0
- data/spec/support/trans_forms/callback_forms.rb +22 -0
- data/spec/support/trans_forms/main_model_forms.rb +27 -0
- data/spec/support/trans_forms/multiple_records_forms.rb +31 -0
- data/spec/support/trans_forms/nested_forms.rb +26 -0
- data/spec/support/trans_forms/simple_forms.rb +13 -0
- data/spec/support/trans_forms/user_creator_1.rb +13 -0
- data/spec/trans_forms/callbacks_spec.rb +27 -0
- data/spec/trans_forms/form_base_spec.rb +70 -0
- data/spec/trans_forms/form_errors_spec.rb +14 -0
- data/spec/trans_forms/main_model/proxy_spec.rb +15 -0
- data/spec/trans_forms/main_model_spec.rb +49 -0
- data/spec/trans_forms/nested_forms_spec.rb +53 -0
- data/trans_forms.gemspec +29 -0
- metadata +214 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
Y2Y0YmRmMDIyZjI0Y2MxYmEwNmY1YTI1Nzk2NjVhN2U0YWFmOGRhMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MDQxZWY0NTJkNWQ5NWIyNTkyOTBjYjhlNmViMjUwZDI5NzkwOTcwZA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
OTVhYjk0ZTMyZDgyZjMyOTIzMGI3YzU4MTAwNGFmYTI3ZTFjM2FmMWQzYzYw
|
10
|
+
ZWE3MjgwNTA3Y2Q3OTM5OGExMmU3Y2Q1Yzk2NzNiODI0MTg1YTY3ODUzOTVj
|
11
|
+
NTBhYjQ2YmRkNWFkNWFjNWVmMGFlMWZjYTMxYWRmMjkzMTk4YWQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
NDE0NzQ4YTY2MmVkZmZjZWUwNGU2NzE3ZGJiY2M0ZTdmMmRhZWM5NDFhMWI4
|
14
|
+
NDM1ODUxZTFmY2VjNGU0YWJlODQ0MjQ5ZThiOGNlZmZhODA0MGMwNmM5ZjVl
|
15
|
+
Mzc5MTBmNWQzNWNmNjQ4ODY3MTk4YmU2YTFjYzlkMDAyNTNlYzY=
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Daniel Viklund
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
[![Build Status](https://travis-ci.org/dannemanne/trans_forms.svg?branch=master)](https://travis-ci.org/dannemanne/trans_forms)
|
2
|
+
[![Code Climate](https://codeclimate.com/github/dannemanne/trans_forms.png)](https://codeclimate.com/github/dannemanne/trans_forms)
|
3
|
+
|
4
|
+
# TransForms
|
5
|
+
|
6
|
+
TransForms is short for Transactional Forms. I created it mainly because I felt that
|
7
|
+
the ActiveRecord Models all to often get cluttered with a lot of conditional validations
|
8
|
+
and processing. As some of my models grew, I noticed that most of the mess was caused
|
9
|
+
by complex validations and callbacks that were sometimes needed, and other times they
|
10
|
+
were out of scope and had to be skipped, just to allow the record to be saved. I felt
|
11
|
+
that there was another layer missing.
|
12
|
+
|
13
|
+
By placing some of the logic and validations, that only needs to be processed in certain
|
14
|
+
scenarios, into dedicated Form Models, the ActiveRecord Models got a lot cleaner. And
|
15
|
+
in the process I gained more control of the save transaction. And the greatest benefit of
|
16
|
+
this setup is the ability to easily update multiple records without having to rely on
|
17
|
+
messy callbacks and exceptions in association validations.
|
18
|
+
|
19
|
+
This is the first extract of what I have been using in one of my projects. So far it
|
20
|
+
is still missing some of the functionality I have in the original (like confirmation
|
21
|
+
handling when conflicts occur) but the core is there. And more will come as I manage
|
22
|
+
to detach it from the more specific business logic.
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
gem 'trans_forms'
|
29
|
+
|
30
|
+
And then execute:
|
31
|
+
|
32
|
+
$ bundle
|
33
|
+
|
34
|
+
Or install it yourself as:
|
35
|
+
|
36
|
+
$ gem install trans_forms
|
37
|
+
|
38
|
+
## Writing Form Models
|
39
|
+
|
40
|
+
Form Models inherit from `TransForms::FormBase` and live in the `app/trans_forms`
|
41
|
+
directory of your application.
|
42
|
+
|
43
|
+
# app/trans_forms/post_form.rb
|
44
|
+
class PostForm < TransForms::FormBase
|
45
|
+
# ...
|
46
|
+
end
|
47
|
+
|
48
|
+
### Generators
|
49
|
+
|
50
|
+
To manually generate a Form Model, you can run...
|
51
|
+
|
52
|
+
rails g trans_form PostForm
|
53
|
+
|
54
|
+
... to create a `PostForm` Form Model.
|
55
|
+
|
56
|
+
### Shared Form Model Methods
|
57
|
+
|
58
|
+
You might want to add some default functionality to all your Form Models. The common
|
59
|
+
practice to accomplish this is to have an application specific model that inherits
|
60
|
+
from the FormBase. Then you have all of your FormModels inherit from this model instead.
|
61
|
+
|
62
|
+
You can generate a default model like this by running:
|
63
|
+
|
64
|
+
TODO: Add generator
|
65
|
+
|
66
|
+
This will add an `ApplicationTransForm` model...
|
67
|
+
|
68
|
+
# app/trans_forms/application_trans_form.rb
|
69
|
+
class ApplicationTransForm < TransForms::FormBase
|
70
|
+
# ...
|
71
|
+
end
|
72
|
+
|
73
|
+
... that you can use in you Form Models:
|
74
|
+
|
75
|
+
class PostForm < ApplicationTransForm
|
76
|
+
# ...
|
77
|
+
end
|
78
|
+
|
79
|
+
Note that by having a model with the exact name `ApplicationTransForm`, the generator
|
80
|
+
for new Form Models will by default inherit from that model instead of the `TransForm::FormBase`
|
81
|
+
|
82
|
+
## Contributing
|
83
|
+
|
84
|
+
1. Fork it ( https://github.com/dannemanne/trans_forms/fork )
|
85
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
86
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
87
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
88
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
task 'default' => 'ci'
|
6
|
+
|
7
|
+
desc 'Run all tests for CI'
|
8
|
+
task 'ci' => 'spec'
|
9
|
+
|
10
|
+
desc 'Run all specs'
|
11
|
+
task 'spec' => 'spec:all'
|
12
|
+
|
13
|
+
namespace 'spec' do
|
14
|
+
task 'all' => ['trans_forms', 'generators']
|
15
|
+
|
16
|
+
def spec_task(name)
|
17
|
+
desc "Run #{name} specs"
|
18
|
+
RSpec::Core::RakeTask.new(name) do |t|
|
19
|
+
t.pattern = "spec/#{name}/**/*_spec.rb"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
spec_task 'trans_forms'
|
24
|
+
spec_task 'generators'
|
25
|
+
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Rails
|
2
|
+
module Generators
|
3
|
+
class SetupGenerator < Base
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
def create_form_file
|
7
|
+
template 'application_trans_form.rb', File.join('app/trans_forms', 'application_trans_form.rb')
|
8
|
+
end
|
9
|
+
|
10
|
+
hook_for :test_framework
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class ApplicationTransForm < TransForms::FormBase
|
2
|
+
# Here you can place application specific code to customize the
|
3
|
+
# default behavior of TransForms.
|
4
|
+
#
|
5
|
+
# Here is an example of a custom instantiator that works to set
|
6
|
+
# a model and current_user attributes in addition to the params
|
7
|
+
# which might come directly from the controller.
|
8
|
+
#
|
9
|
+
# def self.new_in_model(model, params = {}, current_user = nil)
|
10
|
+
# new(params.merge(model: model, current_user: current_user))
|
11
|
+
# end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<%- module_namespacing do -%>
|
2
|
+
<%- if parent_class_name.present? -%>
|
3
|
+
class <%= class_name %> < <%= parent_class_name %>
|
4
|
+
<%- else -%>
|
5
|
+
class <%= class_name %>
|
6
|
+
<%- end -%>
|
7
|
+
|
8
|
+
# Define the attributes available for this specific form model. The attributes
|
9
|
+
# are declared according to the Virtus standard
|
10
|
+
#
|
11
|
+
# attribute :name, String
|
12
|
+
# attribute :age, Numeric
|
13
|
+
# attribute :phone_no, Array
|
14
|
+
|
15
|
+
|
16
|
+
# Define validations according to the ActiveModel conventions
|
17
|
+
#
|
18
|
+
# validates :name, presence: true
|
19
|
+
# validates :age, numericality: { greater_than_or_equal_to: 18 }
|
20
|
+
|
21
|
+
transaction do
|
22
|
+
# Perform all actions inside this block. If anything goes wrong, i.e. an
|
23
|
+
# an error is raised because of validation errors, then everything that
|
24
|
+
# has already been done inside this block is rolled back.
|
25
|
+
#
|
26
|
+
# self.user = User.create!(name: name, age: age, phone_no: phone_no)
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
<% end -%>
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rails
|
2
|
+
module Generators
|
3
|
+
class TransFormGenerator < NamedBase
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
check_class_collision
|
6
|
+
|
7
|
+
class_option :parent, type: :string, desc: 'The parent class for the generated form model'
|
8
|
+
|
9
|
+
def create_form_file
|
10
|
+
template 'trans_form.rb', File.join('app/trans_forms', class_path, "#{file_name}.rb")
|
11
|
+
end
|
12
|
+
|
13
|
+
hook_for :test_framework
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def parent_class_name
|
18
|
+
options.fetch('parent') do
|
19
|
+
begin
|
20
|
+
require 'application_trans_form'
|
21
|
+
ApplicationTransForm
|
22
|
+
rescue LoadError
|
23
|
+
'TransForms::FormBase'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Rails 3.0.X compatibility, copied from https://github.com/jnunemaker/mongomapper/pull/385/files#L1R32
|
29
|
+
unless methods.include?(:module_namespacing)
|
30
|
+
def module_namespacing
|
31
|
+
yield if block_given?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Rspec
|
2
|
+
class SetupGenerator < ::Rails::Generators::Base
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def create_spec_file
|
6
|
+
template 'application_trans_form_spec.rb', File.join('spec/trans_forms', 'application_trans_form_spec.rb')
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Rspec
|
2
|
+
class TransFormGenerator < ::Rails::Generators::NamedBase
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def create_spec_file
|
6
|
+
template 'trans_form_spec.rb', File.join('spec/trans_forms', class_path, "#{file_name}_spec.rb")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
data/lib/trans_forms.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'trans_forms/callbacks'
|
2
|
+
require 'trans_forms/form_base'
|
3
|
+
|
4
|
+
require 'trans_forms/errors'
|
5
|
+
require 'trans_forms/form_errors'
|
6
|
+
require 'trans_forms/version'
|
7
|
+
require 'trans_forms/nested_forms'
|
8
|
+
require 'trans_forms/main_model'
|
9
|
+
require 'trans_forms/main_model/proxy'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module TransForms
|
4
|
+
module Callbacks
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :_tf_cb
|
9
|
+
end
|
10
|
+
|
11
|
+
def after_save_on_error_callback(active_record_error)
|
12
|
+
if _tf_cb && _tf_cb[:after_save_on_error].is_a?(Array)
|
13
|
+
_tf_cb[:after_save_on_error].each do |method|
|
14
|
+
send method, active_record_error
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
def after_save_on_error(*args)
|
22
|
+
self._tf_cb ||= {}
|
23
|
+
_tf_cb[:after_save_on_error] ||= []
|
24
|
+
_tf_cb[:after_save_on_error] += args
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
|
2
|
+
require 'active_record/errors'
|
3
|
+
module TransForms
|
4
|
+
|
5
|
+
class NotImplementedError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# Custom error class to distinguish from regular ActiveRecord not found errors, but
|
9
|
+
# will trigger the same effect when raised within a transaction.
|
10
|
+
class NotFoundFromError < ActiveRecord::RecordNotFound
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
# TODO: Determine the specific modules that needs to be loaded instead of rails/all
|
4
|
+
require 'rails/all'
|
5
|
+
|
6
|
+
module TransForms
|
7
|
+
class FormBase
|
8
|
+
include Virtus.model
|
9
|
+
|
10
|
+
extend ActiveModel::Naming
|
11
|
+
include ActiveModel::Conversion
|
12
|
+
include ActiveModel::Validations
|
13
|
+
include ActiveModel::Validations::Callbacks
|
14
|
+
|
15
|
+
include TransForms::Callbacks
|
16
|
+
|
17
|
+
def save
|
18
|
+
valid? && run_transaction
|
19
|
+
end
|
20
|
+
|
21
|
+
def save!
|
22
|
+
save || (raise ActiveRecord::RecordInvalid)
|
23
|
+
end
|
24
|
+
|
25
|
+
# ActiveModel support
|
26
|
+
def persisted?; false end
|
27
|
+
def to_key; nil end
|
28
|
+
|
29
|
+
protected
|
30
|
+
def self.transaction(&block)
|
31
|
+
class_attribute :_transaction
|
32
|
+
self._transaction = block
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_transaction
|
36
|
+
ActiveRecord::Base.transaction do
|
37
|
+
instance_eval &_transaction
|
38
|
+
true
|
39
|
+
end
|
40
|
+
rescue ActiveRecord::ActiveRecordError => e
|
41
|
+
# Triggers callback
|
42
|
+
after_save_on_error_callback e
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module TransForms
|
2
|
+
class FormErrors
|
3
|
+
attr_accessor :form_model, :original
|
4
|
+
|
5
|
+
def initialize(form_model, original)
|
6
|
+
self.form_model, self.original = form_model, original
|
7
|
+
end
|
8
|
+
|
9
|
+
def error_models
|
10
|
+
[original, form_model.main_instance.try(:errors)].compact
|
11
|
+
end
|
12
|
+
|
13
|
+
def full_messages
|
14
|
+
original.full_messages
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear
|
18
|
+
error_models.each do |em|
|
19
|
+
em.clear
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](key)
|
24
|
+
error_models.inject([]) { |acc, error_model|
|
25
|
+
acc += (error_model[key])
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def empty?
|
30
|
+
error_models.all?(&:empty?)
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(m, *args, &block)
|
34
|
+
original.send(m, *args, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|