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.
@@ -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!
@@ -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
@@ -125,6 +125,11 @@ private
125
125
  options[:collection] = true
126
126
  property(name, options)
127
127
  end
128
+
129
+ def hash(name, options={})
130
+ options[:hash] = true
131
+ property(name, options)
132
+ end
128
133
  end
129
134
 
130
135
 
@@ -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
- private
13
- # Must be called in serialization of concrete ObjectBinding.
14
- def write_object(object)
15
- object
11
+ def serialize(value)
12
+ value
16
13
  end
17
14
 
18
- # Creates a typed property instance.
19
- def create_object
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 #write_object and #create_object to extend typed properties
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 write_object(object)
27
+ def serialize(object)
31
28
  extend_for(super)
32
29
  end
33
30
 
34
- def create_object
31
+ def deserialize(*)
35
32
  extend_for(super)
36
33
  end
37
34
 
38
- def extend_for(object) # TODO: test me.
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
- class Binding < Representable::Binding
6
- private
7
- def collect_for(hash)
8
- nodes = hash[definition.from] or return
9
- nodes = [nodes] unless nodes.is_a?(Array)
10
-
11
- vals = nodes.collect { |node| yield node }
12
-
13
- definition.array? ? vals : vals.first
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
- # Represents plain key-value.
18
- class TextBinding < Binding
19
- def write(hash, value)
20
- hash[definition.from] = value
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
- collect_for(hash) do |value|
25
- value
26
- end
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
- # Represents a tag with object binding.
31
- class ObjectBinding < Binding
32
- include Representable::Binding::Hooks # includes #create_object and #write_object.
33
- include Representable::Binding::Extend
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 read(hash)
44
- collect_for(hash) do |node|
45
- create_object.from_hash(node)
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
- private
50
- def serialize(object)
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
- class Binding < Representable::Binding
6
- def read(xml)
7
- value_from_node(xml)
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
- private
11
- def xpath
12
- definition.from
13
+ def deserialize(hash)
14
+ super(create_object).from_node(hash)
13
15
  end
14
-
15
- def collect_for(xml)
16
- nodes = xml.search("./#{xpath}")
17
- vals = nodes.collect { |node| yield node }
18
-
19
- definition.array? ? vals : vals.first
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
- # Represents a tag attribute.
25
- class AttributeBinding < Binding
26
- def write(xml, values)
27
- xml[definition.from] = values.to_s
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 value_from_node(xml)
32
- xml[definition.from]
70
+ def xpath
71
+ definition.from
33
72
  end
34
73
  end
35
74
 
36
-
37
- # Represents text content in a tag. # FIXME: is this tested???
38
- class TextBinding < Binding
39
- def write(xml, value)
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
- private
50
- def value_from_node(xml)
51
- collect_for(xml) do |node|
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 add(xml, name, value)
57
- child = xml.add_child Nokogiri::XML::Node.new(name, xml.document)
58
- child.content = value
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
- # Represents a tag with object binding.
64
- class ObjectBinding < Binding
65
- include Representable::Binding::Hooks # includes #create_object and #write_object.
66
- include Representable::Binding::Extend
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
- private
80
- # Deserializes the ref's element from +xml+.
81
- def value_from_node(xml)
82
- collect_for(xml) do |node|
83
- create_object.from_node(node)
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 write_entity(xml, entity)
88
- xml.add_child(write_object(entity).to_node)
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, :sought_type, :from, :default, :representer_module, :attribute
4
+ attr_reader :name, :options
5
5
  alias_method :getter, :name
6
6
 
7
7
  def initialize(sym, options={})
8
- @name = sym.to_s
9
- @array = options[:collection]
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
- @array
32
- end
33
-
34
- # Applies the block to +value+ which might also be a collection.
35
- def apply(value)
36
- return value unless value # DISCUSS: is that ok here?
37
-
38
- if array?
39
- value = value.collect do |item|
40
- yield item
41
- end
42
- else
43
- value = yield value
44
- end
45
-
46
- value
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