mixture 0.1.0 → 0.2.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.
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.