representable 0.11.0 → 0.12.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,6 +1,13 @@
1
+ h2. 0.12.0
2
+
3
+ * @:as@ is now @:class@.
4
+
5
+
1
6
  h2. 0.11.0
2
7
 
3
8
  * Representer modules can now be injected into objects using @#extend@.
9
+ * The @:extend@ option allows setting a representer module for a typed property. This will extend the contained object at runtime roughly following the DCI pattern.
10
+ * Renamed @#representable_property@ and @#representable_collection@ to @#property@ and @#collection@ as we don't have to fear namespace collisions in modules.
4
11
 
5
12
  h2. 0.10.3
6
13
 
@@ -14,7 +14,7 @@ This keeps your representation knowledge in one place when implementing REST ser
14
14
 
15
15
  * Bidirectional - rendering and parsing
16
16
  * OOP documents
17
- * Support for JSON and XML
17
+ * Support for JSON, XML and MessagePack
18
18
 
19
19
 
20
20
  == Example
@@ -26,33 +26,40 @@ Since you keep forgetting the heroes of your childhood you decide to implement a
26
26
 
27
27
  == Defining Representations
28
28
 
29
+ Representations are usually defined using a module. This makes them super flexibly, you'll see.
30
+
29
31
  require 'representable/json'
30
32
 
31
- class Hero
33
+ module HeroRepresenter
32
34
  include Representable::JSON
33
35
 
34
- representable_property :forename
35
- representable_property :surename
36
+ property :forename
37
+ property :surename
36
38
  end
37
39
 
38
- This declares two simple properties. Representable will automatically add accessors to the class.
40
+ By using #property we declare two simple attributes. Representable will automatically add accessors to the module.
39
41
 
40
- Alternatively, if you don't want declarations in your models, use a module.
42
+ To use your representer include it in the matching class. Note that you could reuse a representer in multiple classes.
41
43
 
42
- module HeroRepresentation
43
- include Representable
44
-
45
- representable_property :forename
46
- representable_property :surename
44
+ class Hero
45
+ include Representable
46
+ include HeroRepresenter
47
47
  end
48
48
 
49
- This module needs a host class to be used with.
49
+ Many people dislike including representers on class layer. You might also extend an object at runtime.
50
+
51
+ Hero.new.extend(HeroRepresenter)
52
+
53
+ Alternatively, if you don't like modules (which you shouldn't), declarations can be put into classes directly.
50
54
 
51
55
  class Hero
52
56
  include Representable::JSON
53
- include HeroRepresentation
57
+
58
+ property :forename
59
+ property :surename
54
60
  end
55
61
 
62
+
56
63
  == Rendering
57
64
 
58
65
  Now let's create and render our first hero.
@@ -77,12 +84,12 @@ See how easy this is? You can use an object-oriented method to read from the doc
77
84
 
78
85
  == Nesting
79
86
 
80
- You need a second domain object. Every hero has a place he comes from.
87
+ You need a second domain object. Every hero has a place it comes from.
81
88
 
82
89
  class Location
83
90
  include Representable::JSON
84
91
 
85
- representable_property :title
92
+ property :title
86
93
  end
87
94
 
88
95
  Peter, where ya' from?
@@ -92,11 +99,11 @@ Peter, where ya' from?
92
99
 
93
100
  It makes sense to embed the location in the hero's document.
94
101
 
95
- class Hero
96
- representable_property :origin, :as => Location
102
+ module HeroRepresenter
103
+ property :origin, :class => Location
97
104
  end
98
105
 
99
- Using the +:as+ option allows you to include other representable objects.
106
+ Using the +:class+ option allows you to include other representable objects.
100
107
 
101
108
  peter.origin = neverland
102
109
  peter.to_json
@@ -117,26 +124,26 @@ Representable just creates objects from the parsed document - nothing more and n
117
124
 
118
125
  Heroes have features, special abilities that make 'em a superhero.
119
126
 
