tent-client 0.0.1 → 0.2.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +1 -3
  3. data/Gemfile +1 -1
  4. data/LICENSE +27 -0
  5. data/README.md +8 -19
  6. data/lib/tent-client.rb +111 -51
  7. data/lib/tent-client/attachment.rb +13 -0
  8. data/lib/tent-client/cycle_http.rb +124 -14
  9. data/lib/tent-client/discovery.rb +59 -27
  10. data/lib/tent-client/faraday/chunked_adapter.rb +100 -0
  11. data/lib/tent-client/faraday/utils.rb +10 -0
  12. data/lib/tent-client/link_header.rb +5 -0
  13. data/lib/tent-client/middleware/authentication.rb +103 -0
  14. data/lib/tent-client/middleware/content_type_header.rb +24 -0
  15. data/lib/tent-client/middleware/encode_json.rb +5 -6
  16. data/lib/tent-client/multipart-post/parts.rb +100 -0
  17. data/lib/tent-client/post.rb +99 -38
  18. data/lib/tent-client/tent_type.rb +77 -0
  19. data/lib/tent-client/version.rb +1 -1
  20. data/spec/cycle_http_spec.rb +213 -0
  21. data/spec/discovery_spec.rb +114 -0
  22. data/spec/{unit/link_header_spec.rb → link_header_spec.rb} +10 -8
  23. data/spec/spec_helper.rb +7 -3
  24. data/spec/support/discovery_link_behaviour.rb +31 -0
  25. data/spec/timestamp_skew_spec.rb +126 -0
  26. data/tent-client.gemspec +11 -6
  27. metadata +75 -91
  28. data/.rspec +0 -1
  29. data/Guardfile +0 -6
  30. data/LICENSE.txt +0 -22
  31. data/lib/tent-client/app.rb +0 -33
  32. data/lib/tent-client/app_authorization.rb +0 -21
  33. data/lib/tent-client/follower.rb +0 -37
  34. data/lib/tent-client/following.rb +0 -31
  35. data/lib/tent-client/group.rb +0 -19
  36. data/lib/tent-client/middleware/accept_header.rb +0 -14
  37. data/lib/tent-client/middleware/mac_auth.rb +0 -50
  38. data/lib/tent-client/post_attachment.rb +0 -11
  39. data/lib/tent-client/profile.rb +0 -17
  40. data/spec/unit/cycle_http_spec.rb +0 -84
  41. data/spec/unit/discovery_spec.rb +0 -46
  42. data/spec/unit/middleware/mac_auth_spec.rb +0 -68
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 684ae790259d6f77983e1e7564178cdc84783bdf
4
+ data.tar.gz: f0c8d69166831d9fc4fe705ac7a1726273bb9206
5
+ SHA512:
6
+ metadata.gz: fde0ae8fab7e514b187086ce8b2da941a56be9edae08014da3e4458752c2962bd3df11bf0bf834a639b454ef084fe3e4a57ddb99540a0f0f05222edf91d0b52f
7
+ data.tar.gz: b69ff67ec4a30e415d230bde232e91fbf05f11c36720998e65f002bb95b75b2367d0ba0f8b401b020232cb73d81dfe4c15f98aa2bf08e73c7958fcf7bd97dd52
@@ -1,8 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.0.0
3
4
  - 1.9.3
4
5
  - 1.9.2
5
- - rbx-18mode
6
6
  - rbx-19mode
7
- - 1.8.7
8
- - ree
data/Gemfile CHANGED
@@ -3,4 +3,4 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in tent-client.gemspec
4
4
  gemspec
5
5
 
