assertion 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 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