flattener 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.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ .DS_Store
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --backtrace
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in flattener.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Lucas Florio
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.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # Flattener
2
+
3
+ Allows to flat an object (ActiveRecord based) into one level.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'flattener'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install flattener
18
+
19
+ ## Usage
20
+
21
+ Say you have something like this:
22
+
23
+ class Customer < ActiveRecord::Base
24
+ has_one :address
25
+ end
26
+
27
+ class Address < ActiveRecord::Base
28
+ # ... Attributes here
29
+ end
30
+
31
+ You could do something like this:
32
+
33
+ class Customer < ActiveRecord::Base
34
+
35
+ include Flattener
36
+
37
+ has_one :address
38
+
39
+ flat :address do
40
+ attribute :address_line_1
41
+ attribute :address_line_2
42
+ attribute :city
43
+ attribute :state
44
+ attribute :country
45
+ end
46
+
47
+ end
48
+
49
+ and use it like this:
50
+
51
+ customer = Customer.new address_line_1: "Av. Santa Fe 1234", country: "Argentina"
52
+
53
+ Pretty simple. It support all the basic AR methods, like valid?, save or create.
54
+
55
+ I know what you think. "I can do that with a simple delegate call". Indeed, but you can also do this:
56
+
57
+ class Payment < ActiveRecord::Base
58
+
59
+ include Flattener
60
+
61
+ has_one :payment_profile
62
+
63
+ flat :payment_profile do
64
+
65
+ attributes :credit_card, :expiration_date
66
+
67
+ flat :address do
68
+ attribute :address_line_1
69
+ attribute :address_line_2
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ And this way getting something really simple:
77
+
78
+ payment = Payment.new amount: "23.00", payment_profile_credit_card: "XXXXXXXXXX", expiration_date: "2011/12",
79
+ payment_profile_address_line_1: "Av. Mongo 1234"
80
+
81
+ payment.payment_profile_address_line_1 #=> "Av. Mongo 1234"
82
+ # and so on.
83
+
84
+ # supose you have a validation on Address#address_line_2
85
+ payment.valid? #=> false
86
+ payment.errors[:payment_profile_address_line_1] #=> ".... is required, blah..."
87
+
88
+ So what is this gem doing?
89
+
90
+ * It builds the flattened objects automatically, or the way you want (configuration option).
91
+ * It overwrites the AR methods in order to call the flattened methods before, so the right validation errors appear in place.
92
+
93
+ ## Contributing
94
+
95
+ 1. Fork it
96
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
97
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
98
+ 4. Push to the branch (`git push origin my-new-feature`)
99
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/flattener.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/flattener/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Lucas Florio"]
6
+ gem.email = ["lucasefe@gmail.com"]
7
+ gem.description = %q{Allows to flat an object and his children (ActiveRecord based) into one level, so you create them all without building a complex form. }
8
+ gem.summary = gem.description
9
+ gem.homepage = ""
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "flattener"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Flattener::VERSION
17
+
18
+ gem.add_dependency 'activerecord'
19
+ gem.add_development_dependency 'rspec'
20
+ gem.add_development_dependency 'sqlite3'
21
+ gem.add_development_dependency 'shoulda'
22
+ gem.add_development_dependency 'guard-rspec'
23
+ end
@@ -0,0 +1,13 @@
1
+ module Flattener
2
+ class Base
3
+
4
+ include ActiveModel::Validations
5
+ extend Flattener
6
+
7
+ def initialize attributes = {}
8
+ attributes.each do |key, value|
9
+ self.send("#{key}=", value)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,147 @@
1
+ module Flattener
2
+
3
+ class Configurator
4
+
5
+ def self.flat klass, object, options = {}, &block
6
+ new klass, object, options, &block
7
+ end
8
+
9
+ # # nesting
10
+ def flat object, options = {}, &block
11
+ NestedConfigurator.new self, object, options, &block
12
+ end
13
+
14
+ attr_accessor :klass, :accessor, :name, :prefix, :options, :attributes
15
+
16
+ def initialize(klass, name, options = {}, &block)
17
+ @klass = klass
18
+ @name = name
19
+ @options = options
20
+ @attributes = {}
21
+ @prefix = @options[:prefix] || @name.to_s
22
+ @accessor = @options[:accessor] || @name.to_s
23
+ prepare!
24
+ self.instance_eval(&block) if block_given?
25
+ end
26
+
27
+ def prepare!
28
+ add_build_method!
29
+ add_proxy_method!
30
+ add_validator_method!
31
+
32
+ unless klass.respond_to?(:flatteners)
33
+ klass.class_attribute :flatteners
34
+ klass.flatteners = {}
35
+ end
36
+
37
+ klass.flatteners[name] = self
38
+ end
39
+
40
+ def add_build_method!
41
+ unless @klass.instance_methods.include?(build_method_name.to_sym)
42
+ raise "must specify a build option, or the method named #{build_method_name}" unless @options.has_key?(:build)
43
+ @klass.send :define_method, build_method_name, &@options[:build]
44
+ end
45
+ end
46
+
47
+ def add_proxy_method!
48
+ code = <<-CODE
49
+ def #{proxy_method_name}
50
+ if self.#{accessor_method_name}.nil?
51
+ #{build_method_name}
52
+ end
53
+ self.#{accessor_method_name}
54
+ end
55
+ CODE
56
+ @klass.class_eval code, __FILE__, __LINE__
57
+ end
58
+
59
+ def add_validator_method!
60
+ if defined?(ActiveRecord::Validations) && @klass.included_modules.include?(ActiveRecord::Validations)
61
+ code = <<-CODE
62
+ def #{validate_method_name}
63
+ unless self.#{accessor_method_name}.blank?
64
+ self.#{proxy_method_name}.valid?
65
+
66
+ self.#{proxy_method_name}.errors.each do |name, message|
67
+ flattened_name = self.class.flatteners[:#{name}].attributes[name]
68
+ self.errors.add flattened_name, message
69
+ end
70
+ end
71
+ end
72
+ CODE
73
+ @klass.class_eval code
74
+ @klass.validate validate_method_name
75
+ end
76
+ end
77
+
78
+ # How it access the flattened object
79
+ def accessor_method_name
80
+ @accessor
81
+ end
82
+
83
+ def proxy_method_name
84
+ "proxy_#{accessor_method_name}"
85
+ end
86
+
87
+ def build_method_name
88
+ "build_#{accessor_method_name}"
89
+ end
90
+
91
+ def validate_method_name
92
+ "validate_#{accessor_method_name}"
93
+ end
94
+
95
+ def method_name_for(name)
96
+ if prefix.blank?
97
+ name.to_sym
98
+ else
99
+ "#{prefix}_#{name}".to_sym
100
+ end
101
+ end
102
+
103
+ def attribute(name)
104
+ method_name = method_name_for(name)
105
+ code = <<-CODE
106
+ def #{method_name}
107
+ #{proxy_method_name}.#{name}
108
+ end
109
+
110
+ def #{method_name}=(value)
111
+ #{proxy_method_name}.#{name} = value
112
+ end
113
+ CODE
114
+ @klass.class_eval code, __FILE__, __LINE__
115
+ self.attributes[name] = method_name
116
+ end
117
+ end
118
+
119
+ class NestedConfigurator < Configurator
120
+
121
+ attr_accessor :parent
122
+
123
+ def initialize(parent, name, options = {}, &block)
124
+ @parent = parent
125
+ options[:prefix] ||= if @parent.prefix.blank?
126
+ name.to_s
127
+ else
128
+ "#{@parent.prefix}_#{name.to_s}"
129
+ end
130
+ options[:accessor] ||= "#{parent.accessor_method_name}_#{name}"
131
+ super(parent.klass, name, options, &block)
132
+ end
133
+
134
+ def prepare!
135
+ super
136
+ code = <<-CODE
137
+ # customer_payment_profile_address
138
+
139
+ def #{parent.accessor_method_name}_#{name}
140
+ #{parent.proxy_method_name}.#{name}
141
+ end
142
+ CODE
143
+ @klass.class_eval code
144
+ end
145
+ end
146
+
147
+ end
@@ -0,0 +1,7 @@
1
+ module Flattener
2
+
3
+ class Railtie < Rails::Railtie
4
+
5
+ end
6
+
7
+ end
@@ -0,0 +1,3 @@
1
+ module Flattener
2
+ VERSION = "0.0.1"
3
+ end
data/lib/flattener.rb ADDED
@@ -0,0 +1,12 @@
1
+ require "flattener/version"
2
+ require "flattener/configurator"
3
+ require "flattener/base"
4
+ require "flattener/Rails" if defined?(Rails)
5
+
6
+ module Flattener
7
+
8
+ def flat object, options = {}, &block
9
+ Flattener::Configurator.flat(self, object, options, &block)
10
+ end
11
+
12
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ class CustomerBuilder < Flattener::Base
5
+ attr_accessor :customer
6
+
7
+ flat :customer, prefix: "", build: proc {self.customer = Customer.new} do
8
+
9
+ attribute :name
10
+
11
+ flat :payment_profile, build: proc { self.proxy_customer.build_payment_profile } do
12
+ attribute :uuid
13
+
14
+ flat :address, build: proc { self.proxy_customer_payment_profile.build_address } do
15
+ attribute :city
16
+ attribute :street_line_1
17
+ end
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ describe Flattener::Base do
24
+
25
+ it "should work as a class too" do
26
+ instance = CustomerBuilder.new name: "jose", payment_profile_uuid: "GGG-78979", payment_profile_address_city: "Mongo City"
27
+ instance.name.should == "jose"
28
+ instance.customer.name.should == "jose"
29
+
30
+ instance.payment_profile_uuid.should == instance.customer.payment_profile.uuid
31
+ instance.payment_profile_address_city.should == instance.customer.payment_profile.address.city
32
+ instance.should be_valid
33
+ end
34
+
35
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+
3
+ describe "A flattened object" do
4
+
5
+ before do
6
+ @klass = Class.new do
7
+ extend Flattener
8
+ attr_accessor :subobject
9
+ end
10
+
11
+ SubClass = Class.new do
12
+ attr_accessor :name
13
+ end
14
+ end
15
+
16
+ context "configuration" do
17
+
18
+ it "should be flattenable" do
19
+ @klass.should respond_to(:flat)
20
+ end
21
+
22
+ it "should store the flattened configuration" do
23
+ @klass.flat :subobject, build: proc { self.subobject = SubClass.new } do
24
+ attribute :name
25
+ end
26
+
27
+ @klass.should respond_to(:flatteners)
28
+ @klass.flatteners[:subobject].should_not be_blank
29
+ end
30
+
31
+ it "should keep track of the flattened attributes" do
32
+ @klass.flat :subobject, build: proc { self.subobject = SubClass.new } do
33
+ attribute :name
34
+ end
35
+ @klass.flatteners[:subobject].attributes.keys.should == [ :name ]
36
+ @klass.flatteners[:subobject].attributes.values.should == [ :subobject_name ]
37
+ end
38
+
39
+ end
40
+
41
+
42
+ context "basic usage" do
43
+
44
+ it "should raise an error if no build option is provided" do
45
+ lambda {
46
+ @klass.flat :subobject
47
+ }.should raise_error(/build/)
48
+ end
49
+
50
+ it "should flat an extra object attribute" do
51
+ @klass.flat :subobject, build: proc { self.subobject = SubClass.new } do
52
+ attribute :name
53
+ end
54
+
55
+ instance = @klass.new
56
+ instance.should respond_to(:subobject_name)
57
+ instance.should respond_to(:subobject_name=)
58
+ instance.subobject_name = "name from hell"
59
+ instance.subobject.should be_an_instance_of(SubClass)
60
+ instance.subobject.name.should == "name from hell"
61
+ end
62
+
63
+ it "should support to change the prefix (the way the methods are named) " do
64
+ @klass.flat :subobject, build: proc { self.subobject = SubClass.new }, prefix: "so" do
65
+ attribute :name
66
+ end
67
+
68
+ instance = @klass.new
69
+ instance.should respond_to(:so_name)
70
+ instance.should respond_to(:so_name=)
71
+ instance.so_name = "name from hell"
72
+ instance.subobject.should be_an_instance_of(SubClass)
73
+ instance.subobject.name.should == "name from hell"
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+
80
+ describe "a flattened object with validations" do
81
+
82
+ it "jjj" do
83
+ instance = Customer.new
84
+ instance.payment_profile_uuid = "GGG-78979"
85
+ instance.payment_profile_uuid.should == instance.payment_profile.uuid
86
+
87
+ instance.should be_valid
88
+ instance.save.should be_true
89
+ instance.payment_profile.should_not be_new_record
90
+
91
+ instance.payment_profile_uuid = nil
92
+ instance.should_not be_valid
93
+
94
+ instance.errors.should have(1).message
95
+ instance.errors[:payment_profile_uuid].first.should match(/can't be blank/)
96
+
97
+ instance.save.should_not be_true
98
+ end
99
+
100
+ it "nested flattened objects" do
101
+ instance = Customer.new
102
+ instance.payment_profile_uuid = "GGG-78979"
103
+ instance.payment_profile.should respond_to(:build_address)
104
+ instance.payment_profile_uuid.should == instance.payment_profile.uuid
105
+ instance.payment_profile_address_city = "Mongo City"
106
+ instance.payment_profile_address_city.should == instance.payment_profile.address.city
107
+ instance.should be_valid
108
+
109
+ instance.payment_profile_address_city = nil
110
+ instance.should_not be_valid
111
+ instance.errors[:payment_profile_address_city].first.should match(/can't be blank/)
112
+ end
113
+
114
+ it "massive assignment" do
115
+ instance = Customer.new payment_profile_uuid: "GGG-78979", payment_profile_address_city: "Mongo City"
116
+ instance.payment_profile_uuid.should == instance.payment_profile.uuid
117
+ instance.payment_profile_address_city.should == instance.payment_profile.address.city
118
+ instance.should be_valid
119
+ end
120
+ end
@@ -0,0 +1,55 @@
1
+ require 'active_record'
2
+ require 'flattener'
3
+ require 'shoulda'
4
+
5
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
6
+
7
+ ActiveRecord::Schema.define(:version => 1) do
8
+ create_table :customers, force: true do |t|
9
+ t.string :name
10
+ end
11
+
12
+ create_table :payment_profiles, force: true do |t|
13
+ t.integer :customer_id
14
+ t.string :uuid
15
+ end
16
+
17
+ create_table :addresses, force: true do |t|
18
+ t.integer :payment_profile_id
19
+ t.string :street_line_1
20
+ t.string :street_line_2
21
+ t.string :city
22
+ end
23
+
24
+ end
25
+
26
+ class Address < ActiveRecord::Base
27
+ belongs_to :payment_profile
28
+
29
+ validates :city, presence: true
30
+
31
+ end
32
+
33
+ class PaymentProfile < ActiveRecord::Base
34
+ has_one :address
35
+ belongs_to :customer
36
+
37
+ validates :uuid, presence: true
38
+ end
39
+
40
+ class Customer < ActiveRecord::Base
41
+
42
+ has_one :payment_profile
43
+
44
+ extend Flattener
45
+
46
+ flat :payment_profile do
47
+ attribute :uuid
48
+
49
+ flat :address, build: proc { self.proxy_payment_profile.build_address } do
50
+ attribute :city
51
+ attribute :street_line_1
52
+ end
53
+ end
54
+
55
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flattener
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Lucas Florio
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-01-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: &70174276620400 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70174276620400
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70174276619820 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70174276619820
36
+ - !ruby/object:Gem::Dependency
37
+ name: sqlite3
38
+ requirement: &70174276619140 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70174276619140
47
+ - !ruby/object:Gem::Dependency
48
+ name: shoulda
49
+ requirement: &70174276618720 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70174276618720
58
+ - !ruby/object:Gem::Dependency
59
+ name: guard-rspec
60
+ requirement: &70174276618300 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70174276618300
69
+ description: ! 'Allows to flat an object and his children (ActiveRecord based) into
70
+ one level, so you create them all without building a complex form. '
71
+ email:
72
+ - lucasefe@gmail.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - .gitignore
78
+ - .rspec
79
+ - Gemfile
80
+ - Guardfile
81
+ - LICENSE
82
+ - README.md
83
+ - Rakefile
84
+ - flattener.gemspec
85
+ - lib/flattener.rb
86
+ - lib/flattener/base.rb
87
+ - lib/flattener/configurator.rb
88
+ - lib/flattener/rails.rb
89
+ - lib/flattener/version.rb
90
+ - spec/lib/flattener/base_spec.rb
91
+ - spec/lib/flattener/configurator_spec.rb
92
+ - spec/spec_helper.rb
93
+ homepage: ''
94
+ licenses: []
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 1.8.15
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Allows to flat an object and his children (ActiveRecord based) into one level,
117
+ so you create them all without building a complex form.
118
+ test_files:
119
+ - spec/lib/flattener/base_spec.rb
120
+ - spec/lib/flattener/configurator_spec.rb
121
+ - spec/spec_helper.rb