6
- gem 'faraday_middleware-multi_json', :git => 'git://github.com/jvatic/faraday_middleware-multi_json.git', :branch => :master
6
+ gem 'hawk-auth', :git => 'git://github.com/tent/hawk-ruby.git', :branch => 'master'
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2013 Apollic Software, LLC. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ * Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above
10
+ copyright notice, this list of conditions and the following disclaimer
11
+ in the documentation and/or other materials provided with the
12
+ distribution.
13
+ * Neither the name of Apollic Software, LLC nor the names of its
14
+ contributors may be used to endorse or promote products derived from
15
+ this software without specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md CHANGED
@@ -1,24 +1,13 @@
1
- # Tent Ruby Client ![Build Status](https://magnum.travis-ci.com/tent/tent-client-ruby.png?branch=master&token=YmxLSPgdpsxNUMWJpzRx)
1
+ # Tent Ruby Client [![Build Status](https://travis-ci.org/tent/tent-client-ruby.png?branch=master)](https://travis-ci.org/tent/tent-client-ruby)
2
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.
3
+ TentClient implements a [Tent Protocol](https://tent.io) client library in Ruby.
7
4
 
8
5
  ## Usage
9
6
 
10
- ```ruby
11
- # Tent profile discovery
12
- TentClient.new.discover("http://tent-user.example.org")
7
+ ## Contributing
13
8
 
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.
9
+ 1. Fork it
10
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
11
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
12
+ 4. Push to the branch (`git push origin my-new-feature`)
13
+ 5. Create new Pull Request
@@ -1,81 +1,141 @@
1
1
  require 'tent-client/version'
2
- require 'oj'
3
2
  require 'faraday'
4
- require 'faraday_middleware'
5
- require 'faraday_middleware/multi_json'
3
+ require 'tent-client/faraday/utils'
4
+ require 'tent-client/multipart-post/parts'
5
+ require 'tent-client/faraday/chunked_adapter'
6
+ require 'tent-client/tent_type'
7
+ require 'tent-client/middleware/content_type_header'
8
+ require 'tent-client/middleware/encode_json'
9
+ require 'tent-client/middleware/authentication'
10
+ require 'tent-client/cycle_http'
11
+ require 'tent-client/discovery'
12
+ require 'tent-client/post'
13
+ require 'tent-client/attachment'
14
+
15
+ ##
16
+ # Ruby 1.8.7 compatibility
17
+ require 'uri'
18
+ unless URI.respond_to?(:encode_www_form_component)
19
+ require 'addressable/uri'
20
+
21
+ URI.class_eval do
22
+ def self.encode_www_form_component(str)
23
+ Addressable::URI.encode_component(
24
+ str.to_s.gsub(/(\r\n|\n|\r)/, "\r\n"),
25
+ Addressable::URI::CharacterClasses::UNRESERVED
26
+ ).gsub("%20", "+")
27
+ end
28
+ end
29
+ end
6
30
 
7
31
  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)
32
+ POST_MEDIA_TYPE = %(application/vnd.tent.post.v0+json).freeze
33
+ POST_CONTENT_TYPE = %(#{POST_MEDIA_TYPE}; type="%s").freeze
34
+ POST_MENTIONS_CONTENT_TYPE = %(application/vnd.tent.post-mentions.v0+json).freeze
35
+ POST_VERSIONS_CONTENT_TYPE = %(application/vnd.tent.post-versions.v0+json).freeze
36
+ POST_CHILDREN_CONTENT_TYPE = %(application/vnd.tent.post-children.v0+json).freeze
37
+ OAUTH_TOKEN_CONTENT_TYPE = %(application/vnd.tent.oauth.token.v0+json).freeze
38
+ MULTIPART_CONTENT_TYPE = 'multipart/form-data'.freeze
39
+ MULTIPART_BOUNDARY = "-----------TentPart".freeze
40
+
41
+ MalformedServerMeta = Class.new(StandardError)
42
+ ServerNotFound = Class.new(StandardError)
43
+
44
+ attr_reader :entity_uri, :options
45
+ attr_writer :faraday_adapter, :faraday_setup, :server_meta_post
46
+ attr_accessor :ts_skew
47
+ def initialize(entity_uri, options = {})
48
+ @server_meta_post = options.delete(:server_meta)
31
49
  @faraday_adapter = options.delete(:faraday_adapter)
32
- @options = options
50
+ @faraday_setup = options.delete(:faraday_setup)
51
+ @ts_skew = options.delete(:ts_skew)
52
+ @entity_uri, @options = entity_uri, options
33
53
  end
34
54
 
35
- def http
36
- @http ||= CycleHTTP.new(self) do |f|
55
+ def dup
56
+ self.class.new(@entity_uri, @options.merge(
57
+ :server_meta => @server_meta_post,
58
+ :faraday_adapter => @faraday_adapter,
59
+ :faraday_setup => @faraday_setup
60
+ ))
61
+ end
62
+
63
+ def server_meta
64
+ server_meta_post['content'] if server_meta_post
65
+ end
66
+
67
+ def server_meta_post
68
+ @server_meta_post ||= entity_uri ? Discovery.discover(self, entity_uri) : nil
69
+ end
70
+
71
+ def primary_server
72
+ server_meta['servers'].sort_by { |s| s['preference'] }.first
73
+ end
74
+
75
+ def new_http
76
+ authentication_options = {}
77
+ authentication_options[:ts_skew] = @ts_skew if @ts_skew
78
+ authentication_options[:ts_skew_retry_enabled] = @options.has_key?(:ts_skew_retry_enabled) ? @options[:ts_skew_retry_enabled] : true
79
+ authentication_options[:update_ts_skew] = proc do |skew|
80
+ @ts_skew = skew
81
+ end
82
+
83
+ @http = CycleHTTP.new(self) do |f|
84
+ f.use Middleware::ContentTypeHeader
37
85
  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
86
+ f.use Middleware::Authentication, @options[:credentials], authentication_options if @options[:credentials]
87
+ f.response :multi_json, :content_type => /\bjson\Z/ unless @options[:skip_serialization] || @options[:skip_response_serialization]
88
+ @faraday_setup.call(f) if @faraday_setup
41
89
  f.adapter *Array(faraday_adapter)
42
90
  end
43
91
  end
44
92
 
93
+ def http
94
+ @http || new_http
95
+ end
96
+
45
97
  def faraday_adapter
46
98
  @faraday_adapter || Faraday.default_adapter
47
99
  end
48
100
 
49
- def server_url=(v)
50
- @server_urls = Array(v)
51
- @http = nil # reset Faraday connection
101
+ def faraday_adapter=(adapter)
102
+ @faraday_adapter = adapter
52
103
  end
53
104
 
54
- def discover(url)
55
- Discovery.new(self, url).tap { |d| d.perform }
105
+ def hex_digest(data)
106
+ if data.kind_of?(IO)
107
+ _data = data.read
108
+ data.rewind
109
+ data = _data
110
+ end
111
+ Digest::SHA512.new.update(data).to_s[0...64]
56
112
  end
57
113
 
58
- def follower
59
- Follower.new(self)
114
+ def post
115
+ Post.new(self)
60
116
  end
61
117
 
62
- def following
63
- Following.new(self)
118
+ def attachment
119
+ Attachment.new(self)
64
120
  end
65
121
 
66
- def group
67
- Group.new(self)
68
- end
122
+ def oauth_redirect_uri(params = {})
123
+ uri = URI(primary_server['urls']['oauth_auth'])
69
124
 
70
- def app
71
- App.new(self)
72
- end
125
+ query = params.inject([]) { |m, (k,v)| m << "#{k}=#{URI.encode_www_form_component(v)}"; m }.join('&')
126
+ uri.query ? uri.query += "&#{query}" : uri.query = query
73
127
 
74
- def post
75
- Post.new(self)
128
+ uri
76
129
  end
77
130
 
78
- def profile
79
- Profile.new(self)
131
+ def oauth_token_exchange(data, &block)
132
+ new_block = proc do |request|
133
+ request.headers['Content-Type'] = OAUTH_TOKEN_CONTENT_TYPE
134
+ yield(request) if block_given?
135
+ end
136
+ http.post(:oauth_token, params = {}, {
137
+ :token_type => 'https://tent.io/oauth/hawk-token'
138
+ }.merge(data), &new_block)
80
139
  end
140
+
81
141
  end
@@ -0,0 +1,13 @@
1
+ class TentClient
2
+ class Attachment
3
+ attr_reader :client
4
+ def initialize(client)
5
+ @client = client.dup
6
+ @client.faraday_adapter = :net_http_stream
7
+ end
8
+
9
+ def get(entity, digest, params = {}, &block)
10
+ client.http.get(:attachment, { :entity => entity, :digest => digest }.merge(params), &block)
11
+ end
12
+ end
13
+ end
@@ -1,39 +1,112 @@
1
+ require 'yajl'
2
+
1
3
  class TentClient
2
4
 
3
5
  # Proxies to Faraday and cycles through server urls
4
6
  # until either non left or response status in the 200s or 400s
5
7
  class CycleHTTP
6
- attr_reader :client, :server_urls
8
+ attr_reader :client, :servers
7
9
  def initialize(client, &faraday_block)
8
10
  @faraday_block = faraday_block
9
11
  @client = client
10
- @server_urls = client.server_urls.dup
12
+
13
+ if client.entity_uri
14
+ unless (Hash === client.server_meta) && (Array === client.server_meta['servers'])
15
+ raise MalformedServerMeta.new("Server meta post for Entity(#{client.entity_uri.inspect}) is malformed: #{client.server_meta.inspect}")
16
+ end
17
+
18
+ @servers = client.server_meta['servers'].sort_by { |s| s['preference'] }
19
+ else
20
+ @servers = []
21
+ end
22
+ end
23
+
24
+ def current_server
25
+ @current_server || servers.first
11
26
  end
12
27
 
13
28
  def new_http
14
- @http = Faraday.new(:url => server_urls.shift) do |f|
29
+ @current_server = servers.shift
30
+ @http = Faraday.new do |f|
15
31
  @faraday_block.call(f)
16
32
  end
17
33
  end
18
34
 
19
35
  def http
20
- @http || new_http
36
+ @http ||= new_http
21
37
  end
22
38
 
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)
39
+ def named_url(name, params = {})
40
+ unless (Hash === current_server) && (Hash === current_server['urls']) && (template = current_server['urls'][name.to_s])
41
+ raise ServerNotFound.new("Failed to match #{name.to_s.inspect} to a url for server: #{Yajl::Encoder.encode(current_server)}")
42
+ end
43
+
44
+ template.to_s.gsub(/\{([^\}]+)\}/) {
45
+ param = (params.delete($1) || params.delete($1.to_sym)).to_s
46
+ URI.encode_www_form_component(param)
47
+ }
48
+ end
49
+
50
+ %w( options get head delete ).map(&:to_sym).each do |verb|
51
+ class_eval(<<-RUBY
52
+ def #{verb}(url, params = {}, headers = {}, &block)
53
+ run_request(#{verb.inspect}, url, params, nil, headers, &block)
54
+ end
55
+ RUBY
56
+ )
57
+ end
58
+
59
+ %w( post put patch ).map(&:to_sym).each do |verb|
60
+ class_eval(<<-RUBY
61
+ def #{verb}(url, params = {}, body = nil, headers = {}, &block)
62
+ run_request(#{verb.inspect}, url, params, body, headers, &block)
33
63
  end
64
+ RUBY
65
+ )
66
+ end
67
+
68
+ def multipart_request(verb, url, params, parts, headers = {}, &block)
69
+ body = multipart_body(parts)
70
+ run_request(verb.to_sym, url, params, body, headers) do |request|
71
+ request.headers['Content-Type'] = "#{MULTIPART_CONTENT_TYPE}; boundary=#{MULTIPART_BOUNDARY}"
72
+ request.headers['Content-Length'] = body.length.to_s
73
+ yield(request) if block_given?
34
74
  end
35
75
  end
36
76
 
77
+ def run_request(verb, url, params, body, headers, &block)
78
+ args = [verb, url, params, body, headers]
79
+ if Symbol === url
80
+ name = url
81
+ url = named_url(url, params || {})
82
+ else
83
+ name = nil
84
+ end
85
+
86
+ res = http.run_request(verb, url, body, headers) do |request|
87
+ request.params.update(params) if params
88
+ yield request if block_given?
89
+ end
90
+
91
+ if name
92
+ res.env[:tent_server] = current_server
93
+ end
94
+
95
+ return res if servers.empty? || !name
96
+
97
+ case res.status
98
+ when 200...300, 400...500
99
+ res
100
+ else
101
+ new_http
102
+ run_request(*args, &block)
103
+ end
104
+ rescue Faraday::Error::TimeoutError, Faraday::Error::ConnectionFailed
105
+ raise if servers.empty?
106
+ new_http
107
+ run_request(*args, &block)
108
+ end
109
+
37
110
  def respond_to_missing?(method_name, include_private = false)
38
111
  http.respond_to?(method_name, include_private)
39
112
  end
@@ -45,5 +118,42 @@ class TentClient
45
118
  super
46
119
  end
47
120
  end
121
+
122
+ private
123
+
124
+ def multipart_body(parts)
125
+ # group by category
126
+ parts = parts.inject(Hash.new) do |memo, part|
127
+ category = part[:category] || part['category']
128
+ memo[category] ||= []
129
+ memo[category] << part
130
+ memo
131
+ end
132
+
133
+ # expend into request parts
134
+ parts = parts.inject(Array.new) do |memo, (category, category_parts)|
135
+ if category_parts.size > 1
136
+ memo.concat category_parts.each_with_index.map { |part, index|
137
+ headers = part[:headers] || part['headers']
138
+ Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, "#{category}[#{index}]", upload_io(part), headers)
139
+ }
140
+ else
141
+ part = category_parts.first
142
+ headers = part[:headers] || part['headers']
143
+ memo << Faraday::Parts::FilePart.new(MULTIPART_BOUNDARY, category, upload_io(part), :headers => headers)
144
+ end
145
+ end
146
+
147
+ parts << Faraday::Parts::EpiloguePart.new(MULTIPART_BOUNDARY)
148
+ Faraday::CompositeReadIO.new(parts)
149
+ end
150
+
151
+ def upload_io(part)
152
+ Faraday::UploadIO.new(
153
+ (part[:file] || part['file']) || StringIO.new(part[:data] || part['data']),
154
+ part[:content_type] || part['content-type'],
155
+ part[:filename] || part['filename']
156
+ )
157
+ end
48
158
  end
49
159
  end