the86-client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in the86-client.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard "minitest" do
2
+ watch(%r{^spec/(.*)_spec\.rb})
3
+ watch(%r{^lib/(.*)([^/]+)\.rb}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
4
+ watch(%r{^spec/spec_helper\.rb}) { "spec" }
5
+ end
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
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ task default: :test
5
+
6
+ # MiniTest
7
+ require "rake/testtask"
8
+ Rake::TestTask.new do |t|
9
+ ENV["TESTOPTS"] = "-v"
10
+ t.pattern = "spec/*_spec.rb"
11
+ end
@@ -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