representable 1.7.7 → 1.8.0

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