value_semantics 2.1.0 → 3.0.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
  SHA256:
3
- metadata.gz: 17ee24bc8ffd744493e9957a4cb7ee0071f0c33aa8560b3ed36689575b9c0ab1
4
- data.tar.gz: 05da8d260966f9257578f0ebf3891ba447b51032709bb75b39bdacba3e081961
3
+ metadata.gz: 1b538b4f5ab14c310ffdbc26ccb92cb9b47806126d0bc77ef466e2d9e7d64fcb
4
+ data.tar.gz: 85a0542f08c4a832b21acf75a697f999b7eae42f6a8989ae8c1d17838b76fd8a
5
5
  SHA512:
6
- metadata.gz: 4a4da5150969146f5fd8cb06a17f8e43b1c897cd40f228cdddd7b2a8367fdbbceeacc66843668bf74a48575d72f752d3f24fd101c2ac7e946335688a1eec95a3
7
- data.tar.gz: c0e71a37be6df7bcafa4833f7b7cea4304468818a4391e020770c6f85e16edaf68a56c84c572f3bd26399aa85b2dc149abb48660d82750a656d746d87da5a16a
6
+ metadata.gz: 3f869be70a14418aa6247cca5222e53aed3bc7d4b506f1231e4f010b382cd962d08558a92506b47817600acca04d0f5cad1c4f9375ca6c249042ada9ea709fb3
7
+ data.tar.gz: 3c26016b77c88eb8ae851dadaf2853eb0792beb68a8dfb2961dca77d2b98414e6ba4f31ba0141a23544fb18dfcc73425cc9f1737df6539933e1b815a6037f564
data/.travis.yml CHANGED
@@ -1,13 +1,24 @@
1
1
  language: ruby
2
+ script: bin/test
3
+
4
+ # test old Ruby versions WITHOUT mutation testing
2
5
  rvm:
3
- - 2.3.7
4
- - 2.4.4
5
- - 2.5.1
6
- script: bin/mutation_test.sh
6
+ - 2.3.8
7
+ - 2.4.5
8
+ - 2.5.3
9
+ env: MUTATION_TEST=false
10
+
11
+ # test the latest Ruby version WITH mutation testing
12
+ matrix:
13
+ include:
14
+ - rvm: 2.6.0
15
+ env: MUTATION_TEST=true
16
+
17
+ # deploy gem on tagged commits, on the latest Ruby version only
7
18
  deploy:
8
19
  provider: rubygems
9
20
  on:
10
21
  tags: true
11
- rvm: 2.5.1
22
+ env: MUTATION_TEST=true
12
23
  api_key:
13
24
  secure: nL74QuUczEpA0qbhSBN2zjGdviWgKB3wR6vFvwervv1MZNWmwOQUYe99Oq9kPeyc8/x2MR/H6PQm5qbrk/WAfRede01WxlZ/EBUW+9CYGrxcBsGONx9IULO8A0I8/yN/YJHW2vjo3dfR66EwVsXTVWq8U63PRRcwJIyTqnIiUm2sxauMQoPRBbXG+pD9v/EJSn3ugpdtxp0lVYDn8LDKk5Ho4/wbpY4ML11XUJa9mz9CyR/GsAzdy5FTXaDMOwuWOVEx9cab7m4qPOBhmlJY4TrmooFpxTxRwChcvByjq1IboEd2M3RT5on7Q/xDTlHSOuT0OS8mnS2AocGT4a1gC+W/xOlghgEcN+xs2V5mfucR6+iUYlCy32uz1w3ey7T2X5xN4ubut09r1xLi7eu1NisAoAc+GOJ4TIxQNqkeRhY4X/fs8j7SMfOEMDr6pPxSLKZxgSvExt+IbdcZD/uQ7rTBQkadYCbc9MX5dHazBievmar3ZsFffbIf+n13FVDXsaPgRt7DlFM5dqGrEwVwt1jFRhdFuDCjkj4QWOLn7E1uY3XqgrqGvgUBlF8Znwc6qicW8zxV4SIWhqIzCOH6L9WIZGLHNq0remoCd9sq9Ter9av3jL+6UmZRRAr+JceeZfZmsYIXKomECzleM9FXMx7FXlpjJKOlf3JnrfeCTwI=
