representable 2.0.4 → 2.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.
- 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
|
|