representable 2.3.0 → 2.4.0.rc1

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