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
@@ -1,16 +1,36 @@
|
|
1
|
+
require 'uber/options'
|
2
|
+
require "representable/parse_strategies"
|
3
|
+
|
1
4
|
module Representable
|
2
5
|
# Created at class compile time. Keeps configuration options for one property.
|
3
|
-
class Definition
|
4
|
-
attr_reader :name
|
6
|
+
class Definition < Hash
|
7
|
+
attr_reader :name
|
5
8
|
alias_method :getter, :name
|
6
9
|
|
7
10
|
def initialize(sym, options={})
|
8
|
-
|
9
|
-
|
11
|
+
super()
|
12
|
+
options = options.clone
|
13
|
+
|
14
|
+
handle_deprecations!(options)
|
15
|
+
|
16
|
+
@name = sym.to_s
|
17
|
+
# defaults:
|
18
|
+
options[:as] ||= @name
|
19
|
+
|
20
|
+
setup!(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
# TODO: test merge!.
|
24
|
+
def merge!(options)
|
25
|
+
setup!(options)
|
26
|
+
self
|
10
27
|
end
|
11
28
|
|
12
|
-
|
13
|
-
|
29
|
+
private :default, :[]=
|
30
|
+
|
31
|
+
def options # TODO: remove in 2.0.
|
32
|
+
warn "Representable::Definition#option is deprecated, use #[] directly."
|
33
|
+
self
|
14
34
|
end
|
15
35
|
|
16
36
|
def setter
|
@@ -18,65 +38,77 @@ module Representable
|
|
18
38
|
end
|
19
39
|
|
20
40
|
def typed?
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
def array?
|
25
|
-
options[:collection]
|
41
|
+
self[:class] or self[:extend] or self[:instance]
|
26
42
|
end
|
27
43
|
|
28
|
-
def
|
29
|
-
|
44
|
+
def representable?
|
45
|
+
return if self[:representable] === false
|
46
|
+
self[:representable] or typed?
|
30
47
|
end
|
31
48
|
|
32
|
-
def
|
33
|
-
|
49
|
+
def array?
|
50
|
+
self[:collection]
|
34
51
|
end
|
35
52
|
|
36
|
-
def
|
37
|
-
|
38
|
-
(options[:from] || options[:as] || name).to_s
|
53
|
+
def hash?
|
54
|
+
self[:hash]
|
39
55
|
end
|
40
56
|
|
41
57
|
def default_for(value)
|
42
|
-
return default if skipable_nil_value?(value)
|
58
|
+
return self[:default] if skipable_nil_value?(value)
|
43
59
|
value
|
44
60
|
end
|
45
61
|
|
46
62
|
def has_default?
|
47
|
-
|
63
|
+
has_key?(:default)
|
48
64
|
end
|
49
65
|
|
50
66
|
def representer_module
|
51
|
-
|
67
|
+
self[:extend]
|
52
68
|
end
|
53
69
|
|
54
|
-
def
|
55
|
-
|
70
|
+
def skipable_nil_value?(value)
|
71
|
+
value.nil? and not self[:render_nil]
|
56
72
|
end
|
57
73
|
|
58
|
-
def
|
59
|
-
|
74
|
+
def create_binding(*args)
|
75
|
+
self[:binding].call(self, *args)
|
60
76
|
end
|
61
77
|
|
62
|
-
|
63
|
-
|
78
|
+
private
|
79
|
+
def setup!(options)
|
80
|
+
handle_extend!(options)
|
81
|
+
handle_as!(options)
|
82
|
+
|
83
|
+
# DISCUSS: we could call more macros here (e.g. for :nested).
|
84
|
+
Representable::ParseStrategy.apply!(options)
|
85
|
+
|
86
|
+
for name, value in options
|
87
|
+
value = Uber::Options::Value.new(value) if dynamic_options.include?(name)
|
88
|
+
self[name] = value
|
89
|
+
end
|
64
90
|
end
|
65
91
|
|
66
|
-
def
|
67
|
-
|
92
|
+
def dynamic_options
|
93
|
+
[:as, :getter, :setter, :class, :instance, :reader, :writer, :extend, :prepare, :if]
|
68
94
|
end
|
69
95
|
|
70
|
-
def
|
71
|
-
options[:
|
96
|
+
def handle_extend!(options)
|
97
|
+
mod = options.delete(:extend) || options.delete(:decorator) and options[:extend] = mod
|
72
98
|
end
|
73
99
|
|
74
|
-
def
|
75
|
-
|
100
|
+
def handle_as!(options)
|
101
|
+
options[:as] = options[:as].to_s if options[:as].is_a?(Symbol) # Allow symbols for as:
|
76
102
|
end
|
77
103
|
|
78
|
-
|
79
|
-
|
104
|
+
# TODO: remove in 2.0.
|
105
|
+
def handle_deprecations!(options)
|
106
|
+
raise "The :from option got replaced by :as in Representable 1.8!" if options[:from]
|
107
|
+
|
108
|
+
if options[:decorator_scope]
|
109
|
+
warn "[Representable] Deprecation: `decorator_scope: true` is deprecated, use `exec_context: :decorator` instead."
|
110
|
+
options.merge!(:exec_context => :decorator)
|
111
|
+
end
|
80
112
|
end
|
81
113
|
end
|
82
114
|
end
|
@@ -1,56 +1,52 @@
|
|
1
1
|
module Representable
|
2
|
-
class CollectionDeserializer
|
2
|
+
class CollectionDeserializer
|
3
3
|
def initialize(binding) # TODO: get rid of binding dependency
|
4
|
-
# next step: use #get always.
|
5
4
|
@binding = binding
|
6
|
-
collection = []
|
7
|
-
# should be call to #default:
|
8
|
-
collection = binding.get if binding.sync?
|
9
|
-
|
10
|
-
super collection
|
11
5
|
end
|
12
6
|
|
13
7
|
def deserialize(fragment)
|
14
8
|
# next step: get rid of collect.
|
15
|
-
fragment.enum_for(:each_with_index).collect
|
16
|
-
@deserializer = ObjectDeserializer.new(@binding
|
9
|
+
fragment.enum_for(:each_with_index).collect do |item_fragment, i|
|
10
|
+
@deserializer = ObjectDeserializer.new(@binding)
|
17
11
|
|
18
|
-
@deserializer.call(item_fragment) # FIXME: what if obj nil?
|
19
|
-
|
12
|
+
@deserializer.call(item_fragment, i) # FIXME: what if obj nil?
|
13
|
+
end
|
20
14
|
end
|
21
15
|
end
|
22
16
|
|
23
17
|
|
24
18
|
class ObjectDeserializer
|
25
|
-
# dependencies: Def#options, Def#create_object
|
26
|
-
def initialize(binding
|
19
|
+
# dependencies: Def#options, Def#create_object
|
20
|
+
def initialize(binding)
|
27
21
|
@binding = binding
|
28
|
-
@object = object
|
29
22
|
end
|
30
23
|
|
31
|
-
def call(fragment)
|
32
|
-
|
33
|
-
return fragment unless @binding.typed?
|
24
|
+
def call(fragment, *args) # FIXME: args is always i.
|
25
|
+
return fragment unless @binding.typed? # customize with :extend. this is not really straight-forward.
|
34
26
|
|
35
|
-
if
|
36
|
-
|
37
|
-
@object = @object.call # call Binding#get or Binding#get[i]
|
38
|
-
else
|
39
|
-
@object = @binding.create_object(fragment)
|
40
|
-
end
|
27
|
+
# what if create_object is responsible for providing the deserialize-to object?
|
28
|
+
object = @binding.create_object(fragment, *args) # customize with :instance and :class.
|
41
29
|
|
42
30
|
# DISCUSS: what parts should be in this class, what in Binding?
|
43
|
-
representable = prepare(
|
44
|
-
|
45
|
-
#
|
31
|
+
representable = prepare(object) # customize with :prepare and :extend.
|
32
|
+
|
33
|
+
deserialize(representable, fragment, @binding.user_options) # deactivate-able via :representable => false.
|
46
34
|
end
|
47
35
|
|
48
36
|
private
|
49
|
-
def deserialize(object, fragment, options)
|
37
|
+
def deserialize(object, fragment, options) # TODO: merge with #serialize.
|
38
|
+
return object unless @binding.representable?
|
39
|
+
|
50
40
|
object.send(@binding.deserialize_method, fragment, options)
|
51
41
|
end
|
52
42
|
|
53
43
|
def prepare(object)
|
44
|
+
@binding.send(:evaluate_option, :prepare, object) do
|
45
|
+
prepare!(object)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def prepare!(object)
|
54
50
|
mod = @binding.representer_module_for(object)
|
55
51
|
|
56
52
|
return object unless mod
|
data/lib/representable/hash.rb
CHANGED
@@ -26,10 +26,11 @@ module Representable
|
|
26
26
|
end
|
27
27
|
|
28
28
|
|
29
|
+
# Note: `#from_hash` still does _not_ stringify incoming hashes. This is per design: Representable is not made for hashes, only,
|
30
|
+
# but for any arbitrary data structure. A generic `key.to_s` with non-hash data would result in weird issues.
|
31
|
+
# I decided it's more predictable to require the user to provide stringified keys.
|
29
32
|
def from_hash(data, options={}, binding_builder=PropertyBinding)
|
30
|
-
|
31
|
-
data = data[wrap.to_s] || {} # DISCUSS: don't initialize this more than once. # TODO: this should be done with #read.
|
32
|
-
end
|
33
|
+
data = filter_wrap(data, options)
|
33
34
|
|
34
35
|
update_properties_from(data, options, binding_builder)
|
35
36
|
end
|
@@ -37,9 +38,19 @@ module Representable
|
|
37
38
|
def to_hash(options={}, binding_builder=PropertyBinding)
|
38
39
|
hash = create_representation_with({}, options, binding_builder)
|
39
40
|
|
40
|
-
return hash unless wrap = options[:wrap] || representation_wrap
|
41
|
+
return hash unless wrap = options[:wrap] || representation_wrap(options)
|
41
42
|
|
42
43
|
{wrap => hash}
|
43
44
|
end
|
45
|
+
|
46
|
+
private
|
47
|
+
def filter_wrap(data, options)
|
48
|
+
return data unless wrap = options[:wrap] || representation_wrap(options)
|
49
|
+
filter_wrap_for(data, wrap)
|
50
|
+
end
|
51
|
+
|
52
|
+
def filter_wrap_for(data, wrap)
|
53
|
+
data[wrap.to_s] || {} # DISCUSS: don't initialize this more than once. # TODO: this should be done with #read.
|
54
|
+
end
|
44
55
|
end
|
45
56
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Representable
|
2
|
+
module Hash
|
3
|
+
module AllowSymbols
|
4
|
+
private
|
5
|
+
def filter_wrap_for(data, *args)
|
6
|
+
super(Conversion.stringify_keys(data), *args)
|
7
|
+
end
|
8
|
+
|
9
|
+
def update_properties_from(data, *args)
|
10
|
+
super(Conversion.stringify_keys(data), *args)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Conversion
|
15
|
+
# DISCUSS: we could think about mixin in IndifferentAccess here (either hashie or ActiveSupport).
|
16
|
+
# or decorating the hash.
|
17
|
+
def self.stringify_keys(hash)
|
18
|
+
hash = hash.dup
|
19
|
+
|
20
|
+
hash.keys.each do |k|
|
21
|
+
hash[k.to_s] = hash.delete(k)
|
22
|
+
end
|
23
|
+
hash
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/representable/json.rb
CHANGED
data/lib/representable/mapper.rb
CHANGED
@@ -1,12 +1,11 @@
|
|
1
|
-
require 'representable/
|
1
|
+
require 'representable/readable_writeable'
|
2
2
|
|
3
3
|
module Representable
|
4
4
|
# Render and parse by looping over the representer's properties and dispatching to bindings.
|
5
5
|
# Conditionals are handled here, too.
|
6
6
|
class Mapper
|
7
|
-
# DISCUSS: we need @represented here for evaluating the :if blocks. this could be done in the bindings_for asset.
|
8
7
|
module Methods
|
9
|
-
def initialize(bindings, represented, options)
|
8
|
+
def initialize(bindings, represented, options) # TODO: get rid of represented dependency.
|
10
9
|
@represented = represented # the (extended) model.
|
11
10
|
@bindings = bindings
|
12
11
|
end
|
@@ -17,7 +16,7 @@ module Representable
|
|
17
16
|
bindings.each do |bin|
|
18
17
|
deserialize_property(bin, doc, options)
|
19
18
|
end
|
20
|
-
represented
|
19
|
+
@represented
|
21
20
|
end
|
22
21
|
|
23
22
|
def serialize(doc, options)
|
@@ -28,8 +27,6 @@ module Representable
|
|
28
27
|
end
|
29
28
|
|
30
29
|
private
|
31
|
-
attr_reader :represented
|
32
|
-
|
33
30
|
def serialize_property(binding, doc, options)
|
34
31
|
return if skip_property?(binding, options)
|
35
32
|
compile_fragment(binding, doc)
|
@@ -54,13 +51,9 @@ module Representable
|
|
54
51
|
end
|
55
52
|
|
56
53
|
def skip_conditional_property?(binding)
|
57
|
-
|
58
|
-
return unless condition = binding.options[:if]
|
59
|
-
|
60
|
-
args = []
|
61
|
-
args << binding.user_options if condition.arity > 0 # TODO: remove arity check. users should know whether they pass options or not.
|
54
|
+
return unless condition = binding[:if]
|
62
55
|
|
63
|
-
not
|
56
|
+
not binding.send(:evaluate_option, :if)
|
64
57
|
end
|
65
58
|
|
66
59
|
def compile_fragment(bin, doc)
|
@@ -73,6 +66,6 @@ module Representable
|
|
73
66
|
end
|
74
67
|
|
75
68
|
include Methods
|
76
|
-
include
|
69
|
+
include ReadableWriteable # DISCUSS: make this pluggable.
|
77
70
|
end
|
78
71
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Representable
|
2
|
+
# Parse strategies are just a combination of representable's options. They save you from memoizing the
|
3
|
+
# necessary parameters.
|
4
|
+
#
|
5
|
+
# Feel free to contribute your strategy if you think it's worth sharing!
|
6
|
+
class ParseStrategy
|
7
|
+
def self.apply!(options)
|
8
|
+
return unless strategy = options[:parse_strategy]
|
9
|
+
|
10
|
+
strategy = :proc if strategy.is_a?(::Proc)
|
11
|
+
|
12
|
+
parse_strategies[strategy].apply!(name, options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.parse_strategies
|
16
|
+
{
|
17
|
+
:sync => Sync,
|
18
|
+
:find_or_instantiate => FindOrInstantiate,
|
19
|
+
:proc => Proc
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
class Proc
|
25
|
+
def self.apply!(name, options)
|
26
|
+
options[:setter] = lambda { |*| }
|
27
|
+
options[:pass_options] = true
|
28
|
+
options[:instance] = options[:parse_strategy]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
class Sync
|
34
|
+
def self.apply!(name, options)
|
35
|
+
options[:setter] = lambda { |*| }
|
36
|
+
options[:pass_options] = true
|
37
|
+
options[:instance] = options[:collection] ?
|
38
|
+
lambda { |fragment, i, options| options.binding.get[i] } :
|
39
|
+
lambda { |fragment, options| options.binding.get }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# replaces current collection.
|
45
|
+
class FindOrInstantiate
|
46
|
+
def self.apply!(name, options)
|
47
|
+
options[:pass_options] = true
|
48
|
+
options[:instance] = lambda { |fragment, *args|
|
49
|
+
args = args.last # TODO: don't pass i as separate block parameter but in Options.
|
50
|
+
object_class = args.binding[:class].evaluate(self, fragment, args)
|
51
|
+
|
52
|
+
object_class.find(fragment["id"]) or object_class.new
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Representable
|
2
|
+
module ReadableWriteable
|
3
|
+
def deserialize_property(binding, doc, options)
|
4
|
+
return unless binding.writeable?
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
def serialize_property(binding, doc, options)
|
9
|
+
return unless binding.readable?
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# TODO: i hate monkey-patching Definition here since it globally adds this options. However, for now this should be ok :-)
|
16
|
+
Definition.class_eval do
|
17
|
+
# Checks and returns if the property is writeable
|
18
|
+
def writeable?
|
19
|
+
return self[:writeable] if self.has_key?(:writeable)
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
# Checks and returns if the property is readable
|
24
|
+
def readable?
|
25
|
+
return self[:readable] if self.has_key?(:readable)
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|