representable 2.3.0 → 2.4.0.rc1

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +47 -0
  3. data/Gemfile +5 -0
  4. data/README.md +33 -0
  5. data/lib/representable.rb +60 -73
  6. data/lib/representable/binding.rb +37 -194
  7. data/lib/representable/cached.rb +10 -46
  8. data/lib/representable/coercion.rb +8 -8
  9. data/lib/representable/config.rb +15 -75
  10. data/lib/representable/debug.rb +41 -59
  11. data/lib/representable/declarative.rb +34 -53
  12. data/lib/representable/decorator.rb +11 -40
  13. data/lib/representable/definition.rb +14 -15
  14. data/lib/representable/deprecations.rb +90 -0
  15. data/lib/representable/deserializer.rb +87 -82
  16. data/lib/representable/for_collection.rb +5 -3
  17. data/lib/representable/hash.rb +5 -3
  18. data/lib/representable/hash/binding.rb +6 -15
  19. data/lib/representable/hash/collection.rb +10 -6
  20. data/lib/representable/hash_methods.rb +5 -5
  21. data/lib/representable/insert.rb +31 -0
  22. data/lib/representable/json.rb +7 -3
  23. data/lib/representable/json/hash.rb +1 -1
  24. data/lib/representable/object/binding.rb +5 -5
  25. data/lib/representable/parse_strategies.rb +37 -3
  26. data/lib/representable/pipeline.rb +37 -5
  27. data/lib/representable/pipeline_factories.rb +88 -0
  28. data/lib/representable/serializer.rb +38 -44
  29. data/lib/representable/version.rb +1 -1
  30. data/lib/representable/xml.rb +4 -0
  31. data/lib/representable/xml/binding.rb +25 -31
  32. data/lib/representable/xml/collection.rb +5 -3
  33. data/lib/representable/xml/hash.rb +7 -2
  34. data/lib/representable/yaml.rb +6 -3
  35. data/lib/representable/yaml/binding.rb +4 -4
  36. data/representable.gemspec +3 -3
  37. data/test/---deserialize-pipeline_test.rb +37 -0
  38. data/test/binding_test.rb +7 -7
  39. data/test/cached_test.rb +31 -19
  40. data/test/coercion_test.rb +2 -2
  41. data/test/config/inherit_test.rb +13 -12
  42. data/test/config_test.rb +12 -67
  43. data/test/decorator_test.rb +4 -5
  44. data/test/default_test.rb +34 -0
  45. data/test/defaults_options_test.rb +93 -0
  46. data/test/definition_test.rb +19 -39
  47. data/test/exec_context_test.rb +1 -1
  48. data/test/filter_test.rb +18 -20
  49. data/test/getter_setter_test.rb +1 -8
  50. data/test/hash_bindings_test.rb +13 -13
  51. data/test/heritage_test.rb +62 -0
  52. data/test/if_test.rb +1 -0
  53. data/test/inherit_test.rb +5 -3
  54. data/test/instance_test.rb +3 -4
  55. data/test/json_test.rb +3 -59
  56. data/test/lonely_test.rb +47 -3
  57. data/test/nested_test.rb +8 -2
  58. data/test/pipeline_test.rb +259 -0
  59. data/test/populator_test.rb +76 -0
  60. data/test/realistic_benchmark.rb +39 -7
  61. data/test/render_nil_test.rb +21 -0
  62. data/test/represent_test.rb +2 -2
  63. data/test/representable_test.rb +33 -103
  64. data/test/schema_test.rb +5 -15
  65. data/test/serialize_deserialize_test.rb +2 -2
  66. data/test/skip_test.rb +1 -1
  67. data/test/test_helper.rb +6 -0
  68. data/test/uncategorized_test.rb +67 -0
  69. data/test/xml_bindings_test.rb +6 -6
  70. data/test/xml_test.rb +6 -6
  71. metadata +33 -13
  72. data/lib/representable/apply.rb +0 -13
  73. data/lib/representable/inheritable.rb +0 -71
  74. data/lib/representable/mapper.rb +0 -83
  75. data/lib/representable/populator.rb +0 -56
  76. data/test/inheritable_test.rb +0 -97
