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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c16909a79d122afbd6289561e3f503e179d44734
4
+ data.tar.gz: 1b036b2b58b8a7c6726e8d249519c8edbafa9c32
5
+ SHA512:
6
+ metadata.gz: 6255701286835abd2de5b460ecbac170d97c4aa696762b5f48b5922e34655608fec42d1d3752015a5e2f1b8a9d725c7f2199bd6b06ea5b97cb643aaeb8660ef4
7
+ data.tar.gz: 389a8f8796ef43e13ef4abc58cbfb0e4cc82156c148301ff4485fe6e19dac966205019efd3f094f9d27bab5842ba1483cd18103cb17c617d83bb87c7657d403a
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *~
data/.travis.yml ADDED
@@ -0,0 +1,23 @@
1
+ language: ruby
2
+ rvm:
3
+ - ruby-head
4
+ - jruby-head
5
+ - 2.1.1
6
+ - 2.0.0
7
+ - 1.9.3
8
+ - 1.9.2
9
+ - jruby-19mode
10
+ - jruby-18mode
11
+ - rbx-2.1.1
12
+ - rbx-2.0.0
13
+ - 1.8.7
14
+ matrix:
15
+ allow_failures:
16
+ - rvm: rbx-2.1.1
17
+ - rvm: rbx-2.0.0
18
+ - rvm: 1.8.7
19
+ - rvm: jruby-head
20
+ - rvm: jruby-18mode
21
+ addons:
22
+ code_climate:
23
+ repo_token: 56b7b3204fa5160be4b85c3777da27214d517c3874ca238a473f76f0b83f3f69
data/.yardops ADDED
@@ -0,0 +1 @@
1
+ -m markdown
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "codeclimate-test-reporter", group: :test, require: nil
4
+
5
+ # Specify your gem's dependencies in subjoin.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Sean Redmond
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,431 @@
1
+ # Subjoin
2
+
3
+ A practical wrapper for [JSON-API](http://jsonapi.org) interactions.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'subjoin'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install subjoin
20
+
21
+ ## Documentation
22
+
23
+ For full documentation run
24
+
25
+ $ yardoc
26
+ $ yard server
27
+
28
+ Then load http://localhost:8808 in your browser
29
+
30
+ ## Usage
31
+
32
+ ### Document
33
+
34
+ Everything starts with a document, specifically a `Subjoin::Document` -- the equivalent of a [JSON-API document](http://jsonapi.org/format/) which you
35
+ can create with a URI:
36
+
37
+ require "subjoin"
38
+ doc = Subjoin::Document.new(URI("http://example.com/articles"))
39
+
40
+ (all examples here based on examples in the
41
+ [JSON-API documentation](http://jsonapi.org/format/))
42
+
43
+ Note that you must pass a
44
+ [URI object](http://ruby-doc.org/stdlib-2.2.2/libdoc/uri/rdoc/URI.html). A
45
+ string would be interpreted as a JSON-API `type`.
46
+
47
+ A `Subjoin::Document` probably has "primary data" which, if present is an Array
48
+ of `Subjoin::Resource` objects:
49
+
50
+ doc.has_data? # true if there is primary data
51
+ => true
52
+ doc.data # Array of Subjoin::Resource objects
53
+ doc.data.first # One resource
54
+
55
+ The `data` member of a JSON-API document can be either a single resource object
56
+ or an array of resource objects. `Subjoin::Document#data` always returns an
57
+ Array. In a document with a single resource object, the Array will have one
58
+ element.
59
+
60
+ You can access all the other members of the [top-level document](http://jsonapi.org/format/#document-top-level) (all the objects returned are covered below):
61
+
62
+ doc.links # Hash of Link objects
63
+ doc.included # Inclusions object
64
+ doc.meta # Meta object
65
+ doc.jsonapi # JsonApi object
66
+
67
+ There are, in addition, methods to test whether any of the above members are
68
+ present:
69
+
70
+ doc.has_data?
71
+ doc.has_links?
72
+ doc.has_included?
73
+ doc.has_meta?
74
+ doc.has_jsonapi?
75
+
76
+ ### Resources
77
+
78
+ Every `Subjoin::Resource` has a `type` and `id`. The JSON response:
79
+
80
+ {
81
+ "data": {
82
+ "type": "articles",
83
+ "id": "1",
84
+ "attributes": {
85
+ "title": "JSON API paints my bikeshed!"
86
+ },
87
+ ...
88
+ }
89
+
90
+ would correspond to:
91
+
92
+ article = doc.data.first
93
+ article.type
94
+ => "articles"
95
+ article.id
96
+ => "1"
97
+
98
+ The attributes of a `Subjoin::Resource` object, or any object that
99
+ includes `Subjoin::Attributable`, can be accessed like hash on the
100
+ object itself:
101
+
102
+ article["title"]
103
+ => "JSON API paints my bikeshed!"
104
+
105
+ You can also get the entire attributes Hash as
106
+ `Subjoin::Attributable#attributes`:
107
+
108
+ article.attributes # Hash
109
+ article.attributes.keys
110
+ => ["title"]
111
+ article.attributes["title"]
112
+ => "JSON API paints my bikeshed!"
113
+
114
+ The other expected members of a
115
+ [resource object](http://jsonapi.org/format/#document-resource-objects) are
116
+ available. The objects returned by these methods are all explained below:
117
+
118
+ article.links # Hash of Link objects
119
+ article.relationships # Array of Relationship objects
120
+ article.meta # Meta object
121
+
122
+ As with `Subjoin::Document`, there are methods to see if any of the above are available
123
+
124
+ article.has_links?
125
+ article.has_meta?
126
+
127
+ ### Links
128
+
129
+ `Subjoin::Document`, `Subjoin::Resource`, and `Subjoin::Relationship` can all
130
+ have [links](http://jsonapi.org/format/#document-links). They all have
131
+ the `Subjoin::Linkable#links` method which returns a Hash of `Subjoin::Link`
132
+ objects:
133
+
134
+ article.links.keys
135
+ => ["self"]
136
+ article.links["self"].href.to_s
137
+ => "http://example.com/articles/1"
138
+
139
+ JSON-API allows for two link object formats. One simply has a link
140
+
141
+ "links": {
142
+ "self": "http://example.com/articles/1"
143
+ }
144
+
145
+ and one has an `href` attribute and `meta` object:
146
+
147
+ "links": {
148
+ "related": {
149
+ "href": "http://example.com/articles/1/comments",
150
+ "meta": {
151
+ "count": 10
152
+ }
153
+ }
154
+ }
155
+
156
+ Subjoin treats either variation like the latter:
157
+
158
+ article.links["self"].href.to_s
159
+ => "http://example.com/articles/1"
160
+ article.links["self"].has_meta?
161
+ => false
162
+ article.links["self"].meta?
163
+ => nil
164
+
165
+ article.links["related"].href.to_s
166
+ => "http://example.com/articles/1/comments"
167
+ article.links["related"].has_meta?
168
+ => true
169
+ article.links["related"].meta["count"]
170
+ => 10
171
+
172
+ Note that the `href` is always returned as a `URI` object. If you have a `Subjoin::Link` you can get the corresponding `Subjoin::Document`:
173
+
174
+ article.links["related"].get # Same thing as Subjoin::Document.new
175
+ # with the URL
176
+
177
+ ### Resource Identifiers
178
+
179
+ Before getting to relationships, we should take a minute to look at
180
+ [resource identifiers](http://jsonapi.org/format/#document-resource-identifier-objects). Above,
181
+ we saw that every `Subjoin::Resource` has a `type` and `id`.
182
+
183
+ article.type # "articles"
184
+ article.id # "1"
185
+
186
+ Though the above attributes exist individually, these two attributes
187
+ work together as a compound key and are, in fact put together in
188
+ Subjoin as a `Subjoin::Identifier` object:
189
+
190
+ article.identifier # Identifier object
191
+ article.identifier.type # "articles"
192
+ article.identifier.id # "1"
193
+
194
+ `Subjoin::Identifier` objects are used for equality: two
195
+ `Subjoin::Resource` objects are considered equal if they have equal
196
+ `Identifer`s:
197
+
198
+ article1 == article2 # Really tests...
199
+ article1.identifier == article2.identifier # Really tests...
200
+ article1.identifier.type == article2.identifier.type &&
201
+ article1.identifer.id == article2.identifier.id
202
+
203
+ More importantly, identifiers occur in Relationship objects as
204
+ pointers to other resources. These pointers are called
205
+ [linkages](http://jsonapi.org/format/#document-resource-object-linkage):
206
+
207
+ article.relationships.author.linkages # Array of Identifier objects
208
+
209
+ They may have, optionally, a `meta` attribute as well, and `meta`
210
+ attributes are ignored in tests for equality.
211
+
212
+ ### Relationships, Linkages, Included Resources
213
+
214
+ Okay, now we can get to how you'll really use JSON-API resources and why you would JSON-API over other options: resource linking and included resources.
215
+
216
+ In many RESTful APIs, resources have embedded child resources which
217
+ is, in my experience, a principle source of the bikeshedding arguments
218
+ that JSON-API tries to avoid ("should X be a child of Y, or Y a child
219
+ of X?" "How should the X response be different when it is achild of
220
+ another resource?"). Instead of nesting and embedding, in JSON-API
221
+ resources may have
222
+ [relationships](http://jsonapi.org/format/#document-resource-object-relationships)
223
+ to other resources.
224
+
225
+ article.relationships # A Hash of Subjoin::Relationship objects
226
+ article.relationships.keys
227
+ => ["author", "comments"]
228
+
229
+ This much tells you that an "article" can have an "author" and
230
+ "comments". In Subjoin, relationships are instantiated as
231
+ `Subjoin::Relationship` objects whose two important properties are
232
+ `links` and `linkages`. `Subjoin::Relationship` are
233
+ `Subjoin::Linkable` so `links` works as it does in other objects.
234
+
235
+ author = article.relationships["author"] # Relationship object
236
+ author.links.keys
237
+ => ["self", "related"]
238
+ author.links["related"].to_s
239
+ => "http://example.com/articles/1/author"
240
+ author.links["related"].get # Get a new Document
241
+
242
+ Alongside `links`, `linkages` give you resource identifiers for the
243
+ related resources. while the "comments" `link` tells us how to get a
244
+ document with all the related comments:
245
+
246
+ comments.links["self"].to_s
247
+ => "http://example.com/articles/1/relationships/comments"
248
+
249
+ The corresponding `linkages` returns an Array of
250
+ `Subjoin::Identifier` that are pointers to specific resources:
251
+
252
+ comments = article.relationships["comments"]
253
+ comments.linkages.count
254
+ => 2
255
+
256
+ This tells us that there are two related comments. If we look at one,
257
+ we can get the type and id:
258
+
259
+ comments.linkages[0].type
260
+ => "comments"
261
+ comments.linkages[0].id
262
+ => "5"
263
+
264
+ So far so good, but now what? [Inclusion](http://jsonapi.org/format/#fetching-includes)
265
+
266
+ With Subjoin, you can request that these related resources be included
267
+ in the document, one of three ways:
268
+
269
+ # URI parameters
270
+ doc = Subjoin::Document.new(URI("http://example.com/articles/1?include=author,comments"))
271
+
272
+ # Parameters Hash with a string
273
+ doc = Subjoin::Document.new(URI("http://example.com/article/1s", {"include" => "author,comments"}))
274
+
275
+ # Parameters Hash with an array of strings
276
+ doc = Subjoin::Document.new(URI("http://example.com/articles/1", {"include" => ["author" ,"comments"]}))
277
+
278
+ All three are equivalent. The array of strings version works especially well with relationship keys
279
+
280
+ doc2 = Subjoin::Document.new(URI("http://example.com/articles/1", {"include" => articles.relationships.keys}))
281
+
282
+ When a document is requested with included resources, they can be accessed via `included`
283
+
284
+ # Get the document
285
+ doc = Subjoin::Document.new(URI("http://example.com/articles/1", {"include" => ["author" ,"comments"]}))
286
+
287
+ # Get the article
288
+ article = doc.data.first
289
+
290
+ # Get the realted author identifier
291
+ auth_identifier = article.related["author"].linkages.first
292
+ auth_identifier.type
293
+ => "people"
294
+ auth_identifier.id
295
+ => "9"
296
+
297
+ # Get the embedded resource
298
+ doc.has_included?
299
+ => true
300
+
301
+ # Look up included resource by identifier
302
+ author = doc.included[auth_identifier]
303
+
304
+ # Now we have access to the whole author resource
305
+ author.type
306
+ => "people"
307
+ author.id
308
+ => "9"
309
+ author["twitter"]
310
+ => "dgeb"
311
+
312
+ If that sounds kind of complicated, it is. But you can...
313
+
314
+ ### Let Subjoin resolve the linkages for you
315
+
316
+ To make all this easier, `Subjoin::Resource` provides a `rels` method that does all this under the hood:
317
+
318
+ article.rels.keys
319
+ => ["author", "comments"]
320
+ author = article.rels["author"] # Returns the author Resource
321
+ author["twitter"]
322
+ => "dgeb"
323
+
324
+ ### Meta Information
325
+
326
+ [Meta information](http://jsonapi.org/format/#document-meta) is represented by
327
+ ```Subjoin::Meta``` objects. Any object that might have meta information will
328
+ have a ```#meta``` attribute. ```Meta``` object attributes are accessible by
329
+ name like other attributes.
330
+
331
+ Given this JSON:
332
+
333
+ {
334
+ "meta": {
335
+ "copyright": "Copyright 2015 Example Corp.",
336
+ "authors": [
337
+ "Yehuda Katz",
338
+ "Steve Klabnik",
339
+ "Dan Gebhardt",
340
+ "Tyler Kellen"
341
+ ]
342
+ },
343
+ "data": {
344
+ // ...
345
+ }
346
+ }
347
+
348
+ The data might be accessed in this way:
349
+
350
+ article.meta # Meta object
351
+ article.meta.copyright # "Copyright 2015 Example Corp."
352
+
353
+ ## Using Inheritance
354
+
355
+ Another way to use Subjoin is via inheritance. Using this approach you
356
+ create your own classes to represent JSON-API resource types of a
357
+ specific JSON-API server implementation. These classes must be
358
+ sub-classes of `Subjoin::Resource` and must include
359
+ `Subjoin::Inheritable`, which adds some constants and methods that
360
+ `Subjoin::Document` will use to instantiate objects correctly. Next
361
+ you must override a class variable, `ROOT_URI`, which should be the
362
+ root of all URIs of the API. For instance, in the examples above,
363
+ "http://example.com" would be the value of `ROOT_URI`. By default,
364
+ Subjoin will use the lower-cased name of the class as the type in
365
+ URIs. If the class name does not match the type, you can further
366
+ override `TYPE_PATH` to indicate the name (or longer URI fragment)
367
+ that should be used in URIs to request the resource type.
368
+
369
+ Your custom classes must also be part of the ```Subjoin``` module. You
370
+ should probably create one sub-class of `Subjoin::Resource` that
371
+ overrides `ROOT_URI`, and then create other classes as sub-classes of
372
+ this:
373
+
374
+ require "subjoin"
375
+
376
+ module Subjoin
377
+ # Use this class as the parent of further subclasses.
378
+ # They will inherit the ROOT_URI defined here
379
+ class ExampleResource < Subjoin::Resource
380
+ include Inheritable
381
+ ROOT_URI="http://example.com"
382
+ end
383
+
384
+ # Subjoin will make requests to http://example.com/articles
385
+ class Articles < ExampleResource
386
+ end
387
+
388
+ # Use TYPE_PATH if you don't want to name the class the same thing as
389
+ # the type
390
+ class ArticleComments < ExampleResource
391
+ TYPE_PATH="comments"
392
+ end
393
+ end
394
+
395
+ Now, when you get a document, the resources in it will be mapped to an available type:
396
+
397
+ doc = Subjoin::Document.new(URI("http://example.com/articles/1", {"include" => ["author" ,"comments"]}))
398
+
399
+ # We mapped the article type to the Article class
400
+ article = doc.data.first
401
+ article.class
402
+ => Subjoin::Article
403
+
404
+ # We mapped the comment type to the ArticleComment class
405
+ comment = article.rels["comments"].first
406
+ comment.class
407
+ => Subjoin::ArticleComment
408
+
409
+ # We didn't map the person type to anything, so we get a Resource
410
+ author = article.rels["author"].first
411
+ author.class
412
+ => Subjoin::Resource
413
+
414
+ ## Limitations
415
+
416
+ Subjoin currently only supports GET requests.
417
+
418
+ Some features, such as pagination, should work generically but could
419
+ have more convenient methods to handle them.
420
+
421
+ ## Why is it called "Subjoin"
422
+
423
+ Nice word. Sounds coder-y. Has most of the letters of "Ruby JSON-API".
424
+
425
+ ## Contributing
426
+
427
+ 1. Fork it ( https://github.com/seanredmond/subjoin/fork )
428
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
429
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
430
+ 4. Push to the branch (`git push origin my-new-feature`)
431
+ 5. Create a new Pull Request