representable 1.0.1 → 1.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.
- data/CHANGES.textile +10 -0
- data/README.rdoc +34 -0
- data/lib/representable.rb +5 -0
- data/lib/representable/binding.rb +12 -15
- data/lib/representable/bindings/json_bindings.rb +54 -34
- data/lib/representable/bindings/xml_bindings.rb +96 -59
- data/lib/representable/definition.rb +30 -30
- data/lib/representable/json.rb +3 -2
- data/lib/representable/json/collection.rb +38 -0
- data/lib/representable/json/hash.rb +38 -0
- data/lib/representable/version.rb +1 -1
- data/lib/representable/xml.rb +4 -3
- data/lib/representable/xml/collection.rb +38 -0
- data/lib/representable/xml/hash.rb +38 -0
- data/test/definition_test.rb +23 -24
- data/test/json_bindings_test.rb +119 -0
- data/test/json_test.rb +127 -3
- data/test/polymorphic_test.rb +45 -0
- data/test/representable_test.rb +0 -1
- data/test/test_helper.rb +4 -0
- data/test/xml_bindings_test.rb +153 -0
- data/test/xml_test.rb +10 -5
- metadata +109 -73
- data/test/bindings_test.rb +0 -16
data/CHANGES.textile
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
h2. 1.1.0
|
2
|
+
|
3
|
+
* Added `JSON::Collection` to have plain list representations. And `JSON::Hash` for hashes.
|
4
|
+
* Added the `hash` class method to XML and JSON to represent hashes.
|
5
|
+
* Defining `:extend` only on a property now works for rendering. If you try parsing without a `:class` there'll be an exception, though.
|
6
|
+
|
7
|
+
h2. 1.0.1
|
8
|
+
|
9
|
+
* Allow passing a list of modules to :extend, like @:extend => [Ingredient, IngredientRepresenter]@.
|
10
|
+
|
1
11
|
h2. 1.0.0
|
2
12
|
|
3
13
|
* 1.0.0 release! Party time!
|
data/README.rdoc
CHANGED
@@ -165,6 +165,40 @@ I always wanted to be Peter's bro... in this example it is possible!
|
|
165
165
|
#=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"},"features":["stays young","can fly"],"friends":[{"name":"Nick"},{"name":"El"}]}
|
166
166
|
|
167
167
|
|
168
|
+
== Hashes
|
169
|
+
|
170
|
+
Hashes can be represented the same way collections work. Here, use the #hash class method.
|
171
|
+
|
172
|
+
== Lonely Collections
|
173
|
+
|
174
|
+
Need an array represented without any wrapping?
|
175
|
+
|
176
|
+
["stays young", "can fly"].extend(Representable::JSON::Collection).to_json
|
177
|
+
#=> "[\"stays young\", \"can fly\"]"
|
178
|
+
|
179
|
+
You can use #items to configure the element representations contained in the array.
|
180
|
+
|
181
|
+
module FeaturesRepresenter
|
182
|
+
include Representable::JSON::Collection
|
183
|
+
|
184
|
+
items :class => Hero, :extend => HeroRepresenter
|
185
|
+
end
|
186
|
+
|
187
|
+
Collections and hashes can also be deserialized.
|
188
|
+
|
189
|
+
== Lonely Hashes
|
190
|
+
|
191
|
+
The same goes with hashes where #values lets you configure the hash's values.
|
192
|
+
|
193
|
+
module FriendsRepresenter
|
194
|
+
include Representable::JSON::Hash
|
195
|
+
|
196
|
+
values :class => Hero, :extend => HeroRepresenter
|
197
|
+
end
|
198
|
+
|
199
|
+
{:stu => Hero.new("Stu"), :clive => Hero.new("Cleavage")}.extend(FriendsRepresenter).to_json
|
200
|
+
|
201
|
+
|
168
202
|
== Customizing
|
169
203
|
|
170
204
|
=== Wrapping
|
data/lib/representable.rb
CHANGED
@@ -6,38 +6,35 @@ module Representable
|
|
6
6
|
@definition = definition
|
7
7
|
end
|
8
8
|
|
9
|
-
|
10
|
-
# Usually called in concrete ObjectBinding in #write and #read.
|
9
|
+
# Main entry point for rendering/parsing a property object.
|
11
10
|
module Hooks
|
12
|
-
|
13
|
-
|
14
|
-
def write_object(object)
|
15
|
-
object
|
11
|
+
def serialize(value)
|
12
|
+
value
|
16
13
|
end
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
definition.sought_type.new
|
15
|
+
def deserialize(fragment)
|
16
|
+
fragment
|
21
17
|
end
|
22
18
|
end
|
23
19
|
|
20
|
+
include Hooks
|
21
|
+
|
24
22
|
|
25
|
-
# Hooks into #
|
23
|
+
# Hooks into #serialize and #deserialize to extend typed properties
|
26
24
|
# at runtime.
|
27
25
|
module Extend
|
28
|
-
private
|
29
26
|
# Extends the object with its representer before serialization.
|
30
|
-
def
|
27
|
+
def serialize(object)
|
31
28
|
extend_for(super)
|
32
29
|
end
|
33
30
|
|
34
|
-
def
|
31
|
+
def deserialize(*)
|
35
32
|
extend_for(super)
|
36
33
|
end
|
37
34
|
|
38
|
-
def extend_for(object)
|
35
|
+
def extend_for(object)
|
39
36
|
if mod = definition.representer_module
|
40
|
-
object.extend(mod)
|
37
|
+
object.extend(*mod)
|
41
38
|
end
|
42
39
|
|
43
40
|
object
|
@@ -2,53 +2,73 @@ require 'representable/binding'
|
|
2
2
|
|
3
3
|
module Representable
|
4
4
|
module JSON
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
5
|
+
module ObjectBinding
|
6
|
+
# TODO: provide a base ObjectBinding for XML/JSON/MP.
|
7
|
+
include Binding::Extend # provides #serialize/#deserialize with extend.
|
8
|
+
|
9
|
+
def serialize(object)
|
10
|
+
super(object).to_hash(:wrap => false)
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(hash)
|
14
|
+
super(create_object).from_hash(hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_object
|
18
|
+
definition.sought_type.new
|
14
19
|
end
|
15
20
|
end
|
16
21
|
|
17
|
-
|
18
|
-
class
|
19
|
-
def
|
20
|
-
|
22
|
+
|
23
|
+
class JSONBinding < Representable::Binding
|
24
|
+
def initialize(definition) # FIXME. make generic.
|
25
|
+
super
|
26
|
+
extend ObjectBinding if definition.typed?
|
21
27
|
end
|
22
28
|
|
23
29
|
def read(hash)
|
24
|
-
|
25
|
-
|
26
|
-
|
30
|
+
fragment = hash[definition.from]
|
31
|
+
deserialize_from(fragment)
|
32
|
+
end
|
33
|
+
|
34
|
+
def write(hash, value)
|
35
|
+
hash[definition.from] = serialize_for(value)
|
27
36
|
end
|
28
37
|
end
|
29
|
-
|
30
|
-
|
31
|
-
class
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
def write(hash, object)
|
36
|
-
if definition.array?
|
37
|
-
hash[definition.from] = object.collect { |obj| serialize(obj) }
|
38
|
-
else
|
39
|
-
hash[definition.from] = serialize(object)
|
40
|
-
end
|
38
|
+
|
39
|
+
|
40
|
+
class PropertyBinding < JSONBinding
|
41
|
+
def serialize_for(value)
|
42
|
+
serialize(value)
|
41
43
|
end
|
42
44
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
45
|
+
def deserialize_from(fragment)
|
46
|
+
deserialize(fragment)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
class CollectionBinding < JSONBinding
|
52
|
+
def serialize_for(value)
|
53
|
+
value.collect { |obj| serialize(obj) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def deserialize_from(fragment)
|
57
|
+
fragment ||= {}
|
58
|
+
fragment.collect { |item_fragment| deserialize(item_fragment) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
class HashBinding < JSONBinding
|
64
|
+
def serialize_for(value)
|
65
|
+
{}.tap do |hash|
|
66
|
+
value.each { |key, obj| hash[key] = serialize(obj) }
|
46
67
|
end
|
47
68
|
end
|
48
69
|
|
49
|
-
|
50
|
-
|
51
|
-
write_object(object).to_hash(:wrap => false)
|
70
|
+
def deserialize_from(fragment)
|
71
|
+
fragment.each { |key, item_fragment| fragment[key] = deserialize(item_fragment) }
|
52
72
|
end
|
53
73
|
end
|
54
74
|
end
|
@@ -2,90 +2,127 @@ require 'representable/binding'
|
|
2
2
|
|
3
3
|
module Representable
|
4
4
|
module XML
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
module ObjectBinding
|
6
|
+
# TODO: provide a base ObjectBinding for XML/JSON/MP.
|
7
|
+
include Binding::Extend # provides #serialize/#deserialize with extend.
|
8
|
+
|
9
|
+
def serialize(object)
|
10
|
+
super(object).to_node(:wrap => false)
|
8
11
|
end
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
definition.from
|
13
|
+
def deserialize(hash)
|
14
|
+
super(create_object).from_node(hash)
|
13
15
|
end
|
14
|
-
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
|
17
|
+
def deserialize_node(node)
|
18
|
+
deserialize(node)
|
19
|
+
end
|
20
|
+
|
21
|
+
def serialize_node(node, value)
|
22
|
+
serialize(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_object
|
26
|
+
definition.sought_type.new
|
20
27
|
end
|
21
28
|
end
|
22
29
|
|
23
30
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
31
|
+
class PropertyBinding < Binding
|
32
|
+
def initialize(definition)
|
33
|
+
super
|
34
|
+
extend ObjectBinding if definition.typed? # FIXME.
|
28
35
|
end
|
29
|
-
|
36
|
+
|
37
|
+
def write(parent, value)
|
38
|
+
parent << serialize_for(value, parent)
|
39
|
+
end
|
40
|
+
|
41
|
+
def read(node)
|
42
|
+
nodes = node.search("./#{xpath}")
|
43
|
+
return if nodes.size == 0 # TODO: write dedicated test!
|
44
|
+
|
45
|
+
deserialize_from(nodes)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Creates wrapped node for the property.
|
49
|
+
def serialize_for(value, parent)
|
50
|
+
#def serialize_for(value, parent, tag_name=definition.from)
|
51
|
+
node = Nokogiri::XML::Node.new(definition.from, parent.document)
|
52
|
+
serialize_node(node, value)
|
53
|
+
end
|
54
|
+
|
55
|
+
def serialize_node(node, value)
|
56
|
+
node.content = serialize(value)
|
57
|
+
node
|
58
|
+
end
|
59
|
+
|
60
|
+
def deserialize_from(nodes)
|
61
|
+
deserialize_node(nodes.first)
|
62
|
+
end
|
63
|
+
|
64
|
+
# DISCUSS: rename to #read_from ?
|
65
|
+
def deserialize_node(node)
|
66
|
+
deserialize(node.content)
|
67
|
+
end
|
68
|
+
|
30
69
|
private
|
31
|
-
def
|
32
|
-
|
70
|
+
def xpath
|
71
|
+
definition.from
|
33
72
|
end
|
34
73
|
end
|
35
74
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
if definition.array?
|
41
|
-
value.each do |v|
|
42
|
-
add(xml, definition.from, v)
|
43
|
-
end
|
44
|
-
else
|
45
|
-
add(xml, definition.from, value)
|
75
|
+
class CollectionBinding < PropertyBinding
|
76
|
+
def write(parent, value)
|
77
|
+
serialize_items(value, parent).each do |node|
|
78
|
+
parent << node
|
46
79
|
end
|
47
80
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
node.content
|
81
|
+
|
82
|
+
def serialize_items(value, parent)
|
83
|
+
value.collect do |obj|
|
84
|
+
serialize_for(obj, parent)
|
53
85
|
end
|
54
86
|
end
|
55
87
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
88
|
+
def deserialize_from(nodes)
|
89
|
+
nodes.collect do |item|
|
90
|
+
deserialize_node(item)
|
91
|
+
end
|
59
92
|
end
|
60
93
|
end
|
61
94
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
# Adds the ref's markup to +xml+.
|
69
|
-
def write(xml, object)
|
70
|
-
if definition.array?
|
71
|
-
object.each do |item|
|
72
|
-
write_entity(xml, item)
|
73
|
-
end
|
74
|
-
else
|
75
|
-
write_entity(xml, object)
|
95
|
+
|
96
|
+
class HashBinding < CollectionBinding
|
97
|
+
def serialize_items(value, parent)
|
98
|
+
value.collect do |k, v|
|
99
|
+
node = Nokogiri::XML::Node.new(k, parent.document)
|
100
|
+
serialize_node(node, v)
|
76
101
|
end
|
77
102
|
end
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
103
|
+
|
104
|
+
def deserialize_from(nodes)
|
105
|
+
{}.tap do |hash|
|
106
|
+
nodes.children.each do |node|
|
107
|
+
hash[node.name] = deserialize_node(node)
|
108
|
+
end
|
84
109
|
end
|
85
110
|
end
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# Represents a tag attribute. Currently this only works on the top-level tag.
|
115
|
+
class AttributeBinding < PropertyBinding
|
116
|
+
def read(node)
|
117
|
+
deserialize(node[definition.from])
|
118
|
+
end
|
119
|
+
|
120
|
+
def serialize_for(value, parent)
|
121
|
+
parent[definition.from] = serialize(value.to_s)
|
122
|
+
end
|
86
123
|
|
87
|
-
def
|
88
|
-
|
124
|
+
def write(parent, value)
|
125
|
+
serialize_for(value, parent)
|
89
126
|
end
|
90
127
|
end
|
91
128
|
end
|
@@ -1,22 +1,12 @@
|
|
1
1
|
module Representable
|
2
2
|
# Created at class compile time. Keeps configuration options for one property.
|
3
3
|
class Definition
|
4
|
-
attr_reader :name, :
|
4
|
+
attr_reader :name, :options
|
5
5
|
alias_method :getter, :name
|
6
6
|
|
7
7
|
def initialize(sym, options={})
|
8
|
-
@name
|
9
|
-
@
|
10
|
-
@from = (options[:from] || name).to_s
|
11
|
-
@sought_type = options[:class]
|
12
|
-
@default = options[:default]
|
13
|
-
@default ||= [] if array?
|
14
|
-
@representer_module = options[:extend] # DISCUSS: move to Representable::DCI?
|
15
|
-
@attribute = options[:attribute]
|
16
|
-
end
|
17
|
-
|
18
|
-
def instance_variable_name
|
19
|
-
:"@#{name}"
|
8
|
+
@name = sym.to_s
|
9
|
+
@options = options
|
20
10
|
end
|
21
11
|
|
22
12
|
def setter
|
@@ -24,26 +14,36 @@ module Representable
|
|
24
14
|
end
|
25
15
|
|
26
16
|
def typed?
|
27
|
-
sought_type.is_a?(Class)
|
17
|
+
sought_type.is_a?(Class) or representer_module # also true if only :extend is set, for people who want solely rendering.
|
28
18
|
end
|
29
19
|
|
30
20
|
def array?
|
31
|
-
|
32
|
-
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
21
|
+
options[:collection]
|
22
|
+
end
|
23
|
+
|
24
|
+
def hash?
|
25
|
+
options[:hash]
|
26
|
+
end
|
27
|
+
|
28
|
+
def sought_type
|
29
|
+
options[:class]
|
30
|
+
end
|
31
|
+
|
32
|
+
def from
|
33
|
+
(options[:from] || name).to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def default
|
37
|
+
options[:default] ||= [] if array? # FIXME: move to CollectionBinding!
|
38
|
+
options[:default]
|
39
|
+
end
|
40
|
+
|
41
|
+
def representer_module
|
42
|
+
options[:extend]
|
43
|
+
end
|
44
|
+
|
45
|
+
def attribute
|
46
|
+
options[:attribute]
|
47
47
|
end
|
48
48
|
end
|
49
49
|
end
|