representable 2.0.4 → 2.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.
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