mixture 0.2.0 → 0.3.0

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/Gemfile +5 -1
  4. data/Gemfile.lock +3 -1
  5. data/README.md +25 -2
  6. data/lib/mixture/attribute.rb +6 -3
  7. data/lib/mixture/coerce/array.rb +20 -5
  8. data/lib/mixture/coerce/base.rb +62 -26
  9. data/lib/mixture/coerce/class.rb +12 -0
  10. data/lib/mixture/coerce/date.rb +10 -10
  11. data/lib/mixture/coerce/datetime.rb +10 -10
  12. data/lib/mixture/coerce/float.rb +9 -9
  13. data/lib/mixture/coerce/hash.rb +10 -4
  14. data/lib/mixture/coerce/integer.rb +9 -9
  15. data/lib/mixture/coerce/nil.rb +10 -10
  16. data/lib/mixture/coerce/object.rb +13 -13
  17. data/lib/mixture/coerce/rational.rb +9 -9
  18. data/lib/mixture/coerce/set.rb +12 -4
  19. data/lib/mixture/coerce/string.rb +9 -9
  20. data/lib/mixture/coerce/symbol.rb +4 -4
  21. data/lib/mixture/coerce/time.rb +10 -9
  22. data/lib/mixture/coerce.rb +28 -8
  23. data/lib/mixture/extensions/coercable.rb +2 -10
  24. data/lib/mixture/extensions.rb +15 -9
  25. data/lib/mixture/types/access.rb +45 -0
  26. data/lib/mixture/types/array.rb +13 -0
  27. data/lib/mixture/types/boolean.rb +20 -0
  28. data/lib/mixture/types/class.rb +16 -0
  29. data/lib/mixture/types/collection.rb +14 -0
  30. data/lib/mixture/types/date.rb +13 -0
  31. data/lib/mixture/types/datetime.rb +13 -0
  32. data/lib/mixture/types/enumerable.rb +14 -0
  33. data/lib/mixture/types/float.rb +12 -0
  34. data/lib/mixture/types/hash.rb +15 -0
  35. data/lib/mixture/types/integer.rb +12 -0
  36. data/lib/mixture/types/nil.rb +11 -0
  37. data/lib/mixture/types/numeric.rb +12 -0
  38. data/lib/mixture/types/object.rb +24 -0
  39. data/lib/mixture/types/rational.rb +13 -0
  40. data/lib/mixture/types/set.rb +16 -0
  41. data/lib/mixture/types/string.rb +12 -0
  42. data/lib/mixture/types/symbol.rb +14 -0
  43. data/lib/mixture/types/time.rb +13 -0
  44. data/lib/mixture/types/type.rb +171 -0
  45. data/lib/mixture/types.rb +110 -0
  46. data/lib/mixture/version.rb +1 -1
  47. data/lib/mixture.rb +22 -2
  48. data/mixture.gemspec +2 -3
  49. metadata +31 -37
  50. data/lib/mixture/type.rb +0 -188
@@ -1,8 +1,8 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require "mixture/coerce/base"
4
- require "mixture/coerce/array"
5
4
  require "mixture/coerce/date"
5
+ require "mixture/coerce/array"
6
6
  require "mixture/coerce/datetime"
7
7
  require "mixture/coerce/float"
8
8
  require "mixture/coerce/hash"
@@ -32,24 +32,46 @@ module Mixture
32
32
  #
33
33
  # @return [Hash{Mixture::Type => Mixture::Coerce::Base}]
34
34
  def self.coercers
35
- @_coercers ||= {}
35
+ @_coercers ||= ThreadSafe::Hash.new
36
36
  end
37
37
 
38
38
  # Returns a block that takes one argument: the value.
39
39
  #
40
- # @param from [Mixture::Type]
40
+ # @param from [Mixture::Types::Type]
41
41
  # The type to coerce from.
42
- # @param to [Mixture::Type]
42
+ # @param to [Mixture::Types::Type]
43
43
  # The type to coerce to.
