active_type 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,71 @@
1
+ require 'active_type/nested_attributes/association'
2
+
3
+ module ActiveType
4
+
5
+ module NestedAttributes
6
+
7
+ class NestsManyAssociation < Association
8
+
9
+ def assign_attributes(parent, attributes_collection)
10
+ return if attributes_collection.nil?
11
+
12
+ unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
13
+ raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
14
+ end
15
+
16
+ new_records = []
17
+
18
+ if attributes_collection.is_a?(Hash)
19
+ keys = attributes_collection.keys
20
+ attributes_collection = if keys.include?('id') || keys.include?(:id)
21
+ Array.wrap(attributes_collection)
22
+ else
23
+ attributes_collection.sort_by { |i, _| i.to_i }.map { |_, attributes| attributes }
24
+ end
25
+ end
26
+
27
+ attributes_collection.each do |attributes|
28
+ attributes = attributes.with_indifferent_access
29
+ next if reject?(parent, attributes)
30
+
31
+ destroy = truthy?(attributes.delete(:_destroy)) && @allow_destroy
32
+
33
+ if id = attributes.delete(:id)
34
+ child = fetch_child(parent, id.to_i)
35
+ if destroy
36
+ child.mark_for_destruction
37
+ else
38
+ child.attributes = attributes
39
+ end
40
+ elsif !destroy
41
+ new_records << build_child(parent, attributes)
42
+ end
43
+ end
44
+
45
+ add_children(parent, new_records)
46
+ end
47
+
48
+
49
+ private
50
+
51
+ def add_child(parent, child)
52
+ add_children(parent, [child])
53
+ end
54
+
55
+ def add_children(parent, children)
56
+ parent[@target_name] = assigned_children(parent) + children
57
+ end
58
+
59
+ def assign_children(parent, children)
60
+ parent[@target_name] = children
61
+ end
62
+
63
+ def derive_class_name
64
+ @target_name.to_s.classify
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_type/nested_attributes/association'
2
+
3
+ module ActiveType
4
+
5
+ module NestedAttributes
6
+
7
+ class AssignmentError < StandardError; end
8
+
9
+ class NestsOneAssociation < Association
10
+
11
+ def assign_attributes(parent, attributes)
12
+ return if attributes.nil?
13
+ attributes = attributes.with_indifferent_access
14
+ return if reject?(parent, attributes)
15
+
16
+ assigned_child = assigned_children(parent).first
17
+ destroy = truthy?(attributes.delete(:_destroy)) && @allow_destroy
18
+
19
+ if id = attributes.delete(:id)
20
+ assigned_child ||= fetch_child(parent, id.to_i)
21
+ if assigned_child
22
+ if assigned_child.id == id.to_i
23
+ assigned_child.attributes = attributes
24
+ else
25
+ raise AssignmentError, "child record '#{@target_name}' did not match id '#{id}'"
26
+ end
27
+ if destroy
28
+ assigned_child.mark_for_destruction
29
+ end
30
+ end
31
+ elsif !destroy
32
+ assigned_child ||= add_child(parent, build_child(parent, {}))
33
+ assigned_child.attributes = attributes
34
+ end
35
+ end
36
+
37
+
38
+ private
39
+
40
+ def add_child(parent, child)
41
+ parent[@target_name] = child
42
+ end
43
+
44
+ def assign_children(parent, children)
45
+ parent[@target_name] = children.first
46
+ end
47
+
48
+ def derive_class_name
49
+ @target_name.to_s.camelize
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+
56
+ end
@@ -0,0 +1,39 @@
1
+ require 'active_type/nested_attributes/builder'
2
+
3
+ module ActiveType
4
+
5
+ module NestedAttributes
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ attr_accessor :_nested_attribute_scopes
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ def nests_one(association_name, options = {})
16
+ Builder.new(self, generated_nested_attribute_methods).build(association_name, :one, options)
17
+ end
18
+
19
+ def nests_many(association_name, options = {})
20
+ Builder.new(self, generated_nested_attribute_methods).build(association_name, :many, options)
21
+ end
22
+
23
+
24
+ private
25
+
26
+ def generated_nested_attribute_methods
27
+ @generated_nested_attribute_methods ||= begin
28
+ mod = Module.new
29
+ include mod
30
+ mod
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
@@ -39,29 +39,42 @@ module ActiveType
39
39
  yield
40
40
  end
41
41
 
42
- def create(*)
43
- true
42
+ def destroy
43
+ @destroyed = true
44
+ freeze
44
45
  end
45
46
 