@@ -14,14 +14,16 @@ module Representable
14
14
  end
15
15
  end
16
16
 
17
-
18
17
  module ClassMethods
18
+ def format_engine
19
+ Representable::Hash
20
+ end
21
+
19
22
  def collection_representer_class
20
23
  Collection
21
24
  end
22
25
  end
23
26
 
24
-
25
27
  def from_hash(data, options={}, binding_builder=Binding)
26
28
  data = filter_wrap(data, options)
27
29
 
@@ -31,7 +33,7 @@ module Representable
31
33
  def to_hash(options={}, binding_builder=Binding)
32
34
  hash = create_representation_with({}, options, binding_builder)
33
35
 
34
- return hash if options[:wrap] == false # TODO: same for parse.
36
+ return hash if options[:wrap] == false
35
37
  return hash unless wrap = options[:wrap] || representation_wrap(options)
36
38
 
37
39
  {wrap => hash}
@@ -3,20 +3,16 @@ require 'representable/binding'
3
3
  module Representable
4
4
  module Hash
5
5
  class Binding < Representable::Binding
6
- def self.build_for(definition, *args) # TODO: remove default arg.
7
- # puts "@@@build@@ #{definition.inspect}"
8
- return Collection.new(definition, *args) if definition.array?
9
- return Hash.new(definition, *args) if definition.hash?
10
- new(definition, *args)
6
+ def self.build_for(definition)
7
+ return Collection.new(definition) if definition.array?
8
+ new(definition)
11
9
  end
12
10
 
13
- def read(hash)
14
- return FragmentNotFound unless hash.has_key?(as)
15
-
16
- hash[as] # fragment
11
+ def read(hash, as)
12
+ hash.has_key?(as) ? hash[as] : FragmentNotFound
17
13
  end
18
14
 
19
- def write(hash, fragment)
15
+ def write(hash, fragment, as)
20
16
  hash[as] = fragment
21
17
  end
22
18
 
@@ -31,11 +27,6 @@ module Representable
31
27
  class Collection < self
32
28
  include Representable::Binding::Collection
33
29
  end
34
-
35
-
36
- class Hash < self
37
- include Representable::Binding::Hash
38
- end
39
30
  end
40
31
  end
41
32
  end
@@ -6,7 +6,7 @@ module Representable::Hash
6
6
  base.class_eval do
7
7
  include Representable::Hash
8
8
  extend ClassMethods
9
- representable_attrs.add(:_self, {:collection => true})
9
+ property(:_self, {:collection => true})
10
10
  end
11
11
  end
12
12
 
@@ -19,14 +19,18 @@ module Representable::Hash
19
19
 
20
20
 
21
21
  def create_representation_with(doc, options, format)
22
- bin = representable_mapper(format, options).bindings(represented, options).first
23
- bin.render_fragment(represented, doc)
22
+ bin = representable_bindings_for(format, options).first
23
+
24
+ Collect[*bin.default_render_fragment_functions].
25
+ (represented, {doc: doc, fragment: represented, user_options: options, binding: bin, represented: represented})
24
26
  end
25
27
 
26
28
  def update_properties_from(doc, options, format)
27
- bin = representable_mapper(format, options).bindings(represented, options).first
28
- #value = bin.deserialize_from(doc)
29
- value = Deserializer::Collection.new(bin).call(doc)
29
+ bin = representable_bindings_for(format, options).first
30
+
31
+ value = Collect[*bin.default_parse_fragment_functions].
32
+ (doc, fragment: doc, document: doc, user_options: options, binding: bin, represented: represented)
33
+
30
34
  represented.replace(value)
31
35
  end
32
36
  end
@@ -2,17 +2,17 @@ module Representable
2
2
  module HashMethods
3
3
  def create_representation_with(doc, options, format)
