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.
Files changed (4) hide show
  1. data/README.md +86 -0
  2. data/lib/resty.rb +43 -0
  3. data/lib/resty/attributes.rb +44 -23
  4. metadata +11 -8
@@ -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"
@@ -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
@@ -4,34 +4,34 @@ class Resty::Attributes
4
4
 
5
5
  def initialize(data)
6
6
  @href = data[':href']
7
- @populated = if @href
8
- !data[':partial'] && data.length > 1
9
- else
10
- true
11
- end
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
- populate! unless populated?
19
- @data.key?(translate_key(name))
20
- end
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
- populate! unless populated?
33
- name = translate_key(name)
34
- @wrapped[name] ||= Resty.wrap(@data[name])
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
- @populated
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
- @populated = true
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.2.2
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: &23422920 !ruby/object:Gem::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: *23422920
25
+ version_requirements: *79995340
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: json
28
- requirement: &23422440 !ruby/object:Gem::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: *23422440
37
- description:
38
- email:
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: []