120
- class Hero
121
- representable_collection :features
127
+ module HeroRepresenter
128
+ collection :features
122
129
  end
123
130
 
124
- The second API method is +representable_collection+ and, well, declares a collection.
131
+ The second representable API method is +collection+ and, well, declares a collection.
125
132
 
126
133
  peter.features = ["stays young", "can fly"]
127
134
  peter.to_json
128
- #=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"},"features":["stays young","can fly"]}
135
+ #=> {"forename":"Peter","surename":"Pan","origin":{"title":"Neverland"},"features":["stays young","can fly"]}
129
136
 
130
137
 
131
138
  == Typed Collections
132
139
 
133
- Ok, things start working out. Your hero has a name, an origin and a list of features so far. Why not allows adding friends to Peter - nobody wants to be alone!
140
+ Ok, things start working out. Your hero has a name, an origin and a list of features so far. Why not allow adding buddies to Peter - nobody wants to be alone!
134
141
 
135
- class Hero
136
- representable_collection :friends, :as => Hero
142
+ module HeroRepresenter
143
+ collection :friends, :class => Hero
137
144
  end
138
145
 
139
- Again, we type the collection by using the +:as+ option.
146
+ Again, we type the collection by using the +:class+ option.
140
147
 
141
148
  nick = Hero.new
142
149
  nick.forename = "Nick"
@@ -158,7 +165,7 @@ I always wanted to be Peter's bro... in this example it is possible!
158
165
 
159
166
  Representable is designed to be very simple. However, a few tweaks are available. What if you want to wrap your document?
160
167
 
161
- class Hero
168
+ module HeroRepresenter
162
169
  self.representation_wrap = true
163
170
  end
164
171
 
@@ -166,7 +173,7 @@ Representable is designed to be very simple. However, a few tweaks are available
166
173
 
167
174
  You can also provide a custom wrapper.
168
175
 
169
- class Hero
176
+ module HeroRepresenter
170
177
  self.representation_wrap = :boy
171
178
  end
172
179
 
@@ -177,23 +184,40 @@ You can also provide a custom wrapper.
177
184
 
178
185
  If your accessor name doesn't match the attribute name in the document, use the +:from+ matcher.
179
186
 
180
- class Hero
181
- representable_property :forename, :from => :name
187
+ module HeroRepresenter
188
+ property :forename, :from => :i_am_called
182
189
  end
183
190
 
184
- peter.to_json #=> {"name":"Peter","surename":"Pan"}
191
+ peter.to_json #=> {"i_am_called":"Peter","surename":"Pan"}
185
192
 
186
193
 
187
194
  === Filtering
188
195
 
189
- Representable allows you to skip properties when rendering or parsing.
190
-
191
- peter.to_json do |name|
192
- name == :forename
193
- end
196
+ Representable allows you to skip and include properties when rendering or parsing.
194
197
 
198
+ peter.to_json(:include => :forename)
195
199
  #=> {"forename":"Peter"}
196
200
 
