mixture 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2be9a2d68accc0bae55d6a86edac6d0d0d0fd034
4
- data.tar.gz: 28db784d61898c2f4bfc5ed546f5644724ea627f
3
+ metadata.gz: 3e1746012422c7e9f856706e2820d0423f404784
4
+ data.tar.gz: 0591ce1acb37f9805c37b06298bf7f4c10c4f3f7
5
5
  SHA512:
6
- metadata.gz: 88764ff3ef98d101f7acc87221cb715a9544a47bc330dc679a07bf9ec0696c1eb8bd1d792ccb7f9e602418289d40416195cd26b62b06011ac4cd0370bc46a3c3
7
- data.tar.gz: 9fcfa02187da824b8d21eb726526230f178f5c48ec9eb2f638d3214bd08a29d22f5aeb740a7be9278ab4ffc780c1161ae4b1eb8a76d6afd0c1a8ab1da0602e18
6
+ metadata.gz: 6a2b4634e7d03ef8ed5e8c4730895e497bd1c98307f87f012f27ab4d4a1d401912076c39b366194d52cdeaf1f9beb2dcf673c57729b6f8f679a0656bb39280f1
7
+ data.tar.gz: a15206cb105329873a6890deeb46d8aa797385835315568a427c3e98272a8d3862f3af158777a9afbe077c546621b3af42b89ac7427a1a2e5bc073558407669e
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  /spec/examples.txt
11
+ /.rbenv-gemsets
data/.travis.yml CHANGED
@@ -4,3 +4,6 @@ rvm:
4
4
  - 2.1.0
5
5
  - 1.9.3
6
6
  - rbx
