active_data 0.1.0 → 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.
Files changed (41) hide show
  1. data/.rspec +1 -0
  2. data/.travis.yml +1 -1
  3. data/Gemfile +5 -7
  4. data/Guardfile +8 -3
  5. data/active_data.gemspec +2 -2
  6. data/lib/active_data/attributes/base.rb +10 -5
  7. data/lib/active_data/attributes/localized.rb +1 -1
  8. data/lib/active_data/model/associations/{many.rb → association.rb} +9 -3
  9. data/lib/active_data/model/associations/embeds_many.rb +33 -0
  10. data/lib/active_data/model/associations/embeds_one.rb +30 -0
  11. data/lib/active_data/model/associations.rb +19 -22
  12. data/lib/active_data/model/attributable.rb +35 -29
  13. data/lib/active_data/model/collectionizable.rb +13 -12
  14. data/lib/active_data/model/extensions/array.rb +3 -3
  15. data/lib/active_data/model/extensions/big_decimal.rb +2 -2
  16. data/lib/active_data/model/extensions/date.rb +1 -8
  17. data/lib/active_data/model/extensions/date_time.rb +17 -0
  18. data/lib/active_data/model/extensions/float.rb +1 -1
  19. data/lib/active_data/model/extensions/hash.rb +1 -1
  20. data/lib/active_data/model/extensions/integer.rb +1 -1
  21. data/lib/active_data/model/extensions/localized.rb +2 -2
  22. data/lib/active_data/model/extensions/object.rb +17 -0
  23. data/lib/active_data/model/extensions/time.rb +17 -0
  24. data/lib/active_data/model/localizable.rb +3 -3
  25. data/lib/active_data/model/nested_attributes.rb +10 -4
  26. data/lib/active_data/model.rb +17 -11
  27. data/lib/active_data/validations/associated.rb +17 -0
  28. data/lib/active_data/validations.rb +7 -0
  29. data/lib/active_data/version.rb +1 -1
  30. data/lib/active_data.rb +1 -1
  31. data/spec/lib/active_data/model/associations/embeds_many_spec.rb +93 -0
  32. data/spec/lib/active_data/model/associations/embeds_one_spec.rb +57 -0
  33. data/spec/lib/active_data/model/attributable_spec.rb +118 -2
  34. data/spec/lib/active_data/model/attributes/localized_spec.rb +1 -0
  35. data/spec/lib/active_data/model/collectionizable_spec.rb +41 -15
  36. data/spec/lib/active_data/model/nested_attributes_spec.rb +47 -26
  37. data/spec/lib/active_data/model/type_cast_spec.rb +31 -3
  38. data/spec/lib/active_data/model_spec.rb +26 -7
  39. data/spec/lib/active_data/validations/associated_spec.rb +88 -0
  40. metadata +27 -16
  41. data/spec/lib/active_data/model/associations_spec.rb +0 -35
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --colour
2
2
  --backtrace
3
+ --order random
data/.travis.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  rvm:
2
2
  - 1.9.2
3
3
  - 1.9.3
4
- - ruby-head
4
+ - 2.0.0
5
5
  #- rbx-19mode
data/Gemfile CHANGED
@@ -3,12 +3,10 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in active_data.gemspec
4
4
  gemspec
5
5
 
6
+ gem 'timecop'
7
+
6
8
  gem 'guard'
7
9
  gem 'guard-rspec'
8
-
9
- if RUBY_PLATFORM =~ /darwin/i
10
- gem 'rb-fsevent'
11
- else
12
- gem 'rb-inotify'
13
- gem 'libnotify'
14
- end
10
+ gem 'rb-inotify', require: false
11
+ gem 'rb-fsevent', require: false
12
+ gem 'rb-fchange', require: false
data/Guardfile CHANGED
@@ -1,7 +1,7 @@
1
1
  # A sample Guardfile
2
2
  # More info at https://github.com/guard/guard#readme
3
3
 
4
- guard 'rspec', :version => 2 do
4
+ guard :rspec do
5
5
  watch(%r{^spec/.+_spec\.rb$})
6
6
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
7
  watch('spec/spec_helper.rb') { "spec" }
@@ -13,7 +13,12 @@ guard 'rspec', :version => 2 do
13
13
  watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
14
  watch('config/routes.rb') { "spec/routing" }
15
15
  watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
- # Capybara request specs
17
- watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
16
+
17
+ # Capybara features specs
18
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
19
+
20
+ # Turnip features and steps
21
+ watch(%r{^spec/acceptance/(.+)\.feature$})
22
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
18
23
  end