201
+ It gives you convenient +:exclude+ and +:include+ options.
202
+
203
+
204
+ == DCI
205
+
206
+ Representers roughly follow the {DCI}[http://en.wikipedia.org/wiki/Data,_context_and_interaction] pattern when used on objects, only.
207
+
208
+ Hero.new.extend(HeroRepresenter)
209
+
210
+ The only difference is that you have to define which representers to use for typed properties.
211
+
212
+ module HeroRepresenter
213
+ property :forename
214
+ property :surename
215
+ collection :features
216
+ property :origin, :class => Location
217
+ collection :friends, :class => Hero, :extend => HeroRepresenter
218
+ end
219
+
220
+ There's no need to specify a representer for the +origin+ property since the +Location+ class statically includes its representation. For +friends+, we can use +:extend+ to tell representable which module to mix in dynamically.
197
221
 
198
222
  == XML support
199
223
 
@@ -3,6 +3,7 @@ require 'representable/definition'
3
3
  module Representable
4
4
  def self.included(base)
5
5
  base.class_eval do
6
+ extend ClassMethods
6
7
  extend ClassMethods::Declarations
7
8
  extend ClassMethods::Accessors
8
9
 
@@ -21,9 +22,9 @@ module Representable
21
22
  end
22
23
 
23
24
  # Reads values from +doc+ and sets properties accordingly.
24
- def update_properties_from(doc, &block)
25
+ def update_properties_from(doc, options, &block)
25
26
  representable_bindings.each do |bin|
26
- next if eval_property_block(bin, &block) # skip if block is false.
27
+ next if skip_property?(bin, options)
27
28
 
28
29
  value = bin.read(doc) || bin.definition.default
29
30
  send(bin.definition.setter, value)
@@ -33,9 +34,9 @@ module Representable
33
34
 
34
35
  private
35
36
  # Compiles the document going through all properties.
36
- def create_representation_with(doc, &block)
37
+ def create_representation_with(doc, options, &block)
37
38
  representable_bindings.each do |bin|
38
- next if eval_property_block(bin, &block) # skip if block is false.
39
+ next if skip_property?(bin, options)
39
40
 
40
41
  value = send(bin.definition.getter) || bin.definition.default # DISCUSS: eventually move back to Ref.
41
42
  bin.write(doc, value) if value
@@ -43,11 +44,11 @@ private
43
44
  doc
44
45
  end
45
46
 
46
- # Returns true unless a eventually given block returns false. Yields the symbolized
47
- # property name.
48
- def eval_property_block(binding)
49
- # TODO: no magic symbol conversion!
50
- block_given? and not yield binding.definition.name.to_sym
47
+ # Checks and returns if the property should be included.
48
+ def skip_property?(binding, options)
49
+ return unless props = options[:except] || options[:include]
50
+ res = props.include?(binding.definition.name.to_sym)
51
+ options[:include] ? !res : res
51
52
  end
52
53
 
53
54
  def representable_attrs
@@ -65,6 +66,13 @@ private
65
66
 
66
67
 
67
68
  module ClassMethods # :nodoc:
69
+ # Create and yield object and options. Called in .from_json and friends.
70
+ def create_represented(document, *args)
71
+ new.tap do |represented|
72
+ yield represented, *args if block_given?
73
+ end
74
+ end
75
+
68
76
  module Declarations
69
77
  def definition_class
70
78
  Definition
@@ -74,13 +82,13 @@ private
74
82
  #
75
83
  # Examples:
76
84
  #
77
- # representable_property :name
78
- # representable_property :name, :from => :title
79
- # representable_property :name, :as => Name
80
- # representable_property :name, :accessors => false
81
- # representable_property :name, :default => "Mike"
82
- def representable_property(name, options={})
83
- attr = add_representable_property(name, options)
85
+ # property :name
86
+ # property :name, :from => :title
87
+ # property :name, :class => Name
88
+ # property :name, :accessors => false
89
+ # property :name, :default => "Mike"
90
+ def property(name, options={})
91
+ attr = add_property(name, options)
84
92
 
85
93
  attr_reader(attr.getter) unless options[:accessors] == false
86
94
  attr_writer(attr.getter) unless options[:accessors] == false
@@ -90,22 +98,23 @@ private
90
98
  #
91
99
  # Examples:
92
100
  #
93
- # representable_collection :products
94
- # representable_collection :products, :from => :item
95
- # representable_collection :products, :as => Product
96
- def representable_collection(name, options={})
101
+ # collection :products
102
+ # collection :products, :from => :item
103
+ # collection :products, :class => Product
104
+ def collection(name, options={})
97
105
  options[:collection] = true
98
- representable_property(name, options)
106
+ property(name, options)
99
107
  end
100
108
 
101
109
  private
102
- def add_representable_property(*args)
110
+ def add_property(*args)
103
111
  definition_class.new(*args).tap do |attr|
104
112
  representable_attrs << attr
105
113
  end
106
114
  end
107
115
  end
108
-
116
+
117
+
109
118
  module Accessors
110
119
  def representable_attrs
111
120
  @representable_attrs ||= Config.new
@@ -117,6 +126,7 @@ private
117
126
  end
118
127
  end
119
128
 
129
+
120
130
  class Config < Array
121
131
  attr_accessor :wrap
122
132
 
@@ -135,4 +145,17 @@ private
135
145
  downcase
136
146
  end
137
147
  end
148
+
149
+
150
+ # Allows mapping formats to representer classes.
151
+ # DISCUSS: this module might be removed soon.
152
+ module Represents
153
+ def represents(format, options)
154
+ representer[format] = options[:with]
155
+ end
156
+
157
+ def representer
158
+ @represents_map ||= {}
159
+ end
160
+ end
138
161
  end
@@ -0,0 +1,47 @@
1
+ module Representable
2
+ class Binding
3
+ attr_reader :definition
4
+
5
+ def initialize(definition)
6
+ @definition = definition
7
+ end
8
+
9
+
10
+ # Usually called in concrete ObjectBinding in #write and #read.
11
+ module Hooks
12
+ private
13
+ # Must be called in serialization of concrete ObjectBinding.
14
+ def write_object(object)
15
+ object
16
+ end
17
+
18
+ # Creates a typed property instance.
19
+ def create_object
20
+ definition.sought_type.new
21
+ end
22
+ end
23
+
24
+
25
+ # Hooks into #write_object and #create_object to extend typed properties
26
+ # at runtime.
27
+ module Extend
28
+ private
29
+ # Extends the object with its representer before serialization.
30
+ def write_object(object)
31
+ extend_for(super)
32
+ end
33
+
34
+ def create_object
35
+ extend_for(super)
36
+ end
37
+
38
+ def extend_for(object) # TODO: test me.
39
+ if mod = definition.representer_module
40
+ object.extend(mod)
41
+ end
42
+
43
+ object
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,16 +1,8 @@
1
+ require 'representable/binding'
2
+
1
3
  module Representable
2
4
  module JSON
3
- class Binding
4
- attr_reader :definition
5
-
6
- def initialize(definition)
7
- @definition = definition
8
- end
9
-
10
- def read(hash)
11
- value_from_hash(hash)
12
- end
13
-
5
+ class Binding < Representable::Binding
14
6
  private
15
7
  def collect_for(hash)
16
8
  nodes = hash[definition.from] or return
@@ -27,9 +19,8 @@ module Representable
27
19
  def write(hash, value)
28
20
  hash[definition.from] = value
29
21
  end
30
-
31
- private
32
- def value_from_hash(hash)
22
+
23
+ def read(hash)
33
24
  collect_for(hash) do |value|
34
25
  value
35
26
  end
@@ -38,20 +29,27 @@ module Representable
38
29
 
39
30
  # Represents a tag with object binding.
40
31
  class ObjectBinding < Binding
41
- def write(hash, value)
32
+ include Representable::Binding::Hooks # includes #create_object and #write_object.
33
+ include Representable::Binding::Extend
34
+
35
+ def write(hash, object)
42
36
  if definition.array?
43
- hash[definition.from] = value.collect {|v| v.to_hash(:wrap => false)}
37
+ hash[definition.from] = object.collect { |obj| serialize(obj) }
44
38
  else
45
- hash[definition.from] = value.to_hash(:wrap => false)
39
+ hash[definition.from] = serialize(object)
46
40
  end
47
41
  end
48
-
49
- private
50
- def value_from_hash(hash)
42
+
43
+ def read(hash)
51
44
  collect_for(hash) do |node|
52
- definition.sought_type.from_hash(node) # call #from_hash as it's already deserialized.
45
+ create_object.from_hash(node)
53
46
  end
54
47
  end
48
+
49
+ private
50
+ def serialize(object)
51
+ write_object(object).to_hash(:wrap => false)
52
+ end
55
53
  end
56
54
  end
57
55
  end