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 +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
|
+
[](https://travis-ci.org/medcat/mixture) [](https://coveralls.io/github/medcat/mixture?branch=master) [](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.
|