46
- def create_record(*)
47
- true
47
+ def reload
48
+ self
48
49
  end
49
50
 
50
- def update(*)
51
+
52
+ private
53
+
54
+ def create(*)
51
55
  true
52
56
  end
53
57
 
54
- def update_record(*)
58
+ def update(*)
55
59
  true
56
60
  end
57
61
 
58
- def destroy
59
- @destroyed = true
60
- freeze
61
- end
62
+ if ActiveRecord::Base.private_method_defined?(:create_record)
63
+ def create_record(*)
64
+ true
65
+ end
62
66
 
63
- def reload
64
- self
67
+ def update_record(*)
68
+ true
69
+ end
70
+ else
71
+ def _create_record(*)
72
+ true
73
+ end
74
+
75
+ def _update_record(*)
76
+ true
77
+ end
65
78
  end
66
79
 
67
80
  end
@@ -1,5 +1,6 @@
1
1
  require 'active_type/no_table'
2
2
  require 'active_type/virtual_attributes'
3
+ require 'active_type/nested_attributes'
3
4
 
4
5
  module ActiveType
5
6
 
@@ -7,6 +8,7 @@ module ActiveType
7
8
 
8
9
  include NoTable
9
10
  include VirtualAttributes
11
+ include NestedAttributes
10
12
 
11
13
  end
12
14
 
@@ -1,5 +1,6 @@
1
1
  require 'active_type/virtual_attributes'
2
2
  require 'active_type/extended_record'
3
+ require 'active_type/nested_attributes'
3
4
 
4
5
  module ActiveType
5
6
 
@@ -8,6 +9,7 @@ module ActiveType
8
9
  @abstract_class = true
9
10
 
10
11
  include VirtualAttributes
12
+ include NestedAttributes
11
13
  include ExtendedRecord
12
14
 
13
15
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveType
2
- VERSION = '0.1.3'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -1,15 +1,17 @@
1
1
  module ActiveType
2
2
 
3
- class InvalidAttributeNameError < StandardError; end
4
- class MissingAttributeError < StandardError; end
3
+ class InvalidAttributeNameError < ::StandardError; end
4
+ class MissingAttributeError < ::StandardError; end
5
+ class ArgumentError < ::ArgumentError; end
5
6
 
6
7
  module VirtualAttributes
7
8
 
8
9
  class VirtualColumn < ActiveRecord::ConnectionAdapters::Column
9
10
 
10
- def initialize(name, type)
11
+ def initialize(name, type, options)
11
12
  @name = name
12
13
  @type = type
14
+ @options = options
13
15
  end
14
16
 
15
17
  def type_cast(value)
@@ -43,23 +45,35 @@ module ActiveType
43
45
  end
44
46
  end
45
47
 
48
+ def default_value(object)
49
+ default = @options[:default]
50
+ default.respond_to?(:call) ? object.instance_eval(&default) : default
51
+ end
52
+
46
53
  end
47
54
 
48
- class AccessorGenerator
55
+ class Builder
49
56
 
50
- def initialize(mod)
57
+ def initialize(owner, mod)
58
+ @owner = owner
51
59
  @module = mod
52
60
  end
53
61
 
54
- def generate_accessors(name)
62
+ def build(name, type, options)
55
63
  validate_attribute_name!(name)
56
- generate_reader(name)
57
- generate_writer(name)
64
+ options.assert_valid_keys(:default)
65
+ add_virtual_column(name, type, options)
66
+ build_reader(name)
67
+ build_writer(name)
58
68
  end
59
69
 
60
70
  private
61
71
 
62
- def generate_reader(name)
72
+ def add_virtual_column(name, type, options)
73
+ @owner.virtual_columns_hash = @owner.virtual_columns_hash.merge(name.to_s => VirtualColumn.new(name, type, options.slice(:default)))
74
+ end
75
+
76
+ def build_reader(name)
63
77
  @module.module_eval <<-BODY, __FILE__, __LINE__ + 1
64
78
  def #{name}
65
79
  read_virtual_attribute('#{name}')
@@ -71,7 +85,7 @@ module ActiveType
71
85
  BODY
72
86
  end
73
87
 
74
- def generate_writer(name)
88
+ def build_writer(name)
75
89
  @module.module_eval <<-BODY, __FILE__, __LINE__ + 1
76
90
  def #{name}=(value)
77
91
  write_virtual_attribute('#{name}', value)
@@ -127,8 +141,14 @@ module ActiveType
127
141
 
128
142
  def read_virtual_attribute(name)
