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