representable 1.7.7 → 1.8.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +42 -8
  3. data/README.md +208 -55
  4. data/Rakefile +0 -6
  5. data/lib/representable.rb +39 -43
  6. data/lib/representable/binding.rb +59 -37
  7. data/lib/representable/bindings/hash_bindings.rb +3 -4
  8. data/lib/representable/bindings/xml_bindings.rb +10 -10
  9. data/lib/representable/bindings/yaml_bindings.rb +2 -2
  10. data/lib/representable/coercion.rb +1 -1
  11. data/lib/representable/config.rb +11 -5
  12. data/lib/representable/definition.rb +67 -35
  13. data/lib/representable/deserializer.rb +23 -27
  14. data/lib/representable/hash.rb +15 -4
  15. data/lib/representable/hash/allow_symbols.rb +27 -0
  16. data/lib/representable/json.rb +0 -1
  17. data/lib/representable/json/collection.rb +0 -2
  18. data/lib/representable/mapper.rb +6 -13
  19. data/lib/representable/parse_strategies.rb +57 -0
  20. data/lib/representable/readable_writeable.rb +29 -0
  21. data/lib/representable/serializer.rb +9 -4
  22. data/lib/representable/version.rb +1 -1
  23. data/lib/representable/xml.rb +1 -1
  24. data/lib/representable/xml/collection.rb +0 -2
  25. data/lib/representable/yaml.rb +0 -1
  26. data/representable.gemspec +1 -0
  27. data/test/as_test.rb +43 -0
  28. data/test/class_test.rb +124 -0
  29. data/test/config_test.rb +13 -3
  30. data/test/decorator_scope_test.rb +28 -0
  31. data/test/definition_test.rb +46 -35
  32. data/test/exec_context_test.rb +93 -0
  33. data/test/generic_test.rb +0 -154
  34. data/test/getter_setter_test.rb +28 -0
  35. data/test/hash_bindings_test.rb +35 -35
  36. data/test/hash_test.rb +0 -20
  37. data/test/if_test.rb +78 -0
  38. data/test/inherit_test.rb +21 -1
  39. data/test/inheritance_test.rb +1 -1
  40. data/test/inline_test.rb +40 -2
  41. data/test/instance_test.rb +286 -0
  42. data/test/is_representable_test.rb +77 -0
  43. data/test/json_test.rb +6 -29
  44. data/test/nested_test.rb +30 -0
  45. data/test/parse_strategy_test.rb +249 -0
  46. data/test/pass_options_test.rb +27 -0
  47. data/test/prepare_test.rb +67 -0
  48. data/test/reader_writer_test.rb +19 -0
  49. data/test/representable_test.rb +25 -265
  50. data/test/stringify_hash_test.rb +41 -0
  51. data/test/test_helper.rb +12 -4
  52. data/test/wrap_test.rb +48 -0
  53. data/test/xml_bindings_test.rb +37 -37
  54. data/test/xml_test.rb +14 -14
  55. metadata +94 -30
  56. data/lib/representable/deprecations.rb +0 -4
  57. data/lib/representable/feature/readable_writeable.rb +0 -30
data/Rakefile CHANGED
@@ -9,9 +9,3 @@ Rake::TestTask.new(:test) do |test|
9
9
  test.test_files = FileList['test/*_test.rb']
10
10
  test.verbose = true
11
11
  end
12
-
13
- Rake::TestTask.new(:test18) do |test|
14
- test.libs << 'test'
15
- test.test_files = FileList['test/*_test.rb'] - ['test/mongoid_test.rb', 'test/yaml_test.rb']
16
- test.verbose = true
17
- end
@@ -1,4 +1,3 @@
1
- require 'representable/deprecations'
2
1
  require 'representable/definition'
3
2
  require 'representable/mapper'
4
3
  require 'representable/config'
@@ -12,8 +11,6 @@ module Representable
12
11
  extend ClassMethods
13
12
  extend ClassMethods::Declarations
14
13
  extend DSLAdditions
15
-
16
- include Deprecations
17
14
  end
18
15
  end
19
16
 
@@ -35,9 +32,7 @@ private
35
32
  end
36
33
 
37
34
  def representable_binding_for(attribute, format, options)
38
- context = attribute.options[:decorator_scope] ? self : represented # DISCUSS: pass both represented and representer into Binding and do it there?
39
-
40
- format.build(attribute, represented, options, context)
35
+ format.build(attribute, represented, self, options)
41
36
  end
