the86-client 0.0.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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +80 -0
- data/Rakefile +11 -0
- data/lib/the86-client.rb +69 -0
- data/lib/the86-client/active_model.rb +37 -0
- data/lib/the86-client/connection.rb +81 -0
- data/lib/the86-client/conversation.rb +14 -0
- data/lib/the86-client/errors.rb +20 -0
- data/lib/the86-client/oauth_bearer_authorization.rb +21 -0
- data/lib/the86-client/post.rb +26 -0
- data/lib/the86-client/resource.rb +149 -0
- data/lib/the86-client/resource_collection.rb +66 -0
- data/lib/the86-client/site.rb +18 -0
- data/lib/the86-client/user.rb +14 -0
- data/lib/the86-client/version.rb +5 -0
- data/spec/conversations_spec.rb +75 -0
- data/spec/oauth_bearer_authorization_spec.rb +19 -0
- data/spec/post_spec.rb +15 -0
- data/spec/posts_spec.rb +81 -0
- data/spec/resource_spec.rb +32 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/webmock.rb +41 -0
- data/spec/users_spec.rb +78 -0
- data/the86-client.gemspec +47 -0
- metadata +256 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
module The86::Client
|
2
|
+
class ResourceCollection
|
3
|
+
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
# Connection is a The86::Client::Connection instance.
|
7
|
+
# Path is the API-relative path, e.g. "users".
|
8
|
+
# Klass is class of each record in the collection, e.g. User
|
9
|
+
# Attributes is a Hash of attributes common to all items in collection,
|
10
|
+
# and not fetched in HTTP response, e.g. parent items.
|
11
|
+
# e.g. for conversations: { site: Site.new(slug: "...") }
|
12
|
+
# Records is an array of hashes, for pre-populating the collection.
|
13
|
+
# e.g. when an API response contains collections of child resources.
|
14
|
+
def initialize(connection, path, klass, parent, records = nil)
|
15
|
+
@connection = connection
|
16
|
+
@path = path
|
17
|
+
@klass = klass
|
18
|
+
@parent = parent
|
19
|
+
@records = records
|
20
|
+
end
|
21
|
+
|
22
|
+
def build(attributes)
|
23
|
+
@klass.new(attributes.merge(parent: @parent))
|
24
|
+
end
|
25
|
+
|
26
|
+
def create(attributes)
|
27
|
+
build(attributes).tap(&:save)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Find and load a resource.
|
31
|
+
#
|
32
|
+
# If Resource#url_id is overridden, specify the attribute name.
|
33
|
+
# TODO: the resource should know its URL attribute name.
|
34
|
+
#
|
35
|
+
# Note that this currently triggers an HTTP GET, then a POST:
|
36
|
+
# conversation.find(10).posts.create(attributes)
|
37
|
+
# As an alternative, this only triggers the HTTP POST:
|
38
|
+
# conversation.build(id: 10).posts.create(attributes)
|
39
|
+
def find(id, attribute = :id)
|
40
|
+
build(id: id).load
|
41
|
+
end
|
42
|
+
|
43
|
+
def each
|
44
|
+
records.each do |attributes|
|
45
|
+
yield build(attributes)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Cache array representation.
|
50
|
+
# Save building Resources for each record multiple times.
|
51
|
+
def to_a
|
52
|
+
@_to_a = super
|
53
|
+
end
|
54
|
+
|
55
|
+
def [](index)
|
56
|
+
to_a[index]
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def records
|
62
|
+
@records ||= @connection.get(path: @path, status: 200)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module The86::Client
|
2
|
+
class Site < Resource
|
3
|
+
|
4
|
+
attribute :id, Integer
|
5
|
+
attribute :name, String
|
6
|
+
attribute :slug, String
|
7
|
+
attribute :created_at, DateTime
|
8
|
+
attribute :updated_at, DateTime
|
9
|
+
|
10
|
+
path "sites"
|
11
|
+
has_many :conversations, ->{ Conversation }
|
12
|
+
|
13
|
+
def url_id
|
14
|
+
slug
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
|
5
|
+
describe "Conversations" do
|
6
|
+
|
7
|
+
describe "listing conversations" do
|
8
|
+
it "returns empty array for site without conversations" do
|
9
|
+
expect_get_conversations(response_body: [])
|
10
|
+
site.conversations.to_a.size.must_equal 0
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns collection of conversations" do
|
14
|
+
expect_get_conversations(response_body: [{id: 10}, {id: 12}])
|
15
|
+
conversations = site.conversations
|
16
|
+
conversations.to_a.size.must_equal 2
|
17
|
+
c = conversations.first
|
18
|
+
c.must_be_instance_of Conversation
|
19
|
+
c.id.must_equal 10
|
20
|
+
c.site.must_equal site
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "creating conversations" do
|
25
|
+
it "posts and returns a conversation with the first post content" do
|
26
|
+
expect_request(
|
27
|
+
url: "https://example.org/api/v1/sites/test/conversations",
|
28
|
+
method: :post,
|
29
|
+
status: 201,
|
30
|
+
request_body: {content: "A new conversation."},
|
31
|
+
response_body: {id: 2, posts: [{id: 5, content: "A new conversation."}]},
|
32
|
+
request_headers: {"Authorization" => "Bearer secrettoken"},
|
33
|
+
)
|
34
|
+
|
35
|
+
c = site.conversations.create(
|
36
|
+
content: "A new conversation.",
|
37
|
+
oauth_token: "secrettoken",
|
38
|
+
)
|
39
|
+
|
40
|
+
c.id.must_equal 2
|
41
|
+
posts = c.posts
|
42
|
+
posts.to_a.length.must_equal 1
|
43
|
+
posts[0].content.must_equal "A new conversation."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "finding a conversation" do
|
48
|
+
it "gets the conversation, loads data into the resource" do
|
49
|
+
expect_request(
|
50
|
+
url: "https://user:pass@example.org/api/v1/sites/test/conversations/4",
|
51
|
+
method: :get,
|
52
|
+
status: 200,
|
53
|
+
response_body: {id: 4, posts: [{id: 8, content: "A post."}]},
|
54
|
+
)
|
55
|
+
c = site.conversations.find(4)
|
56
|
+
c.id.must_equal 4
|
57
|
+
c.posts.first.content.must_equal "A post."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def site
|
62
|
+
The86::Client.site("test")
|
63
|
+
end
|
64
|
+
|
65
|
+
def expect_get_conversations(options)
|
66
|
+
expect_request({
|
67
|
+
url: "https://user:pass@example.org/api/v1/sites/test/conversations",
|
68
|
+
method: :get,
|
69
|
+
status: 200,
|
70
|
+
}.merge(options))
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
describe OauthBearerAuthorization do
|
5
|
+
|
6
|
+
it "sets Authorization header, calls delegate" do
|
7
|
+
env = {request_headers: {}}
|
8
|
+
|
9
|
+
app = MiniTest::Mock.new
|
10
|
+
app.expect(:call, nil, [env])
|
11
|
+
|
12
|
+
OauthBearerAuthorization.new(app, "secret").call(env)
|
13
|
+
|
14
|
+
env[:request_headers]["Authorization"].must_equal "Bearer secret"
|
15
|
+
app.verify
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/spec/post_spec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
require "the86-client/post"
|
3
|
+
|
4
|
+
module The86::Client
|
5
|
+
describe Post do
|
6
|
+
describe "#reply?" do
|
7
|
+
it "is true when in_reply_to_id is present" do
|
8
|
+
Post.new(in_reply_to_id: 32).reply?.must_equal true
|
9
|
+
end
|
10
|
+
it "is false when in_reply_to_id is not present" do
|
11
|
+
Post.new(in_reply_to_id: nil).reply?.must_equal false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/spec/posts_spec.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
|
5
|
+
describe Post do
|
6
|
+
|
7
|
+
describe "replying to a post" do
|
8
|
+
it "sends in_reply_to_id" do
|
9
|
+
expect_request(
|
10
|
+
url: "https://example.org/api/v1/sites/test/conversations/32/posts",
|
11
|
+
method: :post,
|
12
|
+
status: 201,
|
13
|
+
request_body: {content: "Hi!", in_reply_to_id: 64},
|
14
|
+
response_body: {
|
15
|
+
id: 96, content: "Hi!", in_reply_to_id: 64, in_reply_to: {
|
16
|
+
id: 64,
|
17
|
+
content: "Hello!",
|
18
|
+
user: {
|
19
|
+
id: 128,
|
20
|
+
name: "Johnny Original"
|
21
|
+
}
|
22
|
+
}
|
23
|
+
},
|
24
|
+
request_headers: {"Authorization" => "Bearer SecretTokenHere"},
|
25
|
+
)
|
26
|
+
post = original_post.reply(
|
27
|
+
content: "Hi!",
|
28
|
+
oauth_token: "SecretTokenHere",
|
29
|
+
)
|
30
|
+
post.conversation.id.must_equal conversation.id
|
31
|
+
post.in_reply_to_id.must_equal original_post.id
|
32
|
+
post.in_reply_to.user.name.must_equal "Johnny Original"
|
33
|
+
post.content.must_equal "Hi!"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "following up to a conversation" do
|
38
|
+
it "creates new Post in the Conversation" do
|
39
|
+
expect_request(
|
40
|
+
url: "https://example.org/api/v1/sites/test/conversations/32/posts",
|
41
|
+
method: :post,
|
42
|
+
status: 201,
|
43
|
+
request_body: {content: "+1"},
|
44
|
+
response_body: {id: 96, content: "+1"},
|
45
|
+
request_headers: {"Authorization" => "Bearer SecretTokenHere"},
|
46
|
+
)
|
47
|
+
post = conversation.posts.create(
|
48
|
+
content: "+1",
|
49
|
+
oauth_token: "SecretTokenHere",
|
50
|
+
)
|
51
|
+
post.conversation.id.must_equal conversation.id
|
52
|
+
post.in_reply_to_id.must_equal nil
|
53
|
+
post.content.must_equal "+1"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#user" do
|
58
|
+
let(:post) { Post.new(id: 1, user: {id: 2, name: "John Citizen"}) }
|
59
|
+
it "returns instance of The86::Client::User" do
|
60
|
+
post.user.must_be_instance_of(The86::Client::User)
|
61
|
+
end
|
62
|
+
it "contains the user details" do
|
63
|
+
post.user.name.must_equal "John Citizen"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def original_post
|
68
|
+
Post.new(id: 64, conversation: conversation, content: "Hello!")
|
69
|
+
end
|
70
|
+
|
71
|
+
def conversation
|
72
|
+
Conversation.new(id: 32, site: site)
|
73
|
+
end
|
74
|
+
|
75
|
+
def site
|
76
|
+
The86::Client.site("test")
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
describe Resource do
|
5
|
+
|
6
|
+
describe "equality" do
|
7
|
+
resource = Class.new(Resource) do
|
8
|
+
attribute :id, Integer
|
9
|
+
attribute :code, String
|
10
|
+
end
|
11
|
+
|
12
|
+
parent_resource = Class.new(Resource) do
|
13
|
+
attribute :id, Integer
|
14
|
+
end
|
15
|
+
|
16
|
+
it "is equal by attributes" do
|
17
|
+
resource.new(id: 1, code: "A").
|
18
|
+
must_equal resource.new(id: 1, code: "A")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "is inequal by attributes" do
|
22
|
+
resource.new(id: 2, code: "A").
|
23
|
+
wont_equal resource.new(id: 1, code: "A")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "is inequal by parent" do
|
27
|
+
resource.new(code: "A", parent: parent_resource.new(id: 1)).
|
28
|
+
wont_equal resource.new(code: "A", parent: parent_resource.new(id: 2))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require "json"
|
2
|
+
require "webmock/minitest"
|
3
|
+
|
4
|
+
WebMock.disable_net_connect!
|
5
|
+
|
6
|
+
##
|
7
|
+
# WebMock request expectations.
|
8
|
+
module RequestExpectations
|
9
|
+
|
10
|
+
def self.included(klass)
|
11
|
+
klass.before { @expected_requests = [] }
|
12
|
+
klass.after { @expected_requests.each { |sr| assert_requested(sr) } }
|
13
|
+
end
|
14
|
+
|
15
|
+
def stub_and_assert_request(*parameters)
|
16
|
+
stub_request(*parameters).tap do |request|
|
17
|
+
@expected_requests << request
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def expect_request(options)
|
22
|
+
url = options.fetch(:url)
|
23
|
+
method = options.fetch(:method)
|
24
|
+
request_body = options[:request_body]
|
25
|
+
response_body = options[:response_body]
|
26
|
+
request_headers = options[:request_headers]
|
27
|
+
|
28
|
+
request = {}
|
29
|
+
request[:body] = request_body if request_body
|
30
|
+
request[:headers] = request_headers if request_headers
|
31
|
+
|
32
|
+
response = {status: options[:status] || 200}
|
33
|
+
response[:body] = JSON.generate(response_body) if response_body
|
34
|
+
|
35
|
+
stub_and_assert_request(method, url).
|
36
|
+
with(request).
|
37
|
+
to_return(response)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
MiniTest::Spec.send(:include, RequestExpectations)
|
data/spec/users_spec.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
|
3
|
+
module The86::Client
|
4
|
+
|
5
|
+
describe "Users" do
|
6
|
+
|
7
|
+
describe "creating a user" do
|
8
|
+
it "is successful with 200 OK" do
|
9
|
+
expect_create_user(response_body: {id: 1, name: "John Appleseed"})
|
10
|
+
user = The86::Client.users.create(name: "John Appleseed")
|
11
|
+
user.id.must_be_instance_of Fixnum
|
12
|
+
user.name.must_equal "John Appleseed"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "raises error for 200 OK" do
|
16
|
+
expect_create_user(status: 200, response_body: {id: 1, name: "John Appleseed"})
|
17
|
+
->{ The86::Client.users.create(name: "John Appleseed") }.must_raise Error
|
18
|
+
end
|
19
|
+
|
20
|
+
it "raises ValidationFailed for 422 response" do
|
21
|
+
expect_create_user(status: 422, request_body: {name: ""})
|
22
|
+
->{ The86::Client.users.create(name: "") }.must_raise ValidationFailed
|
23
|
+
end
|
24
|
+
|
25
|
+
it "raises Unauthorized for 401 response" do
|
26
|
+
expect_create_user(status: 401, request_body: {name: "Test"})
|
27
|
+
->{ The86::Client.users.create(name: "Test") }.must_raise Unauthorized
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "updating a user (may not be supported by server)" do
|
32
|
+
it "PATCHes users/:id" do
|
33
|
+
time_before = DateTime.now - 86400
|
34
|
+
time_after = DateTime.now
|
35
|
+
expect_request(
|
36
|
+
url: "https://user:pass@example.org/api/v1/users/5",
|
37
|
+
method: :patch,
|
38
|
+
status: 200,
|
39
|
+
request_body: {name: "New Name"},
|
40
|
+
response_body: {id: 5, name: "New Name", updated_at: time_after}
|
41
|
+
)
|
42
|
+
user = User.new(
|
43
|
+
id: 5,
|
44
|
+
name: "Old Name",
|
45
|
+
updated_at: time_before
|
46
|
+
)
|
47
|
+
user.name = "New Name"
|
48
|
+
user.updated_at.to_s.must_equal time_before.to_s
|
49
|
+
user.save
|
50
|
+
user.updated_at.to_s.must_equal time_after.to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "listing users (may not be supported by server)" do
|
55
|
+
it "returns collection of users" do
|
56
|
+
expect_request(
|
57
|
+
url: "https://user:pass@example.org/api/v1/users",
|
58
|
+
method: :get,
|
59
|
+
status: 200,
|
60
|
+
response_body: [{id: 4, name: "Paul"}, {id: 8, name: "James"}]
|
61
|
+
)
|
62
|
+
users = The86::Client.users
|
63
|
+
users.to_a.length.must_equal 2
|
64
|
+
users.first.name.must_equal "Paul"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def expect_create_user(options = {})
|
69
|
+
expect_request({
|
70
|
+
url: "https://user:pass@example.org/api/v1/users",
|
71
|
+
method: :post,
|
72
|
+
status: 201,
|
73
|
+
}.merge(options))
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|