tent-client 0.0.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,66 +1,127 @@
1
1
  class TentClient
2
2
  class Post
3
- MULTIPART_TYPE = 'multipart/form-data'.freeze
4
- MULTIPART_BOUNDARY = "-----------TentAttachment".freeze
3
+ attr_reader :client, :request_method
4
+ def initialize(client, options = {})
5
+ @client, @request_method = client, options.delete(:request_method)
6
+ end
7
+
8
+ def head
9
+ self.class.new(client, :request_method => :head)
10
+ end
11
+
12
+ def get(entity, post_id, params = {}, &block)
13
+ new_block = proc do |request|
14
+ request.headers['Accept'] = POST_MEDIA_TYPE
15
+ yield(request) if block_given?
16
+ end
17
+
18
+ client.http.send(request_method || :get, :post, { :entity => entity, :post => post_id }.merge(params), &new_block)
19
+ end
5
20
 
6
- def initialize(client)
7
- @client = client
21
+ def get_attachment(entity, post_id, attachment_name, params = {}, &block)
22
+ client.http.send(request_method || :get, :post_attachment, { :entity => entity, :post => post_id, :name => attachment_name }.merge(params), &block)
8
23
  end
9
24
 
10
- def count(params={})
11
- @client.http.get('posts/count', params)
25
+ def delete(entity, post_id, params = {}, &block)
26
+ client.http.delete(:post, { :entity => entity, :post => post_id }.merge(params), &block)
12
27
  end
13
28
 
14
- def list(params = {})
15
- @client.http.get('posts', params)
29
+ def list(params = {}, &block)
30
+ client.http.send(request_method || :get, :posts_feed, params, &block)
16
31
  end
17
32
 
18
- def create(post, options={})
19
- if options[:attachments]
20
- multipart_post(post, options, options.delete(:attachments))
33
+ def create(data, params = {}, options = {}, &block)
34
+ if (Array === (attachments = options.delete(:attachments))) && attachments.any?
35
+ parts = multipart_parts(data, attachments)
36
+ client.http.multipart_request(:post, :new_post, params, parts, &block)
21
37
  else
22
- @client.http.post(options[:url] || 'posts', post)
38
+ client.http.post(:new_post, params, data, &block)
23
39
  end
24
40
  end
25
41
 
26
- def get(id)
27
- @client.http.get("posts/#{id}")
42
+ def update(entity, post_id, data, params = {}, options = {}, &block)
43
+ params = { :entity => entity, :post => post_id }.merge(params)
44
+ if (Array === (attachments = options.delete(:attachments))) && attachments.any?
45
+ parts = multipart_parts(data, attachments, options)
46
+ client.http.multipart_request(:put, :post, params, parts, &block)
47
+ else
48
+ new_block = proc do |request|
49
+ if options.delete(:import)
50
+ request.options['tent.import'] = true
51
+ elsif options.delete(:notification)
52
+ request.options['tent.notification'] = true
53
+ end
54
+ yield(request) if block_given?
55
+ end
56
+
57
+ client.http.put(:post, params, data, &new_block)
58
+ end
28
59
  end
29
60
 
30
- def delete(id)
31
- @client.http.delete("posts/#{id}")
61
+ def mentions(entity, post_id, params = {}, options = {}, &block)
62
+ # TODO: handle options[:page] => :first || :last || page-id
63
+
64
+ params = { :entity => entity, :post => post_id }.merge(params)
65
+
66
+ new_block = proc do |request|
67
+ request.headers['Accept'] = POST_MENTIONS_CONTENT_TYPE
68
+ yield(request) if block_given?
69
+ end
70
+
71
+ client.http.send(request_method || :get, :post, params, &new_block)
32
72
  end
33
73
 
34
- def attachment
35
- PostAttachment.new(@client)
74
+ def versions(entity, post_id, params = {}, options = {}, &block)
75
+ # TODO: handle options[:page] => :first || :last || page-id
76
+
77
+ params = { :entity => entity, :post => post_id }.merge(params)
78
+
79
+ new_block = proc do |request|
80
+ request.headers['Accept'] = POST_VERSIONS_CONTENT_TYPE
81
+ yield(request) if block_given?
82
+ end
83
+
84
+ client.http.send(request_method || :get, :post, params, &new_block)
36
85
  end
37
86
 
38
- private
87
+ def children(entity, post_id, params = {}, options = {}, &block)
88
+ # TODO: handle options[:page] => :first || :last || page-id
89
+
90
+ params = { :entity => entity, :post => post_id }.merge(params)
91
+
92
+ new_block = proc do |request|
93
+ request.headers['Accept'] = POST_CHILDREN_CONTENT_TYPE
94
+ yield(request) if block_given?
95
+ end
39
96
 
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}")
97
+ client.http.send(request_method || :get, :post, params, &new_block)
44
98
  end
