roar 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +14 -6
- data/CHANGES.markdown +75 -58
- data/CONTRIBUTING.md +31 -0
- data/Gemfile +12 -2
- data/ISSUE_TEMPLATE.md +20 -0
- data/LICENSE +1 -1
- data/README.markdown +126 -250
- data/Rakefile +3 -1
- data/examples/example.rb +0 -0
- data/examples/example_server.rb +0 -0
- data/lib/roar.rb +3 -3
- data/lib/roar/client.rb +8 -3
- data/lib/roar/decorator.rb +2 -2
- data/lib/roar/http_verbs.rb +0 -16
- data/lib/roar/hypermedia.rb +30 -56
- data/lib/roar/json.rb +5 -5
- data/lib/roar/json/collection.rb +10 -2
- data/lib/roar/json/hal.rb +72 -82
- data/lib/roar/version.rb +1 -1
- data/lib/roar/xml.rb +1 -1
- data/roar.gemspec +6 -6
- data/test/client_test.rb +1 -1
- data/test/coercion_feature_test.rb +7 -2
- data/test/decorator_test.rb +17 -7
- data/test/hal_json_test.rb +98 -106
- data/test/hypermedia_feature_test.rb +13 -31
- data/test/hypermedia_test.rb +26 -92
- data/test/{decorator_client_test.rb → integration/decorator_client_test.rb} +5 -4
- data/test/{faraday_http_transport_test.rb → integration/faraday_http_transport_test.rb} +1 -0
- data/test/{http_verbs_test.rb → integration/http_verbs_test.rb} +3 -2
- data/test/integration/json_collection_test.rb +35 -0
- data/test/{net_http_transport_test.rb → integration/net_http_transport_test.rb} +1 -0
- data/test/integration/runner.rb +2 -3
- data/test/integration/server.rb +6 -0
- data/test/json_representer_test.rb +2 -29
- data/test/lonely_test.rb +1 -2
- data/test/ssl_client_certs_test.rb +1 -1
- data/test/test_helper.rb +21 -3
- data/test/xml_representer_test.rb +6 -5
- metadata +22 -36
- data/gemfiles/Gemfile.representable-1.7 +0 -6
- data/gemfiles/Gemfile.representable-1.8 +0 -6
- data/gemfiles/Gemfile.representable-2.0 +0 -5
- data/gemfiles/Gemfile.representable-2.1 +0 -5
- data/gemfiles/Gemfile.representable-head +0 -6
- data/lib/roar/json/collection_json.rb +0 -208
- data/lib/roar/json/json_api.rb +0 -233
- data/test/collection_json_test.rb +0 -132
- data/test/hal_links_test.rb +0 -31
- data/test/json_api_test.rb +0 -451
- data/test/lib/runner.rb +0 -134
data/lib/roar/json/json_api.rb
DELETED
@@ -1,233 +0,0 @@
|
|
1
|
-
require 'roar/json'
|
2
|
-
require 'roar/decorator'
|
3
|
-
|
4
|
-
module Roar
|
5
|
-
module JSON
|
6
|
-
module JSONAPI
|
7
|
-
def self.included(base)
|
8
|
-
base.class_eval do
|
9
|
-
include Representable::JSON
|
10
|
-
include Roar::JSON::JSONAPI::Singular
|
11
|
-
include Roar::JSON::JSONAPI::Resource
|
12
|
-
include Roar::JSON::JSONAPI::Document
|
13
|
-
|
14
|
-
extend ForCollection
|
15
|
-
|
16
|
-
representable_attrs[:resource_representer] = Class.new(Resource::Representer)
|
17
|
-
|
18
|
-
private
|
19
|
-
def create_representation_with(doc, options, format)
|
20
|
-
super(doc, options.merge(:only_body => true), format)
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
module ForCollection
|
26
|
-
def for_collection # same API as representable. TODO: we could use ::collection_representer! here.
|
27
|
-
singular = self # e.g. Song::Representer
|
28
|
-
|
29
|
-
# this basically does Module.new { include Hash::Collection .. }
|
30
|
-
build_inline(nil, [Representable::Hash::Collection, Document::Collection, Roar::JSON], "", {}) do
|
31
|
-
items extend: singular, :parse_strategy => :sync
|
32
|
-
|
33
|
-
representable_attrs[:resource_representer] = singular.representable_attrs[:resource_representer]
|
34
|
-
representable_attrs[:meta_representer] = singular.representable_attrs[:meta_representer] # DISCUSS: do we need that?
|
35
|
-
representable_attrs[:_wrap] = singular.representable_attrs[:_wrap]
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
|
41
|
-
module Singular
|
42
|
-
def to_hash(options={})
|
43
|
-
# per resource:
|
44
|
-
super(options.merge(:exclude => [:links])).tap do |hash|
|
45
|
-
hash["links"] = hash.delete("_links") if hash["_links"]
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def from_hash(hash, options={})
|
50
|
-
hash["_links"] = hash["links"]
|
51
|
-
super
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
|
56
|
-
module Resource
|
57
|
-
# ::link is delegated to Representer which handles the hypermedia (rendering
|
58
|
-
# and parsing links).
|
59
|
-
class Representer < Roar::Decorator
|
60
|
-
include Roar::JSON
|
61
|
-
include Roar::Hypermedia
|
62
|
-
|
63
|
-
def self.links_definition_options
|
64
|
-
{
|
65
|
-
:extend => LinkCollectionRepresenter,
|
66
|
-
:exec_context => :decorator
|
67
|
-
}
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def self.included(base)
|
72
|
-
base.extend Declarative # inject our ::link.
|
73
|
-
end
|
74
|
-
|
75
|
-
# New API for JSON-API representers.
|
76
|
-
module Declarative
|
77
|
-
def type(name=nil)
|
78
|
-
return super unless name # original name.
|
79
|
-
representable_attrs[:_wrap] = name.to_s
|
80
|
-
end
|
81
|
-
|
82
|
-
# Define global document links for the links: directive.
|
83
|
-
def link(*args, &block)
|
84
|
-
representable_attrs[:resource_representer].link(*args, &block)
|
85
|
-
end
|
86
|
-
|
87
|
-
# Per-model links.
|
88
|
-
def links(&block)
|
89
|
-
nested(:_links, :inherit => true, &block)
|
90
|
-
end
|
91
|
-
|
92
|
-
# TODO: always create _links.
|
93
|
-
def has_one(name)
|
94
|
-
property :_links, :inherit => true, :use_decorator => true do # simply extend the Decorator _links.
|
95
|
-
property "#{name}_id", :as => name
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def has_many(name)
|
100
|
-
property :_links, :inherit => true, :use_decorator => true do # simply extend the Decorator _links.
|
101
|
-
collection "#{name.to_s.sub(/s$/, "")}_ids", :as => name
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def compound(&block)
|
106
|
-
nested(:linked, &block)
|
107
|
-
end
|
108
|
-
|
109
|
-
def meta(&block)
|
110
|
-
representable_attrs[:meta_representer] = Class.new(Roar::Decorator, &block)
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
|
116
|
-
# TODO: don't use Document for singular+wrap AND singular in collection (this way, we can get rid of the only_body)
|
117
|
-
module Document
|
118
|
-
def to_hash(options={})
|
119
|
-
# per resource:
|
120
|
-
res = super # render single resource or collection.
|
121
|
-
return res if options[:only_body]
|
122
|
-
# this is the only "dirty" part: this module is always included in the Singular document representer, when used in collection, we don't want it to do the extra work. this mechanism here might be changed soon.
|
123
|
-
|
124
|
-
to_document(res, options)
|
125
|
-
end
|
126
|
-
|
127
|
-
def from_hash(hash, options={})
|
128
|
-
|
129
|
-
return super(hash, options) if options[:only_body] # singular
|
130
|
-
|
131
|
-
super(from_document(hash)) # singular
|
132
|
-
end
|
133
|
-
|
134
|
-
private
|
135
|
-
def to_document(res, options)
|
136
|
-
links = render_links
|
137
|
-
meta = render_meta(options)
|
138
|
-
# FIXME: provide two different #to_document
|
139
|
-
|
140
|
-
if res.is_a?(Array)
|
141
|
-
compound = collection_compound!(res, {})
|
142
|
-
else
|
143
|
-
compound = compile_compound!(res.delete("linked"), {})
|
144
|
-
end
|
145
|
-
|
146
|
-
{representable_attrs[:_wrap] => res}.tap do |doc|
|
147
|
-
doc.merge!(links)
|
148
|
-
doc.merge!(meta)
|
149
|
-
doc.merge!("linked" => compound) if compound && compound.size > 0 # FIXME: make that like the above line.
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def from_document(hash)
|
154
|
-
hash[representable_attrs[:_wrap]]
|
155
|
-
end
|
156
|
-
|
157
|
-
# Compiles the linked: section for compound objects in the document.
|
158
|
-
def collection_compound!(collection, compound)
|
159
|
-
collection.each { |res|
|
160
|
-
kv = res.delete("linked") or next
|
161
|
-
|
162
|
-
compile_compound!(kv, compound)
|
163
|
-
}
|
164
|
-
|
165
|
-
compound
|
166
|
-
end
|
167
|
-
|
168
|
-
# Go through {"album"=>{"title"=>"Hackers"}, "musicians"=>[{"name"=>"Eddie Van Halen"}, ..]} from linked:
|
169
|
-
# and wrap every item in an array.
|
170
|
-
def compile_compound!(linked, compound)
|
171
|
-
return unless linked
|
172
|
-
|
173
|
-
linked.each { |k,v| # {"album"=>{"title"=>"Hackers"}, "musicians"=>[{"name"=>"Eddie Van Halen"}, {"name"=>"Greg Howe"}]}
|
174
|
-
compound[k] ||= []
|
175
|
-
|
176
|
-
if v.is_a?(::Hash) # {"title"=>"Hackers"}
|
177
|
-
compound[k] << v
|
178
|
-
else
|
179
|
-
compound[k].push(*v) # [{"name"=>"Eddie Van Halen"}, {"name"=>"Greg Howe"}]
|
180
|
-
end
|
181
|
-
|
182
|
-
compound[k] = compound[k].uniq
|
183
|
-
}
|
184
|
-
|
185
|
-
compound
|
186
|
-
end
|
187
|
-
|
188
|
-
def render_links
|
189
|
-
representable_attrs[:resource_representer].new(represented).to_hash # creates links: section.
|
190
|
-
end
|
191
|
-
|
192
|
-
def render_meta(options)
|
193
|
-
# TODO: this will call collection.page etc, directly on the collection. we could allow using a "meta"
|
194
|
-
# object to hold this data.
|
195
|
-
# `meta call_meta: true` or something
|
196
|
-
return {"meta" => options["meta"]} if options["meta"]
|
197
|
-
return {} unless representer = representable_attrs[:meta_representer]
|
198
|
-
{"meta" => representer.new(represented).extend(Representable::Hash).to_hash}
|
199
|
-
end
|
200
|
-
|
201
|
-
|
202
|
-
module Collection
|
203
|
-
include Document
|
204
|
-
|
205
|
-
def to_hash(options={})
|
206
|
-
res = super(options.merge(:only_body => true))
|
207
|
-
to_document(res, options)
|
208
|
-
end
|
209
|
-
|
210
|
-
def from_hash(hash, options={})
|
211
|
-
hash = from_document(hash)
|
212
|
-
super(hash, options.merge(:only_body => true))
|
213
|
-
end
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
|
218
|
-
module LinkRepresenter
|
219
|
-
include Roar::JSON
|
220
|
-
|
221
|
-
property :href
|
222
|
-
property :type
|
223
|
-
end
|
224
|
-
|
225
|
-
require 'representable/json/hash'
|
226
|
-
module LinkCollectionRepresenter
|
227
|
-
include Representable::JSON::Hash
|
228
|
-
|
229
|
-
values :extend => LinkRepresenter # TODO: parsing.
|
230
|
-
end
|
231
|
-
end
|
232
|
-
end
|
233
|
-
end
|
@@ -1,132 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
require 'roar/json/collection_json'
|
3
|
-
|
4
|
-
if RUBY_ENGINE != "rbx"
|
5
|
-
class CollectionJsonTest < MiniTest::Spec
|
6
|
-
let(:song) { OpenStruct.new(:title => "scarifice", :length => 43) }
|
7
|
-
|
8
|
-
representer_for([Roar::JSON::CollectionJSON]) do
|
9
|
-
version "1.0"
|
10
|
-
href { "//songs/" }
|
11
|
-
|
12
|
-
link(:feed) { "//songs/feed" }
|
13
|
-
|
14
|
-
items(:class => Song) do
|
15
|
-
href { "//songs/scarifice" }
|
16
|
-
|
17
|
-
property :title, :prompt => "Song title"
|
18
|
-
property :length, :prompt => "Song length"
|
19
|
-
|
20
|
-
link(:download) { "//songs/scarifice.mp3" }
|
21
|
-
link(:stats) { "//songs/scarifice/stats" }
|
22
|
-
end
|
23
|
-
|
24
|
-
template do
|
25
|
-
property :title, :prompt => "Song title"
|
26
|
-
property :length, :prompt => "Song length"
|
27
|
-
end
|
28
|
-
|
29
|
-
queries do
|
30
|
-
link :search do
|
31
|
-
{:href => "//search", :data => [{:name => "q", :value => ""}]}
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
describe "#to_json" do
|
37
|
-
it "renders document" do
|
38
|
-
[song].extend(rpr).to_hash.must_equal(
|
39
|
-
{
|
40
|
-
"collection"=>{
|
41
|
-
"version"=>"1.0",
|
42
|
-
"href"=>"//songs/",
|
43
|
-
|
44
|
-
"template"=>{
|
45
|
-
:data=>[
|
46
|
-
{:name=>"title", :value=>nil},
|
47
|
-
{:name=>"length", :value=>nil}
|
48
|
-
]
|
49
|
-
},
|
50
|
-
|
51
|
-
"queries"=>[
|
52
|
-
{"rel"=>"search", "href"=>"//search",
|
53
|
-
"data"=>[
|
54
|
-
{:name=>"q", :value=>""}
|
55
|
-
]
|
56
|
-
}
|
57
|
-
],
|
58
|
-
|
59
|
-
"items"=>[
|
60
|
-
{
|
61
|
-
"links"=>[
|
62
|
-
{"rel"=>"download", "href"=>"//songs/scarifice.mp3"},
|
63
|
-
{"rel"=>"stats", "href"=>"//songs/scarifice/stats"}
|
64
|
-
],
|
65
|
-
"href"=>"//songs/scarifice",
|
66
|
-
:data=>[
|
67
|
-
{:name=>"title", :value=>"scarifice"},
|
68
|
-
{:name=>"length", :value=>43}
|
69
|
-
]
|
70
|
-
}
|
71
|
-
],
|
72
|
-
|
73
|
-
"links"=>[
|
74
|
-
{"rel"=>"feed", "href"=>"//songs/feed"}
|
75
|
-
]
|
76
|
-
}
|
77
|
-
})# %{{"collection":{"version":"1.0","href":"//songs/","items":[{"href":"//songs/scarifice","links":[{"rel":"download","href":"//songs/scarifice.mp3"},{"rel":"stats","href":"//songs/scarifice/stats"}],"data":[{"name":"title","value":"scarifice"},{"name":"length","value":43}]}],"template":{"data":[{"name":"title","value":null},{"name":"length","value":null}]},"queries":[{"rel":"search","href":"//search","data":[{"name":"q","value":""}]}],"links":[{"rel":"feed","href":"//songs/feed"}]}}}
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
describe "#from_json" do
|
82
|
-
subject { [].extend(rpr).from_json [song].extend(rpr).to_json }
|
83
|
-
|
84
|
-
it "provides #version" do
|
85
|
-
subject.version.must_equal "1.0"
|
86
|
-
end
|
87
|
-
|
88
|
-
it "provides #href" do
|
89
|
-
subject.href.must_equal link(:href => "//songs/")
|
90
|
-
end
|
91
|
-
|
92
|
-
it "provides #template" do
|
93
|
-
# DISCUSS: this might return a Template instance, soon.
|
94
|
-
subject.template.must_equal([
|
95
|
-
{"name"=>"title", "value"=>nil},
|
96
|
-
{"name"=>"length", "value"=>nil}])
|
97
|
-
end
|
98
|
-
|
99
|
-
it "provides #queries" do
|
100
|
-
# DISCUSS: this might return CollectionJSON::Hyperlink instances that support some kind of substitution operation for the :data attribute.
|
101
|
-
# FIXME: this is currently _not_ parsed!
|
102
|
-
subject.queries.must_equal([link(:rel => :search, :href=>"//search", :data=>[{:name=>"q", :value=>""}])])
|
103
|
-
end
|
104
|
-
|
105
|
-
it "provides #items" do
|
106
|
-
subject.items.must_equal([Song.new(:title => "scarifice", :length => "43")])
|
107
|
-
song = subject.items.first
|
108
|
-
song.title.must_equal "scarifice"
|
109
|
-
song.length.must_equal 43
|
110
|
-
song.links.must_equal("download" => link({:rel=>"download", :href=>"//songs/scarifice.mp3"}), "stats" => link({:rel=>"stats", :href=>"//songs/scarifice/stats"}))
|
111
|
-
song.href.must_equal link(:href => "//songs/scarifice")
|
112
|
-
end
|
113
|
-
|
114
|
-
it "provides #links" do
|
115
|
-
subject.links.must_equal({"feed" => link(:rel => "feed", :href => "//songs/feed")})
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
describe "template_representer#from_json" do
|
120
|
-
it "parses object" do
|
121
|
-
song = OpenStruct.new.extend(rpr.template_representer).from_hash(
|
122
|
-
"template"=>{
|
123
|
-
"data"=>[
|
124
|
-
{"name"=>"title", "value"=>"Black Star"},
|
125
|
-
{"name"=>"length", "value"=>"4.53"}
|
126
|
-
]
|
127
|
-
})
|
128
|
-
song.title.must_equal "Black Star"
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
data/test/hal_links_test.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
require 'ostruct'
|
2
|
-
require 'test_helper'
|
3
|
-
require 'roar/json/hal'
|
4
|
-
|
5
|
-
|
6
|
-
class HalLinkTest < MiniTest::Spec
|
7
|
-
let(:rpr) do
|
8
|
-
Module.new do
|
9
|
-
include Roar::JSON
|
10
|
-
include Roar::JSON::HAL::Links
|
11
|
-
link :self do
|
12
|
-
"//songs"
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
subject { Object.new.extend(rpr) }
|
18
|
-
|
19
|
-
describe "#to_json" do
|
20
|
-
it "uses 'links' key" do
|
21
|
-
subject.to_json.must_equal "{\"links\":{\"self\":{\"href\":\"//songs\"}}}"
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
describe "#from_json" do
|
26
|
-
it "uses 'links' key" do
|
27
|
-
subject.from_json("{\"links\":{\"self\":{\"href\":\"//lifer\"}}}").links.values.must_equal [link("href" => "//lifer", "rel" => "self")]
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
data/test/json_api_test.rb
DELETED
@@ -1,451 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
require 'roar/json/json_api'
|
3
|
-
require 'json'
|
4
|
-
|
5
|
-
require "representable/version"
|
6
|
-
if Gem::Version.new(Representable::VERSION) >= Gem::Version.new("2.1.4") # TODO: remove check once we bump representable dependency.
|
7
|
-
class JSONAPITest < MiniTest::Spec
|
8
|
-
let(:song) {
|
9
|
-
s = OpenStruct.new(
|
10
|
-
bla: "halo",
|
11
|
-
id: "1",
|
12
|
-
title: 'Computadores Fazem Arte',
|
13
|
-
album: OpenStruct.new(id: 9, title: "Hackers"),
|
14
|
-
:album_id => "9",
|
15
|
-
:musician_ids => ["1","2"],
|
16
|
-
:composer_id => "10",
|
17
|
-
:listener_ids => ["8"],
|
18
|
-
musicians: [OpenStruct.new(id: 1, name: "Eddie Van Halen"), OpenStruct.new(id: 2, name: "Greg Howe")]
|
19
|
-
)
|
20
|
-
|
21
|
-
}
|
22
|
-
|
23
|
-
# minimal resource, singular
|
24
|
-
module MinimalSingular
|
25
|
-
include Roar::JSON::JSONAPI
|
26
|
-
type :songs
|
27
|
-
|
28
|
-
property :id
|
29
|
-
end
|
30
|
-
|
31
|
-
class MinimalSingularDecorator < Roar::Decorator
|
32
|
-
include Roar::JSON::JSONAPI
|
33
|
-
type :songs
|
34
|
-
|
35
|
-
property :id
|
36
|
-
end
|
37
|
-
|
38
|
-
[MinimalSingular, MinimalSingularDecorator].each do |representer|
|
39
|
-
describe "minimal singular with #{representer}" do
|
40
|
-
subject { representer.prepare(song) }
|
41
|
-
|
42
|
-
it { subject.to_json.must_equal "{\"songs\":{\"id\":\"1\"}}" }
|
43
|
-
it { subject.from_json("{\"songs\":{\"id\":\"2\"}}").id.must_equal "2" }
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
module Singular
|
48
|
-
include Roar::JSON::JSONAPI
|
49
|
-
type :songs
|
50
|
-
|
51
|
-
property :id
|
52
|
-
property :title, if: lambda { |args| args[:omit_title] != true }
|
53
|
-
|
54
|
-
# local per-model "id" links
|
55
|
-
links do
|
56
|
-
property :album_id, :as => :album
|
57
|
-
collection :musician_ids, :as => :musicians
|
58
|
-
end
|
59
|
-
has_one :composer
|
60
|
-
has_many :listeners
|
61
|
-
|
62
|
-
|
63
|
-
# global document links.
|
64
|
-
link "songs.album" do
|
65
|
-
{
|
66
|
-
type: "album",
|
67
|
-
href: "http://example.com/albums/{songs.album}"
|
68
|
-
}
|
69
|
-
end
|
70
|
-
|
71
|
-
compound do
|
72
|
-
property :album do
|
73
|
-
property :title
|
74
|
-
end
|
75
|
-
|
76
|
-
collection :musicians do
|
77
|
-
property :name
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
class SingularDecorator < Roar::Decorator
|
83
|
-
include Roar::JSON::JSONAPI
|
84
|
-
type :songs
|
85
|
-
|
86
|
-
property :id
|
87
|
-
property :title, if: lambda { |args| args[:omit_title] != true }
|
88
|
-
|
89
|
-
# NOTE: it is important to call has_one, then links, then has_many to assert that they all write
|
90
|
-
#to the same _links property and do NOT override things.
|
91
|
-
has_one :composer
|
92
|
-
# local per-model "id" links
|
93
|
-
links do
|
94
|
-
property :album_id, :as => :album
|
95
|
-
collection :musician_ids, :as => :musicians
|
96
|
-
end
|
97
|
-
has_many :listeners
|
98
|
-
|
99
|
-
|
100
|
-
# global document links.
|
101
|
-
link "songs.album" do
|
102
|
-
{
|
103
|
-
type: "album",
|
104
|
-
href: "http://example.com/albums/{songs.album}"
|
105
|
-
}
|
106
|
-
end
|
107
|
-
|
108
|
-
compound do
|
109
|
-
property :album do
|
110
|
-
property :title
|
111
|
-
end
|
112
|
-
|
113
|
-
collection :musicians do
|
114
|
-
property :name
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
[Singular, SingularDecorator].each do |representer|
|
120
|
-
describe "singular with #{representer}" do
|
121
|
-
subject { song.extend(Singular) }
|
122
|
-
|
123
|
-
let (:document) do
|
124
|
-
{
|
125
|
-
"songs" => {
|
126
|
-
"id" => "1",
|
127
|
-
"title" => "Computadores Fazem Arte",
|
128
|
-
"links" => {
|
129
|
-
"album" => "9",
|
130
|
-
"musicians" => [ "1", "2" ],
|
131
|
-
"composer"=>"10",
|
132
|
-
"listeners"=>["8"]
|
133
|
-
}
|
134
|
-
},
|
135
|
-
"links" => {
|
136
|
-
"songs.album"=> {
|
137
|
-
"href"=>"http://example.com/albums/{songs.album}", "type"=>"album"
|
138
|
-
}
|
139
|
-
},
|
140
|
-
"linked" => {
|
141
|
-
"album"=> [{"title"=>"Hackers"}],
|
142
|
-
"musicians"=> [
|
143
|
-
{"name"=>"Eddie Van Halen"},
|
144
|
-
{"name"=>"Greg Howe"}
|
145
|
-
]
|
146
|
-
}
|
147
|
-
}
|
148
|
-
end
|
149
|
-
|
150
|
-
# to_hash
|
151
|
-
it do
|
152
|
-
subject.to_hash.must_equal document
|
153
|
-
end
|
154
|
-
|
155
|
-
# to_hash(options)
|
156
|
-
it do
|
157
|
-
subject.to_hash(omit_title: true)['songs'].wont_include('title')
|
158
|
-
end
|
159
|
-
|
160
|
-
# #to_json
|
161
|
-
it do
|
162
|
-
subject.to_json.must_equal JSON.generate(document)
|
163
|
-
end
|
164
|
-
|
165
|
-
# #from_json
|
166
|
-
it do
|
167
|
-
song = OpenStruct.new.extend(Singular)
|
168
|
-
song.from_json(
|
169
|
-
JSON.generate(
|
170
|
-
{
|
171
|
-
"songs" => {
|
172
|
-
"id" => "1",
|
173
|
-
"title" => "Computadores Fazem Arte",
|
174
|
-
"links" => {
|
175
|
-
"album" => "9",
|
176
|
-
"musicians" => [ "1", "2" ],
|
177
|
-
"composer"=>"10",
|
178
|
-
"listeners"=>["8"]
|
179
|
-
}
|
180
|
-
},
|
181
|
-
"links" => {
|
182
|
-
"songs.album"=> {
|
183
|
-
"href"=>"http://example.com/albums/{songs.album}", "type"=>"album"
|
184
|
-
}
|
185
|
-
}
|
186
|
-
}
|
187
|
-
)
|
188
|
-
)
|
189
|
-
|
190
|
-
song.id.must_equal "1"
|
191
|
-
song.title.must_equal "Computadores Fazem Arte"
|
192
|
-
song.album_id.must_equal "9"
|
193
|
-
song.musician_ids.must_equal ["1", "2"]
|
194
|
-
song.composer_id.must_equal "10"
|
195
|
-
song.listener_ids.must_equal ["8"]
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
|
201
|
-
# collection with links
|
202
|
-
[Singular, SingularDecorator].each do |representer|
|
203
|
-
describe "collection with links and compound with #{representer}" do
|
204
|
-
subject { representer.for_collection.prepare([song, song]) }
|
205
|
-
|
206
|
-
let (:document) do
|
207
|
-
{
|
208
|
-
"songs" => [
|
209
|
-
{
|
210
|
-
"id" => "1",
|
211
|
-
"title" => "Computadores Fazem Arte",
|
212
|
-
"links" => {
|
213
|
-
"composer"=>"10",
|
214
|
-
"album" => "9",
|
215
|
-
"musicians" => [ "1", "2" ],
|
216
|
-
"listeners"=>["8"]
|
217
|
-
}
|
218
|
-
}, {
|
219
|
-
"id" => "1",
|
220
|
-
"title" => "Computadores Fazem Arte",
|
221
|
-
"links" => {
|
222
|
-
"composer"=>"10",
|
223
|
-
"album" => "9",
|
224
|
-
"musicians" => [ "1", "2" ],
|
225
|
-
"listeners"=>["8"]
|
226
|
-
}
|
227
|
-
}
|
228
|
-
],
|
229
|
-
"links" => {
|
230
|
-
"songs.album" => {
|
231
|
-
"href" => "http://example.com/albums/{songs.album}",
|
232
|
-
"type" => "album" # DISCUSS: does that have to be albums ?
|
233
|
-
},
|
234
|
-
},
|
235
|
-
"linked"=>{
|
236
|
-
"album" =>[{"title"=>"Hackers"}], # only once!
|
237
|
-
"musicians"=>[{"name"=>"Eddie Van Halen"}, {"name"=>"Greg Howe"}]
|
238
|
-
}
|
239
|
-
}
|
240
|
-
end
|
241
|
-
|
242
|
-
# to_hash
|
243
|
-
it do
|
244
|
-
subject.to_hash.must_equal document
|
245
|
-
end
|
246
|
-
|
247
|
-
# to_hash(options)
|
248
|
-
it do
|
249
|
-
subject.to_hash(omit_title: true)['songs'].each do |song|
|
250
|
-
song.wont_include('title')
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
# #to_json
|
255
|
-
it { subject.to_json.must_match /linked/ } # hash ordering changes, and i don't care why.
|
256
|
-
end
|
257
|
-
|
258
|
-
|
259
|
-
# from_json
|
260
|
-
it do
|
261
|
-
song1, song2 = Singular.for_collection.prepare([OpenStruct.new, OpenStruct.new]).from_json(
|
262
|
-
JSON.generate(
|
263
|
-
{
|
264
|
-
"songs" => [
|
265
|
-
{
|
266
|
-
"id" => "1",
|
267
|
-
"title" => "Computadores Fazem Arte",
|
268
|
-
"links" => {
|
269
|
-
"album" => "9",
|
270
|
-
"musicians" => [ "1", "2" ],
|
271
|
-
"composer"=>"10",
|
272
|
-
"listeners"=>["8"]
|
273
|
-
},
|
274
|
-
},
|
275
|
-
{
|
276
|
-
"id" => "2",
|
277
|
-
"title" => "Talking To Remind Me",
|
278
|
-
"links" => {
|
279
|
-
"album" => "1",
|
280
|
-
"musicians" => [ "3", "4" ],
|
281
|
-
"composer"=>"2",
|
282
|
-
"listeners"=>["6"]
|
283
|
-
}
|
284
|
-
},
|
285
|
-
],
|
286
|
-
"links" => {
|
287
|
-
"songs.album"=> {
|
288
|
-
"href"=>"http://example.com/albums/{songs.album}", "type"=>"album"
|
289
|
-
}
|
290
|
-
}
|
291
|
-
}
|
292
|
-
)
|
293
|
-
)
|
294
|
-
|
295
|
-
song1.id.must_equal "1"
|
296
|
-
song1.title.must_equal "Computadores Fazem Arte"
|
297
|
-
song1.album_id.must_equal "9"
|
298
|
-
song1.musician_ids.must_equal ["1", "2"]
|
299
|
-
song1.composer_id.must_equal "10"
|
300
|
-
song1.listener_ids.must_equal ["8"]
|
301
|
-
|
302
|
-
song2.id.must_equal "2"
|
303
|
-
song2.title.must_equal "Talking To Remind Me"
|
304
|
-
song2.album_id.must_equal "1"
|
305
|
-
song2.musician_ids.must_equal ["3", "4"]
|
306
|
-
song2.composer_id.must_equal "2"
|
307
|
-
song2.listener_ids.must_equal ["6"]
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
|
312
|
-
class CollectionWithoutCompound < self
|
313
|
-
module Representer
|
314
|
-
include Roar::JSON::JSONAPI
|
315
|
-
type :songs
|
316
|
-
|
317
|
-
property :id
|
318
|
-
property :title
|
319
|
-
|
320
|
-
# local per-model "id" links
|
321
|
-
links do
|
322
|
-
property :album_id, :as => :album
|
323
|
-
collection :musician_ids, :as => :musicians
|
324
|
-
end
|
325
|
-
has_one :composer
|
326
|
-
has_many :listeners
|
327
|
-
|
328
|
-
|
329
|
-
# global document links.
|
330
|
-
link "songs.album" do
|
331
|
-
{
|
332
|
-
type: "album",
|
333
|
-
href: "http://example.com/albums/{songs.album}"
|
334
|
-
}
|
335
|
-
end
|
336
|
-
end
|
337
|
-
|
338
|
-
subject { [song, song].extend(Singular.for_collection) }
|
339
|
-
|
340
|
-
# to_json
|
341
|
-
it do
|
342
|
-
subject.extend(Representer.for_collection).to_hash.must_equal(
|
343
|
-
{
|
344
|
-
"songs"=>[{"id"=>"1", "title"=>"Computadores Fazem Arte", "links"=>{"album"=>"9", "musicians"=>["1", "2"], "composer"=>"10", "listeners"=>["8"]}}, {"id"=>"1", "title"=>"Computadores Fazem Arte", "links"=>{"album"=>"9", "musicians"=>["1", "2"], "composer"=>"10", "listeners"=>["8"]}}],
|
345
|
-
"links"=>{"songs.album"=>{"href"=>"http://example.com/albums/{songs.album}", "type"=>"album"}
|
346
|
-
}
|
347
|
-
}
|
348
|
-
)
|
349
|
-
end
|
350
|
-
end
|
351
|
-
|
352
|
-
class CompoundCollectionUsingExtend < self
|
353
|
-
module SongRepresenter
|
354
|
-
include Roar::JSON::JSONAPI
|
355
|
-
|
356
|
-
type :songs
|
357
|
-
property :id
|
358
|
-
property :title
|
359
|
-
end
|
360
|
-
|
361
|
-
module AlbumRepresenter
|
362
|
-
include Roar::JSON::JSONAPI
|
363
|
-
|
364
|
-
type :albums
|
365
|
-
property :id
|
366
|
-
compound do
|
367
|
-
collection :songs, extend: SongRepresenter
|
368
|
-
end
|
369
|
-
end
|
370
|
-
|
371
|
-
let(:songs) do
|
372
|
-
struct = Struct.new(:id, :title)
|
373
|
-
[struct.new(1, 'Stand Up'), struct.new(2, 'Audition Mantra')]
|
374
|
-
end
|
375
|
-
|
376
|
-
let(:album) { Struct.new(:id, :songs).new(1, songs) }
|
377
|
-
|
378
|
-
subject { album.extend(AlbumRepresenter) }
|
379
|
-
|
380
|
-
# to_hash
|
381
|
-
it do
|
382
|
-
subject.to_hash.must_equal({
|
383
|
-
'albums' => { 'id' => 1 },
|
384
|
-
'linked' => {
|
385
|
-
'songs' => [
|
386
|
-
{'id' => 1, 'title' => 'Stand Up'},
|
387
|
-
{'id' => 2, 'title' => 'Audition Mantra'}
|
388
|
-
]
|
389
|
-
}
|
390
|
-
})
|
391
|
-
end
|
392
|
-
end
|
393
|
-
|
394
|
-
class ExplicitMeta < self
|
395
|
-
module Representer
|
396
|
-
include Roar::JSON::JSONAPI
|
397
|
-
|
398
|
-
type :songs
|
399
|
-
property :id
|
400
|
-
|
401
|
-
meta do
|
402
|
-
property :page
|
403
|
-
end
|
404
|
-
end
|
405
|
-
|
406
|
-
module Page
|
407
|
-
def page
|
408
|
-
2
|
409
|
-
end
|
410
|
-
end
|
411
|
-
|
412
|
-
let (:song) { Struct.new(:id).new(1) }
|
413
|
-
|
414
|
-
subject { [song, song].extend(Representer.for_collection).extend(Page) }
|
415
|
-
|
416
|
-
# to_json
|
417
|
-
it do
|
418
|
-
subject.to_hash.must_equal(
|
419
|
-
{
|
420
|
-
"songs"=>[{"id"=>1}, {"id"=>1}],
|
421
|
-
"meta" =>{"page"=>2}
|
422
|
-
}
|
423
|
-
)
|
424
|
-
end
|
425
|
-
end
|
426
|
-
|
427
|
-
|
428
|
-
class ImplicitMeta < self
|
429
|
-
module Representer
|
430
|
-
include Roar::JSON::JSONAPI
|
431
|
-
|
432
|
-
type :songs
|
433
|
-
property :id
|
434
|
-
end
|
435
|
-
|
436
|
-
let (:song) { Struct.new(:id).new(1) }
|
437
|
-
|
438
|
-
subject { [song, song].extend(Representer.for_collection) }
|
439
|
-
|
440
|
-
# to_json
|
441
|
-
it do
|
442
|
-
subject.to_hash("meta" => {"page" => 2}).must_equal(
|
443
|
-
{
|
444
|
-
"songs"=>[{"id"=>1}, {"id"=>1}],
|
445
|
-
"meta" =>{"page"=>2}
|
446
|
-
}
|
447
|
-
)
|
448
|
-
end
|
449
|
-
end
|
450
|
-
end
|
451
|
-
end
|