form_objects 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+
6
+ task default: :spec
@@ -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
+ require 'active_support/concern'
2
+
3
+ module FormObjects
4
+ module Naming
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ def self.model_name
9
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.gsub(/Form\Z/, ''))
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,3 @@
1
+ module FormObjects
2
+ VERSION = "1.0.0"
3
+ end
@@ -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
@@ -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