representable 0.11.0 → 0.12.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/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
|