active_data 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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