form_objects 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +11 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +524 -0
- data/Rakefile +6 -0
- data/form_objects.gemspec +27 -0
- data/lib/form_objects/associated_validator.rb +9 -0
- data/lib/form_objects/base.rb +33 -0
- data/lib/form_objects/naming.rb +13 -0
- data/lib/form_objects/nesting.rb +13 -0
- data/lib/form_objects/params_converter/collection_converter.rb +51 -0
- data/lib/form_objects/params_converter/date_converter.rb +41 -0
- data/lib/form_objects/params_converter.rb +18 -0
- data/lib/form_objects/serializer.rb +18 -0
- data/lib/form_objects/version.rb +3 -0
- data/lib/form_objects.rb +11 -0
- data/spec/base_spec.rb +97 -0
- data/spec/naming_spec.rb +14 -0
- data/spec/nesting_spec.rb +88 -0
- data/spec/params_converter_spec.rb +91 -0
- data/spec/serializer_spec.rb +75 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/examples.rb +44 -0
- metadata +148 -0
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'form_objects/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "form_objects"
|
8
|
+
spec.version = FormObjects::VERSION
|
9
|
+
spec.authors = ["Piotr Nielacny", "Przemek Lusar"]
|
10
|
+
spec.email = ["piotr.nielacny@gmail.com", "przemyslaw.lusar@gmail.com"]
|
11
|
+
spec.description = %q{Micro library for creating and managing complex forms}
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = "https://github.com/lluzak/form_objects"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
|
24
|
+
spec.add_dependency "virtus", "~> 1.0"
|
25
|
+
spec.add_dependency "activemodel", ">= 3.2"
|
26
|
+
spec.add_dependency "activesupport", ">= 3.2"
|
27
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module FormObjects
|
2
|
+
class AssociatedValidator < ActiveModel::EachValidator
|
3
|
+
def validate_each(record, attribute, value)
|
4
|
+
if Array[value].flatten.reject { |r| r.valid? }.any?
|
5
|
+
record.errors.add(attribute, :invalid, options.merge(:value => value))
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module FormObjects
|
2
|
+
class Base
|
3
|
+
include Virtus.model
|
4
|
+
include Serializer
|
5
|
+
extend Nesting
|
6
|
+
|
7
|
+
if ActiveModel::VERSION::MAJOR > 3
|
8
|
+
include ActiveModel::Model
|
9
|
+
else
|
10
|
+
include ActiveModel::Validations
|
11
|
+
include ActiveModel::Conversion
|
12
|
+
extend ActiveModel::Naming
|
13
|
+
end
|
14
|
+
|
15
|
+
def as_json
|
16
|
+
to_hash.as_json
|
17
|
+
end
|
18
|
+
|
19
|
+
alias :update :attributes=
|
20
|
+
|
21
|
+
class << self
|
22
|
+
alias_method :field, :attribute
|
23
|
+
end
|
24
|
+
|
25
|
+
def persisted?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.validates_associated(*attr_names)
|
30
|
+
validates_with AssociatedValidator, _merge_attributes(attr_names)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module FormObjects
|
2
|
+
module Nesting
|
3
|
+
def nested_form(attribute, form, options = {})
|
4
|
+
attribute(attribute, form, options)
|
5
|
+
validates_associated(attribute)
|
6
|
+
define_nested_writer_method(attribute)
|
7
|
+
end
|
8
|
+
|
9
|
+
def define_nested_writer_method(method_name)
|
10
|
+
alias_method :"#{method_name}_attributes=", :"#{method_name}="
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module FormObjects
|
2
|
+
class ParamsConverter
|
3
|
+
class CollectionConverter
|
4
|
+
def initialize(params)
|
5
|
+
@params = params
|
6
|
+
end
|
7
|
+
|
8
|
+
def params
|
9
|
+
convert_attributes_to_array(@params)
|
10
|
+
end
|
11
|
+
|
12
|
+
def convert_attributes_to_array(object)
|
13
|
+
return object unless object.respond_to?(:each_pair)
|
14
|
+
|
15
|
+
object.inject({}) { |hash, attributes|
|
16
|
+
key, value = attributes.first, attributes.last
|
17
|
+
value = value.to_a.sort.map { |attributes| attributes.last } if candidate_for_conversion?(key, value)
|
18
|
+
hash[key] = convert_attributes_to_array(value)
|
19
|
+
|
20
|
+
hash
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def candidate_for_conversion?(key, value)
|
27
|
+
attribute_key?(key) and value.is_a?(Hash) and incrementing_sequence?(value.keys)
|
28
|
+
rescue ArgumentError
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_sequence_from(keys)
|
33
|
+
keys.map { |key| Integer(key) }.sort
|
34
|
+
end
|
35
|
+
|
36
|
+
def sequence_to(max)
|
37
|
+
(0..max).to_a
|
38
|
+
end
|
39
|
+
|
40
|
+
def incrementing_sequence?(keys)
|
41
|
+
sequence = generate_sequence_from(keys)
|
42
|
+
sequence == sequence_to(sequence.max)
|
43
|
+
end
|
44
|
+
|
45
|
+
def attribute_key?(key)
|
46
|
+
key =~ /_attributes$/
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module FormObjects
|
2
|
+
class ParamsConverter
|
3
|
+
class DateConverter
|
4
|
+
DATE_ATTRIBUTES = /^(\w+)\(.i\)$/
|
5
|
+
DATE_FORMAT = "%s.%s.%s %s:%s:%s".freeze
|
6
|
+
|
7
|
+
def initialize(params)
|
8
|
+
@params = params
|
9
|
+
end
|
10
|
+
|
11
|
+
def params
|
12
|
+
convert_attributes_to_date(@params)
|
13
|
+
end
|
14
|
+
|
15
|
+
def convert_attributes_to_date(object)
|
16
|
+
return object unless object.respond_to?(:each_pair)
|
17
|
+
|
18
|
+
object.inject({}) { |hash, attributes|
|
19
|
+
key, value = attributes.first, attributes.last
|
20
|
+
attribute = date_attribute_name_for(key)
|
21
|
+
hash[attribute] = DATE_FORMAT % date_values_for(key, object) if candidate_for_date_conversion?(key)
|
22
|
+
hash[key] = convert_attributes_to_date(value)
|
23
|
+
|
24
|
+
hash
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def date_attribute_name_for(key)
|
29
|
+
key[DATE_ATTRIBUTES, 1]
|
30
|
+
end
|
31
|
+
|
32
|
+
def date_values_for(key, object)
|
33
|
+
(1..6).map { |value| "#{object.delete("#{date_attribute_name_for(key)}(#{value}i)") { "00" }}" }
|
34
|
+
end
|
35
|
+
|
36
|
+
def candidate_for_date_conversion?(key)
|
37
|
+
date_attribute_name_for(key)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
require 'form_objects/params_converter/date_converter'
|
3
|
+
require 'form_objects/params_converter/collection_converter'
|
4
|
+
|
5
|
+
module FormObjects
|
6
|
+
class ParamsConverter
|
7
|
+
def initialize(params)
|
8
|
+
@params = params
|
9
|
+
end
|
10
|
+
|
11
|
+
def params
|
12
|
+
params = CollectionConverter.new(@params).params
|
13
|
+
params = DateConverter.new(params).params
|
14
|
+
|
15
|
+
HashWithIndifferentAccess.new(params)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module FormObjects
|
2
|
+
module Serializer
|
3
|
+
|
4
|
+
def serialized_attributes
|
5
|
+
(attributes || {}).inject({}) do |hash, (name, value)|
|
6
|
+
hash[name] = value.is_a?(Array) ? value.map { |item| serialize(item) } : serialize(value)
|
7
|
+
hash
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def serialize(value)
|
14
|
+
value.respond_to?(:serialized_attributes) ? value.serialized_attributes : value
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/lib/form_objects.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "virtus"
|
2
|
+
require "active_model"
|
3
|
+
require "active_support/json"
|
4
|
+
|
5
|
+
require "form_objects/version"
|
6
|
+
require "form_objects/serializer"
|
7
|
+
require "form_objects/associated_validator"
|
8
|
+
require "form_objects/nesting"
|
9
|
+
require "form_objects/naming"
|
10
|
+
require "form_objects/base"
|
11
|
+
require "form_objects/params_converter"
|
data/spec/base_spec.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FormObjects::Base do
|
4
|
+
|
5
|
+
describe "#validates_associated" do
|
6
|
+
let(:klass) do
|
7
|
+
Class.new(described_class) do
|
8
|
+
nested_form :addresses, AddressForm
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it "creates validator instance inside #validators array" do
|
13
|
+
klass.validators.should_not be_empty
|
14
|
+
end
|
15
|
+
|
16
|
+
it "creates instance of AssociatedValidator" do
|
17
|
+
klass.validators.any? { |validator| validator.should be_kind_of(FormObjects::AssociatedValidator) }.should be_truthy
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'includes Virtus Core module' do
|
22
|
+
described_class.included_modules.should include Virtus::Model::Core
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'includes Serializer module ' do
|
26
|
+
described_class.included_modules.should include FormObjects::Serializer
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'when ActiveModel major version is above 3' do
|
30
|
+
it 'includes ActiveModel::Model module' do
|
31
|
+
described_class.included_modules.should include ActiveModel::Model
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'when ActiveModel major version is lower than or equal to 3' do
|
36
|
+
|
37
|
+
it 'includes ActiveModel Validations module' do
|
38
|
+
described_class.included_modules.should include ActiveModel::Validations
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'includes ActiveModel Conversion module' do
|
42
|
+
described_class.included_modules.should include ActiveModel::Conversion
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'extends itself by ActiveModel Naming module' do
|
46
|
+
described_class.singleton_class.included_modules.should include ActiveModel::Naming
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#persisted?' do
|
52
|
+
it 'always returns false' do
|
53
|
+
subject.persisted?.should == false
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#as_json" do
|
58
|
+
let(:form) do
|
59
|
+
Class.new(described_class) do
|
60
|
+
nested_form :addresses, Array[AddressForm]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
subject { form.new(:addresses => [{ :street => "Kazimierza" } ]) }
|
65
|
+
|
66
|
+
it 'returns hash of nested forms' do
|
67
|
+
subject.as_json.should == { "addresses" => [ { "street" => "Kazimierza", "city" => nil }] }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe "#update" do
|
72
|
+
let(:form) do
|
73
|
+
Class.new(described_class) do
|
74
|
+
attribute :addresses, Array[AddressForm]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
subject { form.new }
|
79
|
+
|
80
|
+
it "updates attributes for addresses" do
|
81
|
+
subject.update(:addresses => [{ :street => "Kazimierza" }])
|
82
|
+
subject.addresses.first.street.should == "Kazimierza"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "#field" do
|
87
|
+
let(:form) do
|
88
|
+
Class.new(described_class) do
|
89
|
+
field :addresses, AddressForm
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'setup attribute for form' do
|
94
|
+
form.new.should respond_to(:addresses)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/spec/naming_spec.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FormObjects::Naming do
|
4
|
+
before do
|
5
|
+
Object.send(:remove_const, 'MessageForm')
|
6
|
+
load 'support/examples.rb'
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#model_name' do
|
10
|
+
it 'returns Message' do
|
11
|
+
MessageForm.model_name.to_s.should == 'Message'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FormObjects::Nesting do
|
4
|
+
before(:each) do
|
5
|
+
Object.send(:remove_const, 'UserForm')
|
6
|
+
load 'support/examples.rb'
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'define #nested_form method' do
|
10
|
+
FormObjects::Base.methods.include?(:nested_form).should be_truthy
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#nested_form"
|
14
|
+
before do
|
15
|
+
UserForm.nested_form(:personal_info, PersonalInfoForm)
|
16
|
+
end
|
17
|
+
|
18
|
+
subject { UserForm.new }
|
19
|
+
|
20
|
+
it 'defined array attribute' do
|
21
|
+
subject.personal_info = PersonalInfoForm.new
|
22
|
+
subject.personal_info.should be_kind_of(PersonalInfoForm)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'defines writer method for attributes' do
|
26
|
+
subject.methods.include?(:personal_info_attributes=).should be_truthy
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'nested writer method' do
|
31
|
+
before do
|
32
|
+
UserForm.nested_form(:personal_info, PersonalInfoForm)
|
33
|
+
UserForm.nested_form(:addresses, Array[AddressForm])
|
34
|
+
end
|
35
|
+
|
36
|
+
let(:addresses_data) { [{street: 'Diagon Alley', city: 'London'}] }
|
37
|
+
|
38
|
+
subject { UserForm.new }
|
39
|
+
|
40
|
+
it 'can mass-assign attributes to PersonalInfoForm' do
|
41
|
+
subject.personal_info_attributes = { :first_name => "Piotr" }
|
42
|
+
subject.personal_info.first_name.should == "Piotr"
|
43
|
+
end
|
44
|
+
|
45
|
+
it '#*_attributes method should set data to corresponding object' do
|
46
|
+
subject.addresses_attributes = addresses_data
|
47
|
+
subject.addresses.map(&:attributes).should == addresses_data
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'validates nested forms' do
|
52
|
+
let(:address) { AddressForm.new(city: 'London') }
|
53
|
+
let(:secondary_address) { AddressForm.new(street: 'Privet Drive', city: 'Little Whinging Alley') }
|
54
|
+
let(:personal_info) { PersonalInfoForm.new(first_name: '', last_name: 'Granger') }
|
55
|
+
|
56
|
+
subject { UserForm.new }
|
57
|
+
|
58
|
+
before do
|
59
|
+
UserForm.clear_validators!
|
60
|
+
UserForm.nested_form(:addresses, Array[AddressForm])
|
61
|
+
UserForm.nested_form(:personal_info, PersonalInfoForm)
|
62
|
+
|
63
|
+
subject.addresses = [address, secondary_address]
|
64
|
+
subject.personal_info = personal_info
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'validate nested form' do
|
68
|
+
subject.personal_info = PersonalInfoForm.new
|
69
|
+
subject.valid?.should be_falsey
|
70
|
+
|
71
|
+
subject.personal_info.errors.messages.should include :first_name
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'calls #valid? on each nested forms objects' do
|
75
|
+
address.should_receive(:valid?)
|
76
|
+
secondary_address.should_receive(:valid?)
|
77
|
+
personal_info.should_receive(:valid?)
|
78
|
+
|
79
|
+
subject.valid?
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'add errors into parent object' do
|
83
|
+
subject.valid?
|
84
|
+
|
85
|
+
subject.errors.messages.should include :addresses
|
86
|
+
subject.errors.messages.should include :personal_info
|
87
|
+
end
|
88
|
+
end
|