representable 1.5.1 → 1.5.2
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.
- data/CHANGES.textile +7 -0
- data/README.md +25 -13
- data/TODO +1 -2
- data/lib/representable/binding.rb +11 -9
- data/lib/representable/coercion.rb +1 -1
- data/lib/representable/decorator.rb +1 -1
- data/lib/representable/decorator/coercion.rb +45 -0
- data/lib/representable/json.rb +7 -7
- data/lib/representable/version.rb +1 -1
- data/representable.gemspec +1 -0
- data/test/coercion_test.rb +26 -8
- data/test/representable_test.rb +23 -5
- metadata +19 -3
- data/test/Gemfile +0 -3
data/CHANGES.textile
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
h2. 1.5.2
|
2
|
+
|
3
|
+
* Rename `:representer_exec` to `:decorator_scope` and make it a documented (!) feature.
|
4
|
+
* Accessors for properties defined with `decorator_scope: true` will now be invoked on the decorator, not on the represented instance anymore. This allows having decorators with helper methods.
|
5
|
+
* Use `MultiJson` instead of `JSON` when parsing and rendering.
|
6
|
+
* Make `Representable::Decorator::Coercion` work.
|
7
|
+
|
1
8
|
h2. 1.5.1
|
2
9
|
|
3
10
|
* Make lonely collections and hashes work with decorators.
|
data/README.md
CHANGED
@@ -218,9 +218,21 @@ module SongRepresenter
|
|
218
218
|
end
|
219
219
|
```
|
220
220
|
|
221
|
-
That works as the method is mixed into the represented object.
|
221
|
+
That works as the method is mixed into the represented object. When adding a helper method to a decorator, representable will still invoke accessors on the represented instance - unless you tell it the scope.
|
222
222
|
|
223
|
-
|
223
|
+
```ruby
|
224
|
+
class SongRepresenter < Representable::Decorator
|
225
|
+
property :title, decorator_scope: true
|
226
|
+
|
227
|
+
def title
|
228
|
+
represented.name
|
229
|
+
end
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
This will call `title` getter and setter on the decorator instance, not on the represented object. You can still access the represented object in the decorator method using `represented`. BTW, in a module representer this option is ignored.
|
234
|
+
|
235
|
+
Or use `:getter` or `:setter` to dynamically add a method for the represented object.
|
224
236
|
|
225
237
|
```ruby
|
226
238
|
class SongRepresenter < Representable::Decorator
|
@@ -667,6 +679,17 @@ module SongRepresenter
|
|
667
679
|
end
|
668
680
|
```
|
669
681
|
|
682
|
+
When using a decorator representer, use the `Representable::Decorator::Coercion` module.
|
683
|
+
|
684
|
+
```ruby
|
685
|
+
module SongRepresenter < Representable::Decorator
|
686
|
+
include Representable::JSON
|
687
|
+
include Representable::Decorator::Coercion
|
688
|
+
|
689
|
+
property :recorded_at, :type => DateTime
|
690
|
+
end
|
691
|
+
```
|
692
|
+
|
670
693
|
## Undocumented Features
|
671
694
|
|
672
695
|
(Please don't read this section!)
|
@@ -678,17 +701,6 @@ end
|
|
678
701
|
property :title, :binding => lambda { |*args| JSON::TitleBinding.new(*args) }
|
679
702
|
```
|
680
703
|
|
681
|
-
* Lambdas are usually executed in the represented object's context. If your writing a `Decorator` representer and you need to execute lambdas in its context use the `:representer_exec` option.
|
682
|
-
|
683
|
-
<!-- and some more in a beautiful cuddle -->
|
684
|
-
```ruby
|
685
|
-
class SongRepresenter < Representable::Decorator
|
686
|
-
property :title, :representer_exec => true, :getter => lambda {..}
|
687
|
-
end
|
688
|
-
```
|
689
|
-
|
690
|
-
You can still access the represented object in the lambda using `represented`. In a module representer this option is ignored.
|
691
|
-
|
692
704
|
## Copyright
|
693
705
|
|
694
706
|
Representable started as a heavily simplified fork of the ROXML gem. Big thanks to Ben Woosley for his inspiring work.
|
data/TODO
CHANGED
@@ -12,14 +12,14 @@ module Representable
|
|
12
12
|
build_for(definition, *args)
|
13
13
|
end
|
14
14
|
|
15
|
-
def initialize(definition, represented, user_options={},
|
15
|
+
def initialize(definition, represented, user_options={}, exec_context=represented) # TODO: remove default arg for user options. # DISCUSS: make exec_context an options hash?
|
16
16
|
super(definition)
|
17
|
-
@represented
|
18
|
-
@user_options
|
19
|
-
@
|
17
|
+
@represented = represented
|
18
|
+
@user_options = user_options
|
19
|
+
@exec_context = exec_context
|
20
20
|
end
|
21
21
|
|
22
|
-
attr_reader :user_options, :represented
|
22
|
+
attr_reader :user_options, :represented # TODO: make private/remove.
|
23
23
|
|
24
24
|
# Main entry point for rendering/parsing a property object.
|
25
25
|
def serialize(value)
|
@@ -74,17 +74,19 @@ module Representable
|
|
74
74
|
|
75
75
|
def get
|
76
76
|
represented_exec_for(:getter) do
|
77
|
-
|
77
|
+
exec_context.send(getter)
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
81
|
def set(value)
|
82
82
|
represented_exec_for(:setter, value) do
|
83
|
-
|
83
|
+
exec_context.send(setter, value)
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
87
87
|
private
|
88
|
+
attr_reader :exec_context
|
89
|
+
|
88
90
|
# Execute the block for +option_name+ on the represented object.
|
89
91
|
# Executes passed block when there's no lambda for option.
|
90
92
|
def represented_exec_for(option_name, *args)
|
@@ -92,12 +94,12 @@ module Representable
|
|
92
94
|
call_proc_for(options[option_name], *args)
|
93
95
|
end
|
94
96
|
|
95
|
-
# All lambdas are executed on
|
97
|
+
# All lambdas are executed on exec_context which is either represented or the decorator instance.
|
96
98
|
def call_proc_for(proc, *args)
|
97
99
|
return proc unless proc.is_a?(Proc)
|
98
100
|
# TODO: call method when proc is sympbol.
|
99
101
|
args << user_options # DISCUSS: we assume user_options is a Hash!
|
100
|
-
|
102
|
+
exec_context.instance_exec(*args, &proc)
|
101
103
|
end
|
102
104
|
|
103
105
|
|
@@ -7,7 +7,7 @@ module Representable::Coercion
|
|
7
7
|
extend ClassMethods
|
8
8
|
end
|
9
9
|
end
|
10
|
-
|
10
|
+
|
11
11
|
module ClassMethods
|
12
12
|
def property(name, args={})
|
13
13
|
attribute(name, args[:type]) if args[:type] # FIXME (in virtus): undefined method `superclass' for VirtusCoercionTest::SongRepresenter:Module
|
@@ -14,7 +14,7 @@ module Representable
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def representable_binding_for(attr, format, options)
|
17
|
-
context = attr.options[:
|
17
|
+
context = attr.options[:decorator_scope] ? self : represented # DISCUSS: should Decorator know this kinda stuff?
|
18
18
|
|
19
19
|
format.build(attr, represented, options, context)
|
20
20
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class Representable::Decorator
|
2
|
+
module Coercion
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
include Virtus
|
6
|
+
extend Representable::Coercion::ClassMethods
|
7
|
+
extend ClassMethods
|
8
|
+
|
9
|
+
def initialize(represented) # override Virtus' #initialize.
|
10
|
+
@represented = represented
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def property(name, options={})
|
17
|
+
if options[:type]
|
18
|
+
options[:decorator_scope] = true # call setter on decorator so coercion kicks in.
|
19
|
+
create_writer(name)
|
20
|
+
create_reader(name)
|
21
|
+
end
|
22
|
+
|
23
|
+
super # Representable::Coercion.
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
# FIXME: dear @solnic, please make this better!
|
28
|
+
def create_writer(name)
|
29
|
+
# the call to super makes the actual coercion, which is then delegated to the represented instance.
|
30
|
+
define_method "#{name}=" do |v|
|
31
|
+
coerced_value = super(v).get(self)
|
32
|
+
represented.send("#{name}=", coerced_value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_reader(name)
|
37
|
+
# the call to super makes the actual coercion, which is then delegated to the represented instance.
|
38
|
+
define_method "#{name}" do
|
39
|
+
send("#{name}=", represented.send(name))
|
40
|
+
super()
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/representable/json.rb
CHANGED
@@ -14,25 +14,25 @@ module Representable
|
|
14
14
|
extend Representable::Hash::ClassMethods # DISCUSS: this is only for .from_hash, remove in 2.3?
|
15
15
|
end
|
16
16
|
end
|
17
|
-
|
18
|
-
|
17
|
+
|
18
|
+
|
19
19
|
module ClassMethods
|
20
20
|
# Creates a new object from the passed JSON document.
|
21
21
|
def from_json(*args, &block)
|
22
22
|
create_represented(*args, &block).from_json(*args)
|
23
23
|
end
|
24
24
|
end
|
25
|
-
|
26
|
-
|
25
|
+
|
26
|
+
|
27
27
|
# Parses the body as JSON and delegates to #from_hash.
|
28
28
|
def from_json(data, *args)
|
29
|
-
data =
|
29
|
+
data = MultiJson.load(data)
|
30
30
|
from_hash(data, *args)
|
31
31
|
end
|
32
|
-
|
32
|
+
|
33
33
|
# Returns a JSON string representing this object.
|
34
34
|
def to_json(*args)
|
35
|
-
to_hash(*args)
|
35
|
+
MultiJson.dump to_hash(*args)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
data/representable.gemspec
CHANGED
data/test/coercion_test.rb
CHANGED
@@ -4,7 +4,7 @@ require 'representable/coercion'
|
|
4
4
|
class VirtusCoercionTest < MiniTest::Spec
|
5
5
|
class Song # note that we don't define accessors for the properties here.
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
describe "Coercion with Virtus" do
|
9
9
|
describe "on object level" do
|
10
10
|
module SongRepresenter
|
@@ -13,7 +13,7 @@ class VirtusCoercionTest < MiniTest::Spec
|
|
13
13
|
property :composed_at, :type => DateTime
|
14
14
|
property :track, :type => Integer
|
15
15
|
end
|
16
|
-
|
16
|
+
|
17
17
|
it "coerces properties in #from_json" do
|
18
18
|
song = Song.new.extend(SongRepresenter).from_json("{\"composed_at\":\"November 18th, 1983\",\"track\":\"18\"}")
|
19
19
|
assert_kind_of DateTime, song.composed_at
|
@@ -21,29 +21,47 @@ class VirtusCoercionTest < MiniTest::Spec
|
|
21
21
|
assert_equal DateTime.parse("Fri, 18 Nov 1983 00:00:00 +0000"), song.composed_at
|
22
22
|
end
|
23
23
|
end
|
24
|
-
|
25
|
-
|
24
|
+
|
25
|
+
|
26
26
|
describe "on class level" do
|
27
27
|
class ImmigrantSong
|
28
28
|
include Representable::JSON
|
29
29
|
include Representable::Coercion
|
30
|
-
|
30
|
+
|
31
31
|
property :composed_at, :type => DateTime, :default => "May 12th, 2012"
|
32
32
|
property :track, :type => Integer
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
it "coerces into the provided type" do
|
36
36
|
song = ImmigrantSong.new.from_json("{\"composed_at\":\"November 18th, 1983\",\"track\":\"18\"}")
|
37
37
|
assert_equal DateTime.parse("Fri, 18 Nov 1983 00:00:00 +0000"), song.composed_at
|
38
38
|
assert_equal 18, song.track
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
41
|
it "respects the :default options" do
|
42
42
|
song = ImmigrantSong.new.from_json("{}")
|
43
43
|
assert_kind_of DateTime, song.composed_at
|
44
44
|
assert_equal DateTime.parse("Mon, 12 May 2012 00:00:00 +0000"), song.composed_at
|
45
45
|
end
|
46
46
|
end
|
47
|
-
|
47
|
+
|
48
|
+
require 'representable/decorator/coercion'
|
49
|
+
describe "on decorator" do
|
50
|
+
class SongRepresentation < Representable::Decorator
|
51
|
+
include Representable::JSON
|
52
|
+
include Representable::Decorator::Coercion
|
53
|
+
|
54
|
+
property :composed_at, :type => DateTime
|
55
|
+
end
|
56
|
+
|
57
|
+
it "coerces when parsing" do
|
58
|
+
song = SongRepresentation.new(OpenStruct.new).from_json("{\"composed_at\":\"November 18th, 1983\"}")
|
59
|
+
song.composed_at.must_equal DateTime.parse("Fri, 18 Nov 1983")
|
60
|
+
end
|
61
|
+
|
62
|
+
it "coerces when rendering" do
|
63
|
+
SongRepresentation.new(OpenStruct.new(:composed_at => "November 18th, 1983")).to_json.must_equal "{\"composed_at\":\"1983-11-18T00:00:00+00:00\"}"
|
64
|
+
end
|
65
|
+
end
|
48
66
|
end
|
49
67
|
end
|
data/test/representable_test.rb
CHANGED
@@ -683,11 +683,19 @@ class RepresentableTest < MiniTest::Spec
|
|
683
683
|
end
|
684
684
|
end
|
685
685
|
|
686
|
-
describe ":
|
686
|
+
describe ":decorator_scope" do
|
687
687
|
representer! do
|
688
|
-
property :title, :getter => lambda { |*| title_from_representer }, :
|
688
|
+
property :title, :getter => lambda { |*| title_from_representer }, :decorator_scope => true
|
689
689
|
end
|
690
690
|
|
691
|
+
let (:representer_with_method) {
|
692
|
+
Module.new do
|
693
|
+
include Representable::Hash
|
694
|
+
property :title, :decorator_scope => true
|
695
|
+
def title; "Crystal Planet"; end
|
696
|
+
end
|
697
|
+
}
|
698
|
+
|
691
699
|
it "executes lambdas in represented context" do
|
692
700
|
Class.new do
|
693
701
|
def title_from_representer
|
@@ -696,8 +704,11 @@ class RepresentableTest < MiniTest::Spec
|
|
696
704
|
end.new.extend(representer).to_hash.must_equal({"title"=>"Sounds Of Silence"})
|
697
705
|
end
|
698
706
|
|
699
|
-
|
707
|
+
it "executes method in represented context" do
|
708
|
+
Object.new.extend(representer_with_method).to_hash.must_equal({"title"=>"Crystal Planet"})
|
709
|
+
end
|
700
710
|
|
711
|
+
describe "with decorator" do
|
701
712
|
it "executes lambdas in representer context" do
|
702
713
|
rpr = representer
|
703
714
|
Class.new(Representable::Decorator) do
|
@@ -706,13 +717,20 @@ class RepresentableTest < MiniTest::Spec
|
|
706
717
|
def title_from_representer
|
707
718
|
"Sounds Of Silence"
|
708
719
|
end
|
709
|
-
end.new(Object.new).to_hash
|
720
|
+
end.new(Object.new).to_hash.must_equal({"title"=>"Sounds Of Silence"})
|
721
|
+
end
|
722
|
+
|
723
|
+
it "executes method in representer context" do
|
724
|
+
rpr = representer_with_method
|
725
|
+
Class.new(Representable::Decorator) do
|
726
|
+
include rpr # mixes in #title.
|
727
|
+
end.new(Object.new).to_hash.must_equal({"title"=>"Crystal Planet"})
|
710
728
|
end
|
711
729
|
|
712
730
|
it "still allows accessing the represented object" do
|
713
731
|
Class.new(Representable::Decorator) do
|
714
732
|
include Representable::Hash
|
715
|
-
property :title, :getter => lambda { |*| represented.title }, :
|
733
|
+
property :title, :getter => lambda { |*| represented.title }, :decorator_scope => true
|
716
734
|
|
717
735
|
def title
|
718
736
|
"Sounds Of Silence"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: representable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-05-
|
12
|
+
date: 2013-05-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: nokogiri
|
@@ -155,6 +155,22 @@ dependencies:
|
|
155
155
|
- - ! '>='
|
156
156
|
- !ruby/object:Gem::Version
|
157
157
|
version: '0'
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: json
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ~>
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: 1.7.7
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ~>
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: 1.7.7
|
158
174
|
description: Maps representation documents from and to Ruby objects. Includes XML
|
159
175
|
and JSON support, plain properties, collections and compositions.
|
160
176
|
email:
|
@@ -178,6 +194,7 @@ files:
|
|
178
194
|
- lib/representable/bindings/yaml_bindings.rb
|
179
195
|
- lib/representable/coercion.rb
|
180
196
|
- lib/representable/decorator.rb
|
197
|
+
- lib/representable/decorator/coercion.rb
|
181
198
|
- lib/representable/definition.rb
|
182
199
|
- lib/representable/deprecations.rb
|
183
200
|
- lib/representable/feature/readable_writeable.rb
|
@@ -192,7 +209,6 @@ files:
|
|
192
209
|
- lib/representable/xml/hash.rb
|
193
210
|
- lib/representable/yaml.rb
|
194
211
|
- representable.gemspec
|
195
|
-
- test/Gemfile
|
196
212
|
- test/coercion_test.rb
|
197
213
|
- test/definition_test.rb
|
198
214
|
- test/example.rb
|
data/test/Gemfile
DELETED