representable 2.0.4 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9ae87cf0d1e79ff26480467bc624e3df5633e2b5
4
- data.tar.gz: f46dd9c54daa7efb76af07add37e17780fb81b59
3
+ metadata.gz: 783116ac1174ef6a0aa6eaf44c768bfe3786dec0
4
+ data.tar.gz: bc85eaa1d218cd5c54161a607d80d6d3539937cf
5
5
  SHA512:
6
- metadata.gz: 77511aff2f7d3d88a31c29b4feeec1d4b2c4dd31ec612a3c7521994007adcef4541b89ed14bf54338e796f539023b2cb6cbefc40efe71c7699c30d720c81bcce
7
- data.tar.gz: 1983166eda318f1a45cbb349707aae61e2482952e9c29143a5791c6384ed9d4e7cfd1b0efc3a2d6ef083d9626e16e56aef9c6e857ad118775dbbe2d1d4e1859c
6
+ metadata.gz: 347783301a5545df8e009130eb741970d5635796368c3aae726f52e21f0357daefbfa367e7296b053aa14c8357ac1dcdff95b2cca828bfe2bf3f79fc34defe86
7
+ data.tar.gz: ff5d1a1825af70bee387b09ee7f997718744e7a69b66f80fb379dbbd726e1640aec81888dd651cbfa6c78c218342bfc2a5305f1bf741972a677037e3d6176894
data/CHANGES.md CHANGED
@@ -1,3 +1,20 @@
1
+ # 2.1.0
2
+
3
+ ## Breaking Changes
4
+
5
+ * None, unless you messed around with internals like `Binding`.
6
+
7
+ ## Changes
8
+
9
+ * Added `:skip_parse` to skip deserialization of fragments.
10
+ * It's now `Binding#read_fragment -> Populator -> Deserializer`. Mostly, this got changed to allow better support for complex collection semantics when populating/deserializing as found in Object-HAL.
11
+ * Likewise, it is `Binding#write_fragment -> Serializer`, clearly separating format-specific and generic logic.
12
+ * Make `Definition#inspect` more readable by filtering out some instance variables like `@runtime_options`.
13
+ * Remove `Binding#write_fragment_for`. This is `#render_fragment` now.
14
+ * Almost 100% speedup for rendering and parsing by removing Ruby's delegation and `method_missing`.
15
+ * Bindings are now in the following naming format: `Representable::Hash::Binding[::Collection]`. The file name is `representable/hash/binding`.
16
+ * Move `Definition#skipable_empty_value?` and `Definition#default_for` to `Binding` as it is runtime-specific.
17
+
1
18
  # 2.0.4
2
19
 
3
20
  * Fix implicit rendering of JSON and XML collections where json/collection wasn't loaded properly, resulting in the native JSON's `#to_json` to be called.
