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