129
143
  name = name.to_s
130
- virtual_attributes_cache[name] ||= begin
131
- self.singleton_class._virtual_column(name).type_cast(virtual_attributes[name])
144
+ if virtual_attributes_cache.has_key?(name)
145
+ virtual_attributes_cache[name]
146
+ else
147
+ virtual_attributes_cache[name] = begin
148
+ virtual_column = self.singleton_class._virtual_column(name)
149
+ raw_value = virtual_attributes.fetch(name) { virtual_column.default_value(self) }
150
+ virtual_column.type_cast(raw_value)
151
+ end
132
152
  end
133
153
  end
134
154
 
@@ -178,9 +198,11 @@ module ActiveType
178
198
  end
179
199
  end
180
200
 
181
- def attribute(name, type = nil)
182
- self.virtual_columns_hash = virtual_columns_hash.merge(name.to_s => VirtualColumn.new(name, type))
183
- AccessorGenerator.new(generated_virtual_attribute_methods).generate_accessors(name)
201
+ def attribute(name, *args)
202
+ options = args.extract_options!
203
+ type = args.first
204
+
205
+ Builder.new(self, generated_virtual_attribute_methods).build(name, type, options)
184
206
  end
185
207
 
186
208
  end
data/lib/active_type.rb CHANGED
@@ -2,5 +2,7 @@
2
2
 
3
3
  require 'active_type/version'
4
4
 
5
+ require 'active_record'
6
+
5
7
  require 'active_type/record'
6
8
  require 'active_type/object'
@@ -20,6 +20,9 @@ module ExtendedRecordSpec
20
20
  attribute :another_virtual_string, :string
21
21
  end
22
22
 
23
+ class InheritingFromExtendedRecord < ExtendedRecord
24
+ attribute :yet_another_virtual_string, :string
25
+ end
23
26
 
24
27
  class ExtendedRecordWithValidations < ExtendedActiveTypeRecord
25
28
  validates :persisted_string, :presence => true
@@ -38,6 +41,10 @@ describe "ActiveType::Record[ActiveRecord::Base]" do
38
41
  subject.should be_a(ExtendedRecordSpec::BaseRecord)
39
42
  end
40
43
 
44
+ it 'has the same model name as the base class' do
45
+ subject.class.model_name.singular.should == ExtendedRecordSpec::BaseRecord.model_name.singular
46
+ end
47
+
41
48
  describe 'constructors' do
42
49
  subject { ExtendedRecordSpec::ExtendedRecord }
43
50
 
@@ -92,6 +99,57 @@ describe "ActiveType::Record[ActiveRecord::Base]" do
92
99
 
93
100
  end
94
101
 
102
+ describe "class ... < ActiveType::Record[ActiveRecord::Base]" do
103
+
104
+ subject { ExtendedRecordSpec::InheritingFromExtendedRecord.new }
105
+
106
+ it 'is inherits from the base type' do
107
+ subject.should be_a(ExtendedRecordSpec::ExtendedRecord)
108
+ end
109
+
110
+ it 'has the same model name as the base class' do
111
+ subject.class.model_name.singular.should == ExtendedRecordSpec::BaseRecord.model_name.singular
112
+ end
113
+
114
+ describe '#attributes' do
115
+
116
+ it 'returns a hash of virtual and persisted attributes' do
117
+ subject.persisted_string = "string"
118
+ subject.another_virtual_string = "string"
119
+ subject.yet_another_virtual_string = "string"
120
+
121
+ subject.attributes.should == {
122
+ "another_virtual_string" => "string",
123
+ "yet_another_virtual_string" => "string",
124
+ "id" => nil,
125
+ "persisted_string" => "string",
126
+ "persisted_integer" => nil,
127
+ "persisted_time" => nil,
128
+ "persisted_date" => nil,
129
+ "persisted_boolean" => nil
130
+ }
131
+ end
132
+
133
+ end
134
+
135
+ describe 'persistence' do
136
+ it 'persists to the database' do
137
+ subject.persisted_string = "persisted string"
138
+ subject.save.should be_true
139
+
140
+ subject.class.find(subject.id).persisted_string.should == "persisted string"
141
+ end
142
+ end
143
+
144
+ describe '.find' do
145
+ it 'returns an instance of the inheriting model' do
146
+ subject.save
147
+
148
+ subject.class.find(subject.id).should be_a(subject.class)
149
+ end
150
+ end
151
+
152
+ end
95
153
 
96
154
  describe "ActiveType::Record[ActiveType::Record]" do
97
155