representable 1.5.3 → 1.6.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 +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
|