data/README.md CHANGED
@@ -527,6 +527,7 @@ Here's a list of all dynamic options and their argument signature.
527
527
  * `instance: lambda { |fragment, [i], args| }` ([see Object Creation](#polymorphic-object-creation))
528
528
  * `reader: lambda { |document, args| }` ([see Read And Write](#overriding-read-and-write))
529
529
  * `writer: lambda { |document, args| }` ([see Read And Write](#overriding-read-and-write))
530
+ * `skip_parse: lambda { |fragment, args| }` ([see Skip Parsing](#skip-parsing))
530
531
  * `parse_filter: lambda { |fragment, document, args| }` ([see Filters](#filters)))
531
532
  * `render_filter: lambda { |value, document, args| }` ([see Filters](#filters))
532
533
  * `if: lambda { |args| }` ([see Conditions](#conditions))
@@ -571,6 +572,24 @@ property :title, parse_filter: lambda { |fragment, doc, *args| Sanitizer.call(fr
571
572
  Just before setting the fragment to the object via the `:setter`, the `:parse_filter` is called.
572
573
 
573
574
 
575
+ ## Skip Parsing
576
+
577
+ You can skip parsing for particular fragments which will completely ignore them as if they weren't present in the parsed document.
578
+
579
+ ```ruby
580
+ property :title, skip_parse: lambda { |fragment, options| fragment.blank? }
581
+ ```
582
+
583
+ Note that when used with collections, this is evaluated per item.
584
+
585
+ ```ruby
586
+ collection :songs, skip_parse: lambda { |fragment, options| fragment["title"].blank? } do
587
+ property :title
588
+ end
589
+ ```
590
+
591
+ This won't parse empty incoming songs in the collection.
592
+
574
593
  ## Callable Options
575
594
 
576
595
  While lambdas are one option for dynamic options, you might also pass a "callable" object to a directive.
@@ -838,7 +857,7 @@ module AlbumRepresenter
838
857
  end
839
858
  ```
840
859
 
841
- The block for `:class` receives the currently parsed fragment. Here, this might be somthing like `{"title"=>"Weirdo", "track"=>5}`.
860
+ The block for `:class` receives the currently parsed fragment. Here, this might be something like `{"title"=>"Weirdo", "track"=>5}`.
842
861
 
843
862
  If this is not enough, you may override the entire object creation process using `:instance`.
844
863
 
@@ -41,7 +41,9 @@ private
41
41
 
42
42
  def representable_bindings_for(format, options)
43
43
  options = cleanup_options(options) # FIXME: make representable-options and user-options two different hashes.
44
+
44
45
  representable_attrs.collect {|attr| representable_binding_for(attr, format, options) }
46
+ # representable_attrs.binding_cache[format] ||= representable_attrs.collect {|attr| representable_binding_for(attr, format, options) }
45
47
  end
46
48
 
47
49
  def representable_binding_for(attribute, format, options)
@@ -61,7 +63,6 @@ private
61
63
  Mapper.new(bindings, represented, options) # TODO: remove self, or do we need it? and also represented!
62
64
  end
63
65
 
64
-
65
66
  def representation_wrap(*args)
66
67
  representable_attrs.wrap_for(self.class.name, represented, *args)
67
68
  end
@@ -1,8 +1,14 @@
1
+ require "representable/populator"
1
2
  require "representable/deserializer"
2
3
  require "representable/serializer"
3
4
 
4
5
  module Representable
5
6
  # The Binding wraps the Definition instance for this property and provides methods to read/write fragments.
7
+
8
+ # The flow when parsing is Binding#read_fragment -> Populator -> Deserializer.
9
+ # Actual parsing the fragment from the document happens in Binding#read, everything after that is generic.
10
+ #
11
+ # Serialization: Serializer -> {frag}/[frag]/frag -> Binding#write
6
12
  class Binding
7
13
  class FragmentNotFound
8
14
  end
@@ -15,11 +21,8 @@ module Representable
15
21
 
16
22
  def initialize(definition, represented, decorator, user_options={}) # TODO: remove default arg for user options.
17
23
  @definition = definition
18
- @represented = represented
19
- @decorator = decorator
20
- @user_options = user_options
21
24
 
22
- setup_exec_context!
25
+ setup!(represented, decorator, user_options) # this can be used in #compile_fragment/#uncompile_fragment in case we wanna reuse the Binding instance.
23
26
  end
24
27
 
25
28
  attr_reader :user_options, :represented # TODO: make private/remove.
@@ -39,37 +42,28 @@ module Representable
39
42
  # Parse value from doc and update the model property.
40
43
  def uncompile_fragment(doc)
41
44
  evaluate_option(:reader, doc) do
42
- read_fragment(doc) do |value|
43
- value = parse_filter(value, doc)
44
- set(value)
45
- end
45
+ read_fragment(doc)
46
46
  end
47
47
  end
48
48
 
49
49
  def write_fragment(doc, value)
50
50
  value = default_for(value)
51
51
 
52
- write_fragment_for(value, doc)
53
- end
54
-
55
- def write_fragment_for(value, doc)
56
52
  return if skipable_empty_value?(value)
57
- write(doc, value)
58
- end
59
53
 
60
- def read_fragment(doc)
61
- value = read_fragment_for(doc)
54
+ render_fragment(value, doc)
55
+ end
62
56
 
63
- if value == FragmentNotFound
64
- return unless has_default?
65
- value = self[:default]
66
- end
57
+ def render_fragment(value, doc)
58
+ fragment = serialize(value) # render fragments of hash, xml, yaml.
67
59
 
68
- yield value
60
+ write(doc, fragment)
69
61
  end
70
62
 
71
- def read_fragment_for(doc)
72
- read(doc)
63
+ def read_fragment(doc)
64
+ fragment = read(doc) # scalar, Array, or Hash (abstract format) or un-deserialised fragment(s).
65
+
66
+ populator.call(fragment, doc)
73
67
  end
74
68
 
75
69
  def render_filter(value, doc)
@@ -98,11 +92,75 @@ module Representable
98
92
  evaluate_option(:extend, object) # TODO: pass args? do we actually have args at the time this is called (compile-time)?
99
93
  end
100
94
 
95
+ # Evaluate the option (either nil, static, a block or an instance method call) or
96
+ # executes passed block when option not defined.
97
+ def evaluate_option(name, *args)
98
+ unless proc = self[name]
99
+ return yield if block_given?
100
+ return
101
+ end
102
+
103
+ # TODO: it would be better if user_options was nil per default and then we just don't pass it into lambdas.
104
+ options = self[:pass_options] ? Options.new(self, user_options, represented, decorator) : user_options
105
+
106
+ proc.evaluate(exec_context, *(args<<options)) # from Uber::Options::Value.
107
+ end
108
+
109
+ def [](name)
110
+ @definition[name]
111
+ end
112
+ # TODO: i don't want to define all methods here, but it is faster!
113
+ # TODO: test public interface.
114
+ def getter
115
+ @definition.getter
116
+ end
117
+ def setter
118
+ @definition.setter
119
+ end
120
+ def typed?
121
+ @definition.typed?
122
+ end
123
+ def representable?
124
+ @definition.representable?
125
+ end
126
+ def has_default?(*args)
127
+ @definition.has_default?(*args)
128
+ end
129
+ def name
130
+ @definition.name
131
+ end
132
+ def representer_module
133
+ @definition.representer_module
134
+ end
135
+ # perf : 1.7-1.9
136
+ #extend Forwardable
137
+ #def_delegators :@definition, *%w([] getter setter typed? representable? has_default? name representer_module)
138
+ # perf : 1.7-1.9
139
+ # %w([] getter setter typed? representable? has_default? name representer_module).each do |name|
140
+ # define_method(name.to_sym) { |*args| @definition.send(name, *args) }
141
+ # end
142
+
143
+ def skipable_empty_value?(value)
144
+ return true if array? and self[:render_empty] == false and value and value.size == 0 # TODO: change in 2.0, don't render emtpy.
145
+ value.nil? and not self[:render_nil]
146
+ end
147
+
148
+ def default_for(value)
149
+ return self[:default] if skipable_empty_value?(value)
150
+ value
151
+ end
152
+
153
+ def array?
154
+ @definition.array?
155
+ end
156
+
101
157
  private
102
- # Apparently, SimpleDelegator is super slow due to a regex, so we do it
103
- # ourselves, right, Jimmy?
104
- def method_missing(*args, &block)
105
- @definition.send(*args, &block)
158
+ def setup!(represented, decorator, user_options)
159
+ @represented = represented
160
+ @decorator = decorator
161
+ @user_options = user_options
162
+
163
+ setup_exec_context!
106
164
  end
107
165
 
108
166
  def setup_exec_context!
@@ -115,18 +173,24 @@ module Representable
115
173
 
116
174
  attr_reader :exec_context, :decorator
117
175
 
118
- # Evaluate the option (either nil, static, a block or an instance method call) or
119
- # executes passed block when option not defined.
120
- def evaluate_option(name, *args)
121
- unless proc = self[name]
122
- return yield if block_given?
123
- return
124
- end
176
+ def serialize(object)
177
+ serializer.call(object)
178
+ end
125
179
 
126
- # TODO: it would be better if user_options was nil per default and then we just don't pass it into lambdas.
127
- options = self[:pass_options] ? Options.new(self, user_options, represented, decorator) : user_options
180
+ def serializer_class
181
+ Serializer
182
+ end
128
183
 
129
- proc.evaluate(exec_context, *(args<<options)) # from Uber::Options::Value.
184
+ def serializer
185
+ serializer_class.new(self)
186
+ end
187
+
188
+ def populator
189
+ populator_class.new(self)
190
+ end
191
+
192
+ def populator_class
193
+ Populator
130
194
  end
131
195
 
132
196
 
@@ -135,35 +199,27 @@ module Representable
135
199
  Options = Struct.new(:binding, :user_options, :represented, :decorator)
136
200
 
137
201
 
138
- # Delegates to call #to_*/from_*.
139
- module Object
140
- def serialize(object)
141
- ObjectSerializer.new(self, object).call
142
- end
143
-
144
- def deserialize(data)
145
- # DISCUSS: does it make sense to skip deserialization of nil-values here?
146
- ObjectDeserializer.new(self).call(data)
202
+ # generics for collection bindings.
203
+ module Collection
204
+ private
205
+ def populator_class
206
+ Populator::Collection
147
207
  end
148
208
 
149
- def create_object(fragment, *args)
150
- instance_for(fragment, *args) or class_for(fragment, *args)
209
+ def serializer_class
210
+ Serializer::Collection
151
211
  end
212
+ end
152
213
 
214
+ # and the same for hashes.
215
+ module Hash
153
216
  private
154
- # DISCUSS: deprecate :class in favour of :instance and simplicity?
155
- def class_for(fragment, *args)
156
- item_class = class_from(fragment, *args) or raise DeserializeError.new(":class did not return class constant.")
157
- item_class.new
158
- end
159
-
160
- def class_from(fragment, *args)
161
- evaluate_option(:class, fragment, *args)
217
+ def populator_class
218
+ Populator::Hash
162
219
  end
163
220
 
164
- def instance_for(fragment, *args)
165
- # cool: if no :instance set, { return } will jump out of this method.
166
- evaluate_option(:instance, fragment, *args) { return } or raise DeserializeError.new(":instance did not return object.")
221
+ def serializer_class
222
+ Serializer::Hash
167
223
  end
168
224
  end
169
225
  end
@@ -1,3 +1,7 @@
1
+ # Caching of Bindings
2
+ # in Decorators, this could be an instance class var. when inherited, it is automatically busted.
3
+ # however, in modules we can't do that. we never know when a module is "finalised", so we don't really know when to bust the cache.
4
+
1
5
  module Representable
2
6
  # Config contains three independent, inheritable directives: features, options and definitions.
3
7
  # It is a hash - just add directives if you need them.
@@ -77,6 +81,10 @@ module Representable
77
81
  value
78
82
  end
79
83
 
84
+ # def binding_cache
85
+ # @binding_cache ||= {}
86
+ # end
87
+
80
88
  private
81
89
  def infer_name_for(name)
82
90
  name.to_s.split('::').last.
@@ -33,8 +33,9 @@ module Representable
33
33
  self
34
34
  end
35
35
 
36
- extend Forwardable
37
- def_delegators :@runtime_options, :[], :each
36
+ def [](name)
37
+ @runtime_options[name]
38
+ end
38
39
 
39
40
  def clone
40
41
  self.class.new(name, @options.clone)
@@ -49,7 +50,7 @@ module Representable
49
50
  end
50
51
 
51
52
  def representable?
52
- return if self[:representable] === false
53
+ return if self[:representable] == false
53
54
  self[:representable] or typed?
54
55
  end
55
56
 
@@ -61,11 +62,6 @@ module Representable
61
62
  self[:hash]
62
63
  end
63
64
 
64
- def default_for(value)
65
- return self[:default] if skipable_empty_value?(value)
66
- value
67
- end
68
-
69
65
  def has_default?
70
66
  @options.has_key?(:default)
71
67
  end
@@ -74,15 +70,15 @@ module Representable
74
70
  @options[:extend]
75
71
  end
76
72
 
77
- def skipable_empty_value?(value)
78
- return true if array? and self[:render_empty] == false and value and value.size == 0 # TODO: change in 2.0, don't render emtpy.
79
- value.nil? and not self[:render_nil]
80
- end
81
-
82
73
  def create_binding(*args)
83
74
  self[:binding].call(self, *args)
84
75
  end
85
76
 
77
+ def inspect
78
+ state = (instance_variables-[:@runtime_options, :@name]).collect { |ivar| "#{ivar}=#{instance_variable_get(ivar)}" }
79
+ "#<Representable::Definition ==>#{name} #{state.join(" ")}>"
80
+ end
81
+
86
82
  private
87
83
  def setup!(options, &block)
88
84
  handle_extend!(options)
@@ -109,7 +105,7 @@ module Representable
109
105
  end
110
106
 
111
107
  def dynamic_options
112
- [:as, :getter, :setter, :class, :instance, :reader, :writer, :extend, :prepare, :if, :deserialize, :serialize, :render_filter, :parse_filter]
108
+ [:as, :getter, :setter, :class, :instance, :reader, :writer, :extend, :prepare, :if, :deserialize, :serialize, :render_filter, :parse_filter, :skip_parse]
113
109
  end
114
110
 
115
111
  def handle_extend!(options)
@@ -1,24 +1,14 @@
1
1
  module Representable
2
- class CollectionDeserializer
3
- def initialize(binding) # TODO: get rid of binding dependency
4
- @binding = binding
5
- end
6
-
7
- def deserialize(fragment)
8
- # puts "deserialize #{@binding.name}" # TODO: introduce Representable::Debug.
9
-
10
- # next step: get rid of collect.
11
- fragment.enum_for(:each_with_index).collect do |item_fragment, i|
12
- @deserializer = ObjectDeserializer.new(@binding)
13
-
14
- @deserializer.call(item_fragment, i) # FIXME: what if obj nil?
15
- end
16
- end
17
- end
18
-
19
-
20
- class ObjectDeserializer
21
- # dependencies: Def#options, Def#create_object
2
+ # Deserializer's job is deserializing the already parsed fragment into a scalar or an object.
3
+ # This object is then returned to the Populator.
4
+ #
5
+ # It respects :deserialize, :prepare, :class, :instance
6
+ #
7
+ # Collection bindings return an array of parsed fragment items (still native format, e.g. Nokogiri node, for nested objects).
8
+ #
9
+ # Workflow
10
+ # call -> instance/class -> prepare -> deserialize -> from_json.
11
+ class Deserializer
22
12
  def initialize(binding)
23
13
  @binding = binding
24
14
  end
@@ -27,9 +17,7 @@ module Representable
27
17
  return fragment unless @binding.typed? # customize with :extend. this is not really straight-forward.
28
18
 
29
19
  # what if create_object is responsible for providing the deserialize-to object?
30
- object = @binding.create_object(fragment, *args) # customize with :instance and :class.
31
-
32
- # DISCUSS: what parts should be in this class, what in Binding?
20
+ object = create_object(fragment, *args) # customize with :instance and :class.
33
21
  representable = prepare(object) # customize with :prepare and :extend.
34
22
 
35
23
  deserialize(representable, fragment, @binding.user_options) # deactivate-able via :representable => false.
@@ -39,13 +27,13 @@ module Representable
39
27
  def deserialize(object, fragment, options) # TODO: merge with #serialize.
40
28
  return object unless @binding.representable?
41
29
 
42
- @binding.send(:evaluate_option, :deserialize, object, fragment) do
30
+ @binding.evaluate_option(:deserialize, object, fragment) do
43
31
  object.send(@binding.deserialize_method, fragment, options)
44
32
  end
45
33
  end
46
34
 
47
35
  def prepare(object)
48
- @binding.send(:evaluate_option, :prepare, object) do
36
+ @binding.evaluate_option(:prepare, object) do
49
37
  prepare!(object)
50
38
  end
51
39
  end
@@ -59,5 +47,56 @@ module Representable
59
47
  mod.prepare(object)
60
48
  end
61
49
  # in deserialize, we should get the original object?
50
+
51
+ def create_object(fragment, *args)
52
+ instance_for(fragment, *args) or class_for(fragment, *args)
53
+ end
54
+
55
+ def class_for(fragment, *args)
56
+ item_class = class_from(fragment, *args) or raise DeserializeError.new(":class did not return class constant.")
57
+ item_class.new
58
+ end
59
+
60
+ def class_from(fragment, *args)
61
+ @binding.evaluate_option(:class, fragment, *args)
62
+ end
63
+
64
+ def instance_for(fragment, *args)
65
+ # cool: if no :instance set, { return } will jump out of this method.
66
+ @binding.evaluate_option(:instance, fragment, *args) { return } or raise DeserializeError.new(":instance did not return object.")
67
+ end
68
+
69
+
70
+
71
+ # Collection does exactly the same as Deserializer but for a collection.
72
+ class Collection < self
73
+ def call(fragment)
74
+ collection = [] # this can be replaced, e.g. AR::Collection or whatever.
75
+
76
+ fragment.each_with_index do |item_fragment, i|
77
+ # add more per-item options here!
78
+ next if @binding.evaluate_option(:skip_parse, item_fragment)
79
+
80
+ collection << deserialize!(item_fragment, i) # FIXME: what if obj nil?
81
+ end
82
+
83
+ collection # with parse_strategy: :sync, this is ignored.
84
+ end
85
+
86
+ private
87
+ def deserialize!(*args)
88
+ # TODO: re-use deserializer.
89
+ Deserializer.new(@binding).call(*args)
90
+ end
91
+ end
92
+
93
+
94
+ class Hash < Collection
95
+ def call(hash)
96
+ {}.tap do |hsh|
97
+ hash.each { |key, fragment| hsh[key] = deserialize!(fragment) }
98
+ end
99
+ end
100
+ end
62
101
  end
63
102
  end