virtus2 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +39 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +1 -0
  5. data/CONTRIBUTING.md +18 -0
  6. data/Changelog.md +258 -0
  7. data/Gemfile +10 -0
  8. data/Guardfile +19 -0
  9. data/LICENSE +20 -0
  10. data/README.md +630 -0
  11. data/Rakefile +15 -0
  12. data/TODO.md +6 -0
  13. data/lib/virtus/attribute/accessor.rb +103 -0
  14. data/lib/virtus/attribute/boolean.rb +55 -0
  15. data/lib/virtus/attribute/builder.rb +182 -0
  16. data/lib/virtus/attribute/coercer.rb +45 -0
  17. data/lib/virtus/attribute/coercible.rb +20 -0
  18. data/lib/virtus/attribute/collection.rb +103 -0
  19. data/lib/virtus/attribute/default_value/from_callable.rb +35 -0
  20. data/lib/virtus/attribute/default_value/from_clonable.rb +35 -0
  21. data/lib/virtus/attribute/default_value/from_symbol.rb +35 -0
  22. data/lib/virtus/attribute/default_value.rb +51 -0
  23. data/lib/virtus/attribute/embedded_value.rb +67 -0
  24. data/lib/virtus/attribute/enum.rb +45 -0
  25. data/lib/virtus/attribute/hash.rb +130 -0
  26. data/lib/virtus/attribute/lazy_default.rb +18 -0
  27. data/lib/virtus/attribute/nullify_blank.rb +24 -0
  28. data/lib/virtus/attribute/strict.rb +26 -0
  29. data/lib/virtus/attribute.rb +245 -0
  30. data/lib/virtus/attribute_set.rb +240 -0
  31. data/lib/virtus/builder/hook_context.rb +51 -0
  32. data/lib/virtus/builder.rb +133 -0
  33. data/lib/virtus/class_inclusions.rb +48 -0
  34. data/lib/virtus/class_methods.rb +90 -0
  35. data/lib/virtus/coercer.rb +41 -0
  36. data/lib/virtus/configuration.rb +72 -0
  37. data/lib/virtus/const_missing_extensions.rb +18 -0
  38. data/lib/virtus/extensions.rb +105 -0
  39. data/lib/virtus/instance_methods.rb +218 -0
  40. data/lib/virtus/model.rb +68 -0
  41. data/lib/virtus/module_extensions.rb +88 -0
  42. data/lib/virtus/support/equalizer.rb +128 -0
  43. data/lib/virtus/support/options.rb +113 -0
  44. data/lib/virtus/support/type_lookup.rb +109 -0
  45. data/lib/virtus/value_object.rb +150 -0
  46. data/lib/virtus/version.rb +3 -0
  47. data/lib/virtus.rb +310 -0
  48. data/spec/integration/attributes_attribute_spec.rb +28 -0
  49. data/spec/integration/building_module_spec.rb +90 -0
  50. data/spec/integration/collection_member_coercion_spec.rb +96 -0
  51. data/spec/integration/custom_attributes_spec.rb +42 -0
  52. data/spec/integration/custom_collection_attributes_spec.rb +101 -0
  53. data/spec/integration/default_values_spec.rb +87 -0
  54. data/spec/integration/defining_attributes_spec.rb +86 -0
  55. data/spec/integration/embedded_value_spec.rb +50 -0
  56. data/spec/integration/extending_objects_spec.rb +35 -0
  57. data/spec/integration/hash_attributes_coercion_spec.rb +54 -0
  58. data/spec/integration/inheritance_spec.rb +42 -0
  59. data/spec/integration/injectible_coercers_spec.rb +48 -0
  60. data/spec/integration/mass_assignment_with_accessors_spec.rb +44 -0
  61. data/spec/integration/overriding_virtus_spec.rb +46 -0
  62. data/spec/integration/required_attributes_spec.rb +25 -0
  63. data/spec/integration/struct_as_embedded_value_spec.rb +28 -0
  64. data/spec/integration/using_modules_spec.rb +55 -0
  65. data/spec/integration/value_object_with_custom_constructor_spec.rb +42 -0
  66. data/spec/integration/virtus/instance_level_attributes_spec.rb +23 -0
  67. data/spec/integration/virtus/value_object_spec.rb +99 -0
  68. data/spec/shared/constants_helpers.rb +9 -0
  69. data/spec/shared/freeze_method_behavior.rb +40 -0
  70. data/spec/shared/idempotent_method_behaviour.rb +5 -0
  71. data/spec/shared/options_class_method.rb +19 -0
  72. data/spec/spec_helper.rb +41 -0
  73. data/spec/unit/virtus/attribute/boolean/coerce_spec.rb +43 -0
  74. data/spec/unit/virtus/attribute/boolean/value_coerced_predicate_spec.rb +25 -0
  75. data/spec/unit/virtus/attribute/class_methods/build_spec.rb +180 -0
  76. data/spec/unit/virtus/attribute/class_methods/coerce_spec.rb +32 -0
  77. data/spec/unit/virtus/attribute/coerce_spec.rb +129 -0
  78. data/spec/unit/virtus/attribute/coercible_predicate_spec.rb +20 -0
  79. data/spec/unit/virtus/attribute/collection/class_methods/build_spec.rb +105 -0
  80. data/spec/unit/virtus/attribute/collection/coerce_spec.rb +74 -0
  81. data/spec/unit/virtus/attribute/collection/value_coerced_predicate_spec.rb +31 -0
  82. data/spec/unit/virtus/attribute/comparison_spec.rb +20 -0
  83. data/spec/unit/virtus/attribute/custom_collection_spec.rb +29 -0
  84. data/spec/unit/virtus/attribute/defined_spec.rb +20 -0
  85. data/spec/unit/virtus/attribute/embedded_value/class_methods/build_spec.rb +70 -0
  86. data/spec/unit/virtus/attribute/embedded_value/coerce_spec.rb +91 -0
  87. data/spec/unit/virtus/attribute/get_spec.rb +32 -0
  88. data/spec/unit/virtus/attribute/hash/class_methods/build_spec.rb +106 -0
  89. data/spec/unit/virtus/attribute/hash/coerce_spec.rb +92 -0
  90. data/spec/unit/virtus/attribute/lazy_predicate_spec.rb +20 -0
  91. data/spec/unit/virtus/attribute/rename_spec.rb +16 -0
  92. data/spec/unit/virtus/attribute/required_predicate_spec.rb +19 -0
  93. data/spec/unit/virtus/attribute/set_default_value_spec.rb +107 -0
  94. data/spec/unit/virtus/attribute/set_spec.rb +29 -0
  95. data/spec/unit/virtus/attribute/value_coerced_predicate_spec.rb +19 -0
  96. data/spec/unit/virtus/attribute_set/append_spec.rb +47 -0
  97. data/spec/unit/virtus/attribute_set/define_reader_method_spec.rb +36 -0
  98. data/spec/unit/virtus/attribute_set/define_writer_method_spec.rb +36 -0
  99. data/spec/unit/virtus/attribute_set/each_spec.rb +65 -0
  100. data/spec/unit/virtus/attribute_set/element_reference_spec.rb +17 -0
  101. data/spec/unit/virtus/attribute_set/element_set_spec.rb +64 -0
  102. data/spec/unit/virtus/attribute_set/merge_spec.rb +34 -0
  103. data/spec/unit/virtus/attribute_set/reset_spec.rb +71 -0
  104. data/spec/unit/virtus/attribute_spec.rb +229 -0
  105. data/spec/unit/virtus/attributes_reader_spec.rb +41 -0
  106. data/spec/unit/virtus/attributes_writer_spec.rb +51 -0
  107. data/spec/unit/virtus/class_methods/finalize_spec.rb +67 -0
  108. data/spec/unit/virtus/class_methods/new_spec.rb +39 -0
  109. data/spec/unit/virtus/config_spec.rb +13 -0
  110. data/spec/unit/virtus/element_reader_spec.rb +21 -0
  111. data/spec/unit/virtus/element_writer_spec.rb +19 -0
  112. data/spec/unit/virtus/freeze_spec.rb +41 -0
  113. data/spec/unit/virtus/model_spec.rb +197 -0
  114. data/spec/unit/virtus/module_spec.rb +174 -0
  115. data/spec/unit/virtus/set_default_attributes_spec.rb +32 -0
  116. data/spec/unit/virtus/value_object_spec.rb +138 -0
  117. data/virtus2.gemspec +26 -0
  118. metadata +225 -0
