virtual-attributes 0.1.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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c4254b8bcb72697664cc7d0770e7a7d9ad80cc89
4
+ data.tar.gz: 2486a778c6ef88d55c635b5f333d4fa6796cee86
5
+ SHA512:
6
+ metadata.gz: 6f3a8f1bc69509b641619878ce1dc43e976a9b52712cd9e17684633fa6b10b350da3abb84fa932d9f4fed6af9e8349ca1c96ffcf636341fe3b8afe539d0f0595
7
+ data.tar.gz: f91098c4691b063c1439698b90169234bc244282860f66e4130ec0931ba0b2caa0908055c491db23813d4157bd9bab3a05debdee681d7af788ce510aa388d92f
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Ihcène Med.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,13 @@
1
+ class VirtualAttributes
2
+ end
3
+
4
+ require 'virtual-attributes/base/attributes'
5
+ require 'virtual-attributes/base/casts'
6
+ require 'virtual-attributes/base/conversions'
7
+ require 'virtual-attributes/base/defaults'
8
+ require 'virtual-attributes/base/validations'
9
+ require 'virtual-attributes/base'
10
+
11
+ require 'virtual-attributes/serialization'
12
+
13
+ ActiveRecord::Base.singleton_class.send :include, VirtualAttributes::Serialization::ClassMethods
@@ -0,0 +1,7 @@
1
+ class VirtualAttributes::Base
2
+ include Attributes
3
+ include Casts
4
+ include Defaults
5
+ include Validations
6
+ include Conversions
7
+ end
@@ -0,0 +1,39 @@
1
+ class VirtualAttributes::Base
2
+ module Attributes
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :columns
7
+ end
8
+
9
+ def initialize(attrs = {})
10
+ attrs.each_pair do |k, v|
11
+ send "#{k}=", v
12
+ end
13
+ end
14
+
15
+ def read_attribute(column)
16
+ instance_variable_get("@#{column}")
17
+ end
18
+
19
+ def write_attribute(column, value)
20
+ instance_variable_set "@#{column}", value
21
+ end
22
+
23
+ module ClassMethods
24
+ def column(name, options = {})
25
+ (self.columns ||= {})[name] = {
26
+ options: options.with_indifferent_access
27
+ }
28
+
29
+ define_method name do
30
+ read_attribute(name)
31
+ end
32
+
33
+ define_method :"#{name}=" do |value|
34
+ write_attribute(name, value)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,39 @@
1
+ class VirtualAttributes::Base
2
+ module Casts
3
+ extend ActiveSupport::Concern
4
+
5
+ TYPES = ActiveRecord::Type::Value.subclasses.map{ |k| k.name.split('::').last.underscore.to_sym }
6
+
7
+ def write_attribute(column, value)
8
+ super column, cast_type(column, value)
9
+ end
10
+
11
+ def cast_type(column, value)
12
+ self.class.columns[column][:castor].send(:cast_value, value)
13
+ end
14
+
15
+ module ClassMethods
16
+ def column(name, type, options = {})
17
+ raise ArgumentError unless type.to_sym.in? TYPES
18
+
19
+ super(name, options)
20
+
21
+ self.columns[name.to_sym][:type] = type.to_sym,
22
+ self.columns[name.to_sym][:castor] =
23
+ ActiveRecord::Type.const_get(type.to_s.classify)
24
+ .new(options.symbolize_keys
25
+ .slice(:precision, :scale, :limit))
26
+ end
27
+
28
+ TYPES.each do |type|
29
+ define_method type do |*args|
30
+ options = args.extract_options!
31
+
32
+ args.each do |column_name|
33
+ column column_name, type, options
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ class VirtualAttributes::Base
2
+ module Conversions
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def wrap(val)
7
+ case val
8
+ when Hash
9
+ new(val)
10
+ when String
11
+ val.blank? ? new({}) : raise(ArgumentError('String is not convertible to #{self.name}'))
12
+ when self
13
+ val
14
+ when NilClass
15
+ new({})
16
+ when OpenStruct
17
+ new val.to_h
18
+ else
19
+ raise ArgumentError.new("#{val.class.name} is not convertible to #{self.name}")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ class VirtualAttributes::Base
2
+ module Defaults
3
+ extend ActiveSupport::Concern
4
+
5
+ def read_attribute(column)
6
+ val = super
7
+ val.nil? ? default_value(column) : val
8
+ end
9
+
10
+ private
11
+ def default_value(column)
12
+ default = self.class.columns[column][:options][:default]
13
+
14
+ if default
15
+ cast_type(
16
+ column,
17
+ default.respond_to?(:call) ? default.call(self) : default)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ class VirtualAttributes::Base
2
+ module Validations
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include ActiveModel::Validations
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,43 @@
1
+ module VirtualAttributes::Serialization
2
+ extend ActiveSupport::Concern
3
+
4
+ module ClassMethods
5
+ def serialize(*args, &block)
6
+ if block_given?
7
+ attr_name = args.first.to_s.presence || raise(ArgumentError.new('Attr name is mandatory'))
8
+ klass_name = "#{attr_name.classify}VirtualAttributes"
9
+
10
+ klass = Class.new VirtualAttributes::Base
11
+ const_set klass_name, klass
12
+
13
+ yield const_get klass_name
14
+
15
+ super attr_name, const_get(klass_name)
16
+
17
+ validates attr_name, virtual_attributes: true
18
+
19
+ define_method :"#{attr_name}=" do |val|
20
+ super self.class.const_get(klass_name).wrap(val)
21
+ end
22
+
23
+ define_method :"#{attr_name}" do
24
+ val = super()
25
+ val.is_a?(self.class.const_get(klass_name)) ? val : self.class.const_get(klass_name).wrap(val)
26
+ end
27
+ else
28
+ super(*args)
29
+ end
30
+ end
31
+
32
+ class ::VirtualAttributesValidator < ActiveModel::EachValidator
33
+ def validate_each(record, attribute, value)
34
+ return unless value.is_a?(VirtualAttributes::Base::Validations)
35
+
36
+ if value.invalid?
37
+ record.errors[attribute] << value.errors.full_messages.join('. ')
38
+ end
39
+ end
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,160 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe VirtualAttributes::Base do
4
+ describe "Accessors" do
5
+ it "should define accessors" do
6
+ klass = Class.new(VirtualAttributes::Base) do
7
+ string :username
8
+ end
9
+
10
+ instance = klass.new
11
+ expect(instance).to respond_to :username
12
+ expect(instance).to respond_to(:'username=').with(1).argument
13
+
14
+ instance.username = 'John'
15
+ expect(instance.username).to eql('John')
16
+ end
17
+
18
+ it "should allow defining several attributes at once" do
19
+ klass = Class.new(VirtualAttributes::Base) do
20
+ string :firstname, :lastname
21
+ end
22
+ instance = klass.new
23
+
24
+ expect(instance).to respond_to :firstname, :lastname
25
+ end
26
+ end
27
+
28
+ describe "Type casts" do
29
+ before(:each) do
30
+ @klass = Class.new(VirtualAttributes::Base) do
31
+ string :username
32
+ date :birthdate
33
+ boolean :rockstar
34
+ integer :age
35
+ end
36
+ @instance = @klass.new
37
+ end
38
+
39
+ it "should cast dates correctly" do
40
+ @instance.birthdate = "1963/11/22"
41
+ expect(@instance.birthdate).to eql Date.new(1963, 11, 22)
42
+ end
43
+
44
+ it "should cast booleans correctly" do
45
+ @instance.rockstar = 1
46
+ expect(@instance.rockstar).to eql true
47
+
48
+ @instance.rockstar = "1"
49
+ expect(@instance.rockstar).to eql true
50
+
51
+ @instance.rockstar = "t"
52
+ expect(@instance.rockstar).to eql true
53
+
54
+ @instance.rockstar = "true"
55
+ expect(@instance.rockstar).to eql true
56
+
57
+ @instance.rockstar = 0
58
+ expect(@instance.rockstar).to eql false
59
+
60
+ @instance.rockstar = "0"
61
+ expect(@instance.rockstar).to eql false
62
+
63
+ @instance.rockstar = "f"
64
+ expect(@instance.rockstar).to eql false
65
+
66
+ @instance.rockstar = "false"
67
+ expect(@instance.rockstar).to eql false
68
+ end
69
+
70
+ it "should cast integer correctly" do
71
+ @instance.age = "23"
72
+ expect(@instance.age).to eql 23
73
+
74
+ @instance.age = "23 apples"
75
+ expect(@instance.age).to eql 23
76
+
77
+ @instance.age = "I have 23 apples"
78
+ expect(@instance.age).to eql 0
79
+
80
+ @instance.age = 42.0
81
+ expect(@instance.age).to eql 42
82
+
83
+ @instance.age = BigDecimal.new('42.67')
84
+ expect(@instance.age).to eql 42
85
+ end
86
+
87
+ it "should cast strings correctly" do
88
+ @instance.username = 42
89
+ expect(@instance.username).to eql "42"
90
+
91
+ @instance.username = String
92
+ expect(@instance.username).to eql "String"
93
+ end
94
+ end
95
+
96
+ describe "Default values" do
97
+ before :each do
98
+ @klass = Class.new(VirtualAttributes::Base) do
99
+ string :fullname, default: "John Doe"
100
+ string :login, default: ->(e){ e.fullname.reverse }
101
+ date :birthdate, default: ->(){ raise ArgumentError }
102
+ date :birthdate_to_cast, default: "1963/11/22"
103
+ boolean :rockstar
104
+ integer :age
105
+ end
106
+ @instance = @klass.new
107
+ end
108
+
109
+ it "should return default value when nil" do
110
+ expect(@instance.fullname).to eql 'John Doe'
111
+ end
112
+
113
+ it "should ignore default value if not nil" do
114
+ @instance.fullname = 'Stephen King'
115
+ expect(@instance.fullname).to_not eql 'John Doe'
116
+ expect(@instance.fullname).to eql 'Stephen King'
117
+ end
118
+
119
+ it "should ignore default value even if blank" do
120
+ @instance.fullname = ''
121
+ expect(@instance.fullname).to_not eql 'John Doe'
122
+ end
123
+
124
+ it "should call default value proc" do
125
+ expect{@instance.birthdate}.to raise_error(ArgumentError)
126
+ end
127
+
128
+ it "should allow passing the model to default value procs" do
129
+ expect(@instance.login).to eql "eoD nhoJ"
130
+ end
131
+
132
+ it "should cast default value" do
133
+ expect(@instance.birthdate_to_cast).to eql Date.new(1963, 11, 22)
134
+ end
135
+ end
136
+
137
+ describe "Validations" do
138
+ before :each do
139
+ @klass = ValidationTestClass = Class.new(VirtualAttributes::Base) do
140
+ string :fullname, default: ""
141
+ string :login, default: ->(e){ e.fullname.reverse }
142
+ date :birthdate, default: "1963/11/22"
143
+ boolean :rockstar, default: false
144
+ integer :age
145
+
146
+ validates_presence_of :fullname, :login, :age
147
+ end
148
+ @instance = @klass.new
149
+ end
150
+
151
+ it "should validate correctly" do
152
+ expect(@instance.valid?).to be(false)
153
+
154
+ expect(@instance.errors[:fullname].length).to eql 1
155
+ expect(@instance.errors[:login].length).to eql 1
156
+ expect(@instance.errors[:age].length).to eql 1
157
+ expect(@instance.errors[:birthdate].length).to eql 0
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe VirtualAttributes::Serialization do
4
+ before :each do
5
+ @klass = ValidationTestSerializeClass = Class.new(ActiveRecord::Base) do
6
+ concerning :NoNeedDatabase do
7
+ attr_accessor :contact
8
+
9
+ def initialize(*)
10
+ end
11
+
12
+ def save(validate = true)
13
+ validate ? valid? : true
14
+ end
15
+
16
+ module ClassMethods
17
+ def columns
18
+ @columns ||= [];
19
+ end
20
+
21
+ def column(name, sql_type = nil, default = nil, null = true)
22
+ columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default,
23
+ sql_type.to_s, null)
24
+ end
25
+ end
26
+ end
27
+
28
+ serialize :contact do |t|
29
+ t.string :fullname, default: ""
30
+ t.string :login, default: ->(e){ e.fullname.reverse }
31
+ t.date :birthdate, default: "1963/11/22"
32
+ t.boolean :rockstar, default: false
33
+ t.integer :age
34
+ end
35
+ end
36
+
37
+ @instance = @klass.new
38
+ end
39
+
40
+ it "create a namespaced class of the right type" do
41
+ expect(@klass).to be_const_defined(:'ContactVirtualAttributes')
42
+ expect(@klass.const_get(:'ContactVirtualAttributes').superclass).to eql(VirtualAttributes::Base)
43
+ end
44
+
45
+ it "should produce instances that return the right type" do
46
+ expect(@instance.contact).to be_a(VirtualAttributes::Base)
47
+ end
48
+
49
+ it "should accept open structs and hashes" do
50
+ expect{ @instance.contact = OpenStruct.new(fullname: 'John Doe') }.to_not raise_error
51
+ expect(@instance.contact.fullname).to eql 'John Doe'
52
+
53
+ expect{ @instance.contact = {login: 'John Doe'} }.to_not raise_error
54
+ expect(@instance.contact.login).to eql 'John Doe'
55
+ end
56
+
57
+ it "should refuses other types" do
58
+ expect{ @instance.contact = 42 }.to raise_error(/is not convertible/)
59
+ end
60
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'active_support'
4
+ require 'active_support/core_ext'
5
+
6
+ require 'virtual-attributes'
7
+
8
+ RSpec.configure do |config|
9
+
10
+ end
@@ -0,0 +1,18 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'virtual-attributes'
5
+ s.version = '0.1.1'
6
+ s.date = '2015-03-19'
7
+ s.summary = "Enhance ActiveRecord's Serialize with types attributes"
8
+ s.description = "This gem allow you to add whenever columns you want to your model through one single serialized attribute"
9
+ s.authors = ["ihcene"]
10
+ s.email = 'ihcene@aritylabs.com'
11
+ s.files = `git ls-files`.split($/)
12
+ s.require_paths = ["lib"]
13
+ s.required_ruby_version = '>= 1.9.3'
14
+ s.homepage = 'https://github.com/ihcene/virtual-attributes'
15
+ s.license = 'MIT'
16
+ s.add_runtime_dependency 'activerecord', '>= 3.0', '< 5.0'
17
+ s.add_development_dependency 'rspec', '~> 3.2', '>= 3.2.0'
18
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: virtual-attributes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - ihcene
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: rspec
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.2'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 3.2.0
43
+ type: :development
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '3.2'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 3.2.0
53
+ description: This gem allow you to add whenever columns you want to your model through
54
+ one single serialized attribute
55
+ email: ihcene@aritylabs.com
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - ".rspec"
61
+ - LICENSE
62
+ - lib/virtual-attributes.rb
63
+ - lib/virtual-attributes/base.rb
64
+ - lib/virtual-attributes/base/attributes.rb
65
+ - lib/virtual-attributes/base/casts.rb
66
+ - lib/virtual-attributes/base/conversions.rb
67
+ - lib/virtual-attributes/base/defaults.rb
68
+ - lib/virtual-attributes/base/validations.rb
69
+ - lib/virtual-attributes/serialization.rb
70
+ - spec/base_spec.rb
71
+ - spec/serialization_spec.rb
72
+ - spec/spec_helper.rb
73
+ - virtual-attributes.gemspec
74
+ homepage: https://github.com/ihcene/virtual-attributes
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 1.9.3
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.2.2
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Enhance ActiveRecord's Serialize with types attributes
98
+ test_files: []