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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGES.md +36 -0
- data/Gemfile +3 -3
- data/README.md +62 -2
- data/Rakefile +1 -1
- data/lib/representable.rb +32 -109
- data/lib/representable/TODO.getting_serious +7 -1
- data/lib/representable/autoload.rb +10 -0
- data/lib/representable/binding.rb +20 -16
- data/lib/representable/bindings/xml_bindings.rb +0 -1
- data/lib/representable/coercion.rb +23 -31
- data/lib/representable/config.rb +45 -46
- data/lib/representable/declarative.rb +78 -0
- data/lib/representable/decorator.rb +39 -10
- data/lib/representable/definition.rb +40 -33
- data/lib/representable/deserializer.rb +2 -0
- data/lib/representable/for_collection.rb +25 -0
- data/lib/representable/hash.rb +4 -8
- data/lib/representable/hash/collection.rb +2 -9
- data/lib/representable/hash_methods.rb +0 -7
- data/lib/representable/inheritable.rb +50 -0
- data/lib/representable/json.rb +3 -9
- data/lib/representable/json/collection.rb +1 -3
- data/lib/representable/json/hash.rb +4 -9
- data/lib/representable/mapper.rb +8 -5
- data/lib/representable/parse_strategies.rb +1 -0
- data/lib/representable/pipeline.rb +14 -0
- data/lib/representable/represent.rb +6 -0
- data/lib/representable/version.rb +1 -1
- data/lib/representable/xml.rb +3 -18
- data/lib/representable/xml/collection.rb +2 -4
- data/lib/representable/xml/hash.rb +2 -10
- data/lib/representable/yaml.rb +1 -20
- data/representable.gemspec +2 -2
- data/test/class_test.rb +5 -10
- data/test/coercion_test.rb +31 -92
- data/test/config/inherit_test.rb +128 -0
- data/test/config_test.rb +114 -80
- data/test/definition_test.rb +107 -64
- data/test/features_test.rb +41 -0
- data/test/filter_test.rb +59 -0
- data/test/for_collection_test.rb +74 -0
- data/test/inherit_test.rb +44 -3
- data/test/inheritable_test.rb +97 -0
- data/test/inline_test.rb +0 -18
- data/test/instance_test.rb +0 -19
- data/test/json_test.rb +9 -44
- data/test/lonely_test.rb +1 -0
- data/test/parse_strategy_test.rb +30 -0
- data/test/represent_test.rb +88 -0
- data/test/representable_test.rb +3 -50
- data/test/schema_test.rb +123 -0
- data/test/test_helper.rb +1 -1
- data/test/xml_test.rb +34 -38
- metadata +25 -15
- data/lib/representable/decorator/coercion.rb +0 -4
- data/lib/representable/readable_writeable.rb +0 -29
- data/test/inheritance_test.rb +0 -22
@@ -1,43 +1,35 @@
|
|
1
1
|
require "virtus"
|
2
2
|
|
3
|
-
module Representable
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
17
|
-
base
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
22
|
+
module ClassMethods
|
23
|
+
def build_definition(name, options, &block) # Representable::Declarative
|
24
|
+
return super unless type = options[:type]
|
28
25
|
|
29
|
-
|
26
|
+
options[:pass_options] = true # TODO: remove, standard.
|
30
27
|
|
31
|
-
|
32
|
-
|
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
|
-
|
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
|
data/lib/representable/config.rb
CHANGED
@@ -1,73 +1,72 @@
|
|
1
1
|
module Representable
|
2
|
-
#
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
23
|
-
|
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
|
-
|
27
|
-
|
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
|
31
|
-
|
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
|
-
|
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
|
61
|
+
return unless self[:wrap]
|
42
62
|
|
43
|
-
value =
|
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.
|
11
|
-
|
12
|
-
|
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
|
-
|
19
|
-
|
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
|
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
|
-
|
12
|
-
options =
|
13
|
-
|
14
|
-
|
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[:
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
36
|
+
extend Forwardable
|
37
|
+
def_delegators :@runtime_options, :[], :each
|
30
38
|
|
31
|
-
def
|
32
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|