smart_properties 1.9.0 → 1.10.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 +4 -4
- data/README.md +62 -24
- data/lib/smart_properties/errors.rb +73 -0
- data/lib/smart_properties/property.rb +146 -0
- data/lib/smart_properties/property_collection.rb +81 -0
- data/lib/smart_properties/version.rb +3 -0
- data/lib/smart_properties.rb +36 -249
- data/smart_properties.gemspec +2 -1
- data/spec/base_spec.rb +60 -0
- data/spec/default_values_spec.rb +1 -1
- data/spec/inheritance_spec.rb +71 -2
- data/spec/property_collection_caching_spec.rb +29 -0
- data/spec/spec_helper.rb +75 -0
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5343469849e0419f91004d5beab3e1516f1d609
|
4
|
+
data.tar.gz: 90469264e820a223d9d77a896f5899b10c67e3ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dacd46d89ef227413d25bb3770dcd3bfad5b340f212a46cbd385fd9063d66fa7979fbb84a030600810acc95b1cefdd4a2c562bf7c2cbab47c6afb90fc5dabc77
|
7
|
+
data.tar.gz: 73e2cac3ff5075e1cb1bc61d28c85e8a54d6e14174ad0dfcaf55cd3c1b419d916b3dad78c1a0e481a1aa57582e59ca3fb6e68030e72206820beff2bc681b38a3
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# SmartProperties
|
2
2
|
|
3
3
|
Ruby accessors on steroids.
|
4
4
|
|
@@ -61,34 +61,43 @@ require 'smart_properties'
|
|
61
61
|
class Message
|
62
62
|
include SmartProperties
|
63
63
|
|
64
|
-
property :subject, :
|
65
|
-
:
|
64
|
+
property :subject, converts: :to_s,
|
65
|
+
required: true
|
66
66
|
|
67
|
-
property :body, :
|
67
|
+
property :body, converts: :to_s
|
68
68
|
|
69
|
-
property :priority, :
|
70
|
-
:
|
71
|
-
:
|
72
|
-
:
|
69
|
+
property :priority, converts: :to_sym,
|
70
|
+
accepts: [:low, :normal, :high],
|
71
|
+
default: :normal,
|
72
|
+
required: true
|
73
73
|
end
|
74
74
|
```
|
75
75
|
|
76
76
|
Creating an instance of this class without specifying any attributes will
|
77
|
-
result in an `
|
78
|
-
`subject`.
|
77
|
+
result in an `SmartProperties::InitializationError` telling you to specify the
|
78
|
+
required property `subject`.
|
79
79
|
|
80
80
|
```ruby
|
81
|
-
Message.new # => raises
|
81
|
+
Message.new # => raises SmartProperties::InitializationError, "Message requires the following properties to be set: subject"
|
82
82
|
```
|
83
83
|
|
84
|
-
|
85
|
-
property `priority` will also result in an
|
86
|
-
|
84
|
+
Creating an instance of this class with all required properties but then
|
85
|
+
setting the property `priority` to an invalid value will also result in an
|
86
|
+
`SmartProperties::InvalidValueError`. Since the property is required, assigning
|
87
|
+
`nil` is also prohibited and will result in a
|
88
|
+
`SmartProperties::MissingValueError`. All errors `SmartProperties` raises are
|
89
|
+
subclasses of `ArgumentError`.
|
87
90
|
|
88
91
|
```ruby
|
89
|
-
m = Message.new :
|
92
|
+
m = Message.new subject: 'Lorem ipsum'
|
90
93
|
m.priority # => :normal
|
91
|
-
|
94
|
+
|
95
|
+
begin
|
96
|
+
m.priority = :urgent
|
97
|
+
rescue ArgumentError => error
|
98
|
+
error.class # => raises SmartProperties::InvalidValueError
|
99
|
+
error.message # => "Message does not accept :urgent as value for the property priority"
|
100
|
+
end
|
92
101
|
```
|
93
102
|
|
94
103
|
Next, we discuss the various configuration options `SmartProperties` provide.
|
@@ -110,7 +119,7 @@ to implement a property that automatically converts all given input to a
|
|
110
119
|
|
111
120
|
```ruby
|
112
121
|
class Article
|
113
|
-
property :title, :
|
122
|
+
property :title, converts: :to_s
|
114
123
|
end
|
115
124
|
```
|
116
125
|
|
@@ -122,7 +131,7 @@ converts all given input to a slug representation.
|
|
122
131
|
|
123
132
|
```ruby
|
124
133
|
class Article
|
125
|
-
property :slug, :
|
134
|
+
property :slug, converts: lambda { |slug| slug.downcase.gsub(/\s+/, '-').gsub(/\W/, '') }
|
126
135
|
end
|
127
136
|
```
|
128
137
|
|
@@ -136,7 +145,7 @@ of type `String` as input.
|
|
136
145
|
|
137
146
|
```ruby
|
138
147
|
class Article
|
139
|
-
property :title, :
|
148
|
+
property :title, accepts: String
|
140
149
|
end
|
141
150
|
```
|
142
151
|
|
@@ -146,7 +155,7 @@ example below shows how to implement a property that only accepts `true` or
|
|
146
155
|
|
147
156
|
```ruby
|
148
157
|
class Article
|
149
|
-
property :published, :
|
158
|
+
property :published, accepts: [true, false]
|
150
159
|
end
|
151
160
|
```
|
152
161
|
|
@@ -158,7 +167,7 @@ only accepts values which match the given regular expression.
|
|
158
167
|
|
159
168
|
```ruby
|
160
169
|
class Article
|
161
|
-
property :title, :
|
170
|
+
property :title, accepts: lambda { |title| /^Lorem \w+$/ =~ title }
|
162
171
|
end
|
163
172
|
```
|
164
173
|
|
@@ -171,12 +180,12 @@ default value.
|
|
171
180
|
|
172
181
|
```ruby
|
173
182
|
class Article
|
174
|
-
property :id, :
|
183
|
+
property :id, default: 42
|
175
184
|
end
|
176
185
|
```
|
177
186
|
|
178
187
|
Default values can also be specified using blocks which are evaluated at
|
179
|
-
runtime.
|
188
|
+
runtime and only if no value was supplied.
|
180
189
|
|
181
190
|
#### Presence checking
|
182
191
|
|
@@ -187,7 +196,7 @@ how to implement a property that may not be `nil`.
|
|
187
196
|
|
188
197
|
```ruby
|
189
198
|
class Article
|
190
|
-
property :title, :
|
199
|
+
property :title, required: true
|
191
200
|
end
|
192
201
|
```
|
193
202
|
|
@@ -203,6 +212,35 @@ class Person
|
|
203
212
|
end
|
204
213
|
```
|
205
214
|
|
215
|
+
### Constructor argument forwarding
|
216
|
+
|
217
|
+
The `SmartProperties` initializer forwards anything to the super constructor
|
218
|
+
it does not process itself. This is true for all positional arguments
|
219
|
+
and those keyword arguments that do not correspond to a property. The example
|
220
|
+
below demonstrates how Ruby's `SimpleDelegator` in conjunction with
|
221
|
+
`SmartProperties` can be used to quickly construct a very flexible presenter.
|
222
|
+
|
223
|
+
```ruby
|
224
|
+
class PersonPresenter < SimpleDelegator
|
225
|
+
include SmartProperties
|
226
|
+
property :name_formatter, accepts: Proc,
|
227
|
+
required: true,
|
228
|
+
default: lambda { |p| "#{p.firstname} #{p.lastname}" }
|
229
|
+
|
230
|
+
def full_name
|
231
|
+
name_formatter.call(self)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
person = OpenStruct.new(firstname: "John", lastname: "Doe")
|
236
|
+
presenter = PersonPresenter.new(person)
|
237
|
+
presenter.full_name # => "John Doe"
|
238
|
+
|
239
|
+
# Changing the format is easy
|
240
|
+
presenter.name_formatter = lambda { |p| "#{p.lastename}, #{p.firstname}" }
|
241
|
+
presenter.full_name # => "Doe, John"
|
242
|
+
```
|
243
|
+
|
206
244
|
## Contributing
|
207
245
|
|
208
246
|
1. Fork it
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module SmartProperties
|
2
|
+
class Error < ::ArgumentError; end
|
3
|
+
class ConfigurationError < Error; end
|
4
|
+
|
5
|
+
class AssignmentError < Error
|
6
|
+
attr_accessor :sender
|
7
|
+
attr_accessor :property
|
8
|
+
|
9
|
+
def initialize(sender, property, message)
|
10
|
+
@sender = sender
|
11
|
+
@property = property
|
12
|
+
super(message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class MissingValueError < AssignmentError
|
17
|
+
def initialize(sender, property)
|
18
|
+
super(
|
19
|
+
sender,
|
20
|
+
property,
|
21
|
+
"%s requires the property %s to be set" % [
|
22
|
+
sender.class.name,
|
23
|
+
property.name
|
24
|
+
]
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_hash
|
29
|
+
Hash[property.name, "must be set"]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class InvalidValueError < AssignmentError
|
34
|
+
attr_accessor :value
|
35
|
+
|
36
|
+
def initialize(sender, property, value)
|
37
|
+
@value = value
|
38
|
+
super(
|
39
|
+
sender,
|
40
|
+
property,
|
41
|
+
"%s does not accept %s as value for the property %s" % [
|
42
|
+
sender.class.name,
|
43
|
+
value.inspect,
|
44
|
+
property.name
|
45
|
+
]
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_hash
|
50
|
+
Hash[property.name, "does not accept %s as value" % value.inspect]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class InitializationError < Error
|
55
|
+
attr_accessor :sender
|
56
|
+
attr_accessor :properties
|
57
|
+
|
58
|
+
def initialize(sender, properties)
|
59
|
+
@sender = sender
|
60
|
+
@properties = properties
|
61
|
+
super(
|
62
|
+
"%s requires the following properties to be set: %s" % [
|
63
|
+
sender.class.name,
|
64
|
+
properties.map(&:name).sort.join(', ')
|
65
|
+
]
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_hash
|
70
|
+
properties.each_with_object({}) { |property, errors| errors[property.name] = "must be set" }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
module SmartProperties
|
2
|
+
class Property
|
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
|
+
public_send(name) if self.class.properties.key?(name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=(name, value)
|
15
|
+
return if name.nil?
|
16
|
+
public_send(:"#{name.to_sym}=", value) if self.class.properties.key?(name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :name
|
21
|
+
attr_reader :converter
|
22
|
+
attr_reader :accepter
|
23
|
+
attr_reader :instance_variable_name
|
24
|
+
|
25
|
+
def self.define(scope, name, options = {})
|
26
|
+
new(name, options).tap { |p| p.define(scope) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(name, attrs = {})
|
30
|
+
attrs = attrs.dup
|
31
|
+
|
32
|
+
@name = name.to_sym
|
33
|
+
@default = attrs.delete(:default)
|
34
|
+
@converter = attrs.delete(:converts)
|
35
|
+
@accepter = attrs.delete(:accepts)
|
36
|
+
@required = attrs.delete(:required)
|
37
|
+
|
38
|
+
@instance_variable_name = :"@#{name}"
|
39
|
+
|
40
|
+
unless attrs.empty?
|
41
|
+
raise ConfigurationError, "SmartProperties do not support the following configuration options: #{attrs.keys.map { |m| m.to_s }.sort.join(', ')}."
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def required?(scope)
|
46
|
+
@required.kind_of?(Proc) ? scope.instance_exec(&@required) : !!@required
|
47
|
+
end
|
48
|
+
|
49
|
+
def optional?(scope)
|
50
|
+
!required?(scope)
|
51
|
+
end
|
52
|
+
|
53
|
+
def missing?(scope)
|
54
|
+
required?(scope) && !present?(scope)
|
55
|
+
end
|
56
|
+
|
57
|
+
def present?(scope)
|
58
|
+
!null_object?(get(scope))
|
59
|
+
end
|
60
|
+
|
61
|
+
def convert(scope, value)
|
62
|
+
return value unless converter
|
63
|
+
return value if null_object?(value)
|
64
|
+
scope.instance_exec(value, &converter)
|
65
|
+
end
|
66
|
+
|
67
|
+
def default(scope)
|
68
|
+
@default.kind_of?(Proc) ? scope.instance_exec(&@default) : @default
|
69
|
+
end
|
70
|
+
|
71
|
+
def accepts?(value, scope)
|
72
|
+
return true unless accepter
|
73
|
+
return true if null_object?(value)
|
74
|
+
|
75
|
+
if accepter.respond_to?(:to_proc)
|
76
|
+
!!scope.instance_exec(value, &accepter)
|
77
|
+
else
|
78
|
+
Array(accepter).any? { |accepter| accepter === value }
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def prepare(scope, value)
|
83
|
+
required = required?(scope)
|
84
|
+
raise MissingValueError.new(scope, self) if required && null_object?(value)
|
85
|
+
value = convert(scope, value)
|
86
|
+
raise MissingValueError.new(scope, self) if required && null_object?(value)
|
87
|
+
raise InvalidValueError.new(scope, self, value) unless accepts?(value, scope)
|
88
|
+
value
|
89
|
+
end
|
90
|
+
|
91
|
+
def define(klass)
|
92
|
+
property = self
|
93
|
+
|
94
|
+
scope =
|
95
|
+
if klass.instance_variable_defined?(MODULE_REFERENCE)
|
96
|
+
klass.instance_variable_get(MODULE_REFERENCE)
|
97
|
+
else
|
98
|
+
m = Module.new { include IndexMethods }
|
99
|
+
klass.send(:include, m)
|
100
|
+
klass.instance_variable_set(MODULE_REFERENCE, m)
|
101
|
+
m
|
102
|
+
end
|
103
|
+
|
104
|
+
scope.send(:attr_reader, name)
|
105
|
+
scope.send(:define_method, :"#{name}=") do |value|
|
106
|
+
property.set(self, value)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def set(scope, value)
|
111
|
+
scope.instance_variable_set(instance_variable_name, prepare(scope, value))
|
112
|
+
end
|
113
|
+
|
114
|
+
def set_default(scope)
|
115
|
+
return false if present?(scope)
|
116
|
+
|
117
|
+
default_value = default(scope)
|
118
|
+
return false if null_object?(default_value)
|
119
|
+
|
120
|
+
set(scope, default_value)
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
def get(scope)
|
125
|
+
return nil unless scope.instance_variable_defined?(instance_variable_name)
|
126
|
+
scope.instance_variable_get(instance_variable_name)
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def null_object?(object)
|
132
|
+
return true if object == nil
|
133
|
+
return true if object.nil?
|
134
|
+
false
|
135
|
+
rescue NoMethodError => error
|
136
|
+
# BasicObject does not respond to #nil? by default, so we need to double
|
137
|
+
# check if somebody implemented it and it fails internally or if the
|
138
|
+
# error occured because the method is actually not present. In the former
|
139
|
+
# case, we want to raise the exception because there is something wrong
|
140
|
+
# with the implementation of object#nil?. In the latter case we treat the
|
141
|
+
# object as truthy because we don't know better.
|
142
|
+
raise error if (class << object; self; end).public_instance_methods.include?(:nil?)
|
143
|
+
false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module SmartProperties
|
2
|
+
class PropertyCollection
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :parent
|
6
|
+
|
7
|
+
def self.for(scope)
|
8
|
+
parent = scope.ancestors[1..-1].find do |ancestor|
|
9
|
+
ancestor.ancestors.include?(SmartProperties) && ancestor != SmartProperties
|
10
|
+
end
|
11
|
+
|
12
|
+
if parent.nil?
|
13
|
+
new
|
14
|
+
else
|
15
|
+
parent.properties.register(collection = new)
|
16
|
+
collection
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@collection = {}
|
22
|
+
@collection_with_parent_collection = {}
|
23
|
+
@children = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(name, value)
|
27
|
+
name = name.to_s
|
28
|
+
collection[name] = value
|
29
|
+
collection_with_parent_collection[name] = value
|
30
|
+
notify_children
|
31
|
+
value
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](name)
|
35
|
+
collection_with_parent_collection[name.to_s]
|
36
|
+
end
|
37
|
+
|
38
|
+
def key?(name)
|
39
|
+
collection_with_parent_collection.key?(name.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
def keys
|
43
|
+
collection_with_parent_collection.keys.map(&:to_sym)
|
44
|
+
end
|
45
|
+
|
46
|
+
def values
|
47
|
+
collection_with_parent_collection.values
|
48
|
+
end
|
49
|
+
|
50
|
+
def each(&block)
|
51
|
+
return to_enum(:each) if block.nil?
|
52
|
+
collection_with_parent_collection.each { |name, value| block.call([name.to_sym, value]) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_hash
|
56
|
+
Hash[each.to_a]
|
57
|
+
end
|
58
|
+
|
59
|
+
def register(child)
|
60
|
+
children.push(child)
|
61
|
+
child.refresh(collection_with_parent_collection)
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
attr_accessor :children
|
68
|
+
attr_accessor :collection
|
69
|
+
attr_accessor :collection_with_parent_collection
|
70
|
+
|
71
|
+
def notify_children
|
72
|
+
@children.each { |child| child.refresh(collection_with_parent_collection) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def refresh(parent_collection)
|
76
|
+
@collection_with_parent_collection = parent_collection.merge(collection)
|
77
|
+
notify_children
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/smart_properties.rb
CHANGED
@@ -21,211 +21,7 @@
|
|
21
21
|
# :required => true
|
22
22
|
#
|
23
23
|
module SmartProperties
|
24
|
-
VERSION = "1.9.0"
|
25
|
-
|
26
|
-
class Error < ::ArgumentError; end
|
27
|
-
class ConfigurationError < Error; end
|
28
|
-
|
29
|
-
class AssignmentError < Error
|
30
|
-
attr_accessor :sender
|
31
|
-
attr_accessor :property
|
32
|
-
|
33
|
-
def initialize(sender, property, message)
|
34
|
-
@sender = sender
|
35
|
-
@property = property
|
36
|
-
super(message)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
class MissingValueError < AssignmentError
|
41
|
-
def initialize(sender, property)
|
42
|
-
super(
|
43
|
-
sender,
|
44
|
-
property,
|
45
|
-
"%s requires the property %s to be set" % [
|
46
|
-
sender.class.name,
|
47
|
-
property.name
|
48
|
-
]
|
49
|
-
)
|
50
|
-
end
|
51
|
-
|
52
|
-
def to_hash
|
53
|
-
Hash[property.name, "must be set"]
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
class InvalidValueError < AssignmentError
|
58
|
-
attr_accessor :value
|
59
|
-
|
60
|
-
def initialize(sender, property, value)
|
61
|
-
@value = value
|
62
|
-
super(
|
63
|
-
sender,
|
64
|
-
property,
|
65
|
-
"%s does not accept %s as value for the property %s" % [
|
66
|
-
sender.class.name,
|
67
|
-
value.inspect,
|
68
|
-
property.name
|
69
|
-
]
|
70
|
-
)
|
71
|
-
end
|
72
|
-
|
73
|
-
def to_hash
|
74
|
-
Hash[property.name, "does not accept %s as value" % value.inspect]
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
class InitializationError < Error
|
79
|
-
attr_accessor :sender
|
80
|
-
attr_accessor :properties
|
81
|
-
|
82
|
-
def initialize(sender, properties)
|
83
|
-
@sender = sender
|
84
|
-
@properties = properties
|
85
|
-
super(
|
86
|
-
"%s requires the following properties to be set: %s" % [
|
87
|
-
sender.class.name,
|
88
|
-
properties.map(&:name).sort.join(', ')
|
89
|
-
]
|
90
|
-
)
|
91
|
-
end
|
92
|
-
|
93
|
-
def to_hash
|
94
|
-
properties.each_with_object({}) { |property, errors| errors[property.name] = "must be set" }
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
class Property
|
99
|
-
# Defines the two index methods #[] and #[]=. This module will be included
|
100
|
-
# in the SmartProperties method scope.
|
101
|
-
module IndexMethods
|
102
|
-
def [](name)
|
103
|
-
return if name.nil?
|
104
|
-
name &&= name.to_sym
|
105
|
-
public_send(name) if self.class.properties.key?(name)
|
106
|
-
end
|
107
|
-
|
108
|
-
def []=(name, value)
|
109
|
-
return if name.nil?
|
110
|
-
public_send(:"#{name.to_sym}=", value) if self.class.properties.key?(name)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
attr_reader :name
|
115
|
-
attr_reader :converter
|
116
|
-
attr_reader :accepter
|
117
|
-
|
118
|
-
def initialize(name, attrs = {})
|
119
|
-
attrs = attrs.dup
|
120
|
-
|
121
|
-
@name = name.to_sym
|
122
|
-
@default = attrs.delete(:default)
|
123
|
-
@converter = attrs.delete(:converts)
|
124
|
-
@accepter = attrs.delete(:accepts)
|
125
|
-
@required = attrs.delete(:required)
|
126
|
-
|
127
|
-
unless attrs.empty?
|
128
|
-
raise ConfigurationError, "SmartProperties do not support the following configuration options: #{attrs.keys.map { |m| m.to_s }.sort.join(', ')}."
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
def required?(scope)
|
133
|
-
@required.kind_of?(Proc) ? scope.instance_exec(&@required) : !!@required
|
134
|
-
end
|
135
|
-
|
136
|
-
def convert(value, scope)
|
137
|
-
return value unless converter
|
138
|
-
scope.instance_exec(value, &converter)
|
139
|
-
end
|
140
|
-
|
141
|
-
def default(scope)
|
142
|
-
@default.kind_of?(Proc) ? scope.instance_exec(&@default) : @default
|
143
|
-
end
|
144
|
-
|
145
|
-
def accepts?(value, scope)
|
146
|
-
return true unless value
|
147
|
-
return true unless accepter
|
148
|
-
|
149
|
-
if accepter.respond_to?(:to_proc)
|
150
|
-
!!scope.instance_exec(value, &accepter)
|
151
|
-
else
|
152
|
-
Array(accepter).any? { |accepter| accepter === value }
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
def prepare(value, scope)
|
157
|
-
raise MissingValueError.new(scope, self) if required?(scope) && value.nil?
|
158
|
-
value = convert(value, scope) unless value.nil?
|
159
|
-
raise MissingValueError.new(scope, self) if required?(scope) && value.nil?
|
160
|
-
raise InvalidValueError.new(scope, self, value) unless accepts?(value, scope)
|
161
|
-
value
|
162
|
-
end
|
163
|
-
|
164
|
-
def define(klass)
|
165
|
-
property = self
|
166
|
-
|
167
|
-
scope = klass.instance_variable_get(:"@_smart_properties_method_scope") || begin
|
168
|
-
m = Module.new { include IndexMethods }
|
169
|
-
klass.send(:include, m)
|
170
|
-
klass.instance_variable_set(:"@_smart_properties_method_scope", m)
|
171
|
-
m
|
172
|
-
end
|
173
|
-
|
174
|
-
scope.send(:attr_reader, name)
|
175
|
-
scope.send(:define_method, :"#{name}=") do |value|
|
176
|
-
instance_variable_set("@#{property.name}", property.prepare(value, self))
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
end
|
181
|
-
|
182
|
-
class PropertyCollection
|
183
|
-
|
184
|
-
include Enumerable
|
185
|
-
|
186
|
-
attr_reader :parent
|
187
|
-
|
188
|
-
def initialize(parent = nil)
|
189
|
-
@parent = parent
|
190
|
-
@collection = {}
|
191
|
-
end
|
192
|
-
|
193
|
-
def []=(name, value)
|
194
|
-
collection[name] = value
|
195
|
-
end
|
196
|
-
|
197
|
-
def [](name)
|
198
|
-
collection_with_parent_collection[name]
|
199
|
-
end
|
200
|
-
|
201
|
-
def key?(name)
|
202
|
-
collection_with_parent_collection.key?(name)
|
203
|
-
end
|
204
|
-
|
205
|
-
def keys
|
206
|
-
collection_with_parent_collection.keys
|
207
|
-
end
|
208
|
-
|
209
|
-
def values
|
210
|
-
collection_with_parent_collection.values
|
211
|
-
end
|
212
|
-
|
213
|
-
def each(&block)
|
214
|
-
collection_with_parent_collection.each(&block)
|
215
|
-
end
|
216
|
-
|
217
|
-
protected
|
218
|
-
|
219
|
-
attr_accessor :collection
|
220
|
-
|
221
|
-
def collection_with_parent_collection
|
222
|
-
parent.nil? ? collection : parent.collection.merge(collection)
|
223
|
-
end
|
224
|
-
|
225
|
-
end
|
226
|
-
|
227
24
|
module ClassMethods
|
228
|
-
|
229
25
|
##
|
230
26
|
# Returns a class's smart properties. This includes the properties that
|
231
27
|
# have been defined in the parent classes.
|
@@ -233,13 +29,7 @@ module SmartProperties
|
|
233
29
|
# @return [Hash<String, Property>] A map of property names to property instances.
|
234
30
|
#
|
235
31
|
def properties
|
236
|
-
@_smart_properties ||=
|
237
|
-
parent = if self != SmartProperties
|
238
|
-
(ancestors[1..-1].find { |klass| klass.ancestors.include?(SmartProperties) && klass != SmartProperties })
|
239
|
-
end
|
240
|
-
|
241
|
-
parent.nil? ? PropertyCollection.new : PropertyCollection.new(parent.properties)
|
242
|
-
end
|
32
|
+
@_smart_properties ||= PropertyCollection.for(self)
|
243
33
|
end
|
244
34
|
|
245
35
|
##
|
@@ -290,29 +80,23 @@ module SmartProperties
|
|
290
80
|
# :required => true
|
291
81
|
#
|
292
82
|
def property(name, options = {})
|
293
|
-
|
294
|
-
p.define(self)
|
295
|
-
|
296
|
-
properties[name] = p
|
83
|
+
properties[name] = Property.define(self, name, options)
|
297
84
|
end
|
298
85
|
protected :property
|
299
|
-
|
300
86
|
end
|
301
87
|
|
302
88
|
class << self
|
303
|
-
|
304
89
|
private
|
305
90
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
91
|
+
##
|
92
|
+
# Extends the class, which this module is included in, with a property
|
93
|
+
# method to define properties.
|
94
|
+
#
|
95
|
+
# @param [Class] base the class this module is included in
|
96
|
+
#
|
97
|
+
def included(base)
|
98
|
+
base.extend(ClassMethods)
|
99
|
+
end
|
316
100
|
end
|
317
101
|
|
318
102
|
##
|
@@ -322,38 +106,41 @@ module SmartProperties
|
|
322
106
|
# @param [Hash] attrs the set of attributes that is used for initialization
|
323
107
|
#
|
324
108
|
def initialize(*args, &block)
|
325
|
-
attrs = args.last.is_a?(Hash) ? args.pop : {}
|
326
|
-
|
109
|
+
attrs = args.last.is_a?(Hash) ? args.pop.dup : {}
|
110
|
+
properties = self.class.properties
|
327
111
|
|
328
|
-
|
112
|
+
# Track missing properties
|
329
113
|
missing_properties = []
|
330
114
|
|
331
|
-
#
|
332
|
-
properties.each do |
|
333
|
-
if attrs.key?(
|
334
|
-
|
115
|
+
# Set values
|
116
|
+
properties.each do |name, property|
|
117
|
+
if attrs.key?(name)
|
118
|
+
property.set(self, attrs.delete(name))
|
119
|
+
elsif attrs.key?(name.to_s)
|
120
|
+
property.set(self, attrs.delete(name.to_s))
|
335
121
|
else
|
336
122
|
missing_properties.push(property)
|
337
123
|
end
|
338
124
|
end
|
339
125
|
|
340
|
-
#
|
126
|
+
# Call the super constructor and forward unprocessed arguments
|
127
|
+
attrs.empty? ? super(*args) : super(*args.push(attrs))
|
128
|
+
|
129
|
+
# Execute configuration block
|
341
130
|
block.call(self) if block
|
342
131
|
|
343
|
-
# Set
|
344
|
-
missing_properties.
|
345
|
-
variable = "@#{property.name}"
|
346
|
-
if instance_variable_get(variable).nil? && !(default_value = property.default(self)).nil?
|
347
|
-
instance_variable_set(variable, property.prepare(default_value, self))
|
348
|
-
end
|
349
|
-
end
|
132
|
+
# Set default values for missing properties
|
133
|
+
missing_properties.delete_if { |property| property.set_default(self) }
|
350
134
|
|
351
|
-
#
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
raise error
|
357
|
-
end
|
135
|
+
# Recheck - cannot be done while assigning default values because
|
136
|
+
# one property might depend on the default value of another property
|
137
|
+
missing_properties.delete_if { |property| property.present?(self) || property.optional?(self) }
|
138
|
+
|
139
|
+
raise SmartProperties::InitializationError.new(self, missing_properties) unless missing_properties.empty?
|
358
140
|
end
|
359
141
|
end
|
142
|
+
|
143
|
+
require_relative 'smart_properties/property_collection'
|
144
|
+
require_relative 'smart_properties/property'
|
145
|
+
require_relative 'smart_properties/errors'
|
146
|
+
require_relative 'smart_properties/version'
|
data/smart_properties.gemspec
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
|
2
|
+
require_relative 'lib/smart_properties/version'
|
3
3
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
5
|
gem.authors = ["Konstantin Tennhard"]
|
@@ -21,4 +21,5 @@ Gem::Specification.new do |gem|
|
|
21
21
|
|
22
22
|
gem.add_development_dependency "rspec", "~> 3.0"
|
23
23
|
gem.add_development_dependency "rake", "~> 10.0"
|
24
|
+
gem.add_development_dependency "pry"
|
24
25
|
end
|
data/spec/base_spec.rb
CHANGED
@@ -7,6 +7,20 @@ RSpec.describe SmartProperties do
|
|
7
7
|
expect { klass.property }.to raise_error(NoMethodError)
|
8
8
|
end
|
9
9
|
|
10
|
+
context "when used to build a class with a property that uses none of the features provided by SmartProperties" do
|
11
|
+
subject(:klass) { DummyClass.new { property :title } }
|
12
|
+
|
13
|
+
context "an instance of this class" do
|
14
|
+
it 'should accept an instance of BasicObject, which does not respond to nil?, as value for title' do
|
15
|
+
instance = klass.new
|
16
|
+
expect { instance.title = BasicObject.new }.to_not raise_error
|
17
|
+
expect { instance[:title] = BasicObject.new }.to_not raise_error
|
18
|
+
|
19
|
+
expect { instance = klass.new(title: BasicObject) }.to_not raise_error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
10
24
|
context "when used to build a class that has a property called title that utilizes the full feature set of SmartProperties" do
|
11
25
|
subject(:klass) do
|
12
26
|
default_title = double(to_title: 'chunky')
|
@@ -64,12 +78,58 @@ RSpec.describe SmartProperties do
|
|
64
78
|
end
|
65
79
|
end
|
66
80
|
|
81
|
+
context 'an instance of this class when initialized with a null object' do
|
82
|
+
let(:null_object) do
|
83
|
+
Class.new(BasicObject) do
|
84
|
+
def nil?
|
85
|
+
true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should raise an error during initialization' do
|
91
|
+
exception = SmartProperties::MissingValueError
|
92
|
+
message = "Dummy requires the property title to be set"
|
93
|
+
further_expectations = lambda do |error|
|
94
|
+
expect(error.to_hash[:title]).to eq('must be set')
|
95
|
+
end
|
96
|
+
|
97
|
+
instance = klass.new
|
98
|
+
expect { instance.title = null_object.new }.to raise_error(exception, message, &further_expectations)
|
99
|
+
expect { instance[:title] = null_object.new }.to raise_error(exception, message, &further_expectations)
|
100
|
+
|
101
|
+
expect { klass.new(title: null_object.new) }.to raise_error(exception, message, &further_expectations)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'an instance of this class when initialized with a subclass of BasicObject that responds to #to_title but by design not to #nil?' do
|
106
|
+
let(:title) do
|
107
|
+
Class.new(BasicObject) do
|
108
|
+
def to_title
|
109
|
+
"Chunky bacon"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should have the correct title' do
|
115
|
+
instance = klass.new(title: title.new)
|
116
|
+
expect(instance.title).to eq("Chunky bacon")
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
67
120
|
context 'an instance of this class when initialized with a title argument' do
|
68
121
|
it "should have the title specified by the corresponding keyword argument" do
|
69
122
|
instance = klass.new(title: double(to_title: 'bacon'))
|
70
123
|
expect(instance.title).to eq('bacon')
|
71
124
|
expect(instance[:title]).to eq('bacon')
|
72
125
|
end
|
126
|
+
|
127
|
+
it "should have the title specified in the corresponding attributes hash that uses strings as keys" do
|
128
|
+
attributes = {"title" => double(to_title: 'bacon')}
|
129
|
+
instance = klass.new(attributes)
|
130
|
+
expect(instance.title).to eq('bacon')
|
131
|
+
expect(instance[:title]).to eq('bacon')
|
132
|
+
end
|
73
133
|
end
|
74
134
|
|
75
135
|
context "an instance of this class when initialized with a block" do
|
data/spec/default_values_spec.rb
CHANGED
@@ -7,7 +7,7 @@ RSpec.describe SmartProperties, 'default values' do
|
|
7
7
|
context "an instance of this class" do
|
8
8
|
it "should evaluate the lambda in its own scope and thus differ from every other instance" do
|
9
9
|
first_instance, second_instance = klass.new, klass.new
|
10
|
-
expect(
|
10
|
+
expect(first_instance.id).to_not eq(second_instance.id)
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
data/spec/inheritance_spec.rb
CHANGED
@@ -17,14 +17,16 @@ RSpec.describe SmartProperties, 'intheritance' do
|
|
17
17
|
context 'when modeling the following class hiearchy: Base > Section > SectionWithSubtitle' do
|
18
18
|
let!(:base) do
|
19
19
|
Class.new do
|
20
|
-
attr_reader :content
|
21
|
-
def initialize(content = nil)
|
20
|
+
attr_reader :content, :options
|
21
|
+
def initialize(content = nil, options = {})
|
22
22
|
@content = content
|
23
|
+
@options = options
|
23
24
|
end
|
24
25
|
end
|
25
26
|
end
|
26
27
|
let!(:section) { DummyClass.new(base) { property :title } }
|
27
28
|
let!(:subsection) { DummyClass.new(section) { property :subtitle } }
|
29
|
+
let!(:subsubsection) { DummyClass.new(subsection) { property :subsubtitle } }
|
28
30
|
|
29
31
|
context 'the base class' do
|
30
32
|
it('should not respond to #properties') { expect(base).to_not respond_to(:properties) }
|
@@ -47,6 +49,11 @@ RSpec.describe SmartProperties, 'intheritance' do
|
|
47
49
|
subject { section.new }
|
48
50
|
it { is_expected.to have_smart_property(:title) }
|
49
51
|
it { is_expected.to_not have_smart_property(:subtitle) }
|
52
|
+
|
53
|
+
it 'should forward keyword arguments that do not correspond to a property to the super class constructor' do
|
54
|
+
instance = section.new('some content', answer: 42)
|
55
|
+
expect(instance.options).to eq({answer: 42})
|
56
|
+
end
|
50
57
|
end
|
51
58
|
|
52
59
|
context 'an instance of this class when initialized with content' do
|
@@ -89,6 +96,68 @@ RSpec.describe SmartProperties, 'intheritance' do
|
|
89
96
|
expect(instance.title).to eq('some title')
|
90
97
|
expect(instance.subtitle).to eq('some subtitle')
|
91
98
|
end
|
99
|
+
|
100
|
+
it 'should forward keyword arguments that do not correspond to a property to the super class constructor' do
|
101
|
+
instance = subsection.new('some content', answer: 42)
|
102
|
+
expect(instance.options).to eq({answer: 42})
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'the subsubsectionclass' do
|
108
|
+
it "should expose the names of the properties through its property collection" do
|
109
|
+
expect(subsubsection.properties.keys).to eq([:title, :subtitle, :subsubtitle])
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should expose the the properties through its property collection" do
|
113
|
+
properties = subsubsection.properties.values
|
114
|
+
|
115
|
+
expect(properties[0]).to be_kind_of(SmartProperties::Property)
|
116
|
+
expect(properties[0].name).to eq(:title)
|
117
|
+
|
118
|
+
expect(properties[1]).to be_kind_of(SmartProperties::Property)
|
119
|
+
expect(properties[1].name).to eq(:subtitle)
|
120
|
+
|
121
|
+
expect(properties[2]).to be_kind_of(SmartProperties::Property)
|
122
|
+
expect(properties[2].name).to eq(:subsubtitle)
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'an instance of this class' do
|
126
|
+
subject(:instance) { subsubsection.new }
|
127
|
+
it { is_expected.to have_smart_property(:title) }
|
128
|
+
it { is_expected.to have_smart_property(:subtitle) }
|
129
|
+
it { is_expected.to have_smart_property(:subsubtitle) }
|
130
|
+
|
131
|
+
it 'should have content, a title, and a subtile when initialized with these parameters' do
|
132
|
+
instance = subsubsection.new('some content', title: 'some title', subtitle: 'some subtitle', subsubtitle: 'some subsubtitle')
|
133
|
+
expect(instance.content).to eq('some content')
|
134
|
+
expect(instance.title).to eq('some title')
|
135
|
+
expect(instance.subtitle).to eq('some subtitle')
|
136
|
+
expect(instance.subsubtitle).to eq('some subsubtitle')
|
137
|
+
|
138
|
+
instance = subsubsection.new('some content') do |s|
|
139
|
+
s.title, s.subtitle, s.subsubtitle = 'some title', 'some subtitle', 'some subsubtitle'
|
140
|
+
end
|
141
|
+
expect(instance.content).to eq('some content')
|
142
|
+
expect(instance.title).to eq('some title')
|
143
|
+
expect(instance.subtitle).to eq('some subtitle')
|
144
|
+
expect(instance.subsubtitle).to eq('some subsubtitle')
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should not accidentally forward attributes as options because the keys where strings' do
|
148
|
+
attributes = {'title' => 'some title', 'subtitle' => 'some subtitle', 'subsubtitle' => "some subsubtitle", "answer" => 42}
|
149
|
+
instance = subsubsection.new('some content', attributes)
|
150
|
+
expect(instance.content).to eq('some content')
|
151
|
+
expect(instance.title).to eq('some title')
|
152
|
+
expect(instance.subtitle).to eq('some subtitle')
|
153
|
+
expect(instance.subsubtitle).to eq('some subsubtitle')
|
154
|
+
expect(instance.options).to eq({"answer" => 42})
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should forward keyword arguments that do not correspond to a property to the super class constructor' do
|
158
|
+
instance = subsubsection.new('some content', answer: 42)
|
159
|
+
expect(instance.options).to eq({answer: 42})
|
160
|
+
end
|
92
161
|
end
|
93
162
|
end
|
94
163
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe SmartProperties, "property collection caching:" do
|
4
|
+
specify "SmartProperty enabled objects should be extendable at runtime" do
|
5
|
+
base_class = DummyClass.new { property :title }
|
6
|
+
subclass = DummyClass.new(base_class) { property :body }
|
7
|
+
subsubclass = DummyClass.new(subclass) { property :attachment }
|
8
|
+
|
9
|
+
expect(base_class.new).to have_smart_property(:title)
|
10
|
+
expect(subclass.new).to have_smart_property(:title)
|
11
|
+
expect(subclass.new).to have_smart_property(:body)
|
12
|
+
|
13
|
+
base_class.class_eval { property :severity }
|
14
|
+
expect(base_class.new).to have_smart_property(:severity)
|
15
|
+
expect(subclass.new).to have_smart_property(:severity)
|
16
|
+
expect(subsubclass.new).to have_smart_property(:severity)
|
17
|
+
|
18
|
+
expected_names = [:title, :body, :attachment, :severity]
|
19
|
+
|
20
|
+
names = subsubclass.properties.map(&:first) # Using enumerable
|
21
|
+
expect(names - expected_names).to be_empty
|
22
|
+
|
23
|
+
names = subsubclass.properties.each.map(&:first) # Using enumerator
|
24
|
+
expect(names - expected_names).to be_empty
|
25
|
+
|
26
|
+
expect(subsubclass.properties.keys - expected_names).to be_empty
|
27
|
+
expect(subsubclass.properties.to_hash.keys - expected_names).to be_empty
|
28
|
+
end
|
29
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,9 +1,84 @@
|
|
1
1
|
require 'bundler/setup'
|
2
2
|
require 'rspec'
|
3
|
+
require 'pry'
|
3
4
|
|
4
5
|
require 'smart_properties'
|
5
6
|
|
6
7
|
Dir[File.join(File.dirname(__FILE__), 'support', '**', '*.rb')].each { |f| require f }
|
7
8
|
|
9
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
8
10
|
RSpec.configure do |config|
|
11
|
+
# rspec-expectations config goes here. You can use an alternate
|
12
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
13
|
+
# assertions if you prefer.
|
14
|
+
config.expect_with :rspec do |expectations|
|
15
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
16
|
+
# and `failure_message` of custom matchers include text for helper methods
|
17
|
+
# defined using `chain`, e.g.:
|
18
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
19
|
+
# # => "be bigger than 2 and smaller than 4"
|
20
|
+
# ...rather than:
|
21
|
+
# # => "be bigger than 2"
|
22
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
23
|
+
end
|
24
|
+
|
25
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
26
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
27
|
+
config.mock_with :rspec do |mocks|
|
28
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
29
|
+
# a real object. This is generally recommended, and will default to
|
30
|
+
# `true` in RSpec 4.
|
31
|
+
mocks.verify_partial_doubles = true
|
32
|
+
end
|
33
|
+
|
34
|
+
# These two settings work together to allow you to limit a spec run
|
35
|
+
# to individual examples or groups you care about by tagging them with
|
36
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
37
|
+
# get run.
|
38
|
+
config.filter_run :focus
|
39
|
+
config.run_all_when_everything_filtered = true
|
40
|
+
|
41
|
+
# Allows RSpec to persist some state between runs in order to support
|
42
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
43
|
+
# you configure your source control system to ignore this file.
|
44
|
+
#
|
45
|
+
# config.example_status_persistence_file_path = "spec/examples.txt"
|
46
|
+
|
47
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
48
|
+
# recommended. For more details, see:
|
49
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
50
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
51
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
52
|
+
config.disable_monkey_patching!
|
53
|
+
|
54
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
55
|
+
# be too noisy due to issues in dependencies.
|
56
|
+
config.warnings = true
|
57
|
+
|
58
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
59
|
+
# file, and it's useful to allow more verbose output when running an
|
60
|
+
# individual spec file.
|
61
|
+
if config.files_to_run.one?
|
62
|
+
# Use the documentation formatter for detailed output,
|
63
|
+
# unless a formatter has already been configured
|
64
|
+
# (e.g. via a command-line flag).
|
65
|
+
config.default_formatter = 'doc'
|
66
|
+
end
|
67
|
+
|
68
|
+
# Print the 10 slowest examples and example groups at the
|
69
|
+
# end of the spec run, to help surface which specs are running
|
70
|
+
# particularly slow.
|
71
|
+
config.profile_examples = 10
|
72
|
+
|
73
|
+
# Run specs in random order to surface order dependencies. If you find an
|
74
|
+
# order dependency and want to debug it, you can fix the order by providing
|
75
|
+
# the seed, which is printed after each run.
|
76
|
+
# --seed 1234
|
77
|
+
config.order = :random
|
78
|
+
|
79
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
80
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
81
|
+
# test failures related to randomization by passing the same `--seed` value
|
82
|
+
# as the one that triggered the failure.
|
83
|
+
Kernel.srand config.seed
|
9
84
|
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.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Konstantin Tennhard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-10-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry
|
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'
|
41
55
|
description: |2
|
42
56
|
SmartProperties are a more flexible and feature-rich alternative to
|
43
57
|
traditional Ruby accessors. They provide support for input conversion,
|
@@ -56,6 +70,10 @@ files:
|
|
56
70
|
- README.md
|
57
71
|
- Rakefile
|
58
72
|
- lib/smart_properties.rb
|
73
|
+
- lib/smart_properties/errors.rb
|
74
|
+
- lib/smart_properties/property.rb
|
75
|
+
- lib/smart_properties/property_collection.rb
|
76
|
+
- lib/smart_properties/version.rb
|
59
77
|
- smart_properties.gemspec
|
60
78
|
- spec/acceptance_checking_spec.rb
|
61
79
|
- spec/base_spec.rb
|
@@ -63,6 +81,7 @@ files:
|
|
63
81
|
- spec/conversion_spec.rb
|
64
82
|
- spec/default_values_spec.rb
|
65
83
|
- spec/inheritance_spec.rb
|
84
|
+
- spec/property_collection_caching_spec.rb
|
66
85
|
- spec/required_values_spec.rb
|
67
86
|
- spec/spec_helper.rb
|
68
87
|
- spec/support/dummy_class.rb
|
@@ -97,6 +116,7 @@ test_files:
|
|
97
116
|
- spec/conversion_spec.rb
|
98
117
|
- spec/default_values_spec.rb
|
99
118
|
- spec/inheritance_spec.rb
|
119
|
+
- spec/property_collection_caching_spec.rb
|
100
120
|
- spec/required_values_spec.rb
|
101
121
|
- spec/spec_helper.rb
|
102
122
|
- spec/support/dummy_class.rb
|