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
@@ -1,11 +1,8 @@
1
1
  require "representable"
2
+ require "uber/inheritable_attr"
2
3
 
3
4
  module Representable
4
5
  class Decorator
5
- class << self
6
- include Cloneable
7
- end
8
-
9
6
  attr_reader :represented
10
7
  alias_method :decorated, :represented
11
8
 
@@ -14,57 +11,31 @@ module Representable
14
11
  new(represented)
15
12
  end
16
13
 
17
- def self.default_inline_class
14
+ def self.default_nested_class #FIXME. SHOULD we move that into NestedBuilder?
18
15
  Representable::Decorator
19
16
  end
20
17
 
18
+ # extend ::Declarative::Heritage::Inherited # DISCUSS: currently, that is handled via Representable::inherited.
19
+
21
20
  # This is called from inheritable_attr when inheriting a decorator class to a subclass.
22
21
  # Explicitly subclassing the Decorator makes sure representable_attrs is a clean version.
23
22
  def self.clone
24
- Class.new(self) # DISCUSS: why isn't this called by Ruby?
23
+ Class.new(self)
25
24
  end
26
25
 
27
26
  include Representable # include after class methods so Decorator::prepare can't be overwritten by Representable::prepare.
27
+ include Cached
28
28
 
29
- # TODO: implement that just by calling ::property(name, options){include mod} on the inheriting representer.
30
- module InheritModule
31
- def inherit_module!(parent)
32
- inherited_attrs = parent.representable_attrs[:definitions].keys
33
-
34
- super # in Representable, calls representable_attrs.inherit!(parent.representable_attrs).
35
- # now, inline representers are still modules, which is wrong.
36
- manifest!(inherited_attrs)
37
- end
38
-
39
- private
40
- # one level deep manifesting modules into Decorators.
41
- def manifest!(names)
42
- names.each do |name| # only definitions.
43
- definition = representable_attrs.get(name)
44
- next unless definition[:_inline] and mod = definition.representer_module # only inline representers.
45
-
46
- # here, we can include Decorator features.
47
- inline_representer = build_inline(nil, representable_attrs.features, definition.name, {}) {
48
- include mod
49
- } # the includer controls what "wraps" the module.
50
-
51
- definition.merge!(:extend => inline_representer)
52
- end
53
- end
54
- end
55
- extend InheritModule
56
-
29
+ extend Uber::InheritableAttr
30
+ inheritable_attr :map
31
+ self.map = Binding::Map.new
57
32
 
58
33
  def initialize(represented)
59
34
  @represented = represented
60
35
  end
61
36
 
62
- private
63
- def self.build_inline(base, features, name, options, &block)
64
- Class.new(base || default_inline_class) do
65
- feature *features
66
- class_eval &block
67
- end
37
+ def self.nested_builder
38
+ ::Declarative::Schema::DSL::NestedBuilder
68
39
  end
69
40
  end
70
41
  end
@@ -1,27 +1,27 @@
1
- require 'uber/options'
1
+ require "uber/options"
2
2
  require "representable/parse_strategies"
3
3
 
4
4
  module Representable
5
5
  # Created at class compile time. Keeps configuration options for one property.
6
- class Definition
7
- include Representable::Cloneable
8
-
9
- attr_reader :name
10
- alias_method :getter, :name
6
+ class Definition < ::Declarative::Definitions::Definition
11
7
 
12
8
  def initialize(sym, options={}, &block)
13
- @options = Cloneable::Hash.new # allows deep cloning. we then had to set Pipeline cloneable.
14
- @name = sym.to_s
15
- options = options.clone
9
+ options[:extend] = options[:nested] if options[:nested]
10
+
11
+ super
16
12
 
17
13
  # defaults:
18
14
  options[:parse_filter] = Pipeline[*options[:parse_filter]]
19
15
  options[:render_filter] = Pipeline[*options[:render_filter]]
20
- options[:as] ||= @name
21
16
 
22
17
  setup!(options, &block)
23
18
  end
24
19
 
20
+ def name
21
+ self[:name]
22
+ end
23
+ alias_method :getter, :name
24
+
25
25
  def merge!(options, &block)
26
26
  options = options.clone
27
27
 
@@ -39,13 +39,11 @@ module Representable
39
39
  end
40
40
 
41
41
  def [](name)
42
+ # return nil if name==:extend && self[:nested].nil?
43
+ # return Uber::Options::Value.new(self[:nested]) if name==:extend
42
44
  @runtime_options[name]
43
45
  end
44
46
 
45
- def clone
46
- self.class.new(name, @options.clone)
47
- end
48
-
49
47
  def setter
50
48
  :"#{name}="
51
49
  end
@@ -91,6 +89,7 @@ module Representable
91
89
 
92
90
  # DISCUSS: we could call more macros here (e.g. for :nested).