42
37
 
43
38
  def cleanup_options(options) # TODO: remove me. this clearly belongs in Representable.
@@ -54,8 +49,8 @@ private
54
49
  end
55
50
 
56
51
 
57
- def representation_wrap
58
- representable_attrs.wrap_for(self.class.name) # FIXME: where is this needed?
52
+ def representation_wrap(*args)
53
+ representable_attrs.wrap_for(self.class.name, represented, *args)
59
54
  end
60
55
 
61
56
  def represented
@@ -105,28 +100,10 @@ private
105
100
  representable_attrs.wrap = name
106
101
  end
107
102
 
108
- # Declares a represented document node, which is usually a XML tag or a JSON key.
109
- #
110
- # Examples:
111
- #
112
- # property :name
113
- # property :name, :from => :title
114
- # property :name, :class => Name
115
- # property :name, :default => "Mike"
116
- # property :name, :render_nil => true
117
- # property :name, :readable => false
118
- # property :name, :writeable => false
119
103
  def property(name, options={}, &block)
120
104
  representable_attrs << definition_class.new(name, options)
121
105
  end
122
106
 
123
- # Declares a represented document node collection.
124
- #
125
- # Examples:
126
- #
127
- # collection :products
128
- # collection :products, :from => :item
129
- # collection :products, :class => Product
130
107
  def collection(name, options={}, &block)
131
108
  options[:collection] = true # FIXME: don't override original.
132
109
  property(name, options, &block)
@@ -155,25 +132,30 @@ private
155
132
  # Allows you to nest a block of properties in a separate section while still mapping them to the outer object.
156
133
  def nested(name, options={}, &block)
157
134
  options = options.merge(
158
- :decorator => true,
159
- :getter => lambda { |*| self },
160
- :setter => lambda { |*| },
161
- :instance => lambda { |*| self }
162
- )
135
+ :use_decorator => true,
136
+ :getter => lambda { |*| self },
137
+ :setter => lambda { |*| },
138
+ :instance => lambda { |*| self }
139
+ ) # DISCUSS: should this be a macro just as :parse_strategy?
163
140
 
164
141
  property(name, options, &block)
165
142
  end
166
143
 
167
144
  def property(name, options={}, &block)
168
- parent = representable_attrs[name]
145
+ modules = []
146
+
147
+ if options[:inherit] # TODO: move this to Definition.
148
+ parent = representable_attrs[name]
149
+ modules << parent[:extend].evaluate(nil) if parent[:extend]# we can savely assume this is _not_ a lambda. # DISCUSS: leave that in #representer_module?
150
+ end # FIXME: can we handle this in super/Definition.new ?
169
151
 
170
152
  if block_given?
171
- options[:extend] = inline_representer_for(parent, name, options, &block) # FIXME: passing parent sucks since we don't use it all the time.
153
+ handle_deprecated_inline_extend!(modules, options)
154
+
155
+ options[:extend] = inline_representer_for(modules, name, options, &block)
172
156
  end
173
157
 
174
- if options[:inherit]
175
- parent.options.merge!(options) and return parent
176
- end # FIXME: can we handle this in super/Definition.new ?
158
+ return parent.merge!(options) if options.delete(:inherit)
177
159
 
178
160
  super
179
161
  end
@@ -186,16 +168,30 @@ private
186
168
  end
187
169
 
188
170
  private
189
- def inline_representer_for(parent, name, options, &block)
190
- representer = options[:decorator] ? Decorator : self
191
-
192
- modules = [representer_engine]
193
- modules << parent.representer_module if options[:inherit]
194
- modules << options[:extend]
171
+ def inline_representer_for(modules, name, options, &block)
172
+ representer = options[:use_decorator] ? Decorator : self
173
+ modules = [representer_engine] + modules
195
174
 
196
175
  representer.inline_representer(modules.compact.reverse, name, options, &block)
197
176
  end
177
+
178
+ def handle_deprecated_inline_extend!(modules, options) # TODO: remove in 2.0.
179
+ return unless include_module = options.delete(:extend) and not options[:inherit]
180
+
181
+ warn "[Representable] Using :extend with an inline representer is deprecated. Include the module in the inline block."
182
+ modules << include_module
183
+ end
198
184
  end # DSLAdditions
199
185
  end
200
186
 
