hactor 0.0.1 → 0.0.2
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 +56 -1
- data/example.rb +49 -0
- data/hactor.gemspec +1 -0
- data/lib/hactor.rb +6 -3
- data/lib/hactor/hal/document.rb +20 -7
- data/lib/hactor/hal/embedded_collection.rb +11 -7
- data/lib/hactor/hal/flat_collection.rb +12 -8
- data/lib/hactor/hal/link.rb +25 -1
- data/lib/hactor/hal/link_collection.rb +14 -6
- data/lib/hactor/hal/null_link.rb +8 -0
- data/lib/hactor/hal/resource.rb +37 -7
- data/lib/hactor/http/client.rb +30 -14
- data/lib/hactor/http/response.rb +19 -12
- data/lib/hactor/null_actor.rb +7 -0
- data/lib/hactor/version.rb +1 -1
- data/spec/hactor/hal/document_spec.rb +19 -9
- data/spec/hactor/hal/link_collection_spec.rb +4 -2
- data/spec/hactor/hal/link_spec.rb +38 -0
- data/spec/hactor/hal/resource_spec.rb +48 -11
- data/spec/hactor/http/client_spec.rb +68 -33
- data/spec/hactor/http/response_spec.rb +12 -15
- data/spec/hactor/null_actor_spec.rb +12 -0
- data/spec/hactor_spec.rb +1 -1
- metadata +32 -5
- data/spec/integration.rb +0 -37
data/README.md
CHANGED
@@ -1,6 +1,61 @@
|
|
1
1
|
# Hactor
|
2
2
|
|
3
|
-
|
3
|
+
Here's an example of how to use Hactor:
|
4
|
+
|
5
|
+
|
6
|
+
```ruby
|
7
|
+
require 'hactor'
|
8
|
+
|
9
|
+
class UserListActor
|
10
|
+
include Hactor::Actor
|
11
|
+
|
12
|
+
def on_200(response)
|
13
|
+
user_links = response.body.links.select { |link| link.rel == 'ht:user' }
|
14
|
+
user_links.each do |link|
|
15
|
+
puts "#{link.title} (#{link.href})"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class LatestPostActor
|
21
|
+
include Hactor::Actor
|
22
|
+
|
23
|
+
def on_200(response)
|
24
|
+
puts response.body.embedded_resources.first.links.all
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class HomeActor
|
29
|
+
include Hactor::Actor
|
30
|
+
|
31
|
+
def on_200(response)
|
32
|
+
response.follow 'ht:users', actor: UserListActor.new
|
33
|
+
response.follow 'ht:latest-posts', actor: LatestPostActor.new
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Hactor.start url: 'http://haltalk.herokuapp.com/', actor: HomeActor.new
|
38
|
+
```
|
39
|
+
|
40
|
+
The following demonstrates the various things an actor can do with a response:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
class SomeActor
|
44
|
+
include Hactor::Actor
|
45
|
+
|
46
|
+
def on_200(response)
|
47
|
+
# Follow the rel_name link
|
48
|
+
response.follow rel_name, actor: some_actor
|
49
|
+
# Expand link's URI template with query_vars and follow that
|
50
|
+
response.follow rel_name, expand_with: query_vars , actor: some_actor
|
51
|
+
# Traverse the link with some_method, some_headers, and some_body
|
52
|
+
response.traverse rel_name, method: some_method, headers: some_headers, body: some_string, actor: some_actor
|
53
|
+
# In each case, the response produce will be handled by some_actor
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Hactor.start url: some_entry_point_url, actor: SomeActor.new
|
58
|
+
```
|
4
59
|
|
5
60
|
## Installation
|
6
61
|
|
data/example.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'hactor'
|
2
|
+
|
3
|
+
def start
|
4
|
+
Hactor.start url: 'http://haltalk.herokuapp.com/', actor: HomeActor.new
|
5
|
+
end
|
6
|
+
|
7
|
+
class HomeActor
|
8
|
+
include Hactor::Actor
|
9
|
+
|
10
|
+
def on_200(response)
|
11
|
+
name = 'CHANGE ME'
|
12
|
+
user_name = 'PLEASE_CHANGE_ME'
|
13
|
+
|
14
|
+
response.follow 'ht:latest-posts', actor: LatestPostActor.new
|
15
|
+
|
16
|
+
response.traverse('ht:signup', method: 'POST',
|
17
|
+
body: { username: user_name, real_name: name, password: 'blarb' },
|
18
|
+
actor: SignedUpActor.new)
|
19
|
+
|
20
|
+
response.follow('ht:me', expand_with: { name: user_name },
|
21
|
+
actor: ->(res) { puts res.properties })
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class LatestPostActor
|
26
|
+
include Hactor::Actor
|
27
|
+
|
28
|
+
def on_200(response)
|
29
|
+
response.embedded_resources.each do |resource|
|
30
|
+
puts resource.link('self').href
|
31
|
+
puts resource.properties
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class SignedUpActor
|
37
|
+
include Hactor::Actor
|
38
|
+
|
39
|
+
def on_200(response)
|
40
|
+
puts "Created an account"
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_400(response)
|
44
|
+
puts "Failed to create account"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
start()
|
data/hactor.gemspec
CHANGED
data/lib/hactor.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
require 'hactor/version'
|
2
2
|
require 'hactor/http/client'
|
3
|
+
require 'hactor/null_actor'
|
3
4
|
require 'hactor/actor'
|
4
5
|
|
5
6
|
module Hactor
|
6
7
|
def self.start(options)
|
7
8
|
url = options.fetch :url
|
8
|
-
|
9
|
-
|
10
|
-
http_client
|
9
|
+
|
10
|
+
actor = options[:actor] || NullActor.new
|
11
|
+
http_client = options[:http_client] || Hactor::HTTP::Client.new
|
12
|
+
|
13
|
+
http_client.get(url, actor: actor)
|
11
14
|
end
|
12
15
|
end
|
data/lib/hactor/hal/document.rb
CHANGED
@@ -1,22 +1,35 @@
|
|
1
|
-
require 'delegate'
|
2
1
|
require 'json'
|
2
|
+
require 'delegate'
|
3
|
+
require 'forwardable'
|
3
4
|
require 'hactor/hal/resource'
|
4
5
|
|
5
6
|
module Hactor
|
6
7
|
module HAL
|
7
|
-
class Document <
|
8
|
-
|
8
|
+
class Document < Delegator
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_reader :body, :response, :resource_class
|
12
|
+
|
13
|
+
def_delegators :response, :http_client
|
9
14
|
|
10
15
|
def initialize(body, options={})
|
11
|
-
@
|
16
|
+
@response = options.fetch(:response)
|
17
|
+
@resource_class = options[:resource_class] || Hactor::HAL::Resource
|
18
|
+
body = '{}' if body == 'null'
|
12
19
|
@body = JSON.parse(body)
|
13
|
-
|
20
|
+
end
|
21
|
+
|
22
|
+
def __getobj__
|
23
|
+
root_resource
|
14
24
|
end
|
15
25
|
|
16
26
|
def root_resource
|
17
|
-
@root_resource ||= resource_class.new(body)
|
27
|
+
@root_resource ||= resource_class.new(body, rel: 'self', context: self)
|
28
|
+
end
|
29
|
+
|
30
|
+
def base_url
|
31
|
+
response.env[:url]
|
18
32
|
end
|
19
33
|
end
|
20
34
|
end
|
21
35
|
end
|
22
|
-
|
@@ -9,21 +9,25 @@ module Hactor
|
|
9
9
|
extend Forwardable
|
10
10
|
include Enumerable
|
11
11
|
|
12
|
-
attr_reader :hash, :embedded_class, :flat_collection_class
|
13
|
-
def_delegators :each
|
12
|
+
attr_reader :hash, :parent, :embedded_class, :flat_collection_class
|
14
13
|
|
15
|
-
|
14
|
+
def_delegators :all, :each
|
15
|
+
|
16
|
+
def initialize(hash, options)
|
16
17
|
#TODO: throw/log parsing error if not hash
|
17
|
-
@embedded_class = options.fetch(:embedded_class) { Resource }
|
18
|
-
@flat_collection_class = options.fetch(:flat_collection_class) { FlatCollection }
|
19
18
|
@hash = hash
|
19
|
+
@parent = options.fetch(:parent)
|
20
|
+
@embedded_class = options[:embedded_class] || Resource
|
21
|
+
@flat_collection_class = options[:flat_collection_class] || FlatCollection
|
20
22
|
end
|
21
23
|
|
22
24
|
def all
|
23
|
-
@all ||= flat_collection_class.new
|
25
|
+
@all ||= flat_collection_class.new hash,
|
26
|
+
parent: parent,
|
27
|
+
item_class: embedded_class
|
24
28
|
end
|
25
29
|
|
26
|
-
def
|
30
|
+
def find(rel)
|
27
31
|
all.find(->{ NullResource.new }) { |resource| resource.rel == rel }
|
28
32
|
end
|
29
33
|
end
|
@@ -1,23 +1,27 @@
|
|
1
1
|
module Hactor
|
2
2
|
module HAL
|
3
3
|
class FlatCollection < Array
|
4
|
-
attr_reader :item_class
|
4
|
+
attr_reader :parent, :item_class
|
5
5
|
|
6
|
-
def initialize(hash, options
|
6
|
+
def initialize(hash, options)
|
7
|
+
@parent = options.fetch(:parent)
|
7
8
|
@item_class = options.fetch(:item_class)
|
8
9
|
super flatten(hash)
|
9
10
|
end
|
10
11
|
|
11
12
|
private
|
12
13
|
def flatten(hash)
|
14
|
+
hash ||= {}
|
13
15
|
arr = []
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
|
17
|
+
hash.each do |rel, value|
|
18
|
+
if value.is_a? Array
|
19
|
+
arr += value.map { |obj| item_class.new(obj, rel: rel, context: parent) }
|
20
|
+
else
|
21
|
+
arr.push item_class.new(value, rel: rel, context: parent)
|
20
22
|
end
|
23
|
+
end
|
24
|
+
|
21
25
|
arr
|
22
26
|
end
|
23
27
|
end
|
data/lib/hactor/hal/link.rb
CHANGED
@@ -1,7 +1,31 @@
|
|
1
1
|
require 'ostruct'
|
2
|
+
require 'addressable/template'
|
2
3
|
|
3
4
|
module Hactor
|
4
5
|
module HAL
|
5
|
-
class Link < OpenStruct
|
6
|
+
class Link < OpenStruct
|
7
|
+
attr_reader :rel, :parent_resource, :template_class
|
8
|
+
|
9
|
+
def initialize(obj, options)
|
10
|
+
@href = obj['href']
|
11
|
+
@rel = options.fetch(:rel)
|
12
|
+
@parent_resource = options.fetch(:context)
|
13
|
+
@template_class = options[:template_class] || Addressable::Template
|
14
|
+
super(obj)
|
15
|
+
end
|
16
|
+
|
17
|
+
def href(variables=nil)
|
18
|
+
if templated?
|
19
|
+
variables ||= {}
|
20
|
+
template_class.new(@href).expand(variables).to_s
|
21
|
+
else
|
22
|
+
@href
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def templated?
|
27
|
+
!!templated
|
28
|
+
end
|
29
|
+
end
|
6
30
|
end
|
7
31
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
require 'hactor/hal/flat_collection'
|
3
3
|
require 'hactor/hal/link'
|
4
|
-
|
4
|
+
require 'hactor/hal/null_link'
|
5
5
|
|
6
6
|
module Hactor
|
7
7
|
module HAL
|
@@ -9,23 +9,31 @@ module Hactor
|
|
9
9
|
extend Forwardable
|
10
10
|
include Enumerable
|
11
11
|
|
12
|
-
attr_reader :hash, :link_class, :flat_collection_class
|
12
|
+
attr_reader :hash, :parent, :link_class, :flat_collection_class
|
13
13
|
def_delegators :all, :each
|
14
14
|
|
15
15
|
def initialize(hash, options={})
|
16
16
|
#TODO: throw/log parsing error if not hash
|
17
|
-
@link_class = options.fetch(:link_class) { Link }
|
18
|
-
@flat_collection_class = options.fetch(:flat_collection_class) { FlatCollection }
|
19
17
|
@hash = hash
|
18
|
+
@parent = options.fetch(:parent)
|
19
|
+
@link_class = options[:link_class] || Link
|
20
|
+
@flat_collection_class = options[:flat_collection_class] || FlatCollection
|
20
21
|
end
|
21
22
|
|
22
23
|
def all
|
23
|
-
@all ||= flat_collection_class.new
|
24
|
+
@all ||= flat_collection_class.new hash,
|
25
|
+
item_class: link_class,
|
26
|
+
parent: parent
|
27
|
+
|
24
28
|
end
|
25
29
|
|
26
|
-
def
|
30
|
+
def find(rel)
|
27
31
|
all.find(->{ NullLink.new }) { |link| link.rel == rel }
|
28
32
|
end
|
33
|
+
|
34
|
+
def with_rel(rel)
|
35
|
+
all.select { |link| link.rel == rel }
|
36
|
+
end
|
29
37
|
end
|
30
38
|
end
|
31
39
|
end
|
data/lib/hactor/hal/resource.rb
CHANGED
@@ -1,19 +1,33 @@
|
|
1
|
+
require 'forwardable'
|
1
2
|
require 'hactor/hal/link_collection'
|
2
3
|
require 'hactor/hal/embedded_collection'
|
3
4
|
|
4
5
|
module Hactor
|
5
6
|
module HAL
|
6
7
|
class Resource
|
8
|
+
extend Forwardable
|
9
|
+
|
7
10
|
RESERVED_PROPERTIES = ['_links', '_embedded']
|
8
11
|
|
9
|
-
attr_reader :state, :
|
12
|
+
attr_reader :rel, :state, :http_client, :context
|
13
|
+
attr_accessor :link_collection_class, :embedded_collection_class
|
14
|
+
|
15
|
+
def_delegators :context, :http_client, :base_url
|
10
16
|
|
11
|
-
def initialize(state, options
|
12
|
-
@
|
13
|
-
@
|
17
|
+
def initialize(state, options)
|
18
|
+
@rel = options.fetch(:rel)
|
19
|
+
@context = options.fetch(:context)
|
14
20
|
@state = state
|
15
21
|
end
|
16
22
|
|
23
|
+
def follow(rel, options={})
|
24
|
+
http_client.follow link(rel), options_in_context(options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def traverse(rel, options={})
|
28
|
+
http_client.traverse link(rel), options_in_context(options)
|
29
|
+
end
|
30
|
+
|
17
31
|
def properties
|
18
32
|
@properties ||= state.reject { |k,v|
|
19
33
|
RESERVED_PROPERTIES.include? k
|
@@ -21,11 +35,12 @@ module Hactor
|
|
21
35
|
end
|
22
36
|
|
23
37
|
def link(rel, options={})
|
24
|
-
links.
|
38
|
+
links.find(rel)
|
25
39
|
end
|
26
40
|
|
27
41
|
def links
|
28
|
-
@links ||= link_collection_class.new
|
42
|
+
@links ||= link_collection_class.new state['_links'],
|
43
|
+
parent: self
|
29
44
|
end
|
30
45
|
|
31
46
|
def embedded_resource(rel)
|
@@ -33,7 +48,22 @@ module Hactor
|
|
33
48
|
end
|
34
49
|
|
35
50
|
def embedded_resources
|
36
|
-
@embedded_resources ||= embedded_collection_class.new
|
51
|
+
@embedded_resources ||= embedded_collection_class.new state['_embedded'],
|
52
|
+
parent: self
|
53
|
+
end
|
54
|
+
|
55
|
+
def embedded_collection_class
|
56
|
+
@embedded_collection_class ||= EmbeddedCollection
|
57
|
+
end
|
58
|
+
|
59
|
+
def link_collection_class
|
60
|
+
@link_collection_class ||= LinkCollection
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def options_in_context(options)
|
66
|
+
options.merge context_url: context.base_url
|
37
67
|
end
|
38
68
|
end
|
39
69
|
end
|
data/lib/hactor/http/client.rb
CHANGED
@@ -1,33 +1,49 @@
|
|
1
|
-
require 'hactor/http/response'
|
2
1
|
require 'faraday'
|
2
|
+
require 'hactor/null_actor'
|
3
|
+
require 'hactor/http/response'
|
3
4
|
|
4
5
|
module Hactor
|
5
6
|
module HTTP
|
6
|
-
class Client <
|
7
|
+
class Client < Delegator
|
7
8
|
attr_reader :response_class, :backend
|
8
9
|
|
9
10
|
def initialize(options={})
|
10
|
-
@response_class = options
|
11
|
-
@backend = options
|
12
|
-
super(@backend)
|
11
|
+
@response_class = options[:response_class] || Hactor::HTTP::Response
|
12
|
+
@backend = options[:backend] || Faraday.new
|
13
13
|
end
|
14
14
|
|
15
|
-
def follow(link, options
|
16
|
-
actor = options.fetch(:actor)
|
15
|
+
def follow(link, options)
|
17
16
|
context_url = options.fetch(:context_url)
|
18
17
|
|
19
|
-
url = context_url.merge(link.href)
|
20
|
-
get(url
|
18
|
+
url = context_url.merge(link.href(options[:expand_with]))
|
19
|
+
get(url, options)
|
21
20
|
end
|
22
21
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
22
|
+
def traverse(link, options)
|
23
|
+
context_url = options.fetch(:context_url)
|
24
|
+
method = options.fetch(:method).to_s.downcase
|
26
25
|
|
27
|
-
|
28
|
-
|
26
|
+
actor = options[:actor] || Hactor::NullActor.new
|
27
|
+
headers = options[:headers]
|
28
|
+
body = options[:body]
|
29
|
+
|
30
|
+
url = context_url.merge link.href(options[:expand_with])
|
31
|
+
response = response_class.new backend.send(method, url, body, headers),
|
32
|
+
http_client: self
|
29
33
|
actor.call response
|
30
34
|
end
|
35
|
+
|
36
|
+
def get(url, options)
|
37
|
+
actor = options[:actor] || Hactor::NullActor.new
|
38
|
+
headers = options[:headers]
|
39
|
+
response = response_class.new backend.get(url, nil, headers),
|
40
|
+
http_client: self
|
41
|
+
actor.call response
|
42
|
+
end
|
43
|
+
|
44
|
+
def __getobj__
|
45
|
+
backend
|
46
|
+
end
|
31
47
|
end
|
32
48
|
end
|
33
49
|
end
|
data/lib/hactor/http/response.rb
CHANGED
@@ -1,28 +1,35 @@
|
|
1
1
|
require 'delegate'
|
2
|
+
require 'forwardable'
|
2
3
|
require 'hactor/hal/document'
|
3
4
|
|
4
5
|
module Hactor
|
5
6
|
module HTTP
|
6
|
-
class Response <
|
7
|
-
|
7
|
+
class Response < Delegator
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_reader :codec, :response
|
11
|
+
attr_accessor :http_client
|
12
|
+
|
13
|
+
def_delegators :body, :follow,
|
14
|
+
:traverse,
|
15
|
+
:properties,
|
16
|
+
:embedded_resources,
|
17
|
+
:links
|
8
18
|
|
9
19
|
def initialize(response, options={})
|
10
|
-
@
|
20
|
+
@http_client = options.fetch(:http_client)
|
11
21
|
@codec = options.fetch(:codec) { Hactor::HAL::Document }
|
12
22
|
@body = options[:body]
|
13
|
-
|
23
|
+
@response = response
|
14
24
|
end
|
15
25
|
|
16
|
-
def
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
link = body.link(rel)
|
21
|
-
client.follow(link, context_url: env[:url], actor: actor)
|
26
|
+
def body
|
27
|
+
@body ||= codec.new __getobj__.body,
|
28
|
+
response: self
|
22
29
|
end
|
23
30
|
|
24
|
-
def
|
25
|
-
|
31
|
+
def __getobj__
|
32
|
+
response
|
26
33
|
end
|
27
34
|
end
|
28
35
|
end
|
data/lib/hactor/version.rb
CHANGED
@@ -2,21 +2,31 @@ require 'spec_helper'
|
|
2
2
|
require 'hactor/hal/document'
|
3
3
|
|
4
4
|
describe Hactor::HAL::Document do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
let(:resource_class) { mock }
|
6
|
+
let(:response) { mock }
|
7
|
+
let(:sentinel) { stub }
|
8
|
+
let(:body) { '{}' }
|
9
|
+
|
10
|
+
let(:doc) do
|
11
|
+
Hactor::HAL::Document.new body,
|
12
|
+
response: response,
|
13
|
+
resource_class: resource_class
|
14
|
+
end
|
13
15
|
|
16
|
+
describe "#root_resource" do
|
14
17
|
it "returns new Resource object" do
|
15
18
|
resource_class.should_receive(:new)
|
16
|
-
.with(JSON.parse(body))
|
19
|
+
.with(JSON.parse(body), { rel: 'self', context: doc })
|
17
20
|
.and_return(sentinel)
|
18
21
|
|
19
22
|
doc.root_resource.should == sentinel
|
20
23
|
end
|
21
24
|
end
|
25
|
+
|
26
|
+
describe "#base_url" do
|
27
|
+
it "returns the url via the response object" do
|
28
|
+
response.should_receive(:env).and_return url: sentinel
|
29
|
+
doc.base_url.should == sentinel
|
30
|
+
end
|
31
|
+
end
|
22
32
|
end
|
@@ -11,8 +11,10 @@ describe Hactor::HAL::LinkCollection do
|
|
11
11
|
let(:flat_collection_class) { mock }
|
12
12
|
let(:flat_collection) { mock }
|
13
13
|
let(:link) { stub }
|
14
|
+
let(:parent) { stub }
|
14
15
|
let(:collection) do
|
15
16
|
Hactor::HAL::LinkCollection.new(simple_link_hash,
|
17
|
+
parent: parent,
|
16
18
|
link_class: link_class,
|
17
19
|
flat_collection_class: flat_collection_class)
|
18
20
|
end
|
@@ -20,11 +22,11 @@ describe Hactor::HAL::LinkCollection do
|
|
20
22
|
describe "#find" do
|
21
23
|
it "finds a link with the supplied rel" do
|
22
24
|
flat_collection_class.should_receive(:new)
|
23
|
-
.with(simple_link_hash, item_class: link_class)
|
25
|
+
.with(simple_link_hash, parent: parent, item_class: link_class)
|
24
26
|
.and_return(flat_collection)
|
25
27
|
flat_collection.should_receive(:find).and_return(link)
|
26
28
|
|
27
|
-
collection.
|
29
|
+
collection.find(:self).should == link
|
28
30
|
end
|
29
31
|
end
|
30
32
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'hactor/hal/link'
|
3
|
+
|
4
|
+
describe Hactor::HAL::Link do
|
5
|
+
context "templated link" do
|
6
|
+
let(:template_class) { mock('templ_class') }
|
7
|
+
let(:template) { mock('template') }
|
8
|
+
let(:href) { stub }
|
9
|
+
|
10
|
+
let(:state) do
|
11
|
+
{
|
12
|
+
'href' => href,
|
13
|
+
'templated' => true
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:link) do
|
18
|
+
Hactor::HAL::Link.new(state,{
|
19
|
+
rel: stub,
|
20
|
+
context: stub,
|
21
|
+
template_class: template_class
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#href" do
|
26
|
+
it "expands using the variables passed in" do
|
27
|
+
sentinel, variables = stub, stub
|
28
|
+
|
29
|
+
template_class.should_receive(:new).with(href).
|
30
|
+
and_return(template)
|
31
|
+
template.should_receive(:expand).with(variables).
|
32
|
+
and_return(stub(to_s: sentinel))
|
33
|
+
|
34
|
+
link.href(variables).should == sentinel
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -15,17 +15,53 @@ describe Hactor::HAL::Resource do
|
|
15
15
|
}
|
16
16
|
end
|
17
17
|
|
18
|
-
let(:resource) do
|
19
|
-
Hactor::HAL::Resource.new(json,
|
20
|
-
link_collection_class: link_collection_class,
|
21
|
-
embedded_collection_class: embedded_collection_class)
|
22
|
-
end
|
23
|
-
|
24
18
|
let(:link_collection_class) { mock }
|
25
|
-
let(:embedded_collection_class) { mock }
|
26
19
|
let(:link_collection) { mock }
|
20
|
+
let(:embedded_collection_class) { mock }
|
27
21
|
let(:embedded_collection) { mock }
|
22
|
+
let(:http_client) { mock }
|
23
|
+
|
28
24
|
let(:rel) { stub }
|
25
|
+
let(:link) { stub }
|
26
|
+
let(:base_url) { stub }
|
27
|
+
let(:document) { stub(http_client: http_client, base_url: base_url) }
|
28
|
+
|
29
|
+
let(:resource) do
|
30
|
+
Hactor::HAL::Resource.new json,
|
31
|
+
rel: rel,
|
32
|
+
context: document
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
before :each do
|
37
|
+
resource.link_collection_class = link_collection_class
|
38
|
+
resource.embedded_collection_class = embedded_collection_class
|
39
|
+
end
|
40
|
+
|
41
|
+
context "http transition methods" do
|
42
|
+
before :each do
|
43
|
+
link_collection_class.should_receive(:new).and_return(link_collection)
|
44
|
+
link_collection.should_receive(:find).with(rel).and_return(link)
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#follow" do
|
48
|
+
it "tells the http client to follow the link and also supplies the context URL" do
|
49
|
+
http_client.should_receive(:follow).with(link, { context_url: base_url })
|
50
|
+
|
51
|
+
resource.follow(rel)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#traverse" do
|
56
|
+
it "tells the http client to traverse the link using the supplied method and also supplies the context URL" do
|
57
|
+
method = stub
|
58
|
+
http_client.should_receive(:traverse).
|
59
|
+
with(link, { method: method, context_url: base_url })
|
60
|
+
|
61
|
+
resource.traverse(rel, method: method)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
29
65
|
|
30
66
|
describe "#properties" do
|
31
67
|
it "returns a Hash containing the json with the reserved properties removed" do
|
@@ -35,13 +71,13 @@ describe Hactor::HAL::Resource do
|
|
35
71
|
|
36
72
|
context "links" do
|
37
73
|
before :each do
|
38
|
-
link_collection_class.should_receive(:new).
|
74
|
+
link_collection_class.should_receive(:new).
|
75
|
+
with(json['_links'], parent: resource).and_return(link_collection)
|
39
76
|
end
|
40
77
|
|
41
78
|
describe "#link" do
|
42
79
|
it "finds a link with the given rel" do
|
43
|
-
link
|
44
|
-
link_collection.should_receive(:find_by_rel).with(rel).and_return(link)
|
80
|
+
link_collection.should_receive(:find).with(rel).and_return(link)
|
45
81
|
resource.link(rel).should == link
|
46
82
|
end
|
47
83
|
end
|
@@ -55,7 +91,8 @@ describe Hactor::HAL::Resource do
|
|
55
91
|
|
56
92
|
context "embedded resources" do
|
57
93
|
before :each do
|
58
|
-
embedded_collection_class.should_receive(:new).
|
94
|
+
embedded_collection_class.should_receive(:new).
|
95
|
+
with(json['_embedded'], parent: resource).and_return(embedded_collection)
|
59
96
|
end
|
60
97
|
|
61
98
|
describe "#embedded_resource" do
|
@@ -5,48 +5,83 @@ describe Hactor::HTTP::Client do
|
|
5
5
|
let(:response_class) { mock }
|
6
6
|
let(:backend) { mock }
|
7
7
|
let(:actor) { mock }
|
8
|
+
let(:link) { mock }
|
9
|
+
let(:context_url) { mock }
|
10
|
+
|
11
|
+
let(:variables) { stub }
|
12
|
+
let(:uri) { stub }
|
13
|
+
let(:resolved_uri) { stub }
|
14
|
+
let(:response) { stub }
|
15
|
+
let(:headers) { stub }
|
16
|
+
let(:hactor_response) { stub }
|
17
|
+
let(:url) { 'http://example.com/' }
|
18
|
+
|
8
19
|
let(:client) do
|
9
|
-
Hactor::HTTP::Client.new
|
10
|
-
backend: backend
|
20
|
+
Hactor::HTTP::Client.new response_class: response_class,
|
21
|
+
backend: backend
|
11
22
|
end
|
12
23
|
|
13
|
-
describe "#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
let(:hactor_response) { stub }
|
18
|
-
|
19
|
-
it "GETs from URL and passes Response object to the actor" do
|
20
|
-
backend.should_receive(:get)
|
21
|
-
.with(url)
|
22
|
-
.and_return(response)
|
23
|
-
response_class.should_receive(:new)
|
24
|
-
.with(response, http_client: client)
|
25
|
-
.and_return(hactor_response)
|
26
|
-
actor.should_receive(:call)
|
27
|
-
.with(hactor_response)
|
28
|
-
|
29
|
-
client.get(url: url, actor: actor)
|
30
|
-
end
|
31
|
-
end
|
24
|
+
describe "#follow" do
|
25
|
+
before :each do
|
26
|
+
context_url.should_receive(:merge).with(uri).
|
27
|
+
and_return(resolved_uri)
|
32
28
|
|
33
|
-
|
34
|
-
|
35
|
-
let(:uri) { stub }
|
36
|
-
let(:context_url) { mock }
|
37
|
-
let(:resolved_uri) { stub }
|
29
|
+
response_class.should_receive(:new).with(response, http_client: backend).
|
30
|
+
and_return(hactor_response)
|
38
31
|
|
32
|
+
actor.should_receive(:call).with(hactor_response)
|
33
|
+
end
|
34
|
+
|
35
|
+
context "plain url (no expand_with in option)" do
|
39
36
|
it "should use the context url to resolve link href" do
|
40
|
-
|
41
|
-
client.stub!(:get).with(url: resolved_uri, actor: actor)
|
42
|
-
link.should_receive(:href)
|
37
|
+
link.should_receive(:href).with(nil)
|
43
38
|
.and_return(uri)
|
44
|
-
|
45
|
-
|
46
|
-
.and_return(resolved_uri)
|
39
|
+
backend.should_receive(:get).with(resolved_uri, nil, headers).
|
40
|
+
and_return(response)
|
47
41
|
|
48
|
-
client.follow
|
42
|
+
client.follow link,
|
43
|
+
context_url: context_url,
|
44
|
+
actor: actor,
|
45
|
+
headers: headers
|
49
46
|
end
|
50
47
|
end
|
48
|
+
|
49
|
+
context "URI template (expand_with in option)" do
|
50
|
+
it "should apply the variables to the template and follow resulting URI" do
|
51
|
+
link.should_receive(:href).with(variables).and_return(uri)
|
52
|
+
backend.should_receive(:get).with(resolved_uri, nil, headers).
|
53
|
+
and_return(response)
|
54
|
+
|
55
|
+
client.follow link,
|
56
|
+
context_url: context_url,
|
57
|
+
actor: actor,
|
58
|
+
expand_with: variables,
|
59
|
+
headers: headers
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "#traverse" do
|
65
|
+
let(:method) { 'POST' }
|
66
|
+
let(:body) { stub 'body' }
|
67
|
+
|
68
|
+
it "should send a message of the given method to the http backend" do
|
69
|
+
link.should_receive(:href).with(variables).and_return(uri)
|
70
|
+
context_url.should_receive(:merge).with(uri).
|
71
|
+
and_return(resolved_uri)
|
72
|
+
backend.should_receive('post').with(resolved_uri, body, headers).
|
73
|
+
and_return(response)
|
74
|
+
response_class.should_receive(:new).with(response, http_client: backend).
|
75
|
+
and_return(hactor_response)
|
76
|
+
actor.should_receive(:call).with(hactor_response)
|
77
|
+
|
78
|
+
client.traverse link,
|
79
|
+
context_url: context_url,
|
80
|
+
method: method,
|
81
|
+
headers: headers,
|
82
|
+
body: body,
|
83
|
+
actor: actor,
|
84
|
+
expand_with: variables
|
85
|
+
end
|
51
86
|
end
|
52
87
|
end
|
@@ -2,16 +2,13 @@ require 'spec_helper'
|
|
2
2
|
require 'hactor/http/response'
|
3
3
|
|
4
4
|
describe Hactor::HTTP::Response do
|
5
|
-
let(:codec) { mock }
|
6
|
-
let(:body) { mock }
|
7
|
-
let(:wrapped_response) { mock }
|
8
|
-
let(:http_client) {
|
5
|
+
let(:codec) { mock 'codec' }
|
6
|
+
let(:body) { mock 'body' }
|
7
|
+
let(:wrapped_response) { mock 'wrapped_response' }
|
8
|
+
let(:http_client) { stub }
|
9
9
|
|
10
10
|
describe "#follow" do
|
11
11
|
let(:rel) { stub }
|
12
|
-
let(:link) { stub }
|
13
|
-
let(:actor) { stub }
|
14
|
-
let(:context_url) { stub }
|
15
12
|
let(:response) do
|
16
13
|
Hactor::HTTP::Response.new(wrapped_response,
|
17
14
|
http_client: http_client,
|
@@ -19,14 +16,12 @@ describe Hactor::HTTP::Response do
|
|
19
16
|
body: body)
|
20
17
|
end
|
21
18
|
|
22
|
-
it "
|
23
|
-
|
24
|
-
http_client.should_receive(:follow)
|
25
|
-
.with(link, context_url: context_url, actor: actor)
|
26
|
-
wrapped_response.should_receive(:env)
|
27
|
-
.and_return({ url: context_url })
|
19
|
+
it "delegates to the body" do
|
20
|
+
options = stub
|
28
21
|
|
29
|
-
|
22
|
+
body.should_receive(:follow).with(rel, options)
|
23
|
+
|
24
|
+
response.follow(rel, options)
|
30
25
|
end
|
31
26
|
end
|
32
27
|
|
@@ -41,7 +36,9 @@ describe Hactor::HTTP::Response do
|
|
41
36
|
|
42
37
|
it "passes itself into the codec" do
|
43
38
|
wrapped_response.should_receive(:body).and_return(body)
|
44
|
-
codec.should_receive(:new).
|
39
|
+
codec.should_receive(:new).
|
40
|
+
with(body, response: response).
|
41
|
+
and_return(sentinel)
|
45
42
|
|
46
43
|
response.body.should == sentinel
|
47
44
|
end
|
data/spec/hactor_spec.rb
CHANGED
@@ -8,7 +8,7 @@ describe Hactor do
|
|
8
8
|
let(:actor) { mock }
|
9
9
|
let(:http_client) { mock }
|
10
10
|
it "GETs from the URL and passes a response object to an actor" do
|
11
|
-
http_client.should_receive(:get).with(url
|
11
|
+
http_client.should_receive(:get).with(url, actor: actor)
|
12
12
|
Hactor.start(url: url, actor: actor, http_client: http_client)
|
13
13
|
end
|
14
14
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hactor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-01-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -43,6 +43,22 @@ dependencies:
|
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: addressable
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
46
62
|
description: A framework for building hypermedia clients
|
47
63
|
email:
|
48
64
|
- mikekelly321@gmail.com
|
@@ -56,6 +72,7 @@ files:
|
|
56
72
|
- LICENSE.txt
|
57
73
|
- README.md
|
58
74
|
- Rakefile
|
75
|
+
- example.rb
|
59
76
|
- hactor.gemspec
|
60
77
|
- lib/hactor.rb
|
61
78
|
- lib/hactor/actor.rb
|
@@ -64,19 +81,22 @@ files:
|
|
64
81
|
- lib/hactor/hal/flat_collection.rb
|
65
82
|
- lib/hactor/hal/link.rb
|
66
83
|
- lib/hactor/hal/link_collection.rb
|
84
|
+
- lib/hactor/hal/null_link.rb
|
67
85
|
- lib/hactor/hal/resource.rb
|
68
86
|
- lib/hactor/http/client.rb
|
69
87
|
- lib/hactor/http/response.rb
|
88
|
+
- lib/hactor/null_actor.rb
|
70
89
|
- lib/hactor/version.rb
|
71
90
|
- spec/hactor/hal/document_spec.rb
|
72
91
|
- spec/hactor/hal/embedded_collection_spec.rb
|
73
92
|
- spec/hactor/hal/flat_collection_spec.rb
|
74
93
|
- spec/hactor/hal/link_collection_spec.rb
|
94
|
+
- spec/hactor/hal/link_spec.rb
|
75
95
|
- spec/hactor/hal/resource_spec.rb
|
76
96
|
- spec/hactor/http/client_spec.rb
|
77
97
|
- spec/hactor/http/response_spec.rb
|
98
|
+
- spec/hactor/null_actor_spec.rb
|
78
99
|
- spec/hactor_spec.rb
|
79
|
-
- spec/integration.rb
|
80
100
|
- spec/spec_helper.rb
|
81
101
|
homepage: https://github.com/mikekelly/hactor
|
82
102
|
licenses: []
|
@@ -90,15 +110,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
110
|
- - ! '>='
|
91
111
|
- !ruby/object:Gem::Version
|
92
112
|
version: '0'
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
hash: -4470173572118963512
|
93
116
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
117
|
none: false
|
95
118
|
requirements:
|
96
119
|
- - ! '>='
|
97
120
|
- !ruby/object:Gem::Version
|
98
121
|
version: '0'
|
122
|
+
segments:
|
123
|
+
- 0
|
124
|
+
hash: -4470173572118963512
|
99
125
|
requirements: []
|
100
126
|
rubyforge_project:
|
101
|
-
rubygems_version: 1.8.
|
127
|
+
rubygems_version: 1.8.23
|
102
128
|
signing_key:
|
103
129
|
specification_version: 3
|
104
130
|
summary: A framework for building hypermedia clients
|
@@ -107,9 +133,10 @@ test_files:
|
|
107
133
|
- spec/hactor/hal/embedded_collection_spec.rb
|
108
134
|
- spec/hactor/hal/flat_collection_spec.rb
|
109
135
|
- spec/hactor/hal/link_collection_spec.rb
|
136
|
+
- spec/hactor/hal/link_spec.rb
|
110
137
|
- spec/hactor/hal/resource_spec.rb
|
111
138
|
- spec/hactor/http/client_spec.rb
|
112
139
|
- spec/hactor/http/response_spec.rb
|
140
|
+
- spec/hactor/null_actor_spec.rb
|
113
141
|
- spec/hactor_spec.rb
|
114
|
-
- spec/integration.rb
|
115
142
|
- spec/spec_helper.rb
|
data/spec/integration.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'hactor'
|
3
|
-
require 'hactor/actor'
|
4
|
-
|
5
|
-
class UserListActor
|
6
|
-
include Hactor::Actor
|
7
|
-
|
8
|
-
def on_200(response)
|
9
|
-
user_links = response.body.links.select { |link| link.rel == 'ht:user' }
|
10
|
-
user_links.each do |link|
|
11
|
-
puts "#{link.title} (#{link.href})"
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
class LatestPostActor
|
17
|
-
include Hactor::Actor
|
18
|
-
|
19
|
-
def on_200(response)
|
20
|
-
puts response.body.embedded_resources.all.first.links.all
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
class HomeActor
|
25
|
-
include Hactor::Actor
|
26
|
-
|
27
|
-
def on_200(response)
|
28
|
-
response.follow 'ht:users', actor: UserListActor.new
|
29
|
-
response.follow 'ht:latest-posts', actor: LatestPostActor.new
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
describe Hactor do
|
34
|
-
it "should work as expected :)" do
|
35
|
-
Hactor.start url: 'http://haltalk.herokuapp.com/', actor: HomeActor.new
|
36
|
-
end
|
37
|
-
end
|