19
24
 
data/active_data.gemspec CHANGED
@@ -17,6 +17,6 @@ Gem::Specification.new do |gem|
17
17
 
18
18
  gem.add_development_dependency "rake"
19
19
  gem.add_development_dependency "rspec"
20
- gem.add_runtime_dependency "activesupport"
21
- gem.add_runtime_dependency "activemodel"
20
+ gem.add_runtime_dependency "activesupport", "~> 3.0"
21
+ gem.add_runtime_dependency "activemodel", "~> 3.0"
22
22
  end
@@ -10,7 +10,7 @@ module ActiveData
10
10
  end
11
11
 
12
12
  def type
13
- @type ||= options[:type] || String
13
+ @type ||= options[:type] || Object
14
14
  end
15
15
 
16
16
  def values
@@ -18,15 +18,20 @@ module ActiveData
18
18
  end
19
19
 
20
20
  def default
21
- @default ||= options[:default].respond_to?(:call) ? options[:default] : proc { options[:default] }
21
+ @default ||= @options[:default]
22
+ end
23
+
24
+ def default_blank?
25
+ @default_blank ||= !!@options[:default_blank]
22
26
  end
23
27
 
24
28
  def default_value instance
25
- default.call instance
29
+ default.respond_to?(:call) ? default.call(instance) : default unless default.nil?
26
30
  end
27
31
 
28
32
  def type_cast value
29
- type.active_data_type_cast value
33
+ return value if value.instance_of?(type)
34
+ type.active_data_type_cast(value)
30
35
  end
31
36
 
32
37
  def generate_instance_methods context
@@ -49,7 +54,7 @@ module ActiveData
49
54
  EOS
50
55
  end
51
56
 
52
- def generate_singleton_methods context
57
+ def generate_class_methods context
53
58
  if values
54
59
  context.class_eval <<-EOS
55
60
  def #{name}_values
@@ -34,7 +34,7 @@ module ActiveData
34
34
  EOS
35
35
  end
36
36
 
37
- def generate_singleton_methods context
37
+ def generate_class_methods context
38
38
  end
39
39
 
40
40
  end
@@ -1,7 +1,7 @@
1
1
  module ActiveData
2
2
  module Model
3
3
  module Associations
4
- class Many
4
+ class Association
5
5
  attr_reader :name, :klass, :options
6
6
 
7
7
  def initialize name, options = {}
@@ -14,10 +14,16 @@ module ActiveData
14
14
  klass.to_s
15
15
  end
16
16
 
17
- def collection?
18
- true
17
+ def define_accessor klass
18
+ define_reader klass
19
+ define_writer klass
19
20
  end
20
21
 
22
+ def define_reader klass
23
+ end
24
+
25
+ def define_writer klass
26
+ end
21
27
  end
22
28
  end
23
29
  end
@@ -0,0 +1,33 @@
1
+ module ActiveData
2
+ module Model
3
+ module Associations
4
+ class EmbedsMany < Association
5
+
6
+ def collection?
7
+ true
8
+ end
9
+
10
+ def define_reader target
11
+ target.class_eval <<-EOS
12
+ def #{name}
13
+ @#{name} ||= begin
14
+ association = self.class.reflect_on_association('#{name}')
15
+ association.klass.collection
16
+ end
17
+ end
18
+ EOS
19
+ end
20
+
21
+ def define_writer target
22
+ target.class_eval <<-EOS
23
+ def #{name}= value
24
+ association = self.class.reflect_on_association('#{name}')
25
+ @#{name} = association.klass.collection(value)
26
+ end
27
+ EOS
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ module ActiveData
2
+ module Model
3
+ module Associations
4
+ class EmbedsOne < Association
5
+
6
+ def collection?
7
+ false
8
+ end
9
+
10
+ def define_reader target
11
+ target.class_eval <<-EOS
12
+ def #{name}
13
+ @#{name}
14
+ end
15
+ EOS
16
+ end
17
+
18
+ def define_writer target
19
+ target.class_eval <<-EOS
20
+ def #{name}= value
21
+ association = self.class.reflect_on_association('#{name}')
22
+ @#{name} = association.klass.instantiate(value) if value
23
+ end
24
+ EOS
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,4 +1,6 @@
1
- require 'active_data/model/associations/many'
1
+ require 'active_data/model/associations/association'
2
+ require 'active_data/model/associations/embeds_many'
3
+ require 'active_data/model/associations/embeds_one'
2
4
 
