smart_properties 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +20 -0
- data/.yardopts +1 -0
- data/Gemfile +6 -0
- data/Guardfile +9 -0
- data/LICENSE +22 -0
- data/README.md +164 -0
- data/Rakefile +8 -0
- data/lib/smart_properties.rb +217 -0
- data/smart_properties.gemspec +27 -0
- data/spec/smart_properties_spec.rb +325 -0
- data/spec/spec_helper.rb +10 -0
- metadata +147 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--protected
|
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Konstantin Tennhard
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
# SmartProperties
|
2
|
+
|
3
|
+
Ruby accessors on steroids.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'smart_properties'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install smart_properties
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
`SmartProperties` are meant to extend standard Ruby classes. Simply include
|
22
|
+
the `SmartProperties` module and use the `property` method along with a name
|
23
|
+
and optional configuration parameters to define new properties. Calling this
|
24
|
+
method results in the generation of a getter and setter pair. In contrast to
|
25
|
+
traditional Ruby accessors -- created by calling `attr_accessor`,
|
26
|
+
`SmartProperties` provide much more functionality:
|
27
|
+
|
28
|
+
1. input conversion,
|
29
|
+
2. input validation,
|
30
|
+
3. default values, and
|
31
|
+
4. presence checking.
|
32
|
+
|
33
|
+
These features can be configured by calling the `property` method with
|
34
|
+
additional configuration parameters. The module also provides a default
|
35
|
+
implementation for a constructor that accepts a set of attributes. This is
|
36
|
+
comparable to the constructor of `ActiveRecord` objects.
|
37
|
+
|
38
|
+
Before we discuss the configuration of properties in more detail, we first
|
39
|
+
present a short synopsis of all the functionality provided by
|
40
|
+
`SmartProperties`.
|
41
|
+
|
42
|
+
### Synopsis
|
43
|
+
|
44
|
+
The example below shows how to implement a class called `Message` which has
|
45
|
+
three properties: `subject`, `body`, and `priority`. The two properties,
|
46
|
+
`subject` and `priority`, are required whereas `body` is optional.
|
47
|
+
Furthermore, all properties use input conversion. The `priority` property also
|
48
|
+
uses validation and has a default value.
|
49
|
+
|
50
|
+
class Message
|
51
|
+
property :subject, :converts => :to_s
|
52
|
+
|
53
|
+
property :body, :converts => :to_s
|
54
|
+
|
55
|
+
property :priority, :converts => :to_sym,
|
56
|
+
:accepts => [:low, :normal, :high],
|
57
|
+
:default => :normal
|
58
|
+
:required => true
|
59
|
+
end
|
60
|
+
|
61
|
+
Creating an instance of this class without specifying any attributes will
|
62
|
+
result in an `ArgumentError` telling you to specify the required property
|
63
|
+
`subject`.
|
64
|
+
|
65
|
+
Message.new # => raises ArgumentError, "Message requires the property subject to be set"
|
66
|
+
|
67
|
+
Providing the constructor with a title but with an invalid value for the
|
68
|
+
property `priority` will also result in an `ArgumentError` telling you to
|
69
|
+
provide a proper value for the property `priority`.
|
70
|
+
|
71
|
+
m = Message.new :subject => 'Lorem ipsum'
|
72
|
+
m.priority # => :normal
|
73
|
+
m.priority = :urgent # => raises ArgumentError, Message does not accept :urgent as value for the property priority
|
74
|
+
|
75
|
+
Next, we discuss the various configuration options `SmartProperties` provide.
|
76
|
+
|
77
|
+
### Property Configuration
|
78
|
+
|
79
|
+
This subsection explains the various configuration options `SmartProperties`
|
80
|
+
provide.
|
81
|
+
|
82
|
+
#### Input conversion
|
83
|
+
|
84
|
+
To automatically convert a given value for a property, you can use the
|
85
|
+
`:converts` configuration parameter. The parameter can either be a `Symbol` or
|
86
|
+
a `lambda` statement. Using a `Symbol` will instruct the setter to call the
|
87
|
+
method identified by this symbol on the object provided as input data and take
|
88
|
+
the result of this method call as value instead. The example below shows how
|
89
|
+
to implement a property that automatically converts all given input to a
|
90
|
+
`String` by calling `#to_s` on the object provided as input.
|
91
|
+
|
92
|
+
class Article
|
93
|
+
property :title, :converts => :to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
If you need more fine-grained control, you can use a lambda statement to
|
97
|
+
specify how the conversion should be done. The statement will be evaluated in
|
98
|
+
the context of the class defining the property and takes the given value as
|
99
|
+
input. The example below shows how to implement a property that automatically
|
100
|
+
converts all given input to a slug representation.
|
101
|
+
|
102
|
+
class Article
|
103
|
+
property :slug, :converts => lambda { |slug| slug.downcase.gsub(/\s+/, '-').gsub(/\W/, '') }
|
104
|
+
end
|
105
|
+
|
106
|
+
#### Input validation
|
107
|
+
|
108
|
+
To ensure that a given value for a property is always of a certain type, you
|
109
|
+
can specify the `:accepts` configuration parameter. This will result in an
|
110
|
+
automatic validation whenever the setter for a certain property is called. The
|
111
|
+
example below shows how to implement a property which only accepts instances
|
112
|
+
of type `String` as input.
|
113
|
+
|
114
|
+
class Article
|
115
|
+
property :title, :accepts => String
|
116
|
+
end
|
117
|
+
|
118
|
+
Instead of using a class, you can also use a list of permitted values. The
|
119
|
+
example below shows how to implement a property that only accepts `true` or
|
120
|
+
`false` as values.
|
121
|
+
|
122
|
+
class Article
|
123
|
+
property :published, :accepts => [true, false]
|
124
|
+
end
|
125
|
+
|
126
|
+
You can also use a `lambda` statement for input validation if a more complex
|
127
|
+
validation procedure is required. The `lambda` statement is evaluated in the
|
128
|
+
context of the class that defines the property and receives the given value as
|
129
|
+
input. The example below shows how to implement a property called title that
|
130
|
+
only accepts values which match the given regular expression.
|
131
|
+
|
132
|
+
class Article
|
133
|
+
property :title, :accepts => lambda { |title| /^Lorem \w+$/ =~ title }
|
134
|
+
end
|
135
|
+
|
136
|
+
#### Default values
|
137
|
+
|
138
|
+
There is also support for default values. Simply use the `:default`
|
139
|
+
configuration parameter to configure a default value for a certain property.
|
140
|
+
The example below demonstrates how to implement a property that has 42 as
|
141
|
+
default value.
|
142
|
+
|
143
|
+
class Article
|
144
|
+
property :id, :default => 42
|
145
|
+
end
|
146
|
+
|
147
|
+
#### Presence checking
|
148
|
+
|
149
|
+
To ensure that a property is always set set and never `nil`, you can use the
|
150
|
+
`:required` configuration parameter. If present, this parameter will instruct
|
151
|
+
the setter of a property to not accept nil as input. The example below shows
|
152
|
+
how to implement a property that may not be `nil`.
|
153
|
+
|
154
|
+
class Article
|
155
|
+
property :title, :required => true
|
156
|
+
end
|
157
|
+
|
158
|
+
## Contributing
|
159
|
+
|
160
|
+
1. Fork it
|
161
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
162
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
163
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
164
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
##
|
2
|
+
# {SmartProperties} can be used to easily build more full-fledged accessors
|
3
|
+
# for standard Ruby classes. In contrast to regular accessors,
|
4
|
+
# {SmartProperties} support validation and conversion of input data, as well
|
5
|
+
# as, the specification of default values. Additionally, individual
|
6
|
+
# {SmartProperties} can be marked as required. This causes the runtime to
|
7
|
+
# throw an +ArgumentError+ whenever a required property has not been
|
8
|
+
# specified.
|
9
|
+
#
|
10
|
+
# In order to use {SmartProperties}, simply include the {SmartProperties}
|
11
|
+
# module and use the {ClassMethods#property} method to define properties.
|
12
|
+
#
|
13
|
+
# @see ClassMethods#property
|
14
|
+
# More information on how to configure properties
|
15
|
+
#
|
16
|
+
# @example Definition of a property that makes use of all {SmartProperties} features.
|
17
|
+
#
|
18
|
+
# property :language_code, :accepts => [:de, :en],
|
19
|
+
# :converts => :to_sym,
|
20
|
+
# :default => :de,
|
21
|
+
# :required => true
|
22
|
+
#
|
23
|
+
module SmartProperties
|
24
|
+
|
25
|
+
VERSION = "1.0.0"
|
26
|
+
|
27
|
+
class Property
|
28
|
+
|
29
|
+
attr_reader :name
|
30
|
+
attr_reader :default
|
31
|
+
attr_reader :converter
|
32
|
+
attr_reader :accepter
|
33
|
+
|
34
|
+
def initialize(name, attrs = {})
|
35
|
+
@name = name.to_sym
|
36
|
+
@default = attrs[:default]
|
37
|
+
@converter = attrs[:converts]
|
38
|
+
@accepter = attrs[:accepts]
|
39
|
+
@required = !!attrs[:required]
|
40
|
+
end
|
41
|
+
|
42
|
+
def required=(value)
|
43
|
+
@required = !!value
|
44
|
+
end
|
45
|
+
|
46
|
+
def required?
|
47
|
+
@required
|
48
|
+
end
|
49
|
+
|
50
|
+
def convert(value, scope)
|
51
|
+
return value unless converter
|
52
|
+
|
53
|
+
if converter.respond_to?(:call)
|
54
|
+
scope.instance_exec(value, &converter)
|
55
|
+
else
|
56
|
+
begin
|
57
|
+
value.send(:"#{converter}")
|
58
|
+
rescue NoMethodError
|
59
|
+
raise ArgumentError, "#{value.class.name} does not respond to ##{converter}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def accepts?(value, scope)
|
65
|
+
return true unless value
|
66
|
+
return true unless accepter
|
67
|
+
|
68
|
+
if accepter.kind_of?(Enumerable)
|
69
|
+
accepter.include?(value)
|
70
|
+
elsif !accepter.kind_of?(Proc)
|
71
|
+
accepter === value
|
72
|
+
else
|
73
|
+
!!scope.instance_exec(value, &accepter)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def prepare(value, scope)
|
78
|
+
if required? && value.nil?
|
79
|
+
raise ArgumentError, "#{scope.class.name} requires the property #{self.name} to be set"
|
80
|
+
end
|
81
|
+
|
82
|
+
value = convert(value, scope) unless value.nil?
|
83
|
+
|
84
|
+
unless accepts?(value, scope)
|
85
|
+
raise ArgumentError, "#{scope.class.name} does not accept #{value.inspect} as value for the property #{self.name}"
|
86
|
+
end
|
87
|
+
|
88
|
+
@value = value
|
89
|
+
end
|
90
|
+
|
91
|
+
def define(klass)
|
92
|
+
property = self
|
93
|
+
|
94
|
+
scope = klass.instance_variable_get(:"@_smart_properties_method_scope") || begin
|
95
|
+
m = Module.new
|
96
|
+
klass.send(:include, m)
|
97
|
+
klass.instance_variable_set(:"@_smart_properties_method_scope", m)
|
98
|
+
m
|
99
|
+
end
|
100
|
+
|
101
|
+
scope.send(:attr_reader, name)
|
102
|
+
scope.send(:define_method, :"#{name}=") do |value|
|
103
|
+
instance_variable_set("@#{property.name}", property.prepare(value, self))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
module ClassMethods
|
110
|
+
|
111
|
+
##
|
112
|
+
# Returns the list of smart properties that for this class. This
|
113
|
+
# includes the properties than have been defined in the parent classes.
|
114
|
+
#
|
115
|
+
# @return [Array<Property>] The list of properties.
|
116
|
+
#
|
117
|
+
def properties
|
118
|
+
(@_smart_properties || {}).dup
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Defines a new property from a name and a set of options. This results
|
123
|
+
# results in creating an accessor that has additional features:
|
124
|
+
#
|
125
|
+
# 1. Validation of input data by specifiying the +:accepts+ option:
|
126
|
+
# If you use a class as value for this option, the setter will check
|
127
|
+
# if the value it is about to assign is of this type. If you use an
|
128
|
+
# array, the setter will check if the value it is about to assign is
|
129
|
+
# included in this array. Finally, if you specify a block, it will
|
130
|
+
# invoke the block with the value it is about to assign and check if
|
131
|
+
# the block returns a thruthy value, meaning anything but +false+ and
|
132
|
+
# +nil+.
|
133
|
+
#
|
134
|
+
# 2. Conversion of input data by specifiying the +:converts+ option:
|
135
|
+
# If you use provide a symbol as value for this option, the setter will
|
136
|
+
# invoke this method on the object it is about to assign and take the
|
137
|
+
# result of this call instead. If you provide a block, it will invoke
|
138
|
+
# the block with the value it is about to assign and take the result
|
139
|
+
# of the block instead.
|
140
|
+
#
|
141
|
+
# 3. Providing a default value by specifiying the +:default+ option.
|
142
|
+
#
|
143
|
+
# 4. Forcing a property to be present by setting the +:required+ option
|
144
|
+
# to true.
|
145
|
+
#
|
146
|
+
#
|
147
|
+
# @param [Symbol] name the name of the property
|
148
|
+
#
|
149
|
+
# @param [Hash] options the list of options used to configure the property
|
150
|
+
# @option options [Array, Class, Proc] :accepts
|
151
|
+
# specifies how the validation is done
|
152
|
+
# @option options [Proc, Symbol] :converts
|
153
|
+
# specifies how the conversion is done
|
154
|
+
# @option options :default
|
155
|
+
# specifies the default value of the property
|
156
|
+
# @option options [true, false] :required
|
157
|
+
# specifies whether or not this property is required
|
158
|
+
#
|
159
|
+
# @return [Property] The defined property.
|
160
|
+
#
|
161
|
+
# @example Definition of a property that makes use of all {SmartProperties} features.
|
162
|
+
#
|
163
|
+
# property :language_code, :accepts => [:de, :en],
|
164
|
+
# :converts => :to_sym,
|
165
|
+
# :default => :de,
|
166
|
+
# :required => true
|
167
|
+
#
|
168
|
+
def property(name, options = {})
|
169
|
+
@_smart_properties ||= begin
|
170
|
+
parent = if self != SmartProperties
|
171
|
+
(ancestors[1..-1].find { |klass| klass.ancestors.include?(SmartProperties) && klass != SmartProperties })
|
172
|
+
end
|
173
|
+
|
174
|
+
parent ? parent.properties : {}
|
175
|
+
end
|
176
|
+
|
177
|
+
p = Property.new(name, options)
|
178
|
+
p.define(self)
|
179
|
+
|
180
|
+
@_smart_properties[name] = p
|
181
|
+
end
|
182
|
+
protected :property
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
class << self
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
##
|
191
|
+
# Extends the class, which this module is included in, with a property
|
192
|
+
# method to define properties.
|
193
|
+
#
|
194
|
+
# @param [Class] base the class this module is included in
|
195
|
+
#
|
196
|
+
def included(base)
|
197
|
+
base.extend(ClassMethods)
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# Implements a key-value enabled constructor that acts as default
|
204
|
+
# constructor for all {SmartProperties}-enabled classes.
|
205
|
+
#
|
206
|
+
# @param [Hash] attrs the set of attributes that is used for initialization
|
207
|
+
#
|
208
|
+
def initialize(attrs = {})
|
209
|
+
attrs ||= {}
|
210
|
+
|
211
|
+
self.class.properties.each do |_, property|
|
212
|
+
value = attrs.key?(property.name) ? attrs.delete(property.name) : property.default
|
213
|
+
send(:"#{property.name}=", value)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/smart_properties', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Konstantin Tennhard"]
|
6
|
+
gem.email = ["me@t6d.de"]
|
7
|
+
gem.description = <<-DESCRIPTION
|
8
|
+
SmartProperties are a more flexible and feature-rich alternative to
|
9
|
+
traditional Ruby accessors. They provide support for input conversion,
|
10
|
+
input validation, specifying default values and presence checking.
|
11
|
+
DESCRIPTION
|
12
|
+
gem.summary = %q{SmartProperties – Ruby accessors on steroids}
|
13
|
+
gem.homepage = ""
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($\)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.name = "smart_properties"
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
gem.version = SmartProperties::VERSION
|
21
|
+
|
22
|
+
gem.add_development_dependency "rspec", "~> 2.0"
|
23
|
+
gem.add_development_dependency "rake", "~> 0.8"
|
24
|
+
gem.add_development_dependency "yard", "~> 0.8"
|
25
|
+
gem.add_development_dependency "redcarpet", "~> 2.1"
|
26
|
+
gem.add_development_dependency "guard-rspec", "~> 0.7"
|
27
|
+
end
|
@@ -0,0 +1,325 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SmartProperties do
|
4
|
+
|
5
|
+
context "when extending an other class" do
|
6
|
+
|
7
|
+
subject do
|
8
|
+
Class.new.tap do |c|
|
9
|
+
c.send(:include, described_class)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should add a .property method" do
|
14
|
+
subject.should respond_to(:property)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
context "when used to build a class that has a property called :title on a class" do
|
20
|
+
|
21
|
+
subject do
|
22
|
+
title = Object.new.tap do |o|
|
23
|
+
def o.to_title; 'chunky'; end
|
24
|
+
end
|
25
|
+
|
26
|
+
klass = Class.new.tap do |c|
|
27
|
+
c.send(:include, described_class)
|
28
|
+
c.instance_eval do
|
29
|
+
def name; "TestDummy"; end
|
30
|
+
|
31
|
+
property :title, :accepts => String,
|
32
|
+
:converts => :to_title,
|
33
|
+
:required => true,
|
34
|
+
:default => title
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
klass
|
39
|
+
end
|
40
|
+
|
41
|
+
context "instances of this class" do
|
42
|
+
|
43
|
+
klass = subject.call
|
44
|
+
|
45
|
+
subject do
|
46
|
+
klass.new
|
47
|
+
end
|
48
|
+
|
49
|
+
it { should respond_to(:title) }
|
50
|
+
it { should respond_to(:title=) }
|
51
|
+
|
52
|
+
it "should have 'chucky' as default value for title" do
|
53
|
+
subject.title.should be == 'chunky'
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should convert all values that are assigned to title into strings" do
|
57
|
+
subject.title = double(:to_title => 'bacon')
|
58
|
+
subject.title.should be == 'bacon'
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should not allow to set nil as title" do
|
62
|
+
expect { subject.title = nil }.to raise_error(ArgumentError, "TestDummy requires the property title to be set")
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should not allow to set objects as title that do not respond to #to_title" do
|
66
|
+
expect { subject.title = Object.new }.to raise_error(ArgumentError, "Object does not respond to #to_title")
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should not influence other instances that have been initialized with different attributes" do
|
70
|
+
other = klass.new :title => double(:to_title => 'Lorem ipsum')
|
71
|
+
|
72
|
+
subject.title.should be == 'chunky'
|
73
|
+
other.title.should be == 'Lorem ipsum'
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
context "when subclassed and extended with a property called text" do
|
79
|
+
|
80
|
+
superklass = subject.call
|
81
|
+
|
82
|
+
subject do
|
83
|
+
Class.new(superklass).tap do |c|
|
84
|
+
c.instance_eval do
|
85
|
+
property :text
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "instances of this subclass" do
|
91
|
+
|
92
|
+
klass = subject.call
|
93
|
+
|
94
|
+
subject do
|
95
|
+
klass.new
|
96
|
+
end
|
97
|
+
|
98
|
+
it { should respond_to(:title) }
|
99
|
+
it { should respond_to(:title=) }
|
100
|
+
it { should respond_to(:text) }
|
101
|
+
it { should respond_to(:text=) }
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
context "instances of the super class" do
|
106
|
+
|
107
|
+
subject do
|
108
|
+
superklass.new
|
109
|
+
end
|
110
|
+
|
111
|
+
it { should_not respond_to(:text) }
|
112
|
+
it { should_not respond_to(:text=) }
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
context "instances of this subclass that have been intialized from a set of attributes" do
|
117
|
+
|
118
|
+
klass = subject.call
|
119
|
+
|
120
|
+
subject do
|
121
|
+
klass.new :title => stub(:to_title => 'Message'), :text => "Hello"
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should have the correct title" do
|
125
|
+
subject.title.should be == 'Message'
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should have the correct text" do
|
129
|
+
subject.text.should be == 'Hello'
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
context "when extended with a :type property at runtime" do
|
137
|
+
|
138
|
+
klass = subject.call
|
139
|
+
|
140
|
+
subject do
|
141
|
+
klass.tap do |c|
|
142
|
+
c.instance_eval do
|
143
|
+
property :type, :converts => :to_sym
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "instances of this class" do
|
149
|
+
|
150
|
+
klass = subject.call
|
151
|
+
|
152
|
+
subject do
|
153
|
+
klass.new :title => double(:to_title => 'Lorem ipsum')
|
154
|
+
end
|
155
|
+
|
156
|
+
it { should respond_to(:type) }
|
157
|
+
it { should respond_to(:type=) }
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
context "when subclassing this class" do
|
162
|
+
|
163
|
+
superklass = subject.call
|
164
|
+
|
165
|
+
subject do
|
166
|
+
Class.new(superklass)
|
167
|
+
end
|
168
|
+
|
169
|
+
context "instances of this class" do
|
170
|
+
|
171
|
+
klass = subject.call
|
172
|
+
|
173
|
+
subject do
|
174
|
+
klass.new :title => double(:to_title => 'Lorem ipsum')
|
175
|
+
end
|
176
|
+
|
177
|
+
it { should respond_to :title }
|
178
|
+
it { should respond_to :title= }
|
179
|
+
|
180
|
+
it { should respond_to :type }
|
181
|
+
it { should respond_to :type= }
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
context "when used to build a class that has a property called :title which a lambda statement for conversion" do
|
192
|
+
|
193
|
+
subject do
|
194
|
+
Class.new.tap do |c|
|
195
|
+
c.send(:include, described_class)
|
196
|
+
c.instance_eval do
|
197
|
+
property :title, :converts => lambda { |t| "<title>#{t.to_s}</title>"}
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "instances of this class" do
|
203
|
+
|
204
|
+
klass = subject.call
|
205
|
+
|
206
|
+
subject do
|
207
|
+
klass.new
|
208
|
+
end
|
209
|
+
|
210
|
+
it "should convert the property title as specified the lambda statement" do
|
211
|
+
subject.title = "Lorem ipsum"
|
212
|
+
subject.title.should be == "<title>Lorem ipsum</title>"
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
context "when used to build a class that has a property called :visible which uses an array of valid values for acceptance checking" do
|
220
|
+
|
221
|
+
subject do
|
222
|
+
Class.new.tap do |c|
|
223
|
+
def c.name; "TestDummy"; end
|
224
|
+
|
225
|
+
c.send(:include, described_class)
|
226
|
+
|
227
|
+
c.instance_eval do
|
228
|
+
property :visible, :accepts => [true, false]
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
context "instances of this class" do
|
234
|
+
|
235
|
+
klass = subject.call
|
236
|
+
|
237
|
+
subject do
|
238
|
+
klass.new
|
239
|
+
end
|
240
|
+
|
241
|
+
it "should allow to set true as value for visible" do
|
242
|
+
expect { subject.visible = true }.to_not raise_error
|
243
|
+
end
|
244
|
+
|
245
|
+
it "should allow to set false as value for visible" do
|
246
|
+
expect { subject.visible = false }.to_not raise_error
|
247
|
+
end
|
248
|
+
|
249
|
+
it "should not allow to set :maybe as value for visible" do
|
250
|
+
expect { subject.visible = :maybe }.to raise_error(ArgumentError, "TestDummy does not accept :maybe as value for the property visible")
|
251
|
+
end
|
252
|
+
|
253
|
+
end
|
254
|
+
|
255
|
+
end
|
256
|
+
|
257
|
+
context 'when used to build a class that has a property called :license_plate which uses a lambda statement for accpetance checking' do
|
258
|
+
|
259
|
+
subject do
|
260
|
+
Class.new.tap do |c|
|
261
|
+
def c.name; 'TestDummy'; end
|
262
|
+
|
263
|
+
c.send(:include, described_class)
|
264
|
+
|
265
|
+
c.instance_eval do
|
266
|
+
property :license_plate, :accepts => lambda { |v| /\w{1,2} \w{1,2} \d{1,4}/.match(v) }
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
context 'instances of this class' do
|
272
|
+
|
273
|
+
klass = subject.call
|
274
|
+
|
275
|
+
subject do
|
276
|
+
klass.new
|
277
|
+
end
|
278
|
+
|
279
|
+
it 'should not a accept "invalid" as value for license_plate' do
|
280
|
+
expect { subject.license_plate = "invalid" }.to raise_error(ArgumentError, 'TestDummy does not accept "invalid" as value for the property license_plate')
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'should accept "NE RD 1337" as license plate' do
|
284
|
+
expect { subject.license_plate = "NE RD 1337" }.to_not raise_error
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
end
|
290
|
+
|
291
|
+
context 'when used to build a class that has a property called :text whose getter is overriden' do
|
292
|
+
|
293
|
+
subject do
|
294
|
+
Class.new.tap do |c|
|
295
|
+
c.send(:include, described_class)
|
296
|
+
|
297
|
+
c.instance_eval do
|
298
|
+
property :text, :default => 'Hello'
|
299
|
+
end
|
300
|
+
|
301
|
+
c.class_eval do
|
302
|
+
def text
|
303
|
+
"<em>#{super}</em>"
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
context "instances of this class" do
|
310
|
+
|
311
|
+
klass = subject.call
|
312
|
+
|
313
|
+
subject do
|
314
|
+
klass.new
|
315
|
+
end
|
316
|
+
|
317
|
+
it "should return the accepted value for the property called :text" do
|
318
|
+
subject.text.should be == '<em>Hello</em>'
|
319
|
+
end
|
320
|
+
|
321
|
+
end
|
322
|
+
|
323
|
+
end
|
324
|
+
|
325
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: smart_properties
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Konstantin Tennhard
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-28 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '2.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0.8'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0.8'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: yard
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.8'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.8'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: redcarpet
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2.1'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '2.1'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: guard-rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0.7'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0.7'
|
94
|
+
description: ! " SmartProperties are a more flexible and feature-rich alternative
|
95
|
+
to\n traditional Ruby accessors. They provide support for input conversion,\n input
|
96
|
+
validation, specifying default values and presence checking.\n"
|
97
|
+
email:
|
98
|
+
- me@t6d.de
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- .gitignore
|
104
|
+
- .yardopts
|
105
|
+
- Gemfile
|
106
|
+
- Guardfile
|
107
|
+
- LICENSE
|
108
|
+
- README.md
|
109
|
+
- Rakefile
|
110
|
+
- lib/smart_properties.rb
|
111
|
+
- smart_properties.gemspec
|
112
|
+
- spec/smart_properties_spec.rb
|
113
|
+
- spec/spec_helper.rb
|
114
|
+
homepage: ''
|
115
|
+
licenses: []
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
segments:
|
127
|
+
- 0
|
128
|
+
hash: 2007724824068192771
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
segments:
|
136
|
+
- 0
|
137
|
+
hash: 2007724824068192771
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 1.8.24
|
141
|
+
signing_key:
|
142
|
+
specification_version: 3
|
143
|
+
summary: SmartProperties – Ruby accessors on steroids
|
144
|
+
test_files:
|
145
|
+
- spec/smart_properties_spec.rb
|
146
|
+
- spec/spec_helper.rb
|
147
|
+
has_rdoc:
|