representable 1.8.5 → 2.0.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGES.md +36 -0
  4. data/Gemfile +3 -3
  5. data/README.md +62 -2
  6. data/Rakefile +1 -1
  7. data/lib/representable.rb +32 -109
  8. data/lib/representable/TODO.getting_serious +7 -1
  9. data/lib/representable/autoload.rb +10 -0
  10. data/lib/representable/binding.rb +20 -16
  11. data/lib/representable/bindings/xml_bindings.rb +0 -1
  12. data/lib/representable/coercion.rb +23 -31
  13. data/lib/representable/config.rb +45 -46
  14. data/lib/representable/declarative.rb +78 -0
  15. data/lib/representable/decorator.rb +39 -10
  16. data/lib/representable/definition.rb +40 -33
  17. data/lib/representable/deserializer.rb +2 -0
  18. data/lib/representable/for_collection.rb +25 -0
  19. data/lib/representable/hash.rb +4 -8
  20. data/lib/representable/hash/collection.rb +2 -9
  21. data/lib/representable/hash_methods.rb +0 -7
  22. data/lib/representable/inheritable.rb +50 -0
  23. data/lib/representable/json.rb +3 -9
  24. data/lib/representable/json/collection.rb +1 -3
  25. data/lib/representable/json/hash.rb +4 -9
  26. data/lib/representable/mapper.rb +8 -5
  27. data/lib/representable/parse_strategies.rb +1 -0
  28. data/lib/representable/pipeline.rb +14 -0
  29. data/lib/representable/represent.rb +6 -0
  30. data/lib/representable/version.rb +1 -1
  31. data/lib/representable/xml.rb +3 -18
  32. data/lib/representable/xml/collection.rb +2 -4
  33. data/lib/representable/xml/hash.rb +2 -10
  34. data/lib/representable/yaml.rb +1 -20
  35. data/representable.gemspec +2 -2
  36. data/test/class_test.rb +5 -10
  37. data/test/coercion_test.rb +31 -92
  38. data/test/config/inherit_test.rb +128 -0
  39. data/test/config_test.rb +114 -80
  40. data/test/definition_test.rb +107 -64
  41. data/test/features_test.rb +41 -0
  42. data/test/filter_test.rb +59 -0
  43. data/test/for_collection_test.rb +74 -0
  44. data/test/inherit_test.rb +44 -3
  45. data/test/inheritable_test.rb +97 -0
  46. data/test/inline_test.rb +0 -18
  47. data/test/instance_test.rb +0 -19
  48. data/test/json_test.rb +9 -44
  49. data/test/lonely_test.rb +1 -0
  50. data/test/parse_strategy_test.rb +30 -0
  51. data/test/represent_test.rb +88 -0
  52. data/test/representable_test.rb +3 -50
  53. data/test/schema_test.rb +123 -0
  54. data/test/test_helper.rb +1 -1
  55. data/test/xml_test.rb +34 -38
  56. metadata +25 -15
  57. data/lib/representable/decorator/coercion.rb +0 -4
  58. data/lib/representable/readable_writeable.rb +0 -29
  59. data/test/inheritance_test.rb +0 -22
@@ -34,7 +34,6 @@ module Representable
34
34
 
35
35
  # Creates wrapped node for the property.
36
36
  def serialize_for(value, parent)
37
- #def serialize_for(value, parent, tag_name=definition.from)
38
37
  node = node_for(parent, as)
39
38
  serialize_node(node, value)
40
39
  end
@@ -1,43 +1,35 @@
1
1
  require "virtus"
2
2
 
3
- module Representable::Coercion
4
- class Coercer
5
- virtus_coercer = Virtus.respond_to?(:model) ? Virtus.model : Virtus
6
- include virtus_coercer
7
-
8
- def coerce(name, v) # TODO: test me.
9
- # set and get the value as i don't know where exactly coercion happens in virtus.
10
- send("#{name}=", v)
11
- send(name)
3
+ module Representable
4
+ module Coercion
5
+ class Coercer
6
+ # This gets called when the :render_filter or :parse_filter option is evaluated.
7
+ # Usually the Coercer instance is an element in a Pipeline to allow >1 filters per property.
8
+ def call(value, doc, options)
9
+ Virtus::Attribute.build(options.binding[:type]).coerce(value)
10
+ end
12
11
  end