@@ -0,0 +1,128 @@
1
+ module Virtus
2
+
3
+ # Define equality, equivalence and inspection methods
4
+ class Equalizer < Module
5
+
6
+ # Initialize an Equalizer with the given keys
7
+ #
8
+ # Will use the keys with which it is initialized to define #cmp?,
9
+ # #hash, and #inspect
10
+ #
11
+ # @param [String] name
12
+ #
13
+ # @param [Array<Symbol>] keys
14
+ #
15
+ # @return [undefined]
16
+ #
17
+ # @api private
18
+ def initialize(name, keys = [])
19
+ @name = name.dup.freeze
20
+ @keys = keys.dup
21
+ define_methods
22
+ include_comparison_methods
23
+ end
24
+
25
+ # Append a key and compile the equality methods
26
+ #
27
+ # @return [Equalizer] self
28
+ #
29
+ # @api private
30
+ def <<(key)
31
+ @keys << key
32
+ self
33
+ end
34
+
35
+ private
36
+
37
+ # Define the equalizer methods based on #keys
38
+ #
39
+ # @return [undefined]
40
+ #
41
+ # @api private
42
+ def define_methods
43
+ define_cmp_method
44
+ define_hash_method
45
+ define_inspect_method
46
+ end
47
+
48
+ # Define an #cmp? method based on the instance's values identified by #keys
49
+ #
50
+ # @return [undefined]
51
+ #
52
+ # @api private
53
+ def define_cmp_method
54
+ keys = @keys
55
+ define_method(:cmp?) do |comparator, other|
56
+ keys.all? { |key| send(key).send(comparator, other.send(key)) }
57
+ end
58
+ end
59
+
60
+ # Define a #hash method based on the instance's values identified by #keys
61
+ #
62
+ # @return [undefined]
63
+ #
64
+ # @api private
65
+ def define_hash_method
66
+ keys = @keys
67
+ define_method(:hash) do
68
+ keys.map { |key| send(key) }.push(self.class).hash
69
+ end
70
+ end
71
+
72
+ # Define an inspect method that reports the values of the instance's keys
73
+ #
74
+ # @return [undefined]
75
+ #
76
+ # @api private
77
+ def define_inspect_method
78
+ name, keys = @name, @keys
79
+ define_method(:inspect) do
80
+ "#<#{name}#{keys.map { |key| " #{key}=#{send(key).inspect}" }.join}>"
81
+ end
82
+ end
83
+
84
+ # Include the #eql? and #== methods
85
+ #
86
+ # @return [undefined]
87
+ #
88
+ # @api private
89
+ def include_comparison_methods
90
+ module_eval { include Methods }
91
+ end
92
+
93
+ # The comparison methods
94
+ module Methods
95
+
96
+ # Compare the object with other object for equality
97
+ #
98
+ # @example
99
+ # object.eql?(other) # => true or false
100
+ #
101
+ # @param [Object] other
102
+ # the other object to compare with
103
+ #
104
+ # @return [Boolean]
105
+ #
106
+ # @api public
107
+ def eql?(other)
108
+ instance_of?(other.class) && cmp?(__method__, other)
109
+ end
110
+
111
+ # Compare the object with other object for equivalency
112
+ #
113
+ # @example
114
+ # object == other # => true or false
115
+ #
116
+ # @param [Object] other
117
+ # the other object to compare with
118
+ #
119
+ # @return [Boolean]
120
+ #
121
+ # @api public
122
+ def ==(other)
123
+ other.kind_of?(self.class) && cmp?(__method__, other)
124
+ end
125
+
126
+ end # module Methods
127
+ end # class Equalizer
128
+ end # module Virtus
@@ -0,0 +1,113 @@
1
+ module Virtus
2
+
3
+ # A module that adds class and instance level options
4
+ module Options
5
+
6
+ # Returns default options hash for a given attribute class
7
+ #
8
+ # @example
9
+ # Virtus::Attribute::String.options
10
+ # # => {:primitive => String}
11
+ #
12
+ # @return [Hash]
13
+ # a hash of default option values
14
+ #
15
+ # @api public
16
+ def options
17
+ accepted_options.each_with_object({}) do |option_name, options|
18
+ option_value = send(option_name)
19
+ options[option_name] = option_value unless option_value.nil?
20
+ end
21
+ end
22
+
23
+ # Returns an array of valid options
24
+ #
25
+ # @example
26
+ # Virtus::Attribute::String.accepted_options
27
+ # # => [:primitive, :accessor, :reader, :writer]
28
+ #
29
+ # @return [Array]
30
+ # the array of valid option names
31
+ #
32
+ # @api public
33
+ def accepted_options
34
+ @accepted_options ||= []
35
+ end
36
+
37
+ # Defines which options are valid for a given attribute class
38
+ #
39
+ # @example
40
+ # class MyAttribute < Virtus::Attribute
41
+ # accept_options :foo, :bar
42
+ # end
43
+ #
44
+ # @return [self]
45
+ #
46
+ # @api public
47
+ def accept_options(*new_options)
48
+ add_accepted_options(new_options)
49
+ new_options.each { |option| define_option_method(option) }
50
+ descendants.each { |descendant| descendant.add_accepted_options(new_options) }
51
+ self
52
+ end
53
+
54
+ protected
55
+
56
+ # Adds a reader/writer method for the give option name
57
+ #
58
+ # @return [undefined]
59
+ #
60
+ # @api private
61
+ def define_option_method(option)
62
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
63
+ def self.#{option}(value = Undefined) # def self.primitive(value = Undefined)
64
+ @#{option} = nil unless defined?(@#{option}) # @primitive = nil unless defined?(@primitive)
65
+ return @#{option} if value.equal?(Undefined) # return @primitive if value.equal?(Undefined)
66
+ @#{option} = value # @primitive = value
67
+ self # self
68
+ end # end
69
+ RUBY
70
+ end
71
+
72
+ # Sets default options
73
+ #
74
+ # @param [#each] new_options
75
+ # options to be set
76
+ #
77
+ # @return [self]
78
+ #
79
+ # @api private
80
+ def set_options(new_options)
81
+ new_options.each { |pair| send(*pair) }
82
+ self
83
+ end
84
+
85
+ # Adds new options that an attribute class can accept
86
+ #
87
+ # @param [#to_ary] new_options
88
+ # new options to be added
89
+ #
90
+ # @return [self]
91
+ #
92
+ # @api private
93
+ def add_accepted_options(new_options)
94
+ accepted_options.concat(new_options)
95
+ self
96
+ end
97
+
98
+ private
99
+
100
+ # Adds descendant to descendants array and inherits default options
101
+ #
102
+ # @param [Class] descendant
103
+ #
104
+ # @return [undefined]
105
+ #
106
+ # @api private
107
+ def inherited(descendant)
108
+ super
109
+ descendant.add_accepted_options(accepted_options).set_options(options)
110
+ end
111
+
112
+ end # module Options
113
+ end # module Virtus
@@ -0,0 +1,109 @@
1
+ module Virtus
2
+
3
+ # A module that adds type lookup to a class
4
+ module TypeLookup
5
+
6
+ TYPE_FORMAT = /\A[A-Z]\w*\z/.freeze
7
+
8
+ # Set cache ivar on the model
9
+ #
10
+ # @param [Class] model
11
+ #
12
+ # @return [undefined]
13
+ #
14
+ # @api private
15
+ def self.extended(model)
16
+ model.instance_variable_set('@type_lookup_cache', {})
17
+ end
18
+
19
+ # Returns a descendant based on a name or class
20
+ #
21
+ # @example
22
+ # MyClass.determine_type('String') # => MyClass::String
23
+ #
24
+ # @param [Class, #to_s] class_or_name
25
+ # name of a class or a class itself
26
+ #
27
+ # @return [Class]
28
+ # a descendant
29
+ #
30
+ # @return [nil]
31
+ # nil if the type cannot be determined by the class_or_name
32
+ #
33
+ # @api public
34
+ def determine_type(class_or_name)
35
+ @type_lookup_cache[class_or_name] ||= determine_type_and_cache(class_or_name)
36
+ end
37
+
38
+ # Return the default primitive supported
39
+ #
40
+ # @return [Class]
41
+ #
42
+ # @api private
43
+ def primitive
44
+ raise NotImplementedError, "#{self}.primitive must be implemented"
45
+ end
46
+
47
+ private
48
+
49
+ # @api private
50
+ def determine_type_and_cache(class_or_name)
51
+ case class_or_name
52
+ when singleton_class
53
+ determine_type_from_descendant(class_or_name)
54
+ when Class
55
+ determine_type_from_primitive(class_or_name)
56
+ else
57
+ determine_type_from_string(class_or_name.to_s)
58
+ end
59
+ end
60
+
61
+ # Return the class given a descendant
62
+ #
63
+ # @param [Class] descendant
64
+ #
65
+ # @return [Class]
66
+ #
67
+ # @api private
68
+ def determine_type_from_descendant(descendant)
69
+ descendant if descendant < self
70
+ end
71
+
72
+ # Return the class given a primitive
73
+ #
74
+ # @param [Class] primitive
75
+ #
76
+ # @return [Class]
77
+ #
78
+ # @return [nil]
79
+ # nil if the type cannot be determined by the primitive
80
+ #
81
+ # @api private
82
+ def determine_type_from_primitive(primitive)
83
+ type = nil
84
+ descendants.select(&:primitive).reverse_each do |descendant|
85
+ descendant_primitive = descendant.primitive
86
+ next unless primitive <= descendant_primitive
87
+ type = descendant if type.nil? or type.primitive > descendant_primitive
88
+ end
89
+ type
90
+ end
91
+
92
+ # Return the class given a string
93
+ #
94
+ # @param [String] string
95
+ #
96
+ # @return [Class]
97
+ #
98
+ # @return [nil]
99
+ # nil if the type cannot be determined by the string
100
+ #
101
+ # @api private
102
+ def determine_type_from_string(string)
103
+ if string =~ TYPE_FORMAT and const_defined?(string, *EXTRA_CONST_ARGS)
104
+ const_get(string, *EXTRA_CONST_ARGS)
105
+ end
106
+ end
107
+
108
+ end # module TypeLookup
109
+ end # module Virtus
@@ -0,0 +1,150 @@
1
+ module Virtus
2
+
3
+ # Include this Module for Value Object semantics
4
+ #
5
+ # The idea is that instances should be immutable and compared based on state
6
+ # (rather than identity, as is typically the case)
7
+ #
8
+ # @example
9
+ # class GeoLocation
10
+ # include Virtus::ValueObject
11
+ # attribute :latitude, Float
12
+ # attribute :longitude, Float
13
+ # end
14
+ #
15
+ # location = GeoLocation.new(:latitude => 10, :longitude => 100)
16
+ # same_location = GeoLocation.new(:latitude => 10, :longitude => 100)
17
+ # location == same_location #=> true
18
+ # hash = { location => :foo }
19
+ # hash[same_location] #=> :foo
20
+ module ValueObject
21
+
22
+ # Callback to configure including Class as a Value Object
23
+ #
24
+ # Including Class will include Virtus and have additional
25
+ # value object semantics defined in this module
26
+ #
27
+ # @return [Undefined]
28
+ #
29
+ # TODO: stacking modules is getting painful
30
+ # time for Facets' module_inheritance, ActiveSupport::Concern or the like
31
+ #
32
+ # @api private
33
+ def self.included(base)
34
+ Virtus.warn "Virtus::ValueObject is deprecated and will be removed in 1.0.0 #{caller.first}"
35
+
36
+ base.instance_eval do
37
+ include Virtus
38
+ include InstanceMethods
39
+ extend ClassMethods
40
+ extend AllowedWriterMethods
41
+ private :attributes=
42
+ end
43
+ end
44
+
45
+ private_class_method :included
46
+
47
+ module InstanceMethods
48
+
49
+ # ValueObjects are immutable and can't be cloned
50
+ #
51
+ # They always represent the same value
52
+ #
53
+ # @example
54
+ #
55
+ # value_object.clone === value_object # => true
56
+ #
57
+ # @return [self]
58
+ #
59
+ # @api public
60
+ def clone
61
+ self
62
+ end
63
+ alias dup clone
64
+
65
+ # Create a new ValueObject by combining the passed attribute hash with
66
+ # the instances attributes.
67
+ #
68
+ # @example
69
+ #
70
+ # number = PhoneNumber.new(kind: "mobile", number: "123-456-78-90")
71
+ # number.with(number: "987-654-32-10")
72
+ # # => #<PhoneNumber kind="mobile" number="987-654-32-10">
73
+ #
74
+ # @return [Object]
75
+ #
76
+ # @api public
77
+ def with(attribute_updates)
78
+ self.class.new(attribute_set.get(self).merge(attribute_updates))
79
+ end
80
+
81
+ end
82
+
83
+ module AllowedWriterMethods
84
+ # The list of writer methods that can be mass-assigned to in #attributes=
85
+ #
86
+ # @return [Set]
87
+ #
88
+ # @api private
89
+ def allowed_writer_methods
90
+ @allowed_writer_methods ||=
91
+ begin
92
+ allowed_writer_methods = super
93
+ allowed_writer_methods += attribute_set.map{|attr| "#{attr.name}="}
94
+ allowed_writer_methods.to_set.freeze
95
+ end
96
+ end
97
+ end
98
+
99
+ module ClassMethods
100
+
101
+ # Define an attribute on the receiver
102
+ #
103
+ # The Attribute will have private writer methods (eg., immutable instances)
104
+ # and be used in equality/equivalence comparisons
105
+ #
106
+ # @example
107
+ # class GeoLocation
108
+ # include Virtus::ValueObject
109
+ #
110
+ # attribute :latitude, Float
111
+ # attribute :longitude, Float
112
+ # end
113
+ #
114
+ # @see Virtus::ClassMethods.attribute
115
+ #
116
+ # @return [self]
117
+ #
118
+ # @api public
119
+ def attribute(name, type, options = {})
120
+ equalizer << name
121
+ super name, type, options.merge(:writer => :private)
122
+ end
123
+
124
+ # Define and include a module that provides Value Object semantics
125
+ #
126
+ # Included module will have #inspect, #eql?, #== and #hash
127
+ # methods whose definition is based on the _keys_ argument
128
+ #
129
+ # @example
130
+ # virtus_class.equalizer
131
+ #
132
+ # @return [Equalizer]
133
+ # An Equalizer module which defines #inspect, #eql?, #== and #hash
134
+ # for instances of this class
135
+ #
136
+ # @api public
137
+ def equalizer
138
+ @equalizer ||=
139
+ begin
140
+ equalizer = Virtus::Equalizer.new(name || inspect)
141
+ include equalizer
142
+ equalizer
143
+ end
144
+ end
145
+
146
+ end # module ClassMethods
147
+
148
+ end # module ValueObject
149
+
150
+ end # module Virtus