attributable 0.0.4 → 0.1.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 +31 -9
- data/lib/attributable.rb +24 -27
- data/lib/attributable/version.rb +1 -1
- data/spec/attributable/accessors_spec.rb +1 -1
- data/spec/attributable/construction_spec.rb +17 -11
- data/spec/attributable/specialisation_spec.rb +119 -69
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff3f791c159b7f77582d20aea0a0a2a608bf0089
|
4
|
+
data.tar.gz: 73fbc9b201af71fc14149c134ea87e55d88f01e7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86a48e5ec8a1e34b2c3535194a947d4a9f4ec4fdc0d571873cac0586392c0840c63f61fd5067273e31254db98f9828aa07731dea69d61626a022f6428cf9f451
|
7
|
+
data.tar.gz: 667d00b4f7c7bff03e151da759f2d6e75e773154627ec39a84f7c6e9e47159c779365935e813b2fd87e6a305bbf4db5155722f97e59bc86daedd78e333803ea0
|
data/README.md
CHANGED
@@ -98,31 +98,53 @@ Note that, by default, Attributable adds the following `initialize` method:
|
|
98
98
|
initialize_attributes(attributes)
|
99
99
|
end
|
100
100
|
|
101
|
-
##
|
101
|
+
## Reuse via inheritance and mix-ins
|
102
102
|
|
103
|
-
To
|
103
|
+
To reuse attribute declarations, either user Ruby's built-in inheritance, mix-ins, or both:
|
104
104
|
|
105
|
+
class Author < User
|
106
|
+
attributes blogs: []
|
107
|
+
end
|
108
|
+
|
109
|
+
ronson = Author.new(forename: "Jon", surname: "Ronson")
|
110
|
+
ronson.inspect # => <Author forename="Jon", surname="Ronson", blogs=[]>
|
111
|
+
|
112
|
+
The default values defined in superclasses or mixed-in modules can be changed:
|
113
|
+
|
114
|
+
class Ronson < User
|
115
|
+
attributes surname: "Ronson"
|
116
|
+
end
|
117
|
+
|
118
|
+
Ronson.new(forename: "Jon").inspect # <Ronson forename="Jon", surname="Ronson">
|
119
|
+
Ronson.new(forename: "Mark").inspect # <Ronson forename="Mark", surname="Ronson">
|
120
|
+
|
121
|
+
Here's the same example, but using a module:
|
122
|
+
|
123
|
+
module User
|
124
|
+
extend Attributable
|
125
|
+
|
126
|
+
attributes :forename, :surname
|
127
|
+
end
|
128
|
+
|
105
129
|
class Author
|
130
|
+
include User
|
106
131
|
extend Attributable
|
107
|
-
|
108
|
-
specialises User
|
109
132
|
attributes blogs: []
|
110
133
|
end
|
111
134
|
|
112
135
|
ronson = Author.new(forename: "Jon", surname: "Ronson")
|
113
136
|
ronson.inspect # => <Author forename="Jon", surname="Ronson", blogs=[]>
|
114
137
|
|
115
|
-
Specialising classes can override the defaults set in specialised classes.
|
116
|
-
|
117
138
|
class Ronson
|
139
|
+
include User
|
118
140
|
extend Attributable
|
119
|
-
|
120
|
-
specialises User
|
121
141
|
attributes surname: "Ronson"
|
122
142
|
end
|
123
|
-
|
143
|
+
|
124
144
|
Ronson.new(forename: "Jon").inspect # <Ronson forename="Jon", surname="Ronson">
|
125
145
|
Ronson.new(forename: "Mark").inspect # <Ronson forename="Mark", surname="Ronson">
|
146
|
+
|
147
|
+
__Note__: the `include` must occur before any call to `attributes`.
|
126
148
|
|
127
149
|
### Automatic specialisation for superclasses
|
128
150
|
|
data/lib/attributable.rb
CHANGED
@@ -2,24 +2,22 @@ require "attributable/version"
|
|
2
2
|
|
3
3
|
module Attributable
|
4
4
|
def attributes(*without_defaults, **with_defaults)
|
5
|
-
@
|
6
|
-
@
|
7
|
-
|
5
|
+
@attributes ||= {}
|
6
|
+
@attributes.merge!(attributes_from(superclass)) if respond_to?(:superclass)
|
7
|
+
@attributes.merge!(attributes_from(*included_modules))
|
8
|
+
@attributes.merge!(from(without_defaults, with_defaults))
|
9
|
+
add_instance_methods(@attributes)
|
8
10
|
end
|
9
11
|
|
10
|
-
|
11
|
-
unless clazz.kind_of? Attributable
|
12
|
-
fail ArgumentError, "specialisation requires a class that extends Attributable"
|
13
|
-
end
|
12
|
+
private
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
def attributes_from(*modules)
|
15
|
+
modules
|
16
|
+
.select { |m| m.kind_of?(Attributable) }
|
17
|
+
.map { |m| m.instance_variable_get(:@attributes) }
|
18
|
+
.reduce({}, &:merge)
|
19
19
|
end
|
20
20
|
|
21
|
-
private
|
22
|
-
|
23
21
|
# Converts a list of attribute names and a hash of attribute names to default values
|
24
22
|
# to a hash of attribute names to default values
|
25
23
|
def from(without_defaults, with_defaults)
|
@@ -29,24 +27,23 @@ module Attributable
|
|
29
27
|
end
|
30
28
|
end
|
31
29
|
|
32
|
-
def add_instance_methods(
|
33
|
-
add_constructor(
|
34
|
-
add_accessors(
|
35
|
-
add_equality_methods(
|
36
|
-
add_inspect_method(
|
30
|
+
def add_instance_methods(attributes)
|
31
|
+
add_constructor(attributes)
|
32
|
+
add_accessors(attributes.keys)
|
33
|
+
add_equality_methods(attributes.keys)
|
34
|
+
add_inspect_method(attributes.keys)
|
37
35
|
end
|
38
36
|
|
39
|
-
def add_constructor(
|
40
|
-
define_method "initialize" do |
|
41
|
-
initialize_attributes(
|
37
|
+
def add_constructor(attributes)
|
38
|
+
define_method "initialize" do |values = {}|
|
39
|
+
initialize_attributes(values)
|
42
40
|
end
|
43
41
|
|
44
|
-
define_method "initialize_attributes" do |
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
@attributes = predefined_attributes.merge(attributes)
|
42
|
+
define_method "initialize_attributes" do |values = {}|
|
43
|
+
unknown_keys = values.keys - attributes.keys
|
44
|
+
fail KeyError, "Unknown attributes: #{(unknown_keys).join(", ")}" unless unknown_keys.empty?
|
45
|
+
|
46
|
+
@attributes = attributes.merge(values)
|
50
47
|
end
|
51
48
|
end
|
52
49
|
|
data/lib/attributable/version.rb
CHANGED
@@ -7,7 +7,7 @@ end
|
|
7
7
|
|
8
8
|
describe Attributable do
|
9
9
|
describe "accessors" do
|
10
|
-
it "should
|
10
|
+
it "should not have an accessor for an unknown attribute" do
|
11
11
|
i = User.new(id: 1, forename: "John", surname: "Doe")
|
12
12
|
expect(i.respond_to?(:address)).to be_false
|
13
13
|
end
|
@@ -5,17 +5,6 @@ class User
|
|
5
5
|
attributes :id, :forename, surname: "Bloggs"
|
6
6
|
end
|
7
7
|
|
8
|
-
class UserWithDerivedAttribute
|
9
|
-
extend Attributable
|
10
|
-
attributes :id, :forename, surname: "Bloggs"
|
11
|
-
attr_reader :fullname
|
12
|
-
|
13
|
-
def initialize(attributes = {})
|
14
|
-
initialize_attributes(attributes)
|
15
|
-
@fullname = "#{forename} #{surname}"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
8
|
describe Attributable do
|
20
9
|
describe "construction" do
|
21
10
|
it "should accept a hash" do
|
@@ -41,10 +30,27 @@ describe Attributable do
|
|
41
30
|
expect(i.forename).to be_nil
|
42
31
|
expect(i.surname).to eq("Doe")
|
43
32
|
end
|
33
|
+
|
34
|
+
it "should raise error when values for unknown attributes are specified" do
|
35
|
+
expect { User.new(password: "secret", admin: true, surname: "Doe") }.to(
|
36
|
+
raise_error(KeyError, "Unknown attributes: password, admin")
|
37
|
+
)
|
38
|
+
end
|
44
39
|
end
|
45
40
|
|
46
41
|
describe "constructor" do
|
47
42
|
it "should be overridable" do
|
43
|
+
class UserWithDerivedAttribute
|
44
|
+
extend Attributable
|
45
|
+
attributes :id, :forename, surname: "Bloggs"
|
46
|
+
attr_reader :fullname
|
47
|
+
|
48
|
+
def initialize(attributes = {})
|
49
|
+
initialize_attributes(attributes)
|
50
|
+
@fullname = "#{forename} #{surname}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
48
54
|
i = UserWithDerivedAttribute.new(id: 1, forename: "John", surname: "Doe")
|
49
55
|
|
50
56
|
expect(i.id).to eq(1)
|
@@ -1,38 +1,35 @@
|
|
1
1
|
require "attributable"
|
2
2
|
|
3
|
-
class User
|
4
|
-
extend Attributable
|
5
|
-
attributes :id, :forename, surname: "Bloggs"
|
6
|
-
end
|
7
|
-
|
8
3
|
describe Attributable do
|
9
|
-
describe "specialisation" do
|
10
|
-
it "should
|
11
|
-
class
|
4
|
+
describe "prerequisites for specialisation" do
|
5
|
+
it "should be possible to call attributes more than once" do
|
6
|
+
class Person
|
12
7
|
extend Attributable
|
13
|
-
|
14
|
-
attributes :
|
15
|
-
specialises User
|
8
|
+
attributes :forename, surname: "Bloggs"
|
9
|
+
attributes :id, active: true
|
16
10
|
end
|
17
11
|
|
18
|
-
|
12
|
+
p = Person.new(id: 1, forename: "Bob")
|
19
13
|
|
20
|
-
expect(
|
21
|
-
expect(
|
22
|
-
expect(
|
23
|
-
expect(
|
24
|
-
expect(s.active).to be_false
|
14
|
+
expect(p.id).to eq(1)
|
15
|
+
expect(p.forename).to eq("Bob")
|
16
|
+
expect(p.surname).to eq("Bloggs")
|
17
|
+
expect(p.active).to be_true
|
25
18
|
end
|
19
|
+
end
|
26
20
|
|
27
|
-
|
28
|
-
|
29
|
-
|
21
|
+
describe "inheritance" do
|
22
|
+
class User
|
23
|
+
extend Attributable
|
24
|
+
attributes :id, :forename, surname: "Bloggs"
|
25
|
+
end
|
30
26
|
|
31
|
-
|
27
|
+
it "should inherit attributes from superclass" do
|
28
|
+
class SuperUser < User
|
32
29
|
attributes :password, active: true
|
33
30
|
end
|
34
31
|
|
35
|
-
s =
|
32
|
+
s = SuperUser.new(id: 1, forename: "Bob", password: "secret", active: false)
|
36
33
|
|
37
34
|
expect(s.id).to eq(1)
|
38
35
|
expect(s.forename).to eq("Bob")
|
@@ -41,84 +38,137 @@ describe Attributable do
|
|
41
38
|
expect(s.active).to be_false
|
42
39
|
end
|
43
40
|
|
44
|
-
it "should ensure that
|
45
|
-
class
|
46
|
-
extend Attributable
|
47
|
-
|
48
|
-
specialises User
|
41
|
+
it "should ensure that subclass's attributes precede superclass's" do
|
42
|
+
class SuperUser2 < User
|
49
43
|
attributes surname: "Smith"
|
50
44
|
end
|
51
45
|
|
52
|
-
s =
|
46
|
+
s = SuperUser2.new
|
53
47
|
|
54
48
|
expect(s.surname).to eq("Smith")
|
55
49
|
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "mixins" do
|
53
|
+
module Author
|
54
|
+
extend Attributable
|
55
|
+
attributes articles: []
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should inherit attributes from mixed-in module" do
|
59
|
+
class Columnist
|
60
|
+
include Author
|
56
61
|
|
57
|
-
it "should ensure that position of specialises does not affect precedence" do
|
58
|
-
class SuperUser4
|
59
62
|
extend Attributable
|
63
|
+
attributes :column
|
64
|
+
end
|
60
65
|
|
61
|
-
|
62
|
-
|
66
|
+
c = Columnist.new(column: "Agony Aunt", articles: [:teen_advice])
|
67
|
+
|
68
|
+
expect(c.column).to eq("Agony Aunt")
|
69
|
+
expect(c.articles).to eq([:teen_advice])
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should inherit defaults from mixed-in module" do
|
73
|
+
class Columnist2
|
74
|
+
include Author
|
75
|
+
|
76
|
+
extend Attributable
|
77
|
+
attributes :column
|
63
78
|
end
|
64
79
|
|
65
|
-
|
80
|
+
c = Columnist2.new
|
66
81
|
|
67
|
-
expect(
|
82
|
+
expect(c.articles).to eq([])
|
68
83
|
end
|
69
84
|
|
70
|
-
it "should
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
77
|
-
|
78
|
-
|
79
|
-
|
85
|
+
it "should ensure that includee's attributes precede included's" do
|
86
|
+
class Columnist3
|
87
|
+
include Author
|
88
|
+
|
89
|
+
extend Attributable
|
90
|
+
attributes articles: [:daily_column]
|
91
|
+
end
|
92
|
+
|
93
|
+
c = Columnist3.new
|
94
|
+
|
95
|
+
expect(c.articles).to eq([:daily_column])
|
80
96
|
end
|
81
|
-
end
|
82
97
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
attributes :
|
98
|
+
xit "should work when include is last" do
|
99
|
+
class Columnist4
|
100
|
+
extend Attributable
|
101
|
+
attributes :column
|
102
|
+
include Author
|
87
103
|
end
|
88
104
|
|
89
|
-
|
105
|
+
c = Columnist4.new(column: "Agony Aunt", articles: [:teen_advice])
|
90
106
|
|
91
|
-
expect(
|
92
|
-
expect(
|
93
|
-
expect(s.surname).to eq("Bloggs")
|
94
|
-
expect(s.password).to eq("secret")
|
95
|
-
expect(s.active).to be_false
|
107
|
+
expect(c.articles).to eq([:teen_advice])
|
108
|
+
expect(c).to eq(Columnist4.new(column: "Agony Aunt", articles: [:teen_advice]))
|
96
109
|
end
|
97
110
|
|
98
|
-
it "should
|
99
|
-
class
|
100
|
-
|
101
|
-
|
102
|
-
|
111
|
+
it "should work when include is in the middle" do
|
112
|
+
class Columnist5
|
113
|
+
extend Attributable
|
114
|
+
include Author
|
115
|
+
attributes :column
|
103
116
|
end
|
104
117
|
|
105
|
-
|
118
|
+
c = Columnist5.new(column: "Agony Aunt", articles: [:teen_advice])
|
106
119
|
|
107
|
-
expect(
|
108
|
-
expect(
|
120
|
+
expect(c.articles).to eq([:teen_advice])
|
121
|
+
expect(c).to eq(Columnist5.new(column: "Agony Aunt", articles: [:teen_advice]))
|
109
122
|
end
|
110
123
|
|
111
|
-
it "
|
112
|
-
|
113
|
-
|
124
|
+
it "should work with several includes" do
|
125
|
+
module Editor
|
126
|
+
extend Attributable
|
127
|
+
attributes :section
|
114
128
|
end
|
115
129
|
|
116
|
-
class
|
130
|
+
class AuthorAndEditor
|
131
|
+
include Author, Editor
|
117
132
|
extend Attributable
|
118
|
-
attributes :
|
133
|
+
attributes :name
|
119
134
|
end
|
120
135
|
|
121
|
-
|
136
|
+
ae = AuthorAndEditor.new(name: "Joe Bloggs", section: "Sport")
|
137
|
+
|
138
|
+
expect(ae.name).to eq("Joe Bloggs")
|
139
|
+
expect(ae.section).to eq("Sport")
|
140
|
+
expect(ae.articles).to eq([])
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "inheritance and mixins" do
|
145
|
+
class Blogger < User
|
146
|
+
include Author
|
147
|
+
extend Attributable
|
148
|
+
attributes :blog_name
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should inherit attributes from both" do
|
152
|
+
b = Blogger.new(
|
153
|
+
id: 1,
|
154
|
+
forename: "John",
|
155
|
+
surname: "Doe",
|
156
|
+
articles: [:daily_column],
|
157
|
+
blog_name: "Research Rants"
|
158
|
+
)
|
159
|
+
|
160
|
+
expect(b.id).to eq(1)
|
161
|
+
expect(b.forename).to eq("John")
|
162
|
+
expect(b.surname).to eq("Doe")
|
163
|
+
expect(b.articles).to eq([:daily_column])
|
164
|
+
expect(b.blog_name).to eq("Research Rants")
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should inherit defaults from both" do
|
168
|
+
b = Blogger.new
|
169
|
+
|
170
|
+
expect(b.surname).to eq("Bloggs")
|
171
|
+
expect(b.articles).to eq([])
|
122
172
|
end
|
123
173
|
end
|
124
174
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attributable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Louis Rose
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-04-
|
11
|
+
date: 2014-04-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|