13
- end
14
- # separate coercion object doesn't give us initializer and accessors in the represented object (with module representer)!
15
12
 
16
- def self.included(base)
17
- base.class_eval do
18
- extend ClassMethods
19
- # FIXME: use inheritable_attr when it's ready.
20
- representable_attrs.inheritable_array(:coercer_class) << Class.new(Coercer) unless representable_attrs.inheritable_array(:coercer_class).first
13
+
14
+ def self.included(base)
15
+ base.class_eval do
16
+ extend ClassMethods
17
+ register_feature Coercion
18
+ end
21
19
  end
22
20
 
23
- end
24
21
 
25
- module ClassMethods
26
- def property(name, options={})
27
- return super unless options[:type]
22
+ module ClassMethods
23
+ def build_definition(name, options, &block) # Representable::Declarative
24
+ return super unless type = options[:type]
28
25
 
29
- representable_attrs.inheritable_array(:coercer_class).first.attribute(name, options[:type])
26
+ options[:pass_options] = true # TODO: remove, standard.
30
27
 
31
- # By using :getter we "pre-occupy" this directive, but we avoid creating accessors, which i find is the cleaner way.
32
- options[:exec_context] = :decorator
33
- options[:getter] = lambda { |*| coercer.coerce(name, represented.send(name)) }
34
- options[:setter] = lambda { |v,*| represented.send("#{name}=", coercer.coerce(name, v)) }
28
+ options[:render_filter] << coercer = Coercer.new
29
+ options[:parse_filter] << coercer
35
30
 
36
- super
31
+ super
32
+ end
37
33
  end
38
34
  end
39
-
40
- def coercer
41
- @coercer ||= representable_attrs.inheritable_array(:coercer_class).first.new
42
- end
43
- end
35
+ end
@@ -1,73 +1,72 @@
1
1
  module Representable
2
- # NOTE: the API of Config is subject to change so don't rely too much on this private object.
3
- class Config < Hash
4
- # DISCUSS: experimental. this will soon be moved to a separate gem
5
- module InheritableArray
6
- def inheritable_array(name)
7
- inheritable_arrays[name] ||= []
8
- end
9
- def inheritable_arrays
10
- @inheritable_arrays ||= {}
11
- end
12
-
13
- def inherit(parent)
14
- super
2
+ # Config contains three independent, inheritable directives: features, options and definitions.
3
+ # It is a hash - just add directives if you need them.
4
+ #
5
+ # You may access/alter the property Definitions using #each, #collect, #add, #get.
6
+ #
7
+ # * features, [options]: "horizontally"+"vertically" inherited values (inline representer)
8
+ # * definitions, [options], wrap: "vertically" inherited (class inheritance, module inclusion)
9
+ #
10
+ # Inheritance works via Config#inherit!(parent).
11
+ class Config < Inheritable::Hash
12
+ # Keep in mind that performance doesn't matter here as 99.9% of all representers are created
13
+ # at compile-time.
15
14
 
16
- parent.inheritable_arrays.keys.each do |k|
17
- inheritable_array(k).push *parent.inheritable_array(k).clone
15
+ # Stores Definitions from ::property. It preserves the adding order (1.9+).
16
+ # Same-named properties get overridden, just like in a Hash.
17
+ class Definitions < Inheritable::Hash
18
+ def add(name, options, &block)
19
+ if options[:inherit] # i like that: the :inherit shouldn't be handled outside.
20
+ return get(name).merge!(options, &block)
18
21
  end
22
+
23
+ self[name.to_s] = Definition.new(name, options, &block)
19
24
  end
20
- end
21
25
 
22
- def <<(definition)
23
- self[definition.name] = definition
26
+ def get(name)
27
+ self[name.to_s]
28
+ end
29
+
30
+ extend Forwardable
31
+ def_delegators :values, :each # so we look like an array. this is only used in Mapper. we could change that so we don't need to hide the hash.
24
32
  end
25
33
 
