invariable 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 +7 -0
- data/.gitignore +6 -0
- data/.yardopts +6 -0
- data/LICENSE +28 -0
- data/README.md +65 -0
- data/gems.rb +4 -0
- data/invariable.gemspec +35 -0
- data/lib/invariable/version.rb +6 -0
- data/lib/invariable.rb +384 -0
- data/rakefile.rb +12 -0
- data/samples/http_options.rb +90 -0
- data/samples/person.rb +49 -0
- data/spec/helper.rb +12 -0
- data/spec/invariable_include_spec.rb +97 -0
- data/spec/invariable_new_spec.rb +53 -0
- data/spec/invariable_spec.rb +291 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9c53305cea17a18d1408e1a877638284b336815349579758e185c01a9a5cc344
|
4
|
+
data.tar.gz: aeb9efc0033747f03a137d39c118e5d961e6cca2c805b4736043edf37b7ae5ad
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 437de015755448224c646e7e4a7cd07063658e75181442a437018869d40ee845eabd9f89823b7d6cefdcb3f0483193f0437ff19d1760184a76385e5089826fc2
|
7
|
+
data.tar.gz: a4a18c103fcfa25451c7be911640f870e236f782385283099829580fc3373b667c9a1c2e21fbcfcbd03d4ce3319c7c3b81520fa14abc0a3c38a96d5c05bc0cde
|
data/.gitignore
ADDED
data/.yardopts
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
BSD 3-Clause License
|
2
|
+
|
3
|
+
Copyright (c) 2017-2021, Mike Blumtritt. All rights reserved.
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
9
|
+
list of conditions and the following disclaimer.
|
10
|
+
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
13
|
+
and/or other materials provided with the distribution.
|
14
|
+
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
16
|
+
contributors may be used to endorse or promote products derived from
|
17
|
+
this software without specific prior written permission.
|
18
|
+
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Invariable
|
2
|
+
|
3
|
+
An Invariable bundles a number of read-only attributes.
|
4
|
+
It can be used like a Hash as well as an Array. It supports subclassing and pattern matching.
|
5
|
+
|
6
|
+
An Invariable can be created explicitly as a Class like a Struct. Or existing classes can easily be extended to an Invariable.
|
7
|
+
|
8
|
+
- Gem: [rubygems.org](https://rubygems.org/gems/invariable)
|
9
|
+
- Source: [github.com](https://github.com/mblumtritt/invariable)
|
10
|
+
- Help: [rubydoc.info](https://rubydoc.info/github/mblumtritt/invariable/main/index)
|
11
|
+
|
12
|
+
## Sample
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
require 'invariable'
|
16
|
+
|
17
|
+
class Person
|
18
|
+
include Invariable
|
19
|
+
attributes :name, :last_name
|
20
|
+
attribute address: Invariable.new(:city, :zip, :street)
|
21
|
+
|
22
|
+
def full_name
|
23
|
+
"#{name} #{last_name}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
...
|
27
|
+
john = Person.new(name: 'John', last_name: 'Doe')
|
28
|
+
john.full_name #=> "John Doe"
|
29
|
+
john.address.city #=> nil
|
30
|
+
john = john.update(
|
31
|
+
address: { street: '123 Main St', city: 'Anytown', zip: '45678' }
|
32
|
+
)
|
33
|
+
john.dig(:address, :city) #=> "Anytown"
|
34
|
+
|
35
|
+
```
|
36
|
+
|
37
|
+
For more samples see [the samples dir](https://github.com/mblumtritt/invariable/tree/main/samples)
|
38
|
+
|
39
|
+
## Installation
|
40
|
+
|
41
|
+
Use [Bundler](http://gembundler.com/) to use TCPClient in your own project:
|
42
|
+
|
43
|
+
Add to your `Gemfile`:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
gem 'invariable'
|
47
|
+
```
|
48
|
+
|
49
|
+
and install it by running Bundler:
|
50
|
+
|
51
|
+
```bash
|
52
|
+
bundle
|
53
|
+
```
|
54
|
+
|
55
|
+
To install the gem globally use:
|
56
|
+
|
57
|
+
```bash
|
58
|
+
gem install invariable
|
59
|
+
```
|
60
|
+
|
61
|
+
After that you need only a single line of code in your project to have all tools on board:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
require 'invariable'
|
65
|
+
```
|
data/gems.rb
ADDED
data/invariable.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './lib/invariable/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'invariable'
|
7
|
+
spec.version = Invariable::VERSION
|
8
|
+
spec.required_ruby_version = '>= 2.7.0'
|
9
|
+
|
10
|
+
spec.author = 'Mike Blumtritt'
|
11
|
+
spec.summary = 'The Invariable data class for Ruby.'
|
12
|
+
spec.description = <<~description
|
13
|
+
An Invariable bundles a number of read-only attributes.
|
14
|
+
It can be used like a Hash as well as an Array.
|
15
|
+
It supports subclassing and pattern matching.
|
16
|
+
description
|
17
|
+
|
18
|
+
spec.homepage = 'https://github.com/mblumtritt/invariable'
|
19
|
+
spec.license = 'BSD-3-Clause'
|
20
|
+
spec.metadata.merge!(
|
21
|
+
'source_code_uri' => 'https://github.com/mblumtritt/invariable',
|
22
|
+
'bug_tracker_uri' => 'https://github.com/mblumtritt/invariable/issues',
|
23
|
+
'documentation_uri' => 'https://rubydoc.info/github/mblumtritt/invariable'
|
24
|
+
)
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler'
|
27
|
+
spec.add_development_dependency 'rake'
|
28
|
+
spec.add_development_dependency 'rspec'
|
29
|
+
spec.add_development_dependency 'yard'
|
30
|
+
|
31
|
+
all_files = Dir.chdir(__dir__) { `git ls-files -z`.split(0.chr) }
|
32
|
+
spec.test_files = all_files.grep(%r{^spec/})
|
33
|
+
spec.files = all_files - spec.test_files
|
34
|
+
spec.extra_rdoc_files = %w[README.md LICENSE]
|
35
|
+
end
|
data/lib/invariable.rb
ADDED
@@ -0,0 +1,384 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# An Invariable bundles a number of read-only attributes.
|
5
|
+
# It can be used like a Hash as well as an Array. It supports subclassing
|
6
|
+
# and pattern matching.
|
7
|
+
#
|
8
|
+
# An Invariable can be created explicitly as a Class like a Struct. Or existing
|
9
|
+
# classes can easily be extended to an Invariable.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class Person
|
13
|
+
# include Invariable
|
14
|
+
# attributes :name, :last_name
|
15
|
+
# attribute address: Invariable.new(:city, :zip, :street)
|
16
|
+
#
|
17
|
+
# def full_name
|
18
|
+
# "#{name} #{last_name}"
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
# ...
|
22
|
+
# john = Person.new(name: 'John', last_name: 'Doe')
|
23
|
+
# john.full_name #=> "John Doe"
|
24
|
+
# john.address.city #=> nil
|
25
|
+
# john = john.update(
|
26
|
+
# address: { street: '123 Main St', city: 'Anytown', zip: '45678' }
|
27
|
+
# )
|
28
|
+
# john.dig(:address, :city) #=> "Anytown"
|
29
|
+
#
|
30
|
+
module Invariable
|
31
|
+
class << self
|
32
|
+
#
|
33
|
+
# @!attribute [r] members
|
34
|
+
# @return [Array<Symbol>] all attribute names of this class
|
35
|
+
#
|
36
|
+
# @!method attributes(*names, **defaults)
|
37
|
+
# Defines new attributes
|
38
|
+
# @param names [Array<Symbol>] attribute names
|
39
|
+
# @param defaults [Hash<Symbol,Object|Class>] attribute names with default
|
40
|
+
# values
|
41
|
+
# @return [Array<Symbols>] names of defined attributes
|
42
|
+
#
|
43
|
+
# @!method member?(name)
|
44
|
+
# @return [Boolean] wheter the given name is a valid attribute name for
|
45
|
+
# this class
|
46
|
+
#
|
47
|
+
|
48
|
+
#
|
49
|
+
# Creates a new class with the given attribute names. It also allows to
|
50
|
+
# specify default values which are used when an instance is created.
|
51
|
+
#
|
52
|
+
# With an optional block the class can be extended.
|
53
|
+
#
|
54
|
+
# @overload new(*names, **defaults, &block)
|
55
|
+
# @example create a simple User class
|
56
|
+
# User = Invariable.new(:name, :last_name)
|
57
|
+
# User.members #=> [:name, :last_name]
|
58
|
+
#
|
59
|
+
# @example create a User class with a default value
|
60
|
+
# User = Invariable.new(:name, :last_name, processed: false)
|
61
|
+
# User.new(name: 'John', last_name: 'Doe').to_h
|
62
|
+
# #=> {:name=>"John", :last_name=>"Doe", :processed=>false}
|
63
|
+
#
|
64
|
+
# @example create a User class with an additional method
|
65
|
+
# User = Invariable.new(:name, :last_name) do
|
66
|
+
# def full_name
|
67
|
+
# "#{name} #{last_name}"
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
# User.new(name: 'John', last_name: 'Doe').full_name
|
71
|
+
# #=> "John Doe"
|
72
|
+
#
|
73
|
+
# @overload new(base_class, *names, **defaults, &block)
|
74
|
+
# @example create a Person class derived from a User class
|
75
|
+
# User = Invariable.new(:name, :last_name)
|
76
|
+
# Person = Invariable.new(User, :city, :zip, :street)
|
77
|
+
# Person.members #=> [:name, :last_name, :city, :zip, :street]
|
78
|
+
#
|
79
|
+
# @param names [Array<Symbol>] attribute names
|
80
|
+
# @param defaults [Hash<Symbol,Object|Class>] attribute names with default
|
81
|
+
# values
|
82
|
+
# @yieldparam new_class [Class] the created class
|
83
|
+
#
|
84
|
+
# @return [Class] the created class
|
85
|
+
#
|
86
|
+
def new(*names, **defaults, &block)
|
87
|
+
Class.new(names.first.is_a?(Class) ? names.shift : Object) do
|
88
|
+
include(Invariable)
|
89
|
+
attributes(*names, **defaults)
|
90
|
+
class_eval(&block) if block
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def included(base)
|
97
|
+
base.extend(InvariableClassMethods)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Initializes a new instance with the given `attributes` Hash.
|
103
|
+
#
|
104
|
+
# @return [Invariable] itself
|
105
|
+
#
|
106
|
+
def initialize(attributes = nil)
|
107
|
+
super()
|
108
|
+
attributes ||= {}.compare_by_identity
|
109
|
+
@__attr__ = {}
|
110
|
+
self
|
111
|
+
.class
|
112
|
+
.instance_variable_get(:@__attr__)
|
113
|
+
.each_pair do |key, default|
|
114
|
+
@__attr__[key] =
|
115
|
+
if default.is_a?(Class)
|
116
|
+
default.new(attributes[key]).freeze
|
117
|
+
elsif attributes.key?(key)
|
118
|
+
attributes[key]
|
119
|
+
else
|
120
|
+
default
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Compares attributes of itself with the attributes of a given other Object.
|
127
|
+
#
|
128
|
+
# This means that the given object needs to implement the same attributes and
|
129
|
+
# all it's attribute values have to be equal.
|
130
|
+
#
|
131
|
+
# @return [Boolean] wheter the attribute values are equal
|
132
|
+
#
|
133
|
+
def ==(other)
|
134
|
+
@__attr__.each_pair do |k, v|
|
135
|
+
return false if !other.respond_to?(k) || (v != other.__send__(k))
|
136
|
+
end
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
#
|
141
|
+
# Returns the value of the given attribute or the attribute at the given
|
142
|
+
# index.
|
143
|
+
#
|
144
|
+
# @overload [](name)
|
145
|
+
# @param name [Symbol] the name of the attribute
|
146
|
+
#
|
147
|
+
# @overload [](index)
|
148
|
+
# @param index [Integer] the index of the attribute
|
149
|
+
#
|
150
|
+
# @return [Object] the attribute value
|
151
|
+
#
|
152
|
+
# @raise [NameError] if the named attribute does not exist
|
153
|
+
# @raise [IndexError] if the index is out of bounds
|
154
|
+
#
|
155
|
+
def [](arg)
|
156
|
+
return @__attr__[arg] if @__attr__.key?(arg)
|
157
|
+
raise(NameError, "not member - #{arg}", caller) unless Integer === arg
|
158
|
+
if arg >= @__attr__.size || arg < -@__attr__.size
|
159
|
+
raise(IndexError, "invalid offset - #{arg}")
|
160
|
+
end
|
161
|
+
@__attr__.values[arg]
|
162
|
+
end
|
163
|
+
|
164
|
+
# @!visibility private
|
165
|
+
def deconstruct_keys(...)
|
166
|
+
@__attr__.deconstruct_keys(...)
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# Finds and returns the object in nested objects that is specified by the
|
171
|
+
# identifiers. The nested objects may be instances of various classes.
|
172
|
+
#
|
173
|
+
# @param identifiers [Array<Symbol,Integer>] one or more identifiers or
|
174
|
+
# indices
|
175
|
+
#
|
176
|
+
# @return [Object] object found
|
177
|
+
# @return [nil] if nothing was found
|
178
|
+
#
|
179
|
+
def dig(*identifiers)
|
180
|
+
(Integer === identifiers.first ? @__attr__.values : @__attr__).dig(
|
181
|
+
*identifiers
|
182
|
+
)
|
183
|
+
end
|
184
|
+
|
185
|
+
#
|
186
|
+
# @overload each(&block)
|
187
|
+
# Yields the value of each attribute in order.
|
188
|
+
#
|
189
|
+
# @yieldparam value [Object] attribute value
|
190
|
+
# @return [Invariable] itself
|
191
|
+
#
|
192
|
+
# @overload each
|
193
|
+
# Creates an Enumerator about its attribute values.
|
194
|
+
#
|
195
|
+
# @return [Enumerator]
|
196
|
+
#
|
197
|
+
def each(&block)
|
198
|
+
return to_enum(__method__) unless block
|
199
|
+
@__attr__.each_value(&block)
|
200
|
+
self
|
201
|
+
end
|
202
|
+
|
203
|
+
#
|
204
|
+
# @overload each_pair(&block)
|
205
|
+
# Yields the name and value of each attribute in order.
|
206
|
+
#
|
207
|
+
# @yieldparam name [Symbol] attribute name
|
208
|
+
# @yieldparam value [Object] attribute value
|
209
|
+
# @return [Invariable] itself
|
210
|
+
#
|
211
|
+
# @overload each
|
212
|
+
# Creates an Enumerator about its attribute name/values pairs.
|
213
|
+
#
|
214
|
+
# @return [Enumerator]
|
215
|
+
#
|
216
|
+
def each_pair(&block)
|
217
|
+
return to_enum(__method__) unless block
|
218
|
+
@__attr__.each_pair(&block)
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
#
|
223
|
+
# Compares its class and all attributes of itself with the class and
|
224
|
+
# attributes of a given other Object.
|
225
|
+
#
|
226
|
+
# @return [Boolean] wheter the classes and each attribute value are equal
|
227
|
+
#
|
228
|
+
# @see ==
|
229
|
+
#
|
230
|
+
def eql?(other)
|
231
|
+
self.class == other.class && self == other
|
232
|
+
end
|
233
|
+
|
234
|
+
# @!visibility private
|
235
|
+
def hash
|
236
|
+
(to_a << self.class).hash
|
237
|
+
end
|
238
|
+
|
239
|
+
#
|
240
|
+
# @return [String] description of itself as a string
|
241
|
+
#
|
242
|
+
def inspect
|
243
|
+
attributes = @__attr__.map { |k, v| "#{k}: #{v.inspect}" }
|
244
|
+
"<#{self.class}::#{__id__} #{attributes.join(', ')}>"
|
245
|
+
end
|
246
|
+
alias to_s inspect
|
247
|
+
|
248
|
+
#
|
249
|
+
# @return [Boolean] wheter the given name is a valid attribute name
|
250
|
+
#
|
251
|
+
def member?(name)
|
252
|
+
@__attr__.key?(name)
|
253
|
+
end
|
254
|
+
alias key? member?
|
255
|
+
|
256
|
+
#
|
257
|
+
# @attribute [r] members
|
258
|
+
# @return [Array<Symbol>] all attribute names
|
259
|
+
#
|
260
|
+
def members
|
261
|
+
@__attr__.keys
|
262
|
+
end
|
263
|
+
|
264
|
+
#
|
265
|
+
# @attribute [r] size
|
266
|
+
# @return [Integer] number of attributes
|
267
|
+
#
|
268
|
+
def size
|
269
|
+
@__attr__.size
|
270
|
+
end
|
271
|
+
|
272
|
+
#
|
273
|
+
# @return [Array<Object>] the values of all attributes
|
274
|
+
#
|
275
|
+
def to_a
|
276
|
+
@__attr__.values
|
277
|
+
end
|
278
|
+
alias values to_a
|
279
|
+
|
280
|
+
# @!visibility private
|
281
|
+
def deconstruct
|
282
|
+
@__attr__.values
|
283
|
+
end
|
284
|
+
|
285
|
+
#
|
286
|
+
# @overload to_h
|
287
|
+
# @return [Hash<Symbol,Object>] names and values of all attributes
|
288
|
+
#
|
289
|
+
# @overload to_h(compact: true)
|
290
|
+
# @return [Hash<Symbol,Object>] names and values of all attributes which
|
291
|
+
# are not `nil` and which are not empty Invariable results
|
292
|
+
#
|
293
|
+
# @overload to_h(&block)
|
294
|
+
# Returns a Hash containing the results of the block on each pair of the
|
295
|
+
# receiver as pairs.
|
296
|
+
# @yieldparam [Symbol] name the attribute name
|
297
|
+
# @yieldparam [Object] value the attribute value
|
298
|
+
# @yieldreturn [Array<Symbol,Object>] the pair to be stored in the result
|
299
|
+
#
|
300
|
+
# @return [Hash<Object,Object>] pairs returned by the `block`
|
301
|
+
#
|
302
|
+
def to_h(compact: false, &block)
|
303
|
+
return to_compact_h if compact
|
304
|
+
return Hash[@__attr__.map(&block)] if block
|
305
|
+
@__attr__.transform_values { |v| v.is_a?(Invariable) ? v.to_h : v }
|
306
|
+
end
|
307
|
+
|
308
|
+
#
|
309
|
+
# Updates all given attributes.
|
310
|
+
#
|
311
|
+
# @return [Invariable] a new updated instance of itself
|
312
|
+
def update(attributes)
|
313
|
+
opts = {}
|
314
|
+
@__attr__.each_pair do |k, v|
|
315
|
+
opts[k] = attributes.key?(k) ? attributes[k] : v
|
316
|
+
end
|
317
|
+
self.class.new(opts)
|
318
|
+
end
|
319
|
+
|
320
|
+
#
|
321
|
+
# @return [Array<Object>] Array whose elements are the atttributes of self at
|
322
|
+
# the given Integer indexes
|
323
|
+
def values_at(...)
|
324
|
+
@__attr__.values.values_at(...)
|
325
|
+
end
|
326
|
+
|
327
|
+
private
|
328
|
+
|
329
|
+
def to_compact_h
|
330
|
+
result = {}
|
331
|
+
@__attr__.each_pair do |key, value|
|
332
|
+
next if value.nil?
|
333
|
+
next result[key] = value unless value.is_a?(Invariable)
|
334
|
+
value = value.to_h(compact: true)
|
335
|
+
result[key] = value unless value.empty?
|
336
|
+
end
|
337
|
+
result
|
338
|
+
end
|
339
|
+
|
340
|
+
module InvariableClassMethods
|
341
|
+
# @!visibility private
|
342
|
+
def attributes(*names, **defaults)
|
343
|
+
@__attr__ = __attr__init unless defined?(@__attr__)
|
344
|
+
(names + defaults.keys).map do |name|
|
345
|
+
__attr__define(name, defaults[name])
|
346
|
+
end
|
347
|
+
end
|
348
|
+
alias attribute attributes
|
349
|
+
|
350
|
+
# @!visibility private
|
351
|
+
def members
|
352
|
+
@__attr__.keys
|
353
|
+
end
|
354
|
+
|
355
|
+
# @!visibility private
|
356
|
+
def member?(name)
|
357
|
+
@__attr__.key?(name)
|
358
|
+
end
|
359
|
+
|
360
|
+
private
|
361
|
+
|
362
|
+
def __attr__define(name, default)
|
363
|
+
unless name.respond_to?(:to_sym)
|
364
|
+
raise(TypeError, "invalid attribute name type - #{name}", caller(4))
|
365
|
+
end
|
366
|
+
name = name.to_sym
|
367
|
+
if method_defined?(name)
|
368
|
+
raise(NameError, "attribute already defined - #{name}", caller(4))
|
369
|
+
end
|
370
|
+
define_method(name) { @__attr__[name] }
|
371
|
+
@__attr__[name] = default.is_a?(Class) ? default : default.dup.freeze
|
372
|
+
name
|
373
|
+
end
|
374
|
+
|
375
|
+
def __attr__init
|
376
|
+
if superclass.instance_variable_defined?(:@__attr__)
|
377
|
+
Hash[superclass.instance_variable_get(:@__attr__)]
|
378
|
+
else
|
379
|
+
{}.compare_by_identity
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
383
|
+
private_constant(:InvariableClassMethods)
|
384
|
+
end
|
data/rakefile.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'bundler/gem_tasks'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
require 'yard'
|
7
|
+
|
8
|
+
$stdout.sync = $stderr.sync = true
|
9
|
+
CLOBBER << 'prj' << '.yardoc' << 'doc'
|
10
|
+
task(:default) { exec('rake --tasks') }
|
11
|
+
RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
|
12
|
+
YARD::Rake::YardocTask.new { |task| task.stats_options = %w[--list-undoc] }
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# Sample to use Invariables as complex options.
|
4
|
+
# See the options used for Net::HTTP.start to refer the sample.
|
5
|
+
#
|
6
|
+
|
7
|
+
require_relative '../lib/invariable'
|
8
|
+
|
9
|
+
#
|
10
|
+
# HTTP Options
|
11
|
+
# HTTPOptions#to_h is used to generate the options Hash provided to
|
12
|
+
# Net::HTTP.start (see there).
|
13
|
+
#
|
14
|
+
class HTTPOptions
|
15
|
+
include Invariable
|
16
|
+
|
17
|
+
attributes :open_timeout,
|
18
|
+
:read_timeout,
|
19
|
+
:write_timeout,
|
20
|
+
:continue_timeout,
|
21
|
+
:keep_alive_timeout,
|
22
|
+
:close_on_empty_response
|
23
|
+
|
24
|
+
attribute proxy: Invariable.new(:from_env, :address, :port, :user, :pass)
|
25
|
+
|
26
|
+
attribute ssl:
|
27
|
+
Invariable.new(
|
28
|
+
:ca_file,
|
29
|
+
:ca_path,
|
30
|
+
:cert,
|
31
|
+
:cert_store,
|
32
|
+
:ciphers,
|
33
|
+
:extra_chain_cert,
|
34
|
+
:key,
|
35
|
+
:timeout,
|
36
|
+
:version,
|
37
|
+
:min_version,
|
38
|
+
:max_version,
|
39
|
+
:verify_callback,
|
40
|
+
:verify_depth,
|
41
|
+
:verify_mode,
|
42
|
+
:verify_hostname
|
43
|
+
)
|
44
|
+
|
45
|
+
#
|
46
|
+
# Superseded to add some magic and to flatten the Hash provided to eg.
|
47
|
+
# Net::HTTP.start.
|
48
|
+
#
|
49
|
+
def to_h
|
50
|
+
# the compact option allows to skip all values of nil and all empty
|
51
|
+
# Invariable values
|
52
|
+
result = super(compact: true)
|
53
|
+
|
54
|
+
# flatten the SSL options:
|
55
|
+
ssl = result.delete(:ssl)
|
56
|
+
if ssl
|
57
|
+
# prefix two options:
|
58
|
+
ssl[:ssl_timeout] = ssl.delete(:timeout) if ssl.key?(:timeout)
|
59
|
+
ssl[:ssl_version] = ssl.delete(:version) if ssl.key?(:version)
|
60
|
+
result.merge!(ssl)
|
61
|
+
|
62
|
+
# automagic :)
|
63
|
+
result[:use_ssl] = true
|
64
|
+
end
|
65
|
+
|
66
|
+
# flatten the proxy options and prefix the keys
|
67
|
+
proxy = result.delete(:proxy)
|
68
|
+
if proxy
|
69
|
+
result.merge!(proxy.transform_keys! { |key| "proxy_#{key}".to_sym })
|
70
|
+
end
|
71
|
+
|
72
|
+
result
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
puts '- create a sample'
|
77
|
+
sample =
|
78
|
+
HTTPOptions.new(
|
79
|
+
open_timeout: 2,
|
80
|
+
read_timeout: 2,
|
81
|
+
write_timeout: 2,
|
82
|
+
ssl: {
|
83
|
+
timeout: 2,
|
84
|
+
min_version: :TLS1_2
|
85
|
+
},
|
86
|
+
proxy: {
|
87
|
+
from_env: true
|
88
|
+
}
|
89
|
+
)
|
90
|
+
p sample.to_h
|
data/samples/person.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# This sample shows the different aspects of Invariable.
|
4
|
+
#
|
5
|
+
|
6
|
+
require_relative '../lib/invariable'
|
7
|
+
|
8
|
+
#
|
9
|
+
# Person is a sample class which is combined from primitives as well as an
|
10
|
+
# anonymous Invariable class used for the address attribute.
|
11
|
+
#
|
12
|
+
class Person
|
13
|
+
include Invariable
|
14
|
+
attributes :name, :last_name, address: Invariable.new(:city, :zip, :street)
|
15
|
+
|
16
|
+
def full_name
|
17
|
+
"#{name} #{last_name}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
address.to_a.unshift(full_name).compact.join(', ')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
puts '- we can check the members of the class'
|
26
|
+
p Person.members #=> [:name, :last_name, :address]
|
27
|
+
p Person.member?(:last_name) #=> true
|
28
|
+
|
29
|
+
puts '- create a person record'
|
30
|
+
john = Person.new(name: 'John', last_name: 'Doe')
|
31
|
+
puts john #=> "John Doe"
|
32
|
+
|
33
|
+
puts '- we can check the members of the instance'
|
34
|
+
p john.members #=> [:name, :last_name, :address]
|
35
|
+
p john.member?(:last_name) #=> true
|
36
|
+
|
37
|
+
puts '- the address members are nil'
|
38
|
+
p john.address.city #=> nil
|
39
|
+
|
40
|
+
puts '- converted to an compact Hash the address is skipped'
|
41
|
+
p john.to_h(compact: true) #=> {:name=>"John", :last_name=>"Doe"}
|
42
|
+
|
43
|
+
puts '- update the record with an address'
|
44
|
+
john =
|
45
|
+
john.update(address: { street: '123 Main St', city: 'Anytown', zip: '45678' })
|
46
|
+
|
47
|
+
puts '- the city is assigned now'
|
48
|
+
p john.dig(:address, :city) #=> "Anytown"
|
49
|
+
puts john #=> John Doe, Anytown, 45678, 123 Main St
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/core'
|
4
|
+
require_relative '../lib/invariable'
|
5
|
+
|
6
|
+
$stdout.sync = $stderr.sync = true
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.disable_monkey_patching!
|
10
|
+
config.warnings = true
|
11
|
+
config.order = :random
|
12
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'helper'
|
3
|
+
|
4
|
+
RSpec.describe 'include Invariable' do
|
5
|
+
let(:invariable) do
|
6
|
+
Class.new do
|
7
|
+
include Invariable
|
8
|
+
attribute :name
|
9
|
+
attribute :last_name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'defines all attributes' do
|
14
|
+
expect(invariable.members).to eq %i[name last_name]
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'initializes the attributes' do
|
18
|
+
instance = invariable.new(name: 'John', last_name: 'Doe')
|
19
|
+
expect(instance.name).to eq 'John'
|
20
|
+
expect(instance.last_name).to eq 'Doe'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'initializes only given attributes' do
|
24
|
+
instance = invariable.new(last_name: 'Doe')
|
25
|
+
expect(instance.name).to be_nil
|
26
|
+
expect(instance.last_name).to eq 'Doe'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'ignores unknown attributes' do
|
30
|
+
expect {
|
31
|
+
invariable.new(foo: 42, last_name: 'Doe', ignored: true)
|
32
|
+
}.not_to raise_error
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when defining an already defined attribute' do
|
36
|
+
it 'raises an exception' do
|
37
|
+
expect do
|
38
|
+
Class.new do
|
39
|
+
include Invariable
|
40
|
+
attribute :name
|
41
|
+
attribute :name
|
42
|
+
end
|
43
|
+
end.to raise_error(NameError, 'attribute already defined - name')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when used in sub-classing' do
|
48
|
+
let(:invariable) { Class.new(base_class) { attributes :street, :city } }
|
49
|
+
let(:base_class) do
|
50
|
+
Class.new do
|
51
|
+
include Invariable
|
52
|
+
attributes :name, :last_name
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'defines all attributes' do
|
57
|
+
expect(invariable.members).to eq %i[name last_name street city]
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'initializes the attributes' do
|
61
|
+
instance =
|
62
|
+
invariable.new(
|
63
|
+
name: 'John',
|
64
|
+
last_name: 'Doe',
|
65
|
+
street: '123 Main St',
|
66
|
+
city: 'Anytown'
|
67
|
+
)
|
68
|
+
|
69
|
+
expect(instance.name).to eq 'John'
|
70
|
+
expect(instance.last_name).to eq 'Doe'
|
71
|
+
expect(instance.street).to eq '123 Main St'
|
72
|
+
expect(instance.city).to eq 'Anytown'
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'initializes only given attributes' do
|
76
|
+
instance = invariable.new(last_name: 'Doe', city: 'Anytown')
|
77
|
+
expect(instance.name).to be_nil
|
78
|
+
expect(instance.last_name).to eq 'Doe'
|
79
|
+
expect(instance.street).to be_nil
|
80
|
+
expect(instance.city).to eq 'Anytown'
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'ignores unknown attributes' do
|
84
|
+
expect {
|
85
|
+
invariable.new(foo: 42, city: 'Anytown', ignored: true)
|
86
|
+
}.not_to raise_error
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'when defining an already defined attribute of the superclass' do
|
90
|
+
it 'raises an exception' do
|
91
|
+
expect do
|
92
|
+
Class.new(base_class){ attribute :name }
|
93
|
+
end.to raise_error(NameError, 'attribute already defined - name')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'helper'
|
3
|
+
|
4
|
+
RSpec.describe 'Invariable.new' do
|
5
|
+
context 'when only attribute names are given' do
|
6
|
+
subject(:invariable) { Invariable.new(:name, :last_name) }
|
7
|
+
|
8
|
+
it 'creates a new Class' do
|
9
|
+
expect(invariable).to be_a Class
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'is inherited from Object' do
|
13
|
+
expect(invariable).to be < Object
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'defines the attributes as instance methods' do
|
17
|
+
expect(invariable).to be_public_method_defined :name
|
18
|
+
expect(invariable).to be_public_method_defined :last_name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'when a base class and attribute names are given' do
|
23
|
+
subject(:invariable) { Invariable.new(foo_class, :name, :last_name) }
|
24
|
+
let(:foo_class) { Class.new }
|
25
|
+
|
26
|
+
it 'creates a new Class' do
|
27
|
+
expect(invariable).to be_a Class
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'is inherited from the given class' do
|
31
|
+
expect(invariable).to be < foo_class
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'defines the attributes as instance methods' do
|
35
|
+
expect(invariable).to be_public_method_defined :name
|
36
|
+
expect(invariable).to be_public_method_defined :last_name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when a block is given' do
|
41
|
+
subject(:invariable) do
|
42
|
+
Invariable.new(:name, :last_name) do
|
43
|
+
def full_name
|
44
|
+
"#{name} #{last_name}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'allows to extend the new class' do
|
50
|
+
expect(invariable).to be_public_method_defined :full_name
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative 'helper'
|
3
|
+
|
4
|
+
RSpec.describe Invariable do
|
5
|
+
subject(:instance) do
|
6
|
+
sample_class.new(
|
7
|
+
name: 'John',
|
8
|
+
last_name: 'Doe',
|
9
|
+
address: {
|
10
|
+
zip: '45678',
|
11
|
+
city: 'Anytown',
|
12
|
+
street: '123 Main St'
|
13
|
+
}
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:sample_class) do
|
18
|
+
Class.new do
|
19
|
+
include Invariable
|
20
|
+
attributes :name, :last_name
|
21
|
+
attribute address: Invariable.new(:city, :zip, :street)
|
22
|
+
|
23
|
+
def full_name
|
24
|
+
"#{name} #{last_name}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'attributes' do
|
30
|
+
it 'allows to read the attributes by name' do
|
31
|
+
expect(instance.name).to eq 'John'
|
32
|
+
expect(instance.last_name).to eq 'Doe'
|
33
|
+
expect(instance.address).to be_a Invariable
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'provides information about its attributes' do
|
37
|
+
expect(instance.members).to eq %i[name last_name address]
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'can be checked whether an attribute is defined' do
|
41
|
+
expect(instance.member?(:last_name)).to be true
|
42
|
+
expect(instance.member?(:city)).to be false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context 'Hash-like behavior' do
|
47
|
+
it 'provides Hash-like attribute access' do
|
48
|
+
expect(instance[:name]).to eq 'John'
|
49
|
+
expect(instance[:last_name]).to eq 'Doe'
|
50
|
+
expect(instance[:address][:city]).to eq 'Anytown'
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when the attribute name is unknown' do
|
54
|
+
it 'raises a NameError' do
|
55
|
+
expect { instance[:size_of_shoe] }.to raise_error(
|
56
|
+
NameError,
|
57
|
+
'not member - size_of_shoe'
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'can be converted into a Hash' do
|
63
|
+
expect(instance.to_h).to eq(
|
64
|
+
name: 'John',
|
65
|
+
last_name: 'Doe',
|
66
|
+
address: {
|
67
|
+
zip: '45678',
|
68
|
+
city: 'Anytown',
|
69
|
+
street: '123 Main St'
|
70
|
+
}
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'can be converted into a customized Hash' do
|
75
|
+
converted = instance.to_h { |key, value| ["my_#{key}", value] }
|
76
|
+
expect(converted.keys).to eq %w[my_name my_last_name my_address]
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'allows to iterate all attribute name/value pairs' do
|
80
|
+
expect { |b| instance.each_pair(&b) }.to yield_successive_args(
|
81
|
+
[:name, 'John'],
|
82
|
+
[:last_name, 'Doe'],
|
83
|
+
[:address, instance.address]
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'provides an Enumerable for its attributes name/value pairs' do
|
88
|
+
expect(instance.each_pair).to be_a(Enumerable)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'can be converted to a compact Hash' do
|
92
|
+
john = sample_class.new(name: 'John')
|
93
|
+
expect(john.to_h(compact: true)).to eq(name: 'John')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'Array-like behavior' do
|
98
|
+
it 'provides its attribute count' do
|
99
|
+
expect(instance.size).to be 3
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'provides Array-like attribute access' do
|
103
|
+
expect(instance[0]).to eq 'John'
|
104
|
+
expect(instance[1]).to eq 'Doe'
|
105
|
+
expect(instance[2]).to be instance.address
|
106
|
+
expect(instance[-1]).to be instance.address
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'when the access index is out of bounds' do
|
110
|
+
it 'raises a NameError' do
|
111
|
+
expect { instance[3] }.to raise_error(IndexError, 'invalid offset - 3')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'can be converted into an Array' do
|
116
|
+
expect(instance.to_a).to eq ['John', 'Doe', instance.address]
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'allows to iterate all attribute values' do
|
120
|
+
expect { |b| instance.each(&b) }.to yield_successive_args(
|
121
|
+
'John',
|
122
|
+
'Doe',
|
123
|
+
instance.address
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'provides an Enumerable for its attribute values' do
|
128
|
+
expect(instance.each).to be_a(Enumerable)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'comparing' do
|
133
|
+
it 'can be compared to other objects' do
|
134
|
+
other =
|
135
|
+
sample_class.new(
|
136
|
+
name: 'John',
|
137
|
+
last_name: 'Doe',
|
138
|
+
address: {
|
139
|
+
zip: '45678',
|
140
|
+
city: 'Anytown',
|
141
|
+
street: '123 Main St'
|
142
|
+
}
|
143
|
+
)
|
144
|
+
expect(instance == other).to be true
|
145
|
+
|
146
|
+
other =
|
147
|
+
sample_class.new(
|
148
|
+
name: 'John',
|
149
|
+
last_name: 'Doe',
|
150
|
+
address: {
|
151
|
+
zip: '45678',
|
152
|
+
city: 'Anytown',
|
153
|
+
street: '124 Main St' # difffers
|
154
|
+
}
|
155
|
+
)
|
156
|
+
expect(instance == other).to be false
|
157
|
+
|
158
|
+
other =
|
159
|
+
double(
|
160
|
+
:other,
|
161
|
+
name: 'John',
|
162
|
+
last_name: 'Doe',
|
163
|
+
address:
|
164
|
+
double(
|
165
|
+
:other_addr,
|
166
|
+
zip: '45678',
|
167
|
+
city: 'Anytown',
|
168
|
+
street: '123 Main St'
|
169
|
+
)
|
170
|
+
)
|
171
|
+
expect(instance == other).to be true
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'can be tested for equality' do
|
175
|
+
other =
|
176
|
+
sample_class.new(
|
177
|
+
name: 'John',
|
178
|
+
last_name: 'Doe',
|
179
|
+
address: {
|
180
|
+
zip: '45678',
|
181
|
+
city: 'Anytown',
|
182
|
+
street: '123 Main St'
|
183
|
+
}
|
184
|
+
)
|
185
|
+
expect(instance.eql?(other)).to be true
|
186
|
+
|
187
|
+
other =
|
188
|
+
sample_class.new(
|
189
|
+
name: 'John',
|
190
|
+
last_name: 'Doe',
|
191
|
+
address: {
|
192
|
+
zip: '45679', # differs
|
193
|
+
city: 'Anytown',
|
194
|
+
street: '123 Main St'
|
195
|
+
}
|
196
|
+
)
|
197
|
+
expect(instance.eql?(other)).to be false
|
198
|
+
|
199
|
+
other = # class differs
|
200
|
+
double(
|
201
|
+
:other,
|
202
|
+
name: 'John',
|
203
|
+
last_name: 'Doe',
|
204
|
+
address: {
|
205
|
+
zip: '45678',
|
206
|
+
city: 'Anytown',
|
207
|
+
street: '123 Main St'
|
208
|
+
}
|
209
|
+
)
|
210
|
+
expect(instance.eql?(other)).to be false
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
context '#dig pattern' do
|
215
|
+
let(:data) { { person: instance } }
|
216
|
+
|
217
|
+
it 'can be used with attribute names' do
|
218
|
+
expect(data.dig(:person, :last_name)).to eq 'Doe'
|
219
|
+
expect(data.dig(:person, :zip)).to be_nil
|
220
|
+
expect(data.dig(:person, :address, :city)).to eq 'Anytown'
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'can be used with indices' do
|
224
|
+
expect(data.dig(:person, 1)).to eq 'Doe'
|
225
|
+
expect(data.dig(:person, -1, :zip)).to eq '45678'
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
context 'pattern matching' do
|
230
|
+
it 'can be used for named pattern matching' do
|
231
|
+
result =
|
232
|
+
case instance
|
233
|
+
in name: 'Fred', last_name: 'Doe'
|
234
|
+
:fred
|
235
|
+
in name: 'John', last_name: 'New'
|
236
|
+
:not_john
|
237
|
+
in name: 'John', last_name: 'Doe', address: { city: 'NY' }
|
238
|
+
:john_from_ny
|
239
|
+
in name: 'John', last_name: 'Doe', address: { city: 'Anytown' }
|
240
|
+
:john
|
241
|
+
else
|
242
|
+
nil
|
243
|
+
end
|
244
|
+
|
245
|
+
expect(result).to be :john
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'can be used for indexed pattern matching' do
|
249
|
+
result =
|
250
|
+
case instance
|
251
|
+
in 'Fred', 'Doe', *_
|
252
|
+
:fred
|
253
|
+
in 'John', 'New', *_
|
254
|
+
:not_john
|
255
|
+
in 'John', 'Doe', *_
|
256
|
+
:john
|
257
|
+
else
|
258
|
+
nil
|
259
|
+
end
|
260
|
+
|
261
|
+
expect(result).to be :john
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'allows to create an updated version of itself' do
|
266
|
+
result =
|
267
|
+
instance.update(
|
268
|
+
name: 'Fred',
|
269
|
+
address: {
|
270
|
+
zip: '45678',
|
271
|
+
city: 'Anytown',
|
272
|
+
street: '124 Main St'
|
273
|
+
}
|
274
|
+
)
|
275
|
+
expect(result).to be_a sample_class
|
276
|
+
expect(result.name).to eq 'Fred'
|
277
|
+
expect(result.last_name).to eq 'Doe'
|
278
|
+
expect(result.address.to_h).to eq(
|
279
|
+
zip: '45678',
|
280
|
+
city: 'Anytown',
|
281
|
+
street: '124 Main St'
|
282
|
+
)
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'can be inspected' do
|
286
|
+
expect(instance.inspect).to include(' name: "John", last_name: "Doe"')
|
287
|
+
expect(instance.inspect).to include(
|
288
|
+
'city: "Anytown", zip: "45678", street: "123 Main St"'
|
289
|
+
)
|
290
|
+
end
|
291
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: invariable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike Blumtritt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-12-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
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'
|
69
|
+
description: |
|
70
|
+
An Invariable bundles a number of read-only attributes.
|
71
|
+
It can be used like a Hash as well as an Array.
|
72
|
+
It supports subclassing and pattern matching.
|
73
|
+
email:
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files:
|
77
|
+
- README.md
|
78
|
+
- LICENSE
|
79
|
+
files:
|
80
|
+
- ".gitignore"
|
81
|
+
- ".yardopts"
|
82
|
+
- LICENSE
|
83
|
+
- README.md
|
84
|
+
- gems.rb
|
85
|
+
- invariable.gemspec
|
86
|
+
- lib/invariable.rb
|
87
|
+
- lib/invariable/version.rb
|
88
|
+
- rakefile.rb
|
89
|
+
- samples/http_options.rb
|
90
|
+
- samples/person.rb
|
91
|
+
- spec/helper.rb
|
92
|
+
- spec/invariable_include_spec.rb
|
93
|
+
- spec/invariable_new_spec.rb
|
94
|
+
- spec/invariable_spec.rb
|
95
|
+
homepage: https://github.com/mblumtritt/invariable
|
96
|
+
licenses:
|
97
|
+
- BSD-3-Clause
|
98
|
+
metadata:
|
99
|
+
source_code_uri: https://github.com/mblumtritt/invariable
|
100
|
+
bug_tracker_uri: https://github.com/mblumtritt/invariable/issues
|
101
|
+
documentation_uri: https://rubydoc.info/github/mblumtritt/invariable
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.7.0
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubygems_version: 3.3.3
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: The Invariable data class for Ruby.
|
121
|
+
test_files:
|
122
|
+
- spec/helper.rb
|
123
|
+
- spec/invariable_include_spec.rb
|
124
|
+
- spec/invariable_new_spec.rb
|
125
|
+
- spec/invariable_spec.rb
|