201
- require 'representable/decorator'
187
+
188
+ module Representable
189
+ autoload :Hash, 'representable/hash'
190
+
191
+ module Hash
192
+ autoload :AllowSymbols, 'representable/hash/allow_symbols'
193
+ autoload :Collection, 'representable/hash/collection'
194
+ end
195
+
196
+ autoload :Decorator, 'representable/decorator'
197
+ end
@@ -10,29 +10,35 @@ module Representable
10
10
 
11
11
  def self.build(definition, *args)
12
12
  # DISCUSS: move #create_binding to this class?
13
- return definition.create_binding(*args) if definition.binding
13
+ return definition.create_binding(*args) if definition[:binding]
14
14
  build_for(definition, *args)
15
15
  end
16
16
 
17
- def initialize(definition, represented, user_options={}, exec_context=represented) # TODO: remove default arg for user options. # DISCUSS: make exec_context an options hash?
17
+ def initialize(definition, represented, decorator, user_options={}) # TODO: remove default arg for user options.
18
18
  super(definition)
19
19
  @represented = represented
20
+ @decorator = decorator
20
21
  @user_options = user_options
21
- @exec_context = exec_context
22
+
23
+ setup_exec_context!
22
24
  end
23
25
 
24
26
  attr_reader :user_options, :represented # TODO: make private/remove.
25
27
 
28
+ def as # DISCUSS: private?
29
+ evaluate_option(:as)
30
+ end
31
+
26
32
  # Retrieve value and write fragment to the doc.
27
33
  def compile_fragment(doc)
28
- represented_exec_for(:writer, doc) do
34
+ evaluate_option(:writer, doc) do
29
35
  write_fragment(doc, get)
30
36
  end
31
37
  end
32
38
 
33
39
  # Parse value from doc and update the model property.
34
40
  def uncompile_fragment(doc)
35
- represented_exec_for(:reader, doc) do
41
+ evaluate_option(:reader, doc) do
36
42
  read_fragment(doc) do |value|
37
43
  set(value)
38
44
  end
@@ -55,7 +61,7 @@ module Representable
55
61
 
56
62
  if value == FragmentNotFound
57
63
  return unless has_default?
58
- value = default
64
+ value = self[:default]
59
65
  end
60
66
 
61
67
  yield value
@@ -65,47 +71,52 @@ module Representable
65
71
  read(doc)
66
72
  end
67
73
 
68
- # concept: Option#call(*args) => send(string)/lambda()
69
- # dynamic string
70
74
  def get
71
- represented_exec_for(:getter) do
75
+ evaluate_option(:getter) do
72
76
  exec_context.send(getter)
73
77
  end
74
78
  end
75
79
 
76
80
  def set(value)
77
- represented_exec_for(:setter, value) do
81
+ evaluate_option(:setter, value) do
78
82
  exec_context.send(setter, value)
79
83
  end
80
84
  end
81
85
 
82
- # the remaining methods in this class are format-independent and should be in Definition.
86
+ # DISCUSS: do we really need that?
87
+ def representer_module_for(object, *args)
88
+ evaluate_option(:extend, object) # TODO: pass args? do we actually have args at the time this is called (compile-time)?
89
+ end
83
90
 
84
91
  private
85
- attr_reader :exec_context
92
+ def setup_exec_context!
93
+ context = represented
94
+ context = self if self[:exec_context] == :binding
95
+ context = decorator if self[:exec_context] == :decorator
86
96
 
87
- # Execute the block for +option_name+ on the represented object.
88
- # Executes passed block when there's no lambda for option.
89
- def represented_exec_for(option_name, *args)
90
- return yield unless options[option_name]
91
- call_proc_for(options[option_name], *args)
92
- end
93
-
94
- # All lambdas are executed on exec_context which is either represented or the decorator instance.
95
- def call_proc_for(proc, *args)
96
- return proc unless proc.is_a?(Proc)
97
- # TODO: call method when proc is sympbol.
98
- args << user_options # DISCUSS: we assume user_options is a Hash!
99
- exec_context.instance_exec(*args, &proc)
97
+ @exec_context = context
100
98
  end
101
99
 
100
+ attr_reader :exec_context, :decorator
102
101
 
103
- module Prepare
104
- def representer_module_for(object, *args)
105
- call_proc_for(representer_module, object) # TODO: how to pass additional data to the computing block?`
102
+ # Evaluate the option (either nil, static, a block or an instance method call) or
103
+ # executes passed block when option not defined.
104
+ def evaluate_option(name, *args)
105
+ unless proc = self[name]
106
+ return yield if block_given?
107
+ return
106
108
  end
