representable 1.6.1 → 1.7.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 +4 -4
- data/CHANGES.md +7 -0
- data/README.md +1 -1
- data/lib/representable.rb +1 -1
- data/lib/representable/TODO.getting_serious +5 -0
- data/lib/representable/binding.rb +13 -33
- data/lib/representable/bindings/hash_bindings.rb +2 -3
- data/lib/representable/bindings/xml_bindings.rb +40 -36
- data/lib/representable/bindings/yaml_bindings.rb +7 -7
- data/lib/representable/config.rb +19 -4
- data/lib/representable/definition.rb +4 -0
- data/lib/representable/deserializer.rb +63 -0
- data/lib/representable/hash/collection.rb +1 -1
- data/lib/representable/serializer.rb +18 -0
- data/lib/representable/version.rb +1 -1
- data/test/config_test.rb +104 -0
- data/test/definition_test.rb +46 -41
- data/test/generic_test.rb +161 -22
- data/test/representable_test.rb +56 -112
- data/test/test_helper.rb +28 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8702c711c2230d338b5390e3e10cd7af901d41cf
|
4
|
+
data.tar.gz: fb5fd9498c58b6047a3ec221950f76c39d633c42
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a66be6a8a40fa284c674ae0ab63aa77e48cf9257be1de100a474cc0a14bffe2271172104b190f7fc9987eb6927402db5e9f24953d670a3733b619fef3ac2ae9
|
7
|
+
data.tar.gz: 7b89c5a30d057d6e47ebd34f2881acc7d94df6e25e4a42692ce38b92e457aff7a96b1be12bb25312d997fdf6c58cbad46af2906487b7badf78e58e99925a47b5
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
h2. 1.7.0
|
2
|
+
|
3
|
+
* The actual serialization and deserialization (that is, calling `to_hash` etc on the object) now happens in dedicated classes: `ObjectDeserializer` and friends. If you used to override stuff in `Binding`, I'm sorry.
|
4
|
+
* A new option `parse_strategy: :sync`. Instead of creating a new object using the `:class` option when parsing, it uses the original object found in the represented instance. This works for property and collections.
|
5
|
+
* `Config` is now a hash. You may find a particular definition by using `Config#[]`.
|
6
|
+
* Properties are now overridden: when calling `property(:title)` multiple times with the same name, this will override the former `Definition`. While this slightly changes the API, it allows overriding properties cleanly in sub-representers and saves you from manually finding and fiddling with the definitions.
|
7
|
+
|
1
8
|
h2. 1.6.1
|
2
9
|
|
3
10
|
* Using `instance: lambda { nil }` will now treat the property as a representable object without trying to extend it. It simply calls `to_*/from_*` on the property.
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Representable maps ruby objects to documents and back.
|
4
4
|
|
5
|
-
In other words: Take an object and
|
5
|
+
In other words: Take an object and decorate it with a representer module. This will allow you to render a JSON, XML or YAML document from that object. But that's only half of it! You can also use representers to parse a document and create or populate an object.
|
6
6
|
|
7
7
|
Representable is helpful for all kind of rendering and parsing workflows. However, it is mostly useful in API code. Are you planning to write a real REST API with representable? Then check out the [roar](http://github.com/apotonick/roar) gem first, save work and time and make the world a better place instead.
|
8
8
|
|
data/lib/representable.rb
CHANGED
@@ -117,7 +117,7 @@ private
|
|
117
117
|
# property :name, :readable => false
|
118
118
|
# property :name, :writeable => false
|
119
119
|
def property(name, options={}, &block)
|
120
|
-
|
120
|
+
representable_attrs << definition_class.new(name, options)
|
121
121
|
end
|
122
122
|
|
123
123
|
# Declares a represented document node collection.
|
@@ -1,4 +1,6 @@
|
|
1
|
-
require
|
1
|
+
require "delegate"
|
2
|
+
require "representable/deserializer"
|
3
|
+
require "representable/serializer"
|
2
4
|
|
3
5
|
module Representable
|
4
6
|
# The Binding wraps the Definition instance for this property and provides methods to read/write fragments.
|
@@ -26,7 +28,7 @@ module Representable
|
|
26
28
|
value
|
27
29
|
end
|
28
30
|
|
29
|
-
def deserialize(fragment)
|
31
|
+
def deserialize(fragment, *args)
|
30
32
|
fragment
|
31
33
|
end
|
32
34
|
|
@@ -84,6 +86,8 @@ module Representable
|
|
84
86
|
end
|
85
87
|
end
|
86
88
|
|
89
|
+
# the remaining methods in this class are format-independent and should be in Definition.
|
90
|
+
|
87
91
|
private
|
88
92
|
attr_reader :exec_context
|
89
93
|
|
@@ -103,47 +107,23 @@ module Representable
|
|
103
107
|
end
|
104
108
|
|
105
109
|
|
106
|
-
# Hooks into #serialize and #deserialize to setup (extend/decorate) typed properties
|
107
|
-
# at runtime.
|
108
110
|
module Prepare
|
109
|
-
# Extends the object with its representer before serialization.
|
110
|
-
def serialize(*)
|
111
|
-
prepare(super)
|
112
|
-
end
|
113
|
-
|
114
|
-
def deserialize(*)
|
115
|
-
prepare(super)
|
116
|
-
end
|
117
|
-
|
118
|
-
def prepare(object)
|
119
|
-
return object unless mod = representer_module_for(object) # :extend.
|
120
|
-
|
121
|
-
mod = mod.first if mod.is_a?(Array) # TODO: deprecate :extend => [..]
|
122
|
-
mod.prepare(object)
|
123
|
-
end
|
124
|
-
|
125
|
-
private
|
126
111
|
def representer_module_for(object, *args)
|
127
112
|
call_proc_for(representer_module, object) # TODO: how to pass additional data to the computing block?`
|
128
113
|
end
|
129
114
|
end
|
115
|
+
include Prepare
|
130
116
|
|
131
|
-
# Overrides #serialize/#deserialize to call #to_*/from_*.
|
132
|
-
# Computes :class in #deserialize. # TODO: shouldn't this be in a separate module? ObjectSerialize/ObjectDeserialize?
|
133
|
-
module Object
|
134
|
-
include Binding::Prepare
|
135
117
|
|
118
|
+
# Delegates to call #to_*/from_*.
|
119
|
+
module Object
|
136
120
|
def serialize(object)
|
137
|
-
|
138
|
-
|
139
|
-
super.send(serialize_method, @user_options.merge!({:wrap => false})) # TODO: pass :binding => self
|
121
|
+
ObjectSerializer.new(self, object).call
|
140
122
|
end
|
141
123
|
|
142
|
-
def deserialize(data)
|
124
|
+
def deserialize(data, object=lambda { get })
|
143
125
|
# DISCUSS: does it make sense to skip deserialization of nil-values here?
|
144
|
-
|
145
|
-
super(obj).send(deserialize_method, data, @user_options)
|
146
|
-
end
|
126
|
+
ObjectDeserializer.new(self, object).call(data)
|
147
127
|
end
|
148
128
|
|
149
129
|
def create_object(fragment)
|
@@ -152,7 +132,7 @@ module Representable
|
|
152
132
|
|
153
133
|
private
|
154
134
|
def class_for(fragment, *args)
|
155
|
-
item_class = class_from(fragment) or return fragment
|
135
|
+
item_class = class_from(fragment) or return fragment
|
156
136
|
item_class.new
|
157
137
|
end
|
158
138
|
|
@@ -47,15 +47,14 @@ module Representable
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
|
51
50
|
class CollectionBinding < PropertyBinding
|
52
51
|
def serialize_for(value)
|
53
52
|
# value.enum_for(:each_with_index).collect { |obj, i| serialize(obj, i) } # DISCUSS: provide ary index/hash key for representer_module_for?
|
54
|
-
value.collect { |item| serialize(item) }
|
53
|
+
value.collect { |item| serialize(item) } # TODO: i don't want Array but Forms here - what now?
|
55
54
|
end
|
56
55
|
|
57
56
|
def deserialize_from(fragment)
|
58
|
-
|
57
|
+
CollectionDeserializer.new(self).deserialize(fragment)
|
59
58
|
end
|
60
59
|
end
|
61
60
|
|
@@ -1,28 +1,29 @@
|
|
1
1
|
require 'representable/binding'
|
2
|
+
require 'representable/bindings/hash_bindings.rb'
|
2
3
|
|
3
4
|
module Representable
|
4
5
|
module XML
|
5
6
|
module ObjectBinding
|
6
7
|
include Binding::Object
|
7
|
-
|
8
|
+
|
8
9
|
def serialize_method
|
9
10
|
:to_node
|
10
11
|
end
|
11
|
-
|
12
|
+
|
12
13
|
def deserialize_method
|
13
14
|
:from_node
|
14
15
|
end
|
15
|
-
|
16
|
-
def
|
17
|
-
|
16
|
+
|
17
|
+
def content_for(node) # TODO: move to ScalarDecorator.
|
18
|
+
node
|
18
19
|
end
|
19
|
-
|
20
|
+
|
20
21
|
def serialize_node(node, value)
|
21
22
|
serialize(value)
|
22
23
|
end
|
23
24
|
end
|
24
|
-
|
25
|
-
|
25
|
+
|
26
|
+
|
26
27
|
class PropertyBinding < Binding
|
27
28
|
def self.build_for(definition, *args)
|
28
29
|
return CollectionBinding.new(definition, *args) if definition.array?
|
@@ -36,45 +37,41 @@ module Representable
|
|
36
37
|
super
|
37
38
|
extend ObjectBinding if typed? # FIXME.
|
38
39
|
end
|
39
|
-
|
40
|
+
|
40
41
|
def write(parent, value)
|
41
42
|
wrap_node = parent
|
42
|
-
|
43
|
+
|
43
44
|
if wrap = options[:wrap]
|
44
45
|
parent << wrap_node = node_for(parent, wrap)
|
45
46
|
end
|
46
47
|
|
47
48
|
wrap_node << serialize_for(value, parent)
|
48
49
|
end
|
49
|
-
|
50
|
+
|
50
51
|
def read(node)
|
51
52
|
nodes = find_nodes(node)
|
52
53
|
return FragmentNotFound if nodes.size == 0 # TODO: write dedicated test!
|
53
|
-
|
54
|
+
|
54
55
|
deserialize_from(nodes)
|
55
56
|
end
|
56
|
-
|
57
|
+
|
57
58
|
# Creates wrapped node for the property.
|
58
59
|
def serialize_for(value, parent)
|
59
60
|
#def serialize_for(value, parent, tag_name=definition.from)
|
60
61
|
node = node_for(parent, from)
|
61
62
|
serialize_node(node, value)
|
62
63
|
end
|
63
|
-
|
64
|
+
|
64
65
|
def serialize_node(node, value)
|
65
66
|
node.content = serialize(value)
|
66
67
|
node
|
67
68
|
end
|
68
|
-
|
69
|
+
|
69
70
|
def deserialize_from(nodes)
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
# DISCUSS: rename to #read_from ?
|
74
|
-
def deserialize_node(node)
|
75
|
-
deserialize(node.content)
|
71
|
+
content_for deserialize(nodes.first)
|
72
|
+
#deserialize(nodes.first)
|
76
73
|
end
|
77
|
-
|
74
|
+
|
78
75
|
private
|
79
76
|
def xpath
|
80
77
|
from
|
@@ -89,18 +86,25 @@ module Representable
|
|
89
86
|
def node_for(parent, name)
|
90
87
|
Nokogiri::XML::Node.new(name.to_s, parent.document)
|
91
88
|
end
|
89
|
+
|
90
|
+
def content_for(node) # TODO: move this into a ScalarDecorator.
|
91
|
+
node.content
|
92
|
+
end
|
92
93
|
end
|
93
|
-
|
94
|
+
|
94
95
|
class CollectionBinding < PropertyBinding
|
95
96
|
def serialize_for(value, parent)
|
96
97
|
# return NodeSet so << works.
|
97
98
|
set_for(parent, value.collect { |item| super(item, parent) })
|
98
99
|
end
|
99
|
-
|
100
|
+
|
100
101
|
def deserialize_from(nodes)
|
101
|
-
nodes.collect do |item|
|
102
|
-
|
102
|
+
content_nodes = nodes.collect do |item| # TODO: move this to Node?
|
103
|
+
content_for(item)
|
103
104
|
end
|
105
|
+
|
106
|
+
# *Deserializer doesn't want anything format specific!
|
107
|
+
CollectionDeserializer.new(self).deserialize(content_nodes)
|
104
108
|
end
|
105
109
|
|
106
110
|
private
|
@@ -108,8 +112,8 @@ module Representable
|
|
108
112
|
Nokogiri::XML::NodeSet.new(parent.document, nodes)
|
109
113
|
end
|
110
114
|
end
|
111
|
-
|
112
|
-
|
115
|
+
|
116
|
+
|
113
117
|
class HashBinding < CollectionBinding
|
114
118
|
def serialize_for(value, parent)
|
115
119
|
set_for(parent, value.collect do |k, v|
|
@@ -117,16 +121,16 @@ module Representable
|
|
117
121
|
serialize_node(node, v)
|
118
122
|
end)
|
119
123
|
end
|
120
|
-
|
124
|
+
|
121
125
|
def deserialize_from(nodes)
|
122
126
|
{}.tap do |hash|
|
123
127
|
nodes.children.each do |node|
|
124
|
-
hash[node.name] =
|
128
|
+
hash[node.name] = deserialize(content_for node)
|
125
129
|
end
|
126
130
|
end
|
127
131
|
end
|
128
132
|
end
|
129
|
-
|
133
|
+
|
130
134
|
class AttributeHashBinding < CollectionBinding
|
131
135
|
# DISCUSS: use AttributeBinding here?
|
132
136
|
def write(parent, value) # DISCUSS: is it correct overriding #write here?
|
@@ -135,7 +139,7 @@ module Representable
|
|
135
139
|
end
|
136
140
|
parent
|
137
141
|
end
|
138
|
-
|
142
|
+
|
139
143
|
def deserialize_from(node)
|
140
144
|
{}.tap do |hash|
|
141
145
|
node.each do |k,v|
|
@@ -144,18 +148,18 @@ module Representable
|
|
144
148
|
end
|
145
149
|
end
|
146
150
|
end
|
147
|
-
|
148
|
-
|
151
|
+
|
152
|
+
|
149
153
|
# Represents a tag attribute. Currently this only works on the top-level tag.
|
150
154
|
class AttributeBinding < PropertyBinding
|
151
155
|
def read(node)
|
152
156
|
deserialize(node[from])
|
153
157
|
end
|
154
|
-
|
158
|
+
|
155
159
|
def serialize_for(value, parent)
|
156
160
|
parent[from] = serialize(value.to_s)
|
157
161
|
end
|
158
|
-
|
162
|
+
|
159
163
|
def write(parent, value)
|
160
164
|
serialize_for(value, parent)
|
161
165
|
end
|
@@ -4,11 +4,11 @@ module Representable
|
|
4
4
|
module YAML
|
5
5
|
module ObjectBinding
|
6
6
|
include Binding::Object
|
7
|
-
|
7
|
+
|
8
8
|
def serialize_method
|
9
9
|
:to_ast
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
def deserialize_method
|
13
13
|
:from_hash
|
14
14
|
end
|
@@ -28,7 +28,7 @@ module Representable
|
|
28
28
|
super
|
29
29
|
extend ObjectBinding if typed?
|
30
30
|
end
|
31
|
-
|
31
|
+
|
32
32
|
def write(map, value)
|
33
33
|
map.children << Psych::Nodes::Scalar.new(from)
|
34
34
|
map.children << serialize_for(value) # FIXME: should be serialize.
|
@@ -42,8 +42,8 @@ module Representable
|
|
42
42
|
Psych::Nodes::Scalar.new(value.to_s)
|
43
43
|
end
|
44
44
|
end
|
45
|
-
|
46
|
-
|
45
|
+
|
46
|
+
|
47
47
|
class CollectionBinding < PropertyBinding
|
48
48
|
def serialize_for(value)
|
49
49
|
Psych::Nodes::Sequence.new.tap do |seq|
|
@@ -51,9 +51,9 @@ module Representable
|
|
51
51
|
value.each { |obj| seq.children << super(obj) }
|
52
52
|
end
|
53
53
|
end
|
54
|
-
|
54
|
+
|
55
55
|
def deserialize_from(fragment) # FIXME: redundant from Hash::Bindings
|
56
|
-
|
56
|
+
CollectionDeserializer.new(self).deserialize(fragment)
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
data/lib/representable/config.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Representable
|
2
2
|
# NOTE: the API of Config is subject to change so don't rely too much on this private object.
|
3
|
-
class Config <
|
3
|
+
class Config < Hash
|
4
4
|
# DISCUSS: experimental. this will soon be moved to a separate gem
|
5
5
|
module InheritableArray
|
6
6
|
def inheritable_array(name)
|
@@ -19,6 +19,17 @@ module Representable
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
def <<(definition)
|
23
|
+
self[definition.name] = definition
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](name)
|
27
|
+
fetch(name.to_s, nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
def each(*args, &block)
|
31
|
+
values.each(*args, &block)
|
32
|
+
end
|
22
33
|
|
23
34
|
attr_accessor :wrap
|
24
35
|
|
@@ -30,18 +41,22 @@ module Representable
|
|
30
41
|
end
|
31
42
|
|
32
43
|
module InheritMethods
|
33
|
-
def
|
34
|
-
|
44
|
+
def cloned
|
45
|
+
collect { |d| d.clone }
|
35
46
|
end
|
36
47
|
|
37
48
|
def inherit(parent)
|
38
|
-
push(
|
49
|
+
push(parent.cloned)
|
39
50
|
end
|
40
51
|
end
|
41
52
|
include InheritMethods
|
42
53
|
include InheritableArray # overrides #inherit.
|
43
54
|
|
44
55
|
private
|
56
|
+
def push(defs)
|
57
|
+
defs.each { |d| self << d }
|
58
|
+
end
|
59
|
+
|
45
60
|
def infer_name_for(name)
|
46
61
|
name.to_s.split('::').last.
|
47
62
|
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Representable
|
2
|
+
class CollectionDeserializer < Array # always is the targeted collection, already.
|
3
|
+
def initialize(binding) # TODO: get rid of binding dependency
|
4
|
+
# next step: use #get always.
|
5
|
+
@binding = binding
|
6
|
+
collection = []
|
7
|
+
# should be call to #default:
|
8
|
+
collection = binding.get if binding.sync?
|
9
|
+
|
10
|
+
super collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(fragment)
|
14
|
+
# next step: get rid of collect.
|
15
|
+
fragment.enum_for(:each_with_index).collect { |item_fragment, i|
|
16
|
+
@deserializer = ObjectDeserializer.new(@binding, lambda { self[i] })
|
17
|
+
|
18
|
+
@deserializer.call(item_fragment) # FIXME: what if obj nil?
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
class ObjectDeserializer
|
25
|
+
# dependencies: Def#options, Def#create_object, Def#get
|
26
|
+
def initialize(binding, object)
|
27
|
+
@binding = binding
|
28
|
+
@object = object
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(fragment)
|
32
|
+
# TODO: this used to be handled in #serialize where Object added it's behaviour. treat scalars as objects to remove this switch:
|
33
|
+
return fragment unless @binding.typed?
|
34
|
+
|
35
|
+
if @binding.sync?
|
36
|
+
# TODO: this is also done when instance: { nil }
|
37
|
+
@object = @object.call # call Binding#get or Binding#get[i]
|
38
|
+
else
|
39
|
+
@object = @binding.create_object(fragment)
|
40
|
+
end
|
41
|
+
|
42
|
+
# DISCUSS: what parts should be in this class, what in Binding?
|
43
|
+
representable = prepare(@object)
|
44
|
+
deserialize(representable, fragment, @binding.user_options)
|
45
|
+
#yield @object
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def deserialize(object, fragment, options)
|
50
|
+
object.send(@binding.deserialize_method, fragment, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
def prepare(object)
|
54
|
+
mod = @binding.representer_module_for(object)
|
55
|
+
|
56
|
+
return object unless mod
|
57
|
+
|
58
|
+
mod = mod.first if mod.is_a?(Array) # TODO: deprecate :extend => [..]
|
59
|
+
mod.prepare(object)
|
60
|
+
end
|
61
|
+
# in deserialize, we should get the original object?
|
62
|
+
end
|
63
|
+
end
|