representable 1.7.7 → 1.8.0
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/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.
|