representable 2.0.4 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +17 -0
- data/README.md +20 -1
- data/lib/representable.rb +2 -1
- data/lib/representable/binding.rb +115 -59
- data/lib/representable/config.rb +8 -0
- data/lib/representable/definition.rb +10 -14
- data/lib/representable/deserializer.rb +64 -25
- data/lib/representable/hash.rb +3 -3
- data/lib/representable/hash/binding.rb +40 -0
- data/lib/representable/hash/collection.rb +3 -2
- data/lib/representable/hash_methods.rb +4 -2
- data/lib/representable/mapper.rb +1 -1
- data/lib/representable/populator.rb +59 -0
- data/lib/representable/serializer.rb +24 -13
- data/lib/representable/version.rb +1 -1
- data/lib/representable/xml.rb +3 -3
- data/lib/representable/xml/binding.rb +171 -0
- data/lib/representable/yaml.rb +3 -3
- data/lib/representable/yaml/binding.rb +48 -0
- data/representable.gemspec +1 -1
- data/test/benchmarking.rb +83 -0
- data/test/binding_test.rb +46 -0
- data/test/definition_test.rb +5 -58
- data/test/exec_context_test.rb +4 -4
- data/test/hash_bindings_test.rb +4 -52
- data/test/hash_test.rb +6 -6
- data/test/json_test.rb +8 -8
- data/test/lonely_test.rb +1 -1
- data/test/realistic_benchmark.rb +83 -0
- data/test/skip_test.rb +28 -0
- data/test/xml_bindings_test.rb +2 -109
- data/test/xml_test.rb +61 -23
- data/test/yaml_test.rb +5 -8
- metadata +19 -11
- data/lib/representable/bindings/hash_bindings.rb +0 -64
- data/lib/representable/bindings/xml_bindings.rb +0 -172
- data/lib/representable/bindings/yaml_bindings.rb +0 -49
data/lib/representable/hash.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'representable'
|
2
|
-
require 'representable/
|
2
|
+
require 'representable/hash/binding'
|
3
3
|
|
4
4
|
module Representable
|
5
5
|
# The generic representer. Brings #to_hash and #from_hash to your object.
|
@@ -25,13 +25,13 @@ module Representable
|
|
25
25
|
# Note: `#from_hash` still does _not_ stringify incoming hashes. This is per design: Representable is not made for hashes, only,
|
26
26
|
# but for any arbitrary data structure. A generic `key.to_s` with non-hash data would result in weird issues.
|
27
27
|
# I decided it's more predictable to require the user to provide stringified keys.
|
28
|
-
def from_hash(data, options={}, binding_builder=
|
28
|
+
def from_hash(data, options={}, binding_builder=Binding)
|
29
29
|
data = filter_wrap(data, options)
|
30
30
|
|
31
31
|
update_properties_from(data, options, binding_builder)
|
32
32
|
end
|
33
33
|
|
34
|
-
def to_hash(options={}, binding_builder=
|
34
|
+
def to_hash(options={}, binding_builder=Binding)
|
35
35
|
hash = create_representation_with({}, options, binding_builder)
|
36
36
|
|
37
37
|
return hash unless wrap = options[:wrap] || representation_wrap(options)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'representable/binding'
|
2
|
+
|
3
|
+
module Representable
|
4
|
+
module Hash
|
5
|
+
class Binding < Representable::Binding
|
6
|
+
def self.build_for(definition, *args) # TODO: remove default arg.
|
7
|
+
return Collection.new(definition, *args) if definition.array?
|
8
|
+
return Hash.new(definition, *args) if definition.hash?
|
9
|
+
new(definition, *args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def read(hash)
|
13
|
+
return FragmentNotFound unless hash.has_key?(as) # DISCUSS: put it all in #read for performance. not really sure if i like returning that special thing.
|
14
|
+
|
15
|
+
hash[as] # fragment
|
16
|
+
end
|
17
|
+
|
18
|
+
def write(hash, fragment)
|
19
|
+
hash[as] = fragment
|
20
|
+
end
|
21
|
+
|
22
|
+
def serialize_method
|
23
|
+
:to_hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def deserialize_method
|
27
|
+
:from_hash
|
28
|
+
end
|
29
|
+
|
30
|
+
class Collection < self
|
31
|
+
include Representable::Binding::Collection
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
class Hash < self
|
36
|
+
include Representable::Binding::Hash
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -20,12 +20,13 @@ module Representable::Hash
|
|
20
20
|
|
21
21
|
def create_representation_with(doc, options, format)
|
22
22
|
bin = representable_mapper(format, options).bindings.first
|
23
|
-
bin.
|
23
|
+
bin.render_fragment(represented, doc)
|
24
24
|
end
|
25
25
|
|
26
26
|
def update_properties_from(doc, options, format)
|
27
27
|
bin = representable_mapper(format, options).bindings.first
|
28
|
-
value = bin.deserialize_from(doc)
|
28
|
+
#value = bin.deserialize_from(doc)
|
29
|
+
value = Deserializer::Collection.new(bin).call(doc)
|
29
30
|
represented.replace(value)
|
30
31
|
end
|
31
32
|
end
|
@@ -3,13 +3,15 @@ module Representable
|
|
3
3
|
def create_representation_with(doc, options, format)
|
4
4
|
bin = representable_mapper(format, options).bindings.first
|
5
5
|
hash = filter_keys_for(represented, options)
|
6
|
-
bin.
|
6
|
+
bin.render_fragment(hash, doc) # TODO: Use something along Populator, which does
|
7
7
|
end
|
8
8
|
|
9
9
|
def update_properties_from(doc, options, format)
|
10
10
|
bin = representable_mapper(format, options).bindings.first
|
11
11
|
hash = filter_keys_for(doc, options)
|
12
|
-
|
12
|
+
|
13
|
+
value = Deserializer::Hash.new(bin).call(hash)
|
14
|
+
# value = bin.deserialize_from(hash)
|
13
15
|
represented.replace(value)
|
14
16
|
end
|
15
17
|
|
data/lib/representable/mapper.rb
CHANGED
@@ -52,7 +52,7 @@ module Representable
|
|
52
52
|
def skip_conditional_property?(binding)
|
53
53
|
return unless condition = binding[:if]
|
54
54
|
|
55
|
-
not binding.
|
55
|
+
not binding.evaluate_option(:if)
|
56
56
|
end
|
57
57
|
|
58
58
|
# DISCUSS: this could be just another :if option in a Pipeline?
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Representable
|
2
|
+
#
|
3
|
+
# populator
|
4
|
+
# skip_parse? --> return
|
5
|
+
# deserialize (this is where additional logic can happen, e.g. Object-HAL's collection semantics).
|
6
|
+
# parse_filter
|
7
|
+
# set
|
8
|
+
class Populator # rename to Deserializer?
|
9
|
+
def initialize(binding)
|
10
|
+
@binding = binding
|
11
|
+
end
|
12
|
+
|
13
|
+
# goal of this is to have this workflow apply-able to collections AND to items per collection, or for items in hashes.
|
14
|
+
def call(fragment, doc)
|
15
|
+
# the rest should be applied per item (collection) or per fragment (collection and property)
|
16
|
+
if fragment == Binding::FragmentNotFound
|
17
|
+
return unless @binding.has_default?
|
18
|
+
value = @binding[:default]
|
19
|
+
else
|
20
|
+
value = deserialize(fragment) { return } # stop here if skip_parse?
|
21
|
+
end
|
22
|
+
|
23
|
+
value = @binding.parse_filter(value, doc)
|
24
|
+
# parse_filter
|
25
|
+
# set
|
26
|
+
@binding.set(value)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def deserialize(fragment)
|
31
|
+
return yield if @binding.evaluate_option(:skip_parse, fragment) # TODO: move this into Deserializer.
|
32
|
+
|
33
|
+
# use a Deserializer to transform fragment to/into object.
|
34
|
+
deserializer_class.new(@binding).call(fragment) # CollectionDeserializer/HashDeserializer/etc.
|
35
|
+
end
|
36
|
+
|
37
|
+
def deserializer_class
|
38
|
+
Deserializer
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# A separated collection deserializer/populator allows us better dealing with populating/modifying
|
43
|
+
# collections of models. (e.g. replace, update, push, etc.).
|
44
|
+
# That also gives us a place to apply options like :parse_filter, etc. per item.
|
45
|
+
class Collection < self
|
46
|
+
private
|
47
|
+
def deserialize(fragment)
|
48
|
+
return Deserializer::Collection.new(@binding).call(fragment)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Hash < self
|
53
|
+
private
|
54
|
+
def deserializer_class
|
55
|
+
Deserializer::Hash
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,28 +1,39 @@
|
|
1
1
|
require "representable/deserializer"
|
2
2
|
|
3
3
|
module Representable
|
4
|
-
class
|
5
|
-
def
|
6
|
-
|
7
|
-
@object = object
|
8
|
-
end
|
9
|
-
|
10
|
-
def call
|
11
|
-
# return unless @binding.typed? # FIXME: fix that in XML/YAML.
|
12
|
-
return @object if @object.nil? # DISCUSS: move to Object#serialize ?
|
13
|
-
|
14
|
-
representable = prepare(@object)
|
4
|
+
class Serializer < Deserializer
|
5
|
+
def call(object)
|
6
|
+
return object if object.nil? # DISCUSS: move to Object#serialize ?
|
15
7
|
|
16
|
-
serialize(
|
8
|
+
serialize(object, @binding.user_options)
|
17
9
|
end
|
18
10
|
|
19
11
|
private
|
12
|
+
# Serialize one object by calling to_json etc. on it.
|
20
13
|
def serialize(object, user_options)
|
14
|
+
object = prepare(object)
|
15
|
+
|
21
16
|
return object unless @binding.representable?
|
22
17
|
|
23
|
-
@binding.
|
18
|
+
@binding.evaluate_option(:serialize, object) do
|
24
19
|
object.send(@binding.serialize_method, user_options.merge!({:wrap => false}))
|
25
20
|
end
|
26
21
|
end
|
22
|
+
|
23
|
+
|
24
|
+
class Collection < self
|
25
|
+
def serialize(array, *args)
|
26
|
+
array.collect { |item| super(item, *args) } # TODO: i don't want Array but Forms here - what now?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
class Hash < self
|
32
|
+
def serialize(hash, *args)
|
33
|
+
{}.tap do |hsh|
|
34
|
+
hash.each { |key, obj| hsh[key] = super(obj, *args) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
27
38
|
end
|
28
39
|
end
|
data/lib/representable/xml.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'representable'
|
2
|
-
require 'representable/
|
2
|
+
require 'representable/xml/binding'
|
3
3
|
require 'representable/xml/collection'
|
4
4
|
require 'nokogiri'
|
5
5
|
|
@@ -32,7 +32,7 @@ module Representable
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def from_node(node, options={})
|
35
|
-
update_properties_from(node, options,
|
35
|
+
update_properties_from(node, options, Binding)
|
36
36
|
end
|
37
37
|
|
38
38
|
# Returns a Nokogiri::XML object representing this object.
|
@@ -40,7 +40,7 @@ module Representable
|
|
40
40
|
options[:doc] ||= Nokogiri::XML::Document.new
|
41
41
|
root_tag = options[:wrap] || representation_wrap(options)
|
42
42
|
|
43
|
-
create_representation_with(Nokogiri::XML::Node.new(root_tag.to_s, options[:doc]), options,
|
43
|
+
create_representation_with(Nokogiri::XML::Node.new(root_tag.to_s, options[:doc]), options, Binding)
|
44
44
|
end
|
45
45
|
|
46
46
|
def to_xml(*args)
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'representable/binding'
|
2
|
+
require 'representable/hash/binding.rb'
|
3
|
+
|
4
|
+
module Representable
|
5
|
+
module XML
|
6
|
+
class Binding < Representable::Binding
|
7
|
+
def self.build_for(definition, *args)
|
8
|
+
return Collection.new(definition, *args) if definition.array?
|
9
|
+
return Hash.new(definition, *args) if definition.hash? and not definition[:use_attributes] # FIXME: hate this.
|
10
|
+
return AttributeHash.new(definition, *args) if definition.hash? and definition[:use_attributes]
|
11
|
+
return Attribute.new(definition, *args) if definition[:attribute]
|
12
|
+
return Content.new(definition, *args) if definition[:content]
|
13
|
+
new(definition, *args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def write(parent, fragments)
|
17
|
+
wrap_node = parent
|
18
|
+
|
19
|
+
if wrap = self[:wrap]
|
20
|
+
parent << wrap_node = node_for(parent, wrap)
|
21
|
+
end
|
22
|
+
|
23
|
+
wrap_node << serialize_for(fragments, parent)
|
24
|
+
end
|
25
|
+
|
26
|
+
def read(node)
|
27
|
+
nodes = find_nodes(node)
|
28
|
+
return FragmentNotFound if nodes.size == 0 # TODO: write dedicated test!
|
29
|
+
|
30
|
+
deserialize_from(nodes)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Creates wrapped node for the property.
|
34
|
+
def serialize_for(value, parent)
|
35
|
+
node = node_for(parent, as)
|
36
|
+
serialize_node(node, value)
|
37
|
+
end
|
38
|
+
|
39
|
+
def serialize_node(node, value)
|
40
|
+
return value if typed?
|
41
|
+
|
42
|
+
node.content = value
|
43
|
+
node
|
44
|
+
end
|
45
|
+
|
46
|
+
def deserialize_from(nodes)
|
47
|
+
content_for(nodes.first)
|
48
|
+
end
|
49
|
+
|
50
|
+
# DISCUSS: why is this public?
|
51
|
+
def serialize_method
|
52
|
+
:to_node
|
53
|
+
end
|
54
|
+
|
55
|
+
def deserialize_method
|
56
|
+
:from_node
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def xpath
|
61
|
+
as
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_nodes(doc)
|
65
|
+
selector = xpath
|
66
|
+
selector = "#{self[:wrap]}/#{xpath}" if self[:wrap]
|
67
|
+
nodes = doc.xpath(selector)
|
68
|
+
end
|
69
|
+
|
70
|
+
def node_for(parent, name)
|
71
|
+
Nokogiri::XML::Node.new(name.to_s, parent.document)
|
72
|
+
end
|
73
|
+
|
74
|
+
def content_for(node) # TODO: move this into a ScalarDecorator.
|
75
|
+
return node if typed?
|
76
|
+
|
77
|
+
node.content
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
class Collection < self
|
82
|
+
include Representable::Binding::Collection
|
83
|
+
|
84
|
+
def serialize_for(value, parent)
|
85
|
+
# return NodeSet so << works.
|
86
|
+
set_for(parent, value.collect { |item| super(item, parent) })
|
87
|
+
end
|
88
|
+
|
89
|
+
def deserialize_from(nodes)
|
90
|
+
content_nodes = nodes.collect do |item| # TODO: move this to Node?
|
91
|
+
content_for(item)
|
92
|
+
end
|
93
|
+
|
94
|
+
content_nodes
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def set_for(parent, nodes)
|
99
|
+
Nokogiri::XML::NodeSet.new(parent.document, nodes)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
class Hash < Collection
|
105
|
+
include Representable::Binding::Hash
|
106
|
+
|
107
|
+
def serialize_for(value, parent)
|
108
|
+
set_for(parent, value.collect do |k, v|
|
109
|
+
node = node_for(parent, k)
|
110
|
+
serialize_node(node, v)
|
111
|
+
end)
|
112
|
+
end
|
113
|
+
|
114
|
+
def deserialize_from(nodes)
|
115
|
+
hash = {}
|
116
|
+
nodes.children.each do |node|
|
117
|
+
hash[node.name] = content_for node
|
118
|
+
end
|
119
|
+
|
120
|
+
hash
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class AttributeHash < Collection
|
125
|
+
# DISCUSS: use AttributeBinding here?
|
126
|
+
def write(parent, value) # DISCUSS: is it correct overriding #write here?
|
127
|
+
value.collect do |k, v|
|
128
|
+
parent[k] = v.to_s
|
129
|
+
end
|
130
|
+
parent
|
131
|
+
end
|
132
|
+
|
133
|
+
# FIXME: this is not tested!
|
134
|
+
def deserialize_from(node)
|
135
|
+
HashDeserializer.new(self).deserialize(node)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
# Represents a tag attribute. Currently this only works on the top-level tag.
|
141
|
+
class Attribute < self
|
142
|
+
def read(node)
|
143
|
+
node[as]
|
144
|
+
end
|
145
|
+
|
146
|
+
def serialize_for(value, parent)
|
147
|
+
parent[as] = value.to_s
|
148
|
+
end
|
149
|
+
|
150
|
+
def write(parent, value)
|
151
|
+
serialize_for(value, parent)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Represents tag content.
|
156
|
+
class Content < self
|
157
|
+
def read(node)
|
158
|
+
node.content
|
159
|
+
end
|
160
|
+
|
161
|
+
def serialize_for(value, parent)
|
162
|
+
parent.content = value.to_s
|
163
|
+
end
|
164
|
+
|
165
|
+
def write(parent, value)
|
166
|
+
serialize_for(value, parent)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end # Binding
|
170
|
+
end
|
171
|
+
end
|
data/lib/representable/yaml.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'representable/
|
1
|
+
require 'representable/yaml/binding'
|
2
2
|
|
3
3
|
module Representable
|
4
4
|
module YAML
|
@@ -15,7 +15,7 @@ module Representable
|
|
15
15
|
|
16
16
|
def from_yaml(doc, options={})
|
17
17
|
hash = Psych.load(doc)
|
18
|
-
from_hash(hash, options,
|
18
|
+
from_hash(hash, options, Binding)
|
19
19
|
end
|
20
20
|
|
21
21
|
# Returns a Nokogiri::XML object representing this object.
|
@@ -23,7 +23,7 @@ module Representable
|
|
23
23
|
#root_tag = options[:wrap] || representation_wrap
|
24
24
|
|
25
25
|
Psych::Nodes::Mapping.new.tap do |map|
|
26
|
-
create_representation_with(map, options,
|
26
|
+
create_representation_with(map, options, Binding)
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|