hactor 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|