trans_forms 0.1.0

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 (43) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +22 -0
  7. data/README.md +88 -0
  8. data/Rakefile +27 -0
  9. data/lib/generators/rails/setup_generator.rb +14 -0
  10. data/lib/generators/rails/templates/application_trans_form.rb +13 -0
  11. data/lib/generators/rails/templates/trans_form.rb +31 -0
  12. data/lib/generators/rails/trans_form_generator.rb +36 -0
  13. data/lib/generators/rspec/setup_generator.rb +9 -0
  14. data/lib/generators/rspec/templates/application_trans_form_spec.rb +4 -0
  15. data/lib/generators/rspec/templates/trans_form_spec.rb +4 -0
  16. data/lib/generators/rspec/trans_form_generator.rb +9 -0
  17. data/lib/trans_forms.rb +9 -0
  18. data/lib/trans_forms/callbacks.rb +30 -0
  19. data/lib/trans_forms/errors.rb +13 -0
  20. data/lib/trans_forms/form_base.rb +47 -0
  21. data/lib/trans_forms/form_errors.rb +38 -0
  22. data/lib/trans_forms/main_model.rb +115 -0
  23. data/lib/trans_forms/main_model/proxy.rb +38 -0
  24. data/lib/trans_forms/nested_forms.rb +69 -0
  25. data/lib/trans_forms/version.rb +3 -0
  26. data/spec/generators/trans_form/trans_form_generator_spec.rb +8 -0
  27. data/spec/spec_helper.rb +38 -0
  28. data/spec/support/models.rb +12 -0
  29. data/spec/support/schema.rb +23 -0
  30. data/spec/support/trans_forms/callback_forms.rb +22 -0
  31. data/spec/support/trans_forms/main_model_forms.rb +27 -0
  32. data/spec/support/trans_forms/multiple_records_forms.rb +31 -0
  33. data/spec/support/trans_forms/nested_forms.rb +26 -0
  34. data/spec/support/trans_forms/simple_forms.rb +13 -0
  35. data/spec/support/trans_forms/user_creator_1.rb +13 -0
  36. data/spec/trans_forms/callbacks_spec.rb +27 -0
  37. data/spec/trans_forms/form_base_spec.rb +70 -0
  38. data/spec/trans_forms/form_errors_spec.rb +14 -0
  39. data/spec/trans_forms/main_model/proxy_spec.rb +15 -0
  40. data/spec/trans_forms/main_model_spec.rb +49 -0
  41. data/spec/trans_forms/nested_forms_spec.rb +53 -0
  42. data/trans_forms.gemspec +29 -0
  43. metadata +214 -0