44
- # @return [Proc{(Object) => Object}]
44
+ # @return [Proc{(Object, Mixture::Types::Object) => Object}]
45
45
  def self.coerce(from, to)
46
46
  coercers
47
47
  .fetch(from) { fail CoercionError, "No coercer for #{from}" }
48
48
  .to(to)
49
49
  end
50
50
 
51
+ # Performs the actual coercion, since blocks require a value and
52
+ # type arguments.
53
+ #
54
+ # @param type [Mixture::Types::Type] The type to coerce to.
55
+ # @param value [Object] The value to coerce.
56
+ # @return [Object] The coerced value.
57
+ def self.perform(type, value)
58
+ to = Types.infer(type)
59
+ from = Types.infer(value)
60
+ block = coerce(from, to)
61
+
62
+ begin
63
+ block.call(value, to)
64
+ rescue CoercionError
65
+ raise
66
+ rescue StandardError => e
67
+ raise CoercionError, "#{e.class}: #{e.message}", e.backtrace
68
+ end
69
+ end
70
+
51
71
  # Registers the default coercions.
52
- def self.load
72
+ #
73
+ # @return [void]
74
+ def self.finalize
53
75
  register Array
54
76
  register Date
55
77
  register DateTime
@@ -64,7 +86,5 @@ module Mixture
64
86
  register Symbol
65
87
  register Time
66
88
  end
67
-
68
- load
69
89
  end
70
90
  end
@@ -7,6 +7,7 @@ module Mixture
7
7
  # Performs the coercion for the attribute and the value.
8
8
  # It is used in a `:update` callback.
9
9
  #
10
+ # @see Coerce.perform
10
11
  # @param attribute [Attribute] The attribute
11
12
  # @param value [Object] The new value.
12
13
  # @return [Object] The new new value.
@@ -14,16 +15,7 @@ module Mixture
14
15
  # running.
15
16
  def self.coerce_attribute(attribute, value)
16
17
  return value unless attribute.options[:type]
17
- attr_type = Type.infer(attribute.options[:type])
18
- value_type = Type.infer(value)
19
-
20
- block = Coerce.coerce(value_type, attr_type)
21
-
22
- begin
23
- block.call(value)
24
- rescue StandardError => e
25
- raise CoercionError, "#{e.class}: #{e.message}", e.backtrace
26
- end
18
+ Coerce.perform(attribute.options[:type], value)
27
19
  end
28
20
 
29
21
  # Called by Ruby when the module is included.
@@ -1,5 +1,10 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require "mixture/extensions/attributable"
4
+ require "mixture/extensions/coercable"
5
+ require "mixture/extensions/hashable"
6
+ require "mixture/extensions/validatable"
7
+
3
8
  module Mixture
4
9
  # All of the extensions of mixture. Handles registration of
5
10
  # extensions, so that extensions can be referend by a name instead
@@ -32,14 +37,15 @@ module Mixture
32
37
  @_extensions ||= {}
33
38
  end
34
39
 
35
- require "mixture/extensions/attributable"
36
- require "mixture/extensions/coercable"
37
- require "mixture/extensions/hashable"
38
- require "mixture/extensions/validatable"
39
-
40
- register :attribute, Attributable
41
- register :coerce, Coercable
42
- register :hash, Hashable
43
- register :validate, Validatable
40
+ # Finalizes the extension module. It registers the extensions in
41
+ # Mixture.
42
+ #
43
+ # @return [void]
44
+ def self.finalize
45
+ register :attribute, Attributable
46
+ register :coerce, Coercable
47
+ register :hash, Hashable
48
+ register :validate, Validatable
49
+ end
44
50
  end
