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
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
|