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
@@ -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={})