45
51
  end
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # Used by certain types to create subtypes of those types. This
6
+ # is useful in collections and hashes, wherein the collection
7
+ # members and hash keys/values all have types as well (and need
8
+ # to be coerced to them).
9
+ module Access
10
+ # Creates a subtype with the given member types. Any number of
11
+ # subtypes may be used. If a class hasn't been created with the
12
+ # subtypes, it creates a new one.
13
+ #
14
+ # @see #create
15
+ # @param subs [Object] The subtypes to use.
16
+ # @return [Class] The new subtype.
17
+ def [](*subs)
18
+ options[:types].fetch([self, subs]) do
19
+ create(subs)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ # Actually creates the subtype. This should never be called
26
+ # outside of {.[]}. If `:noinfer` is set in the supertype's
27
+ # options, it doesn't infer the type of each subtype; otherwise,
28
+ # it does.
29
+ #
30
+ # @param subs [Array<Object>] The subtypes.
31
+ # @return [Class] The new subtype.
32
+ def create(subs)
33
+ subtype = ::Class.new(self)
34
+ members = if options[:noinfer]
35
+ subs
36
+ else
37
+ subs.map { |sub| Types.infer(sub) }
38
+ end
39
+ name = "#{inspect}[#{members.join(', ')}]"
40
+ subtype.options.merge!(members: members, name: name)
41
+ options[:types][[self, subs]] = subtype
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # The array type. This is a collection, and as such, can be used
6
+ # with the `.[]` accessor.
7
+ class Array < Collection
8
+ options[:primitive] = ::Array
9
+ options[:method] = :to_array
10
+ as :array
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # The boolean type. Unfortunately, ruby doesn't have a clear cut
6
+ # boolean primitive (it has `TrueClass` and `FalseClass`, but no
7
+ # `Boolean` class), so we set the primitive to nil to prevent
8
+ # odd stuff from happening. It can still be matched by other
9
+ # values.
10
+ class Boolean < Object
11
+ options[:primitive] = nil
12
+ as :bool, :boolean, true, false
13
+
14
+ constraints.clear
15
+ constraint do |value|
16
+ [TrueClass, FalseClass].include?(value.class)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A class type. This can be subtyped, and is subtyped for
6
+ # non-primitive classes.
7
+ class Class < Object
8
+ options[:primitive] = ::Class
9
+ options[:noinfer] = true
10
+ options[:member] = Object
11
+ options[:method] = :to_class
12
+ options[:types] = ThreadSafe::Cache.new
13
+ extend Access
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A collection. This is recognized as an actual set of values,
6
+ # rather than just being enumerable. The set of values have a
7
+ # defined type.
8
+ class Collection < Enumerable
9
+ options[:members] = [Object]
10
+ options[:types] = ThreadSafe::Cache.new
11
+ extend Access
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A date type. I don't know why ruby has Date, DateTime, and
6
+ # Time, but someone thinks we need it.
7
+ class Date < Object
8
+ options[:primitive] = ::Date
9
+ options[:method] = :to_date
10
+ as :date
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A datetime type. I don't know why Ruby has a Date, DateTime, and
6
+ # Time type, but someone thinks we needs it.
7
+ class DateTime < Object
8
+ options[:primitive] = ::DateTime
9
+ options[:method] = :to_datetime
10
+ as :datetime
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # An enumerable. This is any value that inherits `Enumerable`.
6
+ class Enumerable < Object
7
+ options[:primitive] = ::Enumerable
8
+ constraint do |value|
9
+ # include Enumerable adds Enumerable to the ancestors list.
10
+ value.class.ancestors.include?(::Enumerable)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A float. Not much to say here.
6
+ class Float < Numeric
7
+ options[:primitive] = ::Float
8
+ options[:method] = :to_float
9
+ as :float
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A hash. This is also accessable, and expects two member types;
6
+ # one for the keys, and one for the values.
7
+ class Hash < Object
8
+ options[:primitive] = ::Hash
9
+ options[:members] = [Object, Object]
10
+ options[:method] = :to_hash
11
+ options[:types] = ThreadSafe::Cache.new
12
+ extend Access
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # An integer. Not much to say here.
6
+ class Integer < Numeric
7
+ options[:primitive] = ::Integer
8
+ options[:method] = :to_integer
9
+ as :int, :integer
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # Represents a nil type. This has no coercions.
6
+ class Nil < Object
7
+ options[:primitive] = ::NilClass
8
+ as nil
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A numeric. This is inherited by {Integer}, {Float}, and
6
+ # {Rational}. Those should be used for coercion instead of this.
7
+ # This just helps represent the type hirearchy.
8
+ class Numeric < Object
9
+ options[:primitive] = ::Numeric
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # An object. This adds the basic constraints all types (that
6
+ # inherit from Object) have; i.e., it must be an object, and
7
+ # it must be the type's primitive.
8
+ class Object < Type
9
+ options[:primitive] = ::Object
10
+ options[:method] = :to_object
11
+ as :object
12
+
13
+ constraint do |value|
14
+ # This may seem a bit odd, but this returns false for
15
+ # BasicObject; and since this is meant to represent Objects,
16
+ # we want to make sure that the value isn't a BasicObject.
17
+ # rubocop:disable Style/CaseEquality
18
+ ::Object === value
19
+ # rubocop:enable Style/CaseEquality
20
+ end
21
+ constraint { |value| value.is_a?(options[:primitive]) }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A rational. I've personally never used this, but I don't see it
6
+ # as a bad thing.
7
+ class Rational < Numeric
8
+ options[:primitive] = ::Rational
9
+ options[:method] = :to_rational
10
+ as :rational
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A set. This is similar to array, but with brilliant iteration
6
+ # and indexing(?) times.
7
+ #
8
+ # @see Array
9
+ # @see http://ruby-doc.org/stdlib/libdoc/set/rdoc/Set.html
10
+ class Set < Collection
11
+ options[:primitive] = ::Set
12
+ options[:method] = :to_set
13
+ as :set
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A string.
6
+ class String < Object
7
+ options[:primitive] = ::String
8
+ options[:method] = :to_string
9
+ as :str, :string
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A symbol. Don't really use this for coercion; in Ruby 2.2.2,
6
+ # they added garbage collection for symbols; however, it is still
7
+ # not a brilliant idea to turn user input into symbols.
8
+ class Symbol < Object
9
+ options[:primitive] = ::Symbol
10
+ options[:method] = :to_symbol
11
+ as :symbol
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A time type. I'm not sure why Ruby has a Date, DateTime, and
6
+ # Time object, but someone thinks we need it.
7
+ class Time < Object
8
+ options[:primitive] = ::Time
9
+ options[:method] = :to_time
10
+ as :time
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,171 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Types
5
+ # A single type. A type is _never_ instantized; it is used as a
6
+ # class to represent a type of value.
7
+ class Type
8
+ private_class_method :new
9
+ private_class_method :allocate
10
+
11
+ # The options for the type. This is inherited by subtypes.
12
+ #
13
+ # @return [Hash{Symbol => Object}]
14
+ def self.options
15
+ @options ||= ThreadSafe::Hash.new
16
+ end
17
+
18
+ # Constraints on the type. A value will not match the type if
19
+ # any of these constraints fail. This is inherited by subtypes.
20
+ #
21
+ # @return [Array<Proc{(Object) => Boolean}>]
22
+ def self.constraints
23
+ @constraints ||= ThreadSafe::Array.new
24
+ end
25
+
26
+ # Returns all of the names that this type can go under. This is
27
+ # used for {Types.mappings} and for inference.
28
+ #
29
+ # @see Types.mappings
30
+ # @return [Array<Symbol, Object>]
31
+ def self.mappings
32
+ @mappings ||= ThreadSafe::Array.new
33
+ end
34
+
35
+ # Sets some names that this type can go under. This is used
36
+ # for {Type.mappings} and for inference.
37
+ #
38
+ # @see Types.mappings
39
+ # @see .mappings
40
+ # @param names [Symbol, Object]
41
+ # @return [void]
42
+ def self.as(*names)
43
+ mappings.concat(names)
44
+ end
45
+
46
+ # Called by ruby when a class inherits this class. This just
47
+ # propogates the options and constraints to the new subclass.
48
+ #
49
+ # @param sub [Class] The new subclass.
50
+ # @return [void]
51
+ def self.inherited(sub)
52
+ Types.types << sub unless sub.anonymous?
53
+ sub.options.merge!(options)
54
+ sub.constraints.concat(constraints)
55
+ end
56
+
57
+ # Checks if the given value passes all of the constraints
58
+ # defined on this type. Each constraint is executed within the
59
+ # context of the class, to provide access to {.options}.
60
+ #
61
+ # @param value [Object] The object to check.
62
+ # @return [Boolean]
63
+ def self.matches?(value)
64
+ constraints.all? do |constraint|
65
+ class_exec(value, &constraint)
66
+ end
67
+ end
68
+
69
+ # Determines if this type is equal to another type. This is
70
+ # used by Ruby's hash, and is used to make an anonymous type
71
+ # equal to its supertype (e.g.
72
+ # `Types::Array[Types::Integer] == Types::Array`), mainly for
73
+ # coercion.
74
+ #
75
+ # @param other [Object]
76
+ # @return [Boolean]
77
+ def self.eql?(other)
78
+ if anonymous?
79
+ superclass == other
80
+ elsif other.respond_to?(:anonymous?) && other.anonymous?
81
+ other.superclass.eql?(self)
82
+ else
83
+ super
84
+ end
85
+ end
86
+
87
+ # (see .eql?)
88
+ def self.==(other)
89
+ eql?(other)
90
+ end
91
+
92
+ # Used by ruby's Hash, this determines the hash of the type. If
93
+ # this is anonymous, it uses its supertype's hash.
94
+ #
95
+ # @see .eql?
96
+ # @return [Numeric]
97
+ def self.hash
98
+ if anonymous?
99
+ superclass.hash
100
+ else
101
+ super
102
+ end
103
+ end
104
+
105
+ # If this class is anonymous. This is counting on the fact that
106
+ # the name of an anonymous class is nil; however, assigning a
107
+ # class to a constant on initialization of a class will make
108
+ # that class non-anonymous.
109
+ #
110
+ # @return [Boolean]
111
+ def self.anonymous?
112
+ name.nil?
113
+ end
114
+
115
+ # Inspects the class. If the class is anonymous, it uses the
116
+ # `:name` value in the options if it exists. Otherwise, it
117
+ # passes it up the chain.
118
+ #
119
+ # @return [String]
120
+ def self.inspect
121
+ to_s
122
+ end
123
+
124
+ # (see .inspect)
125
+ def self.to_s
126
+ if anonymous? && options.key?(:name)
127
+ options[:name]
128
+ else
129
+ super
130
+ end
131
+ end
132
+
133
+ # This is used to determine if a specific object is this type.
134
+ # There can be many constraints, and they're all used to check
135
+ # the given object.
136
+ #
137
+ # @note Constraints are not meant for _validation_. Constraints
138
+ # are **purely** meant for identification, and should be used
139
+ # as such.
140
+ #
141
+ # @overload self.constraint(value)
142
+ # Adds the value as a constraint. Ideally, this should
143
+ # respond to `#call`.
144
+ #
145
+ # @example Subclass constraint.
146
+ # constraint(->(value) { value.is_a?(String) })
147
+ # @param value [#call] The constraint to add.
148
+ # @return [void]
149
+ #
150
+ # @overload self.constraint(&block)
151
+ # Adds the block as a constraint.
152
+ #
153
+ # @example Subclass constraint.
154
+ # constraint { |value| value.is_a?(String) }
155
+ # @yield [value]
156
+ # @yieldparam value [Object] The value to check.
157
+ # @yieldreturn [Boolean] If the constraint was passed.
158
+ # @return [void]
159
+ def self.constraint(value = Undefined, &block)
160
+ if block_given?
161
+ constraints << block
162
+ elsif value != Undefined
163
+ constraints << value
164
+ else
165
+ fail ArgumentError, "Expected an argument or a block, " \
166
+ "got neither"
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end