4
4
  hash = filter_keys_for!(represented, options) # FIXME: this modifies options and replicates logic from Representable.
5
- bin = representable_mapper(format, options).bindings(represented, options).first
5
+ bin = representable_map(options, format).first
6
6
 
7
- bin.render_fragment(hash, doc) # TODO: Use something along Populator, which does
7
+ Collect::Hash[*bin.default_render_fragment_functions].extend(Pipeline::Debug).(hash, {doc: doc, user_options: options, binding: bin, represented: represented, decorator: self})
8
8
  end
9
9
 
10
10
  def update_properties_from(doc, options, format)
11
11
  hash = filter_keys_for!(doc, options)
12
- bin = representable_mapper(format, options).bindings(represented, options).first
12
+ bin = representable_map(options, format).first
13
+
14
+ value = Collect::Hash[*bin.default_parse_fragment_functions].(hash, fragment: hash, document: doc, binding: bin, represented: represented, user_options: options, decorator: self)
13
15
 
14
- value = Deserializer::Hash.new(bin).call(hash)
15
- # value = bin.deserialize_from(hash)
16
16
  represented.replace(value)
17
17
  end
18
18
 
@@ -0,0 +1,31 @@
1
+ module Representable
2
+ Pipeline.class_eval do # FIXME: this doesn't define Function in Pipeline.
3
+ module Function
4
+ class Insert
5
+ def call(arr, func, options)
6
+ arr = arr.dup
7
+ delete!(arr, func) if options[:delete]
8
+ replace!(arr, options[:replace], func) if options[:replace]
9
+ arr
10
+ end
11
+
12
+ private
13
+ def replace!(arr, old_func, new_func)
14
+ arr.each_with_index { |func, index|
15
+ if func.is_a?(Collect)
16
+ arr[index] = Collect[*Pipeline::Insert.(func, new_func, replace: old_func)]
17
+ end
18
+
19
+ arr[index] = new_func if old_func.is_a?(Proc)? (func==old_func) : old_func.instance_of?(func.class)
20
+ }
21
+ end
22
+
23
+ def delete!(arr, func)
24
+ arr.delete(func)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ Pipeline::Insert = Function::Insert.new
31
+ end
@@ -1,8 +1,8 @@
1
- require 'representable/hash'
2
- require 'representable/json/collection'
1
+ require "representable/hash"
2
+ require "representable/json/collection"
3
3
 
4
4
  begin
5
- require 'multi_json'
5
+ require "multi_json"
6
6
  rescue LoadError => _
7
7
  abort "Missing dependency 'multi_json' for Representable::JSON. See dependencies section in README.md for details."
8
8
  end
@@ -23,6 +23,10 @@ module Representable
23
23
 
24
24
 
25
25
  module ClassMethods
26
+ def format_engine
27
+ Representable::Hash
28
+ end
29
+
26
30
  def collection_representer_class
27
31
  JSON::Collection
28
32
  end
@@ -10,7 +10,7 @@ module Representable::JSON
10
10
  extend ClassMethods
11
11
  include Representable::JSON
12
12
  include Representable::HashMethods
13
- representable_attrs.add(:_self, {:hash => true})
13
+ property(:_self, hash: true)
14
14
  end
15
15
  end
16
16
 
@@ -1,19 +1,19 @@
1
1
  module Representable
2
2
  module Object
3
3
  class Binding < Representable::Binding
4
- def self.build_for(definition, *args) # TODO: remove default arg.
5
- return Collection.new(definition, *args) if definition.array?
6
- new(definition, *args)
4
+ def self.build_for(definition) # TODO: remove default arg.
5
+ return Collection.new(definition) if definition.array?
6
+ new(definition)
7
7
  end
8
8
 
9
- def read(hash)
9
+ def read(hash, as)
10
10
  fragment = hash.send(as) # :getter? no, that's for parsing!
11
11
 
12
12
  return FragmentNotFound if fragment.nil? and typed?
13
13
  fragment
14
14
  end
15
15
 