3
5
  module ActiveData
4
6
  module Model
@@ -6,14 +8,22 @@ module ActiveData
6
8
  extend ActiveSupport::Concern
7
9
 
8
10
  included do
9
- class_attribute :_associations, :instance_reader => false, :instance_writer => false
10
- self._associations = ActiveSupport::HashWithIndifferentAccess.new
11
+ class_attribute :_associations, instance_reader: false, instance_writer: false
12
+ self._associations = {}
13
+
14
+ { embeds_many: EmbedsMany, embeds_one: EmbedsOne }.each do |(name, association_class)|
15
+ define_singleton_method name do |*args|
16
+ association = association_class.new *args
17
+ association.define_accessor self
18
+ self._associations = _associations.merge(association.name => association)
19
+ end
20
+ end
11
21
  end
12
22
 
13
23
  module ClassMethods
14
24
 
15
25
  def reflect_on_association name
16
- _associations[name]
26
+ _associations[name.to_s]
17
27
  end
18
28
 
19
29
  def associations
@@ -23,27 +33,14 @@ module ActiveData
23
33
  def association_names
24
34
  _associations.keys
25
35
  end
36
+ end
26
37
 
27
- def embeds_many name, options = {}
28
- association = Many.new name, options
29
- define_collection_reader association
30
- define_collection_writer association
31
- self._associations = _associations.merge!(association.name => association)
32
- end
33
-
34
- def define_collection_reader association
35
- define_method association.name do
36
- instance_variable_get("@#{association.name}") || association.klass.collection([])
37
- end
38
- end
39
-
40
- def define_collection_writer association
41
- define_method "#{association.name}=" do |value|
42
- instance_variable_set "@#{association.name}", association.klass.collection(value)
43
- end
38
+ def == other
39
+ super(other) && self.class.association_names.all? do |association|
40
+ send(association) == other.send(association)
44
41
  end
45
-
46
42
  end
43
+
47
44
  end
48
45
  end
49
46
  end
@@ -5,15 +5,21 @@ module ActiveData
5
5
 
6
6
  included do
7
7
  class_attribute :_attributes, :instance_reader => false, :instance_writer => false
8
- self._attributes = ActiveSupport::HashWithIndifferentAccess.new
9
-
10
- extend generated_class_attributes_methods
11
- include generated_instance_attributes_methods
8
+ self._attributes = {}
12
9
 
13
10
  delegate :attribute_default, :to => 'self.class'
14
11
  end
15
12
 
16
13
  module ClassMethods
14
+ def attribute name, options = {}, &block
15
+ attribute = build_attribute(name, options, &block)
16
+ self._attributes = _attributes.merge(attribute.name => attribute)
17
+
18
+ attribute.generate_instance_methods generated_instance_attributes_methods
19
+ attribute.generate_class_methods generated_class_attributes_methods
20
+ attribute
21
+ end
22
+
17
23
  def build_attribute name, options = {}, &block
18
24
  klass = case options[:type].to_s
19
25
  when 'Localized'
@@ -24,47 +30,44 @@ module ActiveData
24
30
  klass.new name, options, &block
25
31
  end
26
32
 
27
- def attribute name, options = {}, &block
28
- attribute = build_attribute(name, options, &block)
29
- self._attributes = _attributes.merge(attribute.name => attribute)
30
-
31
- attribute.generate_instance_methods generated_instance_attributes_methods
32
- attribute.generate_singleton_methods generated_class_attributes_methods
33
- attribute
34
- end
35
-
36
33
  def generated_class_attributes_methods
37
- @generated_class_attributes_methods ||= Module.new
34
+ @generated_class_attributes_methods ||= Module.new.tap { |proxy| extend proxy }
38
35
  end
39
36
 
40
37
  def generated_instance_attributes_methods
41
- @generated_instance_attributes_methods ||= Module.new
38
+ @generated_instance_attributes_methods ||= Module.new.tap { |proxy| include proxy }
42
39
  end
43
40
 
44
41
  def initialize_attributes
45
- _attributes.inject(ActiveSupport::HashWithIndifferentAccess.new) do |result, (name, value)|
46
- result[name] = nil
47
- result
48
- end
42
+ Hash[_attributes.map { |(name, _)| [name, nil] }]
49
43
  end
50
44
  end
51
45
 
52
46
  def read_attribute name