109
+
110
+ # TODO: it would be better if user_options was nil per default and then we just don't pass it into lambdas.
111
+ options = self[:pass_options] ? Options.new(self, user_options, represented, decorator) : user_options
112
+
113
+ proc.evaluate(exec_context, *(args<<options)) # from Uber::Options::Value.
107
114
  end
108
- include Prepare
115
+
116
+
117
+ # Options instance gets passed to lambdas when pass_options: true.
118
+ # This is considered the new standard way and should be used everywhere for forward-compat.
119
+ Options = Struct.new(:binding, :user_options, :represented, :decorator)
109
120
 
110
121
 
111
122
  # Delegates to call #to_*/from_*.
@@ -114,28 +125,39 @@ module Representable
114
125
  ObjectSerializer.new(self, object).call
115
126
  end
116
127
 
117
- def deserialize(data, object=lambda { get })
128
+ def deserialize(data)
118
129
  # DISCUSS: does it make sense to skip deserialization of nil-values here?
119
- ObjectDeserializer.new(self, object).call(data)
130
+ ObjectDeserializer.new(self).call(data)
120
131
  end
121
132
 
122
- def create_object(fragment)
123
- instance_for(fragment) or class_for(fragment)
133
+ def create_object(fragment, *args)
134
+ instance_for(fragment, *args) or class_for(fragment, *args)
124
135
  end
125
136
 
126
137
  private
127
138
  def class_for(fragment, *args)
128
- item_class = class_from(fragment) or return fragment
139
+ item_class = class_from(fragment, *args) or return handle_deprecated_class(fragment)
129
140
  item_class.new
130
141
  end
131
142
 
132
143
  def class_from(fragment, *args)
133
- call_proc_for(deserialize_class, fragment)
144
+ evaluate_option(:class, fragment, *args)
134
145
  end
135
146
 
136
147
  def instance_for(fragment, *args)
137
- return unless options[:instance]
138
- call_proc_for(options[:instance], fragment) or get
148
+ instance = evaluate_option(:instance, fragment, *args)
149
+
150
+ if instance === true # TODO: remove in 2.0.
151
+ warn "[Representable] `instance: lambda { true }` is deprecated. Apparently, you know what you're doing, so use `parse_strategy: :sync` instead."
152
+ return get
153
+ end
154
+
155
+ instance
156
+ end
157
+
158
+ def handle_deprecated_class(fragment) # TODO: remove in 2.0.
159
+ warn "[Representable] `class: lambda { nil }` is deprecated. To return the fragment from parsing, use `instance: lambda { |fragment, *args| fragment }` instead."
160
+ fragment
139
161
  end
140
162
  end
141
163
  end
@@ -12,14 +12,14 @@ module Representable
12
12
  end
13
13
 
14
14
  def read(hash)
15
- return FragmentNotFound unless hash.has_key?(from) # DISCUSS: put it all in #read for performance. not really sure if i like returning that special thing.
15
+ return FragmentNotFound unless hash.has_key?(as) # DISCUSS: put it all in #read for performance. not really sure if i like returning that special thing.
16
16
 
17
- fragment = hash[from]
17
+ fragment = hash[as]
18
18
  deserialize(fragment)
19
19
  end
20
20
 
21
21
  def write(hash, value)
22
- hash[from] = serialize(value)
22
+ hash[as] = serialize(value)
23
23
  end
24
24
 
25
25
  def deserialize_from(fragment)
@@ -37,7 +37,6 @@ module Representable
37
37
 
38
38
  class CollectionBinding < PropertyBinding
39
39
  def serialize(value)
40
- # value.enum_for(:each_with_index).collect { |obj, i| serialize(obj, i) } # DISCUSS: provide ary index/hash key for representer_module_for?
41
40
  value.collect { |item| super(item) } # TODO: i don't want Array but Forms here - what now?
42
41
  end
43
42
 
@@ -8,17 +8,17 @@ module Representable
8
8
 
9
9
  def self.build_for(definition, *args)
10
10
  return CollectionBinding.new(definition, *args) if definition.array?
