mixture 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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