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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +3 -0
- data/Gemfile.lock +16 -1
- data/README.md +97 -11
- data/lib/mixture/attribute.rb +35 -0
- data/lib/mixture/attribute_list.rb +17 -1
- data/lib/mixture/coerce/base.rb +41 -4
- data/lib/mixture/coerce/object.rb +4 -0
- data/lib/mixture/coerce.rb +1 -1
- data/lib/mixture/errors.rb +1 -0
- data/lib/mixture/extensions/attributable.rb +45 -1
- data/lib/mixture/extensions/coercable.rb +8 -0
- data/lib/mixture/extensions/hashable.rb +60 -3
- data/lib/mixture/extensions/validatable.rb +19 -1
- data/lib/mixture/extensions.rb +15 -0
- data/lib/mixture/model.rb +22 -1
- data/lib/mixture/type.rb +58 -12
- data/lib/mixture/validate/base.rb +30 -0
- data/lib/mixture/validate/exclusion.rb +20 -0
- data/lib/mixture/validate/inclusion.rb +20 -0
- data/lib/mixture/validate/length.rb +109 -0
- data/lib/mixture/validate/match.rb +16 -0
- data/lib/mixture/validate/presence.rb +13 -0
- data/lib/mixture/validate.rb +39 -4
- data/lib/mixture/version.rb +1 -1
- data/mixture.gemspec +1 -0
- metadata +20 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e1746012422c7e9f856706e2820d0423f404784
|
4
|
+
data.tar.gz: 0591ce1acb37f9805c37b06298bf7f4c10c4f3f7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6a2b4634e7d03ef8ed5e8c4730895e497bd1c98307f87f012f27ab4d4a1d401912076c39b366194d52cdeaf1f9beb2dcf673c57729b6f8f679a0656bb39280f1
|
7
|
+
data.tar.gz: a15206cb105329873a6890deeb46d8aa797385835315568a427c3e98272a8d3862f3af158777a9afbe077c546621b3af42b89ac7427a1a2e5bc073558407669e
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
mixture (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
|
-
|
17
|
+
## Usage
|
16
18
|
|
17
|
-
|
19
|
+
It's simple, really.
|
18
20
|
|
19
|
-
|
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
|
-
|
56
|
+
This will automagically cause `name` to be coerced to a string on
|
57
|
+
assignment.
|
22
58
|
|
23
|
-
|
59
|
+
For validation, use the `.validate` class method:
|
24
60
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
|
data/lib/mixture/attribute.rb
CHANGED
@@ -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
|
data/lib/mixture/coerce/base.rb
CHANGED
@@ -8,15 +8,15 @@ module Mixture
|
|
8
8
|
# type, and the instance handles the "to".
|
9
9
|
class Base
|
10
10
|
include Singleton
|
11
|
-
|
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) }
|
data/lib/mixture/coerce.rb
CHANGED
@@ -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 {
|
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.
|
data/lib/mixture/errors.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 !=
|
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
|
data/lib/mixture/extensions.rb
CHANGED
@@ -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 :
|
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
|
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
|
65
|
-
#
|
66
|
-
#
|
67
|
-
# it
|
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
|
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 -
|
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
|
data/lib/mixture/validate.rb
CHANGED
@@ -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
|
-
|
18
|
-
|
19
|
-
|
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
|
|
data/lib/mixture/version.rb
CHANGED
data/mixture.gemspec
CHANGED
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.
|
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-
|
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.
|
169
|
+
rubygems_version: 2.4.5
|
153
170
|
signing_key:
|
154
171
|
specification_version: 4
|
155
172
|
summary: Handle validation, coercion, and attributes.
|