26
- def [](name)
27
- fetch(name.to_s, nil)
34
+
35
+ def initialize
36
+ super
37
+ merge!(
38
+ :features => @features = Inheritable::Hash.new,
39
+ :definitions => @definitions = Definitions.new,
40
+ :options => @options = Inheritable::Hash.new,
41
+ :wrap => nil )
28
42
  end
43
+ attr_reader :options
29
44
 
30
- def each(*args, &block)
31
- values.each(*args, &block)
45
+ def features
46
+ @features.keys
32
47
  end
33
48
 
49
+ # delegate #collect etc to Definitions instance.
50
+ extend Forwardable
51
+ def_delegators :@definitions, :get, :add, :each, :size
52
+ # #collect comes from Hash and then gets delegated to @definitions. don't like that.
53
+
34
54
  def wrap=(value)
35
55
  value = value.to_s if value.is_a?(Symbol)
36
- @wrap = Uber::Options::Value.new(value)
56
+ self[:wrap] = Uber::Options::Value.new(value)
37
57
  end
38
58
 
39
59
  # Computes the wrap string or returns false.
40
60
  def wrap_for(name, context, *args)
41
- return unless @wrap
61
+ return unless self[:wrap]
42
62
 
43
- value = @wrap.evaluate(context, *args)
63
+ value = self[:wrap].evaluate(context, *args)
44
64
 
45
65
  return infer_name_for(name) if value === true
46
66
  value
47
67
  end
48
68
 
49
- # Write representer configuration into this hash.
50
- def options
51
- @options ||= {}
52
- end
53
-
54
- module InheritMethods
55
- def cloned
56
- collect { |d| d.clone }
57
- end
58
-
59
- def inherit(parent)
60
- push(parent.cloned)
61
- end
62
- end
63
- include InheritMethods
64
- include InheritableArray # overrides #inherit.
65
-
66
69
  private
67
- def push(defs)
68
- defs.each { |d| self << d }
69
- end
70
-
71
70
  def infer_name_for(name)
72
71
  name.to_s.split('::').last.
73
72
  gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
@@ -0,0 +1,78 @@
1
+ module Representable
2
+ module Declarative
3
+ def representable_attrs
4
+ @representable_attrs ||= build_config
5
+ end
6
+
7
+ def representation_wrap=(name)
8
+ representable_attrs.wrap = name
9
+ end
10
+
11
+ def collection(name, options={}, &block)
12
+ options[:collection] = true # FIXME: don't override original.
13
+ property(name, options, &block)
14
+ end
15
+
16
+ def hash(name=nil, options={}, &block)
17
+ return super() unless name # allow Object.hash.
18
+
19
+ options[:hash] = true
20
+ property(name, options, &block)
21
+ end
22
+
23
+ # Allows you to nest a block of properties in a separate section while still mapping them to the outer object.
24
+ def nested(name, options={}, &block)
25
+ options = options.merge(
26
+ :use_decorator => true,
27
+ :getter => lambda { |*| self },
28
+ :setter => lambda { |*| },
29
+ :instance => lambda { |*| self }
30
+ ) # DISCUSS: should this be a macro just as :parse_strategy?
31
+
32
+ property(name, options, &block)
33
+ end
34
+
35
+ def property(name, options={}, &block)
36
+ representable_attrs.add(name, options) do |default_options| # handles :inherit.
37
+ build_definition(name, default_options, &block)
38
+ end
39
+ end
40
+
41
+ def build_inline(base, features, name, options, &block) # DISCUSS: separate module?
42
+ Module.new do
43
+ include Representable
44
+ feature *features # Representable::JSON or similar.
45
+ include base if base # base when :inherit, or in decorator.
46
+
47
+ instance_exec &block
48
+ end
49
+ end
50
+
51
+ private
52
+ # NOTE: this will soon be extracted to separate class, use at your own risk.
53
+ def build_definition(name, options, &block)
54
+ # change options[..]= here.
55
+
56
+ base = nil
57
+
58
+ if options[:inherit] # TODO: move this to Definition.
59
+ base = representable_attrs.get(name).representer_module
60
+ end # FIXME: can we handle this in super/Definition.new ?
61
+
62
+ if block
63
+ options[:_inline] = true
64
+ options[:extend] = inline_representer_for(base, representable_attrs.features, name, options, &block)
65
+ end
66
+ end
67
+
68
+ def inline_representer_for(base, features, name, options, &block)
69
+ representer = options[:use_decorator] ? Decorator : self
70
+
71
+ representer.build_inline(base, features.reverse, name, options, &block)
72
+ end
73
+
74
+ def build_config
75
+ Config.new
76
+ end
77
+ end # Declarations
78
+ end
@@ -3,28 +3,57 @@ module Representable
3
3
  attr_reader :represented