53
- attribute = self.class._attributes[name]
54
- source = @attributes[name].nil? ? attribute.default_value(self) : @attributes[name]
55
- attribute.type_cast source
47
+ name = name.to_sym
48
+ if attributes_cache.key? name
49
+ attributes_cache[name]
50
+ else
51
+ attribute = self.class._attributes[name]
52
+ value = attribute.type_cast @attributes[name]
53
+ use_default = attribute.default_blank? && value.blank? || value.nil?
54
+
55
+ attributes_cache[name] = use_default ? attribute.default_value(self) : value
56
+ end
56
57
  end
57
58
  alias_method :[], :read_attribute
58
59
 
59
60
  def has_attribute? name
60
- @attributes.key? name
61
+ @attributes.key? name.to_sym
61
62
  end
62
63
 
63
64
  def read_attribute_before_type_cast name
64
- @attributes[name]
65
+ @attributes[name.to_sym]
65
66
  end
66
67
 
67
68
  def write_attribute name, value
69
+ name = name.to_sym
70
+ attributes_cache.delete name
68
71
  @attributes[name] = value
69
72
  end
70
73
  alias_method :[]=, :write_attribute
@@ -87,9 +90,10 @@ module ActiveData
87
90
  def attributes= attributes
88
91
  assign_attributes(attributes)
89
92
  end
93
+ alias_method :update_attributes, :attributes=
90
94
 
91
- def update_attributes attributes
92
- self.attributes = attributes
95
+ def write_attributes attributes
96
+ attributes.each { |(name, value)| send("#{name}=", value) }
93
97
  end
94
98
 
95
99
  def reverse_update_attributes attributes
@@ -98,18 +102,20 @@ module ActiveData
98
102
 
99
103
  private
100
104
 
105
+ def attributes_cache
106
+ @attributes_cache ||= {}
107
+ end
108
+
101
109
  def assign_attributes attributes
102
110
  (attributes.presence || {}).each do |(name, value)|
103
- send("#{name}=", value) if respond_to?("#{name}=")
111
+ send("#{name}=", value) if has_attribute?(name) || respond_to?("#{name}=")
104
112
  end
105
- self.attributes
106
113
  end
107
114
 
108
115
  def reverse_assign_attributes attributes
109
116
  (attributes.presence || {}).each do |(name, value)|
110
117
  send("#{name}=", value) if respond_to?("#{name}=") && respond_to?(name) && send(name).blank?
111
118
  end
112
- self.attributes
113
119
  end
114
120
 
115
121
  end
@@ -6,27 +6,24 @@ module ActiveData
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- collectionize Array
9
+ class_attribute :_collection_superclass
10
+ collectionize
10
11
  end
11
12
 
12
13
  module ClassMethods
13
14
 
14
- def collectionize collection_superclass = nil
15
- collection = Class.new(collection_superclass) do
16
- include ActiveData::Model::Collectionizable::Proxy
17
- end
18
- collection.collectible = self
19
-
20
- remove_const :Collection if const_defined? :Collection
21
- const_set :Collection, collection
15
+ def collectionize collection_superclass = Array
16
+ self._collection_superclass = collection_superclass
22
17
  end
23
18
 
24
- def respond_to? method
19
+ def respond_to_missing? method, include_private
25
20
  super || collection_class.superclass.method_defined?(method)
26
21
  end
27
22
 
28
23
  def method_missing method, *args, &block
29
- current_scope.send(method, *args, &block) if collection_class.superclass.method_defined?(method)
24
+ collection_class.superclass.method_defined?(method) ?
25
+ current_scope.send(method, *args, &block) :
26
+ super
30
27
  end
31
28
 
32
29
  def collection source = nil
@@ -34,7 +31,11 @@ module ActiveData
34
31
  end
35
32
 
36
33
  def collection_class
37
- @collection_class ||= const_get(:Collection)
34
+ @collection_class ||= begin
35
+ Class.new(_collection_superclass) do
36
+ include ActiveData::Model::Collectionizable::Proxy
37
+ end.tap { |klass| klass.collectible = self }
38
+ end
38
39
  end
39
40
 
40
41
  def current_scope= value
@@ -7,10 +7,10 @@ module ActiveData
7
7
  module ClassMethods
8
8
  def active_data_type_cast value
9
9
  case value
10
- when String then
11
- value.split(',').map(&:strip)
12
- when Array then
10
+ when ::Array then
13
11
  value
12
+ when ::String then
13
+ value.split(',').map(&:strip)
14
14
  else
15
15
  nil
16
16
  end
@@ -1,4 +1,4 @@
1
- module ActiveData
1
+ module ActiveData
2
2
  module Model
3
3
  module Extensions
