form_model 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in form_model.gemspec
4
+ gemspec
5
+
6
+ gem "pry"
7
+ gem "simplecov", :require => false
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 hookercookerman
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,156 @@
1
+ # FormModel
2
+
3
+ Idea is to have an easy way to create form objects;
4
+
5
+ Inspiration
6
+ [http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/](Code Climate Extract From Objects)
7
+
8
+ What are form objects
9
+
10
+ 1. They can validate your form inputs; (leaving your model to validate
11
+ domain validations)
12
+
13
+ 2. hide access to the model; only form attributes are allowed to be
14
+ updated (mass assignment anyone)
15
+
16
+ 3. have a single place to show any transformation that the form needs to
17
+ represent from the model; Example Price Hashes; Time Strings;
18
+ locations and value objects
19
+
20
+ 4. Essentially you now have 1 place to do any form stuff
21
+
22
+ At present this is tied to mongoid; if you want other support I accept
23
+ pull requests :)
24
+
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ gem 'form_model'
31
+
32
+ And then execute:
33
+
34
+ $ bundle
35
+
36
+ Or install it yourself as:
37
+
38
+ $ gem install form_model
39
+
40
+ ## Usage
41
+
42
+ Typical flow In Rails App
43
+
44
+ have a forms directory;
45
+
46
+ Create a Form that binds to a partical model class;
47
+ Define what attributes you want the form to have;
48
+
49
+ ```ruby
50
+ class ProductForm
51
+ include FormModel
52
+ bind_to{Product}
53
+
54
+ # Attributes
55
+ attribute :name, String
56
+ attribute :title, String
57
+ attribute :description, String
58
+ attribute :price, VMoney
59
+ attribute :selected_price, VMoney
60
+
61
+ attribute :start_at_date, String
62
+ attribute :start_at_hr, String
63
+ attribute :start_at_min, String
64
+
65
+ # Mappers
66
+ mapper MoneyMapper, form: {price: :price}
67
+ mapper DateHourMinMapper,
68
+ form: {date: :start_at_date, hour: :start_at_hr, min: :start_at_min},
69
+ model: {time: :start_at}
70
+
71
+ # Validations
72
+ validates :title, presence: true
73
+ validates_associated :price
74
+ end
75
+ ```
76
+
77
+ # Creating a Form Object
78
+
79
+ ```ruby
80
+ product_form = ProductForm.new
81
+ ```
82
+
83
+ As the product form is bound to a product class a new product will be
84
+ associated with this form.
85
+
86
+ # Loading a Form from the model (typical Approach)
87
+
88
+ ```ruby
89
+ product = Product.find(params[:id])
90
+ product_form = ProductForm.load(product)
91
+ ```
92
+
93
+ This will map the model attributes on to the form model; applying any
94
+ specific mappers that have been created for the form object
95
+
96
+ # Saving A Form
97
+
98
+ A form *save* method will use both validations on the form
99
+ model and the model to determine if its valid and a save will also call
100
+ a models save method;
101
+
102
+ ```ruby
103
+ product_form = ProductForm.load(product).update(params[:product])
104
+ if product_form.save
105
+ # do stuff
106
+ else
107
+ # do other stuff
108
+ end
109
+ ```
110
+
111
+ # Updating model Explicity
112
+
113
+ ```ruby
114
+ product_form = ProductForm.new
115
+ product_form.update(params[:product])
116
+ product_form.update_data_model!
117
+ ```
118
+
119
+ updating a form with a hash will only update the form; if you wish to
120
+ set the attributes on the model you will need to call update_data_model!
121
+ *NOTE* this will not save the model
122
+
123
+ # Mappers
124
+
125
+ if you need to transform data in and out of forms and models you can
126
+ create mappers; as shown in the productForm example; look at the
127
+ spec/support mappers for examples
128
+
129
+ ```ruby
130
+ class MoneyMapper
131
+ include FormModel::Mapper
132
+ form_keys :price
133
+ model_keys :price
134
+
135
+ def to_form(model)
136
+ assert_model_values(model, :price) do |values|
137
+ {form_attribute(:price) => VMoney.new(values[:price])}
138
+ end
139
+ end
140
+
141
+ def to_model(form)
142
+ assert_form_values(form, :price) do |values|
143
+ {model_attribute(:price) => values[:price].to_hash.stringify_keys}
144
+ end
145
+ end
146
+ end
147
+ ```
148
+
149
+
150
+ ## Contributing
151
+
152
+ 1. Fork it
153
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
154
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
155
+ 4. Push to the branch (`git push origin my-new-feature`)
156
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'form_model/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "form_model"
8
+ gem.version = FormModel::VERSION
9
+ gem.authors = ["hookercookerman"]
10
+ gem.email = ["hookercookerman@gmail.com"]
11
+ gem.description = %q{A Library to create Form objects to encapsulate forms}
12
+ gem.summary = %q{A Library to create Form objects to encapsulate forms}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'virtus', '>=0.5.0'
21
+ gem.add_dependency "activemodel", ">= 3.0.0"
22
+ gem.add_dependency "activesupport", ">= 3.0.0"
23
+
24
+ gem.add_development_dependency "rspec", "~> 2.11.0"
25
+ gem.add_development_dependency "factory_girl", "~> 4.0.0"
26
+ gem.add_development_dependency "fuubar", "~> 1.0.0"
27
+ gem.add_development_dependency "activemodel", "~> 3.2.8"
28
+ gem.add_development_dependency 'rake'
29
+ end
@@ -0,0 +1,5 @@
1
+ require "form_model/version"
2
+ require "form_model/errors"
3
+ require "form_model/mapper"
4
+ require "form_model/associated_validation"
5
+ require "form_model/model"
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ module FormModel
3
+ class AssociatedValidator < ActiveModel::EachValidator
4
+
5
+ # Validates that the associations provided are either all nil or all
6
+ # valid. If neither is true then the appropriate errors will be added to
7
+ # the parent document.
8
+ #
9
+ # @example Validate the association.
10
+ # validator.validate_each(document, :name, name)
11
+ #
12
+ # @param [ Document ] document The document to validate.
13
+ # @param [ Symbol ] attribute The relation to validate.
14
+ # @param [ Object ] value The value of the relation.
15
+ def validate_each(form, attribute, value)
16
+ begin
17
+ valid = Array.wrap(value).collect do |doc|
18
+ if doc.nil?
19
+ true
20
+ else
21
+ doc.valid?
22
+ end
23
+ end.all?
24
+ end
25
+ form.errors.add(attribute, :invalid, options) unless valid
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,5 @@
1
+ module FormModel
2
+ class Error < StandardError; end
3
+ class ModelMisMatchError < Error; end
4
+ class MapperAssertionError < Error; end
5
+ end
@@ -0,0 +1,117 @@
1
+ require "active_support/concern"
2
+ require "active_model/validations"
3
+
4
+ module FormModel
5
+ module Mapper
6
+ extend ActiveSupport::Concern
7
+ include ActiveModel::Validations
8
+
9
+ included do
10
+ class_attribute :_form_keys
11
+ class_attribute :_model_keys
12
+ self._form_keys = []
13
+ self._model_keys = []
14
+
15
+ attr_accessor :_form_keys_values
16
+ attr_accessor :_model_keys_values
17
+
18
+ def initialize(options = {})
19
+ extract_options!(options)
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ def form_keys(*keys)
25
+ self._form_keys += keys
26
+ end
27
+
28
+ def model_keys(*keys)
29
+ self._model_keys += keys
30
+ end
31
+ end
32
+
33
+ def assert_model_values(model, *values, &block)
34
+ hash = Array(values).inject({}) do |var, key|
35
+ value = model_value(model, key)
36
+ raise FormModel::MapperAssertionError if value.nil?
37
+ var[key] = value
38
+ var
39
+ end
40
+ block.call(hash)
41
+ rescue FormModel::MapperAssertionError
42
+ {}
43
+ end
44
+
45
+ def assert_form_values(form, *values, &block)
46
+ hash = Array(values).inject({}) do |var, key|
47
+ value = form_value(form, key)
48
+ raise FormModel::MapperAssertionError if value.nil?
49
+ var[key] = value
50
+ var
51
+ end
52
+ block.call(hash)
53
+ rescue FormModel::MapperAssertionError
54
+ {}
55
+ end
56
+
57
+ def form_keys
58
+ self.class._form_keys
59
+ end
60
+
61
+ def model_keys
62
+ self.class._model_keys
63
+ end
64
+
65
+ def form_value(form, key)
66
+ if value_key = _form_keys_values[key]
67
+ form.send(value_key)
68
+ end
69
+ end
70
+
71
+ def form_attribute(key)
72
+ _form_keys_values[key].to_s
73
+ end
74
+
75
+ def model_value(model, key)
76
+ if value_key = _model_keys_values[key]
77
+ model.send(value_key)
78
+ end
79
+ end
80
+
81
+ def model_attribute(key)
82
+ _model_keys_values[key].to_s
83
+ end
84
+
85
+ def to_form(attributes)
86
+ {}
87
+ end
88
+
89
+ def to_model(attribute)
90
+ {}
91
+ end
92
+
93
+ private
94
+ def extract_options!(options)
95
+ form_options = options[:form] || options[:model] || {}
96
+ model_options = options[:model] || options[:form] || {}
97
+ extract_form_options!(form_options)
98
+ extract_model_options!(model_options)
99
+ check_have_all_values
100
+ end
101
+
102
+ def check_have_all_values
103
+ true
104
+ end
105
+
106
+ def extract_form_options!(options)
107
+ raise Exception.new unless options.keys.all?{|key| _form_keys.include?(key)}
108
+ @_form_keys_values = options
109
+ end
110
+
111
+ def extract_model_options!(options)
112
+ raise Exception.new unless options.keys.all?{|key| _model_keys.include?(key)}
113
+ @_model_keys_values = options
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,174 @@
1
+ require "active_model"
2
+ require "virtus"
3
+ require "active_support/concern"
4
+ require 'active_support/core_ext/class/attribute_accessors'
5
+ require 'active_support/core_ext/hash/slice'
6
+ require 'active_support/core_ext/hash/keys'
7
+
8
+ module FormModel
9
+ include Virtus
10
+ extend ActiveSupport::Concern
11
+ include ActiveModel::Validations
12
+
13
+ included do
14
+ class_attribute :after_read_block
15
+ class_attribute :before_write_block
16
+ class_attribute :bound_block
17
+ class_attribute :mappers
18
+ self.mappers = []
19
+
20
+ attr_accessor :data_model
21
+ alias :model= :data_model=
22
+ alias :model :data_model
23
+
24
+ def initialize(model, attributes = nil)
25
+ if model.is_a?(Hash)
26
+ super(model)
27
+ else
28
+ super(attributes)
29
+ @data_model = model
30
+ assert_correct_model
31
+ apply_mappers_to_form!
32
+ end
33
+ end
34
+ end
35
+
36
+ module ClassMethods
37
+ def validates_associated(*args)
38
+ validates_with(FormModel::AssociatedValidator, _merge_attributes(args))
39
+ end
40
+ end
41
+
42
+ def valid?(type = :model_and_form)
43
+ return super if type == :form
44
+ update_data_model!
45
+ ok = (super and data_model.valid?)
46
+ merge_data_model_errors! unless ok
47
+ ok
48
+ end
49
+
50
+ def merge_errors_with!(object)
51
+ object.errors.to_hash.each do |key, value|
52
+ self.errors.add(key, value)
53
+ end
54
+ end
55
+
56
+ def bound_class
57
+ self.class.bound_class
58
+ end
59
+
60
+ def save
61
+ valid? ? data_model.save : false
62
+ end
63
+
64
+ def form_valid?
65
+ valid?(:form)
66
+ end
67
+
68
+ def update(attrs = {})
69
+ self.attributes = attrs || {}
70
+ self
71
+ end
72
+
73
+ def persisted?
74
+ data_model.persisted?
75
+ end
76
+
77
+ def to_model
78
+ data_model.to_model
79
+ end
80
+
81
+ def to_key
82
+ data_model.to_key
83
+ end
84
+
85
+ def to_param
86
+ data_model.to_param
87
+ end
88
+
89
+ def to_path
90
+ data_model.to_path
91
+ end
92
+
93
+ def update_data_model!
94
+ unless data_model
95
+ @data_model = bound_class.new
96
+ end
97
+ attrs = attributes.slice(*data_model_attribute_names).stringify_keys
98
+ apply_mappers_to_model!(attrs)
99
+ self.instance_exec(&before_write_block) unless self.class.before_write_block.nil?
100
+ data_model.write_attributes(attrs)
101
+ data_model
102
+ end
103
+
104
+ def respond_to?(method_sym, include_private = false)
105
+ super || data_model.respond_to?(method_sym, include_private)
106
+ end
107
+
108
+ module ClassMethods
109
+ def bind_to(&block)
110
+ self.bound_block = block
111
+ end
112
+
113
+ def mapper(mapper_class, options = {})
114
+ mappers << mapper_class.new(options)
115
+ end
116
+
117
+ def after_read &block
118
+ self.after_read_block = block
119
+ end
120
+
121
+ def before_write &block
122
+ self.before_write_block = block
123
+ end
124
+
125
+ def bound_class
126
+ @bound_class ||= self.bound_block.call
127
+ end
128
+
129
+ def form_attributes
130
+ self.attribute_set.map{|attr| attr.name.to_s}
131
+ end
132
+
133
+ def load(data_model)
134
+ data_model_attributes = data_model.attributes
135
+ attrs = data_model_attributes.slice(*form_attributes)
136
+ self.new(data_model, attrs).tap do |instance|
137
+ instance.instance_exec(&after_read_block) unless after_read_block.nil?
138
+ end
139
+ end
140
+ end
141
+
142
+ private
143
+ def merge_data_model_errors!
144
+ data_model.errors.to_hash.each do |key, value|
145
+ self.errors.add(key, value)
146
+ end
147
+ end
148
+
149
+ def assert_correct_model
150
+ if !data_model.is_a?(bound_class)
151
+ raise ModelMisMatchError.new("Tried to use object of class #{data_model.class.name} only #{bound_class.name} allowed")
152
+ end
153
+ end
154
+
155
+ def apply_mappers_to_form!
156
+ mappers.each do |mapper|
157
+ self.attributes = self.attributes.merge(mapper.to_form(data_model))
158
+ end
159
+ end
160
+
161
+ def apply_mappers_to_model!(attrs)
162
+ mappers.each do |mapper|
163
+ attrs.merge!(mapper.to_model(self))
164
+ end
165
+ end
166
+
167
+ def method_missing(method, *args, &block)
168
+ data_model.send(method, *args, &block)
169
+ end
170
+
171
+ def data_model_attribute_names
172
+ data_model.fields.keys.map(&:to_sym) + data_model.relations.keys.map(&:to_sym)
173
+ end
174
+ end
@@ -0,0 +1,3 @@
1
+ module FormModel
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,80 @@
1
+ # encoding: utf-8
2
+ require File.expand_path("../spec_helper", __FILE__)
3
+
4
+ describe "FormMode::Mappers" do
5
+ context "new mapper" do
6
+ let(:new_mapper) do
7
+ Class.new do
8
+ include FormModel::Mapper
9
+ end
10
+ end
11
+
12
+ it "should be able to set form keys" do
13
+ new_mapper.should respond_to(:form_keys)
14
+ end
15
+
16
+ it "should be able to set form keys" do
17
+ new_mapper.should respond_to(:model_keys)
18
+ end
19
+ end
20
+
21
+ context "mapper instance with set keys" do
22
+ let(:new_mapper) do
23
+ Class.new do
24
+ include FormModel::Mapper
25
+ form_keys :price
26
+ model_keys :price
27
+ end
28
+ end
29
+ let(:options){{form: {price: :price}}}
30
+ let(:form){double(:price => "form_price")}
31
+ let(:model){double(:price => "model_price")}
32
+
33
+ subject{new_mapper.new(options)}
34
+
35
+ it "should have a form attribute price" do
36
+ subject.form_attribute(:price).should eq("price")
37
+ end
38
+
39
+ it "should have a model attribute price" do
40
+ subject.form_attribute(:price).should eq("price")
41
+ end
42
+
43
+ it "should be able to the form value" do
44
+ subject.form_value(form, :price).should eq("form_price")
45
+ end
46
+
47
+ it "should be able to the model value" do
48
+ subject.model_value(model, :price).should eq("model_price")
49
+ end
50
+
51
+ describe "#asset_form_values" do
52
+ it "should return a filled hash with values are not nil" do
53
+ subject.assert_form_values(form, :price) do |values|
54
+ values.should eq({:price => "form_price"})
55
+ end
56
+ end
57
+
58
+ it "should return empty hash if one of the values is nil" do
59
+ subject.assert_form_values(form, :price, :beans) do |values|
60
+ values.should eq({:price => "form_price"})
61
+ end.should eq({})
62
+ end
63
+ end
64
+
65
+ describe "#asset_model_values" do
66
+ it "should return a filled hash with values are not nil" do
67
+ subject.assert_model_values(model, :price) do |values|
68
+ values.should eq({:price => "model_price"})
69
+ end
70
+ end
71
+
72
+ it "should return empty hash if one of the values is nil" do
73
+ subject.assert_model_values(model, :price, :beans) do |values|
74
+ values.should eq({:price => "model_price"})
75
+ end.should eq({})
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,134 @@
1
+ # encoding: utf-8
2
+ require File.expand_path("../../spec_helper", __FILE__)
3
+
4
+ describe "ProductForm" do
5
+ let(:product) do
6
+ FactoryGirl.build :product
7
+ end
8
+ subject{ProductForm.load(product)}
9
+ before {product.stub(:persisted? => false)}
10
+
11
+ it "should delegate persisted?" do
12
+ product.should_receive(:persisted?)
13
+ subject.persisted?
14
+ end
15
+
16
+ describe "when loading from a new product" do
17
+ let(:product) do
18
+ FactoryGirl.build :product
19
+ end
20
+ subject{ProductForm.load(product)}
21
+ before{subject.stub(data_model_attribute_names: [:price])}
22
+ its(:name){should be_nil}
23
+ its(:title){should be_nil}
24
+ its(:description){should be_nil}
25
+ its(:valid?){should be_false}
26
+ end
27
+
28
+ describe "when loading form with incorrect model type" do
29
+ let(:user) {FactoryGirl.build(:user)}
30
+ it "should raise Exception" do
31
+ expect{ProductForm.load(user)}.to raise_error(FormModel::ModelMisMatchError)
32
+ end
33
+ end
34
+
35
+ describe "when loading from a product with given attributes" do
36
+ let(:product) do
37
+ FactoryGirl.build :product, title: "testing title", description: "testing description", price: {cents: 10}
38
+ end
39
+ subject{ProductForm.load(product)}
40
+ before{subject.stub(data_model_attribute_names: [:price])}
41
+ its(:title){should eq("testing title")}
42
+ its(:name){should be_nil}
43
+ its(:description){should eq("testing description")}
44
+ its(:valid?){should be_true}
45
+ end
46
+
47
+ describe "validation" do
48
+ let(:product) do
49
+ FactoryGirl.build :product, title: "testing title", description: "testing description"
50
+ end
51
+ subject{ProductForm.load(product)}
52
+ before {subject.stub(data_model_attribute_names: [:price])}
53
+
54
+ it "should not be valid as the data model is not valid" do
55
+ product.stub(valid?: false, errors: {price: "not valid"})
56
+ subject.should_not be_valid
57
+ end
58
+
59
+ it "should not be valid as price is not valid" do
60
+ product.stub(valid?: true)
61
+ subject.should_not be_valid
62
+ end
63
+ end
64
+
65
+ describe "save" do
66
+ let(:product) do
67
+ FactoryGirl.build :product, title: "testing title", description: "testing description", price: {cents: 10}
68
+ end
69
+ subject{ProductForm.load(product)}
70
+ before do
71
+ subject.stub(data_model_attribute_names: [:title])
72
+ product.stub(save: true)
73
+ end
74
+
75
+ it "should write_attributes to the model" do
76
+ product.should_receive(:write_attributes)
77
+ subject.save
78
+ end
79
+
80
+ it "should save to the model" do
81
+ product.should_receive(:write_attributes)
82
+ subject.save
83
+ end
84
+
85
+ it "should check validation" do
86
+ subject.should_receive(:save)
87
+ subject.save
88
+ end
89
+ end
90
+
91
+ describe "when mapping a price of 50 GBP to the form model" do
92
+ let(:product) {FactoryGirl.build(:product, price: {"currency" => "GBP", cents: 50})}
93
+ subject{ProductForm.load(product)}
94
+ its(:price){should eq(VMoney.new(currency: "GBP", cents: 50))}
95
+ end
96
+
97
+ describe "when setting money via amount" do
98
+ let(:product) {FactoryGirl.build(:product, price: {"currency" => "GBP", cents: 50})}
99
+
100
+ before do
101
+ @form = ProductForm.load(product)
102
+ @form.stub(data_model_attribute_names: [:price])
103
+ @form.update(price: {"amount" => "20.0"})
104
+ @form.update_data_model!
105
+ end
106
+
107
+ it "should set a price hash on the product" do
108
+ product.price["cents"].should eq(2000)
109
+ end
110
+ end
111
+
112
+ describe "when mapping a vmoney price from the form to the model" do
113
+ let(:product) {FactoryGirl.build(:product, price: {"currency" => "GBP", cents: 50})}
114
+ before do
115
+ @form = ProductForm.load(product)
116
+ @form.stub(data_model_attribute_names: [:price])
117
+ @form.price = {"currency" => "EUR"}
118
+ @form.update_data_model!
119
+ end
120
+ it "should set a price hash on the product" do
121
+ product.price["currency"].should eq("EUR")
122
+ end
123
+ end
124
+
125
+ describe "when mapping a start_time with of now" do
126
+ let(:now){Time.now}
127
+ let(:split_time){now.strftime("%R").split(?:)}
128
+ let(:product) {FactoryGirl.build(:product, start_at: now)}
129
+ subject{ProductForm.load(product)}
130
+ its(:start_at_date){ should eq(now.to_date.to_s)}
131
+ its(:start_at_hr) { should eq(split_time.first)}
132
+ its(:start_at_min) { should eq(split_time.last)}
133
+ end
134
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+ require File.expand_path("../../spec_helper", __FILE__)
3
+
4
+ describe "User" do
5
+ let(:user) {FactoryGirl.build(:user)}
6
+ subject{UserForm.load(user)}
7
+ before do
8
+ subject.stub(data_model_attribute_names: [:email])
9
+ subject.email = "hookercookerman@gmail.com"
10
+ subject.update_data_model!
11
+ end
12
+
13
+ it "should map a real email to a dev email" do
14
+ user.email.should eq("hookercookerman$gmail.com")
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+
4
+ unless ENV["TRAVIS"]
5
+ require 'simplecov'
6
+ SimpleCov.start do
7
+ add_group "lib", "lib"
8
+ add_group "spec", "spec"
9
+ end
10
+ end
11
+
12
+ require "pry"
13
+ require "form_model"
14
+
15
+ Dir.glob(File.expand_path('../../spec/support/virtus/**/*.rb', __FILE__)) do |file|
16
+ require file
17
+ end
18
+
19
+ Dir.glob(File.expand_path('../../spec/support/mappers/**/*.rb', __FILE__)) do |file|
20
+ require file
21
+ end
22
+
23
+ Dir.glob(File.expand_path('../../spec/support/**/*.rb', __FILE__)) do |file|
24
+ require file
25
+ end
26
+
27
+ RSpec.configure do |config|
28
+ config.filter_run :focus => true
29
+ config.run_all_when_everything_filtered = true
30
+ end
@@ -0,0 +1,21 @@
1
+ require 'factory_girl'
2
+
3
+ FactoryGirl.define do
4
+
5
+ factory :product do
6
+ trait :base do
7
+ sequence(:id)
8
+ name {'Magic'}
9
+ description {"Do it like someone else"}
10
+ end
11
+ end
12
+
13
+ factory :user do
14
+ trait :base do
15
+ sequence(:id)
16
+ name {'Magic'}
17
+ email {"test@test.com"}
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,25 @@
1
+ class ProductForm
2
+ include FormModel
3
+ bind_to{Product}
4
+
5
+ # Attributes
6
+ attribute :name, String
7
+ attribute :title, String
8
+ attribute :description, String
9
+ attribute :price, VMoney
10
+ attribute :selected_price, VMoney
11
+
12
+ attribute :start_at_date, String
13
+ attribute :start_at_hr, String
14
+ attribute :start_at_min, String
15
+
16
+ # Mappers
17
+ mapper MoneyMapper, form: {price: :price}
18
+ mapper DateHourMinMapper,
19
+ form: {date: :start_at_date, hour: :start_at_hr, min: :start_at_min},
20
+ model: {time: :start_at}
21
+
22
+ # Validations
23
+ validates :title, presence: true
24
+ validates_associated :price
25
+ end
@@ -0,0 +1,11 @@
1
+ class UserForm
2
+ include FormModel
3
+ bind_to{User}
4
+
5
+ attribute :id, String
6
+ attribute :email, String
7
+ attribute :name, String
8
+
9
+ # Mappers
10
+ mapper DevEmailMapper, form: {email: :email}
11
+ end
@@ -0,0 +1,35 @@
1
+ class DateHourMinMapper
2
+ include FormModel::Mapper
3
+
4
+ form_keys :date, :hour, :min
5
+ model_keys :time
6
+
7
+ def to_form(model)
8
+ assert_model_values(model, :time) do |values|
9
+ time = values[:time]
10
+ split_time = split_time(time)
11
+ {
12
+ form_attribute(:date) => time.to_date.to_s,
13
+ form_attribute(:hour) => split_time.first,
14
+ form_attribute(:min) => split_time.last
15
+ }
16
+ end
17
+ end
18
+
19
+ def to_model(form)
20
+ assert_form_values(form, :date, :hour, :min) do |values|
21
+ {model_attribute(:time) => time_from_form(values)}
22
+ end
23
+ end
24
+
25
+ private
26
+ def split_time(time)
27
+ time.strftime("%R").split(?:)
28
+ end
29
+
30
+ def time_from_form(values)
31
+ time = Date.parse(values[:date]).to_time
32
+ time.change(:hour => values[:hour], min: values[:min])
33
+ end
34
+
35
+ end
@@ -0,0 +1,17 @@
1
+ class DevEmailMapper
2
+ include FormModel::Mapper
3
+ form_keys :email
4
+ model_keys :email
5
+
6
+ def to_form(model)
7
+ assert_model_values(model, :email) do |values|
8
+ {form_attribute(:email) => values[:email].gsub("$", "@")}
9
+ end
10
+ end
11
+
12
+ def to_model(form)
13
+ assert_form_values(form, :email) do |values|
14
+ {model_attribute(:email) => values[:email].gsub("@", "$")}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ class MoneyMapper
2
+ include FormModel::Mapper
3
+ form_keys :price
4
+ model_keys :price
5
+
6
+ def to_form(model)
7
+ assert_model_values(model, :price) do |values|
8
+ {form_attribute(:price) => VMoney.new(values[:price])}
9
+ end
10
+ end
11
+
12
+ def to_model(form)
13
+ assert_form_values(form, :price) do |values|
14
+ {model_attribute(:price) => values[:price].to_hash.stringify_keys}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ class Product
2
+ attr_accessor :id, :price, :title, :name, :description, :start_at
3
+
4
+ def price
5
+ @price ||= {}
6
+ end
7
+
8
+ def attributes
9
+ {"id" => id, "price" => price, "description" => description, "title" => title, start_at: start_at}
10
+ end
11
+
12
+ def write_attributes(attributes)
13
+ attributes.each do |attr, value|
14
+ self.send("#{attr}=", value)
15
+ end
16
+ end
17
+
18
+ def valid?
19
+ true
20
+ end
21
+
22
+ def errors
23
+ {}
24
+ end
25
+
26
+ end
@@ -0,0 +1,22 @@
1
+ class User
2
+ attr_accessor :id, :name, :email
3
+
4
+ def attributes
5
+ {"id" => id, "name" => name, "email" => email}
6
+ end
7
+
8
+ def write_attributes(attributes)
9
+ attributes.each do |attr, value|
10
+ self.send("#{attr}=", value)
11
+ end
12
+ end
13
+
14
+ def valid?
15
+ true
16
+ end
17
+
18
+ def errors
19
+ {}
20
+ end
21
+
22
+ end
@@ -0,0 +1,18 @@
1
+ class VMoney
2
+ include Virtus::ValueObject
3
+ include ActiveModel::Validations
4
+
5
+ # attributes
6
+ attribute :cents, Integer, default: 0
7
+ attribute :currency, String
8
+
9
+ def amount
10
+ sprintf "%0.02f", (BigDecimal(cents) / BigDecimal("100"))
11
+ end
12
+
13
+ def amount=(value)
14
+ self.cents = (BigDecimal(value) * BigDecimal("100")).to_i
15
+ end
16
+
17
+ validates :cents, presence: true, :numericality => { :only_integer => true, greater_than: 0 }
18
+ end
metadata ADDED
@@ -0,0 +1,217 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: form_model
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - hookercookerman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: virtus
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.5.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.5.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: activemodel
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 3.0.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 3.0.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: activesupport
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 3.0.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 2.11.0
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 2.11.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: factory_girl
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 4.0.0
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 4.0.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: fuubar
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 1.0.0
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 1.0.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: activemodel
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 3.2.8
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: 3.2.8
126
+ - !ruby/object:Gem::Dependency
127
+ name: rake
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ description: A Library to create Form objects to encapsulate forms
143
+ email:
144
+ - hookercookerman@gmail.com
145
+ executables: []
146
+ extensions: []
147
+ extra_rdoc_files: []
148
+ files:
149
+ - .gitignore
150
+ - Gemfile
151
+ - LICENSE.txt
152
+ - README.md
153
+ - Rakefile
154
+ - form_model.gemspec
155
+ - lib/form_model.rb
156
+ - lib/form_model/associated_validation.rb
157
+ - lib/form_model/errors.rb
158
+ - lib/form_model/mapper.rb
159
+ - lib/form_model/model.rb
160
+ - lib/form_model/version.rb
161
+ - spec/mappers_spec.rb
162
+ - spec/scenerios/product_spec.rb
163
+ - spec/scenerios/user_spec.rb
164
+ - spec/spec_helper.rb
165
+ - spec/support/factories.rb
166
+ - spec/support/forms/product_form.rb
167
+ - spec/support/forms/user_form.rb
168
+ - spec/support/mappers/date_hour_min_mapper.rb
169
+ - spec/support/mappers/dev_email_mapper.rb
170
+ - spec/support/mappers/money_mapper.rb
171
+ - spec/support/models/product.rb
172
+ - spec/support/models/user.rb
173
+ - spec/support/virtus/v_money.rb
174
+ homepage: ''
175
+ licenses: []
176
+ post_install_message:
177
+ rdoc_options: []
178
+ require_paths:
179
+ - lib
180
+ required_ruby_version: !ruby/object:Gem::Requirement
181
+ none: false
182
+ requirements:
183
+ - - ! '>='
184
+ - !ruby/object:Gem::Version
185
+ version: '0'
186
+ segments:
187
+ - 0
188
+ hash: -2564772485630625639
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ none: false
191
+ requirements:
192
+ - - ! '>='
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ segments:
196
+ - 0
197
+ hash: -2564772485630625639
198
+ requirements: []
199
+ rubyforge_project:
200
+ rubygems_version: 1.8.23
201
+ signing_key:
202
+ specification_version: 3
203
+ summary: A Library to create Form objects to encapsulate forms
204
+ test_files:
205
+ - spec/mappers_spec.rb
206
+ - spec/scenerios/product_spec.rb
207
+ - spec/scenerios/user_spec.rb
208
+ - spec/spec_helper.rb
209
+ - spec/support/factories.rb
210
+ - spec/support/forms/product_form.rb
211
+ - spec/support/forms/user_form.rb
212
+ - spec/support/mappers/date_hour_min_mapper.rb
213
+ - spec/support/mappers/dev_email_mapper.rb
214
+ - spec/support/mappers/money_mapper.rb
215
+ - spec/support/models/product.rb
216
+ - spec/support/models/user.rb
217
+ - spec/support/virtus/v_money.rb