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