representable 1.8.5 → 2.0.0.rc1

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