representable 1.5.3 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -3,4 +3,4 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  #gem "virtus", :path => "../virtus"
6
- gem 'rake', "10.1.0.beta3"
6
+ gem 'rake', "10.1.0"
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
- When using a decorator representer, use the `Representable::Decorator::Coercion` module.
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::Decorator::Coercion
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`.
@@ -1,31 +1,8 @@
1
1
  require 'representable/deprecations'
2
2
  require 'representable/definition'
3
- require 'representable/feature/readable_writeable'
4
-
5
- # Representable can be used in two ways.
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
- representable_bindings_for(format, options).each do |bin|
46
- deserialize_property(bin, doc, options)
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
- representable_bindings_for(format, options).each do |bin|
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 serialize_property(binding, doc, options)
61
- return if skip_property?(binding, options)
62
- compile_fragment(binding, doc)
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 skip_conditional_property?(binding)
84
- # TODO: move to Binding.
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
- # TODO: remove in 1.4.
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 1.4.
99
- def uncompile_fragment(bin, doc)
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
- # Returns the wrapper for the representation. Mostly used in XML.
108
- def representation_wrap
109
- representable_attrs.wrap_for(self.class.name)
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 representable_binding_for(attribute, format, options)
119
- # DISCUSS: shouldn't this happen in Binding?
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
- representable_attrs << definition_class.new(name, options)
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
- def clone
227
- self.class.new(collect { |d| d.clone })
228
- end
229
-
230
- def inherit(parent)
231
- push(*parent.clone)
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, args={})
13
- attribute(name, args[:type]) if args[:type] # FIXME (in virtus): undefined method `superclass' for VirtusCoercionTest::SongRepresenter:Module
14
- super(name, args)
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) # was: PrepareStrategy::Decorate.
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