form_model 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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