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
@@ -1,16 +1,36 @@
1
+ require 'uber/options'
2
+ require "representable/parse_strategies"
3
+
1
4
  module Representable
2
5
  # Created at class compile time. Keeps configuration options for one property.
3
- class Definition
4
- attr_reader :name, :options
6
+ class Definition < Hash
7
+ attr_reader :name
5
8
  alias_method :getter, :name
6
9
 
7
10
  def initialize(sym, options={})
8
- @name = sym.to_s
9
- @options = options
11
+ super()
12
+ options = options.clone
13
+
14
+ handle_deprecations!(options)
15
+
16
+ @name = sym.to_s
17
+ # defaults:
18
+ options[:as] ||= @name
19
+
20
+ setup!(options)
21
+ end
22
+
23
+ # TODO: test merge!.
24
+ def merge!(options)
25
+ setup!(options)
26
+ self
10
27
  end
11
28
 
12
- def clone
13
- self.class.new(name, options.clone) # DISCUSS: make generic Definition.cloned_attribute that passes list to constructor.
29
+ private :default, :[]=
30
+
31
+ def options # TODO: remove in 2.0.
32
+ warn "Representable::Definition#option is deprecated, use #[] directly."
33
+ self
14
34
  end
15
35
 
16
36
  def setter
@@ -18,65 +38,77 @@ module Representable
18
38
  end
19
39
 
20
40
  def typed?
21
- deserialize_class.is_a?(Class) or representer_module or options[:instance] # also true if only :extend is set, for people who want solely rendering.
22
- end
23
-
24
- def array?
25
- options[:collection]
41
+ self[:class] or self[:extend] or self[:instance]
26
42
  end
27
43
 
28
- def hash?
29
- options[:hash]
44
+ def representable?
45
+ return if self[:representable] === false
46
+ self[:representable] or typed?
30
47
  end
31
48
 
32
- def deserialize_class
33
- options[:class]
49
+ def array?
50
+ self[:collection]
34
51
  end
35
52
 
36
- def from
37
- # TODO: deprecate :from.
38
- (options[:from] || options[:as] || name).to_s
53
+ def hash?
54
+ self[:hash]
39
55
  end
40
56
 
41
57
  def default_for(value)
42
- return default if skipable_nil_value?(value)
58
+ return self[:default] if skipable_nil_value?(value)
43
59
  value
44
60
  end
45
61
 
46
62
  def has_default?
47
- options.has_key?(:default)
63
+ has_key?(:default)
48
64
  end
49
65
 
50
66
  def representer_module
51
- options[:extend] or options[:decorator]
67
+ self[:extend]
52
68
  end
53
69
 
54
- def attribute
55
- options[:attribute]
70
+ def skipable_nil_value?(value)
71
+ value.nil? and not self[:render_nil]
56
72
  end
57
73
 
58
- def content
59
- options[:content]
74
+ def create_binding(*args)
75
+ self[:binding].call(self, *args)
60
76
  end
61
77
 
62
- def skipable_nil_value?(value)
63
- value.nil? and not options[:render_nil]
78
+ private
79
+ def setup!(options)
80
+ handle_extend!(options)
81
+ handle_as!(options)
82
+
83
+ # DISCUSS: we could call more macros here (e.g. for :nested).
84
+ Representable::ParseStrategy.apply!(options)
85
+
86
+ for name, value in options
87
+ value = Uber::Options::Value.new(value) if dynamic_options.include?(name)
88
+ self[name] = value
89
+ end
64
90
  end
65
91
 
66
- def default
67
- options[:default]
92
+ def dynamic_options
93
+ [:as, :getter, :setter, :class, :instance, :reader, :writer, :extend, :prepare, :if]
68
94
  end
69
95
 
70
- def binding
71
- options[:binding]
96
+ def handle_extend!(options)
97
+ mod = options.delete(:extend) || options.delete(:decorator) and options[:extend] = mod
72
98
  end
