representable 1.5.3 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/{CHANGES.textile → CHANGES.md} +17 -0
- data/Gemfile +1 -1
- data/README.md +24 -3
- data/TODO +11 -3
- data/lib/representable.rb +37 -117
- data/lib/representable/coercion.rb +29 -4
- data/lib/representable/config.rb +52 -0
- data/lib/representable/decorator.rb +8 -7
- data/lib/representable/decorator/coercion.rb +2 -57
- data/lib/representable/definition.rb +1 -0
- data/lib/representable/feature/readable_writeable.rb +1 -1
- data/lib/representable/hash.rb +5 -0
- data/lib/representable/hash/collection.rb +38 -0
- data/lib/representable/hash_methods.rb +2 -2
- data/lib/representable/json.rb +5 -0
- data/lib/representable/json/collection.rb +3 -28
- data/lib/representable/mapper.rb +78 -0
- data/lib/representable/version.rb +1 -1
- data/lib/representable/xml.rb +15 -10
- data/lib/representable/xml/collection.rb +7 -25
- data/lib/representable/yaml.rb +16 -11
- data/test/coercion_test.rb +95 -66
- data/test/decorator_test.rb +10 -0
- data/test/inheritance_test.rb +21 -0
- data/test/json_test.rb +7 -7
- data/test/representable_test.rb +146 -26
- data/test/test_helper.rb +1 -0
- data/test/yaml_test.rb +1 -2
- metadata +26 -44
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f465aa1b0aa43a6e5d2bacba9a09352679d2e9c0
|
4
|
+
data.tar.gz: 63ad7fd426a19a8efdc2ee5b06590a29507011fd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3d7a5b5b2f92d3fba72b01acace5bf14e40c80f8c55e82da3429b8870a67203eb681d2d777598848eec4d0515c9133a235ce5de36ea8365ecdc0d1d7597d7031
|
7
|
+
data.tar.gz: 0a76832f4640f6287abee2668ab58a867b65e573adfbbf71b1be44208215d068f5953dd88a7468b822208a20f3b78050f1f2f64ffdda87dc86b9bb9501386bfd
|
@@ -1,3 +1,20 @@
|
|
1
|
+
h2. 1.6.0
|
2
|
+
|
3
|
+
* You can define inline representers now if you don't wanna use multiple modules and files.
|
4
|
+
<!-- here comes some code -->
|
5
|
+
```ruby
|
6
|
+
property :song, class: Song do
|
7
|
+
property :title
|
8
|
+
end
|
9
|
+
```
|
10
|
+
|
11
|
+
This supersedes the use for `:extend` or `:decorator`, which still works, of course.
|
12
|
+
|
13
|
+
* Coercion now happens in a dedicated coercion object. This means that in your models virtus no longer creates accessors for coerced properties and thus values get coerced when rendering or parsing a document, only. If you want the old behavior, include `Virtus` into your model class and do the coercion yourself.
|
14
|
+
* `Decorator::Coercion` is deprecated, just use `include Representable::Coercion`.
|
15
|
+
* Introducing `Mapper` which does most of the rendering/parsing process. Be careful, if you used to override private methods like `#compile_fragment` this no longer works, you have to override that in `Mapper` now.
|
16
|
+
* Fixed a bug where inheriting from Decorator wouldn't inherit properties correctly.
|
17
|
+
|
1
18
|
h2. 1.5.3
|
2
19
|
|
3
20
|
* `Representable#update_properties_from` now always returns `represented`, which is `self` in a module representer and the decorated object in a decorator (only the latter changed).
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -174,6 +174,25 @@ Album.new.extend(AlbumRepresenter).
|
|
174
174
|
#=> #<Album name="Offspring", songs=[#<Song title="Genocide">, #<Song title="Nitro", composers=["Offspring"]>]>
|
175
175
|
```
|
176
176
|
|
177
|
+
## Inline Representers
|
178
|
+
|
179
|
+
If you don't want to maintain two separate modules when nesting representations you can define the `SongRepresenter` inline.
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
module AlbumRepresenter
|
183
|
+
include Representable::JSON
|
184
|
+
|
185
|
+
property :name
|
186
|
+
|
187
|
+
collection :songs, class: Song do
|
188
|
+
property :title
|
189
|
+
property :track
|
190
|
+
collection :composers
|
191
|
+
end
|
192
|
+
```
|
193
|
+
|
194
|
+
This works both for representer modules and decorators.
|
195
|
+
|
177
196
|
## Decorator vs. Extend
|
178
197
|
|
179
198
|
People who dislike `:extend` go use the `Decorator` strategy!
|
@@ -576,7 +595,7 @@ class Song < OpenStruct
|
|
576
595
|
end
|
577
596
|
```
|
578
597
|
|
579
|
-
I do not recommend this approach as it bloats your domain classes with representation logic that is barely needed elsewhere.
|
598
|
+
I do not recommend this approach as it bloats your domain classes with representation logic that is barely needed elsewhere. Use [decorators](#decorator-vs-extend) instead.
|
580
599
|
|
581
600
|
|
582
601
|
## More Options
|
@@ -679,17 +698,19 @@ module SongRepresenter
|
|
679
698
|
end
|
680
699
|
```
|
681
700
|
|
682
|
-
|
701
|
+
In a decorator it works alike.
|
683
702
|
|
684
703
|
```ruby
|
685
704
|
module SongRepresenter < Representable::Decorator
|
686
705
|
include Representable::JSON
|
687
|
-
include Representable::
|
706
|
+
include Representable::Coercion
|
688
707
|
|
689
708
|
property :recorded_at, :type => DateTime
|
690
709
|
end
|
691
710
|
```
|
692
711
|
|
712
|
+
Coercing values only happens when rendering or parsing a document. Representable does not create accessors in your model as `virtus` does.
|
713
|
+
|
693
714
|
## Undocumented Features
|
694
715
|
|
695
716
|
(Please don't read this section!)
|
data/TODO
CHANGED
@@ -6,7 +6,6 @@ class: |key, hsh|
|
|
6
6
|
|
7
7
|
document `XML::AttributeHash` etc
|
8
8
|
|
9
|
-
* deprecate :from, make :a(lia)s authorative.
|
10
9
|
* cleanup ReadableWriteable
|
11
10
|
* deprecate Representable::*::ClassMethods (::from_hash and friends)
|
12
11
|
|
@@ -14,7 +13,7 @@ document `XML::AttributeHash` etc
|
|
14
13
|
|
15
14
|
* have representable-options (:include, :exclude) and user-options
|
16
15
|
|
17
|
-
* make all properties "Object-like", even arrays of strings etc.
|
16
|
+
* make all properties "Object-like", even arrays of strings etc. This saves us from having `extend ObjectBinding if typed?` and we could just call to_hash/from_hash on all attributes. performance issues here? otherwise: implement!
|
18
17
|
|
19
18
|
|
20
19
|
def compile_fragment(doc)
|
@@ -27,4 +26,13 @@ module ReaderWriter
|
|
27
26
|
|
28
27
|
* make lambda options optional (arity == 0)
|
29
28
|
|
30
|
-
* pass args to methods when arity matches
|
29
|
+
* pass args to methods when arity matches
|
30
|
+
|
31
|
+
* DISCUSS if Decorator.new.representable_attrs != Decorator.representable_attrs ? (what about performance?)
|
32
|
+
* REMOVE :from, make :a(lia)s authorative.
|
33
|
+
|
34
|
+
* does :instance not work with :decorator ?
|
35
|
+
* make it easy to override Binding#options via #to_hash(whatever: {hit: {decorator: HitDecorator}})
|
36
|
+
|
37
|
+
* DISCUSS: should inline representers be created at runtime, so we don't need ::representer_engine?
|
38
|
+
* deprecate `Decorator::Coercion`.
|
data/lib/representable.rb
CHANGED
@@ -1,31 +1,8 @@
|
|
1
1
|
require 'representable/deprecations'
|
2
2
|
require 'representable/definition'
|
3
|
-
require 'representable/
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
# == On class level
|
8
|
-
#
|
9
|
-
# To try out Representable you might include the format module into the represented class directly and then
|
10
|
-
# define the properties.
|
11
|
-
#
|
12
|
-
# class Hero < ActiveRecord::Base
|
13
|
-
# include Representable::JSON
|
14
|
-
# property :name
|
15
|
-
#
|
16
|
-
# This will give you to_/from_json for each instance. However, this approach limits your class to one representation.
|
17
|
-
#
|
18
|
-
# == On module level
|
19
|
-
#
|
20
|
-
# Modules give you much more flexibility since you can mix them into objects at runtime, following the DCI
|
21
|
-
# pattern.
|
22
|
-
#
|
23
|
-
# module HeroRepresenter
|
24
|
-
# include Representable::JSON
|
25
|
-
# property :name
|
26
|
-
# end
|
27
|
-
#
|
28
|
-
# hero.extend(HeroRepresenter).to_json
|
3
|
+
require 'representable/mapper'
|
4
|
+
require 'representable/config'
|
5
|
+
|
29
6
|
module Representable
|
30
7
|
attr_writer :representable_attrs
|
31
8
|
|
@@ -36,103 +13,64 @@ module Representable
|
|
36
13
|
extend ClassMethods::Declarations
|
37
14
|
|
38
15
|
include Deprecations
|
39
|
-
include Feature::ReadableWriteable
|
40
16
|
end
|
41
17
|
end
|
42
18
|
|
43
19
|
# Reads values from +doc+ and sets properties accordingly.
|
44
20
|
def update_properties_from(doc, options, format)
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
represented
|
21
|
+
# deserialize_for(bindings, mapper ? , options)
|
22
|
+
representable_mapper(format, options).deserialize(doc, options)
|
49
23
|
end
|
50
24
|
|
51
25
|
private
|
52
26
|
# Compiles the document going through all properties.
|
53
27
|
def create_representation_with(doc, options, format)
|
54
|
-
|
55
|
-
serialize_property(bin, doc, options)
|
56
|
-
end
|
57
|
-
doc
|
28
|
+
representable_mapper(format, options).serialize(doc, options)
|
58
29
|
end
|
59
30
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
end
|
64
|
-
|
65
|
-
def deserialize_property(binding, doc, options)
|
66
|
-
return if skip_property?(binding, options)
|
67
|
-
uncompile_fragment(binding, doc)
|
68
|
-
end
|
69
|
-
|
70
|
-
# Checks and returns if the property should be included.
|
71
|
-
def skip_property?(binding, options)
|
72
|
-
return true if skip_excluded_property?(binding, options) # no need for further evaluation when :exclude'ed
|
73
|
-
|
74
|
-
skip_conditional_property?(binding)
|
75
|
-
end
|
76
|
-
|
77
|
-
def skip_excluded_property?(binding, options)
|
78
|
-
return unless props = options[:exclude] || options[:include]
|
79
|
-
res = props.include?(binding.name.to_sym)
|
80
|
-
options[:include] ? !res : res
|
31
|
+
def representable_bindings_for(format, options)
|
32
|
+
options = cleanup_options(options) # FIXME: make representable-options and user-options two different hashes.
|
33
|
+
representable_attrs.collect {|attr| representable_binding_for(attr, format, options) }
|
81
34
|
end
|
82
35
|
|
83
|
-
def
|
84
|
-
#
|
85
|
-
return unless condition = binding.options[:if]
|
86
|
-
|
87
|
-
args = []
|
88
|
-
args << binding.user_options if condition.arity > 0 # TODO: remove arity check. users should know whether they pass options or not.
|
89
|
-
|
90
|
-
not represented.instance_exec(*args, &condition)
|
91
|
-
end
|
36
|
+
def representable_binding_for(attribute, format, options)
|
37
|
+
context = attribute.options[:decorator_scope] ? self : represented # DISCUSS: pass both represented and representer into Binding and do it there?
|
92
38
|
|
93
|
-
|
94
|
-
def compile_fragment(bin, doc)
|
95
|
-
bin.compile_fragment(doc)
|
39
|
+
format.build(attribute, represented, options, context)
|
96
40
|
end
|
97
41
|
|
98
|
-
# TODO: remove in
|
99
|
-
|
100
|
-
bin.uncompile_fragment(doc)
|
42
|
+
def cleanup_options(options) # TODO: remove me. this clearly belongs in Representable.
|
43
|
+
options.reject { |k,v| [:include, :exclude].include?(k) }
|
101
44
|
end
|
102
45
|
|
103
46
|
def representable_attrs
|
104
47
|
@representable_attrs ||= self.class.representable_attrs # DISCUSS: copy, or better not?
|
105
48
|
end
|
106
49
|
|
107
|
-
|
108
|
-
|
109
|
-
|
50
|
+
def representable_mapper(format, options)
|
51
|
+
bindings = representable_bindings_for(format, options)
|
52
|
+
Mapper.new(bindings, represented, options) # TODO: remove self, or do we need it? and also represented!
|
110
53
|
end
|
111
54
|
|
112
|
-
private
|
113
|
-
def representable_bindings_for(format, options)
|
114
|
-
options = cleanup_options(options) # FIXME: make representable-options and user-options two different hashes.
|
115
|
-
representable_attrs.map {|attr| representable_binding_for(attr, format, options) }
|
116
|
-
end
|
117
55
|
|
118
|
-
def
|
119
|
-
#
|
120
|
-
format.build(attribute, represented, options)
|
56
|
+
def representation_wrap
|
57
|
+
representable_attrs.wrap_for(self.class.name) # FIXME: where is this needed?
|
121
58
|
end
|
122
59
|
|
123
60
|
def represented
|
124
61
|
self
|
125
62
|
end
|
126
63
|
|
127
|
-
def cleanup_options(options) # TODO: remove me.
|
128
|
-
options.reject { |k,v| [:include, :exclude].include?(k) }
|
129
|
-
end
|
130
|
-
|
131
64
|
module ClassInclusions
|
132
65
|
def included(base)
|
133
66
|
super
|
134
67
|
base.representable_attrs.inherit(representable_attrs)
|
135
68
|
end
|
69
|
+
|
70
|
+
def inherited(base) # DISCUSS: this could be in Decorator? but then we couldn't do B < A(include X) for non-decorators, right?
|
71
|
+
super
|
72
|
+
base.representable_attrs.inherit(representable_attrs)
|
73
|
+
end
|
136
74
|
end
|
137
75
|
|
138
76
|
module ModuleExtensions
|
@@ -177,8 +115,12 @@ private
|
|
177
115
|
# property :name, :render_nil => true
|
178
116
|
# property :name, :readable => false
|
179
117
|
# property :name, :writeable => false
|
180
|
-
def property(name, options={})
|
181
|
-
|
118
|
+
def property(name, options={}, &block)
|
119
|
+
if block_given? # DISCUSS: separate module?
|
120
|
+
options[:extend] = inline_representer(representer_engine, &block)
|
121
|
+
end
|
122
|
+
|
123
|
+
(representable_attrs << definition_class.new(name, options)).last
|
182
124
|
end
|
183
125
|
|
184
126
|
# Declares a represented document node collection.
|
@@ -208,37 +150,15 @@ private
|
|
208
150
|
def build_config
|
209
151
|
Config.new
|
210
152
|
end
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
|
215
|
-
# NOTE: the API of Config is subject to change so don't rely too much on this private object.
|
216
|
-
class Config < Array
|
217
|
-
attr_accessor :wrap
|
218
|
-
|
219
|
-
# Computes the wrap string or returns false.
|
220
|
-
def wrap_for(name)
|
221
|
-
return unless wrap
|
222
|
-
return infer_name_for(name) if wrap === true
|
223
|
-
wrap
|
224
|
-
end
|
225
153
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
end
|
233
|
-
|
234
|
-
private
|
235
|
-
def infer_name_for(name)
|
236
|
-
name.to_s.split('::').last.
|
237
|
-
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
238
|
-
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
239
|
-
downcase
|
154
|
+
def inline_representer(base_module, &block) # DISCUSS: separate module?
|
155
|
+
Module.new do
|
156
|
+
include base_module
|
157
|
+
instance_exec &block
|
158
|
+
end
|
159
|
+
end
|
240
160
|
end
|
241
161
|
end
|
242
162
|
end
|
243
163
|
|
244
|
-
require 'representable/decorator'
|
164
|
+
require 'representable/decorator'
|
@@ -1,17 +1,42 @@
|
|
1
1
|
require "virtus"
|
2
2
|
|
3
3
|
module Representable::Coercion
|
4
|
+
class Coercer
|
5
|
+
include Virtus
|
6
|
+
|
7
|
+
def coerce(name, v) # TODO: test me.
|
8
|
+
# set and get the value as i don't know where exactly coercion happens in virtus.
|
9
|
+
send("#{name}=", v)
|
10
|
+
send(name)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
# separate coercion object doesn't give us initializer and accessors in the represented object (with module representer)!
|
14
|
+
|
4
15
|
def self.included(base)
|
5
16
|
base.class_eval do
|
6
|
-
include Virtus
|
7
17
|
extend ClassMethods
|
18
|
+
# FIXME: use inheritable_attr when it's ready.
|
19
|
+
representable_attrs.inheritable_array(:coercer_class) << Class.new(Coercer) unless representable_attrs.inheritable_array(:coercer_class).first
|
8
20
|
end
|
21
|
+
|
9
22
|
end
|
10
23
|
|
11
24
|
module ClassMethods
|
12
|
-
def property(name,
|
13
|
-
|
14
|
-
|
25
|
+
def property(name, options={})
|
26
|
+
return super unless options[:type]
|
27
|
+
|
28
|
+
representable_attrs.inheritable_array(:coercer_class).first.attribute(name, options[:type])
|
29
|
+
|
30
|
+
# By using :getter we "pre-occupy" this directive, but we avoid creating accessors, which i find is the cleaner way.
|
31
|
+
options[:decorator_scope] = true
|
32
|
+
options[:getter] = lambda { |*| coercer.coerce(name, represented.send(name)) }
|
33
|
+
options[:setter] = lambda { |v,*| represented.send("#{name}=", coercer.coerce(name, v)) }
|
34
|
+
|
35
|
+
super
|
15
36
|
end
|
16
37
|
end
|
38
|
+
|
39
|
+
def coercer
|
40
|
+
@coercer ||= representable_attrs.inheritable_array(:coercer_class).first.new
|
41
|
+
end
|
17
42
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Representable
|
2
|
+
# NOTE: the API of Config is subject to change so don't rely too much on this private object.
|
3
|
+
class Config < Array
|
4
|
+
# DISCUSS: experimental. this will soon be moved to a separate gem
|
5
|
+
module InheritableArray
|
6
|
+
def inheritable_array(name)
|
7
|
+
inheritable_arrays[name] ||= []
|
8
|
+
end
|
9
|
+
def inheritable_arrays
|
10
|
+
@inheritable_arrays ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def inherit(parent)
|
14
|
+
super
|
15
|
+
|
16
|
+
parent.inheritable_arrays.keys.each do |k|
|
17
|
+
inheritable_array(k).push *parent.inheritable_array(k).clone
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
attr_accessor :wrap
|
24
|
+
|
25
|
+
# Computes the wrap string or returns false.
|
26
|
+
def wrap_for(name)
|
27
|
+
return unless wrap
|
28
|
+
return infer_name_for(name) if wrap === true
|
29
|
+
wrap
|
30
|
+
end
|
31
|
+
|
32
|
+
module InheritMethods
|
33
|
+
def clone
|
34
|
+
self.class.new(collect { |d| d.clone })
|
35
|
+
end
|
36
|
+
|
37
|
+
def inherit(parent)
|
38
|
+
push(*parent.clone)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
include InheritMethods
|
42
|
+
include InheritableArray # overrides #inherit.
|
43
|
+
|
44
|
+
private
|
45
|
+
def infer_name_for(name)
|
46
|
+
name.to_s.split('::').last.
|
47
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
48
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
49
|
+
downcase
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -4,7 +4,14 @@ module Representable
|
|
4
4
|
alias_method :decorated, :represented
|
5
5
|
|
6
6
|
def self.prepare(represented)
|
7
|
-
new(represented)
|
7
|
+
new(represented)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.inline_representer(base_module, &block) # DISCUSS: separate module?
|
11
|
+
Class.new(self) do
|
12
|
+
include base_module
|
13
|
+
instance_exec &block
|
14
|
+
end
|
8
15
|
end
|
9
16
|
|
10
17
|
include Representable # include after class methods so Decorator::prepare can't be overwritten by Representable::prepare.
|
@@ -12,11 +19,5 @@ module Representable
|
|
12
19
|
def initialize(represented)
|
13
20
|
@represented = represented
|
14
21
|
end
|
15
|
-
|
16
|
-
def representable_binding_for(attr, format, options)
|
17
|
-
context = attr.options[:decorator_scope] ? self : represented # DISCUSS: should Decorator know this kinda stuff?
|
18
|
-
|
19
|
-
format.build(attr, represented, options, context)
|
20
|
-
end
|
21
22
|
end
|
22
23
|
end
|