4
4
  alias_method :decorated, :represented
5
5
 
6
+ # TODO: when moving all class methods into ClassMethods, i get a segfault.
6
7
  def self.prepare(represented)
7
8
  new(represented)
8
9
  end
9
10
 
10
- def self.inline_representer(base_module, name, options, &block)
11
- # FIXME: it is wrong to inherit from self here as we just want to "inherit" the included modules but nothing else.
12
- Class.new(self).tap do |decorator|
13
- decorator.class_eval do # Ruby 1.8.7 wouldn't properly execute the block passed to Class.new!
14
- # Remove parent's property definitions before defining the inline ones. #FIXME: don't inherit from self, remove those 2 lines.
15
- representable_attrs.clear
16
- representable_attrs.inheritable_arrays.clear
11
+ def self.default_inline_class
12
+ Representable::Decorator
13
+ end
17
14
 
18
- include *base_module
19
- instance_exec &block
15
+ include Representable # include after class methods so Decorator::prepare can't be overwritten by Representable::prepare.
16
+
17
+ module InheritModule
18
+ def inherit_module!(parent)
19
+ inherited_attrs = parent.representable_attrs[:definitions].keys
20
+
21
+ super # in Representable, calls representable_attrs.inherit!(parent.representable_attrs).
22
+ # now, inline representers are still modules, which is wrong.
23
+ manifest!(inherited_attrs)
24
+ end
25
+
26
+ private
27
+ # one level deep manifesting modules into Decorators.
28
+ def manifest!(names)
29
+ names.each do |name| # only definitions.
30
+ definition = representable_attrs.get(name)
31
+ next unless definition[:_inline] and mod = definition.representer_module # only inline representers.
32
+
33
+ # here, we can include Decorator features.
34
+ inline_representer = build_inline(nil, representable_attrs.features, definition.name, {}) {
35
+ include mod
36
+ } # the includer controls what "wraps" the module.
37
+
38
+ definition.merge!(:extend => inline_representer)
20
39
  end
21
40
  end
22
41
  end
42
+ extend InheritModule
23
43
 
24
- include Representable # include after class methods so Decorator::prepare can't be overwritten by Representable::prepare.
25
44
 
26
45
  def initialize(represented)
27
46
  @represented = represented
28
47
  end
48
+
49
+ private
50
+ def self.build_inline(base, features, name, options, &block)
51
+ Class.new(base || default_inline_class).tap do |decorator|
52
+ decorator.class_eval do # Ruby 1.8.7 wouldn't properly execute the block passed to Class.new!
53
+ feature *features
54
+ instance_exec &block
55
+ end
56
+ end
57
+ end
29
58
  end
30
59
  end
@@ -3,34 +3,41 @@ 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 < Hash
6
+ class Definition
7
+ include Representable::Cloneable
8
+
7
9
  attr_reader :name
8
10
  alias_method :getter, :name
9
11
 
10
- def initialize(sym, options={})
11
- super()
12
- options = options.clone
13
-
14
- handle_deprecations!(options)
12
+ def initialize(sym, options={}, &block)
13
+ @options = {}
14
+ # @options = Inheritable::Hash.new # allows deep cloning. we then had to set Pipeline cloneable.
15
+ @name = sym.to_s
16
+ options = options.clone
15
17
 
16
- @name = sym.to_s
17
18
  # defaults:
18
- options[:as] ||= @name
19
+ options[:parse_filter] = Pipeline[*options[:parse_filter]]
20
+ options[:render_filter] = Pipeline[*options[:render_filter]]
21
+ options[:as] ||= @name
19
22
 