data/README.md CHANGED
@@ -103,7 +103,7 @@ Defaults
103
103
  --------
104
104
 
105
105
  Defaults can be specified in one of two ways:
106
- the `:default` option, or the `default_generator` option.
106
+ the `:default` option, or the `:default_generator` option.
107
107
 
108
108
  ```ruby
109
109
  class Cat
@@ -164,8 +164,8 @@ for common situations:
164
164
  class LightSwitch
165
165
  include ValueSemantics.for_attributes {
166
166
 
167
- # Boolean: only allows `true` or `false`
168
- on? Boolean()
167
+ # Bool: only allows `true` or `false`
168
+ on? Bool()
169
169
 
170
170
  # ArrayOf: validates elements in an array
171
171
  light_ids ArrayOf(Integer)
@@ -174,7 +174,7 @@ class LightSwitch
174
174
  color Either(Integer, String, nil)
175
175
 
176
176
  # these validators are composable
177
- wierd_attr Either(Boolean(), ArrayOf(Boolean()))
177
+ wierd_attr Either(Bool(), ArrayOf(Bool()))
178
178
  }
179
179
  end
180
180
 
data/bin/test ADDED
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ set -ue
3
+
4
+ MUTANT_PATTERN=${1:-ValueSemantics*}
5
+
6
+ # if $MUTATION_TEST is false, just run RSpec
7
+ if [[ "${MUTATION_TEST:-true}" == "false" ]] ; then
8
+ bundle exec rspec
9
+ else
10
+ bundle exec mutant \
11
+ --include lib \
12
+ --require value_semantics \
13
+ --use rspec "$MUTANT_PATTERN" \
14
+ # Mutant 0.8.24 introduces new mutations that cause infinite recursion inside
15
+ # #method_missing. These --ignore-subject lines prevent that from happening
16
+ #--ignore-subject "ValueSemantics::DSL#method_missing" \
17
+ #--ignore-subject "ValueSemantics::DSL#respond_to_missing?" \
18
+ #--ignore-subject "ValueSemantics::DSL#def_attr" \
19
+ fi
@@ -1,39 +1,80 @@
1
1
  module ValueSemantics
2
+ class Error < StandardError; end
3
+ class UnrecognizedAttributes < Error; end
4
+ class NoDefaultValue < Error; end
5
+ class MissingAttributes < Error; end
6
+
2
7
  NOT_SPECIFIED = Object.new.freeze
3
8
 
9
+ #
10
+ # Creates a module via the DSL
11
+ #
12
+ # @yield The block containing the DSL
13
+ # @return [Module]
14
+ #
15
+ # @see DSL
16
+ # @see InstanceMethods
17
+ #
4
18
  def self.for_attributes(&block)
5
- attributes = DSL.run(&block)
6
- generate_module(attributes.freeze)
19
+ recipe = DSL.run(&block)
20
+ bake_module(recipe)
7
21
  end
8
22
 
9
- def self.generate_module(attributes)
23
+ #
24
+ # Creates a module from a {Recipe}
25
+ #
26
+ # @param recipe [Recipe]
27
+ # @return [Module]
28
+ #
29
+ def self.bake_module(recipe)
10
30
  Module.new do
11
- # include all the instance methods
12
- include(Semantics)
31
+ const_set(:VALUE_SEMANTICS_RECIPE__, recipe)
32
+ include(InstanceMethods)
13
33
 
14
34
  # define the attr readers
15
- attributes.each do |attr|
35
+ recipe.attributes.each do |attr|
16
36
  module_eval("def #{attr.name}; #{attr.instance_variable}; end")
17
37
  end
18
38
 
19
- # define BaseClass.attributes class method
20
- const_set(:ATTRIBUTES__, attributes)
21
39
  def self.included(base)
22
- base.const_set(:ValueSemantics_Generated, self)
23
- class << base
24
- def attributes
25
- self::ATTRIBUTES__
26
- end
27
- end
40
+ base.const_set(:ValueSemantics_Attributes, self)
41
+ base.extend(ClassMethods)
28
42
  end
29
43
  end
30
44
  end
31
45
 
32
- module Semantics
46
+ #
47
+ # All the class methods available on ValueSemantics classes
48
+ #
49
+ # When a ValueSemantics module is included into a class,
50
+ # the class is extended by this module.
51
+ #
52
+ module ClassMethods
53
+ #
54
+ # @return [Recipe] the recipe used to build the ValueSemantics module that
55
+ # was included into this class.
56
+ #
57
+ def value_semantics
58
+ self::VALUE_SEMANTICS_RECIPE__
59
+ end
60
+ end
61
+
62
+ #
63
+ # All the instance methods available on ValueSemantics objects
64
+ #
65
+ module InstanceMethods
66
+ #
67
+ # Creates a value object based on a Hash of attributes
68
+ #
69
+ # @param given_attrs [Hash] a hash of attributes, with symbols for keys
70
+ # @raise [UnrecognizedAttributes] if given_attrs contains keys that are not attributes
71
+ # @raise [MissingAttributes] if given_attrs is missing any attributes that do not have defaults
72
+ # @raise [ArgumentError] if any attribute values do no pass their validators
73
+ #
33
74
  def initialize(given_attrs = {})
34
75
  remaining_attrs = given_attrs.dup
35
76
 
36
- self.class.attributes.each do |attr|
77
+ self.class.value_semantics.attributes.each do |attr|
37
78
  key, value = attr.determine_from!(remaining_attrs, self.class)
38
79
  instance_variable_set(attr.instance_variable, value)
39
80
  remaining_attrs.delete(key)
@@ -41,30 +82,54 @@ module ValueSemantics
41
82
 
42
83
  unless remaining_attrs.empty?
43
84
  unrecognised = remaining_attrs.keys.map(&:inspect).join(', ')
44
- raise ArgumentError, "Unrecognised attributes: #{unrecognised}"
85
+ raise UnrecognizedAttributes, "Unrecognized attributes: #{unrecognised}"
45
86
  end
46
87
  end
47
88
 
89
+ #
90
+ # Creates a copy of this object, with the given attributes changed (non-destructive update)
91
+ #
92
+ # @param new_attrs [Hash] the attributes to change
93
+ # @return A new object, with the attribute changes applied
94
+ #
48
95
  def with(new_attrs)
49
96
  self.class.new(to_h.merge(new_attrs))
50
97
  end
51
98
 
99
+ #
100
+ # @return [Hash] all of the attributes
101
+ #
52
102
  def to_h
53
- self.class.attributes
103
+ self.class.value_semantics.attributes
54
104
  .map { |attr| [attr.name, public_send(attr.name)] }
55
105
  .to_h
56
106
  end
57
107
 
108
+ #
109
+ # Loose equality
110
+ #
111
+ # @return [Boolean] whether all attributes are equal, and the object
112
+ # classes are ancestors of eachother in any way
113
+ #
58
114
  def ==(other)
59
115
  (other.is_a?(self.class) || is_a?(other.class)) && other.to_h.eql?(to_h)
60
116
  end
61
117
 
118
+ #
119
+ # Strict equality
120
+ #
121
+ # @return [Boolean] whether all attribuets are equal, and both objects
122
+ # has the exact same class
123
+ #
62
124
  def eql?(other)
63
125
  other.class.equal?(self.class) && other.to_h.eql?(to_h)
64
126
  end
65
127
 
128
+ #
129
+ # Unique-ish integer, based on attributes and class of the object
130
+ #
66
131
  def hash
67
- @__hash ||= (to_h.hash ^ self.class.hash)
132
+ to_h.hash ^ self.class.hash
68
133
  end
69
134
 
70
135
  def inspect
@@ -76,12 +141,20 @@ module ValueSemantics
76
141
  end
77
142
  end
78
143
 
144
+ #
145
+ # Represents a single attribute of a value class
146
+ #
79
147
  class Attribute
80
- NO_DEFAULT_GENERATOR = ->{ raise "Attribute does not have a default value" }
148
+ NO_DEFAULT_GENERATOR = lambda do
149
+ raise NoDefaultValue, "Attribute does not have a default value"
150
+ end
81
151
 
82
152
  attr_reader :name, :validator, :coercer, :default_generator
83
153
 
84
- def initialize(name:, default_generator:, validator:, coercer:)
154
+ def initialize(name:,
155
+ default_generator: NO_DEFAULT_GENERATOR,
156
+ validator: Anything,
157
+ coercer: nil)
85
158
  @name = name.to_sym
86
159
  @default_generator = default_generator
87
160
  @validator = validator
@@ -89,10 +162,35 @@ module ValueSemantics
89
162
  freeze
90
163
  end
91
164
 
165
+ def self.define(name,
166
+ validator=Anything,
167
+ default: NOT_SPECIFIED,
168
+ default_generator: nil,
169
+ coerce: nil)
170
+ generator = begin
171
+ if default_generator && !default.equal?(NOT_SPECIFIED)
172
+ raise ArgumentError, "Attribute '#{name}' can not have both a :default and a :default_generator"
173
+ elsif default_generator
174
+ default_generator
175
+ elsif !default.equal?(NOT_SPECIFIED)
176
+ ->{ default }
177
+ else
178
+ NO_DEFAULT_GENERATOR
179
+ end
180
+ end
181
+
182
+ new(
183
+ name: name,
184
+ validator: validator,
185
+ default_generator: generator,
186
+ coercer: coerce,
187
+ )
188
+ end
189
+
92
190
  def determine_from!(attr_hash, klass)
93
191
  raw_value = attr_hash.fetch(name) do
94
192
  if default_generator.equal?(NO_DEFAULT_GENERATOR)
95
- raise ArgumentError, "Value missing for attribute '#{name}'"
193
+ raise MissingAttributes, "Value missing for attribute '#{name}'"
96
194
  else
97
195
  default_generator.call
98
196
  end
@@ -130,11 +228,38 @@ module ValueSemantics
130
228
  end
131
229
  end
132
230
 
231
+ #
232
+ # Contains all the configuration necessary to bake a ValueSemantics module
233
+ #
234
+ # @see ValueSemantics.bake_module
235
+ #
236
+ class Recipe
237
+ attr_reader :attributes
238
+
239
+ def initialize(attributes:)
240
+ @attributes = attributes
241
+ freeze
242
+ end
243
+ end
244
+
245
+ #
246
+ # Builds a {Recipe} via DSL methods
247
+ #
248
+ # DSL blocks are <code>instance_eval</code>d against an object of this class.
249
+ #
250
+ # @see Recipe
251
+ # @see ValueSemantics.for_attributes
252
+ #
133
253
  class DSL
254
+ #
255
+ # Builds a {Recipe} from a DSL block
256
+ #
257
+ # @yield to the block containing the DSL
258
+ # @return [Recipe]
134
259
  def self.run(&block)
135
260
  dsl = new
136
261
  dsl.instance_eval(&block)
137
- dsl.__attributes
262
+ Recipe.new(attributes: dsl.__attributes.freeze)
138
263
  end
139
264
 
140
265
  attr_reader :__attributes
@@ -143,8 +268,8 @@ module ValueSemantics
143
268
  @__attributes = []
144
269
  end
145
270
 
146
- def Boolean
147
- Boolean
271
+ def Bool
272
+ Bool
148
273
  end
149
274
 
150
275
  def Either(*subvalidators)
@@ -159,30 +284,8 @@ module ValueSemantics
159
284
  ArrayOf.new(element_validator)
160
285
  end
161
286
 
162
- def def_attr(attr_name,
163
- validator=Anything,
164
- default: NOT_SPECIFIED,
165
- default_generator: nil,
166
- coerce: nil
167
- )
168
- generator = begin
169
- if default_generator && !default.equal?(NOT_SPECIFIED)
170
- raise ArgumentError, "Attribute '#{attr_name}' can not have both a default, and a default_generator"
171
- elsif default_generator
172
- default_generator
173
- elsif !default.equal?(NOT_SPECIFIED)
174
- ->{ default }
175
- else
176
- Attribute::NO_DEFAULT_GENERATOR
177
- end
178
- end
179
-
180
- __attributes << Attribute.new(
181
- name: attr_name,
182
- validator: validator,
183
- default_generator: generator,
184
- coercer: coerce
185
- )
287
+ def def_attr(*args)
288
+ __attributes << Attribute.define(*args)
186
289
  end
187
290
 
188
291
  def method_missing(name, *args)
@@ -194,25 +297,34 @@ module ValueSemantics
194
297
  end
195
298
 
196
299
  def respond_to_missing?(method_name, _include_private=nil)
197
- first_letter = method_name[0]
300
+ first_letter = method_name.to_s.each_char.first
198
301
  first_letter.eql?(first_letter.downcase)
199
302
  end
200
303
  end
201
304
 
202
- module Boolean
203
- extend self
204
-
205
- def ===(value)
305
+ #
306
+ # Validator that only matches `true` and `false`
307
+ #
308
+ module Bool
309
+ # @return [Boolean]
310
+ def self.===(value)
206
311
  true.equal?(value) || false.equal?(value)
207
312
  end
208
313
  end
209
314
 
315
+ #
316
+ # Validator that matches any and all values
317
+ #
210
318
  module Anything
319
+ # @return [true]
211
320
  def self.===(_)
212
321
  true
213
322
  end
214
323
  end
215
324
 
325
+ #
326
+ # Validator that matches if any of the given subvalidators matches
327
+ #
216
328
  class Either
217
329
  attr_reader :subvalidators
218
330
 
@@ -221,11 +333,15 @@ module ValueSemantics
221
333
  freeze
222
334
  end
223
335
 
336
+ # @return [Boolean]
224
337
  def ===(value)
225
338
  subvalidators.any? { |sv| sv === value }
226
339
  end
227
340
  end
228
341
 
342
+ #
343
+ # Validator that matches arrays if each element matches a given subvalidator
344
+ #
229
345
  class ArrayOf
230
346
  attr_reader :element_validator
231
347
 
@@ -234,6 +350,7 @@ module ValueSemantics
234
350
  freeze
235
351
  end
236
352
 
353
+ # @return [Boolean]
237
354
  def ===(value)
238
355
  Array === value && value.all? { |element| element_validator === element }
239
356
  end
@@ -1,3 +1,3 @@
1
1
  module ValueSemantics
2
- VERSION = "2.1.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "bundler", "~> 1.15"
31
31
  spec.add_development_dependency "rspec", "~> 3.7.0"
32
32
  spec.add_development_dependency "mutant-rspec"
33
+ spec.add_development_dependency "yard"
33
34
  spec.add_development_dependency "byebug"
34
35
  spec.add_development_dependency "gem-release"
35
36
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: value_semantics
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Dalling
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-12-21 00:00:00.000000000 Z
11
+ date: 2019-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: byebug
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -97,8 +111,8 @@ files:
97
111
  - LICENSE.txt
98
112
  - README.md
99
113
  - bin/console
100
- - bin/mutation_test.sh
101
114
  - bin/setup
115
+ - bin/test
102
116
  - lib/value_semantics.rb
103
117
  - lib/value_semantics/version.rb
104
118
  - value_semantics.gemspec
@@ -121,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
135
  - !ruby/object:Gem::Version
122
136
  version: '0'
123
137
  requirements: []
124
- rubygems_version: 3.0.0
138
+ rubygems_version: 3.0.2
125
139
  signing_key:
126
140
  specification_version: 4
127
141
  summary: Create value classes quickly, with all the proper conventions.
data/bin/mutation_test.sh DELETED
@@ -1,6 +0,0 @@
1
- #!/bin/sh
2
- set -ue
3
-
4
- PATTERN=${1:-ValueSemantics*}
5
-
6
- bundle exec mutant --include lib --require value_semantics --use rspec "$PATTERN"