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