representable 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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