7
+ script:
8
+ - bundle exec rubocop --format clang
9
+ - bundle exec rspec spec
data/Gemfile.lock CHANGED
@@ -1,11 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mixture (0.1.0)
4
+ mixture (0.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
+ ast (2.0.0)
10
+ astrolabe (1.3.0)
11
+ parser (>= 2.2.0.pre.3, < 3.0)
9
12
  coderay (1.1.0)
10
13
  coveralls (0.8.2)
11
14
  json (~> 1.8)
@@ -23,10 +26,14 @@ GEM
23
26
  method_source (0.8.2)
24
27
  mime-types (2.6.1)
25
28
  netrc (0.10.3)
29
+ parser (2.2.2.6)
30
+ ast (>= 1.1, < 3.0)
31
+ powerpack (0.1.1)
26
32
  pry (0.10.1)
27
33
  coderay (~> 1.1.0)
28
34
  method_source (~> 0.8.1)
29
35
  slop (~> 3.4)
36
+ rainbow (2.0.0)
30
37
  rake (10.4.2)
31
38
  rest-client (1.8.0)
32
39
  http-cookie (>= 1.0.2, < 2.0)
@@ -45,6 +52,13 @@ GEM
45
52
  diff-lcs (>= 1.2.0, < 2.0)
46
53
  rspec-support (~> 3.3.0)
47
54
  rspec-support (3.3.0)
55
+ rubocop (0.32.1)
56
+ astrolabe (~> 1.3)
57
+ parser (>= 2.2.2.5, < 3.0)
58
+ powerpack (~> 0.1)
59
+ rainbow (>= 1.99.1, < 3.0)
60
+ ruby-progressbar (~> 1.4)
61
+ ruby-progressbar (1.7.5)
48
62
  simplecov (0.10.0)
49
63
  docile (~> 1.1.0)
50
64
  json (~> 1.8)
@@ -69,6 +83,7 @@ DEPENDENCIES
69
83
  pry
70
84
  rake
71
85
  rspec
86
+ rubocop
72
87
 
73
88
  BUNDLED WITH
74
89
  1.10.5
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Mixture
2
2
 
3
+ [![Build Status](https://travis-ci.org/medcat/mixture.svg?branch=master)](https://travis-ci.org/medcat/mixture) [![Coverage Status](https://coveralls.io/repos/medcat/mixture/badge.svg?branch=master&service=github)](https://coveralls.io/github/medcat/mixture?branch=master) [![Gem Version](https://badge.fury.io/rb/mixture.svg)](http://badge.fury.io/rb/mixture)
4
+
3
5
  ## Installation
4
6
 
5
7
  Add this line to your application's Gemfile:
@@ -12,22 +14,106 @@ And then execute:
12
14
 
13
15
  $ bundle
14
16
 
15
- Or install it yourself as:
17
+ ## Usage
16
18
 
17
- $ gem install mixture
19
+ It's simple, really.
18
20
 
19
- ## Usage
21
+ ```ruby
22
+ module MyLibrary
23
+ class MyClass
24
+ include Mixture::Model
25
+ end
26
+ end
27
+ ```
28
+
29
+ This provides a few simple things. First off, it provides the
30
+ `.attribute` class method. The attribute class method allows you to
31
+ define an attribute on your class. Attributes use the instance
32
+ variables just like `attr_reader`/`attr_writer` do - but it also
33
+ allows coercion on assignment, as well. Defining an attribute is as
34
+ simple as `attribute :name`. This provides the `:name` and `:name=`
35
+ methods on the instance.
36
+
37
+ You also get access to the `#attributes=`, `#attributes`, and
38
+ `#attribute` instance methods. The first group assigns attributes,
39
+ running it through any update callbacks defined. The second retrieves
40
+ the attributes on the instance, even if they weren't assigned. The
41
+ last provides easy get/set functionality.
42
+
43
+ If you want to take advantage of the coercion abilities, just add a
44
+ `:type` key to the options for the attribute:
45
+
46
+ ```ruby
47
+ module MyLibrary
48
+ class MyClass
49
+ include Mixture::Model
50
+
51
+ attribute :name, type: String
52
+ end
53
+ end
54
+ ```
20
55
 
21
- TODO: Write usage instructions here
56
+ This will automagically cause `name` to be coerced to a string on
57
+ assignment.
22
58
 
23
- ## Development
59
+ For validation, use the `.validate` class method:
24
60
 
25
- To install this gem onto your local machine, run
26
- `bundle exec rake install`. To release a new version, update the
27
- version number in `version.rb`, and then run
28
- `bundle exec rake release`, which will create a git tag for the
29
- version, push git commits and tags, and push the `.gem` file to
30
- [rubygems.org](https://rubygems.org).
61
+ ```ruby
62
+ module MyLibrary
63
+ class MyClass
64
+ include Mixture::Model
65
+ attribute :name, type: String
66
+ validate :name, format: /^.{3,20}$/
67
+ end
68
+ end
69
+ ```
70
+
71
+ Some validators are available by default:
72
+
73
+ - `:exclusion` - Validates that the value for the attribute is not
74
+ within a given set of values.
75
+ - `:inclusion` - Validates that the value for the attribute _is_
76
+ within a given set of values.
77
+ - `:length` - Validates that the value for the attribute is within a
78
+ certain length.
79
+ - `:match` - Validates that the value for the attribute matches a
80
+ regular expression. This is also known as `:format`.
81
+ - `:presence` - Validates that the value is not nil and isn't empty
82
+ (by checking for `#empty?`).
83
+
84
+ Adding a validator is simple:
85
+
86
+ ```ruby
87
+ module MyLibrary
88
+ class MyValidator < Mixture::Validate::Base
89
+ # This registers it with Mixture, so it can be used within a
90
+ # `validate` call.
91
+ register_as :my_validator
92
+
93
+ def validate(record, attribute, value)
94
+ # this sets instance variables mapping the above arguments.
95
+ super
96
+ my_super_awesome_validation
97
+ end
98
+ end
99
+ end
100
+ ```
101
+
102
+ Adding a coercer is also simple:
103
+
104
+ ```ruby
105
+ module MyLibrary
106
+ module Coerce
107
+ class MyObject < Mixture::Coerce::Base
108
+ type Mixture::Type[MyLibrary::MyObject]
109
+
110
+ coerce_to(Mixture::Type::Array) do |value|
111
+ value.to_a
112
+ end
113
+ end
114
+ end
115
+ end
116
+ ```
31
117
 
32
118
  ## Contributing
33
119
 
@@ -3,27 +3,62 @@
3
3
  module Mixture
4
4
  # An attribute for a mixture object.
5
5
  class Attribute
6
+ # The name of the attribute.
7
+ #
8
+ # @return [Symbol]
6
9
  attr_reader :name
10
+
11
+ # The options for the attribute. This is mainly used for coercion
12
+ # and validation.
13
+ #
14
+ # @return [Hash]
7
15
  attr_reader :options
8
16
 
17
+ # Initialize the attribute.
18
+ #
19
+ # @param name [Symbol] The name of the attribute.
20
+ # @param list [AttributeList] The attribute list this attribute is
21
+ # a part of.
22
+ # @param options [Hash] The optiosn for the attribute.
9
23
  def initialize(name, list, options = {})
10
24
  @name = name
11
25
  @list = list
12
26
  @options = options
13
27
  end
14
28
 
29
+ # Update the attribute with the given value. It runs the value
30
+ # through the callbacks, and returns a new value given by the
31
+ # callbacks.
32
+ #
33
+ # @param value [Object] The new value.
34
+ # @return [Object] The new new value.
15
35
  def update(value)
16
36
  @list.callbacks[:update].inject(value) { |a, e| e.call(self, a) }
17
37
  end
18
38
 
39
+ # The instance variable for this attribute.
40
+ #
41
+ # @example
42
+ # attribute.ivar # => :@name
43
+ # @return [Symbol]
19
44
  def ivar
20
45
  @_ivar ||= :"@#{@name}"
21
46
  end
22
47
 
48
+ # The getter method for this attribute.
49
+ #
50
+ # @example
51
+ # attribute.getter # => :name
52
+ # @return [Symbol]
23
53
  def getter
24
54
  @name
25
55
  end
26
56
 
57
+ # The setter method for this attribute.
58
+ #
59
+ # @example
60
+ # attribute.setter # :name=
61
+ # @return [Symbol]
27
62
  def setter
28
63
  @_setter ||= :"#{@name}="
29
64
  end
@@ -3,7 +3,8 @@
3
3
  require "forwardable"
4
4
 
5
5
  module Mixture
6
- # A list of attributes.
6
+ # A list of attributes. This is used instead of a hash in order to
7
+ # add in the {#options} and {#callbacks}.
7
8
  class AttributeList
8
9
  extend Forwardable
9
10
  # If it looks like a duck...
@@ -13,15 +14,30 @@ module Mixture
13
14
  # Then it must be a duck.
14
15
  delegate [:fetch, :[], :[]=, :key?, :each, :<=>] => :@list
15
16
 
17
+ # Returns a set of options used for the attributes. This isn't
18
+ # used at the moment.
19
+ #
20
+ # @return [Hash{Symbol => Object}]
16
21
  attr_reader :options
22
+
23
+ # Returns a set of callbacks. This is used for coercion, but may
24
+ # be used for other things.
25
+ #
26
+ # @return [Hash{Symbol => Array<Proc>}]
17
27
  attr_reader :callbacks
18
28
 
29
+ # Initializes the attribute list.
19
30
  def initialize
20
31
  @list = {}
21
32
  @options = {}
22
33
  @callbacks = Hash.new { |h, k| h[k] = [] }
23
34
  end
24
35
 
36
+ # Creates a new attribute with the given name and options.
37
+ #
38
+ # @param name [Symbol] The name of the attribute.
39
+ # @param options [Hash] The options for the attribute.
40
+ # @return [Attribute]
25
41
  def create(name, options = {})
26
42
  @list[name] = Attribute.new(name, self, options)
27
43
  end
@@ -8,15 +8,15 @@ module Mixture
8
8
  # type, and the instance handles the "to".
9
9
  class Base
10
10
  include Singleton
11
- # @!method type
11
+
12
+ # @overload type()
12
13
  # Returns the type this instance corresponds to.
13
14
  #
14
15
  # @return [Mixture::Type]
15
- #
16
- # @!method type(value)
16
+ # @overload type(value)
17
17
  # Sets the type this instance corresponds to.
18
18
  #
19
- # @param [Mixture::Type]
19
+ # @param value [Mixture::Type]
20
20
  # @return [void]
21
21
  def self.type(value = Undefined)
22
22
  if value == Undefined
@@ -26,10 +26,40 @@ module Mixture
26
26
  end
27
27
  end
28
28
 
29
+ # The coercions that this class has. It's a map of the type
30
+ # to the method that performs that coercion.
31
+ #
32
+ # @return [Hash{Mixture::Type => Symbol}]
29
33
  def self.coercions
30
34
  @_coercions ||= {}
31
35
  end
32
36
 
37
+ # This is a DSL for the class itself. It essentially defines a
38
+ # method to perform the coercion of the given type.
39
+ #
40
+ # @overload coerce_to(to) { }
41
+ # This is a DSL for the class itself. It essentially defines
42
+ # a method to perform the coercion of the given type.
43
+ #
44
+ # @param to [Mixture::Type] The type to coerce to.
45
+ # @yield [value] The block is called with the value to coerce
46
+ # when coercion needs to happen. Note that the block is not
47
+ # used as the body of the method - the method returns the
48
+ # block.
49
+ # @yieldparam value [Object] The object to coerce.
50
+ # @yieldreturn [Object] The coerced value.
51
+ # @return [void]
52
+ #
53
+ # @overload coerce_to(to, value)
54
+ # This is a DSL for the class itself. It essentially defines
55
+ # a method to perform the coercion of the given type.
56
+ #
57
+ # @param to [Mixture::Type] The type to coerce to.
58
+ # @param value [Proc] The block that is called with the value
59
+ # for coercion. This block is returned by the defined
60
+ # coercion method.
61
+ # @return [void]
62
+ #
33
63
  def self.coerce_to(to, value = Undefined, &block)
34
64
  fail ArgumentError, "Expected Mixture::Type, got #{to.class}" unless
35
65
  to.is_a?(Mixture::Type)
@@ -47,10 +77,17 @@ module Mixture
47
77
  define_method(to.method_name) { body }
48
78
  end
49
79
 
80
+ # (see #to)
50
81
  def self.to(type)
51
82
  instance.to(type)
52
83
  end
53
84
 
85
+ # Returns a block to perform the coercion to the given type.
86
+ # If it cannot find a coercion, it raises {CoercionError}.
87
+ #
88
+ # @param type [Mixture::Type] The type to coerce to.
89
+ # @raise [CoercionError] If it could not find the coercion.
90
+ # @return [Proc{(Object) => Object}]
54
91
  def to(type)
55
92
  method_name = self.class.coercions.fetch(type) do
56
93
  fail CoercionError,
@@ -6,6 +6,10 @@ module Mixture
6
6
  class Object < Base
7
7
  type Type::Object
8
8
 
9
+ # Tries a set of methods on the object, before failing with a
10
+ # coercion error.
11
+ #
12
+ # @return [Proc{(Symbol) => Proc{(Object) => Object}}]
9
13
  TryMethods = proc do |*methods|
10
14
  proc do |value|
11
15
  method = methods.find { |m| value.respond_to?(m) }
@@ -18,7 +18,7 @@ require "mixture/coerce/time"
18
18
  module Mixture
19
19
  # Handles coercion of objects.
20
20
  module Coerce
21
- # Registers a coercion with the module. This uses the {COERCERS}
21
+ # Registers a coercion with the module. This uses the {.coercers}
22
22
  # constant.
23
23
  #
24
24
  # @param coercion [Mixture::Coerce::Base] The coercer to register.
@@ -9,6 +9,7 @@ module Mixture
9
9
  class CoercionError < BasicError
10
10
  end
11
11
 
12
+ # Occurs when a validation fails.
12
13
  class ValidationError < BasicError
13
14
  end
14
15
  end
@@ -6,6 +6,13 @@ module Mixture
6
6
  module Attributable
7
7
  # The class methods for attribution.
8
8
  module ClassMethods
9
+ # Defines an attribute. Defines the getter and the setter for
10
+ # for the attribute, the getter and setter alias to the
11
+ # {#attribute}.
12
+ #
13
+ # @param name [Symbol] The name of the attribute.
14
+ # @param options [Hash] The options for the attribute.
15
+ # @return [Attribute] The new attribute.
9
16
  def attribute(name, options = {})
10
17
  name = name.to_s.intern
11
18
  attr = attributes.create(name, options)
@@ -14,6 +21,10 @@ module Mixture
14
21
  attr
15
22
  end
16
23
 
24
+ # The attribute list. Acts as a hash for the attributes.
25
+ #
26
+ # @see AttributeList
27
+ # @return [AttributeList]
17
28
  def attributes
18
29
  @_attributes ||= AttributeList.new
19
30
  end
@@ -21,21 +32,54 @@ module Mixture
21
32
 
22
33
  # The instance methods for attribution.
23
34
  module InstanceMethods
24
- # Sets the attributes on the instance.
35
+ # Sets the attributes on the instance. It iterates through
36
+ # the given hash and uses {#attribute} to set the key, value
37
+ # pair.
38
+ #
39
+ # @param attrs [Hash] The attributes to set.
40
+ # @return [void]
25
41
  def attributes=(attrs)
26
42
  attrs.each { |key, value| attribute(key, value) }
27
43
  end
28
44
 
45
+ # The attributes defined on this instance. It returns a
46
+ # hash containing the key-value pairs for each attribute.
47
+ #
48
+ # @return [Hash{Symbol => Object}]
29
49
  def attributes
30
50
  Hash[self.class.attributes.map do |name, attr|
31
51
  [name, instance_variable_get(attr.ivar)]
32
52
  end]
33
53
  end
34
54
 
55
+ # Called when an unknown attribute is accessed using
56
+ # {#attribute}. By default, it just raises an `ArgumentError`.
57
+ #
58
+ # @param attr [Symbol] The attribute.
59
+ # @raise [ArgumentError]
35
60
  def unknown_attribute(attr)
36
61
  fail ArgumentError, "Unknown attribute #{attr} passed"
37
62
  end
38
63
 
64
+ # @overload attribute(key)
65
+ # Accesses an attribute by the given key name. If the
66
+ # attribute could not be found, it calls
67
+ # {#unknown_attribute}. It uses the instance variable value
68
+ # of the attribute as the value of the attribute (e.g. it
69
+ # uses `@name` for the `name` attribute).
70
+ #
71
+ # @param key [Symbol] The name of the attribute.
72
+ # @return [Object] The value of the attribute.
73
+ # @overload attribute(key, value)
74
+ # Sets an attribute value by the given key name. If the
75
+ # attribute could not be found, it calls
76
+ # {#unknown_attribute}. It calls any of the update
77
+ # callbacks via {Attribute#update}, and sets the instance
78
+ # variable for the attribute.
79
+ #
80
+ # @param key [Symbol] The name of the attribute.
81
+ # @param value [Object] The new value of the attribute.
82
+ # @return [void]
39
83
  def attribute(key, value = Undefined)
40
84
  attr = self.class.attributes.fetch(key) do
41
85
  return unknown_attribute(key)
@@ -4,6 +4,14 @@ module Mixture
4
4
  module Extensions
5
5
  # Extends the attribute definition to allow coercion.
6
6
  module Coercable
7
+ # Performs the coercion for the attribute and the value.
8
+ # It is used in a `:update` callback.
9
+ #
10
+ # @param attribute [Attribute] The attribute
11
+ # @param value [Object] The new value.
12
+ # @return [Object] The new new value.
13
+ # @raise [CoercionError] If an error occurs while a coercion is
14
+ # running.
7
15
  def self.coerce_attribute(attribute, value)
8
16
  return value unless attribute.options[:type]
9
17
  attr_type = Type.infer(attribute.options[:type])
@@ -10,25 +10,82 @@ module Mixture
10
10
  include Enumerable
11
11
  include Comparable
12
12
 
13
- delegate [:each, :<=>] => :attributes
13
+ # The methods that are mapped directly to the {#to_hash} method.
14
+ #
15
+ # @return [Array<Symbol>]
16
+ MAPPED_METHODS = %w(
17
+ each <=> keys values each_key each_value has_value? value?
18
+ size length empty? each_pair
19
+ ).map(&:intern)
14
20
 
21
+ delegate MAPPED_METHODS => :attributes
22
+
23
+ # Alias for {Attributable::InstanceMethods#attribute}.
24
+ #
25
+ # @param (see Attributable::InstanceMethods#attribute)
26
+ # @return (see Attributable::InstanceMethods#attribute)
15
27
  def [](key)
16
28
  attribute(key.to_s.intern)
17
29
  end
18
30
 
31
+ # Alias for {Attributable::InstanceMethods#attribute}.
32
+ #
33
+ # @param (see Attributable::InstanceMethods#attribute)
34
+ # @return (see Attributable::InstanceMethods#attribute)
19
35
  def []=(key, value)
20
36
  attribute(key.to_s.intern, value)
21
37
  end
38
+ alias_method :store, :[]=
39
+
40
+ # (see Attributable::InstanceMethods#attributes)
41
+ def to_hash
42
+ attributes
43
+ end
44
+ alias_method :to_h, :to_hash
22
45
 
46
+ # Checks for an attribute with the given name.
47
+ #
48
+ # @param key [Symbol] The name of the attribute to check.
49
+ # @return [Boolean]
23
50
  def key?(key)
24
51
  self.class.attributes.key?(key.to_s.intern)
25
52
  end
53
+ alias_method :has_key?, :key?
26
54
 
27
- def fetch(key, default = Unknown)
55
+ # @overload fetch(key)
56
+ # Performs a fetch. This acts just like Hash's `fetch`.
57
+ #
58
+ # @param key [Symbol] The key to check for an attribute.
59
+ # @raise [KeyError] If the key isn't present.
60
+ # @return [Object] The attribute's value.
61
+ # @overload fetch(key, default)
62
+ # Performs a fetch. This acts just like Hash's fetch. If
63
+ # the attribute doesn't exist, the default argument value is
64
+ # used as a value instead.
65
+ #
66
+ # @param key [Symbol] The key to check for an attribute.
67
+ # @param default [Object] The value to use instead of an
68
+ # error.
69
+ # @return [Object] The attribute's value, or the default value
70
+ # instead.
71
+ # @overload fetch(key) { }
72
+ # Performs a fetch. This acts just like Hash's fetch. If
73
+ # the attribute doesn't exist, the value of the block is used
74
+ # as a value instead.
75
+ #
76
+ # @param key [Symbol] The key to check for an attribute.
77
+ # @yield [key] The block is called if an attribute does not
78
+ # exist.
79
+ # @yieldparam key [Symbol] The key of the non-existant
80
+ # attribute.
81
+ # @yieldreturn [Object] Return value of the method.
82
+ # @return [Object] The attribute's value, or the block's
83
+ # value instead.
84
+ def fetch(key, default = Undefined)
28
85
  case
29
86
  when key?(key.to_s.intern) then attribute(key.to_s.intern)
30
87
  when block_given? then yield(key.to_s.intern)
31
- when default != Unknown then default
88
+ when default != Undefined then default
32
89
  else fail KeyError, "Undefined attribute #{key.to_s.intern}"
33
90
  end
34
91
  end
@@ -10,6 +10,13 @@ module Mixture
10
10
  # Creates a new validation for the given attribute. The
11
11
  # attribute _must_ be defined before this call, otherwise it
12
12
  # _will_ error.
13
+ #
14
+ # @param name [Symbol] The name of the attribute to validate.
15
+ # @param options [Hash] The options for validation. This is
16
+ # normally a key-value pair, where the key is the name of
17
+ # the validator, and the value is the options to pass to
18
+ # the validator.
19
+ # @return [void]
13
20
  def validate(name, options = {})
14
21
  attributes.fetch(name).options[:validate] = options
15
22
  end
@@ -17,7 +24,10 @@ module Mixture
17
24
 
18
25
  # The instance methods.
19
26
  module InstanceMethods
20
- # Validates the attributes on the record.
27
+ # Validates the attributes on the record. This will fill up
28
+ # {#errors} with errors, if there are any.
29
+ #
30
+ # @return [Boolean]
21
31
  def valid?
22
32
  @errors = Hash.new { |h, k| h[k] = [] }
23
33
  self.class.attributes.each do |name, attribute|
@@ -27,10 +37,18 @@ module Mixture
27
37
  !@errors.values.any?(&:any?)
28
38
  end
29
39
 
40
+ # Opposite of valid.
41
+ #
42
+ # @see #valid?
43
+ # @return [Boolean]
30
44
  def invalid?
31
45
  !valid?
32
46
  end
33
47
 
48
+ # Returns a hash, mapping attributes to the errors that they
49
+ # have.
50
+ #
51
+ # @return [Hash{Attribute => Array<ValidationError>}]
34
52
  def errors
35
53
  @errors ||= Hash.new { |h, k| h[k] = [] }
36
54
  end
@@ -5,14 +5,29 @@ module Mixture
5
5
  # extensions, so that extensions can be referend by a name instead
6
6
  # of the constant.
7
7
  module Extensions
8
+ # Registers an extension with the module. This maps a name to
9
+ # an extension. Extensions may be registered multiple times,
10
+ # with different names.
11
+ #
12
+ # @param name [Symbol] The name of the extension to register.
13
+ # @param extension [Module] The module to register.
8
14
  def self.register(name, extension)
9
15
  extensions[name.to_s.downcase.intern] = extension
10
16
  end
11
17
 
18
+ # Loads an extension with the given name. If it cannot find the
19
+ # matching extension, it raises a `KeyError`.
20
+ #
21
+ # @param name [Symbol] The name of the extension.
22
+ # @return [Module] The corresponding extension.
23
+ # @raise [KeyError]
12
24
  def self.[](name)
13
25
  extensions.fetch(name)
14
26
  end
15
27
 
28
+ # The extensions that are registered with this module.
29
+ #
30
+ # @return [Hash{Symbol => Module}]
16
31
  def self.extensions
17
32
  @_extensions ||= {}
18
33
  end
data/lib/mixture/model.rb CHANGED
@@ -6,8 +6,29 @@ module Mixture
6
6
  # @example
7
7
  # class MyClass
8
8
  # include Mixture::Model
9
- # mixture_modules :attributable, :hashable
9
+ # mixture_modules :attribute, :hash
10
10
  # end
11
11
  module Model
12
+ # The class methods for the module.
13
+ module ClassMethods
14
+ # Used to include certain extensions.
15
+ #
16
+ # @see Extensions.[]
17
+ # @param mods [Symbol] The mod name to include.
18
+ # @return [void]
19
+ def mixture_modules(*mods)
20
+ mods.each do |mod|
21
+ include Extensions[mod]
22
+ end
23
+ end
24
+ end
25
+
26
+ # A method used internally by ruby.
27
+ #
28
+ # @api private
29
+ def self.included(base)
30
+ base.extend ClassMethods
31
+ base.mixture_modules(:attribute, :coerce, :validate)
32
+ end
12
33
  end
13
34
  end
data/lib/mixture/type.rb CHANGED
@@ -8,14 +8,31 @@ module Mixture
8
8
  class Type
9
9
  extend Forwardable
10
10
  @instances = {}
11
+ # A class to represent the Boolean type (i.e. true, false), since
12
+ # ruby doesn't have a Boolean class.
13
+ #
14
+ # @return [Class]
11
15
  BooleanClass = Class.new
12
- InstanceClass = Class.new
13
16
 
17
+ # Ancestors that two classes might have in common with each other.
18
+ #
19
+ # @return [Array<Module, Class>]
20
+ COMMON_ANCESTORS = BooleanClass.ancestors - [BooleanClass]
21
+
22
+ # The builtin types of Ruby. These are initialized as types
23
+ # before anything else happens.
24
+ #
25
+ # @return [Array<Symbol>]
14
26
  BUILTIN_TYPES = %w(
15
27
  Object Array Hash Integer Rational Float Set String Symbol
16
28
  Time Date DateTime
17
29
  ).map(&:intern).freeze
18
30
 
31
+ # The aliases for types. This overrides any other possible
32
+ # inferences that this class can make. For example, `true` and
33
+ # `false` are automatically mapped to `BooleanClass`.
34
+ #
35
+ # @return [Hash{Object, Symbol => Class}]
19
36
  TYPE_ALIASES = {
20
37
  true => BooleanClass,
21
38
  false => BooleanClass,
@@ -44,7 +61,7 @@ module Mixture
44
61
  private_class_method :new
45
62
 
46
63
  # Returns a {Type} from a given class. It assumes that the type
47
- # given is a class, and passes it to {#new} - which will error if
64
+ # given is a class, and passes it to `#new` - which will error if
48
65
  # it isn't.
49
66
  #
50
67
  # @param type [Class] A type.
@@ -61,12 +78,10 @@ module Mixture
61
78
  end
62
79
 
63
80
  # Determines the best type that represents the given value. If
64
- # the given value is a {Type}, it returns the value. If the
65
- # given value is already defined as a {Type}, it returns the
66
- # {Type}. If the value's class is already defined as a {Type},
67
- # it returns the class's {Type}; otherwise, it returns the types
68
- # for Class and Object, depending on if the value is a Class or
69
- # not, respectively.
81
+ # the given type is listed in {TYPE_ALIASES}, it uses the alias
82
+ # value for a lookup. If the given type is a {Type}, it returns
83
+ # the type. If the given type is a `Class`, it uses {.infer_class}.
84
+ # Otherwise, it uses {.infer_class} on the type's class.
70
85
  #
71
86
  # @example
72
87
  # Mixture::Type.infer(Integer) # => Mixture::Type::Integer
@@ -75,7 +90,7 @@ module Mixture
75
90
  # Mixture::Type.infer(1) # => Mixture::Type::Integer
76
91
  #
77
92
  # @example
78
- # Mixture::Type.infer(MyClass) # => Mixture::Type::Instance
93
+ # Mixture::Type.infer(MyClass) # => Mixture::Type[MyClass]
79
94
  #
80
95
  # @example
81
96
  # Mixture::Type.infer(Object.new) # => Mixture::Type::Object
@@ -91,15 +106,28 @@ module Mixture
91
106
  end
92
107
  end
93
108
 
109
+ # Infer a classes' type. If the class is a type, it returns the
110
+ # type. Otherwise, it checks the most basic ancestors (most basic
111
+ # ancestors being any ancestors not shared with a new class) for
112
+ # any classes that have a {Type}; if there are none, it creates a
113
+ # new type from the class.
114
+ #
115
+ # @param klass [Class] The class to infer type from.
116
+ # @return [Mixture::Type]
94
117
  def self.infer_class(klass)
95
118
  if klass.is_a?(Type)
96
119
  klass
97
120
  else
98
- basic_ancestors = klass.ancestors - ancestors
121
+ basic_ancestors = klass.ancestors - COMMON_ANCESTORS
99
122
  from(basic_ancestors.find { |a| @instances.key?(a) } || klass)
100
123
  end
101
124
  end
102
125
 
126
+ # Loads the builtin types. This includes `Boolean` and `Nil`.
127
+ #
128
+ # @see BUILTIN_TYPES
129
+ # @see BooleanClass
130
+ # @return [void]
103
131
  def self.load
104
132
  BUILTIN_TYPES.each do |sym|
105
133
  const_set(sym, from(::Object.const_get(sym)))
@@ -107,14 +135,24 @@ module Mixture
107
135
 
108
136
  @instances[BooleanClass] = new(BooleanClass, name: "Boolean")
109
137
  const_set("Boolean", @instances[BooleanClass])
110
- @instances[InstanceClass] = new(InstanceClass, name: "Instance")
111
- const_set("Instance", @instances[InstanceClass])
112
138
  @instances[NilClass] = new(NilClass, name: "Nil")
113
139
  const_set("Nil", @instances[NilClass])
114
140
  end
115
141
 
142
+ # The name of the type. If this wasn't provided upon
143
+ # initialization, it is guessed to be the class's name, which is
144
+ # normally good enough.
145
+ #
146
+ # @return [String]
116
147
  attr_reader :name
117
148
 
149
+ # Initialize the type. The class given _must_ be a class;
150
+ # otherwise, it will error. A name can be provided as an option.
151
+ #
152
+ # @param type [Class] The type to create.
153
+ # @param options [Hash{Symbol => Object}] The options.
154
+ # @option options [String] :name The name to provide the type
155
+ # with. If this is not provided, it uses the class name.
118
156
  def initialize(type, options = {})
119
157
  fail ArgumentError, "Expected a Class, got #{type.class}" unless
120
158
  type.is_a?(Class)
@@ -122,11 +160,19 @@ module Mixture
122
160
  @name = options.fetch(:name, @type.name)
123
161
  end
124
162
 
163
+ # Creates a string representation of the type. This normally has
164
+ # the format `Mixture::Type(Class)`, where `Class` is the class.
165
+ #
166
+ # @return [String]
125
167
  def to_s
126
168
  "#{self.class.name}(#{@name})"
127
169
  end
128
170
  alias_method :inspect, :to_s
129
171
 
172
+ # Creates a `:to_` method name for the type.
173
+ #
174
+ # @example
175
+ # Mixture::Type[Array].method_name # => :to_array
130
176
  def method_name
131
177
  @_method_name ||= begin
132
178
  body = name
@@ -2,17 +2,47 @@
2
2
 
3
3
  module Mixture
4
4
  module Validate
5
+ # A base for validations. All validators should inherit this
6
+ # class.
7
+ #
8
+ # @abstract
5
9
  class Base
10
+ # Registers this validator as the given name.
11
+ #
12
+ # @see Validate.register
13
+ # @param name [Symbol] The name of the validator.
14
+ # @return [void]
15
+ def self.register_as(name)
16
+ Validate.register(name, self)
17
+ end
18
+
19
+ # Initialize the validator.
20
+ #
21
+ # @param options [Hash] The options for the validator.
6
22
  def initialize(options)
7
23
  @options = options
8
24
  end
9
25
 
26
+ # Performs the validation.
27
+ #
28
+ # @param record [Mixture::Model] The model that has the
29
+ # attribute. At least, it should respond to `#errors`.
30
+ # @param attribute [Attribute] The attribute to validate.
31
+ # @param value [Object] The value of the attribute.
32
+ # @return [void]
33
+ # @abstract
10
34
  def validate(record, attribute, value)
11
35
  @record = record
12
36
  @attribute = attribute
13
37
  @value = value
14
38
  end
15
39
 
40
+ private
41
+
42
+ # Raises an error with the given a message.
43
+ #
44
+ # @param message [String] The message to raise.
45
+ # @raise [ValidationError]
16
46
  def error(message)
17
47
  fail ValidationError, message
18
48
  end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Validate
5
+ # Checks to make sure that the value isn't in the given set. This
6
+ # uses the `#includes?` method on the `@options`.
7
+ class Exclusion < Base
8
+ register_as :exclusion
9
+ # Performs the validation.
10
+ #
11
+ # @param (see Base#validate)
12
+ # @return (see Base#validate)
13
+ def validate(record, attribute, value)
14
+ super
15
+ error("Value is in the given set") if
16
+ @options[:in].include?(value)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Validate
5
+ # Checks to make sure that the value is in the given set. This
6
+ # uses the `#includes?` method on the `@options`.
7
+ class Inclusion < Base
8
+ register_as :inclusion
9
+ # Performs the validation.
10
+ #
11
+ # @param (see Base#validate)
12
+ # @return (see Base#validate)
13
+ def validate(record, attribute, value)
14
+ super
15
+ error("Value isn't in the given set") unless
16
+ @options[:in].include?(value)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,109 @@
1
+ # encoding: utf-8
2
+
3
+ module Mixture
4
+ module Validate
5
+ # Validates the length of an attribute.
6
+ class Length < Base
7
+ register_as :length
8
+ # Validates the length of the value. It first composes an
9
+ # acceptable range, and then determines if the length is within
10
+ # that acceptable range. If either components error, the
11
+ # validation fails.
12
+ #
13
+ # @param (see Base#validate)
14
+ # @return [void]
15
+ def validate(record, attribute, value)
16
+ super
17
+ error("Length is not acceptable") unless acceptable.cover?(length)
18
+ end
19
+
20
+ # rubocop:disable Metrics/AbcSize
21
+ # rubocop:disable Metrics/CyclomaticComplexity
22
+ # rubocop:disable Metrics/PerceivedComplexity
23
+
24
+ # Determines the acceptable range that the length ca nbe in.
25
+ # This first turns the options into a hash via {#to_hash}, and
26
+ # then checks the hash.
27
+ #
28
+ # @option options [Range] :in If this is provided, it is used
29
+ # as the acceptable range.
30
+ # @option options [Numeric] :is If this is provided, it is used
31
+ # as an exact match.
32
+ # @option options [Numeric] :maximum If this is provided without
33
+ # `:minimum`, it is set as the upper limit on the length (i.e.
34
+ # it is equivalent to `in: 0..maximum`). If it is provided
35
+ # with `:minimum`, it is the upper limit (i.e. equivalent to
36
+ # `in: minimum..maximum`).
37
+ # @option options [Numeric] :minimum If this is provided without
38
+ # `:maximum`, it is set as the lower limit on the length (i.e.
39
+ # equivalent to `in: minimum..Float::INFINITY`). If it is
40
+ # provided with `:maximum`, it is the lower limit (i.e.
41
+ # equivalent to `in: minimum..maximum`).
42
+ # @note If it is unable to find any of the options, or unable to
43
+ # turn the given value into a hash, it will raise a
44
+ # {ValidationError}. This means validation will fail, even if
45
+ # the value may be valid.
46
+ # @return [Range]
47
+ # @see #to_hash
48
+ def acceptable
49
+ @_acceptable ||= begin
50
+ options = to_hash(@options)
51
+
52
+ if options.key?(:in) then options[:in]
53
+ elsif options.key?(:is) then options[:is]..options[:is]
54
+ elsif options.key?(:maximum) && options.key?(:minimum)
55
+ options[:minimum]..options[:maximum]
56
+ elsif options.key?(:maximum) then 0..options[:maximum]
57
+ elsif options.key?(:minimum) then options[:minimum]..Float::INFINITY
58
+ else
59
+ error("Unable to determine acceptable range")
60
+ end
61
+ end
62
+ end
63
+
64
+ # rubocop:enable Metrics/AbcSize
65
+ # rubocop:enable Metrics/CyclomaticComplexity
66
+ # rubocop:enable Metrics/PerceivedComplexity
67
+
68
+ # Turns any other recoginizable value into a hash that
69
+ # {#acceptable} can use. The mappings go as follows:
70
+ #
71
+ # - `Range`: turns into `{ in: value }`.
72
+ # - `Numeric`: turns into `{ in: value..value }`
73
+ # - `Array`: turns into `{ in: value[0]..value[1] }`
74
+ # - `Hash`: turns into itself.
75
+ #
76
+ # Any other class/value causes an error.
77
+ #
78
+ # @param value [Object] The value to turn into a hash.
79
+ # @return [Hash]
80
+ # @raise [ValidationError] If it can't turn into a hash.
81
+ def to_hash(value)
82
+ case value
83
+ when Range then { in: value }
84
+ when Numeric then { in: value..value }
85
+ when Array then { in: value[0]..value[1] }
86
+ when Hash then value
87
+ else
88
+ error("Unable to determine acceptable range")
89
+ end
90
+ end
91
+
92
+ # Attempts to get the value of the length. It tries the
93
+ # `#size`, `#length`, and finally, `#count`; if it can't
94
+ # find any of these, it raises an error.
95
+ #
96
+ # @return [Numeric] The length.
97
+ # @raise [ValidationError] If it cannot determine the length of
98
+ # the value.
99
+ def length
100
+ case
101
+ when @value.respond_to?(:size) then @value.size
102
+ when @value.respond_to?(:length) then @value.length
103
+ when @value.respond_to?(:count) then @value.count
104
+ else error("Value isn't countable")
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -4,13 +4,29 @@ module Mixture
4
4
  module Validate
5
5
  # Checks that a value matches.
6
6
  class Match < Base
7
+ register_as :match
8
+ register_as :format
9
+ # Performs the validation.
10
+ #
11
+ # @param (see Base#validate)
12
+ # @return (see Base#validate)
13
+ # @raise [ValidationError] If {#match?} returns false.
7
14
  def validate(record, attribute, value)
8
15
  super
9
16
  error("Value does not match") unless match?
10
17
  end
11
18
 
19
+ private
20
+
21
+ # Checks if the value matches the given matcher. It uses the
22
+ # `=~` operator. If it fails (i.e. raises an error), it returns
23
+ # false.
24
+ #
25
+ # @return [Boolean]
12
26
  def match?
13
27
  @value =~ @options
28
+ rescue StandardError
29
+ false
14
30
  end
15
31
  end
16
32
  end
@@ -4,11 +4,24 @@ module Mixture
4
4
  module Validate
5
5
  # Checks that a value is present.
6
6
  class Presence < Base
7
+ register_as :presence
8
+ # Performs the validation.
9
+ #
10
+ # @param (see Base#validate)
11
+ # @return (see Base#validate)
12
+ # @raise [ValidationError] If {#empty?} returns true.
7
13
  def validate(record, attribute, value)
8
14
  super
9
15
  error("Value is empty") if empty?
10
16
  end
11
17
 
18
+ private
19
+
20
+ # Determins if the given value is empty. If it's not nil,
21
+ # and it responds to `empty?`, it returns the value of `empty?`;
22
+ # otherwise, it returns the value of `nil?`.
23
+ #
24
+ # @return [Boolean]
12
25
  def empty?
13
26
  @value.nil? || (@value.respond_to?(:empty?) && @value.empty?)
14
27
  end
@@ -1,30 +1,65 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Mixture
4
+ # Handles validation of attributes. You can register validators
5
+ # with this module, and they will be used to validate attributes.
6
+ # Check out {Validate::Base} for the expected interface.
4
7
  module Validate
8
+ # Registers a validator. This lets Mixture know about the
9
+ # validators that can occur.
10
+ #
11
+ # @param name [Symbol] The name of the validator. This is used in
12
+ # the {Extensions::Validatable::ClassMethods#validate} call as
13
+ # a key.
14
+ # @param validator [Validate::Base] The validator to use.
15
+ # @return [void]
5
16
  def self.register(name, validator)
6
17
  validations[name] = validator
7
18
  end
8
19
 
20
+ # The validators that are currently registered. This returns a
21
+ # key-value hash of validations.
22
+ #
23
+ # @return [Hash{Symbol => Validate::Base}]
9
24
  def self.validations
10
25
  @_validations ||= {}
11
26
  end
12
27
 
13
28
  require "mixture/validate/base"
29
+ require "mixture/validate/exclusion"
30
+ require "mixture/validate/inclusion"
31
+ require "mixture/validate/length"
14
32
  require "mixture/validate/match"
15
33
  require "mixture/validate/presence"
16
34
 
17
- register :match, Match
18
- register :format, Match
19
- register :presence, Presence
20
-
35
+ # Performs a validation on the attribute. It loops through the
36
+ # validation requirements on the attribute, and runs each
37
+ # validation with {.validate_with}.
38
+ #
39
+ # @param record [Mixture::Model] A class that has included
40
+ # {Mixture::Model} or {Mixture::Extensions::Validatable}.
41
+ # @param attribute [Mixture::Attribute] The attribute to validate.
42
+ # @param value [Object] The new value of the attribute.
43
+ # @return [void]
21
44
  def self.validate(record, attribute, value)
45
+ return unless attribute.options[:validate]
22
46
  attribute.options[:validate].each do |k, v|
23
47
  validator = validations.fetch(k).new(v)
24
48
  validate_with(validator, record, attribute, value)
25
49
  end
26
50
  end
27
51
 
52
+ # Validates a (record, attribute, value) triplet with the given
53
+ # validator. It calls `#validate` on the validator with the
54
+ # three as arguments, and rescues any validation errors thrown
55
+ # (and places them in `record.errors`).
56
+ #
57
+ # @param validator [Validator::Base, #validate]
58
+ # The validator to validate with.
59
+ # @param record [Mixture::Model]
60
+ # @param attribute [Mixture::Attribute] The attribute to validate.
61
+ # @param value [Object] The new value.
62
+ # @return [void]
28
63
  def self.validate_with(validator, record, attribute, value)
29
64
  validator.validate(record, attribute, value)
30
65
 
@@ -5,5 +5,5 @@ module Mixture
5
5
  # The current version of Mixture.
6
6
  #
7
7
  # @return [String]
8
- VERSION = "0.1.0"
8
+ VERSION = "0.2.0"
9
9
  end
data/mixture.gemspec CHANGED
@@ -27,4 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency "rspec"
28
28
  spec.add_development_dependency "pry"
29
29
  spec.add_development_dependency "coveralls"
30
+ spec.add_development_dependency "rubocop"
30
31
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mixture
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Rodi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-09 00:00:00.000000000 Z
11
+ date: 2015-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  description: Handle validation, coercion, and attributes.
84
98
  email:
85
99
  - redjazz96@gmail.com
@@ -125,6 +139,9 @@ files:
125
139
  - lib/mixture/type.rb
126
140
  - lib/mixture/validate.rb
127
141
  - lib/mixture/validate/base.rb
142
+ - lib/mixture/validate/exclusion.rb
143
+ - lib/mixture/validate/inclusion.rb
144
+ - lib/mixture/validate/length.rb
128
145
  - lib/mixture/validate/match.rb
129
146
  - lib/mixture/validate/presence.rb
130
147
  - lib/mixture/version.rb
@@ -149,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
166
  version: '0'
150
167
  requirements: []
151
168
  rubyforge_project:
152
- rubygems_version: 2.4.8
169
+ rubygems_version: 2.4.5
153
170
  signing_key:
154
171
  specification_version: 4
155
172
  summary: Handle validation, coercion, and attributes.