form_objects 1.0.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.
- 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
|