smart_properties 1.9.0 → 1.10.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|