representable 0.11.0 → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.textile +7 -0
- data/README.rdoc +60 -36
- data/lib/representable.rb +46 -23
- data/lib/representable/binding.rb +47 -0
- data/lib/representable/bindings/json_bindings.rb +19 -21
- data/lib/representable/bindings/xml_bindings.rb +13 -18
- data/lib/representable/definition.rb +8 -7
- data/lib/representable/json.rb +12 -11
- data/lib/representable/version.rb +1 -1
- data/lib/representable/xml.rb +10 -10
- data/test/definition_test.rb +10 -3
- data/test/json_test.rb +88 -52
- data/test/representable_test.rb +55 -46
- data/test/test_helper.rb +13 -0
- data/test/xml_test.rb +103 -55
- metadata +4 -3
data/CHANGES.textile
CHANGED
@@ -1,6 +1,13 @@
|
|
1
|
+
h2. 0.12.0
|
2
|
+
|
3
|
+
* @:as@ is now @:class@.
|
4
|
+
|
5
|
+
|
1
6
|
h2. 0.11.0
|
2
7
|
|
3
8
|
* Representer modules can now be injected into objects using @#extend@.
|
9
|
+
* The @:extend@ option allows setting a representer module for a typed property. This will extend the contained object at runtime roughly following the DCI pattern.
|
10
|
+
* Renamed @#representable_property@ and @#representable_collection@ to @#property@ and @#collection@ as we don't have to fear namespace collisions in modules.
|
4
11
|
|
5
12
|
h2. 0.10.3
|
6
13
|
|
data/README.rdoc
CHANGED
@@ -14,7 +14,7 @@ This keeps your representation knowledge in one place when implementing REST ser
|
|
14
14
|
|
15
15
|
* Bidirectional - rendering and parsing
|
16
16
|
* OOP documents
|
17
|
-
* Support for JSON and
|
17
|
+
* Support for JSON, XML and MessagePack
|
18
18
|
|
19
19
|
|
20
20
|
== Example
|
@@ -26,33 +26,40 @@ Since you keep forgetting the heroes of your childhood you decide to implement a
|
|
26
26
|
|
27
27
|
== Defining Representations
|
28
28
|
|
29
|
+
Representations are usually defined using a module. This makes them super flexibly, you'll see.
|
30
|
+
|
29
31
|
require 'representable/json'
|
30
32
|
|
31
|
-
|
33
|
+
module HeroRepresenter
|
32
34
|
include Representable::JSON
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
+
property :forename
|
37
|
+
property :surename
|
36
38
|
end
|
37
39
|
|
38
|
-
|
40
|
+
By using #property we declare two simple attributes. Representable will automatically add accessors to the module.
|
39
41
|
|
40
|
-
|
42
|
+
To use your representer include it in the matching class. Note that you could reuse a representer in multiple classes.
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
representable_property :forename
|
46
|
-
representable_property :surename
|
44
|
+
class Hero
|
45
|
+
include Representable
|
46
|
+
include HeroRepresenter
|
47
47
|
end
|
48
48
|
|
49
|
-
|
49
|
+
Many people dislike including representers on class layer. You might also extend an object at runtime.
|
50
|
+
|
51
|
+
Hero.new.extend(HeroRepresenter)
|
52
|
+
|
53
|
+
Alternatively, if you don't like modules (which you shouldn't), declarations can be put into classes directly.
|
50
54
|
|
51
55
|
class Hero
|
52
56
|
include Representable::JSON
|
53
|
-
|
57
|
+
|
58
|
+
property :forename
|
59
|
+
property :surename
|
54
60
|
end
|
55
61
|
|
62
|
+
|
56
63
|
== Rendering
|
57
64
|
|
58
65
|
Now let's create and render our first hero.
|
@@ -77,12 +84,12 @@ See how easy this is? You can use an object-oriented method to read from the doc
|
|
77
84
|
|
78
85
|
== Nesting
|
79
86
|
|
80
|
-
You need a second domain object. Every hero has a place
|
87
|
+
You need a second domain object. Every hero has a place it comes from.
|
81
88
|
|
82
89
|
class Location
|
83
90
|
include Representable::JSON
|
84
91
|
|
85
|
-
|
92
|
+
property :title
|
86
93
|
end
|
87
94
|
|
88
95
|
Peter, where ya' from?
|
@@ -92,11 +99,11 @@ Peter, where ya' from?
|
|
92
99
|
|
93
100
|
It makes sense to embed the location in the hero's document.
|
94
101
|
|
95
|
-
|
96
|
-
|
102
|
+
module HeroRepresenter
|
103
|
+
property :origin, :class => Location
|
97
104
|
end
|
98
105
|
|
99
|
-
Using the +:
|
106
|
+
Using the +:class+ option allows you to include other representable objects.
|
100
107
|
|
101
108
|
peter.origin = neverland
|
102
109
|
peter.to_json
|
@@ -117,26 +124,26 @@ Representable just creates objects from the parsed document - nothing more and n
|
|
117
124
|
|
118
125
|
Heroes have features, special abilities that make 'em a superhero.
|
119
126
|
|
120
|
-
|
121
|
-
|
127
|
+
module HeroRepresenter
|
128
|
+
collection :features
|
122
129
|
end
|
123
130
|
|
124
|
-
The second API method is +
|
131
|
+
The second representable API method is +collection+ and, well, declares a collection.
|
125
132
|
|
126
133
|
peter.features = ["stays young", "can fly"]
|
127
134
|
peter.to_json
|
128
|
-
#=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"},"features":["stays young","can fly"]}
|
135
|
+
#=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"},"features":["stays young","can fly"]}
|
129
136
|
|
130
137
|
|
131
138
|
== Typed Collections
|
132
139
|
|
133
|
-
Ok, things start working out. Your hero has a name, an origin and a list of features so far. Why not
|
140
|
+
Ok, things start working out. Your hero has a name, an origin and a list of features so far. Why not allow adding buddies to Peter - nobody wants to be alone!
|
134
141
|
|
135
|
-
|
136
|
-
|
142
|
+
module HeroRepresenter
|
143
|
+
collection :friends, :class => Hero
|
137
144
|
end
|
138
145
|
|
139
|
-
Again, we type the collection by using the +:
|
146
|
+
Again, we type the collection by using the +:class+ option.
|
140
147
|
|
141
148
|
nick = Hero.new
|
142
149
|
nick.forename = "Nick"
|
@@ -158,7 +165,7 @@ I always wanted to be Peter's bro... in this example it is possible!
|
|
158
165
|
|
159
166
|
Representable is designed to be very simple. However, a few tweaks are available. What if you want to wrap your document?
|
160
167
|
|
161
|
-
|
168
|
+
module HeroRepresenter
|
162
169
|
self.representation_wrap = true
|
163
170
|
end
|
164
171
|
|
@@ -166,7 +173,7 @@ Representable is designed to be very simple. However, a few tweaks are available
|
|
166
173
|
|
167
174
|
You can also provide a custom wrapper.
|
168
175
|
|
169
|
-
|
176
|
+
module HeroRepresenter
|
170
177
|
self.representation_wrap = :boy
|
171
178
|
end
|
172
179
|
|
@@ -177,23 +184,40 @@ You can also provide a custom wrapper.
|
|
177
184
|
|
178
185
|
If your accessor name doesn't match the attribute name in the document, use the +:from+ matcher.
|
179
186
|
|
180
|
-
|
181
|
-
|
187
|
+
module HeroRepresenter
|
188
|
+
property :forename, :from => :i_am_called
|
182
189
|
end
|
183
190
|
|
184
|
-
peter.to_json #=> {"
|
191
|
+
peter.to_json #=> {"i_am_called":"Peter","surename":"Pan"}
|
185
192
|
|
186
193
|
|
187
194
|
=== Filtering
|
188
195
|
|
189
|
-
Representable allows you to skip properties when rendering or parsing.
|
190
|
-
|
191
|
-
peter.to_json do |name|
|
192
|
-
name == :forename
|
193
|
-
end
|
196
|
+
Representable allows you to skip and include properties when rendering or parsing.
|
194
197
|
|
198
|
+
peter.to_json(:include => :forename)
|
195
199
|
#=> {"forename":"Peter"}
|
196
200
|
|
201
|
+
It gives you convenient +:exclude+ and +:include+ options.
|
202
|
+
|
203
|
+
|
204
|
+
== DCI
|
205
|
+
|
206
|
+
Representers roughly follow the {DCI}[http://en.wikipedia.org/wiki/Data,_context_and_interaction] pattern when used on objects, only.
|
207
|
+
|
208
|
+
Hero.new.extend(HeroRepresenter)
|
209
|
+
|
210
|
+
The only difference is that you have to define which representers to use for typed properties.
|
211
|
+
|
212
|
+
module HeroRepresenter
|
213
|
+
property :forename
|
214
|
+
property :surename
|
215
|
+
collection :features
|
216
|
+
property :origin, :class => Location
|
217
|
+
collection :friends, :class => Hero, :extend => HeroRepresenter
|
218
|
+
end
|
219
|
+
|
220
|
+
There's no need to specify a representer for the +origin+ property since the +Location+ class statically includes its representation. For +friends+, we can use +:extend+ to tell representable which module to mix in dynamically.
|
197
221
|
|
198
222
|
== XML support
|
199
223
|
|
data/lib/representable.rb
CHANGED
@@ -3,6 +3,7 @@ require 'representable/definition'
|
|
3
3
|
module Representable
|
4
4
|
def self.included(base)
|
5
5
|
base.class_eval do
|
6
|
+
extend ClassMethods
|
6
7
|
extend ClassMethods::Declarations
|
7
8
|
extend ClassMethods::Accessors
|
8
9
|
|
@@ -21,9 +22,9 @@ module Representable
|
|
21
22
|
end
|
22
23
|
|
23
24
|
# Reads values from +doc+ and sets properties accordingly.
|
24
|
-
def update_properties_from(doc, &block)
|
25
|
+
def update_properties_from(doc, options, &block)
|
25
26
|
representable_bindings.each do |bin|
|
26
|
-
next if
|
27
|
+
next if skip_property?(bin, options)
|
27
28
|
|
28
29
|
value = bin.read(doc) || bin.definition.default
|
29
30
|
send(bin.definition.setter, value)
|
@@ -33,9 +34,9 @@ module Representable
|
|
33
34
|
|
34
35
|
private
|
35
36
|
# Compiles the document going through all properties.
|
36
|
-
def create_representation_with(doc, &block)
|
37
|
+
def create_representation_with(doc, options, &block)
|
37
38
|
representable_bindings.each do |bin|
|
38
|
-
next if
|
39
|
+
next if skip_property?(bin, options)
|
39
40
|
|
40
41
|
value = send(bin.definition.getter) || bin.definition.default # DISCUSS: eventually move back to Ref.
|
41
42
|
bin.write(doc, value) if value
|
@@ -43,11 +44,11 @@ private
|
|
43
44
|
doc
|
44
45
|
end
|
45
46
|
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
# Checks and returns if the property should be included.
|
48
|
+
def skip_property?(binding, options)
|
49
|
+
return unless props = options[:except] || options[:include]
|
50
|
+
res = props.include?(binding.definition.name.to_sym)
|
51
|
+
options[:include] ? !res : res
|
51
52
|
end
|
52
53
|
|
53
54
|
def representable_attrs
|
@@ -65,6 +66,13 @@ private
|
|
65
66
|
|
66
67
|
|
67
68
|
module ClassMethods # :nodoc:
|
69
|
+
# Create and yield object and options. Called in .from_json and friends.
|
70
|
+
def create_represented(document, *args)
|
71
|
+
new.tap do |represented|
|
72
|
+
yield represented, *args if block_given?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
68
76
|
module Declarations
|
69
77
|
def definition_class
|
70
78
|
Definition
|
@@ -74,13 +82,13 @@ private
|
|
74
82
|
#
|
75
83
|
# Examples:
|
76
84
|
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
def
|
83
|
-
attr =
|
85
|
+
# property :name
|
86
|
+
# property :name, :from => :title
|
87
|
+
# property :name, :class => Name
|
88
|
+
# property :name, :accessors => false
|
89
|
+
# property :name, :default => "Mike"
|
90
|
+
def property(name, options={})
|
91
|
+
attr = add_property(name, options)
|
84
92
|
|
85
93
|
attr_reader(attr.getter) unless options[:accessors] == false
|
86
94
|
attr_writer(attr.getter) unless options[:accessors] == false
|
@@ -90,22 +98,23 @@ private
|
|
90
98
|
#
|
91
99
|
# Examples:
|
92
100
|
#
|
93
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
def
|
101
|
+
# collection :products
|
102
|
+
# collection :products, :from => :item
|
103
|
+
# collection :products, :class => Product
|
104
|
+
def collection(name, options={})
|
97
105
|
options[:collection] = true
|
98
|
-
|
106
|
+
property(name, options)
|
99
107
|
end
|
100
108
|
|
101
109
|
private
|
102
|
-
def
|
110
|
+
def add_property(*args)
|
103
111
|
definition_class.new(*args).tap do |attr|
|
104
112
|
representable_attrs << attr
|
105
113
|
end
|
106
114
|
end
|
107
115
|
end
|
108
|
-
|
116
|
+
|
117
|
+
|
109
118
|
module Accessors
|
110
119
|
def representable_attrs
|
111
120
|
@representable_attrs ||= Config.new
|
@@ -117,6 +126,7 @@ private
|
|
117
126
|
end
|
118
127
|
end
|
119
128
|
|
129
|
+
|
120
130
|
class Config < Array
|
121
131
|
attr_accessor :wrap
|
122
132
|
|
@@ -135,4 +145,17 @@ private
|
|
135
145
|
downcase
|
136
146
|
end
|
137
147
|
end
|
148
|
+
|
149
|
+
|
150
|
+
# Allows mapping formats to representer classes.
|
151
|
+
# DISCUSS: this module might be removed soon.
|
152
|
+
module Represents
|
153
|
+
def represents(format, options)
|
154
|
+
representer[format] = options[:with]
|
155
|
+
end
|
156
|
+
|
157
|
+
def representer
|
158
|
+
@represents_map ||= {}
|
159
|
+
end
|
160
|
+
end
|
138
161
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Representable
|
2
|
+
class Binding
|
3
|
+
attr_reader :definition
|
4
|
+
|
5
|
+
def initialize(definition)
|
6
|
+
@definition = definition
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
# Usually called in concrete ObjectBinding in #write and #read.
|
11
|
+
module Hooks
|
12
|
+
private
|
13
|
+
# Must be called in serialization of concrete ObjectBinding.
|
14
|
+
def write_object(object)
|
15
|
+
object
|
16
|
+
end
|
17
|
+
|
18
|
+
# Creates a typed property instance.
|
19
|
+
def create_object
|
20
|
+
definition.sought_type.new
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Hooks into #write_object and #create_object to extend typed properties
|
26
|
+
# at runtime.
|
27
|
+
module Extend
|
28
|
+
private
|
29
|
+
# Extends the object with its representer before serialization.
|
30
|
+
def write_object(object)
|
31
|
+
extend_for(super)
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_object
|
35
|
+
extend_for(super)
|
36
|
+
end
|
37
|
+
|
38
|
+
def extend_for(object) # TODO: test me.
|
39
|
+
if mod = definition.representer_module
|
40
|
+
object.extend(mod)
|
41
|
+
end
|
42
|
+
|
43
|
+
object
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -1,16 +1,8 @@
|
|
1
|
+
require 'representable/binding'
|
2
|
+
|
1
3
|
module Representable
|
2
4
|
module JSON
|
3
|
-
class Binding
|
4
|
-
attr_reader :definition
|
5
|
-
|
6
|
-
def initialize(definition)
|
7
|
-
@definition = definition
|
8
|
-
end
|
9
|
-
|
10
|
-
def read(hash)
|
11
|
-
value_from_hash(hash)
|
12
|
-
end
|
13
|
-
|
5
|
+
class Binding < Representable::Binding
|
14
6
|
private
|
15
7
|
def collect_for(hash)
|
16
8
|
nodes = hash[definition.from] or return
|
@@ -27,9 +19,8 @@ module Representable
|
|
27
19
|
def write(hash, value)
|
28
20
|
hash[definition.from] = value
|
29
21
|
end
|
30
|
-
|
31
|
-
|
32
|
-
def value_from_hash(hash)
|
22
|
+
|
23
|
+
def read(hash)
|
33
24
|
collect_for(hash) do |value|
|
34
25
|
value
|
35
26
|
end
|
@@ -38,20 +29,27 @@ module Representable
|
|
38
29
|
|
39
30
|
# Represents a tag with object binding.
|
40
31
|
class ObjectBinding < Binding
|
41
|
-
|
32
|
+
include Representable::Binding::Hooks # includes #create_object and #write_object.
|
33
|
+
include Representable::Binding::Extend
|
34
|
+
|
35
|
+
def write(hash, object)
|
42
36
|
if definition.array?
|
43
|
-
hash[definition.from] =
|
37
|
+
hash[definition.from] = object.collect { |obj| serialize(obj) }
|
44
38
|
else
|
45
|
-
hash[definition.from] =
|
39
|
+
hash[definition.from] = serialize(object)
|
46
40
|
end
|
47
41
|
end
|
48
|
-
|
49
|
-
|
50
|
-
def value_from_hash(hash)
|
42
|
+
|
43
|
+
def read(hash)
|
51
44
|
collect_for(hash) do |node|
|
52
|
-
|
45
|
+
create_object.from_hash(node)
|
53
46
|
end
|
54
47
|
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def serialize(object)
|
51
|
+
write_object(object).to_hash(:wrap => false)
|
52
|
+
end
|
55
53
|
end
|
56
54
|
end
|
57
55
|
end
|