active_type 0.1.3 → 0.2.0

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,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