resty 0.2.2 → 0.3.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.
- data/README.md +86 -0
- data/lib/resty.rb +43 -0
- data/lib/resty/attributes.rb +44 -23
- metadata +11 -8
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
Resty
|
2
|
+
=====
|
3
|
+
|
4
|
+
What is Resty about?
|
5
|
+
--------------------
|
6
|
+
|
7
|
+
Resty is designed as a client for a particular type of discoverable REST API; one that returns JSON, and where that JSON
|
8
|
+
has particular keys which enable navigation of the data graph.
|
9
|
+
|
10
|
+
An example of a Resty response
|
11
|
+
-------------------------------
|
12
|
+
|
13
|
+
A GET made to:
|
14
|
+
|
15
|
+
http://fishy.fish/fish/123
|
16
|
+
|
17
|
+
Might return the following JSON:
|
18
|
+
|
19
|
+
{
|
20
|
+
':href': 'http://fishy.fish/fish/123',
|
21
|
+
'tag_number': '3987349834',
|
22
|
+
'name': 'Bob the Fish'
|
23
|
+
'species' => {
|
24
|
+
':href' => 'http://species.fish/species/555'
|
25
|
+
},
|
26
|
+
'habitat' => {
|
27
|
+
':href' => 'http://fishy.fish/oceans/atlantic',
|
28
|
+
'name' => 'Atlantic Ocean'
|
29
|
+
'size' => 'quite large'
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
- The ':href' indicates the URL this resource is available at. In this case the URL is the one we requested, which is the
|
34
|
+
normal case.
|
35
|
+
|
36
|
+
- The 'species' key is a link to another resource. If a GET is made to that URL, more information about that species will
|
37
|
+
be available. No other information about the species is available without doing that extra request. It's also on a different
|
38
|
+
domain, which doesn't matter to Resty at all.
|
39
|
+
|
40
|
+
- The 'habitat' key is an example of a link containing the information you'll find at the link; this means that it's not
|
41
|
+
necessary to do the extra request. Generally this is done if it's always known that the related resources will be required.
|
42
|
+
|
43
|
+
Accessing the simple example with Resty
|
44
|
+
---------------------------------------
|
45
|
+
|
46
|
+
Given the above resource, you could execute the following code in IRB:
|
47
|
+
|
48
|
+
ruby-1.9.2-p180 :001 > require 'resty'
|
49
|
+
=> true
|
50
|
+
ruby-1.9.2-p180 :002 > fish = Resty.href('http://fishy.fish/fish/123')
|
51
|
+
=> #<Resty:0xa5ba70c ...>
|
52
|
+
|
53
|
+
At this stage, no requests will be made. Once you start look at the properties of "fish", a request will be made to
|
54
|
+
GET the resource.
|
55
|
+
|
56
|
+
ruby-1.9.2-p180 :003 > fish.name
|
57
|
+
=> "Bob the Fish"
|
58
|
+
ruby-1.9.2-p180 :004 > fish.habitat
|
59
|
+
=> #<Resty:0xa590a24 ...>
|
60
|
+
ruby-1.9.2-p180 :005 > fish.habitat.name
|
61
|
+
=> "Atlantic Ocean"
|
62
|
+
|
63
|
+
Navigating the resource graph
|
64
|
+
-----------------------------
|
65
|
+
|
66
|
+
Looking at "habitat" didn't require extra requests (because the data for "habitat" was already populated in the first request),
|
67
|
+
but looking at "species" will require us to fetch that resource. Let's assume that the species GET returns the following JSON:
|
68
|
+
|
69
|
+
{
|
70
|
+
':href' => 'http://species.fish/species/555',
|
71
|
+
'commonName' => 'Atlantic Salmon',
|
72
|
+
'scientificName' => 'Salmo salar'
|
73
|
+
'kingdom' => {
|
74
|
+
':href' => 'http://species.fish/kingdom/223'
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
Then the following will happen; notice that "common_name" is translated to "commonName".
|
79
|
+
|
80
|
+
ruby-1.9.2-p180 :006 > fish.species.common_name
|
81
|
+
=> "Atlantic Salmon"
|
82
|
+
|
83
|
+
Now assume that the kingdom GET returns something sensible; we can do the following, chaining our method calls:
|
84
|
+
|
85
|
+
ruby-1.9.2-p180 :007 > fish.species.kingdom.scientific_name
|
86
|
+
=> "Animalia"
|
data/lib/resty.rb
CHANGED
@@ -2,29 +2,37 @@ require 'rest-client'
|
|
2
2
|
require 'json'
|
3
3
|
require 'base64'
|
4
4
|
|
5
|
+
# @author Simon Russell
|
5
6
|
class Resty
|
6
7
|
include Enumerable
|
7
8
|
|
9
|
+
# @note Generally, use Resty::from instead of Resty::new.
|
8
10
|
def initialize(attributes)
|
9
11
|
@attributes = attributes
|
10
12
|
end
|
11
13
|
|
14
|
+
# @return [String, nil] The URL of the resource, or nil if none present.
|
12
15
|
def _href
|
13
16
|
@attributes.href
|
14
17
|
end
|
15
18
|
|
19
|
+
# The data from the resource; will cause the resource to be loaded if it hasn't already occurred.
|
20
|
+
# @return [Hash] The data from the resource
|
16
21
|
def _populated_data
|
17
22
|
@attributes.populated_data
|
18
23
|
end
|
19
24
|
|
25
|
+
# Make respond_to? return true to the corresponding magic methods added using {#method_missing}.
|
20
26
|
def respond_to_missing?(name, include_private)
|
21
27
|
@attributes.key?(name.to_s)
|
22
28
|
end
|
23
29
|
|
30
|
+
# @return [String, nil] The URL of the resource, encoded for safe insertion into a URL. Used for Rails routing.
|
24
31
|
def to_param
|
25
32
|
Resty.encode_param(_href)
|
26
33
|
end
|
27
34
|
|
35
|
+
# Iterate through each item in the array; doesn't iterate over keys in the hash.
|
28
36
|
def each
|
29
37
|
@attributes.items.each do |x|
|
30
38
|
yield x
|
@@ -35,6 +43,31 @@ class Resty
|
|
35
43
|
@attributes.items[index]
|
36
44
|
end
|
37
45
|
|
46
|
+
# Resty exposes the values and actions from the resource as methods on the object.
|
47
|
+
#
|
48
|
+
# @example Reading an attribute value
|
49
|
+
# r = Resty.href('http://fish.fish/123') # { ':href': 'http://fish.fish/123', 'name': 'Bob' }
|
50
|
+
# r.name # => "Bob"
|
51
|
+
#
|
52
|
+
# @example Checking a boolean value
|
53
|
+
# r = Resty.href('http://fish.fish/123') # { ':href': 'http://fish.fish/123', 'fun': true }
|
54
|
+
# r.fun? # => true
|
55
|
+
#
|
56
|
+
# @example Calling an action on the resource
|
57
|
+
# r = Resty.href('http://fish.fish/123') # {
|
58
|
+
# # ':href': 'http://fish.fish/123',
|
59
|
+
# # ':actions': {
|
60
|
+
# # 'cook': {
|
61
|
+
# # ':href': 'http://fish.fish/123/cook'
|
62
|
+
# # ':method': 'POST'
|
63
|
+
# # }
|
64
|
+
# # }
|
65
|
+
# # }
|
66
|
+
# r.cook!
|
67
|
+
#
|
68
|
+
# @example Calling an action on the resource with params
|
69
|
+
# r = Resty.href('http://fish.fish/123')
|
70
|
+
# r.cook!(style: 'lightly', flavoursomeness: 12) # this will cause those parameters to be posted with the action
|
38
71
|
def method_missing(name, *args)
|
39
72
|
if name =~ /^(.+)!$/
|
40
73
|
if @attributes.actions.exist?($1)
|
@@ -55,10 +88,14 @@ class Resty
|
|
55
88
|
super.gsub('>', _href ? " #{_href}>" : " no-href>")
|
56
89
|
end
|
57
90
|
|
91
|
+
# @return [Resty] A new Resty constructed from the given hash.
|
92
|
+
# @example
|
93
|
+
# r = Resty.from('name' => 'Bob')
|
58
94
|
def self.from(data)
|
59
95
|
new(Resty::Attributes.new(data))
|
60
96
|
end
|
61
97
|
|
98
|
+
# @return The input object, or a Resty wrapping if it's a Hash or Array.
|
62
99
|
def self.wrap(object)
|
63
100
|
case object
|
64
101
|
when Hash
|
@@ -70,18 +107,24 @@ class Resty
|
|
70
107
|
end
|
71
108
|
end
|
72
109
|
|
110
|
+
# @return [Resty] A new Resty pointing at the given URL.
|
111
|
+
# @example
|
112
|
+
# r = Resty.href('http://fish.fish/')
|
73
113
|
def self.href(href)
|
74
114
|
from(':href' => href)
|
75
115
|
end
|
76
116
|
|
117
|
+
# @return [String] The input encoded for safe use in a URL.
|
77
118
|
def self.encode_param(s)
|
78
119
|
s && Base64.urlsafe_encode64(s.to_s)
|
79
120
|
end
|
80
121
|
|
122
|
+
# @return [String] The input (previously encoded using Resty::encode_param) decoded into a string.
|
81
123
|
def self.decode_param(s)
|
82
124
|
s && Base64.urlsafe_decode64(s.to_s)
|
83
125
|
end
|
84
126
|
|
127
|
+
# @return [Resty] A new Resty created from the URL encoded in the input.
|
85
128
|
def self.from_param(s)
|
86
129
|
href(decode_param(s))
|
87
130
|
end
|
data/lib/resty/attributes.rb
CHANGED
@@ -4,34 +4,34 @@ class Resty::Attributes
|
|
4
4
|
|
5
5
|
def initialize(data)
|
6
6
|
@href = data[':href']
|
7
|
-
@
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
@fully_populated = if @href
|
8
|
+
!data[':partial'] && data.length > 1
|
9
|
+
else
|
10
|
+
true
|
11
|
+
end
|
12
12
|
|
13
13
|
@data = data
|
14
14
|
@wrapped = {}
|
15
15
|
end
|
16
16
|
|
17
17
|
def key?(name)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def translate_key(name)
|
23
|
-
populate! unless populated?
|
24
|
-
if @data.key?(name)
|
25
|
-
name
|
26
|
-
elsif @data.key?(camelized_name = camelize_key(name))
|
27
|
-
camelized_name
|
18
|
+
until_populated do
|
19
|
+
key_variants(name) do |key|
|
20
|
+
return true
|
21
|
+
end
|
28
22
|
end
|
29
|
-
end
|
30
23
|
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
31
27
|
def [](name)
|
32
|
-
|
33
|
-
|
34
|
-
|
28
|
+
until_populated do
|
29
|
+
key_variants(name) do |key|
|
30
|
+
return wrap_data(key)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
nil
|
35
35
|
end
|
36
36
|
|
37
37
|
def items
|
@@ -39,13 +39,13 @@ class Resty::Attributes
|
|
39
39
|
@wrapped[':items'] ||= (@data[':items'] || []).map { |item| Resty.wrap(item) }
|
40
40
|
end
|
41
41
|
|
42
|
-
def populated?
|
43
|
-
@
|
42
|
+
def populated?(key = nil)
|
43
|
+
@fully_populated
|
44
44
|
end
|
45
45
|
|
46
46
|
def populate!
|
47
47
|
new_data = Resty::Transport.request_json(@href)
|
48
|
-
|
48
|
+
|
49
49
|
@data = case new_data
|
50
50
|
when Array
|
51
51
|
{ ':href' => @href, ':items' => new_data }
|
@@ -53,7 +53,8 @@ class Resty::Attributes
|
|
53
53
|
new_data
|
54
54
|
end
|
55
55
|
|
56
|
-
@
|
56
|
+
@fully_populated = true
|
57
|
+
@wrapped = {}
|
57
58
|
end
|
58
59
|
|
59
60
|
def populated_data
|
@@ -72,8 +73,28 @@ class Resty::Attributes
|
|
72
73
|
|
73
74
|
private
|
74
75
|
|
76
|
+
def wrap_data(key)
|
77
|
+
@wrapped[key] ||= Resty.wrap(@data[key])
|
78
|
+
end
|
79
|
+
|
75
80
|
def camelize_key(key)
|
76
81
|
key.gsub(/_([a-z])/) { $1.upcase }
|
77
82
|
end
|
83
|
+
|
84
|
+
def key_variants(name)
|
85
|
+
yield name if @data.key?(name)
|
86
|
+
|
87
|
+
camelized_name = camelize_key(name)
|
88
|
+
yield camelized_name if camelized_name != name && @data.key?(camelized_name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def until_populated
|
92
|
+
yield
|
93
|
+
|
94
|
+
unless populated?
|
95
|
+
populate!
|
96
|
+
yield
|
97
|
+
end
|
98
|
+
end
|
78
99
|
|
79
100
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resty
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -14,7 +14,7 @@ default_executable:
|
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rest-client
|
17
|
-
requirement: &
|
17
|
+
requirement: &79995340 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ~>
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: 1.6.3
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *79995340
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: json
|
28
|
-
requirement: &
|
28
|
+
requirement: &79995100 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ~>
|
@@ -33,20 +33,23 @@ dependencies:
|
|
33
33
|
version: 1.5.1
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
37
|
-
description:
|
38
|
-
|
36
|
+
version_requirements: *79995100
|
37
|
+
description: ! " Resty is designed as a client for a particular
|
38
|
+
type of discoverable REST API; one that returns JSON, and \n where
|
39
|
+
that JSON has particular keys which enable navigation of the data graph.\n"
|
40
|
+
email: spam+resty@bellyphant.com
|
39
41
|
executables:
|
40
42
|
- resty
|
41
43
|
extensions: []
|
42
44
|
extra_rdoc_files: []
|
43
45
|
files:
|
44
46
|
- lib/resty/attributes.rb
|
45
|
-
- lib/resty/actions.rb
|
46
47
|
- lib/resty/transport.rb
|
48
|
+
- lib/resty/actions.rb
|
47
49
|
- lib/resty.rb
|
48
50
|
- bin/resty
|
49
51
|
- LICENSE
|
52
|
+
- README.md
|
50
53
|
has_rdoc: true
|
51
54
|
homepage: http://github.com/simonrussell/resty
|
52
55
|
licenses: []
|