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