motion_virtus 1.0.0.beta0

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 (59) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +445 -0
  3. data/lib/motion_virtus.rb +13 -0
  4. data/lib/project/attribute/accessor/builder.rb +69 -0
  5. data/lib/project/attribute/accessor/lazy_accessor.rb +39 -0
  6. data/lib/project/attribute/accessor.rb +100 -0
  7. data/lib/project/attribute/accessor_method.rb +73 -0
  8. data/lib/project/attribute/array.rb +24 -0
  9. data/lib/project/attribute/boolean.rb +52 -0
  10. data/lib/project/attribute/class.rb +23 -0
  11. data/lib/project/attribute/coercer.rb +43 -0
  12. data/lib/project/attribute/collection/coercible_writer.rb +83 -0
  13. data/lib/project/attribute/collection.rb +56 -0
  14. data/lib/project/attribute/date.rb +36 -0
  15. data/lib/project/attribute/date_time.rb +38 -0
  16. data/lib/project/attribute/decimal.rb +23 -0
  17. data/lib/project/attribute/default_value/from_callable.rb +37 -0
  18. data/lib/project/attribute/default_value/from_clonable.rb +37 -0
  19. data/lib/project/attribute/default_value/from_symbol.rb +37 -0
  20. data/lib/project/attribute/default_value.rb +49 -0
  21. data/lib/project/attribute/embedded_value/open_struct_coercer.rb +43 -0
  22. data/lib/project/attribute/embedded_value/struct_coercer.rb +42 -0
  23. data/lib/project/attribute/embedded_value.rb +69 -0
  24. data/lib/project/attribute/float.rb +30 -0
  25. data/lib/project/attribute/hash/coercible_writer.rb +78 -0
  26. data/lib/project/attribute/hash.rb +66 -0
  27. data/lib/project/attribute/integer.rb +27 -0
  28. data/lib/project/attribute/numeric.rb +25 -0
  29. data/lib/project/attribute/object.rb +13 -0
  30. data/lib/project/attribute/reader.rb +39 -0
  31. data/lib/project/attribute/set.rb +22 -0
  32. data/lib/project/attribute/string.rb +24 -0
  33. data/lib/project/attribute/symbol.rb +23 -0
  34. data/lib/project/attribute/time.rb +36 -0
  35. data/lib/project/attribute/writer/coercible.rb +45 -0
  36. data/lib/project/attribute/writer.rb +73 -0
  37. data/lib/project/attribute.rb +292 -0
  38. data/lib/project/attribute_set.rb +260 -0
  39. data/lib/project/class_inclusions.rb +41 -0
  40. data/lib/project/class_methods.rb +102 -0
  41. data/lib/project/configuration.rb +65 -0
  42. data/lib/project/const_missing_extensions.rb +16 -0
  43. data/lib/project/extensions.rb +101 -0
  44. data/lib/project/instance_methods.rb +165 -0
  45. data/lib/project/module_builder.rb +92 -0
  46. data/lib/project/module_extensions.rb +72 -0
  47. data/lib/project/stubs/date.rb +2 -0
  48. data/lib/project/stubs/date_time.rb +2 -0
  49. data/lib/project/stubs/decimal.rb +2 -0
  50. data/lib/project/stubs/ostruct.rb +149 -0
  51. data/lib/project/stubs/set.rb +767 -0
  52. data/lib/project/stubs.rb +5 -0
  53. data/lib/project/support/equalizer.rb +147 -0
  54. data/lib/project/support/options.rb +114 -0
  55. data/lib/project/support/type_lookup.rb +109 -0
  56. data/lib/project/value_object.rb +139 -0
  57. data/lib/project/version.rb +3 -0
  58. data/lib/project/virtus.rb +128 -0
  59. metadata +158 -0
