the86-client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 SitePoint Pty Ltd
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
The86::Client
|
2
|
+
=============
|
3
|
+
|
4
|
+
Ruby client for "The 86" conversation API server.
|
5
|
+
|
6
|
+
Uses [Faraday][1] for HTTP transport, JSON encoding/decoding etc.
|
7
|
+
|
8
|
+
Uses [Virtus][2] for DataMapper-style object attribute declaration.
|
9
|
+
|
10
|
+
Uses [MiniTest][3] and [WebMock][4] for unit/integration testing (run `rake`).
|
11
|
+
|
12
|
+
|
13
|
+
[1]: https://github.com/technoweenie/faraday
|
14
|
+
[2]: https://github.com/solnic/virtus
|
15
|
+
[3]: https://github.com/seattlerb/minitest
|
16
|
+
[4]: https://github.com/bblimke/webmock
|
17
|
+
|
18
|
+
|
19
|
+
Get Code, Run Tests
|
20
|
+
-------------------
|
21
|
+
|
22
|
+
git clone REPO_PATH
|
23
|
+
cd the86-client
|
24
|
+
bundle install
|
25
|
+
bundle exec rake
|
26
|
+
|
27
|
+
|
28
|
+
Install the Gem
|
29
|
+
---------------
|
30
|
+
|
31
|
+
gem install the86-client
|
32
|
+
|
33
|
+
|
34
|
+
Add to a Project
|
35
|
+
----------------
|
36
|
+
|
37
|
+
echo 'gem "the86-client"' >> Gemfile
|
38
|
+
bundle
|
39
|
+
|
40
|
+
|
41
|
+
Usage
|
42
|
+
-----
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
# The domain running The 86 discussion server.
|
46
|
+
The86::Client.domain = "the86.yourdomain.com"
|
47
|
+
|
48
|
+
# HTTP Basic Auth credentials allocated for your API client.
|
49
|
+
The86::Client.credentials = ["username", "password"]
|
50
|
+
|
51
|
+
# Create an end-user account:
|
52
|
+
user = The86::Client.users.create(name: "John Citizen")
|
53
|
+
oauth_token = user.access_tokens.first.token
|
54
|
+
|
55
|
+
# Create a new conversation:
|
56
|
+
conversation = The86::Client.site("example").conversations.create(
|
57
|
+
content: "Hello world!",
|
58
|
+
oauth_token: oauth_token
|
59
|
+
)
|
60
|
+
|
61
|
+
# Reply as another user:
|
62
|
+
user = The86::Client.users.create(name: "Jane Taxpayer")
|
63
|
+
conversation.posts.first.reply(
|
64
|
+
content: "I concur!",
|
65
|
+
oauth_token: user.access_tokens.first.token
|
66
|
+
)
|
67
|
+
|
68
|
+
# Follow up to a conversation:
|
69
|
+
user = The86::Client.users.create(name: "Joe Six-pack")
|
70
|
+
conversation.posts.create(
|
71
|
+
content: "What are you guys talking about?",
|
72
|
+
oauth_token: user.access_tokens.first.token
|
73
|
+
)
|
74
|
+
```
|
75
|
+
|
76
|
+
|
77
|
+
Licence
|
78
|
+
-------
|
79
|
+
|
80
|
+
(c) SitePoint, 2012, MIT license.
|
data/Rakefile
ADDED
data/lib/the86-client.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
%w{
|
2
|
+
version
|
3
|
+
connection
|
4
|
+
errors
|
5
|
+
oauth_bearer_authorization
|
6
|
+
|
7
|
+
resource
|
8
|
+
resource_collection
|
9
|
+
user
|
10
|
+
site
|
11
|
+
post
|
12
|
+
conversation
|
13
|
+
}.each { |r| require "the86-client/#{r}" }
|
14
|
+
|
15
|
+
module The86
|
16
|
+
module Client
|
17
|
+
|
18
|
+
# API entry points.
|
19
|
+
|
20
|
+
def self.sites
|
21
|
+
ResourceCollection.new(
|
22
|
+
Connection.new,
|
23
|
+
"sites",
|
24
|
+
Site,
|
25
|
+
nil
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.site(slug)
|
30
|
+
Site.new(slug: slug)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.users
|
34
|
+
ResourceCollection.new(
|
35
|
+
Connection.new,
|
36
|
+
"users",
|
37
|
+
User,
|
38
|
+
nil
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Configuration.
|
43
|
+
class << self
|
44
|
+
|
45
|
+
attr_writer :domain
|
46
|
+
attr_writer :credentials
|
47
|
+
|
48
|
+
def domain
|
49
|
+
@domain ||
|
50
|
+
raise("Domain not configured: #{name}.domain = \"example.org\"")
|
51
|
+
end
|
52
|
+
|
53
|
+
def credentials
|
54
|
+
@credentials ||
|
55
|
+
raise("Credentials not configured: #{name}.credentials = [username, password]")
|
56
|
+
end
|
57
|
+
|
58
|
+
def disable_https!
|
59
|
+
@scheme = "http"
|
60
|
+
end
|
61
|
+
|
62
|
+
def scheme
|
63
|
+
@scheme || "https"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Patches The86::Client::Resource to implement enough of
|
2
|
+
# ActiveModel's interface to act as a form object.
|
3
|
+
# Depends on ActiveModel being provided externally.
|
4
|
+
|
5
|
+
module The86
|
6
|
+
module Client
|
7
|
+
class Resource
|
8
|
+
|
9
|
+
extend ActiveModel::Naming
|
10
|
+
|
11
|
+
include ActiveModel::Validations
|
12
|
+
|
13
|
+
# Model name relative to The86::Client namespace.
|
14
|
+
# Names form fields like post[...] instead of the86_client_post[...].
|
15
|
+
def self.model_name
|
16
|
+
ActiveModel::Name.new(self, The86::Client)
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_model
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def persisted?
|
24
|
+
!!id
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_key
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_param
|
32
|
+
url_id
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "faraday"
|
2
|
+
require "faraday_middleware"
|
3
|
+
|
4
|
+
module The86
|
5
|
+
module Client
|
6
|
+
class Connection
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@faraday = Faraday.new(url) do |conn|
|
10
|
+
conn.request :json
|
11
|
+
conn.response :json
|
12
|
+
conn.basic_auth(*Client.credentials)
|
13
|
+
conn.adapter Faraday.default_adapter
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def prepend(*parameters)
|
18
|
+
@faraday.builder.insert(0, *parameters)
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(options)
|
22
|
+
dispatch(:get, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def patch(options)
|
26
|
+
dispatch(:patch, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def post(options)
|
30
|
+
dispatch(:post, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
def dispatch(method, options)
|
34
|
+
path = options.fetch(:path)
|
35
|
+
data = options[:data]
|
36
|
+
response = @faraday.run_request(method, path, data, @faraday.headers)
|
37
|
+
assert_http_status(response, options[:status])
|
38
|
+
response.body
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def url
|
44
|
+
"%s://%s/api/v1" % [ Client.scheme, Client.domain ]
|
45
|
+
end
|
46
|
+
|
47
|
+
def assert_http_status(response, status)
|
48
|
+
case response.status
|
49
|
+
when nil, status then return
|
50
|
+
when 401
|
51
|
+
raise Unauthorized
|
52
|
+
when 422
|
53
|
+
raise ValidationFailed, "Validation failed: #{response.body.to_s}"
|
54
|
+
when 500
|
55
|
+
raise ServerError, internal_server_error_message(status, response)
|
56
|
+
else
|
57
|
+
raise Error, "Expected HTTP #{status}, got HTTP #{response.status}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def internal_server_error_message(expected_status, response)
|
62
|
+
body = response.body
|
63
|
+
if body["type"] && body["message"] && body["backtrace"]
|
64
|
+
"Expected HTTP %d, got HTTP %d with error:\n%s\n%s\n\n%s" % [
|
65
|
+
expected_status,
|
66
|
+
response.status,
|
67
|
+
response.body["type"],
|
68
|
+
response.body["message"],
|
69
|
+
response.body["backtrace"].join("\n"),
|
70
|
+
]
|
71
|
+
else
|
72
|
+
"Expected HTTP %d, got %d" % [
|
73
|
+
expected_status,
|
74
|
+
response.status,
|
75
|
+
]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module The86::Client
|
2
|
+
class Conversation < Resource
|
3
|
+
|
4
|
+
attribute :id, Integer
|
5
|
+
attribute :content, String # For creating new Conversation.
|
6
|
+
attribute :created_at, DateTime
|
7
|
+
attribute :updated_at, DateTime
|
8
|
+
|
9
|
+
path "conversations"
|
10
|
+
belongs_to :site
|
11
|
+
has_many :posts, ->{ Post }
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module The86
|
2
|
+
module Client
|
3
|
+
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class ServerError < Error
|
8
|
+
end
|
9
|
+
|
10
|
+
class Unauthorized < Error
|
11
|
+
def message
|
12
|
+
"Unauthorized"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ValidationFailed < Error
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module The86::Client
|
2
|
+
|
3
|
+
# A Faraday middleware which adds or overwrites the
|
4
|
+
# Authorization header with an OAuth2 bearer token.
|
5
|
+
# See: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-22
|
6
|
+
class OauthBearerAuthorization < Faraday::Middleware
|
7
|
+
|
8
|
+
AUTH_HEADER = "Authorization"
|
9
|
+
|
10
|
+
def initialize(app, token)
|
11
|
+
super(app)
|
12
|
+
@token = token
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
env[:request_headers][AUTH_HEADER] = "Bearer #{@token}"
|
17
|
+
@app.call(env)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module The86::Client
|
2
|
+
class Post < Resource
|
3
|
+
|
4
|
+
attribute :id, Integer
|
5
|
+
attribute :content, String
|
6
|
+
attribute :in_reply_to_id, Integer
|
7
|
+
attribute :created_at, DateTime
|
8
|
+
attribute :updated_at, DateTime
|
9
|
+
|
10
|
+
path "posts"
|
11
|
+
belongs_to :conversation
|
12
|
+
has_one :user, ->{ User }
|
13
|
+
has_one :in_reply_to, ->{ Post }
|
14
|
+
|
15
|
+
def reply?
|
16
|
+
!!in_reply_to_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def reply(attributes)
|
20
|
+
conversation.posts.create(
|
21
|
+
attributes.merge(in_reply_to_id: id)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require "virtus"
|
2
|
+
|
3
|
+
module The86
|
4
|
+
module Client
|
5
|
+
class Resource
|
6
|
+
|
7
|
+
include Virtus
|
8
|
+
|
9
|
+
# Assigned by Virtus constructor.
|
10
|
+
attr_accessor :oauth_token
|
11
|
+
|
12
|
+
# Parent resource, see belongs_to.
|
13
|
+
attr_accessor :parent
|
14
|
+
|
15
|
+
##
|
16
|
+
# Declarative API for subclasses.
|
17
|
+
|
18
|
+
class << self
|
19
|
+
|
20
|
+
# The path component for the collection, e.g. "discussions"
|
21
|
+
def path(path)
|
22
|
+
@path = path
|
23
|
+
end
|
24
|
+
|
25
|
+
# The name of the parent resource attribute.
|
26
|
+
# e.g: belongs_to :site
|
27
|
+
def belongs_to(name)
|
28
|
+
alias_method "#{name}=", :parent=
|
29
|
+
alias_method name, :parent
|
30
|
+
end
|
31
|
+
|
32
|
+
# The name of a child collection.
|
33
|
+
def has_many(name, class_proc)
|
34
|
+
define_method "#{name}=" do |items|
|
35
|
+
(@_has_many ||= {})[name] = items
|
36
|
+
end
|
37
|
+
define_method name do
|
38
|
+
has_many_reader(name, class_proc)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_one(name, class_proc)
|
43
|
+
define_method "#{name}=" do |instance|
|
44
|
+
(@_has_one ||= {})[name] = instance
|
45
|
+
end
|
46
|
+
define_method name do
|
47
|
+
has_one_reader(name, class_proc)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Class methods.
|
55
|
+
|
56
|
+
def self.collection_path(parent)
|
57
|
+
[parent && parent.resource_path, @path].compact.join("/")
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Instance methods
|
62
|
+
|
63
|
+
# The value of the identifier in the URL; numeric ID or string slug.
|
64
|
+
# TODO: see ResourceCollection#find.
|
65
|
+
def url_id
|
66
|
+
id
|
67
|
+
end
|
68
|
+
|
69
|
+
def resource_path
|
70
|
+
"%s/%s" % [ self.class.collection_path(@parent), url_id ]
|
71
|
+
end
|
72
|
+
|
73
|
+
def save
|
74
|
+
id ? save_existing : save_new
|
75
|
+
end
|
76
|
+
|
77
|
+
def load
|
78
|
+
self.attributes = connection.get(
|
79
|
+
path: resource_path,
|
80
|
+
status: 200
|
81
|
+
)
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def sendable_attributes
|
86
|
+
attributes.reject do |key, value|
|
87
|
+
[:id, :created_at, :updated_at].include?(key) ||
|
88
|
+
value.kind_of?(Resource) ||
|
89
|
+
value.nil?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def ==(other)
|
94
|
+
attributes == other.attributes &&
|
95
|
+
parent == other.parent
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def save_new
|
101
|
+
self.attributes = connection.post(
|
102
|
+
path: self.class.collection_path(@parent),
|
103
|
+
data: sendable_attributes,
|
104
|
+
status: 201
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def save_existing
|
109
|
+
self.attributes = connection.patch(
|
110
|
+
path: resource_path,
|
111
|
+
data: sendable_attributes,
|
112
|
+
status: 200
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
def connection
|
117
|
+
Connection.new.tap do |c|
|
118
|
+
c.prepend OauthBearerAuthorization, oauth_token if oauth_token
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def has_many_reader(name, class_proc)
|
123
|
+
klass = class_proc.call
|
124
|
+
ResourceCollection.new(
|
125
|
+
connection,
|
126
|
+
klass.collection_path(self),
|
127
|
+
class_proc.call,
|
128
|
+
self,
|
129
|
+
(@_has_many || {})[name] || nil
|
130
|
+
)
|
131
|
+
rescue KeyError
|
132
|
+
raise Error, "No reference to children :#{name}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def has_one_reader(name, class_proc)
|
136
|
+
klass = class_proc.call
|
137
|
+
record = (@_has_one || {}).fetch(name)
|
138
|
+
if record.is_a?(Resource)
|
139
|
+
record
|
140
|
+
else
|
141
|
+
@_has_one[name] = klass.new(record)
|
142
|
+
end
|
143
|
+
rescue KeyError
|
144
|
+
raise Error, "No reference to child :#{name}"
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|