representable 1.7.7 → 1.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +42 -8
- data/README.md +208 -55
- data/Rakefile +0 -6
- data/lib/representable.rb +39 -43
- data/lib/representable/binding.rb +59 -37
- data/lib/representable/bindings/hash_bindings.rb +3 -4
- data/lib/representable/bindings/xml_bindings.rb +10 -10
- data/lib/representable/bindings/yaml_bindings.rb +2 -2
- data/lib/representable/coercion.rb +1 -1
- data/lib/representable/config.rb +11 -5
- data/lib/representable/definition.rb +67 -35
- data/lib/representable/deserializer.rb +23 -27
- data/lib/representable/hash.rb +15 -4
- data/lib/representable/hash/allow_symbols.rb +27 -0
- data/lib/representable/json.rb +0 -1
- data/lib/representable/json/collection.rb +0 -2
- data/lib/representable/mapper.rb +6 -13
- data/lib/representable/parse_strategies.rb +57 -0
- data/lib/representable/readable_writeable.rb +29 -0
- data/lib/representable/serializer.rb +9 -4
- data/lib/representable/version.rb +1 -1
- data/lib/representable/xml.rb +1 -1
- data/lib/representable/xml/collection.rb +0 -2
- data/lib/representable/yaml.rb +0 -1
- data/representable.gemspec +1 -0
- data/test/as_test.rb +43 -0
- data/test/class_test.rb +124 -0
- data/test/config_test.rb +13 -3
- data/test/decorator_scope_test.rb +28 -0
- data/test/definition_test.rb +46 -35
- data/test/exec_context_test.rb +93 -0
- data/test/generic_test.rb +0 -154
- data/test/getter_setter_test.rb +28 -0
- data/test/hash_bindings_test.rb +35 -35
- data/test/hash_test.rb +0 -20
- data/test/if_test.rb +78 -0
- data/test/inherit_test.rb +21 -1
- data/test/inheritance_test.rb +1 -1
- data/test/inline_test.rb +40 -2
- data/test/instance_test.rb +286 -0
- data/test/is_representable_test.rb +77 -0
- data/test/json_test.rb +6 -29
- data/test/nested_test.rb +30 -0
- data/test/parse_strategy_test.rb +249 -0
- data/test/pass_options_test.rb +27 -0
- data/test/prepare_test.rb +67 -0
- data/test/reader_writer_test.rb +19 -0
- data/test/representable_test.rb +25 -265
- data/test/stringify_hash_test.rb +41 -0
- data/test/test_helper.rb +12 -4
- data/test/wrap_test.rb +48 -0
- data/test/xml_bindings_test.rb +37 -37
- data/test/xml_test.rb +14 -14
- metadata +94 -30
- data/lib/representable/deprecations.rb +0 -4
- data/lib/representable/feature/readable_writeable.rb +0 -30
data/Rakefile
CHANGED
@@ -9,9 +9,3 @@ Rake::TestTask.new(:test) do |test|
|
|
9
9
|
test.test_files = FileList['test/*_test.rb']
|
10
10
|
test.verbose = true
|
11
11
|
end
|
12
|
-
|
13
|
-
Rake::TestTask.new(:test18) do |test|
|
14
|
-
test.libs << 'test'
|
15
|
-
test.test_files = FileList['test/*_test.rb'] - ['test/mongoid_test.rb', 'test/yaml_test.rb']
|
16
|
-
test.verbose = true
|
17
|
-
end
|
data/lib/representable.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'representable/deprecations'
|
2
1
|
require 'representable/definition'
|
3
2
|
require 'representable/mapper'
|
4
3
|
require 'representable/config'
|
@@ -12,8 +11,6 @@ module Representable
|
|
12
11
|
extend ClassMethods
|
13
12
|
extend ClassMethods::Declarations
|
14
13
|
extend DSLAdditions
|
15
|
-
|
16
|
-
include Deprecations
|
17
14
|
end
|
18
15
|
end
|
19
16
|
|
@@ -35,9 +32,7 @@ private
|
|
35
32
|
end
|
36
33
|
|
37
34
|
def representable_binding_for(attribute, format, options)
|
38
|
-
|
39
|
-
|
40
|
-
format.build(attribute, represented, options, context)
|
35
|
+
format.build(attribute, represented, self, options)
|
41
36
|
end
|
42
37
|
|
43
38
|
def cleanup_options(options) # TODO: remove me. this clearly belongs in Representable.
|
@@ -54,8 +49,8 @@ private
|
|
54
49
|
end
|
55
50
|
|
56
51
|
|
57
|
-
def representation_wrap
|
58
|
-
representable_attrs.wrap_for(self.class.name
|
52
|
+
def representation_wrap(*args)
|
53
|
+
representable_attrs.wrap_for(self.class.name, represented, *args)
|
59
54
|
end
|
60
55
|
|
61
56
|
def represented
|
@@ -105,28 +100,10 @@ private
|
|
105
100
|
representable_attrs.wrap = name
|
106
101
|
end
|
107
102
|
|
108
|
-
# Declares a represented document node, which is usually a XML tag or a JSON key.
|
109
|
-
#
|
110
|
-
# Examples:
|
111
|
-
#
|
112
|
-
# property :name
|
113
|
-
# property :name, :from => :title
|
114
|
-
# property :name, :class => Name
|
115
|
-
# property :name, :default => "Mike"
|
116
|
-
# property :name, :render_nil => true
|
117
|
-
# property :name, :readable => false
|
118
|
-
# property :name, :writeable => false
|
119
103
|
def property(name, options={}, &block)
|
120
104
|
representable_attrs << definition_class.new(name, options)
|
121
105
|
end
|
122
106
|
|
123
|
-
# Declares a represented document node collection.
|
124
|
-
#
|
125
|
-
# Examples:
|
126
|
-
#
|
127
|
-
# collection :products
|
128
|
-
# collection :products, :from => :item
|
129
|
-
# collection :products, :class => Product
|
130
107
|
def collection(name, options={}, &block)
|
131
108
|
options[:collection] = true # FIXME: don't override original.
|
132
109
|
property(name, options, &block)
|
@@ -155,25 +132,30 @@ private
|
|
155
132
|
# Allows you to nest a block of properties in a separate section while still mapping them to the outer object.
|
156
133
|
def nested(name, options={}, &block)
|
157
134
|
options = options.merge(
|
158
|
-
:
|
159
|
-
:getter
|
160
|
-
:setter
|
161
|
-
:instance
|
162
|
-
)
|
135
|
+
:use_decorator => true,
|
136
|
+
:getter => lambda { |*| self },
|
137
|
+
:setter => lambda { |*| },
|
138
|
+
:instance => lambda { |*| self }
|
139
|
+
) # DISCUSS: should this be a macro just as :parse_strategy?
|
163
140
|
|
164
141
|
property(name, options, &block)
|
165
142
|
end
|
166
143
|
|
167
144
|
def property(name, options={}, &block)
|
168
|
-
|
145
|
+
modules = []
|
146
|
+
|
147
|
+
if options[:inherit] # TODO: move this to Definition.
|
148
|
+
parent = representable_attrs[name]
|
149
|
+
modules << parent[:extend].evaluate(nil) if parent[:extend]# we can savely assume this is _not_ a lambda. # DISCUSS: leave that in #representer_module?
|
150
|
+
end # FIXME: can we handle this in super/Definition.new ?
|
169
151
|
|
170
152
|
if block_given?
|
171
|
-
|
153
|
+
handle_deprecated_inline_extend!(modules, options)
|
154
|
+
|
155
|
+
options[:extend] = inline_representer_for(modules, name, options, &block)
|
172
156
|
end
|
173
157
|
|
174
|
-
if options
|
175
|
-
parent.options.merge!(options) and return parent
|
176
|
-
end # FIXME: can we handle this in super/Definition.new ?
|
158
|
+
return parent.merge!(options) if options.delete(:inherit)
|
177
159
|
|
178
160
|
super
|
179
161
|
end
|
@@ -186,16 +168,30 @@ private
|
|
186
168
|
end
|
187
169
|
|
188
170
|
private
|
189
|
-
def inline_representer_for(
|
190
|
-
representer = options[:
|
191
|
-
|
192
|
-
modules = [representer_engine]
|
193
|
-
modules << parent.representer_module if options[:inherit]
|
194
|
-
modules << options[:extend]
|
171
|
+
def inline_representer_for(modules, name, options, &block)
|
172
|
+
representer = options[:use_decorator] ? Decorator : self
|
173
|
+
modules = [representer_engine] + modules
|
195
174
|
|
196
175
|
representer.inline_representer(modules.compact.reverse, name, options, &block)
|
197
176
|
end
|
177
|
+
|
178
|
+
def handle_deprecated_inline_extend!(modules, options) # TODO: remove in 2.0.
|
179
|
+
return unless include_module = options.delete(:extend) and not options[:inherit]
|
180
|
+
|
181
|
+
warn "[Representable] Using :extend with an inline representer is deprecated. Include the module in the inline block."
|
182
|
+
modules << include_module
|
183
|
+
end
|
198
184
|
end # DSLAdditions
|
199
185
|
end
|
200
186
|
|
201
|
-
|
187
|
+
|
188
|
+
module Representable
|
189
|
+
autoload :Hash, 'representable/hash'
|
190
|
+
|
191
|
+
module Hash
|
192
|
+
autoload :AllowSymbols, 'representable/hash/allow_symbols'
|
193
|
+
autoload :Collection, 'representable/hash/collection'
|
194
|
+
end
|
195
|
+
|
196
|
+
autoload :Decorator, 'representable/decorator'
|
197
|
+
end
|
@@ -10,29 +10,35 @@ module Representable
|
|
10
10
|
|
11
11
|
def self.build(definition, *args)
|
12
12
|
# DISCUSS: move #create_binding to this class?
|
13
|
-
return definition.create_binding(*args) if definition
|
13
|
+
return definition.create_binding(*args) if definition[:binding]
|
14
14
|
build_for(definition, *args)
|
15
15
|
end
|
16
16
|
|
17
|
-
def initialize(definition, represented, user_options={}
|
17
|
+
def initialize(definition, represented, decorator, user_options={}) # TODO: remove default arg for user options.
|
18
18
|
super(definition)
|
19
19
|
@represented = represented
|
20
|
+
@decorator = decorator
|
20
21
|
@user_options = user_options
|
21
|
-
|
22
|
+
|
23
|
+
setup_exec_context!
|
22
24
|
end
|
23
25
|
|
24
26
|
attr_reader :user_options, :represented # TODO: make private/remove.
|
25
27
|
|
28
|
+
def as # DISCUSS: private?
|
29
|
+
evaluate_option(:as)
|
30
|
+
end
|
31
|
+
|
26
32
|
# Retrieve value and write fragment to the doc.
|
27
33
|
def compile_fragment(doc)
|
28
|
-
|
34
|
+
evaluate_option(:writer, doc) do
|
29
35
|
write_fragment(doc, get)
|
30
36
|
end
|
31
37
|
end
|
32
38
|
|
33
39
|
# Parse value from doc and update the model property.
|
34
40
|
def uncompile_fragment(doc)
|
35
|
-
|
41
|
+
evaluate_option(:reader, doc) do
|
36
42
|
read_fragment(doc) do |value|
|
37
43
|
set(value)
|
38
44
|
end
|
@@ -55,7 +61,7 @@ module Representable
|
|
55
61
|
|
56
62
|
if value == FragmentNotFound
|
57
63
|
return unless has_default?
|
58
|
-
value = default
|
64
|
+
value = self[:default]
|
59
65
|
end
|
60
66
|
|
61
67
|
yield value
|
@@ -65,47 +71,52 @@ module Representable
|
|
65
71
|
read(doc)
|
66
72
|
end
|
67
73
|
|
68
|
-
# concept: Option#call(*args) => send(string)/lambda()
|
69
|
-
# dynamic string
|
70
74
|
def get
|
71
|
-
|
75
|
+
evaluate_option(:getter) do
|
72
76
|
exec_context.send(getter)
|
73
77
|
end
|
74
78
|
end
|
75
79
|
|
76
80
|
def set(value)
|
77
|
-
|
81
|
+
evaluate_option(:setter, value) do
|
78
82
|
exec_context.send(setter, value)
|
79
83
|
end
|
80
84
|
end
|
81
85
|
|
82
|
-
#
|
86
|
+
# DISCUSS: do we really need that?
|
87
|
+
def representer_module_for(object, *args)
|
88
|
+
evaluate_option(:extend, object) # TODO: pass args? do we actually have args at the time this is called (compile-time)?
|
89
|
+
end
|
83
90
|
|
84
91
|
private
|
85
|
-
|
92
|
+
def setup_exec_context!
|
93
|
+
context = represented
|
94
|
+
context = self if self[:exec_context] == :binding
|
95
|
+
context = decorator if self[:exec_context] == :decorator
|
86
96
|
|
87
|
-
|
88
|
-
# Executes passed block when there's no lambda for option.
|
89
|
-
def represented_exec_for(option_name, *args)
|
90
|
-
return yield unless options[option_name]
|
91
|
-
call_proc_for(options[option_name], *args)
|
92
|
-
end
|
93
|
-
|
94
|
-
# All lambdas are executed on exec_context which is either represented or the decorator instance.
|
95
|
-
def call_proc_for(proc, *args)
|
96
|
-
return proc unless proc.is_a?(Proc)
|
97
|
-
# TODO: call method when proc is sympbol.
|
98
|
-
args << user_options # DISCUSS: we assume user_options is a Hash!
|
99
|
-
exec_context.instance_exec(*args, &proc)
|
97
|
+
@exec_context = context
|
100
98
|
end
|
101
99
|
|
100
|
+
attr_reader :exec_context, :decorator
|
102
101
|
|
103
|
-
|
104
|
-
|
105
|
-
|
102
|
+
# Evaluate the option (either nil, static, a block or an instance method call) or
|
103
|
+
# executes passed block when option not defined.
|
104
|
+
def evaluate_option(name, *args)
|
105
|
+
unless proc = self[name]
|
106
|
+
return yield if block_given?
|
107
|
+
return
|
106
108
|
end
|
109
|
+
|
110
|
+
# TODO: it would be better if user_options was nil per default and then we just don't pass it into lambdas.
|
111
|
+
options = self[:pass_options] ? Options.new(self, user_options, represented, decorator) : user_options
|
112
|
+
|
113
|
+
proc.evaluate(exec_context, *(args<<options)) # from Uber::Options::Value.
|
107
114
|
end
|
108
|
-
|
115
|
+
|
116
|
+
|
117
|
+
# Options instance gets passed to lambdas when pass_options: true.
|
118
|
+
# This is considered the new standard way and should be used everywhere for forward-compat.
|
119
|
+
Options = Struct.new(:binding, :user_options, :represented, :decorator)
|
109
120
|
|
110
121
|
|
111
122
|
# Delegates to call #to_*/from_*.
|
@@ -114,28 +125,39 @@ module Representable
|
|
114
125
|
ObjectSerializer.new(self, object).call
|
115
126
|
end
|
116
127
|
|
117
|
-
def deserialize(data
|
128
|
+
def deserialize(data)
|
118
129
|
# DISCUSS: does it make sense to skip deserialization of nil-values here?
|
119
|
-
ObjectDeserializer.new(self
|
130
|
+
ObjectDeserializer.new(self).call(data)
|
120
131
|
end
|
121
132
|
|
122
|
-
def create_object(fragment)
|
123
|
-
instance_for(fragment) or class_for(fragment)
|
133
|
+
def create_object(fragment, *args)
|
134
|
+
instance_for(fragment, *args) or class_for(fragment, *args)
|
124
135
|
end
|
125
136
|
|
126
137
|
private
|
127
138
|
def class_for(fragment, *args)
|
128
|
-
item_class = class_from(fragment) or return fragment
|
139
|
+
item_class = class_from(fragment, *args) or return handle_deprecated_class(fragment)
|
129
140
|
item_class.new
|
130
141
|
end
|
131
142
|
|
132
143
|
def class_from(fragment, *args)
|
133
|
-
|
144
|
+
evaluate_option(:class, fragment, *args)
|
134
145
|
end
|
135
146
|
|
136
147
|
def instance_for(fragment, *args)
|
137
|
-
|
138
|
-
|
148
|
+
instance = evaluate_option(:instance, fragment, *args)
|
149
|
+
|
150
|
+
if instance === true # TODO: remove in 2.0.
|
151
|
+
warn "[Representable] `instance: lambda { true }` is deprecated. Apparently, you know what you're doing, so use `parse_strategy: :sync` instead."
|
152
|
+
return get
|
153
|
+
end
|
154
|
+
|
155
|
+
instance
|
156
|
+
end
|
157
|
+
|
158
|
+
def handle_deprecated_class(fragment) # TODO: remove in 2.0.
|
159
|
+
warn "[Representable] `class: lambda { nil }` is deprecated. To return the fragment from parsing, use `instance: lambda { |fragment, *args| fragment }` instead."
|
160
|
+
fragment
|
139
161
|
end
|
140
162
|
end
|
141
163
|
end
|
@@ -12,14 +12,14 @@ module Representable
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def read(hash)
|
15
|
-
return FragmentNotFound unless hash.has_key?(
|
15
|
+
return FragmentNotFound unless hash.has_key?(as) # DISCUSS: put it all in #read for performance. not really sure if i like returning that special thing.
|
16
16
|
|
17
|
-
fragment = hash[
|
17
|
+
fragment = hash[as]
|
18
18
|
deserialize(fragment)
|
19
19
|
end
|
20
20
|
|
21
21
|
def write(hash, value)
|
22
|
-
hash[
|
22
|
+
hash[as] = serialize(value)
|
23
23
|
end
|
24
24
|
|
25
25
|
def deserialize_from(fragment)
|
@@ -37,7 +37,6 @@ module Representable
|
|
37
37
|
|
38
38
|
class CollectionBinding < PropertyBinding
|
39
39
|
def serialize(value)
|
40
|
-
# value.enum_for(:each_with_index).collect { |obj, i| serialize(obj, i) } # DISCUSS: provide ary index/hash key for representer_module_for?
|
41
40
|
value.collect { |item| super(item) } # TODO: i don't want Array but Forms here - what now?
|
42
41
|
end
|
43
42
|
|
@@ -8,17 +8,17 @@ module Representable
|
|
8
8
|
|
9
9
|
def self.build_for(definition, *args)
|
10
10
|
return CollectionBinding.new(definition, *args) if definition.array?
|
11
|
-
return HashBinding.new(definition, *args) if definition.hash? and not definition
|
12
|
-
return AttributeHashBinding.new(definition, *args) if definition.hash? and definition
|
13
|
-
return AttributeBinding.new(definition, *args) if definition
|
14
|
-
return ContentBinding.new(definition, *args) if definition
|
11
|
+
return HashBinding.new(definition, *args) if definition.hash? and not definition[:use_attributes] # FIXME: hate this.
|
12
|
+
return AttributeHashBinding.new(definition, *args) if definition.hash? and definition[:use_attributes]
|
13
|
+
return AttributeBinding.new(definition, *args) if definition[:attribute]
|
14
|
+
return ContentBinding.new(definition, *args) if definition[:content]
|
15
15
|
new(definition, *args)
|
16
16
|
end
|
17
17
|
|
18
18
|
def write(parent, value)
|
19
19
|
wrap_node = parent
|
20
20
|
|
21
|
-
if wrap =
|
21
|
+
if wrap = self[:wrap]
|
22
22
|
parent << wrap_node = node_for(parent, wrap)
|
23
23
|
end
|
24
24
|
|
@@ -35,7 +35,7 @@ module Representable
|
|
35
35
|
# Creates wrapped node for the property.
|
36
36
|
def serialize_for(value, parent)
|
37
37
|
#def serialize_for(value, parent, tag_name=definition.from)
|
38
|
-
node = node_for(parent,
|
38
|
+
node = node_for(parent, as)
|
39
39
|
serialize_node(node, value)
|
40
40
|
end
|
41
41
|
|
@@ -62,12 +62,12 @@ module Representable
|
|
62
62
|
|
63
63
|
private
|
64
64
|
def xpath
|
65
|
-
|
65
|
+
as
|
66
66
|
end
|
67
67
|
|
68
68
|
def find_nodes(doc)
|
69
69
|
selector = xpath
|
70
|
-
selector = "#{
|
70
|
+
selector = "#{self[:wrap]}/#{xpath}" if self[:wrap]
|
71
71
|
nodes = doc.xpath(selector)
|
72
72
|
end
|
73
73
|
|
@@ -143,11 +143,11 @@ module Representable
|
|
143
143
|
# Represents a tag attribute. Currently this only works on the top-level tag.
|
144
144
|
class AttributeBinding < PropertyBinding
|
145
145
|
def read(node)
|
146
|
-
deserialize(node[
|
146
|
+
deserialize(node[as])
|
147
147
|
end
|
148
148
|
|
149
149
|
def serialize_for(value, parent)
|
150
|
-
parent[
|
150
|
+
parent[as] = serialize(value.to_s)
|
151
151
|
end
|
152
152
|
|
153
153
|
def write(parent, value)
|
@@ -9,7 +9,7 @@ module Representable
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def write(map, value)
|
12
|
-
map.children << Psych::Nodes::Scalar.new(
|
12
|
+
map.children << Psych::Nodes::Scalar.new(as)
|
13
13
|
map.children << serialize(value) # FIXME: should be serialize.
|
14
14
|
end
|
15
15
|
|
@@ -36,7 +36,7 @@ module Representable
|
|
36
36
|
class CollectionBinding < PropertyBinding
|
37
37
|
def serialize(value)
|
38
38
|
Psych::Nodes::Sequence.new.tap do |seq|
|
39
|
-
seq.style = Psych::Nodes::Sequence::FLOW if
|
39
|
+
seq.style = Psych::Nodes::Sequence::FLOW if self[:style] == :flow
|
40
40
|
value.each { |obj| seq.children << super(obj) }
|
41
41
|
end
|
42
42
|
end
|
@@ -28,7 +28,7 @@ module Representable::Coercion
|
|
28
28
|
representable_attrs.inheritable_array(:coercer_class).first.attribute(name, options[:type])
|
29
29
|
|
30
30
|
# By using :getter we "pre-occupy" this directive, but we avoid creating accessors, which i find is the cleaner way.
|
31
|
-
options[:
|
31
|
+
options[:exec_context] = :decorator
|
32
32
|
options[:getter] = lambda { |*| coercer.coerce(name, represented.send(name)) }
|
33
33
|
options[:setter] = lambda { |v,*| represented.send("#{name}=", coercer.coerce(name, v)) }
|
34
34
|
|
data/lib/representable/config.rb
CHANGED
@@ -31,13 +31,19 @@ module Representable
|
|
31
31
|
values.each(*args, &block)
|
32
32
|
end
|
33
33
|
|
34
|
-
|
34
|
+
def wrap=(value)
|
35
|
+
value = value.to_s if value.is_a?(Symbol)
|
36
|
+
@wrap = Uber::Options::Value.new(value)
|
37
|
+
end
|
35
38
|
|
36
39
|
# Computes the wrap string or returns false.
|
37
|
-
def wrap_for(name)
|
38
|
-
return unless wrap
|
39
|
-
|
40
|
-
wrap
|
40
|
+
def wrap_for(name, context, *args)
|
41
|
+
return unless @wrap
|
42
|
+
|
43
|
+
value = @wrap.evaluate(context, *args)
|
44
|
+
|
45
|
+
return infer_name_for(name) if value === true
|
46
|
+
value
|
41
47
|
end
|
42
48
|
|
43
49
|
# Write representer configuration into this hash.
|