assertion 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4a42bd3404b1c576172d51bb68c60070e5d0a11f
4
- data.tar.gz: 652283f21156545c9aae42912cf06911e988f141
3
+ metadata.gz: d9fbab813ababd1c0224226d00aa2144dc682b6d
4
+ data.tar.gz: 30d48a47833d65670e480b29196caa1aec783040
5
5
  SHA512:
6
- metadata.gz: 569103f1216ac675a37aa875464c988775f6b5ed8a03150da75db092b6e44b9e35467f192091025168004d7b53efa06cc648d1b45983d6e602ff80bd0a2b63d5
7
- data.tar.gz: 22d4e650d8cdd775a479e92a66eca1b3e466cd61ebc650900910c1386ba6688525b2ff2fb1edbc2fa3e6076a02059921c2f589d48f448e51c543d28eaeda719a
6
+ metadata.gz: 521a9d990f59e2236344f712df369f211e3635c9f6828da4883135962b2f6e51a0e006e0fc18612d9e3474c24a5bdf3681585982c665eec6e8caa21de58961e4
7
+ data.tar.gz: 3e3dacb62cc68dd317e674cbad9424607d4b4a12638445e54a41534f747f9c0af7c07edf1a13728d9b2223349b713f72d716cb9f6cb7525dd7f8e332b46a8978
@@ -0,0 +1,20 @@
1
+ ## v0.1.0 2015-06-20
2
+
3
+ ### Added
4
+
5
+ * New `Guard` base class for guarding objects (nepalez)
6
+ * New `Assertion.guards` builder method to provide specific guards (nepalez)
7
+ * Custom `Assertion::InvalidError#inspect` method that shows `#messages` (nepalez)
8
+
9
+ ### Deleted
10
+
11
+ * Removed `Assertion::NotImplementedError` (use `NoMethodError` instead) (nepalez)
12
+ * Removed `Assertion::NameError` (use generic `NameError` instead) (nepalez)
13
+
14
+ ### Internal
15
+
16
+ * Moved all message-related features to the `Assertion::Messages` (nepalez)
17
+ * Removed `Assertion::I18n` (not the part of public API) (nepalez)
18
+ * Extracted attribute DSL from `Assertion::Base` to `Assertion::Attributes` (nepalez)
19
+
20
+ [Compare v0.0.1...v0.1.0](https://github.com/nepalez/assertion/compare/v0.0.1...v0.1.0)
data/README.md CHANGED
@@ -20,38 +20,38 @@ Immutable assertions and validations for PORO.
20
20
  Synopsis
21
21
  --------
22
22
 
23
- The primary goal of the gem is to decouple assertions about the objects, and their validations, from the data.
23
+ The primary goal of the gem is to make assertions about <decoupled> objects.
24
24
 
25
25
  No `ActiveSupport`, no mutation of any instances.
26
26
 
27
27
  ### Basic Usage
28
28
 
29
- Define the assertion by inheriting it from the `Assertion::Base` class with attributes to which it should be applied.
30
- Then implement the method `check` that should return the boolean value.
29
+ Define an assertion by inheriting it from the `Assertion::Base` class with attributes to which it should be applied.
30
+ Then implement the method `check` that should return a boolean value.
31
31
 
32
- You can do it either in a classic style:
32
+ You can do it either in the classic style:
33
33
 
34
34
  ```ruby
35
35
  class IsAdult < Assertion::Base
36
36
  attribute :age, :name
37
37
 
38
38
  def check
39
- age >= 18
39
+ age.to_i >= 18
40
40
  end
41
41
  end
42
42
  ```
43
43
 
44
- or using a builder for verbosity with the same result:
44
+ or with more verbose builder:
45
45
 
46
46
  ```ruby
47
47
  IsAdult = Assertion.about :age, :name do
48
- age >= 18
48
+ age.to_i >= 18
49
49
  end
50
50
  ```
51
51
 
52
- Define translations for both the *right* and *wrong* states of the assertion.
52
+ Define translations to describe both the *right* and *wrong* states of the assertion.
53
53
 
54
- All the declared attributes are available (that's why we declared a `name` as an attribute):
54
+ All the attributes are available in translations (that's why we declared the `name` as an attribute):
55
55
 
56
56
  ```yaml
57
57
  # config/locales/en.yml
@@ -63,7 +63,7 @@ en:
63
63
  wrong: "%{name} is a child yet (age %{age})"
64
64
  ```
65
65
 
66
- Check a state of a assertion for some argument(s), using class method `[]`:
66
+ Check a state of an assertion for some argument(s), using class method `[]`:
67
67
 
68
68
  ```ruby
69
69
  john = { name: 'John', age: 10, gender: :male }
@@ -81,9 +81,10 @@ state.messages # => ["John is a child yet (age 10)"]
81
81
  state.validate! # => #<Assertion::InvalidError @messages=["John is a child yet (age 10)"]>
82
82
  ```
83
83
 
84
- ### Assertion Inversion
84
+ Inversion
85
+ ---------
85
86
 
86
- Use the `.not` *class* method to negate a assertion:
87
+ Use the `.not` *class* method to negate the assertion:
87
88
 
88
89
  ```ruby
89
90
  jack = { name: 'Jack', age: 21, gender: :male }
@@ -92,7 +93,8 @@ IsAdult.not[jack]
92
93
  # => #<Assertion::State @state=false, @messages=["Jack is already an adult (age 21)"]>
93
94
  ```
94
95
 
95
- ### Composition of States
96
+ Composition
97
+ -----------
96
98
 
97
99
  You can compose assertion states (results):
98
100
 
@@ -121,54 +123,63 @@ state = IsAdult[jane] & IsMale[jane]
121
123
  # => #<Assertion::State @state=false, @messages=["Jane is a child yet (age 16)", "Jane is a female"]>
122
124
  ```
123
125
 
124
- Object Validation
125
- -----------------
126
-
127
- If you are used to "rails style" validations, provide a validator from assertions:
126
+ Guards
127
+ ------
128
128
 
129
- ```ruby
130
- class User
131
- include Virtus.model
129
+ The guard class is a lean wrapper around the state of its object.
132
130
 
133
- attribute :name, String
134
- attribute :age, Integer
135
- attribute :gender, Symbol
136
- end
131
+ It defines the `#state` for the object and checks if the state is valid:
137
132
 
138
- class Women < User
139
- def validate!
140
- state.validate!
141
- end
133
+ ```ruby
134
+ class VoterOnly < Assertion::Guard
135
+ alias_method :user, :object
142
136
 
143
137
  def state
144
- IsAdult[attributes] & IsMale.not[attributes]
138
+ IsAdult[user.attributes] & IsCitizen[user.attributes]
145
139
  end
146
140
  end
147
-
148
- judy = Women.new(name: "Judy", age: 15, gender: :female)
149
- judy.valdate!
150
- # => #<Assertion::InvalidError @messages=["Judy is a child yet (age 15)"]>
151
141
  ```
152
142
 
153
- Edge Cases
154
- ----------
143
+ Or using the verbose builder `Assertion.guards`:
155
144
 
156
- You're expected to declare the `check` method for the assertion before applying it to some data.
145
+ ```ruby
146
+ VoterOnly = Assertion.guards :user do
147
+ IsAdult[user.attributes] & IsCitizen[user.attributes]
148
+ end
149
+ ```
157
150
 
158
- Otherwise `Assertion::NotImplementedError` is raised:
151
+ When the guard is called for some object, its calls `#validate!` and then returns the source object. That simple.
159
152
 
160
153
  ```ruby
161
- IsAdult = Assertion.about :name, :age
154
+ jack = OpenStruct.new(name: "Jack", age: 15, citizen: true)
155
+ john = OpenStruct.new(name: "John", age: 34, citizen: true)
156
+
157
+ voter = VoterOnly[jack]
158
+ # => #<Assertion::InvalidError @messages=["Jack is a child yet (age 15)"]
162
159
 
163
- IsAdult[name: "Jane", age: 10]
164
- # => #<Assertion::NotImplementedError @message="IsAdult#check method not implemented">
160
+ voter = VoterOnly[john]
161
+ # => #<OpenStruct @name="John", @age=34>
165
162
  ```
166
163
 
167
- You cannot define attributes with names, that are already used as an istance methods:
164
+ Naming Convention
165
+ -----------------
166
+
167
+ This is not necessary, but for verbosity you could follow the rules:
168
+
169
+ * use the prefix `Is` for assertions (like `IsAdult`)
170
+ * use the suffix `Only` for guards (like `AdultOnly`)
171
+
172
+ Edge Cases
173
+ ----------
174
+
175
+ You cannot define attributes with names already defined as istance methods:
168
176
 
169
177
  ```ruby
170
- IsAdult = Assertion.about :check, :call
171
- # => #<Assertion::NameError @message="Wrong name(s) for attribute(s): check, call">
178
+ IsAdult = Assertion.about :check
179
+ # => #<Assertion::NameError @message="Wrong name(s) for attribute(s): check">
180
+
181
+ AdultOnly = Assertion.guards :state
182
+ # => #<Assertion::NameError @message="Wrong name(s) for attribute(s): state">
172
183
  ```
173
184
 
174
185
  Installation
@@ -1,20 +1,19 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require "transproc"
4
- require "i18n"
5
4
 
6
5
  require_relative "assertion/transprocs/inflector"
7
- require_relative "assertion/transprocs/i18n"
8
6
  require_relative "assertion/transprocs/list"
9
7
 
10
- require_relative "assertion/exceptions/name_error"
11
- require_relative "assertion/exceptions/not_implemented_error"
12
- require_relative "assertion/exceptions/invalid_error"
8
+ require_relative "assertion/invalid_error"
9
+ require_relative "assertion/attributes"
10
+ require_relative "assertion/messages"
13
11
 
14
12
  require_relative "assertion/state"
15
13
  require_relative "assertion/base"
16
14
  require_relative "assertion/inversion"
17
15
  require_relative "assertion/inverter"
16
+ require_relative "assertion/guard"
18
17
 
19
18
  # The module allows declaring assertions (assertions) about various objects,
20
19
  # and apply (validate) them to concrete data.
@@ -66,7 +65,7 @@ module Assertion
66
65
  # @param [Proc] block
67
66
  # The content for the `check` method
68
67
  #
69
- # @return [Assertion::Base]
68
+ # @return [Class] The specific assertion class
70
69
  #
71
70
  def self.about(*attributes, &block)
72
71
  klass = Class.new(Base)
@@ -76,4 +75,36 @@ module Assertion
76
75
  klass
77
76
  end
78
77
 
78
+ # Builds the subclass of `Assertion::Guard` with given attribute
79
+ # (alias for the `object`) and implementation of the `#state` method.
80
+ #
81
+ # @example
82
+ # VoterOnly = Assertion.guards :user do
83
+ # IsAdult[user.attributes] & IsCitizen[user.attributes]
84
+ # end
85
+ #
86
+ # # This is the same as:
87
+ # class VoterOnly < Assertion::Guard
88
+ # alias_method :user, :object
89
+ #
90
+ # def state
91
+ # IsAdult[user.attributes] & IsCitizen[user.attributes]
92
+ # end
93
+ # end
94
+ #
95
+ # @param [Symbol] attribute
96
+ # The alias for the `object` attribute
97
+ # @param [Proc] block
98
+ # The content for the `state` method
99
+ #
100
+ # @return [Class] The specific guard class
101
+ #
102
+ def self.guards(attribute = nil, &block)
103
+ klass = Class.new(Guard)
104
+ klass.public_send(:attribute, attribute) if attribute
105
+ klass.__send__(:define_method, :state, &block) if block_given?
106
+
107
+ klass
108
+ end
109
+
79
110
  end # module Assertion
@@ -0,0 +1,54 @@
1
+ # encoding: utf-8
2
+
3
+ module Assertion
4
+
5
+ # Module Attributes provides features to define and store a list of attributes
6
+ # shared by the [Assertion::Base] and [Assertion::Guard] classes
7
+ #
8
+ module Attributes
9
+
10
+ # List of attributes, defined for the class
11
+ #
12
+ # @return [Array<Symbol>]
13
+ #
14
+ def attributes
15
+ @attributes ||= []
16
+ end
17
+
18
+ # Adds a new attribute or a list of attributes to the class
19
+ #
20
+ # @param [Symbol, Array<Symbol>] names
21
+ #
22
+ # @return [undefined]
23
+ #
24
+ # @raise [NameError]
25
+ # When the name is either used by instance attribute,
26
+ # or forbidden as a name of the method to be implemented later
27
+ # (not as an attribute)
28
+ #
29
+ def attribute(*names)
30
+ @attributes = List[:symbolize][attributes + names]
31
+ __check_attributes__
32
+ end
33
+
34
+ private
35
+
36
+ # Names of the methods that should be reserved to be used later
37
+ #
38
+ # @return [Array<Symbol>]
39
+ #
40
+ # @abstract
41
+ #
42
+ def __forbidden_attributes__
43
+ []
44
+ end
45
+
46
+ def __check_attributes__
47
+ names = attributes & (instance_methods + __forbidden_attributes__)
48
+ return if names.empty?
49
+ fail NameError.new "Wrong name(s) for attribute(s): #{names.join(", ")}"
50
+ end
51
+
52
+ end # module Attributes
53
+
54
+ end # module Assertion
@@ -35,33 +35,14 @@ module Assertion
35
35
  #
36
36
  class Base
37
37
 
38
+ extend Attributes
39
+ include Messages
40
+
38
41
  # Class DSL
39
42
  #
40
43
  class << self
41
44
 
42
- # List of attributes, defined for the class
43
- #
44
- # @return [Array<Symbol>]
45
- #
46
- def attributes
47
- @attributes ||= []
48
- end
49
-
50
- # Adds a new attribute or a list of attributes to the class
51
- #
52
- # @param [Symbol, Array<Symbol>] names
53
- #
54
- # @return [undefined]
55
- #
56
- # @raise [Assertion::NameError]
57
- # When the name is already used by instance attribute
58
- #
59
- def attribute(*names)
60
- @attributes = List[:symbolize][attributes + names]
61
- __check__
62
- end
63
-
64
- # Initializes a assertion with some attributes (data) and then calls it
45
+ # Initializes an assertion with some attributes (data) and then calls it
65
46
  #
66
47
  # @param [Hash] hash
67
48
  #
@@ -96,10 +77,8 @@ module Assertion
96
77
 
97
78
  private
98
79
 
99
- # Checks if all the attributes have valid names
100
- def __check__
101
- wrong = attributes & instance_methods
102
- fail(NameError.new wrong) if wrong.any?
80
+ def __forbidden_attributes__
81
+ [:check]
103
82
  end
104
83
 
105
84
  end # eigenclass
@@ -126,28 +105,6 @@ module Assertion
126
105
  freeze
127
106
  end
128
107
 
129
- # Returns the translated message about the current state of the assertion
130
- # applied to its attributes
131
- #
132
- # @param [Symbol] state The state to be described
133
- #
134
- # @return [String] The message
135
- #
136
- def message(state = nil)
137
- I18n[:translate, __scope__, attributes][state ? :right : :wrong]
138
- end
139
-
140
- # Checks whether the assertion is right for the current attributes
141
- #
142
- # @return [Boolean]
143
- #
144
- # @raise [Check::NotImplementedError]
145
- # When the [#check] method hasn't been implemented
146
- #
147
- def check
148
- fail NotImplementedError.new(self.class, :check)
149
- end
150
-
151
108
  # Calls the assertion checkup and returns the state of the assertion having
152
109
  # been applied to the current attributes
153
110
  #
@@ -156,26 +113,11 @@ module Assertion
156
113
  #
157
114
  def call
158
115
  state = check
159
- State.new state, message
116
+ State.new state, message(false)
160
117
  end
161
118
 
162
119
  private
163
120
 
164
- # The scope for translating messages using the `I18n` module
165
- #
166
- # @return [Array<Symbol>]
167
- #
168
- def __scope__
169
- I18n[:scope][self.class.name]
170
- end
171
-
172
- # Defines the object attribute and assigns given value to it
173
- #
174
- # @param [Symbol] name The name of the attribute
175
- # @param [Object] value The value of the attribute
176
- #
177
- # @return [undefined]
178
- #
179
121
  def __set_attribute__(name, value)
180
122
  attributes[name] = value
181
123
  singleton_class.__send__(:define_method, name) { value }
@@ -0,0 +1,99 @@
1
+ # encoding: utf-8
2
+
3
+ module Assertion
4
+
5
+ # The base class for object guards
6
+ #
7
+ # The guard defines a desired state for the object and checks
8
+ # if that state is valid.
9
+ #
10
+ # Its `call` method either returns the guarded object, or
11
+ # (when its state is invalid) raises an exception
12
+ #
13
+ # The class DSL also defines a `.[]` shortcut to initialize
14
+ # and call the guard for given object immediately.
15
+ #
16
+ # @example
17
+ # class AdultOnly < Assertion::Guard
18
+ # alias_method :user, :object
19
+ #
20
+ # def state
21
+ # IsAdult[user.attributes]
22
+ # end
23
+ # end
24
+ #
25
+ # jack = User.new name: "Jack", age: 10
26
+ # john = User.new name: "John", age: 59
27
+ #
28
+ # AdultOnly[jack]
29
+ # # => #<Assertion::InvalidError @messages=["Jack is a child (age 10)"]>
30
+ # AdultOnly[john]
31
+ # # => #<User @name="John", @age=59>
32
+ #
33
+ class Guard
34
+
35
+ extend Attributes
36
+
37
+ class << self
38
+
39
+ # Initializes and guard for the provided object and calls it immediately
40
+ #
41
+ # @param [Object] object The object whose state should be tested
42
+ #
43
+ # @return (see #call)
44
+ #
45
+ # @raise (see #call)
46
+ #
47
+ def [](object)
48
+ new(object).call
49
+ end
50
+
51
+ private
52
+
53
+ def __forbidden_attributes__
54
+ [:state]
55
+ end
56
+
57
+ end # eigenclass
58
+
59
+ # @!attribute [r] object
60
+ #
61
+ # @return [Object] The object whose state should be tested
62
+ #
63
+ attr_reader :object
64
+
65
+ # @!scope class
66
+ # @!method new(object)
67
+ # Creates the guard instance for the provided object
68
+ #
69
+ # @param [Object] object
70
+ #
71
+ # @return [Assertion::Guard]
72
+
73
+ # @private
74
+ def initialize(object)
75
+ @object = object
76
+ self.class.attributes.each(&method(:__set_attribute__))
77
+ freeze
78
+ end
79
+
80
+ # Validates the state of the [#object] and returns valid object back
81
+ #
82
+ # @return (see #object)
83
+ #
84
+ # @raise [Assertion::InvalidError] if the [#object] is invalid
85
+ #
86
+ def call
87
+ state.validate!
88
+ object
89
+ end
90
+
91
+ private
92
+
93
+ def __set_attribute__(name)
94
+ singleton_class.instance_eval { alias_method name, :object }
95
+ end
96
+
97
+ end # class Guard
98
+
99
+ end # module Assertion