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
@@ -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