@@ -0,0 +1,5 @@
1
+ motion_require 'stubs/date'
2
+ motion_require 'stubs/date_time'
3
+ motion_require 'stubs/decimal'
4
+ motion_require 'stubs/ostruct'
5
+ motion_require 'stubs/set'
@@ -0,0 +1,147 @@
1
+ module Virtus
2
+
3
+ # Define equality, equivalence and inspection methods
4
+ module Equalizer
5
+
6
+ def self.new(name, keys = [])
7
+ Module.new.tap do |m|
8
+ m.module_eval do
9
+ extend Implementation
10
+ @name = name
11
+ @keys = keys
12
+ define_methods
13
+ include_comparison_methods
14
+ end
15
+ end
16
+ end
17
+
18
+ module Implementation
19
+
20
+ def inspect
21
+ "#<#{Equalizer.name}:0x#{ '%x' % (object_id << 1) }>"
22
+ end
23
+
24
+ # Initialize an Equalizer with the given keys
25
+ #
26
+ # Will use the keys with which it is initialized to define #cmp?,
27
+ # #hash, and #inspect
28
+ #
29
+ # @param [String] name
30
+ #
31
+ # @param [Array<Symbol>] keys
32
+ #
33
+ # @return [undefined]
34
+ #
35
+ # @api private
36
+ def initialize(name, keys = [])
37
+ @name = name.dup.freeze
38
+ @keys = keys.dup
39
+ define_methods
40
+ include_comparison_methods
41
+ end
42
+
43
+ # Append a key and compile the equality methods
44
+ #
45
+ # @return [Equalizer] self
46
+ #
47
+ # @api private
48
+ def <<(key)
49
+ @keys << key
50
+ self
51
+ end
52
+
53
+ private
54
+
55
+ # Define the equalizer methods based on #keys
56
+ #
57
+ # @return [undefined]
58
+ #
59
+ # @api private
60
+ def define_methods
61
+ define_cmp_method
62
+ define_hash_method
63
+ define_inspect_method
64
+ end
65
+
66
+ # Define an #cmp? method based on the instance's values identified by #keys
67
+ #
68
+ # @return [undefined]
69
+ #
70
+ # @api private
71
+ def define_cmp_method
72
+ keys = @keys
73
+ define_method(:cmp?) do |comparator, other|
74
+ keys.all? { |key| send(key).send(comparator, other.send(key)) }
75
+ end
76
+ end
77
+
78
+ # Define a #hash method based on the instance's values identified by #keys
79
+ #
80
+ # @return [undefined]
81
+ #
82
+ # @api private
83
+ def define_hash_method
84
+ keys = @keys
85
+ define_method(:hash) do
86
+ keys.map { |key| send(key).hash }.reduce(self.class.hash, :^)
87
+ end
88
+ end
89
+
90
+ # Define an inspect method that reports the values of the instance's keys
91
+ #
92
+ # @return [undefined]
93
+ #
94
+ # @api private
95
+ def define_inspect_method
96
+ name, keys = @name, @keys
97
+ define_method(:inspect) do
98
+ "#<#{name}#{keys.map { |key| " #{key}=#{send(key).inspect}" }.join}>"
99
+ end
100
+ end
101
+
102
+ # Include the #eql? and #== methods
103
+ #
104
+ # @return [undefined]
105
+ #
106
+ # @api private
107
+ def include_comparison_methods
108
+ module_eval { include Methods }
109
+ end
110
+
111
+ # The comparison methods
112
+ module Methods
113
+
114
+ # Compare the object with other object for equality
115
+ #
116
+ # @example
117
+ # object.eql?(other) # => true or false
118
+ #
119
+ # @param [Object] other
120
+ # the other object to compare with
121
+ #
122
+ # @return [Boolean]
123
+ #
124
+ # @api public
125
+ def eql?(other)
126
+ instance_of?(other.class) && cmp?(__method__, other)
127
+ end
128
+
129
+ # Compare the object with other object for equivalency
130
+ #
131
+ # @example
132
+ # object == other # => true or false
133
+ #
134
+ # @param [Object] other
135
+ # the other object to compare with
136
+ #
137
+ # @return [Boolean]
138
+ #
139
+ # @api public
140
+ def ==(other)
141
+ other.kind_of?(self.class) && cmp?(__method__, other)
142
+ end
143
+
144
+ end # module Methods
145
+ end # module Implementation
146
+ end # module Equalizer
147
+ end # module Virtus
@@ -0,0 +1,114 @@
1
+ module Virtus
2
+
3
+ # A module that adds class and instance level options
4
+ module Options
5
+
6
+ Undefined = Object.new.freeze
7
+
8
+ # Returns default options hash for a given attribute class
9
+ #
10
+ # @example
11
+ # Virtus::Attribute::String.options
12
+ # # => {:primitive => String}
13
+ #
14
+ # @return [Hash]
15
+ # a hash of default option values
16
+ #
17
+ # @api public
18
+ def options
19
+ accepted_options.each_with_object({}) do |option_name, options|
20
+ option_value = send(option_name)
21
+ options[option_name] = option_value unless option_value.nil?
22
+ end
23
+ end
24
+
25
+ # Returns an array of valid options
26
+ #
27
+ # @example
28
+ # Virtus::Attribute::String.accepted_options
29
+ # # => [:primitive, :accessor, :reader, :writer]
30
+ #
31
+ # @return [Array]
32
+ # the array of valid option names
33
+ #
34
+ # @api public
35
+ def accepted_options
36
+ @accepted_options ||= []
37
+ end
38
+
39
+ # Defines which options are valid for a given attribute class
40
+ #
41
+ # @example
42
+ # class MyAttribute < Virtus::Attribute::Object
43
+ # accept_options :foo, :bar
44
+ # end
45
+ #
46
+ # @return [self]
47
+ #
48
+ # @api public
49
+ def accept_options(*new_options)
50
+ add_accepted_options(new_options)
51
+ new_options.each { |option| define_option_method(option) }
52
+ descendants.each { |descendant| descendant.add_accepted_options(new_options) }
53
+ self
54
+ end
55
+
56
+ protected
57
+
58
+ # Adds a reader/writer method for the give option name
59
+ #
60
+ # @return [undefined]
61
+ #
62
+ # @api private
63
+ def define_option_method(option)
64
+ self.class.instance_eval do
65
+ define_method("#{option}") do |value=Undefined|
66
+ return instance_variable_get("@#{option}") if value.equal?(Undefined)
67
+ instance_variable_set("@#{option}", value)
68
+ self
69
+ end
70
+ end
71
+ end
72
+
73
+ # Sets default options
74
+ #
75
+ # @param [#each] new_options
76
+ # options to be set
77
+ #
78
+ # @return [self]
79
+ #
80
+ # @api private
81
+ def set_options(new_options)
82
+ new_options.each { |pair| send(*pair) }
83
+ self
84
+ end
85
+
86
+ # Adds new options that an attribute class can accept
87
+ #
88
+ # @param [#to_ary] new_options
89
+ # new options to be added
90
+ #
91
+ # @return [self]
92
+ #
93
+ # @api private
94
+ def add_accepted_options(new_options)
95
+ accepted_options.concat(new_options)
96
+ self
97
+ end
98
+
99
+ private
100
+
101
+ # Adds descendant to descendants array and inherits default options
102
+ #
103
+ # @param [Class] descendant
104
+ #
105
+ # @return [undefined]
106
+ #
107
+ # @api private
108
+ def inherited(descendant)
109
+ super
110
+ descendant.add_accepted_options(accepted_options).set_options(options)
111
+ end
112
+
113
+ end # module Options
114
+ 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
+ type = 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.reverse_each do |descendant|
85
+ descendant_primitive = descendant.primitive
86
+ next unless primitive <= descendant_primitive
87
+ type = descendant if type.nil? || 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 && 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,139 @@
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
+ base.instance_eval do
35
+ include ::Virtus
36
+ include InstanceMethods
37
+ extend ClassMethods
38
+ private :attributes=
39
+ end
40
+ end
41
+
42
+ private_class_method :included
43
+
44
+ module InstanceMethods
45
+
46
+ # the #get_attributes method accept a Proc object that will filter
47
+ # out an attribute when the block returns false. the ValueObject
48
+ # needs all the attributes, so we allow every attribute.
49
+ FILTER_NONE = proc { true }
50
+
51
+ # @api private
52
+ def with(attribute_updates)
53
+ self.class.new(
54
+ attribute_set.get(self, &FILTER_NONE).merge(attribute_updates)
55
+ )
56
+ end
57
+
58
+ # ValueObjects are immutable and can't be cloned
59
+ #
60
+ # They always represent the same value
61
+ #
62
+ # @example
63
+ #
64
+ # value_object.clone === value_object # => true
65
+ #
66
+ # @return [self]
67
+ #
68
+ # @api public
69
+ def clone
70
+ self
71
+ end
72
+ alias dup clone
73
+
74
+ end
75
+
76
+ module ClassMethods
77
+
78
+ # Define an attribute on the receiver
79
+ #
80
+ # The Attribute will have private writer methods (eg., immutable instances)
81
+ # and be used in equality/equivalence comparisons
82
+ #
83
+ # @example
84
+ # class GeoLocation
85
+ # include Virtus::ValueObject
86
+ #
87
+ # attribute :latitude, Float
88
+ # attribute :longitude, Float
89
+ # end
90
+ #
91
+ # @see Virtus::ClassMethods.attribute
92
+ #
93
+ # @return [self]
94
+ #
95
+ # @api public
96
+ def attribute(name, type, options = {})
97
+ equalizer << name
98
+ super name, type, options.merge(:writer => :private)
99
+ end
100
+
101
+ # Define and include a module that provides Value Object semantics
102
+ #
103
+ # Included module will have #inspect, #eql?, #== and #hash
104
+ # methods whose definition is based on the _keys_ argument
105
+ #
106
+ # @example
107
+ # virtus_class.equalizer
108
+ #
109
+ # @return [Equalizer]
110
+ # An Equalizer module which defines #inspect, #eql?, #== and #hash
111
+ # for instances of this class
112
+ #
113
+ # @api public
114
+ def equalizer
115
+ @equalizer ||=
116
+ begin
117
+ equalizer = Equalizer.new(name || inspect)
118
+ include equalizer
119
+ equalizer
120
+ end
121
+ end
122
+
123
+ # The list of writer methods that can be mass-assigned to in #attributes=
124
+ #
125
+ # @return [Set]
126
+ #
127
+ # @api private
128
+ def allowed_writer_methods
129
+ @allowed_writer_methods ||=
130
+ begin
131
+ allowed_writer_methods = super
132
+ allowed_writer_methods += attribute_set.map{|attr| "#{attr.name}="}
133
+ allowed_writer_methods.to_set.freeze
134
+ end
135
+ end
136
+
137
+ end # module ClassMethods
138
+ end # module ValueObject
139
+ end # module Virtus
@@ -0,0 +1,3 @@
1
+ module Virtus
2
+ VERSION = '1.0.0.beta0'
3
+ end
@@ -0,0 +1,128 @@
1
+ motion_require 'stubs.rb'
2
+ motion_require 'support/equalizer'
3
+ motion_require 'support/type_lookup'
4
+ motion_require 'support/options'
5
+
6
+ module Virtus
7
+
8
+ # Provides args for const_get and const_defined? to make them behave
9
+ # consistently across different versions of ruby
10
+ EXTRA_CONST_ARGS = (RUBY_VERSION < '1.9' ? [] : [ false ]).freeze
11
+
12
+ #Undefined = Object.new.freeze
13
+
14
+ # Extends base class or a module with virtus methods
15
+ #
16
+ # @param [Object] object
17
+ #
18
+ # @return [undefined]
19
+ #
20
+ # @api private
21
+ def self.included(object)
22
+ super
23
+ if object.class.name == "Class"
24
+ object.send(:include, ClassInclusions)
25
+ else
26
+ object.extend(ModuleExtensions)
27
+ end
28
+ end
29
+ private_class_method :included
30
+
31
+ # Extends an object with virtus extensions
32
+ #
33
+ # @param [Object] object
34
+ #
35
+ # @return [undefined]
36
+ #
37
+ # @api private
38
+ def self.extended(object)
39
+ object.extend(Extensions)
40
+ end
41
+ private_class_method :extended
42
+
43
+ # Sets the global coercer configuration
44
+ #
45
+ # @example
46
+ # Virtus.coercer do |config|
47
+ # config.string.boolean_map = { true => '1', false => '0' }
48
+ # end
49
+ #
50
+ # @return [Coercible::Coercer]
51
+ #
52
+ # @api public
53
+ def self.coercer(&block)
54
+ configuration.coercer(&block)
55
+ end
56
+
57
+ # Sets the global coercion configuration value
58
+ #
59
+ # @param [Boolean] value
60
+ #
61
+ # @return [Virtus]
62
+ #
63
+ # @api public
64
+ def self.coerce=(value)
65
+ configuration.coerce = value
66
+ self
67
+ end
68
+
69
+ # Returns the global coercion setting
70
+ #
71
+ # @return [Boolean]
72
+ #
73
+ # @api public
74
+ def self.coerce
75
+ configuration.coerce
76
+ end
77
+
78
+ # Provides access to the global Virtus configuration
79
+ #
80
+ # @example
81
+ # Virtus.config do |config|
82
+ # config.coerce = false
83
+ # end
84
+ #
85
+ # @return [Configuration]
86
+ #
87
+ # @api public
88
+ def self.config(&block)
89
+ configuration.call(&block)
90
+ end
91
+
92
+ # Provides access to the Virtus module builder
93
+ # see Virtus::ModuleBuilder
94
+ #
95
+ # @example
96
+ # MyVirtusModule = Virtus.module { |mod|
97
+ # mod.coerce = true
98
+ # mod.string.boolean_map = { 'yup' => true, 'nope' => false }
99
+ # }
100
+ #
101
+ # class Book
102
+ # include MyVirtusModule
103
+ #
104
+ # attribute :published, Boolean
105
+ # end
106
+ #
107
+ # # This could be made more succinct as well
108
+ # class OtherBook
109
+ # include Virtus.module { |m| m.coerce = false }
110
+ # end
111
+ #
112
+ # @return [Module]
113
+ #
114
+ # @api public
115
+ def self.module(&block)
116
+ ModuleBuilder.call(&block)
117
+ end
118
+
119
+ # Global configuration instance
120
+ #
121
+ # @ return [Configuration]
122
+ #
123
+ # @api private
124
+ def self.configuration
125
+ @configuration ||= Configuration.new
126
+ end
127
+
128
+ end