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
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ require "rspec/core/rake_task"
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+ task default: [:spec]
5
+
6
+ begin
7
+ require "rubocop/rake_task"
8
+
9
+ Rake::Task[:default].enhance [:rubocop]
10
+
11
+ RuboCop::RakeTask.new do |task|
12
+ task.options << "--display-cop-names"
13
+ end
14
+ rescue LoadError
15
+ end
data/TODO.md ADDED
@@ -0,0 +1,6 @@
1
+ 1.0.0.rc ROADMAP:
2
+
3
+ - Refactor to use soon-to-be-extracted axiom-types
4
+ - Add ability to pass in custom reader/writer *objects*
5
+ - Refactor to use extracted equalizer gem
6
+ - Tune Adamantium usage to memoize common methods in Attribute
@@ -0,0 +1,103 @@
1
+ module Virtus
2
+ class Attribute
3
+
4
+ # Accessor extension provides methods to read and write attributes
5
+ #
6
+ # @example
7
+ #
8
+ # attribute = Virtus::Attribute.build(String, :name => :email)
9
+ # model = Class.new { attr_reader :email }
10
+ # object = model.new
11
+ #
12
+ # attribute.set(object, 'jane@doe.com')
13
+ # attribute.get(object) # => 'jane@doe.com'
14
+ #
15
+ module Accessor
16
+
17
+ # Return name of this accessor attribute
18
+ #
19
+ # @return [Symbol]
20
+ #
21
+ # @api public
22
+ attr_reader :name
23
+
24
+ # Return instance_variable_name used by this accessor
25
+ #
26
+ # @api private
27
+ attr_reader :instance_variable_name
28
+
29
+ # @api private
30
+ def self.extended(descendant)
31
+ super
32
+ name = descendant.options.fetch(:name).to_sym
33
+ descendant.instance_variable_set('@name', name)
34
+ descendant.instance_variable_set('@instance_variable_name', "@#{name}")
35
+ end
36
+
37
+ # Return if attribute value is defined
38
+ #
39
+ # @param [Object] instance
40
+ #
41
+ # @return [Boolean]
42
+ #
43
+ # @api public
44
+ def defined?(instance)
45
+ instance.instance_variable_defined?(instance_variable_name)
46
+ end
47
+
48
+ # Return value of the attribute
49
+ #
50
+ # @param [Object] instance
51
+ #
52
+ # @return [Object]
53
+ #
54
+ # @api public
55
+ def get(instance)
56
+ instance.instance_variable_get(instance_variable_name)
57
+ end
58
+
59
+ # Set value of the attribute
60
+ #
61
+ # @param [Object] instance
62
+ # @param [Object] value
63
+ #
64
+ # @return [Object] value that was set
65
+ #
66
+ # @api public
67
+ def set(instance, value)
68
+ instance.instance_variable_set(instance_variable_name, value)
69
+ end
70
+
71
+ # Set default value
72
+ #
73
+ # @param [Object] instance
74
+ #
75
+ # @return [Object] value that was set
76
+ #
77
+ # @api public
78
+ def set_default_value(instance)
79
+ set(instance, default_value.call(instance, self))
80
+ end
81
+
82
+ # Returns a Boolean indicating whether the reader method is public
83
+ #
84
+ # @return [Boolean]
85
+ #
86
+ # @api private
87
+ def public_reader?
88
+ options[:reader] == :public
89
+ end
90
+
91
+ # Returns a Boolean indicating whether the writer method is public
92
+ #
93
+ # @return [Boolean]
94
+ #
95
+ # @api private
96
+ def public_writer?
97
+ options[:writer] == :public
98
+ end
99
+
100
+ end # Accessor
101
+
102
+ end # Attribute
103
+ end # Virtus
@@ -0,0 +1,55 @@
1
+ module Virtus
2
+ class Attribute
3
+
4
+ # Boolean attribute allows true or false values to be set
5
+ # Additionally it adds boolean reader method, like "admin?"
6
+ #
7
+ # @example
8
+ # class Post
9
+ # include Virtus
10
+ #
11
+ # attribute :published, Boolean
12
+ # end
13
+ #
14
+ # post = Post.new(:published => false)
15
+ # post.published? # => false
16
+ #
17
+ class Boolean < Attribute
18
+ primitive TrueClass
19
+
20
+ # @api private
21
+ def self.build_type(*)
22
+ Axiom::Types::Boolean
23
+ end
24
+
25
+ # Returns if the given value is either true or false
26
+ #
27
+ # @example
28
+ # boolean = Virtus::Attribute::Boolean.new(:bool)
29
+ # boolean.value_coerced?(true) # => true
30
+ # boolean.value_coerced?(false) # => true
31
+ # boolean.value_coerced?(1) # => false
32
+ # boolean.value_coerced?('true') # => false
33
+ #
34
+ # @return [Boolean]
35
+ #
36
+ # @api public
37
+ def value_coerced?(value)
38
+ value.equal?(true) || value.equal?(false)
39
+ end
40
+
41
+ # Creates an attribute reader method as a query
42
+ #
43
+ # @param [Module] mod
44
+ #
45
+ # @return [undefined]
46
+ #
47
+ # @api private
48
+ def define_accessor_methods(attribute_set)
49
+ super
50
+ attribute_set.define_reader_method(self, "#{name}?", options[:reader])
51
+ end
52
+
53
+ end # class Boolean
54
+ end # class Attribute
55
+ end # module Virtus
@@ -0,0 +1,182 @@
1
+ module Virtus
2
+
3
+ # Attribute placeholder used when type constant is passed as a string or symbol
4
+ #
5
+ # @private
6
+ class PendingAttribute
7
+ attr_reader :type, :options, :name
8
+
9
+ # @api private
10
+ def initialize(type, options)
11
+ @type, @options = type.to_s, options
12
+ @name = options[:name]
13
+ end
14
+
15
+ # @api private
16
+ def finalize
17
+ Attribute::Builder.call(determine_type, options).finalize
18
+ end
19
+
20
+ # @api private
21
+ def finalized?
22
+ false
23
+ end
24
+
25
+ # @api private
26
+ def determine_type
27
+ if type.include?('::')
28
+ Virtus.constantize(type)
29
+ else
30
+ Object.const_get(type)
31
+ end
32
+ end
33
+
34
+ end # PendingAttribute
35
+
36
+ # Extracts the actual type primitive from input type
37
+ #
38
+ # @private
39
+ class TypeDefinition
40
+ attr_reader :type, :primitive
41
+
42
+ # @api private
43
+ def initialize(type)
44
+ @type = type
45
+ initialize_primitive
46
+ end
47
+
48
+ # @api private
49
+ def pending?
50
+ @pending if defined?(@pending)
51
+ end
52
+
53
+ private
54
+
55
+ # @api private
56
+ def initialize_primitive
57
+ @primitive =
58
+ if type.instance_of?(String) || type.instance_of?(Symbol)
59
+ if !type.to_s.include?('::') && Object.const_defined?(type)
60
+ Object.const_get(type)
61
+ elsif not Attribute::Builder.determine_type(type)
62
+ @pending = true
63
+ type
64
+ else
65
+ type
66
+ end
67
+ elsif not type.is_a?(Class)
68
+ type.class
69
+ else
70
+ type
71
+ end
72
+ end
73
+ end
74
+
75
+ class Attribute
76
+
77
+ # Builder is used to set up an attribute instance based on input type and options
78
+ #
79
+ # @private
80
+ class Builder
81
+ attr_reader :attribute, :options, :type_definition, :klass, :type
82
+
83
+ # @api private
84
+ def self.call(type, options = {})
85
+ type_definition = TypeDefinition.new(type)
86
+
87
+ if type_definition.pending?
88
+ PendingAttribute.new(type, options)
89
+ else
90
+ new(type_definition, options).attribute
91
+ end
92
+ end
93
+
94
+ # @api private
95
+ def self.determine_type(klass, default = nil)
96
+ type = Attribute.determine_type(klass)
97
+
98
+ if klass.is_a?(Class)
99
+ type ||=
100
+ if klass < Axiom::Types::Type
101
+ determine_type(klass.primitive)
102
+ elsif EmbeddedValue.handles?(klass)
103
+ EmbeddedValue
104
+ elsif klass < Enumerable && !(klass <= Range)
105
+ Collection
106
+ end
107
+ end
108
+
109
+ type || default
110
+ end
111
+
112
+ # @api private
113
+ def initialize(type_definition, options)
114
+ @type_definition = type_definition
115
+
116
+ initialize_class
117
+ initialize_type
118
+ initialize_options(options)
119
+ initialize_default_value
120
+ initialize_coercer
121
+ initialize_attribute
122
+ end
123
+
124
+ private
125
+
126
+ # @api private
127
+ def initialize_class
128
+ @klass = self.class.determine_type(type_definition.primitive, Attribute)
129
+ end
130
+
131
+ # @api private
132
+ def initialize_type
133
+ @type = klass.build_type(type_definition)
134
+ end
135
+
136
+ # @api private
137
+ def initialize_options(options)
138
+ @options = klass.options.merge(:coerce => Virtus.coerce).update(options)
139
+ klass.merge_options!(type, @options)
140
+ determine_visibility
141
+ end
142
+
143
+ # @api private
144
+ def initialize_default_value
145
+ options.update(:default_value => DefaultValue.build(options[:default]))
146
+ end
147
+
148
+ # @api private
149
+ def initialize_coercer
150
+ options.update(:coercer => determine_coercer)
151
+ end
152
+
153
+ # @api private
154
+ def initialize_attribute
155
+ @attribute = klass.new(type, options)
156
+
157
+ @attribute.extend(Accessor) if options[:name]
158
+ @attribute.extend(Coercible) if options[:coerce]
159
+ @attribute.extend(NullifyBlank) if options[:nullify_blank]
160
+ @attribute.extend(Strict) if options[:strict]
161
+ @attribute.extend(LazyDefault) if options[:lazy]
162
+
163
+ @attribute.finalize if options[:finalize]
164
+ end
165
+
166
+ # @api private
167
+ def determine_coercer
168
+ options.fetch(:coercer) { klass.build_coercer(type, options) }
169
+ end
170
+
171
+ # @api private
172
+ def determine_visibility
173
+ default_accessor = options.fetch(:accessor)
174
+ reader_visibility = options.fetch(:reader, default_accessor)
175
+ writer_visibility = options.fetch(:writer, default_accessor)
176
+ options.update(:reader => reader_visibility, :writer => writer_visibility)
177
+ end
178
+
179
+ end # class Builder
180
+
181
+ end # class Attribute
182
+ end # module Virtus
@@ -0,0 +1,45 @@
1
+ module Virtus
2
+ class Attribute
3
+
4
+ # Coercer accessor wrapper
5
+ #
6
+ # @api private
7
+ class Coercer < Virtus::Coercer
8
+
9
+ # @api private
10
+ attr_reader :method, :coercers
11
+
12
+ # Initialize a new coercer object
13
+ #
14
+ # @param [Object] coercers accessor
15
+ # @param [Symbol] coercion method
16
+ #
17
+ # @return [undefined]
18
+ #
19
+ # @api private
20
+ def initialize(type, coercers)
21
+ super(type)
22
+ @method = type.coercion_method
23
+ @coercers = coercers
24
+ end
25
+
26
+ # Coerce given value
27
+ #
28
+ # @return [Object]
29
+ #
30
+ # @api private
31
+ def call(value)
32
+ coercers[value.class].public_send(method, value)
33
+ rescue ::Coercible::UnsupportedCoercion
34
+ value
35
+ end
36
+
37
+ # @api public
38
+ def success?(primitive, value)
39
+ coercers[primitive].coerced?(value)
40
+ end
41
+
42
+ end # class Coercer
43
+
44
+ end # class Attribute
45
+ end # module Virtus
@@ -0,0 +1,20 @@
1
+ module Virtus
2
+ class Attribute
3
+
4
+ # Attribute extension providing coercion when setting an attribute value
5
+ #
6
+ module Coercible
7
+
8
+ # Coerce value before setting
9
+ #
10
+ # @see Accessor#set
11
+ #
12
+ # @api public
13
+ def set(instance, value)
14
+ super(instance, coerce(value))
15
+ end
16
+
17
+ end # Coercible
18
+
19
+ end # Attribute
20
+ end # Virtus
@@ -0,0 +1,103 @@
1
+ module Virtus
2
+ class Attribute
3
+
4
+ # Collection attribute handles enumerable-like types
5
+ #
6
+ # Handles coercing members to the designated member type.
7
+ #
8
+ class Collection < Attribute
9
+ default Proc.new { |_, attribute| attribute.primitive.new }
10
+
11
+ # @api private
12
+ attr_reader :member_type
13
+
14
+ # FIXME: temporary hack, remove when Axiom::Type works with EV as member_type
15
+ Type = Struct.new(:primitive, :member_type) do
16
+ def self.infer(type, primitive)
17
+ return type if axiom_type?(type)
18
+
19
+ klass = Axiom::Types.infer(type)
20
+ member = infer_member_type(type) || Object
21
+
22
+ if EmbeddedValue.handles?(member) || pending?(member)
23
+ Type.new(primitive, member)
24
+ else
25
+ klass.new {
26
+ primitive primitive
27
+ member_type Axiom::Types.infer(member)
28
+ }
29
+ end
30
+ end
31
+
32
+ def self.pending?(primitive)
33
+ primitive.is_a?(String) || primitive.is_a?(Symbol)
34
+ end
35
+
36
+ def self.axiom_type?(type)
37
+ type.is_a?(Class) && type < Axiom::Types::Type
38
+ end
39
+
40
+ def self.infer_member_type(type)
41
+ return unless type.respond_to?(:count)
42
+
43
+ member_type =
44
+ if type.count > 1
45
+ raise NotImplementedError, "build SumType from list of types (#{type})"
46
+ else
47
+ type.first
48
+ end
49
+
50
+ if member_type.is_a?(Class) && member_type < Attribute && member_type.primitive
51
+ member_type.primitive
52
+ else
53
+ member_type
54
+ end
55
+ end
56
+
57
+ def coercion_method
58
+ :to_array
59
+ end
60
+ end
61
+
62
+ # @api private
63
+ def self.build_type(definition)
64
+ Type.infer(definition.type, definition.primitive)
65
+ end
66
+
67
+ # @api private
68
+ def self.merge_options!(type, options)
69
+ options[:member_type] ||= Attribute.build(type.member_type, strict: options[:strict])
70
+ end
71
+
72
+ # @api public
73
+ def coerce(value)
74
+ coerced = super
75
+
76
+ return coerced unless coerced.respond_to?(:each_with_object)
77
+
78
+ coerced.each_with_object(primitive.new) do |entry, collection|
79
+ collection << member_type.coerce(entry)
80
+ end
81
+ end
82
+
83
+ # @api public
84
+ def value_coerced?(value)
85
+ super && value.all? { |item| member_type.value_coerced? item }
86
+ end
87
+
88
+ # @api private
89
+ def finalize
90
+ return self if finalized?
91
+ @member_type = @options[:member_type].finalize
92
+ super
93
+ end
94
+
95
+ # @api private
96
+ def finalized?
97
+ super && member_type.finalized?
98
+ end
99
+
100
+ end # class Collection
101
+
102
+ end # class Attribute
103
+ end # module Virtus