4
4
  module BigDecimal
@@ -6,7 +6,7 @@ module ActiveData
6
6
 
7
7
  module ClassMethods
8
8
  def active_data_type_cast value
9
- ::BigDecimal.new value.to_s if value.to_s =~ /\A\d+(?:\.\d*)?\Z/
9
+ ::BigDecimal.new Float(value).to_s rescue nil if value
10
10
  end
11
11
  end
12
12
  end
@@ -6,14 +6,7 @@ module ActiveData
6
6
 
7
7
  module ClassMethods
8
8
  def active_data_type_cast value
9
- case value
10
- when String then
11
- Date.parse(value.to_s) rescue nil
12
- when Date, DateTime, Time then
13
- value.to_date
14
- else
15
- nil
16
- end
9
+ value.to_date rescue nil
17
10
  end
18
11
  end
19
12
  end
@@ -0,0 +1,17 @@
1
+ module ActiveData
2
+ module Model
3
+ module Extensions
4
+ module DateTime
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def active_data_type_cast value
9
+ value.to_datetime rescue nil
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ DateTime.send :include, ActiveData::Model::Extensions::DateTime
@@ -6,7 +6,7 @@ module ActiveData
6
6
 
7
7
  module ClassMethods
8
8
  def active_data_type_cast value
9
- value.try(:to_f) if value.to_s =~ /\A\d+(?:\.\d*)?\Z/
9
+ Float(value) rescue nil if value
10
10
  end
11
11
  end
12
12
  end
@@ -7,7 +7,7 @@ module ActiveData
7
7
  module ClassMethods
8
8
  def active_data_type_cast value
9
9
  case value
10
- when Hash then
10
+ when ::Hash then
11
11
  value
12
12
  else
13
13
  nil
@@ -6,7 +6,7 @@ module ActiveData
6
6
 
7
7
  module ClassMethods
8
8
  def active_data_type_cast value
9
- value.try(:to_i) if value.to_s =~ /\A\d+(?:\.\d*)?\Z/
9
+ Float(value).to_i rescue nil if value
10
10
  end
11
11
  end
12
12
  end
@@ -7,8 +7,8 @@ module ActiveData
7
7
  module ClassMethods
8
8
  def active_data_type_cast value
9
9
  case value
10
- when Hash then
11
- value.stringify_keys!
10
+ when ::Hash then
11
+ value.stringify_keys
12
12
  else
13
13
  nil
14
14
  end
@@ -0,0 +1,17 @@
1
+ module ActiveData
2
+ module Model
3
+ module Extensions
4
+ module Object
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def active_data_type_cast value
9
+ value
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ Object.send :include, ActiveData::Model::Extensions::Object
@@ -0,0 +1,17 @@
1
+ module ActiveData
2
+ module Model
3
+ module Extensions
4
+ module Time
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def active_data_type_cast value
9
+ value.to_time rescue nil
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ Time.send :include, ActiveData::Model::Extensions::Time
@@ -17,13 +17,13 @@ module ActiveData
17
17
  end
18
18
 
19
19
  def read_localized_attribute name, locale = self.class.locale
20
- translations = read_attribute(name)
21
- translations[self.class.fallbacks(locale).detect { |fallback| translations[fallback.to_s] }]
20
+ translations = read_attribute(name.to_s)
21
+ translations[self.class.fallbacks(locale).detect { |fallback| translations[fallback.to_s] }.to_s]
22
22
  end
23
23
  alias_method :read_localized_attribute_before_type_cast, :read_localized_attribute
24
24
 
25
25
  def write_localized_attribute name, value, locale = self.class.locale
26
- translations = read_attribute(name)
26
+ translations = read_attribute(name.to_s)
27
27
  write_attribute(name, translations.merge(locale.to_s => value))
28
28
  end
29
29
  end
@@ -43,10 +43,16 @@ module ActiveData
43
43
 
44
44
  reflection = self.class.reflect_on_association association_name
45
45
 
46
- send("#{association_name}=", attributes_collection.map do |attributes|
47
- reflection.klass.new attributes
48
- end)
46
+ send "#{association_name}=", attributes_collection
47
+ end
48
+
49
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes, assignment_opts = {})
50
+ unless attributes.is_a?(Hash)
51
+ raise ArgumentError, "Hash expected, got #{attributes.class.name} (#{attributes.inspect})"
52
+ end
53
+
54
+ send "#{association_name}=", attributes
49
55
  end
50
56
  end
51
57
  end
52
- end
58
+ end