45
99
 
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
100
+ private
101
+
102
+ def multipart_parts(data, attachments, options = {})
103
+ [data_as_attachment(data, options)] + attachments.map { |a|
104
+ a[:filename] = a.delete(:name) || a.delete('name')
105
+ a[:headers] = a[:headers] || {}
57
106
  a
58
- } << Faraday::Parts::EpiloguePart.new(MULTIPART_BOUNDARY)
59
- Faraday::CompositeReadIO.new(parts)
107
+ }
60
108
  end
61
109
 
62
- def attachment_io(attachment)
63
- Faraday::UploadIO.new(attachment[:file] || StringIO.new(attachment[:data]), attachment[:type], attachment[:filename])
110
+ def data_as_attachment(data, options = {})
111
+ content_type = POST_CONTENT_TYPE % (data[:type] || data['type'])
112
+
113
+ if options[:import]
114
+ content_type << %(; rel="https://tent.io/rels/import")
115
+ elsif options[:notification]
116
+ content_type << %(; rel="https://tent.io/rels/notification")
117
+ end
118
+
119
+ {
120
+ :category => 'post',
121
+ :filename => 'post.json',
122
+ :content_type => content_type,
123
+ :data => Yajl::Encoder.encode(data)
124
+ }
64
125
  end
65
126
  end
66
127
  end