16
- def write(hash, fragment)
16
+ def write(hash, fragment, as)
17
17
  true
18
18
  end
19
19
 
@@ -1,4 +1,36 @@
1
1
  module Representable
2
+ class Populator
3
+ FindOrInstantiate = ->(input, options) {
4
+ AssignFragment.(input, options)
5
+
6
+ binding = options[:binding]
7
+
8
+ object_class = binding[:class].(input, options)
9
+ object = object_class.find_by(id: input["id"]) || object_class.new
10
+ if options[:binding].array?
11
+ # represented.songs[i] = model
12
+ options[:represented].send(binding.getter)[options[:index]] = object
13
+ else
14
+ # represented.song = model
15
+ options[:represented].send(binding.setter, object)
16
+ end
17
+
18
+ object
19
+ }
20
+
21
+ def self.apply!(options)
22
+ return unless populator = options[:populator]
23
+
24
+ options[:parse_pipeline] = ->(input, options) do
25
+ pipeline = Pipeline[*parse_functions] # TODO: AssignFragment
26
+ pipeline = Pipeline::Insert.(pipeline, Set, delete: true) # remove the setter function.
27
+ pipeline = Pipeline::Insert.(pipeline, populator, replace: CreateObject) # let the populator do CreateObject's job.
28
+ end
29
+ end
30
+ end
31
+
32
+ FindOrInstantiate = Populator::FindOrInstantiate
33
+
2
34
  # Parse strategies are just a combination of representable's options. They save you from memoizing the
3
35
  # necessary parameters.
4
36
  #
@@ -7,6 +39,8 @@ module Representable
7
39
  def self.apply!(options)
8
40
  return unless strategy = options[:parse_strategy]
9
41
 
42
+ warn "[Representable] :parse_strategy is deprecated. Please use a populator."
43
+
10
44
  strategy = :proc if strategy.is_a?(::Proc)
11
45
 
12
46
  parse_strategies[strategy].apply!(name, options)
@@ -33,11 +67,11 @@ module Representable
33
67
 
34
68
  class Sync
35
69
  def self.apply!(name, options)
36
- options[:setter] = lambda { |*| }
70
+ options[:setter] = lambda { |*args| }
37
71
  options[:pass_options] = true
38
72
  options[:instance] = options[:collection] ?
39
- lambda { |fragment, i, options| options.binding.get[i] } :
40
- lambda { |fragment, options| options.binding.get }
73
+ lambda { |fragment, i, options| options.binding.get(represented: options.represented)[i] } :
74
+ lambda { |fragment, options| options.binding.get(represented: options.represented) }
41
75
  end
42
76
  end
43
77
 
@@ -1,14 +1,46 @@
1
1
  module Representable
2
2
  # Allows to implement a pipeline of filters where a value gets passed in and the result gets
3
3
  # passed to the next callable object.
4
- #
5
- # Note: this is still experimental.
6
4
  class Pipeline < Array
7
5
  include Uber::Callable
8
- # include Representable::Cloneable
9
6
 
10
- def call(context, value, *args)
11
- inject(value) { |memo, block| block.call(memo, *args) }
7
+ Stop = Class.new
8
+
9
+ # options is mutuable.
10
+ def call(input, options)
11
+ inject(input) do |memo, block|
12
+ res = evaluate(block, memo, options)
13
+ return(Stop)if Stop == res
14
+ res
15
+ end
16
+ end
17
+
18
+ private
19
+ def evaluate(block, input, options)
20
+ block.call(input, options)
21
+ end
22
+ end
23
+
24
+
25
+ # Collect applies a pipeline to each element of input.
26
+ class Collect < Pipeline
27
+ # when stop, the element is skipped. (should that be Skip then?)
28
+ def call(input, options)
29
+ arr = []
30
+ input.each_with_index do |item_fragment, i|
31
+ result = super(item_fragment, options.merge(index: i)) # DISCUSS: NO :fragment set.
32
+ Pipeline::Stop == result ? next : arr << result
33
+ end
34
+ arr
35
+ end
36
+
37
+ class Hash < Pipeline
38
+ def call(input, options)
39
+ {}.tap do |hsh|
40
+ input.each { |key, item_fragment|
41
+ hsh[key] = super(item_fragment, options) }# DISCUSS: NO :fragment set.
42
+ end
43
+ end
12
44
  end
