attributable 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c22dc4806f8e50155cb39df375de2e883edfb086
4
- data.tar.gz: 5edda9c0de87966920404d14b07a8ca78e395657
3
+ metadata.gz: ff3f791c159b7f77582d20aea0a0a2a608bf0089
4
+ data.tar.gz: 73fbc9b201af71fc14149c134ea87e55d88f01e7
5
5
  SHA512:
6
- metadata.gz: b50d6942f2276f8571991bb33470191cceb7a215bc78d0e5318466249bba357c443a3e244db843b09ae8a582fdade35e03492666f29eecd1eccc43ebb4f02e77
7
- data.tar.gz: ee3fad3f3cffafc5fe94d0ef27c56c590e06cbbcc8708f73aa51fe1e0a429b2179b471a3cea5259df251428d45003265476c2e13abc800f5019f477e6be724c0
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
- ## Specialisation
101
+ ## Reuse via inheritance and mix-ins
102
102
 
103
- To allow reuse of attribute declarations, Attributable provides the `specialises` class method.
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
 
@@ -2,24 +2,22 @@ require "attributable/version"
2
2
 
3
3
  module Attributable
4
4
  def attributes(*without_defaults, **with_defaults)
5
- @predefined_attributes ||= {}
6
- @predefined_attributes = @predefined_attributes.merge(from(without_defaults, with_defaults))
7
- add_instance_methods(@predefined_attributes)
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
- def specialises(clazz)
11
- unless clazz.kind_of? Attributable
12
- fail ArgumentError, "specialisation requires a class that extends Attributable"
13
- end
12
+ private
14
13
 
15
- super_attributes = clazz.new.instance_variable_get(:@attributes)
16
- @predefined_attributes ||= {}
17
- @predefined_attributes = super_attributes.merge(@predefined_attributes)
18
- add_instance_methods(@predefined_attributes)
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(predefined_attributes)
33
- add_constructor(predefined_attributes)
34
- add_accessors(predefined_attributes.keys)
35
- add_equality_methods(predefined_attributes.keys)
36
- add_inspect_method(predefined_attributes.keys)
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(predefined_attributes)
40
- define_method "initialize" do |attributes = {}|
41
- initialize_attributes(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 |attributes = {}|
45
- if self.class.superclass.kind_of? Attributable
46
- super_attributes = self.class.superclass.new.instance_variable_get(:@attributes)
47
- predefined_attributes = super_attributes.merge(predefined_attributes)
48
- end
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
 
@@ -1,3 +1,3 @@
1
1
  module Attributable
2
- VERSION = "0.0.4"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -7,7 +7,7 @@ end
7
7
 
8
8
  describe Attributable do
9
9
  describe "accessors" do
10
- it "should raise for an unknown attribute" do
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 pull in attributes from specified class" do
11
- class SuperUser
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 :password, active: true
15
- specialises User
8
+ attributes :forename, surname: "Bloggs"
9
+ attributes :id, active: true
16
10
  end
17
11
 
18
- s = SuperUser.new(id: 1, forename: "Bob", password: "secret", active: false)
12
+ p = Person.new(id: 1, forename: "Bob")
19
13
 
20
- expect(s.id).to eq(1)
21
- expect(s.forename).to eq("Bob")
22
- expect(s.surname).to eq("Bloggs")
23
- expect(s.password).to eq("secret")
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
- it "should be possible to declare specialisation before attributes" do
28
- class SuperUser2
29
- extend Attributable
21
+ describe "inheritance" do
22
+ class User
23
+ extend Attributable
24
+ attributes :id, :forename, surname: "Bloggs"
25
+ end
30
26
 
31
- specialises User
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 = SuperUser2.new(id: 1, forename: "Bob", password: "secret", active: false)
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 local attributes precede specialised attributes" do
45
- class SuperUser3
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 = SuperUser3.new
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
- attributes surname: "Smith"
62
- specialises User
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
- s = SuperUser4.new
80
+ c = Columnist2.new
66
81
 
67
- expect(s.surname).to eq("Smith")
82
+ expect(c.articles).to eq([])
68
83
  end
69
84
 
70
- it "should raise if specialising class is not an instance of Attributable" do
71
- expect do
72
- class SuperUser5
73
- extend Attributable
74
- specialises String
75
- end
76
- end.to raise_error(
77
- ArgumentError,
78
- "specialisation requires a class that extends Attributable"
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
- describe "automatic specialisation" do
84
- it "should automatically specialise if superclass is an instance of Attributable" do
85
- class SuperUser6 < User
86
- attributes :password, active: true
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
- s = SuperUser6.new(id: 1, forename: "Bob", password: "secret", active: false)
105
+ c = Columnist4.new(column: "Agony Aunt", articles: [:teen_advice])
90
106
 
91
- expect(s.id).to eq(1)
92
- expect(s.forename).to eq("Bob")
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 not override any custom methods" do
99
- class SuperUser7 < User
100
- def inspect
101
- "SUPERUSER"
102
- end
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
- s = SuperUser7.new(id: 1)
118
+ c = Columnist5.new(column: "Agony Aunt", articles: [:teen_advice])
106
119
 
107
- expect(s.id).to eq(1)
108
- expect(s.inspect).to eq("SUPERUSER")
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 "shouldn't automatically specialise unless superclass is an instance of Attributable" do
112
- class PORO
113
- attr_accessor :name
124
+ it "should work with several includes" do
125
+ module Editor
126
+ extend Attributable
127
+ attributes :section
114
128
  end
115
129
 
116
- class SuperUser8 < PORO
130
+ class AuthorAndEditor
131
+ include Author, Editor
117
132
  extend Attributable
118
- attributes :forename, :surname
133
+ attributes :name
119
134
  end
120
135
 
121
- expect { SuperUser8.new }.to_not raise_error
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
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-10 00:00:00.000000000 Z
11
+ date: 2014-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler