value_semantics 2.1.0 → 3.0.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
  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"