13
45
  end
14
46
  end
@@ -0,0 +1,88 @@
1
+ # NOTE: this might become a separate class, that's why it's in a separate file.
2
+ module Representable
3
+ module Binding::Factories
4
+ def pipeline_for(name, input, options)
5
+ return yield unless proc = @definition[name]
6
+ # proc.(self, options)
7
+ instance_exec(input, options, &proc)
8
+ end
9
+
10
+ # i decided not to use polymorphism here for the sake of clarity.
11
+ def collect_for(item_functions)
12
+ return [Collect[*item_functions]] if array?
13
+ return [Collect::Hash[*item_functions]] if self[:hash]
14
+ item_functions
15
+ end
16
+
17
+ def parse_functions
18
+ [*default_parse_init_functions, *collect_for(default_parse_fragment_functions), *default_post_functions]
19
+ end
20
+
21
+ # DISCUSS: StopOnNil, before collect
22
+ def render_functions
23
+ [*default_render_init_functions, *collect_for(default_render_fragment_functions), WriteFragment]
24
+ end
25
+
26
+ def default_render_fragment_functions
27
+ functions = []
28
+ functions << SkipRender if self[:skip_render]
29
+ if typed? # TODO: allow prepare regardless of :extend, which makes it independent of typed?
30
+ if self[:prepare]
31
+ functions << Prepare
32
+ end
33
+ # functions << (self[:prepare] ? Prepare : Decorate)
34
+ end
35
+ functions << Decorate if self[:extend] and !self[:prepare]
36
+ if representable?
37
+ functions << (self[:serialize] ? Serializer : Serialize)
38
+ end
39
+ functions
40
+ end
41
+
42
+ def default_render_init_functions
43
+ functions = []
44
+ functions << Stop if self[:readable]==false
45
+ functions << StopOnExcluded
46
+ functions << If if self[:if]
47
+ functions << (self[:getter] ? Getter : Get)
48
+ functions << Writer if self[:writer]
49
+ functions << RenderFilter if self[:render_filter].any?
50
+ functions << RenderDefault if has_default?
51
+ functions << StopOnSkipable
52
+ functions << (self[:as] ? AssignAs : AssignName)
53
+ end
54
+
55
+ def default_parse_init_functions
56
+ functions = []
57
+ functions << Stop if self[:writeable]==false
58
+ functions << StopOnExcluded
59
+ functions << If if self[:if]
60
+ functions << (self[:as] ? AssignAs : AssignName)
61
+ functions << (self[:reader] ? Reader : ReadFragment)
62
+ functions << (has_default? ? Default : StopOnNotFound)
63
+ functions << OverwriteOnNil # include StopOnNil if you don't want to erase things.
64
+ end
65
+
66
+ def default_parse_fragment_functions
67
+ functions = []
68
+ functions << SkipParse if self[:skip_parse]
69
+
70
+ if typed?
71
+ functions << CreateObject
72
+ functions << Prepare if self[:prepare]
73
+ functions << Decorate if self[:extend]
74
+ if representable?
75
+ functions << (self[:deserialize] ? Deserializer : Deserialize)
76
+ end
77
+ end
78
+
79
+ functions
80
+ end
81
+
82
+ def default_post_functions
83
+ funcs = []
84
+ funcs << ParseFilter if self[:parse_filter].any?
85
+ funcs << (self[:setter] ? Setter : Set)
86
+ end
87
+ end
88
+ end
@@ -1,60 +1,54 @@
1
- require "representable/deserializer"
2
-
3
1
  module Representable
