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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -1
- data/README.md +175 -3
- data/Rakefile +1 -2
- data/gemfiles/Gemfile.3.2 +1 -1
- data/gemfiles/Gemfile.3.2.lock +11 -11
- data/gemfiles/Gemfile.4.0 +1 -1
- data/gemfiles/Gemfile.4.0.lock +12 -14
- data/gemfiles/Gemfile.4.1 +2 -2
- data/gemfiles/Gemfile.4.1.lock +14 -16
- data/lib/active_type/nested_attributes/association.rb +128 -0
- data/lib/active_type/nested_attributes/builder.rb +67 -0
- data/lib/active_type/nested_attributes/nests_many_association.rb +71 -0
- data/lib/active_type/nested_attributes/nests_one_association.rb +56 -0
- data/lib/active_type/nested_attributes.rb +39 -0
- data/lib/active_type/no_table.rb +25 -12
- data/lib/active_type/object.rb +2 -0
- data/lib/active_type/record.rb +2 -0
- data/lib/active_type/version.rb +1 -1
- data/lib/active_type/virtual_attributes.rb +37 -15
- data/lib/active_type.rb +2 -0
- data/spec/active_type/extended_record_spec.rb +58 -0
- data/spec/active_type/nested_attributes_spec.rb +682 -0
- data/spec/active_type/object_spec.rb +4 -0
- data/spec/active_type/record_spec.rb +21 -0
- data/spec/integration/holidays_spec.rb +102 -0
- data/spec/integration/shape_spec.rb +112 -0
- data/spec/integration/sign_in_spec.rb +3 -0
- data/spec/integration/sign_up_spec.rb +12 -0
- data/spec/shared_examples/defaults.rb +60 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/support/database.rb +0 -5
- metadata +16 -3
@@ -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
|
+
|
data/lib/active_type/no_table.rb
CHANGED
@@ -39,29 +39,42 @@ module ActiveType
|
|
39
39
|
yield
|
40
40
|
end
|
41
41
|
|
42
|
-
def
|
43
|
-
true
|
42
|
+
def destroy
|
43
|
+
@destroyed = true
|
44
|
+
freeze
|
44
45
|
end
|
45
46
|
|
46
|
-
def
|
47
|
-
|
47
|
+
def reload
|
48
|
+
self
|
48
49
|
end
|
49
50
|
|
50
|
-
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def create(*)
|
51
55
|
true
|
52
56
|
end
|
53
57
|
|
54
|
-
def
|
58
|
+
def update(*)
|
55
59
|
true
|
56
60
|
end
|
57
61
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
+
if ActiveRecord::Base.private_method_defined?(:create_record)
|
63
|
+
def create_record(*)
|
64
|
+
true
|
65
|
+
end
|
62
66
|
|
63
|
-
|
64
|
-
|
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
|
data/lib/active_type/object.rb
CHANGED
@@ -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
|
|
data/lib/active_type/record.rb
CHANGED
@@ -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
|
data/lib/active_type/version.rb
CHANGED
@@ -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
|
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
|
62
|
+
def build(name, type, options)
|
55
63
|
validate_attribute_name!(name)
|
56
|
-
|
57
|
-
|
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
|
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
|
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
|
131
|
-
|
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,
|
182
|
-
|
183
|
-
|
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
@@ -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
|
|