93
91
  Representable::ParseStrategy.apply!(options)
92
+ Representable::Populator.apply!(options)
94
93
 
95
94
  yield options if block_given?
96
95
  @options.merge!(options)
@@ -110,7 +109,7 @@ module Representable
110
109
  end
111
110
 
112
111
  def dynamic_options
113
- [:as, :getter, :setter, :class, :instance, :reader, :writer, :extend, :prepare, :if, :deserialize, :serialize, :render_filter, :parse_filter, :skip_parse, :skip_render]
112
+ [:as, :getter, :setter, :class, :instance, :reader, :writer, :extend, :prepare, :if, :deserialize, :serialize, :skip_parse, :skip_render]
114
113
  end
115
114
 
116
115
  def handle_extend!(options)
@@ -0,0 +1,90 @@
1
+ # WARNING: this will be removed in 3.0.
2
+ module Representable::Binding::Deprecation
3
+ Options = Struct.new(:binding, :user_options, :represented, :decorator)
4
+
5
+ module EvaluateOption
6
+ def evaluate_option(name, input=nil, options={})
7
+ return evaluate_option_with_deprecation(name, input, options, :user_options) if name==:as
8
+ return evaluate_option_with_deprecation(name, input, options, :user_options) if name==:if
9
+ return evaluate_option_with_deprecation(name, input, options, :user_options) if name==:getter
10
+ return evaluate_option_with_deprecation(name, input, options, :doc, :user_options) if name==:writer
11
+ return evaluate_option_with_deprecation(name, input, options, :input, :user_options) if name==:skip_render
12
+ return evaluate_option_with_deprecation(name, input, options, :input, :user_options) if name==:skip_parse
13
+ return evaluate_option_with_deprecation(name, input, options, :input, :user_options) if name==:serialize
14
+ return evaluate_option_with_deprecation(name, input, options, :doc, :user_options) if name==:reader
15
+ return evaluate_option_with_deprecation(name, input, options, :input, :index, :user_options) if name==:instance
16
+ return evaluate_option_with_deprecation(name, input, options, :input, :index, :user_options) if name==:class
17
+ return evaluate_option_with_deprecation(name, input, options, :input, :fragment, :user_options) if name==:deserialize
18
+ return evaluate_option_with_deprecation(name, input, options, :input, :user_options) if name==:prepare
19
+ return evaluate_option_with_deprecation(name, input, options, :input, :user_options) if name==:extend
20
+ return evaluate_option_with_deprecation(name, input, options, :input, :user_options) if name==:setter
21
+ end
22
+
23
+
24
+ def evaluate_option_with_deprecation(name, input, options, *positional_arguments)
25
+ unless proc = self[name]
26
+ return # FIXME: why do we need this?
27
+ end
28
+
29
+ options[:input] = input
30
+
31
+
32
+ __options = if self[:pass_options]
33
+ warn %{[Representable] The :pass_options option is deprecated. Please access environment objects via options[:binding].
34
+ Learn more here: http://trailblazerb.org/gems/representable/upgrading-guide.html#pass-options}
35
+ Options.new(self, options[:user_options], options[:represented], options[:decorator])
36
+ else
37
+ # user_options
38
+ options[:user_options]
39
+ end
40
+ # options[:user_options] = __options # TODO: always make this user_options in Representable 3.0.
41
+
42
+ if proc.send(:proc?) or proc.send(:method?)
43
+ arity = proc.instance_variable_get(:@value).arity if proc.send(:proc?)
44
+ arity = send(:exec_context, options).method(proc.instance_variable_get(:@value)).arity if proc.send(:method?)
45
+ if arity != 1 or name==:getter or name==:if or name==:as
46
+ warn %{[Representable] Positional arguments for `:#{name}` are deprecated. Please use options or keyword arguments.
47
+ #{name}: ->(options) { options[:#{positional_arguments.join(" | :")}] } or
48
+ #{name}: ->(#{positional_arguments.join(":, ")}:) { }
49
+ Learn more here: http://trailblazerb.org/gems/representable/upgrading-guide.html#positional-arguments
50
+ }
51
+ deprecated_args = []
52
+ positional_arguments.each do |arg|
53
+ next if arg == :index && options[:index].nil?
54
+ deprecated_args << __options and next if arg == :user_options# either hash or Options object.
55
+ deprecated_args << options[arg]
56
+ end
57
+
58
+ return proc.(send(:exec_context, options), *deprecated_args)
59
+ end
60
+ end
61
+
62
+ proc.(send(:exec_context, options), options)
63
+ end
64
+ private :evaluate_option_with_deprecation
65
+
66
+
67
+ def represented
68
+ warn "[Representable] Binding#represented is deprecated. Use options[:represented] instead."
69
+ @represented
70
+ end
71
+
72
+ def compile_fragment(options)
73
+ @represented = options[:represented]
74
+ @parent_decorator = options[:decorator]
75
+ super
76
+ end
77
+
78
+ # Parse value from doc and update the model property.
79
+ def uncompile_fragment(options)
80
+ @represented = options[:represented]
81
+ @parent_decorator = options[:decorator]
82
+ super
83
+ end
84
+
85
+ def get(options={}) # DISCUSS: evluate if we really need this.
86
+ warn "[Representable] Binding#get is deprecated."
87
+ self[:getter] ? Representable::Getter.(nil, options.merge(binding: self)) : Representable::Get.(nil, options.merge(binding: self))
88
+ end
89
+ end
90
+ end
@@ -1,115 +1,120 @@
1
1
  module Representable
2
- # Deserializer's job is deserializing the already parsed fragment into a scalar or an object.
3
- # This object is then returned to the Populator.
4
- #
5
- # It respects :deserialize, :prepare, :class, :instance
6
- #
7
- # Collection bindings return an array of parsed fragment items (still native format, e.g. Nokogiri node, for nested objects).
8
- #
9
- # Workflow
10
- # call -> instance/class -> prepare -> deserialize -> from_json.
11
- class Deserializer
12
- def initialize(binding)
13
- @binding = binding
14
- end
2
+ # we don't use keyword args, because i didn't want to discriminate 1.9 users, yet.
3
+ # this will soon get introduces and remove constructs like options[:binding][:default].
15
4
 
16
- def call(fragment, *args) # FIXME: args is always i.
17
- return fragment unless @binding.typed? # customize with :extend. this is not really straight-forward.
18
- return fragment if fragment.nil?
5
+ # Deprecation strategy:
6
+ # binding.evaluate_option_with_deprecation(:reader, options, :doc)
7
+ # => binding.evaluate_option(:reader, options) # always pass in options.
19
8
 
20
- # what if create_object is responsible for providing the deserialize-to object?
21
- object = create_object(fragment, *args) # customize with :instance and :class.
22
- representable = prepare(object) # customize with :prepare and :extend.
9
+ AssignFragment = ->(input, options) { options[:fragment] = input }
23
10
 
24
- deserialize(representable, fragment, @binding.user_options) # deactivate-able via :representable => false.
25
- end
11
+ ReadFragment = ->(input, options) { options[:binding].read(input, options[:as]) }
12
+ Reader = ->(input, options) { options[:binding].evaluate_option(:reader, input, options) }
26
13
 
27
- private
28
- def deserialize(object, fragment, options) # TODO: merge with #serialize.
29
- return object unless @binding.representable?
14
+ StopOnNotFound = ->(input, options) do
15
+ Binding::FragmentNotFound == input ? Pipeline::Stop : input
16
+ end
30
17
 
31
- @binding.evaluate_option(:deserialize, object, fragment) do
32
- demarshal(object, fragment, options)
33
- end
34
- end
18
+ StopOnNil = ->(input, options) do # DISCUSS: Not tested/used, yet.
19
+ input.nil? ? Pipeline::Stop : input
20
+ end
35
21
 
36
- def demarshal(object, fragment, options)
37
- object.send(@binding.deserialize_method, fragment, options)
38
- end
22
+ OverwriteOnNil = ->(input, options) do
23
+ input.nil? ? (Set.(input, options); Pipeline::Stop) : input
24
+ end
39
25
 
40
- module Prepare
41
- def prepare(object)
42
- @binding.evaluate_option(:prepare, object) do
43
- prepare!(object)
44
- end
45
- end
26
+ Default = ->(input, options) do
27
+ Binding::FragmentNotFound == input ? options[:binding][:default] : input
28
+ end
46
29
 
47
- def prepare!(object)
48
- mod = @binding.representer_module_for(object)
30
+ SkipParse = ->(input, options) do
31
+ options[:binding].evaluate_option(:skip_parse, input, options) ? Pipeline::Stop : input
32
+ end
33
+
34
+ Instance = ->(input, options) do
35
+ options[:binding].evaluate_option(:instance, input, options)
36
+ end
49
37
 
50
- return object unless mod
38
+ module Function
39
+ class CreateObject
40
+ def call(input, options)
41
+ AssignFragment.(input, options)
51
42
 
52
- prepare_for(mod, object)
43
+ instance_for(input, options) || class_for(input, options)
53
44
  end
54
45
 
55
- def prepare_for(mod, object)
56
- mod.prepare(object)
46
+ private
47
+ def class_for(input, options)
48
+ item_class = class_from(input, options) or raise DeserializeError.new(":class did not return class constant for `#{options[:binding].name}`.")
49
+ item_class.new
57
50
  end
58
- end
59
- include Prepare
60
51
 
61
- def create_object(fragment, *args)
62
- instance_for(fragment, *args) or class_for(fragment, *args)
63
- end
52
+ def class_from(input, options)
53
+ options[:binding].evaluate_option(:class, input, options) # FIXME: no additional args passed here, yet.
54
+ end
64
55
 
65
- def class_for(fragment, *args)
66
- item_class = class_from(fragment, *args) or raise DeserializeError.new(":class did not return class constant.")
67
- item_class.new
56
+ def instance_for(input, options)
57
+ Instance.(input, options)
58
+ end
68
59
  end
69
60
 
70
- def class_from(fragment, *args)
71
- @binding.evaluate_option(:class, fragment, *args)
61
+ class Prepare
62
+ def call(input, options)
63
+ binding = options[:binding]
64
+
65
+ binding.evaluate_option(:prepare, input, options)
66
+ end
72
67
  end
73
68
 
74
- def instance_for(fragment, *args)
75
- # cool: if no :instance set, { return } will jump out of this method.
76
- @binding.evaluate_option(:instance, fragment, *args) { return } or raise DeserializeError.new(":instance did not return object.")
69
+ class Decorate
70
+ def call(object, options)
71
+ binding = options[:binding]
72
+
73
+ return object unless object # object might be nil.
74
+
75
+ mod = binding.evaluate_option(:extend, object, options)
76
+
77
+ prepare_for(mod, object, binding)
78
+ end
79
+
80
+ def prepare_for(mod, object, binding)
81
+ mod.prepare(object)
82
+ end
77
83
  end
84
+ end
78
85
 
86
+ CreateObject = Function::CreateObject.new
87
+ Prepare = Function::Prepare.new
88
+ Decorate = Function::Decorate.new
89
+ Deserializer = ->(input, options) { options[:binding].evaluate_option(:deserialize, input, options) }
79
90
 
91
+ Deserialize = ->(input, options) do
92
+ binding, fragment, user_options = options[:binding], options[:fragment], options[:user_options]
80
93
 
81
- # Collection does exactly the same as Deserializer but for a collection.
82
- class Collection < self
83
- def call(fragment)
84
- collection = [] # this can be replaced, e.g. AR::Collection or whatever.
94
+ user_options = user_options.merge(wrap: binding[:wrap]) unless binding[:wrap].nil? # DISCUSS: can we leave that here?
95
+ input.send(binding.deserialize_method, fragment, user_options)
96
+ end
85
97
 
86
- fragment.each_with_index do |item_fragment, i|
87
- # add more per-item options here!
88
- next if @binding.evaluate_option(:skip_parse, item_fragment) # TODO: pass in index!
98
+ ParseFilter = ->(input, options) do
99
+ options[:binding][:parse_filter].(input, options)
100
+ end
89
101
 
90
- collection << deserialize!(item_fragment, i) # FIXME: what if obj nil?
91
- end
102
+ Setter = ->(input, options) { options[:binding].evaluate_option(:setter, input, options) }
103
+ Set = ->(input, options) { options[:binding].send(:exec_context, options).send(options[:binding].setter, input) }
92
104
 
93
- collection # with parse_strategy: :sync, this is ignored.
94
- end
95
105
 
96
- private
97
- def deserialize!(*args)
98
- item_deserializer.call(*args)
99
- end
106
+ Stop = ->(*) { Pipeline::Stop }
100
107
 
101
- def item_deserializer
102
- @item_deserializer = Deserializer.new(@binding)
103
- end
104
- end
108
+ If = ->(input, options) { options[:binding].evaluate_option(:if, nil, options) ? input : Pipeline::Stop }
105
109
 
110
+ StopOnExcluded = ->(input, options) do
111
+ return input unless private = options[:_private]
112
+ return input unless props = (private[:exclude] || private[:include])
106
113
 
107
- class Hash < Collection
108
- def call(hash)
109
- {}.tap do |hsh|
110
- hash.each { |key, fragment| hsh[key] = deserialize!(fragment) }
111
- end
112
- end
113
- end
114
+ res = props.include?(options[:binding].name.to_sym) # false with include: Stop. false with exclude: go!
115
+
116
+ return input if private[:include]&&res
117
+ return input if private[:exclude]&&!res
118
+ Pipeline::Stop
114
119
  end
115
120
  end
@@ -13,9 +13,11 @@ module Representable
13
13
 
14
14
  # what happens here is basically
15
15
  # Module.new { include Representable::JSON::Collection; ... }
16
- build_inline(nil, [singular.collection_representer_class], "", {}) {
17
- items options.merge(:extend => singular)
18
- }
16
+ nested_builder.(
17
+ _base: default_nested_class,
18
+ _features: [singular.collection_representer_class],
19
+ _block: ->(*) { items options.merge(:extend => singular) }
20
+ )
19
21
  end
20
22
 
21
23
  def collection_representer(options={})