smart_properties 1.13.0 → 1.16.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 +5 -5
- data/.github/workflows/testing.yml +24 -0
- data/.gitignore +2 -1
- data/.ruby-version +1 -0
- data/README.md +27 -1
- data/dev.yml +7 -0
- data/experiments/initialization_performance.rb +34 -0
- data/lib/smart_properties.rb +21 -0
- data/lib/smart_properties/property.rb +53 -30
- data/lib/smart_properties/property_collection.rb +9 -7
- data/lib/smart_properties/validations.rb +8 -0
- data/lib/smart_properties/validations/ancestor.rb +27 -0
- data/lib/smart_properties/version.rb +1 -1
- data/smart_properties.gemspec +5 -1
- data/spec/base_spec.rb +38 -1
- data/spec/configuration_error_spec.rb +33 -0
- data/spec/default_values_spec.rb +68 -0
- data/spec/inheritance_spec.rb +30 -0
- data/spec/property_collection_caching_spec.rb +10 -0
- data/spec/validations/ancestor_validation_spec.rb +62 -0
- data/spec/writable_spec.rb +39 -0
- metadata +20 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 76b840210991bfec855a2e1159426635bc29e3994269bf305b5f0a8fe40fb0cf
|
4
|
+
data.tar.gz: f10d37b213920338b36b8a2da7bbecc1e8e0caf1d6eecf66084b6cbb4914a95e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 30f8ed85cd3db3e2c17ddc30b63f85cba5541d26d8c8747a92529320531c11a58ae20435f08175061faedc7edf8f3bad2899ff32f2aa36465d4de2a78c8ca75e
|
7
|
+
data.tar.gz: fb602a52c408989d44c9ef0dd232f5891c6bc96f64444d077d498bb06ed1f63dfa671712abfe9ab0680214fb442ee4b029b6e0e518df01a35860e5fc6c072151
|
@@ -0,0 +1,24 @@
|
|
1
|
+
name: Testing
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
workflow_dispatch:
|
9
|
+
|
10
|
+
jobs:
|
11
|
+
test:
|
12
|
+
|
13
|
+
runs-on: ubuntu-latest
|
14
|
+
|
15
|
+
steps:
|
16
|
+
- uses: actions/checkout@v2
|
17
|
+
- name: Set up Ruby
|
18
|
+
uses: ruby/setup-ruby@v1
|
19
|
+
with:
|
20
|
+
ruby-version: 2.6
|
21
|
+
- name: Install dependencies
|
22
|
+
run: bundle install
|
23
|
+
- name: Run tests
|
24
|
+
run: bundle exec rake
|
data/.gitignore
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.6.6
|
data/README.md
CHANGED
@@ -171,6 +171,16 @@ class Article
|
|
171
171
|
end
|
172
172
|
```
|
173
173
|
|
174
|
+
There are also a set of common validation helpers you may use. These common
|
175
|
+
cases are provided to help avoid rewriting validation logic that occurs
|
176
|
+
often. These validations can be found in the `SmartProperties::Validations` module.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class Article
|
180
|
+
property :view_count, accepts: Ancestor.must_be(type: Number)
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
174
184
|
#### Default values
|
175
185
|
|
176
186
|
There is also support for default values. Simply use the `:default`
|
@@ -220,7 +230,7 @@ class Person
|
|
220
230
|
end
|
221
231
|
```
|
222
232
|
|
223
|
-
#### Custom
|
233
|
+
#### Custom reader naming
|
224
234
|
|
225
235
|
In Ruby, predicate methods by convention end with a `?`.
|
226
236
|
This convention is violated in the example above, but can easily be fixed by supplying a custom `reader` name:
|
@@ -235,6 +245,22 @@ end
|
|
235
245
|
To ensure backwards compatibility, boolean properties do not automatically change their reader name.
|
236
246
|
It is thus your responsibility to configure the property properly.
|
237
247
|
|
248
|
+
#### Custom reader implementation
|
249
|
+
|
250
|
+
For convenience, it is possible to use the `super` method to access the original reader when overriding a reader.
|
251
|
+
This is recommended over direct access to the instance variable.
|
252
|
+
|
253
|
+
```ruby
|
254
|
+
class Person
|
255
|
+
property :name
|
256
|
+
property! :address
|
257
|
+
|
258
|
+
def name
|
259
|
+
super || address.name
|
260
|
+
end
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
238
264
|
### Constructor argument forwarding
|
239
265
|
|
240
266
|
The `SmartProperties` initializer forwards anything to the super constructor
|
data/dev.yml
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require_relative '../lib/smart_properties'
|
3
|
+
|
4
|
+
class A
|
5
|
+
include SmartProperties
|
6
|
+
property :a
|
7
|
+
end
|
8
|
+
|
9
|
+
class B < A
|
10
|
+
property :b
|
11
|
+
end
|
12
|
+
|
13
|
+
class C < B
|
14
|
+
property :c
|
15
|
+
end
|
16
|
+
|
17
|
+
class A2
|
18
|
+
def initialize(**attrs)
|
19
|
+
attrs.each { |k, v| send("#{k}=", v) }
|
20
|
+
end
|
21
|
+
attr_accessor :a
|
22
|
+
end
|
23
|
+
|
24
|
+
class B2 < A2
|
25
|
+
attr_accessor :b
|
26
|
+
end
|
27
|
+
|
28
|
+
class C2 < B2
|
29
|
+
attr_accessor :c
|
30
|
+
end
|
31
|
+
|
32
|
+
puts Benchmark.measure { 1_000_000.times { C.new(a: 1, b: 2, c: 3) } }
|
33
|
+
# puts Benchmark.measure { 1_000_000.times { C2.new(a: 1, b: 2, c: 3) } }
|
34
|
+
|
data/lib/smart_properties.rb
CHANGED
@@ -91,6 +91,13 @@ module SmartProperties
|
|
91
91
|
protected :property!
|
92
92
|
end
|
93
93
|
|
94
|
+
module ModuleMethods
|
95
|
+
def included(target)
|
96
|
+
super
|
97
|
+
target.include(SmartProperties)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
94
101
|
class << self
|
95
102
|
private
|
96
103
|
|
@@ -102,6 +109,7 @@ module SmartProperties
|
|
102
109
|
#
|
103
110
|
def included(base)
|
104
111
|
base.extend(ClassMethods)
|
112
|
+
base.extend(ModuleMethods) if base.is_a?(Module)
|
105
113
|
end
|
106
114
|
end
|
107
115
|
|
@@ -153,9 +161,22 @@ module SmartProperties
|
|
153
161
|
|
154
162
|
raise SmartProperties::InitializationError.new(self, missing_properties) unless missing_properties.empty?
|
155
163
|
end
|
164
|
+
|
165
|
+
def [](name)
|
166
|
+
return if name.nil?
|
167
|
+
name = name.to_sym
|
168
|
+
reader = self.class.properties[name].reader
|
169
|
+
public_send(reader) if self.class.properties.key?(name)
|
170
|
+
end
|
171
|
+
|
172
|
+
def []=(name, value)
|
173
|
+
return if name.nil?
|
174
|
+
public_send(:"#{name.to_sym}=", value) if self.class.properties.key?(name)
|
175
|
+
end
|
156
176
|
end
|
157
177
|
|
158
178
|
require_relative 'smart_properties/property_collection'
|
159
179
|
require_relative 'smart_properties/property'
|
160
180
|
require_relative 'smart_properties/errors'
|
161
181
|
require_relative 'smart_properties/version'
|
182
|
+
require_relative 'smart_properties/validations'
|
@@ -1,28 +1,14 @@
|
|
1
1
|
module SmartProperties
|
2
2
|
class Property
|
3
3
|
MODULE_REFERENCE = :"@_smart_properties_method_scope"
|
4
|
-
|
5
|
-
# Defines the two index methods #[] and #[]=. This module will be included
|
6
|
-
# in the SmartProperties method scope.
|
7
|
-
module IndexMethods
|
8
|
-
def [](name)
|
9
|
-
return if name.nil?
|
10
|
-
name = name.to_sym
|
11
|
-
reader = self.class.properties[name].reader
|
12
|
-
public_send(reader) if self.class.properties.key?(name)
|
13
|
-
end
|
14
|
-
|
15
|
-
def []=(name, value)
|
16
|
-
return if name.nil?
|
17
|
-
public_send(:"#{name.to_sym}=", value) if self.class.properties.key?(name)
|
18
|
-
end
|
19
|
-
end
|
4
|
+
ALLOWED_DEFAULT_CLASSES = [Proc, Numeric, String, Range, TrueClass, FalseClass, NilClass, Symbol, Module].freeze
|
20
5
|
|
21
6
|
attr_reader :name
|
22
7
|
attr_reader :converter
|
23
8
|
attr_reader :accepter
|
24
9
|
attr_reader :reader
|
25
10
|
attr_reader :instance_variable_name
|
11
|
+
attr_reader :writable
|
26
12
|
|
27
13
|
def self.define(scope, name, options = {})
|
28
14
|
new(name, options).tap { |p| p.define(scope) }
|
@@ -37,10 +23,16 @@ module SmartProperties
|
|
37
23
|
@accepter = attrs.delete(:accepts)
|
38
24
|
@required = attrs.delete(:required)
|
39
25
|
@reader = attrs.delete(:reader)
|
26
|
+
@writable = attrs.delete(:writable)
|
40
27
|
@reader ||= @name
|
41
28
|
|
42
29
|
@instance_variable_name = :"@#{name}"
|
43
30
|
|
31
|
+
unless ALLOWED_DEFAULT_CLASSES.any? { |cls| @default.is_a?(cls) }
|
32
|
+
raise ConfigurationError, "Default attribute value #{@default.inspect} cannot be specified as literal, "\
|
33
|
+
"use the syntax `default: -> { ... }` instead."
|
34
|
+
end
|
35
|
+
|
44
36
|
unless attrs.empty?
|
45
37
|
raise ConfigurationError, "SmartProperties do not support the following configuration options: #{attrs.keys.map { |m| m.to_s }.sort.join(', ')}."
|
46
38
|
end
|
@@ -62,14 +54,25 @@ module SmartProperties
|
|
62
54
|
!null_object?(get(scope))
|
63
55
|
end
|
64
56
|
|
57
|
+
def writable?
|
58
|
+
return true if @writable.nil?
|
59
|
+
@writable
|
60
|
+
end
|
61
|
+
|
65
62
|
def convert(scope, value)
|
66
63
|
return value unless converter
|
67
64
|
return value if null_object?(value)
|
68
|
-
|
65
|
+
|
66
|
+
case converter
|
67
|
+
when Symbol
|
68
|
+
converter.to_proc.call(value)
|
69
|
+
else
|
70
|
+
scope.instance_exec(value, &converter)
|
71
|
+
end
|
69
72
|
end
|
70
73
|
|
71
74
|
def default(scope)
|
72
|
-
@default.kind_of?(Proc) ? scope.instance_exec(&@default) : @default
|
75
|
+
@default.kind_of?(Proc) ? scope.instance_exec(&@default) : @default.dup
|
73
76
|
end
|
74
77
|
|
75
78
|
def accepts?(value, scope)
|
@@ -99,7 +102,7 @@ module SmartProperties
|
|
99
102
|
if klass.instance_variable_defined?(MODULE_REFERENCE)
|
100
103
|
klass.instance_variable_get(MODULE_REFERENCE)
|
101
104
|
else
|
102
|
-
m = Module.new
|
105
|
+
m = Module.new
|
103
106
|
klass.send(:include, m)
|
104
107
|
klass.instance_variable_set(MODULE_REFERENCE, m)
|
105
108
|
m
|
@@ -108,8 +111,11 @@ module SmartProperties
|
|
108
111
|
scope.send(:define_method, reader) do
|
109
112
|
property.get(self)
|
110
113
|
end
|
111
|
-
|
112
|
-
|
114
|
+
|
115
|
+
if writable?
|
116
|
+
scope.send(:define_method, :"#{name}=") do |value|
|
117
|
+
property.set(self, value)
|
118
|
+
end
|
113
119
|
end
|
114
120
|
end
|
115
121
|
|
@@ -132,21 +138,38 @@ module SmartProperties
|
|
132
138
|
scope.instance_variable_get(instance_variable_name)
|
133
139
|
end
|
134
140
|
|
141
|
+
def to_h
|
142
|
+
{
|
143
|
+
accepter: @accepter,
|
144
|
+
converter: @converter,
|
145
|
+
default: @default,
|
146
|
+
instance_variable_name: @instance_variable_name,
|
147
|
+
name: @name,
|
148
|
+
reader: @reader,
|
149
|
+
required: @required
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
135
153
|
private
|
136
154
|
|
137
155
|
def null_object?(object)
|
138
|
-
|
139
|
-
return true if object.nil?
|
140
|
-
false
|
156
|
+
object.nil?
|
141
157
|
rescue NoMethodError => error
|
142
158
|
# BasicObject does not respond to #nil? by default, so we need to double
|
143
159
|
# check if somebody implemented it and it fails internally or if the
|
144
|
-
# error occured because the method is actually not present.
|
145
|
-
|
146
|
-
#
|
147
|
-
|
148
|
-
|
149
|
-
|
160
|
+
# error occured because the method is actually not present.
|
161
|
+
|
162
|
+
# This is a workaround for the fact that #singleton_class is defined on Object, but not BasicObject.
|
163
|
+
the_singleton_class = (class << object; self; end)
|
164
|
+
|
165
|
+
if the_singleton_class.public_instance_methods.include?(:nil?)
|
166
|
+
# object defines #nil?, but it raised NoMethodError,
|
167
|
+
# something is wrong with the implementation, so raise the exception.
|
168
|
+
raise error
|
169
|
+
else
|
170
|
+
# treat the object as truthy because we don't know better.
|
171
|
+
false
|
172
|
+
end
|
150
173
|
end
|
151
174
|
end
|
152
175
|
end
|
@@ -5,16 +5,18 @@ module SmartProperties
|
|
5
5
|
attr_reader :parent
|
6
6
|
|
7
7
|
def self.for(scope)
|
8
|
-
|
9
|
-
ancestor.ancestors.include?(SmartProperties) &&
|
8
|
+
parents = scope.ancestors[1..-1].select do |ancestor|
|
9
|
+
ancestor.ancestors.include?(SmartProperties) &&
|
10
|
+
ancestor != scope &&
|
11
|
+
ancestor != SmartProperties
|
10
12
|
end
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
parent.properties.register(collection = new)
|
16
|
-
collection
|
14
|
+
parents.reduce(collection = new) do |previous, current|
|
15
|
+
current.properties.register(previous)
|
16
|
+
current.properties
|
17
17
|
end
|
18
|
+
|
19
|
+
collection
|
18
20
|
end
|
19
21
|
|
20
22
|
def initialize
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module SmartProperties
|
3
|
+
module Validations
|
4
|
+
class Ancestor
|
5
|
+
include SmartProperties
|
6
|
+
|
7
|
+
property! :type, accepts: ->(type) { type.is_a?(Class) }
|
8
|
+
|
9
|
+
def validate(klass)
|
10
|
+
klass.is_a?(Class) && klass < type
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_proc
|
14
|
+
validator = self
|
15
|
+
->(klass) { validator.validate(klass) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"subclasses of #{type.to_s}"
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
alias_method :must_be, :new
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/smart_properties.gemspec
CHANGED
@@ -12,6 +12,10 @@ Gem::Specification.new do |gem|
|
|
12
12
|
gem.summary = %q{SmartProperties – Ruby accessors on steroids}
|
13
13
|
gem.homepage = ""
|
14
14
|
|
15
|
+
gem.metadata = {
|
16
|
+
"source_code_uri" => "https://github.com/t6d/smart_properties"
|
17
|
+
}
|
18
|
+
|
15
19
|
gem.files = `git ls-files`.split($\)
|
16
20
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
21
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
@@ -20,6 +24,6 @@ Gem::Specification.new do |gem|
|
|
20
24
|
gem.version = SmartProperties::VERSION
|
21
25
|
|
22
26
|
gem.add_development_dependency "rspec", "~> 3.0"
|
23
|
-
gem.add_development_dependency "rake", "~>
|
27
|
+
gem.add_development_dependency "rake", "~> 13.0"
|
24
28
|
gem.add_development_dependency "pry"
|
25
29
|
end
|
data/spec/base_spec.rb
CHANGED
@@ -41,12 +41,19 @@ RSpec.describe SmartProperties do
|
|
41
41
|
default_title = double(to_title: 'chunky')
|
42
42
|
|
43
43
|
DummyClass.new do
|
44
|
-
property :title, converts: :to_title, accepts: String, required: true, default: default_title
|
44
|
+
property :title, converts: :to_title, accepts: String, required: true, default: -> { default_title }
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
48
|
it { is_expected.to have_smart_property(:title) }
|
49
49
|
|
50
|
+
it "should return a property's configuration with #to_h" do
|
51
|
+
expect(klass.properties.values.first.to_h).to match(
|
52
|
+
accepter: String, converter: :to_title, default: an_instance_of(Proc),
|
53
|
+
instance_variable_name: :@title, name: :title, reader: :title, required: true
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
50
57
|
context "an instance of this class when initialized with no arguments" do
|
51
58
|
subject(:instance) { klass.new }
|
52
59
|
|
@@ -157,4 +164,34 @@ RSpec.describe SmartProperties do
|
|
157
164
|
end
|
158
165
|
end
|
159
166
|
end
|
167
|
+
|
168
|
+
context "when used to build a class that has a property called relation for an arbitrary Relation class" do
|
169
|
+
class Relation
|
170
|
+
attr_reader :equality_tested
|
171
|
+
|
172
|
+
def initialize
|
173
|
+
@equality_tested = false
|
174
|
+
end
|
175
|
+
|
176
|
+
def ==(_)
|
177
|
+
@equality_tested = true
|
178
|
+
false
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
subject(:klass) do
|
183
|
+
DummyClass.new do
|
184
|
+
property :relation, accepts: Relation, required: true
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
context 'with an instance of Relation' do
|
189
|
+
let(:relation) { Relation.new }
|
190
|
+
|
191
|
+
it 'should not execute #== on the object' do
|
192
|
+
instance = klass.new(relation: relation)
|
193
|
+
expect(relation.equality_tested).to eq(false)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
160
197
|
end
|
@@ -13,5 +13,38 @@ RSpec.describe SmartProperties, 'configuration error' do
|
|
13
13
|
|
14
14
|
expect(&invalid_property_definition).to raise_error(SmartProperties::ConfigurationError, "SmartProperties do not support the following configuration options: invalid_option_1, invalid_option_2, invalid_option_3.")
|
15
15
|
end
|
16
|
+
|
17
|
+
it "should accept default values that can't be mutated" do
|
18
|
+
valid_property_definition = lambda do
|
19
|
+
klass.class_eval do
|
20
|
+
property :proc, default: -> { }
|
21
|
+
property :numeric_float, default: 1.23
|
22
|
+
property :numeric_int, default: 456
|
23
|
+
property :string, default: "abc"
|
24
|
+
property :range, default: 123...456
|
25
|
+
property :bool_true, default: true
|
26
|
+
property :bool_false, default: false
|
27
|
+
property :nil, default: nil
|
28
|
+
property :symbol, default: :abc
|
29
|
+
property :module, default: Integer
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
expect(&valid_property_definition).not_to raise_error
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should not accept default values that may be mutated" do
|
37
|
+
invalid_property_definition = lambda do
|
38
|
+
klass.class_eval do
|
39
|
+
property :title, default: []
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
expect(&invalid_property_definition).to(
|
44
|
+
raise_error(SmartProperties::ConfigurationError,
|
45
|
+
"Default attribute value [] cannot be specified as literal, "\
|
46
|
+
"use the syntax `default: -> { ... }` instead.")
|
47
|
+
)
|
48
|
+
end
|
16
49
|
end
|
17
50
|
end
|
data/spec/default_values_spec.rb
CHANGED
@@ -48,4 +48,72 @@ RSpec.describe SmartProperties, 'default values' do
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|
51
|
+
|
52
|
+
context "when defining a new property with a literal default value" do
|
53
|
+
context 'with a numeric default' do
|
54
|
+
subject(:klass) { DummyClass.new { property :var, default: 123 } }
|
55
|
+
|
56
|
+
it 'accepts the default and returns it' do
|
57
|
+
instance = klass.new
|
58
|
+
expect(instance.var).to(be(123))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'with a string default' do
|
63
|
+
DEFAULT_VALUE = 'a string'
|
64
|
+
subject(:klass) { DummyClass.new { property :var, default: DEFAULT_VALUE } }
|
65
|
+
|
66
|
+
it 'accepts the default and returns it' do
|
67
|
+
instance = klass.new
|
68
|
+
expect(instance.var).to(eq(DEFAULT_VALUE))
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'returns a copy of the string' do
|
72
|
+
instance = klass.new
|
73
|
+
expect(instance.var).to_not(be(DEFAULT_VALUE))
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'mutating the instance variable does not mutate the original' do
|
77
|
+
instance = klass.new
|
78
|
+
instance.var[0] = 'o'
|
79
|
+
expect(DEFAULT_VALUE).to(eq('a string'))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'with a range default' do
|
84
|
+
subject(:klass) { DummyClass.new { property :var, default: 1..2 } }
|
85
|
+
|
86
|
+
it 'accepts the default and returns it' do
|
87
|
+
instance = klass.new
|
88
|
+
expect(instance.var).to(eq(1..2))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'with a true default' do
|
93
|
+
subject(:klass) { DummyClass.new { property :var, default: true } }
|
94
|
+
|
95
|
+
it 'accepts the default and returns it' do
|
96
|
+
instance = klass.new
|
97
|
+
expect(instance.var).to(be(true))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'with a false default' do
|
102
|
+
subject(:klass) { DummyClass.new { property :var, default: false } }
|
103
|
+
|
104
|
+
it 'accepts the default and returns it' do
|
105
|
+
instance = klass.new
|
106
|
+
expect(instance.var).to(be(false))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'with a symbol default' do
|
111
|
+
subject(:klass) { DummyClass.new { property :var, default: :foo } }
|
112
|
+
|
113
|
+
it 'accepts the default and returns it' do
|
114
|
+
instance = klass.new
|
115
|
+
expect(instance.var).to(be(:foo))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
51
119
|
end
|
data/spec/inheritance_spec.rb
CHANGED
@@ -225,4 +225,34 @@ RSpec.describe SmartProperties, 'intheritance' do
|
|
225
225
|
end
|
226
226
|
end
|
227
227
|
end
|
228
|
+
|
229
|
+
it "supports multiple inheritance through modules" do
|
230
|
+
m = Module.new do
|
231
|
+
include SmartProperties
|
232
|
+
property :m, default: 1
|
233
|
+
end
|
234
|
+
|
235
|
+
n = Module.new do
|
236
|
+
include SmartProperties
|
237
|
+
property :n, default: 2
|
238
|
+
end
|
239
|
+
|
240
|
+
o = Module.new {}
|
241
|
+
|
242
|
+
klass = Class.new do
|
243
|
+
include m
|
244
|
+
include o
|
245
|
+
include n
|
246
|
+
end
|
247
|
+
|
248
|
+
n.module_eval do
|
249
|
+
property :p, default: 3
|
250
|
+
end
|
251
|
+
|
252
|
+
instance = klass.new
|
253
|
+
|
254
|
+
expect(instance.m).to eq(1)
|
255
|
+
expect(instance.n).to eq(2)
|
256
|
+
expect(instance.p).to eq(3)
|
257
|
+
end
|
228
258
|
end
|
@@ -26,4 +26,14 @@ RSpec.describe SmartProperties, "property collection caching:" do
|
|
26
26
|
expect(subsubclass.properties.keys - expected_names).to be_empty
|
27
27
|
expect(subsubclass.properties.to_hash.keys - expected_names).to be_empty
|
28
28
|
end
|
29
|
+
|
30
|
+
specify "a SmartProperty enabled object should not check itself for properties if prepended" do
|
31
|
+
expect do
|
32
|
+
base_class = DummyClass.new {
|
33
|
+
prepend Module.new
|
34
|
+
property :title
|
35
|
+
}
|
36
|
+
expect(base_class.new).to have_smart_property(:title)
|
37
|
+
end.not_to raise_error(SystemStackError)
|
38
|
+
end
|
29
39
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'smart_properties/validations/ancestor'
|
4
|
+
|
5
|
+
RSpec.describe SmartProperties::Validations::Ancestor, 'validates ancestor' do
|
6
|
+
context 'used to validate the ancestor of a smart_properties value' do
|
7
|
+
let!(:test_base) do
|
8
|
+
Class.new do
|
9
|
+
def self.to_s
|
10
|
+
'TestBase'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
subject(:klass) do
|
16
|
+
test_base_ref = test_base
|
17
|
+
|
18
|
+
DummyClass.new do
|
19
|
+
property :visible, accepts: SmartProperties::Validations::Ancestor.must_be(type: test_base_ref)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should return an error for any non Class based value' do
|
24
|
+
expect { subject.new(visible: true) }
|
25
|
+
.to raise_error(SmartProperties::InvalidValueError, /Only accepts: subclasses of TestBase/)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should return an error for any Class instance instead of a class type' do
|
29
|
+
non_ancestor_class = Class.new
|
30
|
+
|
31
|
+
expect { subject.new(visible: non_ancestor_class.new) }
|
32
|
+
.to raise_error(SmartProperties::InvalidValueError, /Only accepts: subclasses of TestBase/)
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
it 'should return an error for any Class instance instead of a class type even if it is a child' do
|
37
|
+
test_base_child = Class.new(test_base)
|
38
|
+
|
39
|
+
expect { subject.new(visible: test_base_child.new) }
|
40
|
+
.to raise_error(SmartProperties::InvalidValueError, /Only accepts: subclasses of TestBase/)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should return an error for a Class type that is not a child of the required ancestor' do
|
44
|
+
non_ancestor_class = Class.new
|
45
|
+
|
46
|
+
expect { subject.new(visible: non_ancestor_class) }
|
47
|
+
.to raise_error(SmartProperties::InvalidValueError, /Only accepts: subclasses of TestBase/)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'should return an error if the class is the ancestor itself' do
|
51
|
+
expect { subject.new(visible: test_base) }
|
52
|
+
.to raise_error(SmartProperties::InvalidValueError, /Only accepts: subclasses of TestBase/)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should succeed if the given class is a subtype ' do
|
56
|
+
test_valid_class = Class.new(test_base)
|
57
|
+
|
58
|
+
expect { subject.new(visible: test_valid_class) }
|
59
|
+
.not_to raise_error
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe SmartProperties, 'writable properties' do
|
6
|
+
context 'when a property is defined as not writable there should be no accessor for the property' do
|
7
|
+
subject(:klass) { DummyClass.new { property :id, writable: false } }
|
8
|
+
|
9
|
+
it "should throw a no method error when trying to set the property" do
|
10
|
+
new_class_instance = klass.new(id: 42)
|
11
|
+
|
12
|
+
expect(new_class_instance.id).to eq(42)
|
13
|
+
expect { new_class_instance.id = 50 }.to raise_error(NoMethodError)
|
14
|
+
expect(new_class_instance.id).to eq(42)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when a property is defined as writable there should be an accessor available' do
|
19
|
+
subject(:klass) { DummyClass.new { property :id, writable: true } }
|
20
|
+
|
21
|
+
it "should allow changing of the property" do
|
22
|
+
new_class_instance = klass.new(id: 42)
|
23
|
+
|
24
|
+
new_class_instance.id = 50
|
25
|
+
expect(new_class_instance.id).to eq(50)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when writable is not defined on the property it should default to being writable' do
|
30
|
+
subject(:klass) { DummyClass.new { property :id } }
|
31
|
+
|
32
|
+
it "should allow changing of the property" do
|
33
|
+
new_class_instance = klass.new(id: 42)
|
34
|
+
|
35
|
+
new_class_instance.id = 50
|
36
|
+
expect(new_class_instance.id).to eq(50)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_properties
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.16.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Konstantin Tennhard
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-08-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '13.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '13.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: pry
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -62,16 +62,22 @@ executables: []
|
|
62
62
|
extensions: []
|
63
63
|
extra_rdoc_files: []
|
64
64
|
files:
|
65
|
+
- ".github/workflows/testing.yml"
|
65
66
|
- ".gitignore"
|
67
|
+
- ".ruby-version"
|
66
68
|
- ".yardopts"
|
67
69
|
- Gemfile
|
68
70
|
- LICENSE
|
69
71
|
- README.md
|
70
72
|
- Rakefile
|
73
|
+
- dev.yml
|
74
|
+
- experiments/initialization_performance.rb
|
71
75
|
- lib/smart_properties.rb
|
72
76
|
- lib/smart_properties/errors.rb
|
73
77
|
- lib/smart_properties/property.rb
|
74
78
|
- lib/smart_properties/property_collection.rb
|
79
|
+
- lib/smart_properties/validations.rb
|
80
|
+
- lib/smart_properties/validations/ancestor.rb
|
75
81
|
- lib/smart_properties/version.rb
|
76
82
|
- smart_properties.gemspec
|
77
83
|
- spec/acceptance_checking_spec.rb
|
@@ -86,10 +92,13 @@ files:
|
|
86
92
|
- spec/spec_helper.rb
|
87
93
|
- spec/support/dummy_class.rb
|
88
94
|
- spec/support/smart_property_matcher.rb
|
95
|
+
- spec/validations/ancestor_validation_spec.rb
|
96
|
+
- spec/writable_spec.rb
|
89
97
|
homepage: ''
|
90
98
|
licenses: []
|
91
|
-
metadata:
|
92
|
-
|
99
|
+
metadata:
|
100
|
+
source_code_uri: https://github.com/t6d/smart_properties
|
101
|
+
post_install_message:
|
93
102
|
rdoc_options: []
|
94
103
|
require_paths:
|
95
104
|
- lib
|
@@ -104,9 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
104
113
|
- !ruby/object:Gem::Version
|
105
114
|
version: '0'
|
106
115
|
requirements: []
|
107
|
-
|
108
|
-
|
109
|
-
signing_key:
|
116
|
+
rubygems_version: 3.2.3
|
117
|
+
signing_key:
|
110
118
|
specification_version: 4
|
111
119
|
summary: SmartProperties – Ruby accessors on steroids
|
112
120
|
test_files:
|
@@ -122,3 +130,5 @@ test_files:
|
|
122
130
|
- spec/spec_helper.rb
|
123
131
|
- spec/support/dummy_class.rb
|
124
132
|
- spec/support/smart_property_matcher.rb
|
133
|
+
- spec/validations/ancestor_validation_spec.rb
|
134
|
+
- spec/writable_spec.rb
|