tent-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/.rspec +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +6 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +24 -0
- data/Rakefile +8 -0
- data/lib/tent-client.rb +81 -0
- data/lib/tent-client/app.rb +33 -0
- data/lib/tent-client/app_authorization.rb +21 -0
- data/lib/tent-client/cycle_http.rb +49 -0
- data/lib/tent-client/discovery.rb +56 -0
- data/lib/tent-client/follower.rb +37 -0
- data/lib/tent-client/following.rb +31 -0
- data/lib/tent-client/group.rb +19 -0
- data/lib/tent-client/link_header.rb +72 -0
- data/lib/tent-client/middleware/accept_header.rb +14 -0
- data/lib/tent-client/middleware/encode_json.rb +48 -0
- data/lib/tent-client/middleware/mac_auth.rb +50 -0
- data/lib/tent-client/post.rb +66 -0
- data/lib/tent-client/post_attachment.rb +11 -0
- data/lib/tent-client/profile.rb +17 -0
- data/lib/tent-client/version.rb +3 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/unit/cycle_http_spec.rb +84 -0
- data/spec/unit/discovery_spec.rb +46 -0
- data/spec/unit/link_header_spec.rb +24 -0
- data/spec/unit/middleware/mac_auth_spec.rb +68 -0
- data/tent-client.gemspec +31 -0
- metadata +248 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color --backtrace --format documentation
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Apollic Software, LLC
|
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,24 @@
|
|
1
|
+
# Tent Ruby Client ![Build Status](https://magnum.travis-ci.com/tent/tent-client-ruby.png?branch=master&token=YmxLSPgdpsxNUMWJpzRx)
|
2
|
+
|
3
|
+
TentClient implements a [Tent Protocol](http://tent.io) client library in Ruby.
|
4
|
+
It is incomplete, currently only the endpoints required by
|
5
|
+
[tentd](https://github.com/tent/tentd) and
|
6
|
+
[tentd-admin](https://github.com/tent/tentd-admin) have been implemented.
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
# Tent profile discovery
|
12
|
+
TentClient.new.discover("http://tent-user.example.org")
|
13
|
+
|
14
|
+
# Server communication
|
15
|
+
client = TentClient.new('http://tent-user.example.org',
|
16
|
+
:mac_key_id => 'be94a6bf',
|
17
|
+
:mac_key => '974af035',
|
18
|
+
:mac_algorithm => 'hmac-sha-256')
|
19
|
+
client.following.create('http://another-tent.example.com')
|
20
|
+
```
|
21
|
+
|
22
|
+
## Contributions
|
23
|
+
|
24
|
+
If you find missing endpoints/actions, please submit a pull request.
|
data/Rakefile
ADDED
data/lib/tent-client.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'tent-client/version'
|
2
|
+
require 'oj'
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday_middleware'
|
5
|
+
require 'faraday_middleware/multi_json'
|
6
|
+
|
7
|
+
class TentClient
|
8
|
+
autoload :Discovery, 'tent-client/discovery'
|
9
|
+
autoload :LinkHeader, 'tent-client/link_header'
|
10
|
+
autoload :Follower, 'tent-client/follower'
|
11
|
+
autoload :Following, 'tent-client/following'
|
12
|
+
autoload :Group, 'tent-client/group'
|
13
|
+
autoload :Profile, 'tent-client/profile'
|
14
|
+
autoload :App, 'tent-client/app'
|
15
|
+
autoload :AppAuthorization, 'tent-client/app_authorization'
|
16
|
+
autoload :Post, 'tent-client/post'
|
17
|
+
autoload :PostAttachment, 'tent-client/post_attachment'
|
18
|
+
autoload :CycleHTTP, 'tent-client/cycle_http'
|
19
|
+
|
20
|
+
require 'tent-client/middleware/accept_header'
|
21
|
+
require 'tent-client/middleware/mac_auth'
|
22
|
+
require 'tent-client/middleware/encode_json'
|
23
|
+
|
24
|
+
MEDIA_TYPE = 'application/vnd.tent.v0+json'.freeze
|
25
|
+
PROFILE_REL = 'https://tent.io/rels/profile'.freeze
|
26
|
+
|
27
|
+
attr_reader :faraday_adapter, :server_urls
|
28
|
+
|
29
|
+
def initialize(server_urls = [], options={})
|
30
|
+
@server_urls = Array(server_urls)
|
31
|
+
@faraday_adapter = options.delete(:faraday_adapter)
|
32
|
+
@options = options
|
33
|
+
end
|
34
|
+
|
35
|
+
def http
|
36
|
+
@http ||= CycleHTTP.new(self) do |f|
|
37
|
+
f.use Middleware::EncodeJson unless @options[:skip_serialization]
|
38
|
+
f.response :multi_json, :content_type => /\bjson\Z/ unless @options[:skip_serialization]
|
39
|
+
f.use Middleware::AcceptHeader
|
40
|
+
f.use Middleware::MacAuth, @options
|
41
|
+
f.adapter *Array(faraday_adapter)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def faraday_adapter
|
46
|
+
@faraday_adapter || Faraday.default_adapter
|
47
|
+
end
|
48
|
+
|
49
|
+
def server_url=(v)
|
50
|
+
@server_urls = Array(v)
|
51
|
+
@http = nil # reset Faraday connection
|
52
|
+
end
|
53
|
+
|
54
|
+
def discover(url)
|
55
|
+
Discovery.new(self, url).tap { |d| d.perform }
|
56
|
+
end
|
57
|
+
|
58
|
+
def follower
|
59
|
+
Follower.new(self)
|
60
|
+
end
|
61
|
+
|
62
|
+
def following
|
63
|
+
Following.new(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def group
|
67
|
+
Group.new(self)
|
68
|
+
end
|
69
|
+
|
70
|
+
def app
|
71
|
+
App.new(self)
|
72
|
+
end
|
73
|
+
|
74
|
+
def post
|
75
|
+
Post.new(self)
|
76
|
+
end
|
77
|
+
|
78
|
+
def profile
|
79
|
+
Profile.new(self)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class TentClient
|
2
|
+
class App
|
3
|
+
attr_accessor :client
|
4
|
+
|
5
|
+
def initialize(client)
|
6
|
+
@client = client
|
7
|
+
end
|
8
|
+
|
9
|
+
def create(data)
|
10
|
+
@client.http.post("apps", data)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(id)
|
14
|
+
@client.http.get("apps/#{id}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def list(params = {})
|
18
|
+
@client.http.get("apps", params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(id)
|
22
|
+
@client.http.delete("apps/#{id}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def update(id, data)
|
26
|
+
@client.http.put("apps/#{id}", data)
|
27
|
+
end
|
28
|
+
|
29
|
+
def authorization
|
30
|
+
AppAuthorization.new(@client)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class TentClient
|
2
|
+
class AppAuthorization
|
3
|
+
attr_accessor :client
|
4
|
+
|
5
|
+
def initialize(client)
|
6
|
+
@client = client
|
7
|
+
end
|
8
|
+
|
9
|
+
def create(app_id, data)
|
10
|
+
@client.http.post("apps/#{app_id}/authorizations", data)
|
11
|
+
end
|
12
|
+
|
13
|
+
def update(app_id, id, data)
|
14
|
+
@client.http.put("apps/#{app_id}/authorizations/#{id}", data)
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete(app_id, id)
|
18
|
+
@client.http.delete("apps/#{app_id}/authorizations/#{id}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class TentClient
|
2
|
+
|
3
|
+
# Proxies to Faraday and cycles through server urls
|
4
|
+
# until either non left or response status in the 200s or 400s
|
5
|
+
class CycleHTTP
|
6
|
+
attr_reader :client, :server_urls
|
7
|
+
def initialize(client, &faraday_block)
|
8
|
+
@faraday_block = faraday_block
|
9
|
+
@client = client
|
10
|
+
@server_urls = client.server_urls.dup
|
11
|
+
end
|
12
|
+
|
13
|
+
def new_http
|
14
|
+
@http = Faraday.new(:url => server_urls.shift) do |f|
|
15
|
+
@faraday_block.call(f)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def http
|
20
|
+
@http || new_http
|
21
|
+
end
|
22
|
+
|
23
|
+
%w{ head get put post patch delete options }.each do |verb|
|
24
|
+
define_method verb do |*args, &block|
|
25
|
+
res = http.send(verb, *args, &block)
|
26
|
+
return res unless server_urls.any?
|
27
|
+
case res.status
|
28
|
+
when 200...300, 400...500
|
29
|
+
res
|
30
|
+
else
|
31
|
+
new_http
|
32
|
+
send(verb, *args, &block)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def respond_to_missing?(method_name, include_private = false)
|
38
|
+
http.respond_to?(method_name, include_private)
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing(method_name, *args, &block)
|
42
|
+
if http.respond_to?(method_name)
|
43
|
+
http.send(method_name, *args, &block)
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class TentClient
|
4
|
+
class Discovery
|
5
|
+
attr_accessor :url, :profile_urls, :primary_profile_url, :profile
|
6
|
+
|
7
|
+
def initialize(client, url)
|
8
|
+
@client, @url = client, url
|
9
|
+
end
|
10
|
+
|
11
|
+
def http
|
12
|
+
@http ||= Faraday.new do |f|
|
13
|
+
f.response :follow_redirects
|
14
|
+
f.adapter *Array(@client.faraday_adapter)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def perform
|
19
|
+
@profile_urls = perform_head_discovery || perform_get_discovery || []
|
20
|
+
@profile_urls.map! { |l| l =~ %r{\A/} ? URI.join(url, l).to_s : l }
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_profile
|
24
|
+
profile_urls.each do |url|
|
25
|
+
res = @client.http.get(url)
|
26
|
+
if res['Content-Type'].split(';').first == MEDIA_TYPE
|
27
|
+
@profile = res.body
|
28
|
+
@primary_profile_url = url
|
29
|
+
break
|
30
|
+
end
|
31
|
+
end
|
32
|
+
[@profile, @primary_profile_url.to_s.sub(%r{/profile$}, '')]
|
33
|
+
end
|
34
|
+
|
35
|
+
def perform_head_discovery
|
36
|
+
perform_header_discovery http.head(url)
|
37
|
+
end
|
38
|
+
|
39
|
+
def perform_get_discovery
|
40
|
+
res = http.get(url)
|
41
|
+
perform_header_discovery(res) || perform_html_discovery(res)
|
42
|
+
end
|
43
|
+
|
44
|
+
def perform_header_discovery(res)
|
45
|
+
if header = res['Link']
|
46
|
+
links = LinkHeader.parse(header).links.select { |l| l[:rel] == PROFILE_REL }.map { |l| l.uri }
|
47
|
+
links unless links.empty?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def perform_html_discovery(res)
|
52
|
+
return unless res['Content-Type'] == 'text/html'
|
53
|
+
Nokogiri::HTML(res.body).css(%(link[rel="#{PROFILE_REL}"])).map { |l| l['href'] }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class TentClient
|
2
|
+
class Follower
|
3
|
+
def initialize(client)
|
4
|
+
@client = client
|
5
|
+
end
|
6
|
+
|
7
|
+
def create(data)
|
8
|
+
@client.http.post 'followers', data
|
9
|
+
end
|
10
|
+
|
11
|
+
def count(params={})
|
12
|
+
@client.http.get('followers/count', params)
|
13
|
+
end
|
14
|
+
|
15
|
+
def list(params = {})
|
16
|
+
@client.http.get "followers", params
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(id)
|
20
|
+
@client.http.get "followers/#{id}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def update(id, data)
|
24
|
+
@client.http.put "followers/#{id}", data
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(id)
|
28
|
+
@client.http.delete "followers/#{id}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def challenge(path)
|
32
|
+
str = SecureRandom.hex(32)
|
33
|
+
res = @client.http.get(path.sub(%r{\A/}, ''), :challenge => str)
|
34
|
+
res.status == 200 && res.body.match(/\A#{str}/)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class TentClient
|
2
|
+
class Following
|
3
|
+
def initialize(client)
|
4
|
+
@client = client
|
5
|
+
end
|
6
|
+
|
7
|
+
def count(params={})
|
8
|
+
@client.http.get('followings/count', params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def list(params = {})
|
12
|
+
@client.http.get 'followings', params
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(id, data)
|
16
|
+
@client.http.put "followings/#{id}", data
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(entity_uri)
|
20
|
+
@client.http.post 'followings', :entity => entity_uri.sub(%r{/$}, '')
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(id)
|
24
|
+
@client.http.get "followings/#{id}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(id)
|
28
|
+
@client.http.delete "followings/#{id}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class TentClient
|
2
|
+
class Group
|
3
|
+
def initialize(client)
|
4
|
+
@client = client
|
5
|
+
end
|
6
|
+
|
7
|
+
def count(params={})
|
8
|
+
@client.http.get('groups/count', params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def list(params)
|
12
|
+
@client.http.get('/groups', params)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create(data)
|
16
|
+
@client.http.post('/groups', data)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'strscan'
|
3
|
+
|
4
|
+
class TentClient
|
5
|
+
class LinkHeader
|
6
|
+
attr_accessor :links
|
7
|
+
|
8
|
+
def self.parse(header)
|
9
|
+
new header.split(',').map { |l| Link.parse(l) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(links)
|
13
|
+
@links = Array(links)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
links.map(&:to_s).join(', ')
|
18
|
+
end
|
19
|
+
|
20
|
+
class Link
|
21
|
+
attr_accessor :uri, :attributes
|
22
|
+
|
23
|
+
def self.parse(link_text)
|
24
|
+
s = StringScanner.new(link_text)
|
25
|
+
s.scan(/[^<]+/)
|
26
|
+
link = s.scan(/<[^\s]+>/)
|
27
|
+
link = link[1..-2]
|
28
|
+
|
29
|
+
s.scan(/[^a-z]+/)
|
30
|
+
attrs = {}
|
31
|
+
while attr = s.scan(/[a-z0-9*\-]+=/)
|
32
|
+
next if attr =~ /\*/
|
33
|
+
val = s.scan(/".+?"|[^\s";]+/).sub(/\A"/, '').sub(/"\Z/, '')
|
34
|
+
attrs[attr[0..-2]] = val
|
35
|
+
s.scan(/[^a-z]+/)
|
36
|
+
end
|
37
|
+
new(link, attrs)
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(uri, attributes = {})
|
41
|
+
@uri = uri
|
42
|
+
@attributes = indifferent_hash(attributes)
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(other)
|
46
|
+
false unless is_a?(self.class)
|
47
|
+
uri == other.uri && attributes == other.attributes
|
48
|
+
end
|
49
|
+
|
50
|
+
def [](k)
|
51
|
+
attributes[k]
|
52
|
+
end
|
53
|
+
|
54
|
+
def []=(k, v)
|
55
|
+
attributes[k.to_s] = v.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
attr_string = "; " + attributes.sort.map { |k,v| "#{k}=#{v.inspect}" }.join('; ') if attributes
|
60
|
+
"<#{uri}>#{attr_string}"
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def indifferent_hash(old_hash)
|
66
|
+
new_hash = Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
|
67
|
+
old_hash.each { |k,v| new_hash[k.to_s] = v.to_s }
|
68
|
+
new_hash
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'faraday_middleware'
|
2
|
+
|
3
|
+
class TentClient
|
4
|
+
module Middleware
|
5
|
+
# FaradayMiddleware::EncodeJson with our media type
|
6
|
+
# https://github.com/pengwynn/faraday_middleware/blob/master/lib/faraday_middleware/request/encode_json.rb
|
7
|
+
class EncodeJson < Faraday::Middleware
|
8
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
9
|
+
|
10
|
+
dependency do
|
11
|
+
require 'oj' unless defined?(::Oj)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
match_content_type(env) do |data|
|
16
|
+
env[:body] = encode data
|
17
|
+
end
|
18
|
+
@app.call env
|
19
|
+
end
|
20
|
+
|
21
|
+
def encode(data)
|
22
|
+
::Oj.dump data
|
23
|
+
end
|
24
|
+
|
25
|
+
def match_content_type(env)
|
26
|
+
if process_request?(env)
|
27
|
+
env[:request_headers][CONTENT_TYPE] ||= MEDIA_TYPE
|
28
|
+
yield env[:body] unless env[:body].respond_to?(:to_str)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_request?(env)
|
33
|
+
type = request_type(env)
|
34
|
+
has_body?(env) and (type.empty? or type == MEDIA_TYPE)
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_body?(env)
|
38
|
+
body = env[:body] and !(body.respond_to?(:to_str) and body.empty?)
|
39
|
+
end
|
40
|
+
|
41
|
+
def request_type(env)
|
42
|
+
type = env[:request_headers][CONTENT_TYPE].to_s
|
43
|
+
type = type.split(';', 2).first if type.index(';')
|
44
|
+
type
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
class TentClient
|
6
|
+
module Middleware
|
7
|
+
class MacAuth
|
8
|
+
def initialize(app, options={})
|
9
|
+
@app, @mac_key_id, @mac_key, @mac_algorithm = app, options[:mac_key_id], options[:mac_key], options[:mac_algorithm]
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
sign_request(env) if auth_enabled?
|
14
|
+
@app.call(env)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def auth_enabled?
|
20
|
+
@mac_key_id && @mac_key && @mac_algorithm
|
21
|
+
end
|
22
|
+
|
23
|
+
def sign_request(env)
|
24
|
+
time = Time.now.to_i
|
25
|
+
nonce = SecureRandom.hex(3)
|
26
|
+
request_string = build_request_string(time, nonce, env)
|
27
|
+
signature = Base64.encode64(OpenSSL::HMAC.digest(openssl_digest.new, @mac_key, request_string)).sub("\n", '')
|
28
|
+
env[:request_headers]['Authorization'] = build_auth_header(time, nonce, signature)
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_request_string(time, nonce, env)
|
32
|
+
if env[:body].respond_to?(:read)
|
33
|
+
body = env[:body].read
|
34
|
+
env[:body].rewind
|
35
|
+
else
|
36
|
+
body = env[:body]
|
37
|
+
end
|
38
|
+
[time.to_s, nonce, env[:method].to_s.upcase, env[:url].request_uri, env[:url].host, env[:url].port || env[:url].inferred_port, body, nil].join("\n")
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_auth_header(time, nonce, signature)
|
42
|
+
%Q(MAC id="#{@mac_key_id}", ts="#{time}", nonce="#{nonce}", mac="#{signature}")
|
43
|
+
end
|
44
|
+
|
45
|
+
def openssl_digest
|
46
|
+
@openssl_digest ||= OpenSSL::Digest.const_get(@mac_algorithm.to_s.gsub(/hmac|-/, '').upcase)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class TentClient
|
2
|
+
class Post
|
3
|
+
MULTIPART_TYPE = 'multipart/form-data'.freeze
|
4
|
+
MULTIPART_BOUNDARY = "-----------TentAttachment".freeze
|
5
|
+
|
6
|
+
def initialize(client)
|
7
|
+
@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
def count(params={})
|
11
|
+
@client.http.get('posts/count', params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def list(params = {})
|
15
|
+
@client.http.get('posts', params)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create(post, options={})
|
19
|
+
if options[:attachments]
|
20
|
+
multipart_post(post, options, options.delete(:attachments))
|
21
|
+
else
|
22
|
+
@client.http.post(options[:url] || 'posts', post)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def get(id)
|
27
|
+
@client.http.get("posts/#{id}")
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(id)
|
31
|
+
@client.http.delete("posts/#{id}")
|
32
|
+
end
|
33
|
+
|
34
|
+
def attachment
|
35
|
+
PostAttachment.new(@client)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def multipart_post(post, options, attachments)
|
41
|
+
post_body = { :category => 'post', :filename => 'post.json', :type => MEDIA_TYPE, :data => post.to_json }
|
42
|
+
body = multipart_body(attachments.unshift(post_body))
|
43
|
+
@client.http.post(options[:url] || 'posts', body, 'Content-Type' => "#{MULTIPART_TYPE};boundary=#{MULTIPART_BOUNDARY}")
|
44
|
+
end
|
45
|
+
|
46
|
+
def multipart_body(attachments)
|
47
|
+
parts = attachments.inject({}) { |h,a|
|
48
|
+
(h[a[:category]] ||= []) << a; h
|
49
|
+
}.inject([]) { |a,(category,attachments)|
|
50
|
+
if attachments.size > 1
|
51
|
+
a += attachments.each_with_index.map { |attachment,i|
|
52
|
+
Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, "#{category}[#{i}]", attachment_io(attachment))
|
53
|
+
}
|
54
|
+
else
|
55
|
+
a << Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, category, attachment_io(attachments.first))
|
56
|
+
end
|
57
|
+
a
|
58
|
+
} << Faraday::Parts::EpiloguePart.new(MULTIPART_BOUNDARY)
|
59
|
+
Faraday::CompositeReadIO.new(parts)
|
60
|
+
end
|
61
|
+
|
62
|
+
def attachment_io(attachment)
|
63
|
+
Faraday::UploadIO.new(attachment[:file] || StringIO.new(attachment[:data]), attachment[:type], attachment[:filename])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class TentClient
|
2
|
+
class Profile
|
3
|
+
attr_accessor :client
|
4
|
+
|
5
|
+
def initialize(client)
|
6
|
+
@client = client
|
7
|
+
end
|
8
|
+
|
9
|
+
def update(type, data)
|
10
|
+
@client.http.put "profile/#{URI.encode_www_form_component(type)}", data
|
11
|
+
end
|
12
|
+
|
13
|
+
def get
|
14
|
+
@client.http.get 'profile'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'mocha_standalone'
|
6
|
+
require 'tent-client'
|
7
|
+
require 'oj'
|
8
|
+
|
9
|
+
Dir["#{File.dirname(__FILE__)}/support/*.rb"].each { |f| require f }
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.mock_with :mocha
|
13
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TentClient::CycleHTTP do
|
4
|
+
|
5
|
+
let(:http_stubs) { Faraday::Adapter::Test::Stubs.new }
|
6
|
+
let(:client_options) { Hash.new }
|
7
|
+
let(:server_urls) { %w{ http://alex.example.org/tent http://alexsmith.example.com/tent http://smith.example.com/tent } }
|
8
|
+
let(:client) { TentClient.new(server_urls, client_options) }
|
9
|
+
|
10
|
+
def expect_server(env, url)
|
11
|
+
expect(env[:url].to_s).to match(url)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should proxy http verbs to Faraday' do
|
15
|
+
http_stubs.get('/tent/foo/bar') { |env|
|
16
|
+
expect_server(env, server_urls.first)
|
17
|
+
[200, {}, '']
|
18
|
+
}
|
19
|
+
|
20
|
+
cycle_http = described_class.new(client) do |f|
|
21
|
+
f.adapter :test, http_stubs
|
22
|
+
end
|
23
|
+
|
24
|
+
%w{ head get put patch post delete options }.each { |verb|
|
25
|
+
expect(cycle_http).to respond_to(verb)
|
26
|
+
}
|
27
|
+
|
28
|
+
cycle_http.get('foo/bar')
|
29
|
+
|
30
|
+
http_stubs.verify_stubbed_calls
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should retry http with next server url' do
|
34
|
+
http_stubs.get('/tent/foo/bar') { |env|
|
35
|
+
expect_server(env, server_urls.first)
|
36
|
+
[500, {}, '']
|
37
|
+
}
|
38
|
+
|
39
|
+
http_stubs.get('/tent/foo/bar') { |env|
|
40
|
+
expect_server(env, server_urls[1])
|
41
|
+
[300, {}, '']
|
42
|
+
}
|
43
|
+
|
44
|
+
http_stubs.get('/tent/foo/bar') { |env|
|
45
|
+
expect_server(env, server_urls.last)
|
46
|
+
[200, {}, '']
|
47
|
+
}
|
48
|
+
|
49
|
+
cycle_http = described_class.new(client) do |f|
|
50
|
+
f.adapter :test, http_stubs
|
51
|
+
end
|
52
|
+
|
53
|
+
cycle_http.get('foo/bar')
|
54
|
+
|
55
|
+
http_stubs.verify_stubbed_calls
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should return response when on last server url' do
|
59
|
+
http_stubs.get('/tent/foo/bar') { |env|
|
60
|
+
expect_server(env, server_urls.first)
|
61
|
+
[500, {}, '']
|
62
|
+
}
|
63
|
+
|
64
|
+
http_stubs.get('/tent/foo/bar') { |env|
|
65
|
+
expect_server(env, server_urls[1])
|
66
|
+
[500, {}, '']
|
67
|
+
}
|
68
|
+
|
69
|
+
http_stubs.get('/tent/foo/bar') { |env|
|
70
|
+
expect_server(env, server_urls.last)
|
71
|
+
[300, {}, '']
|
72
|
+
}
|
73
|
+
|
74
|
+
http_stubs.get('/tent/foo/bar') { |env|
|
75
|
+
raise StandardError, 'expected stub not be called bus was'
|
76
|
+
}
|
77
|
+
|
78
|
+
cycle_http = described_class.new(client) do |f|
|
79
|
+
f.adapter :test, http_stubs
|
80
|
+
end
|
81
|
+
|
82
|
+
cycle_http.get('foo/bar')
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TentClient::Discovery do
|
4
|
+
LINK_HEADER = %(<https://example.com/tent/profile>; rel="%s") % TentClient::PROFILE_REL
|
5
|
+
LINK_TAG_HTML = %(<html><head><link href="https://example.com/tent/profile" rel="%s" /></head</html>) % TentClient::PROFILE_REL
|
6
|
+
TENT_PROFILE = %({"https://tent.io/types/info/core/v0.1.0":{"licenses":["http://creativecommons.org/licenses/by/3.0/"],"entity":"https://example.com","servers":["https://example.com/tent"]}})
|
7
|
+
|
8
|
+
let(:http_stubs) { Faraday::Adapter::Test::Stubs.new }
|
9
|
+
let(:client) { TentClient.new(nil, :faraday_adapter => [:test, http_stubs]) }
|
10
|
+
|
11
|
+
it 'should discover profile urls via a link header' do
|
12
|
+
http_stubs.head('/') { [200, { 'Link' => LINK_HEADER }, ''] }
|
13
|
+
|
14
|
+
discovery = described_class.new(client, 'http://example.com/')
|
15
|
+
discovery.perform.should eq(['https://example.com/tent/profile'])
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should discover profile urls via a link html tag' do
|
19
|
+
http_stubs.head('/') { [200, { 'Content-Type' => 'text/html' }, ''] }
|
20
|
+
http_stubs.get('/') { [200, { 'Content-Type' => 'text/html' }, LINK_TAG_HTML] }
|
21
|
+
|
22
|
+
discovery = described_class.new(client, 'http://example.com/')
|
23
|
+
discovery.perform.should eq(['https://example.com/tent/profile'])
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should work with relative urls' do
|
27
|
+
http_stubs.head('/') { [200, { 'Link' => LINK_HEADER.sub(%r{https://example.com}, '') }, ''] }
|
28
|
+
|
29
|
+
discovery = described_class.new(client, 'http://example.com/')
|
30
|
+
discovery.perform.should eq(['http://example.com/tent/profile'])
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should fetch a profile' do
|
34
|
+
http_stubs.head('/') { [200, { 'Link' => LINK_HEADER }, ''] }
|
35
|
+
http_stubs.get('/tent/profile') { [200, { 'Content-Type' => TentClient::MEDIA_TYPE }, TENT_PROFILE] }
|
36
|
+
discovery = described_class.new(client, 'http://example.com/')
|
37
|
+
discovery.perform
|
38
|
+
discovery.get_profile.should eq([Oj.load(TENT_PROFILE), "https://example.com/tent"])
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should delegate TentClient.discover' do
|
42
|
+
instance = mock(:perform => 1)
|
43
|
+
described_class.expects(:new).with(client, 'url').returns(instance)
|
44
|
+
client.discover('url').should eq(instance)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TentClient::LinkHeader do
|
4
|
+
def new_link(link, attrs={})
|
5
|
+
TentClient::LinkHeader::Link.new(link, attrs)
|
6
|
+
end
|
7
|
+
it 'should parse a simple link' do
|
8
|
+
link_header = %Q(<http://example.com/TheBook/chapter2>; rel="previous";\n title="previous chapter")
|
9
|
+
expected_link = new_link('http://example.com/TheBook/chapter2', :rel => 'previous', :title => 'previous chapter')
|
10
|
+
described_class.parse(link_header).links.first.should eq(expected_link)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should ignore utf-8 attributes' do
|
14
|
+
link_header = %Q(</TheBook/chapter2>;\n rel="previous"; title*=UTF-8'de'letztes%20Kapitel,\n </TheBook/chapter4>;\n rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel)
|
15
|
+
expected_links = [new_link('/TheBook/chapter2', :rel => 'previous'), new_link('/TheBook/chapter4', :rel => 'next')]
|
16
|
+
described_class.parse(link_header).links.should eq(expected_links)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should convert a link header to a string' do
|
20
|
+
expected_header = %Q(<https://example.com/tent/profile>; rel="profile"; type="application/vnd.tent.profile+json")
|
21
|
+
link = new_link('https://example.com/tent/profile', :rel => 'profile', :type => 'application/vnd.tent.profile+json')
|
22
|
+
described_class.new(link).to_s.should eq(expected_header)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
describe TentClient::Middleware::MacAuth do
|
5
|
+
def auth_header(env)
|
6
|
+
env[:request_headers]['Authorization']
|
7
|
+
end
|
8
|
+
|
9
|
+
def perform(path = '', method = :get, body = nil)
|
10
|
+
make_app.call({
|
11
|
+
:url => URI('http://example.com' + path),
|
12
|
+
:method => method,
|
13
|
+
:request_headers => {},
|
14
|
+
:body => body
|
15
|
+
})
|
16
|
+
end
|
17
|
+
|
18
|
+
def make_app
|
19
|
+
described_class.new(lambda{ |env| env }, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'not configured' do
|
23
|
+
let(:options) { {} }
|
24
|
+
|
25
|
+
it "doesn't add a header" do
|
26
|
+
auth_header(perform).should be_nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'configured' do
|
31
|
+
let(:options) { { :mac_key_id => 'h480djs93hd8', :mac_key => '489dks293j39', :mac_algorithm => 'hmac-sha-1' } }
|
32
|
+
let(:expected_header) { 'MAC id="h480djs93hd8", ts="1336363200", nonce="dj83hs9s", mac="%s"' }
|
33
|
+
before do
|
34
|
+
Time.expects(:now).returns(stub(:to_i => 1336363200))
|
35
|
+
SecureRandom.expects(:hex).with(3).returns('dj83hs9s')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'signs a GET request with no body' do
|
39
|
+
auth_header(perform('/resource/1?b=1&a=2')).should eq(expected_header % '6T3zZzy2Emppni6bzL7kdRxUWL4=')
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'signs POST request with body' do
|
43
|
+
auth_header(perform('/resource/1?b=1&a=2', :post, "asdf\nasdf")).should ==
|
44
|
+
expected_header % 'hqpo01mLJLSYDbxmfRgNMEw38Wg='
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'signs POST request with a readable body' do
|
48
|
+
io = StringIO.new("asdf\nasdf")
|
49
|
+
auth_header(perform('/resource/1?b=1&a=2', :post, io)).should ==
|
50
|
+
expected_header % 'hqpo01mLJLSYDbxmfRgNMEw38Wg='
|
51
|
+
io.eof?.should be_false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'faraday middleware' do
|
56
|
+
let(:options) { { :mac_key_id => 'h480djs93hd8', :mac_key => '489dks293j39', :mac_algorithm => 'hmac-sha-1' } }
|
57
|
+
let(:http_stub) {
|
58
|
+
Faraday::Adapter::Test::Stubs.new do |s|
|
59
|
+
s.post('/') { [200, {}, ''] }
|
60
|
+
end
|
61
|
+
}
|
62
|
+
let(:client) { TentClient.new('http://example.com', options.merge(:faraday_adapter => [:test, http_stub])) }
|
63
|
+
|
64
|
+
it "should be part of the client middleware stack" do
|
65
|
+
client.http.post('/', :foo => 'bar').env[:request_headers]['Authorization'].should =~ /\AMAC/
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/tent-client.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tent-client/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "tent-client"
|
8
|
+
gem.version = TentClient::VERSION
|
9
|
+
gem.authors = ["Jonathan Rudenberg", "Jesse Stuart"]
|
10
|
+
gem.email = ["jonathan@titanous.com", "jessestuart@gmail.com"]
|
11
|
+
gem.description = %q{Tent Protocol client}
|
12
|
+
gem.summary = %q{Tent Protocol client}
|
13
|
+
gem.homepage = "http://tent.io"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_runtime_dependency 'faraday', '0.8.4'
|
21
|
+
gem.add_runtime_dependency 'faraday_middleware', '0.8.8'
|
22
|
+
gem.add_runtime_dependency 'nokogiri'
|
23
|
+
gem.add_runtime_dependency 'oj'
|
24
|
+
gem.add_runtime_dependency 'faraday_middleware-multi_json'
|
25
|
+
|
26
|
+
gem.add_development_dependency 'rspec'
|
27
|
+
gem.add_development_dependency 'bundler'
|
28
|
+
gem.add_development_dependency 'rake'
|
29
|
+
gem.add_development_dependency 'guard-rspec'
|
30
|
+
gem.add_development_dependency 'mocha'
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tent-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jonathan Rudenberg
|
9
|
+
- Jesse Stuart
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-09-20 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: faraday
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - '='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.8.4
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - '='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: 0.8.4
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: faraday_middleware
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - '='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: 0.8.8
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - '='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.8.8
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: nokogiri
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: oj
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :runtime
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: faraday_middleware-multi_json
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: rspec
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ! '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: bundler
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: rake
|
129
|
+
requirement: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
type: :development
|
136
|
+
prerelease: false
|
137
|
+
version_requirements: !ruby/object:Gem::Requirement
|
138
|
+
none: false
|
139
|
+
requirements:
|
140
|
+
- - ! '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
- !ruby/object:Gem::Dependency
|
144
|
+
name: guard-rspec
|
145
|
+
requirement: !ruby/object:Gem::Requirement
|
146
|
+
none: false
|
147
|
+
requirements:
|
148
|
+
- - ! '>='
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
type: :development
|
152
|
+
prerelease: false
|
153
|
+
version_requirements: !ruby/object:Gem::Requirement
|
154
|
+
none: false
|
155
|
+
requirements:
|
156
|
+
- - ! '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: mocha
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
none: false
|
163
|
+
requirements:
|
164
|
+
- - ! '>='
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
type: :development
|
168
|
+
prerelease: false
|
169
|
+
version_requirements: !ruby/object:Gem::Requirement
|
170
|
+
none: false
|
171
|
+
requirements:
|
172
|
+
- - ! '>='
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
description: Tent Protocol client
|
176
|
+
email:
|
177
|
+
- jonathan@titanous.com
|
178
|
+
- jessestuart@gmail.com
|
179
|
+
executables: []
|
180
|
+
extensions: []
|
181
|
+
extra_rdoc_files: []
|
182
|
+
files:
|
183
|
+
- .gitignore
|
184
|
+
- .rspec
|
185
|
+
- .travis.yml
|
186
|
+
- Gemfile
|
187
|
+
- Guardfile
|
188
|
+
- LICENSE.txt
|
189
|
+
- README.md
|
190
|
+
- Rakefile
|
191
|
+
- lib/tent-client.rb
|
192
|
+
- lib/tent-client/app.rb
|
193
|
+
- lib/tent-client/app_authorization.rb
|
194
|
+
- lib/tent-client/cycle_http.rb
|
195
|
+
- lib/tent-client/discovery.rb
|
196
|
+
- lib/tent-client/follower.rb
|
197
|
+
- lib/tent-client/following.rb
|
198
|
+
- lib/tent-client/group.rb
|
199
|
+
- lib/tent-client/link_header.rb
|
200
|
+
- lib/tent-client/middleware/accept_header.rb
|
201
|
+
- lib/tent-client/middleware/encode_json.rb
|
202
|
+
- lib/tent-client/middleware/mac_auth.rb
|
203
|
+
- lib/tent-client/post.rb
|
204
|
+
- lib/tent-client/post_attachment.rb
|
205
|
+
- lib/tent-client/profile.rb
|
206
|
+
- lib/tent-client/version.rb
|
207
|
+
- spec/spec_helper.rb
|
208
|
+
- spec/unit/cycle_http_spec.rb
|
209
|
+
- spec/unit/discovery_spec.rb
|
210
|
+
- spec/unit/link_header_spec.rb
|
211
|
+
- spec/unit/middleware/mac_auth_spec.rb
|
212
|
+
- tent-client.gemspec
|
213
|
+
homepage: http://tent.io
|
214
|
+
licenses: []
|
215
|
+
post_install_message:
|
216
|
+
rdoc_options: []
|
217
|
+
require_paths:
|
218
|
+
- lib
|
219
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
220
|
+
none: false
|
221
|
+
requirements:
|
222
|
+
- - ! '>='
|
223
|
+
- !ruby/object:Gem::Version
|
224
|
+
version: '0'
|
225
|
+
segments:
|
226
|
+
- 0
|
227
|
+
hash: -2628975409218371797
|
228
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
229
|
+
none: false
|
230
|
+
requirements:
|
231
|
+
- - ! '>='
|
232
|
+
- !ruby/object:Gem::Version
|
233
|
+
version: '0'
|
234
|
+
segments:
|
235
|
+
- 0
|
236
|
+
hash: -2628975409218371797
|
237
|
+
requirements: []
|
238
|
+
rubyforge_project:
|
239
|
+
rubygems_version: 1.8.23
|
240
|
+
signing_key:
|
241
|
+
specification_version: 3
|
242
|
+
summary: Tent Protocol client
|
243
|
+
test_files:
|
244
|
+
- spec/spec_helper.rb
|
245
|
+
- spec/unit/cycle_http_spec.rb
|
246
|
+
- spec/unit/discovery_spec.rb
|
247
|
+
- spec/unit/link_header_spec.rb
|
248
|
+
- spec/unit/middleware/mac_auth_spec.rb
|