73
99
 
74
- def create_binding(*args)
75
- binding.call(self, *args)
100
+ def handle_as!(options)
101
+ options[:as] = options[:as].to_s if options[:as].is_a?(Symbol) # Allow symbols for as:
76
102
  end
77
103
 
78
- def sync?
79
- options[:parse_strategy] == :sync
104
+ # TODO: remove in 2.0.
105
+ def handle_deprecations!(options)
106
+ raise "The :from option got replaced by :as in Representable 1.8!" if options[:from]
107
+
108
+ if options[:decorator_scope]
109
+ warn "[Representable] Deprecation: `decorator_scope: true` is deprecated, use `exec_context: :decorator` instead."
110
+ options.merge!(:exec_context => :decorator)
111
+ end
80
112
  end
81
113
  end
82
114
  end
@@ -1,56 +1,52 @@
1
1
  module Representable
2
- class CollectionDeserializer < Array # always is the targeted collection, already.
2
+ class CollectionDeserializer
3
3
  def initialize(binding) # TODO: get rid of binding dependency
4
- # next step: use #get always.
5
4
  @binding = binding
6
- collection = []
7
- # should be call to #default:
8
- collection = binding.get if binding.sync?
9
-
10
- super collection
11
5
  end
12
6
 
13
7
  def deserialize(fragment)
14
8
  # next step: get rid of collect.
15
- fragment.enum_for(:each_with_index).collect { |item_fragment, i|
16
- @deserializer = ObjectDeserializer.new(@binding, lambda { self[i] })
9
+ fragment.enum_for(:each_with_index).collect do |item_fragment, i|
10
+ @deserializer = ObjectDeserializer.new(@binding)
17
11
 
18
- @deserializer.call(item_fragment) # FIXME: what if obj nil?
19
- }
12
+ @deserializer.call(item_fragment, i) # FIXME: what if obj nil?
13
+ end
20
14
  end
21
15
  end
22
16
 
23
17
 
24
18
  class ObjectDeserializer
25
- # dependencies: Def#options, Def#create_object, Def#get
26
- def initialize(binding, object)
19
+ # dependencies: Def#options, Def#create_object
20
+ def initialize(binding)
27
21
  @binding = binding
28
- @object = object
29
22
  end
30
23
 
31
- def call(fragment)
32
- # TODO: this used to be handled in #serialize where Object added it's behaviour. treat scalars as objects to remove this switch:
33
- return fragment unless @binding.typed?
24
+ def call(fragment, *args) # FIXME: args is always i.
25
+ return fragment unless @binding.typed? # customize with :extend. this is not really straight-forward.
34
26
 
35
- if @binding.sync?
36
- # TODO: this is also done when instance: { nil }
37
- @object = @object.call # call Binding#get or Binding#get[i]
38
- else
39
- @object = @binding.create_object(fragment)
40
- end
27
+ # what if create_object is responsible for providing the deserialize-to object?
28
+ object = @binding.create_object(fragment, *args) # customize with :instance and :class.
41
29
 
42
30
  # DISCUSS: what parts should be in this class, what in Binding?
43
- representable = prepare(@object)
44
- deserialize(representable, fragment, @binding.user_options)
45
- #yield @object
31
+ representable = prepare(object) # customize with :prepare and :extend.
32
+
33
+ deserialize(representable, fragment, @binding.user_options) # deactivate-able via :representable => false.
46
34
  end
47
35
 
48
36
  private
49
- def deserialize(object, fragment, options)
37
+ def deserialize(object, fragment, options) # TODO: merge with #serialize.
38
+ return object unless @binding.representable?
39
+
50
40
  object.send(@binding.deserialize_method, fragment, options)
51
41
  end
52
42
 
53
43
  def prepare(object)
44
+ @binding.send(:evaluate_option, :prepare, object) do
45
+ prepare!(object)
46
+ end
47
+ end
48
+
49
+ def prepare!(object)
54
50
  mod = @binding.representer_module_for(object)