11
- return HashBinding.new(definition, *args) if definition.hash? and not definition.options[:use_attributes] # FIXME: hate this.
12
- return AttributeHashBinding.new(definition, *args) if definition.hash? and definition.options[:use_attributes]
13
- return AttributeBinding.new(definition, *args) if definition.attribute
14
- return ContentBinding.new(definition, *args) if definition.content
11
+ return HashBinding.new(definition, *args) if definition.hash? and not definition[:use_attributes] # FIXME: hate this.
12
+ return AttributeHashBinding.new(definition, *args) if definition.hash? and definition[:use_attributes]
13
+ return AttributeBinding.new(definition, *args) if definition[:attribute]
14
+ return ContentBinding.new(definition, *args) if definition[:content]
15
15
  new(definition, *args)
16
16
  end
17
17
 
18
18
  def write(parent, value)
19
19
  wrap_node = parent
20
20
 
21
- if wrap = options[:wrap]
21
+ if wrap = self[:wrap]
22
22
  parent << wrap_node = node_for(parent, wrap)
23
23
  end
24
24
 
@@ -35,7 +35,7 @@ module Representable
35
35
  # Creates wrapped node for the property.
36
36
  def serialize_for(value, parent)
37
37
  #def serialize_for(value, parent, tag_name=definition.from)
38
- node = node_for(parent, from)
38
+ node = node_for(parent, as)
39
39
  serialize_node(node, value)
40
40
  end
41
41
 
@@ -62,12 +62,12 @@ module Representable
62
62
 
63
63
  private
64
64
  def xpath
65
- from
65
+ as
66
66
  end
67
67
 
68
68
  def find_nodes(doc)
69
69
  selector = xpath
70
- selector = "#{options[:wrap]}/#{xpath}" if options[:wrap]
70
+ selector = "#{self[:wrap]}/#{xpath}" if self[:wrap]
71
71
  nodes = doc.xpath(selector)
72
72
  end
73
73
 
@@ -143,11 +143,11 @@ module Representable
143
143
  # Represents a tag attribute. Currently this only works on the top-level tag.
144
144
  class AttributeBinding < PropertyBinding
145
145
  def read(node)
146
- deserialize(node[from])
146
+ deserialize(node[as])
147
147
  end
148
148
 
149
149
  def serialize_for(value, parent)
150
- parent[from] = serialize(value.to_s)
150
+ parent[as] = serialize(value.to_s)
151
151
  end
152
152
 
153
153
  def write(parent, value)
@@ -9,7 +9,7 @@ module Representable
9
9
  end
10
10
 
11
11
  def write(map, value)
12
- map.children << Psych::Nodes::Scalar.new(from)
12
+ map.children << Psych::Nodes::Scalar.new(as)
13
13
  map.children << serialize(value) # FIXME: should be serialize.
14
14
  end
15
15
 
@@ -36,7 +36,7 @@ module Representable
36
36
  class CollectionBinding < PropertyBinding
37
37
  def serialize(value)
38
38
  Psych::Nodes::Sequence.new.tap do |seq|
39
- seq.style = Psych::Nodes::Sequence::FLOW if options[:style] == :flow
39
+ seq.style = Psych::Nodes::Sequence::FLOW if self[:style] == :flow
40
40
  value.each { |obj| seq.children << super(obj) }
41
41
  end
42
42
  end
@@ -28,7 +28,7 @@ module Representable::Coercion
28
28
  representable_attrs.inheritable_array(:coercer_class).first.attribute(name, options[:type])
29
29
 
30
30
  # By using :getter we "pre-occupy" this directive, but we avoid creating accessors, which i find is the cleaner way.
31
- options[:decorator_scope] = true
31
+ options[:exec_context] = :decorator
32
32
  options[:getter] = lambda { |*| coercer.coerce(name, represented.send(name)) }
33
33
  options[:setter] = lambda { |v,*| represented.send("#{name}=", coercer.coerce(name, v)) }
34
34
 
@@ -31,13 +31,19 @@ module Representable
31
31
  values.each(*args, &block)
32
32
  end
33
33
 
34
- attr_accessor :wrap
34
+ def wrap=(value)
35
+ value = value.to_s if value.is_a?(Symbol)
36
+ @wrap = Uber::Options::Value.new(value)
37
+ end
35
38
 
36
39
  # Computes the wrap string or returns false.
37
- def wrap_for(name)
38
- return unless wrap
39
- return infer_name_for(name) if wrap === true
40
- wrap
40
+ def wrap_for(name, context, *args)
41
+ return unless @wrap
42
+
43
+ value = @wrap.evaluate(context, *args)
44
+
45
+ return infer_name_for(name) if value === true
46
+ value
41
47
  end
42
48
 
43
49
  # Write representer configuration into this hash.