subjoin 0.2.1
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/.gitignore +15 -0
- data/.travis.yml +23 -0
- data/.yardops +1 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +431 -0
- data/Rakefile +11 -0
- data/lib/subjoin.rb +85 -0
- data/lib/subjoin/attributable.rb +28 -0
- data/lib/subjoin/document.rb +136 -0
- data/lib/subjoin/errors.rb +15 -0
- data/lib/subjoin/identifier.rb +23 -0
- data/lib/subjoin/inclusions.rb +39 -0
- data/lib/subjoin/inheritable.rb +80 -0
- data/lib/subjoin/jsonapi.rb +13 -0
- data/lib/subjoin/link.rb +30 -0
- data/lib/subjoin/linkable.rb +18 -0
- data/lib/subjoin/meta.rb +10 -0
- data/lib/subjoin/metable.rb +21 -0
- data/lib/subjoin/relationship.rb +32 -0
- data/lib/subjoin/resource.rb +93 -0
- data/lib/subjoin/version.rb +4 -0
- data/spec/document_spec.rb +210 -0
- data/spec/identifier_spec.rb +33 -0
- data/spec/inclusions_spec.rb +69 -0
- data/spec/inheritable_resource_spec.rb +89 -0
- data/spec/link_spec.rb +60 -0
- data/spec/meta_spec.rb +11 -0
- data/spec/relationship_spec.rb +64 -0
- data/spec/resource_spec.rb +139 -0
- data/spec/responses/404.json +8 -0
- data/spec/responses/article_example.json +37 -0
- data/spec/responses/compound_example.json +73 -0
- data/spec/responses/links.json +9 -0
- data/spec/responses/meta.json +13 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/subjoin_spec.rb +99 -0
- data/subjoin.gemspec +27 -0
- metadata +168 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
module Subjoin
|
2
|
+
# Generically construct and handle {Links} objects
|
3
|
+
module Linkable
|
4
|
+
attr_reader :links
|
5
|
+
|
6
|
+
# Load the object's links
|
7
|
+
# @param data [Hash] The object's parsed JSON `links` member
|
8
|
+
# @return [Hash]
|
9
|
+
def load_links(data)
|
10
|
+
return nil if data.nil?
|
11
|
+
Hash[data.map{|k, v| [k, Link.new(v)]}]
|
12
|
+
end
|
13
|
+
|
14
|
+
def has_links?
|
15
|
+
return ! @links.nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/subjoin/meta.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Subjoin
|
2
|
+
# Generically handle meta objects
|
3
|
+
module Metable
|
4
|
+
|
5
|
+
# The object's meta attribute
|
6
|
+
# @return [Meta]
|
7
|
+
attr_reader :meta
|
8
|
+
|
9
|
+
# Load the object's attributes
|
10
|
+
# @param data [Hash] The object's parsed JSON `meta` member
|
11
|
+
# @return [Metable,nil]
|
12
|
+
def load_meta(data)
|
13
|
+
return nil if data.nil?
|
14
|
+
Meta.new(data)
|
15
|
+
end
|
16
|
+
|
17
|
+
def has_meta?
|
18
|
+
return ! @meta.nil?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Subjoin
|
2
|
+
# A related resource link, providing access to resource objects
|
3
|
+
# linked in a relationship
|
4
|
+
# @see http://jsonapi.org/format/#document-resource-object-related-resource-links
|
5
|
+
class Relationship
|
6
|
+
include Linkable
|
7
|
+
include Metable
|
8
|
+
|
9
|
+
attr_reader :links, :linkages
|
10
|
+
def initialize(data, doc)
|
11
|
+
@document = doc
|
12
|
+
@links = load_links(data['links'])
|
13
|
+
@linkages = load_linkages(data['data'], doc)
|
14
|
+
@meta = load_meta(data['meta'])
|
15
|
+
end
|
16
|
+
|
17
|
+
# Resolve available linkages and return related resources
|
18
|
+
# @return [Array<Subjoin::Resource>]
|
19
|
+
def lookup
|
20
|
+
return [] unless @document.has_included?
|
21
|
+
@linkages.map{|l| @document.included[l]}
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def load_linkages(data, doc)
|
26
|
+
return [] if data.nil?
|
27
|
+
return [Identifier.new(data['type'], data['id'], data['meta'])] if data.is_a? Hash
|
28
|
+
data.map{|l| Identifier.new(l['type'], l['id'], l['meta'])}
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Subjoin
|
2
|
+
# A JSON-API Resource object
|
3
|
+
# @see http://jsonapi.org/format/#document-resource-objects
|
4
|
+
class Resource
|
5
|
+
include Attributable
|
6
|
+
#include Keyable
|
7
|
+
include Linkable
|
8
|
+
include Metable
|
9
|
+
|
10
|
+
# The relationships specified for the object
|
11
|
+
# @return [Hash<Relationship>]
|
12
|
+
attr_reader :relationships
|
13
|
+
|
14
|
+
attr_reader :identifier
|
15
|
+
|
16
|
+
def initialize(spec, doc = nil)
|
17
|
+
@document = doc
|
18
|
+
if spec.is_a?(URI)
|
19
|
+
data = Subjoin::get(spec)
|
20
|
+
elsif spec.is_a?(Hash)
|
21
|
+
data = spec
|
22
|
+
end
|
23
|
+
|
24
|
+
if data.has_key?("data")
|
25
|
+
data = data["data"]
|
26
|
+
end
|
27
|
+
|
28
|
+
if data.is_a?(Array)
|
29
|
+
raise UnexpectedTypeError.new
|
30
|
+
end
|
31
|
+
|
32
|
+
@identifier = Identifier.new(data['type'], data['id'])
|
33
|
+
|
34
|
+
load_attributes(data['attributes'])
|
35
|
+
@links = load_links(data['links'])
|
36
|
+
@relationships = load_relationships(data['relationships'], @document)
|
37
|
+
@meta = load_meta(data['meta'])
|
38
|
+
end
|
39
|
+
|
40
|
+
# Resource type
|
41
|
+
# @return [String]
|
42
|
+
def type
|
43
|
+
@identifier.type
|
44
|
+
end
|
45
|
+
|
46
|
+
# Resource id
|
47
|
+
# @return [String]
|
48
|
+
def id
|
49
|
+
@identifier.id
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get a related resource or resources. This method resolves the
|
53
|
+
# relationship linkages and fetches the included {Subjoin::Resource}
|
54
|
+
# objects themselves.
|
55
|
+
# @param spec [String] key for the desired resource
|
56
|
+
# @param doc [Subjoin::Document] Document in which to look for related
|
57
|
+
# resources. By default it is the same document from which the resource
|
58
|
+
# came itself.
|
59
|
+
# @return [Hash, Array<Subjoin:Resource>, nil] If called with a spec
|
60
|
+
# parameter, the return value will be an Array of {Subjoin::Resource}
|
61
|
+
# objects corresponding to the key, or nil if that key doesn't exist. If
|
62
|
+
# called without a spec parameter, the return value will be a Hash whose
|
63
|
+
# keys are the same as {Resource#relationships}, but whose values are
|
64
|
+
# Arrays of resolved {Resource} objects. In practice this means that you
|
65
|
+
# have a choice of idioms (method vs. hash) since
|
66
|
+
#
|
67
|
+
# obj.rels("key")
|
68
|
+
#
|
69
|
+
# and
|
70
|
+
#
|
71
|
+
# obj.rels["key"]
|
72
|
+
#
|
73
|
+
# are equivalent
|
74
|
+
def rels(spec = nil, doc = @document)
|
75
|
+
return nil if doc.nil?
|
76
|
+
return nil unless doc.has_included?
|
77
|
+
|
78
|
+
if spec.nil?
|
79
|
+
return Hash[relationships.keys.map{|k| [k, rels(k, doc)]}]
|
80
|
+
end
|
81
|
+
|
82
|
+
relationships[spec].linkages.map{|l| doc.included[l]}
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
def load_relationships(data, doc)
|
87
|
+
return {} if data.nil?
|
88
|
+
|
89
|
+
Hash[data.map{|k, v| [k, Relationship.new(v, doc)]}]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Subjoin::Document do
|
4
|
+
before :all do
|
5
|
+
rsp = JSON.parse(COMPOUND)
|
6
|
+
@doc = Subjoin::Document.new(rsp)
|
7
|
+
|
8
|
+
dataless = JSON.parse(COMPOUND)
|
9
|
+
dataless.delete("data")
|
10
|
+
@nodata = Subjoin::Document.new(dataless)
|
11
|
+
|
12
|
+
excluded = JSON.parse(COMPOUND)
|
13
|
+
excluded.delete("included")
|
14
|
+
@noincl = Subjoin::Document.new(excluded)
|
15
|
+
|
16
|
+
linkless = JSON.parse(COMPOUND)
|
17
|
+
linkless.delete("links")
|
18
|
+
@nolinks = Subjoin::Document.new(linkless)
|
19
|
+
|
20
|
+
metaless = JSON.parse(COMPOUND)
|
21
|
+
metaless.delete("meta")
|
22
|
+
@nometa = Subjoin::Document.new(metaless)
|
23
|
+
|
24
|
+
versionless = JSON.parse(COMPOUND)
|
25
|
+
versionless.delete("jsonapi")
|
26
|
+
@noversion = Subjoin::Document.new(versionless)
|
27
|
+
|
28
|
+
@simple = Subjoin::Document.new(JSON.parse(ARTICLE))
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#data" do
|
32
|
+
context "when there is primary data" do
|
33
|
+
it "is an Array" do
|
34
|
+
expect(@doc.data).to be_an_instance_of Array
|
35
|
+
end
|
36
|
+
|
37
|
+
it "contains an expected Resource" do
|
38
|
+
expect(@doc.data.first["title"]).to eq "JSON API paints my bikeshed!"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when there is no primary data" do
|
43
|
+
it "is nil" do
|
44
|
+
expect(@nodata.data).to be_nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "when there is a single object in primary data" do
|
49
|
+
it "is still an Array" do
|
50
|
+
expect(@simple.data).to be_an_instance_of Array
|
51
|
+
end
|
52
|
+
|
53
|
+
it "has one element" do
|
54
|
+
expect(@simple.data.count).to eq 1
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#has_data?" do
|
60
|
+
context "when there is primary data" do
|
61
|
+
it "returns true" do
|
62
|
+
expect(@doc.has_data?).to be true
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "when there is no primary data" do
|
67
|
+
it "returns false" do
|
68
|
+
expect(@nodata.has_data?).to be false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
describe "#included" do
|
75
|
+
context "when there are included resources" do
|
76
|
+
it "is an Array" do
|
77
|
+
expect(@doc.included).to be_an_instance_of Subjoin::Inclusions
|
78
|
+
end
|
79
|
+
|
80
|
+
it "contains an expected Resource" do
|
81
|
+
expect(@doc.included[0].type).to eq "people"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context "when there are no included resources" do
|
86
|
+
it "is nil" do
|
87
|
+
expect(@noincl.included).to be_nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#has_included?" do
|
93
|
+
context "when there are included resources" do
|
94
|
+
it "returns true" do
|
95
|
+
expect(@doc.has_included?).to be true
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "when there are no included resources" do
|
100
|
+
it "returns false" do
|
101
|
+
expect(@noincl.has_included?).to be false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "#links" do
|
107
|
+
context "when there are links" do
|
108
|
+
it "is a Hash of Link objects" do
|
109
|
+
expect(@doc.links.map{|k, v| v.class}.uniq).to eq [Subjoin::Link]
|
110
|
+
end
|
111
|
+
|
112
|
+
it "has an expected link" do
|
113
|
+
expect(@doc.links["related"].to_s).to eq "http://jsonapi.org"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "when there are no links" do
|
118
|
+
it "is nil" do
|
119
|
+
expect(@nolinks.links).to be_nil
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#has_links?" do
|
125
|
+
context "when there are links" do
|
126
|
+
it "returns true" do
|
127
|
+
expect(@doc.has_links?).to be true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context "when there no links" do
|
132
|
+
it "returns false" do
|
133
|
+
expect(@nolinks.has_links?).to be false
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#meta" do
|
139
|
+
context "when there is meta information" do
|
140
|
+
it "is a Meta object" do
|
141
|
+
expect(@doc.meta).to be_an_instance_of(Subjoin::Meta)
|
142
|
+
end
|
143
|
+
|
144
|
+
it "has an expected attribute" do
|
145
|
+
expect(@doc.meta["category"]).to eq "Example response"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context "when there is no meta information" do
|
150
|
+
it "is nil" do
|
151
|
+
expect(@nometa.meta).to be nil
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe "#has_meta?" do
|
157
|
+
context "when there is meta information" do
|
158
|
+
it "returns true" do
|
159
|
+
expect(@doc.has_meta?).to be true
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context "when there is no meta information" do
|
164
|
+
it "returns false" do
|
165
|
+
expect(@nometa.has_meta?).to be false
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "#jsonapi" do
|
171
|
+
context "when there is version information" do
|
172
|
+
it "is a JsonApi object" do
|
173
|
+
expect(@doc.jsonapi).to be_an_instance_of(Subjoin::JsonApi)
|
174
|
+
end
|
175
|
+
|
176
|
+
it "has an expected attribute" do
|
177
|
+
expect(@doc.jsonapi.version).to eq "1.0"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context "when there is no version information" do
|
182
|
+
it "is nil" do
|
183
|
+
expect(@noversion.jsonapi).to be nil
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe "#has_jsonapi?" do
|
189
|
+
context "when there is version information" do
|
190
|
+
it "returns true" do
|
191
|
+
expect(@doc.has_jsonapi?).to be true
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context "when there is no version information" do
|
196
|
+
it "returns false" do
|
197
|
+
expect(@noversion.has_jsonapi?).to be false
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "instantiated with a URI" do
|
203
|
+
it "succeeds" do
|
204
|
+
allow_any_instance_of(Faraday::Connection).
|
205
|
+
to receive(:get).and_return(double(Faraday::Response, :body => ARTICLE))
|
206
|
+
expect(Subjoin::Document.new(URI("http://example.com/articles"))).
|
207
|
+
to be_an_instance_of(Subjoin::Document)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Subjoin::Identifier do
|
4
|
+
before :each do
|
5
|
+
data = JSON.parse(ARTICLE)['data']['relationships']['author']
|
6
|
+
@id = Subjoin::Identifier.
|
7
|
+
new(data['data']['type'], data['data']['id'], data['meta'])
|
8
|
+
end
|
9
|
+
|
10
|
+
it "has a type" do
|
11
|
+
expect(@id.type).to eq "people"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "has an id" do
|
15
|
+
expect(@id.id).to eq "9"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "has a Meta object" do
|
19
|
+
expect(@id.meta).to be_an_instance_of Subjoin::Meta
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "equality" do
|
23
|
+
it "is equal if #type and #id are the same" do
|
24
|
+
other = Subjoin::Identifier.new("people", "9")
|
25
|
+
expect(@id == other).to be true
|
26
|
+
end
|
27
|
+
|
28
|
+
it "is other not equal" do
|
29
|
+
other = Subjoin::Identifier.new("schmeople", "9")
|
30
|
+
expect(@id == other).to be false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Subjoin::Inclusions do
|
4
|
+
before :all do
|
5
|
+
@doc = Subjoin::Document.new(JSON.parse(COMPOUND))
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#all" do
|
9
|
+
it "returns an Array" do
|
10
|
+
expect(@doc.included.all).to be_an_instance_of Array
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#first" do
|
15
|
+
it "returns a Resource" do
|
16
|
+
expect(@doc.included.first).to be_an_instance_of Subjoin::Resource
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#[]" do
|
21
|
+
context "when passed an Identifier" do
|
22
|
+
context "when the Identifier matches something included" do
|
23
|
+
it "returns a Resource" do
|
24
|
+
id = Subjoin::Identifier.new("people", "9")
|
25
|
+
expect(@doc.included[id]).to be_an_instance_of Subjoin::Resource
|
26
|
+
end
|
27
|
+
|
28
|
+
it "returns to expected Resource" do
|
29
|
+
id = Subjoin::Identifier.new("people", "9")
|
30
|
+
expect(@doc.included[id]['twitter']).to eq "dgeb"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when passed an Array" do
|
36
|
+
context "when the Identifier matches something included" do
|
37
|
+
it "returns a Resource" do
|
38
|
+
expect(@doc.included[["people", "9"]]).
|
39
|
+
to be_an_instance_of Subjoin::Resource
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns to expected Resource" do
|
43
|
+
expect(@doc.included[["people", "9"]]['twitter']).to eq "dgeb"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "when passed a Fixnum" do
|
49
|
+
context "when the Identifier matches something included" do
|
50
|
+
it "returns a Resource" do
|
51
|
+
expect(@doc.included[0]).
|
52
|
+
to be_an_instance_of Subjoin::Resource
|
53
|
+
end
|
54
|
+
|
55
|
+
it "returns to expected Resource" do
|
56
|
+
expect(@doc.included[["people", "9"]]['twitter']).to eq "dgeb"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when nothing matched" do
|
62
|
+
it "returns nil" do
|
63
|
+
expect(@doc.included[9]).
|
64
|
+
to be_nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|