55
51
 
56
52
  return object unless mod
@@ -26,10 +26,11 @@ module Representable
26
26
  end
27
27
 
28
28
 
29
+ # Note: `#from_hash` still does _not_ stringify incoming hashes. This is per design: Representable is not made for hashes, only,
30
+ # but for any arbitrary data structure. A generic `key.to_s` with non-hash data would result in weird issues.
31
+ # I decided it's more predictable to require the user to provide stringified keys.
29
32
  def from_hash(data, options={}, binding_builder=PropertyBinding)
30
- if wrap = options[:wrap] || representation_wrap
31
- data = data[wrap.to_s] || {} # DISCUSS: don't initialize this more than once. # TODO: this should be done with #read.
32
- end
33
+ data = filter_wrap(data, options)
33
34
 
34
35
  update_properties_from(data, options, binding_builder)
35
36
  end
@@ -37,9 +38,19 @@ module Representable
37
38
  def to_hash(options={}, binding_builder=PropertyBinding)
38
39
  hash = create_representation_with({}, options, binding_builder)
39
40
 
40
- return hash unless wrap = options[:wrap] || representation_wrap
41
+ return hash unless wrap = options[:wrap] || representation_wrap(options)
41
42
 
42
43
  {wrap => hash}
43
44
  end
45
+
46
+ private
47
+ def filter_wrap(data, options)
48
+ return data unless wrap = options[:wrap] || representation_wrap(options)
49
+ filter_wrap_for(data, wrap)
50
+ end
51
+
52
+ def filter_wrap_for(data, wrap)
53
+ data[wrap.to_s] || {} # DISCUSS: don't initialize this more than once. # TODO: this should be done with #read.
54
+ end
44
55
  end
45
56
  end
@@ -0,0 +1,27 @@
1
+ module Representable
2
+ module Hash
3
+ module AllowSymbols
4
+ private
5
+ def filter_wrap_for(data, *args)
6
+ super(Conversion.stringify_keys(data), *args)
7
+ end
8
+
9
+ def update_properties_from(data, *args)
10
+ super(Conversion.stringify_keys(data), *args)
11
+ end
12
+ end
13
+
14
+ class Conversion
15
+ # DISCUSS: we could think about mixin in IndifferentAccess here (either hashie or ActiveSupport).
16
+ # or decorating the hash.
17
+ def self.stringify_keys(hash)
18
+ hash = hash.dup
19
+
20
+ hash.keys.each do |k|
21
+ hash[k.to_s] = hash.delete(k)
22
+ end
23
+ hash
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,4 +1,3 @@
1
- require 'representable/hash'
2
1
  require 'multi_json'
3
2
 
4
3
  module Representable
@@ -1,5 +1,3 @@
1
- require 'representable/hash/collection'
2
-
3
1
  module Representable::JSON
4
2
  module Collection
5
3
  include Representable::JSON
@@ -1,12 +1,11 @@
1
- require 'representable/feature/readable_writeable'
1
+ require 'representable/readable_writeable'
2
2
 
3
3
  module Representable
4
4
  # Render and parse by looping over the representer's properties and dispatching to bindings.
5
5
  # Conditionals are handled here, too.
6
6
  class Mapper
7
- # DISCUSS: we need @represented here for evaluating the :if blocks. this could be done in the bindings_for asset.
8
7
  module Methods
9
- def initialize(bindings, represented, options)
8
+ def initialize(bindings, represented, options) # TODO: get rid of represented dependency.
10
9
  @represented = represented # the (extended) model.
11
10
  @bindings = bindings
12
11
  end
@@ -17,7 +16,7 @@ module Representable
17
16
  bindings.each do |bin|
18
17
  deserialize_property(bin, doc, options)
19
18
  end
20
- represented
19
+ @represented
21
20
  end
22
21
 
23
22
  def serialize(doc, options)