20
- setup!(options)
23
+ setup!(options, &block)
21
24
  end
22
25
 
23
- # TODO: test merge!.
24
- def merge!(options)
25
- setup!(options)
26
+ def merge!(options, &block)
27
+ options = options.clone
28
+
29
+ options[:parse_filter] = @options[:parse_filter].push(*options[:parse_filter])
30
+ options[:render_filter] = @options[:render_filter].push(*options[:render_filter])
31
+
32
+ setup!(options, &block) # FIXME: this doesn't yield :as etc.
26
33
  self
27
34
  end
28
35
 
29
- private :[]= # TODO: re-privatize #default when this is sorted with Rubinius.
36
+ extend Forwardable
37
+ def_delegators :@runtime_options, :[], :each
30
38
 
31
- def options # TODO: remove in 2.0.
32
- warn "Representable::Definition#option is deprecated, use #[] directly."
33
- self
39
+ def clone
40
+ self.class.new(name, @options.clone)
34
41
  end
35
42
 
36
43
  def setter
@@ -55,44 +62,54 @@ module Representable
55
62
  end
56
63
 
57
64
  def default_for(value)
58
- return self[:default] if skipable_nil_value?(value)
65
+ return self[:default] if skipable_empty_value?(value)
59
66
  value
60
67
  end
61
68
 
62
69
  def has_default?
63
- has_key?(:default)
70
+ @options.has_key?(:default)
64
71
  end
65
72
 
66
73
  def representer_module
67
- self[:extend]
74
+ @options[:extend]
68
75
  end
69
76
 
70
77
  def skipable_empty_value?(value)
71
78
  return true if array? and self[:render_empty] == false and value and value.size == 0 # TODO: change in 2.0, don't render emtpy.
72
79
  value.nil? and not self[:render_nil]
73
80
  end
74
- alias_method :skipable_nil_value?, :skipable_empty_value? # TODO: remove in 1.9 .
75
81
 
76
82
  def create_binding(*args)
77
83
  self[:binding].call(self, *args)
78
84
  end
79
85
 
80
86
  private
81
- def setup!(options)
87
+ def setup!(options, &block)
82
88
  handle_extend!(options)
83
89
  handle_as!(options)
84
90
 
85
91
  # DISCUSS: we could call more macros here (e.g. for :nested).
86
92
  Representable::ParseStrategy.apply!(options)
87
93
 
94
+ yield options if block_given?
95
+ @options.merge!(options)
96
+
97
+ runtime_options!(@options)
98
+ end
99
+
100
+ # wrapping dynamic options in Value does save runtime, as this is used very frequently (and totally unnecessary to wrap an option
101
+ # at runtime, its value never changes).
102
+ def runtime_options!(options)
103
+ @runtime_options = {}
104
+
88
105
  for name, value in options
89
106
  value = Uber::Options::Value.new(value) if dynamic_options.include?(name)
90
- self[name] = value
107
+ @runtime_options[name] = value
91
108
  end
92
109
  end
93
110
 
94
111
  def dynamic_options
95
- [:as, :getter, :setter, :class, :instance, :reader, :writer, :extend, :prepare, :if, :deserialize, :serialize]
112
+ [:as, :getter, :setter, :class, :instance, :reader, :writer, :extend, :prepare, :if, :deserialize, :serialize, :render_filter, :parse_filter]
96
113
  end
97
114
 
98
115
  def handle_extend!(options)
@@ -102,15 +119,5 @@ module Representable
102
119
  def handle_as!(options)
103
120
  options[:as] = options[:as].to_s if options[:as].is_a?(Symbol) # Allow symbols for as:
104
121
  end
105
-
106
- # TODO: remove in 2.0.
107
- def handle_deprecations!(options)
108
- raise "The :from option got replaced by :as in Representable 1.8!" if options[:from]
109
-
110
- if options[:decorator_scope]
111
- warn "[Representable] Deprecation: `decorator_scope: true` is deprecated, use `exec_context: :decorator` instead."
112
- options.merge!(:exec_context => :decorator)
113
- end
114
- end
115
122
  end
116
123
  end