@@ -0,0 +1,77 @@
1
+ require 'uri'
2
+
3
+ class TentClient
4
+ class TentType
5
+ ALL = 'all'.freeze
6
+
7
+ attr_accessor :base, :version, :fragment
8
+ def initialize(uri = nil)
9
+ @version = 0
10
+ parse_uri(uri) if uri
11
+ end
12
+
13
+ def all?
14
+ base == ALL
15
+ end
16
+
17
+ def has_fragment?
18
+ !!@fragment_separator
19
+ end
20
+
21
+ def fragment=(new_fragment)
22
+ return if all?
23
+
24
+ @fragment_separator = "#"
25
+ @fragment = new_fragment
26
+ @fragment = decode_fragment(@fragment) if @fragment
27
+ @fragment
28
+ end
29
+
30
+ def to_s(options = {})
31
+ return base if all?
32
+
33
+ options[:encode_fragment] = true unless options.has_key?(:encode_fragment)
34
+ if (!has_fragment? && options[:fragment] != true) || options[:fragment] == false
35
+ "#{base}/v#{version}"
36
+ else
37
+ "#{base}/v#{version}##{options[:encode_fragment] ? encode_fragment(fragment) : fragment}"
38
+ end
39
+ end
40
+
41
+ def ==(other)
42
+ unless TentType === other
43
+ if String === other
44
+ other = TentType.new(other)
45
+ else
46
+ return false
47
+ end
48
+ end
49
+
50
+ base == other.base && version == other.version && has_fragment? == other.has_fragment? && fragment == other.fragment
51
+ end
52
+
53
+ private
54
+
55
+ def parse_uri(uri)
56
+ if uri == ALL
57
+ @base = uri
58
+ elsif m = %r{\A(.+?)/v(.+?)(#(.+)?)?\Z}.match(uri.to_s)
59
+ m, @base, @version, @fragment_separator, @fragment = m.to_a
60
+ @fragment = decode_fragment(@fragment) if @fragment
61
+ @version = @version
62
+ end
63
+ end
64
+
65
+ def decode_fragment(fragment)
66
+ return unless fragment
67
+ f, *r = URI.decode(fragment).split('#')
68
+ ([f] + r.map { |_f| decode_fragment(_f) }).join('#')
69
+ end
70
+
71
+ def encode_fragment(fragment)
72
+ return unless fragment
73
+ parts = fragment.split('#')
74
+ parts.reverse.inject(nil) { |m, _p| URI.encode("#{_p}#{m ? '#' + m : ''}") }
75
+ end
76
+ end
77
+ end
@@ -1,3 +1,3 @@
1
1
  class TentClient
2
- VERSION = '0.0.1'
2
+ VERSION = "0.2.1"
3
3
  end
@@ -0,0 +1,213 @@
1
+ require 'spec_helper'
2
+ require 'tent-client/cycle_http'
3
+
4
+ describe TentClient::CycleHTTP do
5
+
6
+ let(:http_stubs) { Faraday::Adapter::Test::Stubs.new }
7
+ let(:server_urls) { %w(http://example.org/tent http://foo.example.com/tent http://baz.example.com/tent) }
8
+ let(:entity_uri) { server_urls.first }
9
+ let(:server_meta) {
10
+ {
11
+ "content" => {
12
+ "entity" => entity_uri,
13
+ "previous_entities" => [],
14
+ "servers" => server_urls.each_with_index.map { |server_url, index|
15
+ {
16
+ "version" => "0.3",
17
+ "urls" => {
18
+ "oauth_auth" => "#{server_url}/oauth/authorize",
19
+ "oauth_token" => "#{server_url}/oauth/token",
20
+ "posts_feed" => "#{server_url}/posts",
21
+ "new_post" => "#{server_url}/posts",
22
+ "post" => "#{server_url}/posts/{entity}/{post}",
23
+ "post_attachment" => "#{server_url}/posts/{entity}/{post}/attachments/{name}?version={version}",
24
+ "batch" => "#{server_url}/batch",
25
+ "server_info" => "#{server_url}/server"
26
+ },
27
+ "preference" => index
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ let(:client_options) {
34
+ {
35
+ :server_meta => server_meta
36
+ }
37
+ }
38
+ let(:client) { TentClient.new(entity_uri, client_options) }
39
+
40
+ def expect_server(env, url)
41
+ expect(env[:url].to_s).to match(url)
42
+ end
43
+
44
+ it 'proxies http verbs to Faraday' do
45
+ cycle_http = described_class.new(client) do |f|
46
+ f.adapter :test, http_stubs
47
+ end
48
+
49
+ post_entity = 'https://randomentity.example.org/xyz'
50
+ post_id = 'someid'
51
+ %w{ head get put patch post delete options }.each { |verb|
52
+ http_stubs.send(verb, "/tent/posts/#{URI.encode_www_form_component(post_entity)}/#{post_id}") { |env|
53
+ expect_server(env, server_urls.first)
54
+ [200, {}, '']
55
+ }
56
+
57
+ expect(cycle_http).to respond_to(verb)
58
+ cycle_http.send(verb, :post, :entity => post_entity, :post => post_id)
59
+ }
60
+
61
+ http_stubs.verify_stubbed_calls
62
+ end
63
+
64
+ it 'builds multipart requests' do
65
+ cycle_http = described_class.new(client) do |f|
66
+ f.adapter :net_http
67
+ end
68
+
69
+ http_stubs = []
70
+ body = "--#{TentClient::MULTIPART_BOUNDARY}\r\nContent-Disposition: form-data; name=\"photos[0]\"; filename=\"foo.png\"\r\nContent-Length: 17\r\nContent-Type: image/png\r\nContent-Transfer-Encoding: binary\r\n\r\nFake photo data 1\r\n--#{TentClient::MULTIPART_BOUNDARY}\r\nContent-Disposition: form-data; name=\"photos[1]\"; filename=\"bar.png\"\r\nContent-Length: 17\r\nContent-Type: image/png\r\nContent-Transfer-Encoding: binary\r\n\r\nFake photo data 2\r\n--#{TentClient::MULTIPART_BOUNDARY}\r\nContent-Disposition: form-data; name=\"documentation\"; filename=\"README.txt\"\r\nContent-Length: 17\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: binary\r\n\r\nSome instructions\r\n--#{TentClient::MULTIPART_BOUNDARY}--\r\n\r\n"
71
+
72
+ %w{ put patch post }.each { |verb|
73
+ http_stubs << stub_request(verb.to_sym, "#{server_urls.first}/posts").with(
74
+ :header => {
75
+ 'Content-Type' => "#{TentClient::MULTIPART_CONTENT_TYPE};boundary=#{TentClient::MULTIPART_BOUNDARY}",
76
+ 'Content-Length' => body.length
77
+ }
78
+ ).with { |request|
79
+ request.body.to_s.split(%r{-+#{TentClient::MULTIPART_BOUNDARY}-*\r\n}).sort == body.split(%r{-+#{TentClient::MULTIPART_BOUNDARY}-*\r\n}).sort
80
+ }
81
+
82
+ expect(cycle_http).to respond_to(:multipart_request)
83
+ cycle_http.multipart_request(verb, :new_post, {}, [
84
+ {
85
+ :filename => 'foo.png',
86
+ :content_type => 'image/png',
87
+ :data => 'Fake photo data 1',
88
+ :category => 'photos'
89
+ },
90
+ {
91
+ :filename => 'bar.png',
92
+ :content_type => 'image/png',
93
+ :data => 'Fake photo data 2',
94
+ :category => 'photos'
95
+ },
96
+ {
97
+ :filename => 'README.txt',
98
+ :content_type => 'text/plain',
99
+ :data => 'Some instructions',
100
+ :category => 'documentation'
101
+ }
102
+ ])
103
+ }
104
+
105
+ http_stubs.each do |stub|
106
+ expect(stub).to have_been_requested
107
+ end
108
+ end
109
+
110
+ it 'builds multipart requests with custom headers' do
111
+ cycle_http = described_class.new(client) do |f|
112
+ f.adapter :net_http
113
+ end
114
+
115
+ http_stubs = []
116
+ body = "--#{TentClient::MULTIPART_BOUNDARY}\r\nContent-Disposition: form-data; name=\"photos\"; filename=\"foo.png\"\r\nContent-Length: 17\r\nContent-Type: image/vnd.foo.bar.v0+png\r\nContent-Transfer-Encoding: binary\r\nFoo: Bar\r\n\r\nFake photo data 1\r\n--#{TentClient::MULTIPART_BOUNDARY}--\r\n\r\n"
117
+
118
+ %w{ put patch post }.each { |verb|
119
+ http_stubs << stub_request(verb.to_sym, "#{server_urls.first}/posts").with(
120
+ :body => body,
121
+ :header => {
122
+ 'Content-Type' => "#{TentClient::MULTIPART_CONTENT_TYPE};boundary=#{TentClient::MULTIPART_BOUNDARY}",
123
+ 'Content-Length' => body.length
124
+ }
125
+ )
126
+
127
+ expect(cycle_http).to respond_to(:multipart_request)
128
+ cycle_http.multipart_request(verb, :new_post, {}, [
129
+ {
130
+ :filename => 'foo.png',
131
+ :content_type => 'image/png',
132
+ :data => 'Fake photo data 1',
133
+ :category => 'photos',
134
+ :headers => {
135
+ 'Content-Type' => "image/vnd.foo.bar.v0+png",
136
+ 'Foo' => 'Bar'
137
+ }
138
+ }
139
+ ])
140
+ }
141
+
142
+ http_stubs.each do |stub|
143
+ expect(stub).to have_been_requested
144
+ end
145
+ end
146
+
147
+ it 'retries http with next server url' do
148
+ http_stubs.get('/tent/posts') { |env|
149
+ expect_server(env, server_urls.first)
150
+ [500, {}, '']
151
+ }
152
+
153
+ http_stubs.get('/tent/posts') { |env|
154
+ expect_server(env, server_urls[1])
155
+ [300, {}, '']
156
+ }
157
+
158
+ http_stubs.get('/tent/posts') { |env|
159
+ expect_server(env, server_urls.last)
160
+ [200, {}, '']
161
+ }
162
+
163
+ cycle_http = described_class.new(client) do |f|
164
+ f.adapter :test, http_stubs
165
+ end
166
+
167
+ res = cycle_http.get(:new_post)
168
+
169
+ http_stubs.verify_stubbed_calls
170
+
171
+ expect(res.env[:tent_server]).to_not be_nil
172
+ end
173
+
174
+ it 'returns response when on last server url' do
175
+ http_stubs.get('/tent/posts') { |env|
176
+ expect_server(env, server_urls.first)
177
+ raise Faraday::Error::TimeoutError.new("")
178
+ }
179
+
180
+ http_stubs.get('/tent/posts') { |env|
181
+ expect_server(env, server_urls[1])
182
+ raise Faraday::Error::ConnectionFailed.new("")
183
+ }
184
+
185
+ http_stubs.get('/tent/posts') { |env|
186
+ expect_server(env, server_urls.last)
187
+ [300, {}, '']
188
+ }
189
+
190
+ http_stubs.get('/tent/posts') { |env|
191
+ raise StandardError, 'expected stub not be called bus was'
192
+ }
193
+
194
+ cycle_http = described_class.new(client) do |f|
195
+ f.adapter :test, http_stubs
196
+ end
197
+
198
+ cycle_http.get(:new_post)
199
+ end
200
+
201
+ it 'encodes flat params' do
202
+ http_stubs.get('/tent/posts') { |env|
203
+ expect(env[:url].query).to match("color=red")
204
+ expect(env[:url].query).to match("color=blue")
205
+ }
206
+
207
+ cycle_http = described_class.new(client) do |f|
208
+ f.adapter :test, http_stubs
209
+ end
210
+
211
+ cycle_http.get(:new_post, :color => ['red', 'blue'])
212
+ end
213
+ end