@@ -28,8 +27,6 @@ module Representable
28
27
  end
29
28
 
30
29
  private
31
- attr_reader :represented
32
-
33
30
  def serialize_property(binding, doc, options)
34
31
  return if skip_property?(binding, options)
35
32
  compile_fragment(binding, doc)
@@ -54,13 +51,9 @@ module Representable
54
51
  end
55
52
 
56
53
  def skip_conditional_property?(binding)
57
- # TODO: move to Binding.
58
- return unless condition = binding.options[:if]
59
-
60
- args = []
61
- args << binding.user_options if condition.arity > 0 # TODO: remove arity check. users should know whether they pass options or not.
54
+ return unless condition = binding[:if]
62
55
 
63
- not represented.instance_exec(*args, &condition)
56
+ not binding.send(:evaluate_option, :if)
64
57
  end
65
58
 
66
59
  def compile_fragment(bin, doc)
@@ -73,6 +66,6 @@ module Representable
73
66
  end
74
67
 
75
68
  include Methods
76
- include Feature::ReadableWriteable # DISCUSS: make this pluggable.
69
+ include ReadableWriteable # DISCUSS: make this pluggable.
77
70
  end
78
71
  end
@@ -0,0 +1,57 @@
1
+ module Representable
2
+ # Parse strategies are just a combination of representable's options. They save you from memoizing the
3
+ # necessary parameters.
4
+ #
5
+ # Feel free to contribute your strategy if you think it's worth sharing!
6
+ class ParseStrategy
7
+ def self.apply!(options)
8
+ return unless strategy = options[:parse_strategy]
9
+
10
+ strategy = :proc if strategy.is_a?(::Proc)
11
+
12
+ parse_strategies[strategy].apply!(name, options)
13
+ end
14
+
15
+ def self.parse_strategies
16
+ {
17
+ :sync => Sync,
18
+ :find_or_instantiate => FindOrInstantiate,
19
+ :proc => Proc
20
+ }
21
+ end
22
+
23
+
24
+ class Proc
25
+ def self.apply!(name, options)
26
+ options[:setter] = lambda { |*| }
27
+ options[:pass_options] = true
28
+ options[:instance] = options[:parse_strategy]
29
+ end
30
+ end
31
+
32
+
33
+ class Sync
34
+ def self.apply!(name, options)
35
+ options[:setter] = lambda { |*| }
36
+ options[:pass_options] = true
37
+ options[:instance] = options[:collection] ?
38
+ lambda { |fragment, i, options| options.binding.get[i] } :
39
+ lambda { |fragment, options| options.binding.get }
40
+ end
41
+ end
42
+
43
+
44
+ # replaces current collection.
45
+ class FindOrInstantiate
46
+ def self.apply!(name, options)
47
+ options[:pass_options] = true
48
+ options[:instance] = lambda { |fragment, *args|
49
+ args = args.last # TODO: don't pass i as separate block parameter but in Options.
50
+ object_class = args.binding[:class].evaluate(self, fragment, args)
51
+
52
+ object_class.find(fragment["id"]) or object_class.new
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,29 @@
1
+ module Representable
2
+ module ReadableWriteable
3
+ def deserialize_property(binding, doc, options)
4
+ return unless binding.writeable?
5
+ super
6
+ end
7
+
8
+ def serialize_property(binding, doc, options)
9
+ return unless binding.readable?
10
+ super
11
+ end
12
+ end
13
+
14
+
15
+ # TODO: i hate monkey-patching Definition here since it globally adds this options. However, for now this should be ok :-)
16
+ Definition.class_eval do
17
+ # Checks and returns if the property is writeable
18
+ def writeable?
19
+ return self[:writeable] if self.has_key?(:writeable)
20
+ true
21
+ end
22
+
23
+ # Checks and returns if the property is readable
24
+ def readable?
25
+ return self[:readable] if self.has_key?(:readable)
26
+ true
27
+ end
28
+ end
29
+ end