@@ -0,0 +1,115 @@
1
+ module TransForms
2
+ module MainModel
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+
7
+ # This method will extend the BaseForm functionality with the
8
+ # TransForms::MainModel::Active module.
9
+ def set_main_model(model, options = {})
10
+ include TransForms::MainModel::Active
11
+
12
+ # Stores the main_model record in a class_attribute
13
+ class_attribute :main_model
14
+ self.main_model = model
15
+
16
+ # Defines an instance accessor for the main_model
17
+ attr_accessor model
18
+
19
+ # Implements proxy module that overwrites model_name method
20
+ # to instead return an ActiveModel::Mame class for the
21
+ # main_model class
22
+ include TransForms::MainModel::Proxy if options[:proxy]
23
+ end
24
+
25
+ end
26
+
27
+ module Active
28
+ extend ActiveSupport::Concern
29
+
30
+ included do
31
+ # Since the after_save_on_error is not part of this module, we make sure
32
+ # that the methods exist before calling it. It should not be necessary since
33
+ # the Callbacks module is included by default in the FormBase, but added
34
+ # anyway. If this module is included manually (like in one of the specs) then
35
+ # it prevents any error to occur.
36
+ after_save_on_error :assert_record_on_error if respond_to?(:after_save_on_error)
37
+ end
38
+
39
+ # Called from FormError to collect error messages from all possible
40
+ # models involved in the form transation.
41
+ def main_instance
42
+ send main_model
43
+ end
44
+
45
+ # Combines the errors from the FormModel as well as the main model instances
46
+ def errors
47
+ @errors ||= FormErrors.new(self, super)
48
+ end
49
+
50
+ # In it's default implementation, this method will look at the class name
51
+ # and try to match it to an attribute defined in the form model. If a match
52
+ # is found, then it will assign the model to that attribute.
53
+ #
54
+ # This method is encouraged to overwrite when there is custom conditions
55
+ # for how to handle the assignments.
56
+ #
57
+ # For example:
58
+ #
59
+ # class UserUpdater < ApplicationTransForm
60
+ # attr_accessor :admin
61
+ # set_main_model :user
62
+ #
63
+ # def model=(model)
64
+ # if model.role == :admin
65
+ # self.admin = model
66
+ # else
67
+ # self.user = model
68
+ # end
69
+ # end
70
+ #
71
+ # # ...
72
+ # end
73
+ #
74
+ def model=(model)
75
+ element = to_element(model)
76
+ if respond_to?("#{element}=")
77
+ send("#{element}=", model)
78
+ else
79
+ raise TransForms::NotImplementedError
80
+ end
81
+ end
82
+
83
+ protected
84
+
85
+ # This method is assigned to the after_save_on_error callback
86
+ #
87
+ # If an error was raised in a create! statement then the variable might not
88
+ # have been assigned and would prevent error messages to be displayed. This
89
+ # makes sure that the instance that raised the error will be assigned if it
90
+ # would have been the main model.
91
+ def assert_record_on_error(e)
92
+ if e.respond_to?(:record) && e.record.present?
93
+ element = to_element(e.record)
94
+ if main_model == element.to_sym && send(element).nil?
95
+ send("#{element}=", e.record)
96
+ end
97
+ end
98
+ end
99
+
100
+ # A method to retrieve the underscored version of a record's class name.
101
+ # The procedure is slightly different in Rails 3 and Rails 4, that's what
102
+ # this method takes into consideration.
103
+ def to_element(record)
104
+ if record.class.model_name.is_a?(ActiveModel::Name)
105
+ record.class.model_name.element
106
+ else
107
+ record.class.model_name.underscore
108
+ end
109
+ end
110
+
111
+ end
112
+ end
113
+ end
114
+
115
+ TransForms::FormBase.send(:include, TransForms::MainModel)
@@ -0,0 +1,38 @@
1
+ module TransForms
2
+ module MainModel
3
+ module Proxy
4
+ extend ActiveSupport::Concern
5
+ # A module that adds functionality to delegate certain
6
+ # methods to the main model. This is done to make forms
7
+ # and controllers handle the form model as if it were
8
+ # the main model itself
9
+
10
+ module ClassMethods
11
+
12
+ # Returns an ActiveModel::Name object for module. It can be
13
+ # used to retrieve all kinds of naming-related information
14
+ # (See ActiveModel::Name for more information).
15
+ #
16
+ # class PostForm < TransForms::FormBase
17
+ # set_main_model :post, proxy: true
18
+ # end
19
+ #
20
+ # PostForm.model_name # => Post
21
+ # PostForm.model_name.class # => ActiveModel::Name
22
+ # PostForm.model_name.singular # => "post"
23
+ # PostForm.model_name.plural # => "posts"
24
+ def model_name
25
+ @_model_name ||= begin
26
+ klass = respond_to?(:main_model) ? main_model.to_s.classify.constantize : self
27
+
28
+ namespace = klass.parents.detect do |n|
29
+ n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
30
+ end
31
+ ActiveModel::Name.new(klass, namespace)
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,69 @@
1
+ module TransForms
2
+ module NestedForms
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+
7
+ # Call this method from any Form class that uses a Hash attribute to
8
+ # process nested form models. It will include a number of methods
9
+ # to more efficiently go through params and records.
10
+ def has_nested_forms
11
+ include TransForms::NestedForms::Active
12
+ end
13
+
14
+ end
15
+
16
+ module Active
17
+ extend ActiveSupport::Concern
18
+
19
+ protected
20
+
21
+ # A method that can be used to iterate through a Hash value that is
22
+ # structured the way Rails form helper +fields_for+ will create.
23
+ #
24
+ # Example:
25
+ #
26
+ # # Given the following Hash:
27
+ # attr = { '0' => { name: 'John' }, '0' => { name: 'Peter' } }
28
+ #
29
+ # # It can be processed like this:
30
+ # each_nested_hash_for attr do |nested_hash|
31
+ # name = nested_hash['name']
32
+ # # Do something with the name, like create multiple Contact
33
+ # # records
34
+ # end
35
+ #
36
+ def each_nested_hash_for(attr, &block)
37
+ if attr.is_a?(Hash)
38
+ attr.values.each do |v|
39
+ if v.is_a?(Hash)
40
+ block.call(v.stringify_keys)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ # A method that can be used to make sure that there are at least
47
+ # one nested hash that has a present value. Useful for validating
48
+ # an attribute when there needs to be at least one association
49
+ # created together with a main record.
50
+ def any_nested_hash_for?(attr)
51
+ attr.is_a?(Hash) && attr.values.detect { |v|
52
+ v.is_a?(Hash) && v.values.detect(&:present?).present?
53
+ }.present?
54
+ end
55
+
56
+ def find_from!(collection, identifier, find_by = :id)
57
+ if identifier.present?
58
+ collection.detect { |instance| instance.send(find_by) == identifier.to_i } || (raise NotFoundFromError)
59
+ else
60
+ raise NotFoundFromError
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+ end
68
+
69
+ TransForms::FormBase.send(:include, TransForms::NestedForms)
@@ -0,0 +1,3 @@
1
+ module TransForms
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+ require 'rails'
3
+ require 'rails/generators'
4
+ require 'generators/rails/trans_form_generator'
5
+
6
+ describe Rails::Generators::TransFormGenerator do
7
+
8
+ end
@@ -0,0 +1,38 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ require 'bundler/setup'
9
+ require 'trans_forms'
10
+ require 'rails/version'
11
+ require 'support/schema'
12
+ require 'support/models'
13
+ require 'database_cleaner'
14
+
15
+ Dir[File.expand_path('../support/trans_forms/*.rb', __FILE__)].each { |f| require f }
16
+
17
+ I18n.enforce_available_locales = false
18
+
19
+ RSpec.configure do |config|
20
+ config.treat_symbols_as_metadata_keys_with_true_values = true
21
+ config.run_all_when_everything_filtered = true
22
+ config.filter_run :focus
23
+
24
+ config.before(:each) do
25
+ DatabaseCleaner.strategy = :transaction
26
+ DatabaseCleaner.start
27
+ end
28
+
29
+ config.after(:each) do
30
+ DatabaseCleaner.clean
31
+ end
32
+
33
+ # Run specs in random order to surface order dependencies. If you find an
34
+ # order dependency and want to debug it, you can fix the order by providing
35
+ # the seed, which is printed after each run.
36
+ # --seed 1234
37
+ config.order = 'random'
38
+ end
@@ -0,0 +1,12 @@
1
+ class User < ActiveRecord::Base
2
+ has_many :phone_numbers, dependent: :destroy
3
+
4
+ validates :name, presence: true, uniqueness: true
5
+ end
6
+
7
+ class PhoneNumber < ActiveRecord::Base
8
+ belongs_to :user
9
+
10
+ validates :user_id, presence: true
11
+ validates :number, presence: true, uniqueness: { scope: :user_id }
12
+ end
@@ -0,0 +1,23 @@
1
+ require 'active_record'
2
+
3
+ ActiveRecord::Base.establish_connection({
4
+ adapter: 'sqlite3',
5
+ database: ':memory:'
6
+ })
7
+
8
+ # prepare test data
9
+ class CreateTestSchema < ActiveRecord::Migration
10
+ def change
11
+ create_table "users", :force => true do |t|
12
+ t.string "name"
13
+ t.integer "age"
14
+ end
15
+
16
+ create_table "phone_numbers", :force => true do |t|
17
+ t.integer "user_id"
18
+ t.string "number"
19
+ end
20
+ end
21
+ end
22
+
23
+ CreateTestSchema.migrate(:up)
@@ -0,0 +1,22 @@
1
+ class CallbackForm < TransForms::FormBase
2
+ attr_reader :user1, :user2, :i_was_called
3
+
4
+ attribute :name1, String
5
+ attribute :name2, String
6
+
7
+ validates :name1, presence: true
8
+ validates :name2, presence: true
9
+
10
+ after_save_on_error :call_me
11
+
12
+ transaction do
13
+ @user1 = User.create!(name: name1)
14
+ @user2 = User.create!(name: name2)
15
+ end
16
+
17
+ protected
18
+ def call_me(error)
19
+ @i_was_called = true
20
+ end
21
+
22
+ end
@@ -0,0 +1,27 @@
1
+ class MainModelModel
2
+ include TransForms::MainModel
3
+ end
4
+
5
+ class ProxyModel
6
+ class_attribute :main_model
7
+ self.main_model = :user
8
+ include TransForms::MainModel::Proxy
9
+ end
10
+
11
+ class UserUpdater < TransForms::FormBase
12
+ set_main_model :user
13
+
14
+ attribute :name, String
15
+
16
+ validates :name, presence: true
17
+
18
+ transaction do
19
+ user.attributes = { name: name }
20
+ user.save!
21
+ end
22
+
23
+ end
24
+
25
+ class UserProxyModel < TransForms::FormBase
26
+ set_main_model :user, proxy: true
27
+ end
@@ -0,0 +1,31 @@
1
+ class MultipleRecordsCreator < TransForms::FormBase
2
+ attr_reader :user1, :user2
3
+
4
+ attribute :name1, String
5
+ attribute :name2, String
6
+
7
+ validates :name1, presence: true
8
+ validates :name2, presence: true
9
+
10
+ transaction do
11
+ @user1 = User.create!(name: name1)
12
+ @user2 = User.create!(name: name2)
13
+ end
14
+
15
+ end
16
+
17
+ class MultipleRecordsCreatorNoBang < TransForms::FormBase
18
+ attr_reader :user1, :user2
19
+
20
+ attribute :name1, String
21
+ attribute :name2, String
22
+
23
+ validates :name1, presence: true
24
+ validates :name2, presence: true
25
+
26
+ transaction do
27
+ @user1 = User.create(name: name1)
28
+ @user2 = User.create(name: name2)
29
+ end
30
+
31
+ end
@@ -0,0 +1,26 @@
1
+ class NestedFormsModel
2
+ include TransForms::NestedForms::Active
3
+
4
+ # Makes the methods public for easier Unit testing
5
+ public :each_nested_hash_for, :any_nested_hash_for?, :find_from!
6
+ end
7
+
8
+ class UserAndPoneNumbersCreator < TransForms::FormBase
9
+ has_nested_forms
10
+
11
+ attr_reader :user
12
+
13
+ attribute :name, String
14
+ attribute :phone_numbers_attributes, Hash
15
+
16
+ validates :name, presence: true
17
+
18
+ transaction do
19
+ @user = User.create!(name: name)
20
+
21
+ each_nested_hash_for phone_numbers_attributes do |attr|
22
+ @user.phone_numbers.create!(attr)
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,13 @@
1
+ class NoErrorInTransactionForm < TransForms::FormBase
2
+ transaction do
3
+ # Bunch of actions
4
+ 1 + 1; 'No errors'; [1,2,3].map(&:to_s)
5
+ false # Can return false, save will be success anyway
6
+ end
7
+ end
8
+
9
+ class ErrorInTransactionForm < TransForms::FormBase
10
+ transaction do
11
+ raise ActiveRecord::ActiveRecordError
12
+ end
13
+ end