virtus2 2.0.1

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