4
- # serialize -> serialize! -> marshal. # TODO: same flow in deserialize.
5
- class Serializer < Deserializer
6
- def call(object, &block)
7
- return object if object.nil? # DISCUSS: move to Object#serialize ?
2
+ Getter = ->(input, options) do
3
+ options[:binding].evaluate_option(:getter, input, options)
4
+ end
8
5
 
9
- serialize(object, @binding.user_options, &block)
10
- end
6
+ Get = ->(input, options) { options[:binding].send(:exec_context, options).send(options[:binding].getter) }
11
7
 
12
- private
13
- def serialize(object, user_options, &block)
14
- return yield if @binding.evaluate_option(:skip_render, object) # this will jump out of #render_fragment. introduce Skip object here.
8
+ Writer = ->(input, options) do
9
+ options[:binding].evaluate_option(:writer, input, options)
10
+ Pipeline::Stop
11
+ end
15
12
 
16
- serialize!(object, user_options)
17
- end
13
+ # TODO: evaluate this, if we need this.
14
+ RenderDefault = ->(input, options) do
15
+ binding = options[:binding]
18
16
 
19
- # Serialize one object by calling to_json etc. on it.
20
- def serialize!(object, user_options)
21
- object = prepare(object)
17
+ binding.skipable_empty_value?(input) ? binding[:default] : input
18
+ end
22
19
 
23
- return object unless @binding.representable?
20
+ StopOnSkipable = ->(input, options) do
21
+ options[:binding].send(:skipable_empty_value?, input) ? Pipeline::Stop : input
22
+ end
24
23
 
25
- @binding.evaluate_option(:serialize, object) do
26
- marshal(object, user_options)
27
- end
28
- end
24
+ RenderFilter = ->(input, options) do
25
+ options[:binding][:render_filter].(input, options)
26
+ end
29
27
 
30
- # 0.33 0.004 0.004 0.000 0.000 20001 Hash#merge!
31
- # 0.00 0.000 0.000 0.000 0.000 1 Hash#merge!
32
- def marshal(object, user_options)
33
- object.send(@binding.serialize_method, user_options)
34
- end
28
+ SkipRender = ->(input, options) do
29
+ options[:binding].evaluate_option(:skip_render, input, options) ? Pipeline::Stop : input
30
+ end
35
31
 
32
+ Serializer = ->(input, options) do
33
+ return if input.nil? # DISCUSS: how can we prevent that?
36
34
 
37
- class Collection < self
38
- def serialize(array, *args)
39
- collection = [] # TODO: unify with Deserializer::Collection.
35
+ options[:binding].evaluate_option(:serialize, input, options)
36
+ end
40
37
 
41
- array.each do |item|
42
- next if @binding.evaluate_option(:skip_render, item) # TODO: allow skipping entire collections? same for deserialize.
38
+ Serialize = ->(input, options) do
39
+ return if input.nil? # DISCUSS: how can we prevent that?
40
+ binding, user_options = options[:binding], options[:user_options]
43
41
 
44
- collection << serialize!(item, *args)
45
- end # TODO: i don't want Array but Forms here - what now?
42
+ user_options = user_options.merge(wrap: binding[:wrap]) unless binding[:wrap].nil? # DISCUSS: can we leave that here?
46
43
 
47
- collection
48
- end
49
- end
44
+ input.send(binding.serialize_method, user_options)
45
+ end
50
46
 
47
+ WriteFragment = ->(input, options) { options[:binding].write(options[:doc], input, options[:as]) }
51
48
 
52
- class Hash < self
53
- def serialize(hash, *args)
54
- {}.tap do |hsh|
55
- hash.each { |key, obj| hsh[key] = super(obj, *args) }
56
- end
57
- end
58
- end
59
- end
49
+ As = ->(input, options) { options[:binding].evaluate_option(:as, input, options) }
50
+
51
+ # Warning: don't rely on AssignAs/AssignName, i am not sure if i leave that as functions.
52
+ AssignAs = ->(input, options) { options[:as] = As.(input, options); input }
53
+ AssignName = ->(input, options) { options[:as] = options[:binding].name; input }
60
54
  end