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.
- 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
|