representable 0.11.0 → 0.12.0

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