clarify 1.1.1 → 2.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -16
  3. data/.rspec +2 -0
  4. data/.simplecov +3 -0
  5. data/.travis.yml +7 -0
  6. data/CHANGELOG.md +4 -0
  7. data/Dockerfile +11 -0
  8. data/Gemfile +2 -2
  9. data/LICENSE +1 -1
  10. data/README.md +202 -47
  11. data/Rakefile +62 -4
  12. data/clarify.gemspec +10 -7
  13. data/cucumber.yml +1 -0
  14. data/features/create-bundles.feature +10 -0
  15. data/features/delete-bundle.feature +7 -0
  16. data/features/identity-steps.feature +6 -0
  17. data/features/list-bundles.feature +13 -0
  18. data/features/search-bundles.feature +10 -0
  19. data/features/step_definitions/curied_url_steps.rb +6 -0
  20. data/features/step_definitions/error_steps.rb +4 -0
  21. data/features/step_definitions/http_verification_step.rb +4 -0
  22. data/features/step_definitions/identity_steps.rb +12 -0
  23. data/features/step_definitions/list_steps.rb +76 -0
  24. data/features/support/env.rb +36 -0
  25. data/features/support/lib/curies.rb +19 -0
  26. data/features/support/lib/customer.rb +41 -0
  27. data/features/support/lib/exceptions.rb +27 -0
  28. data/features/support/lib/names.rb +16 -0
  29. data/lib/clarify.rb +16 -14
  30. data/lib/clarify/bundle_repository.rb +26 -0
  31. data/lib/clarify/client.rb +51 -0
  32. data/lib/clarify/collection_iterator.rb +27 -0
  33. data/lib/clarify/configuration.rb +29 -7
  34. data/lib/clarify/errors.rb +22 -0
  35. data/lib/clarify/response.rb +29 -0
  36. data/lib/clarify/response_factory.rb +34 -0
  37. data/lib/clarify/responses/bundle.rb +11 -0
  38. data/lib/clarify/responses/collection.rb +31 -0
  39. data/lib/clarify/responses/no_body.rb +13 -0
  40. data/lib/clarify/responses/search_collection.rb +18 -0
  41. data/lib/clarify/responses/tracks.rb +15 -0
  42. data/lib/clarify/rest_client.rb +129 -0
  43. data/lib/clarify/version.rb +3 -1
  44. data/spec/clarify/bundle_repository_spec.rb +37 -0
  45. data/spec/clarify/client_spec.rb +93 -0
  46. data/spec/clarify/collection_iterator_spec.rb +86 -0
  47. data/spec/clarify/configuration_spec.rb +77 -0
  48. data/spec/clarify/errors_spec.rb +15 -0
  49. data/spec/clarify/response_factory_spec.rb +51 -0
  50. data/spec/clarify/response_spec.rb +69 -0
  51. data/spec/clarify/responses/bundle_spec.rb +8 -0
  52. data/spec/clarify/responses/collection_spec.rb +58 -0
  53. data/spec/clarify/responses/search_collection_spec.rb +40 -0
  54. data/spec/clarify/responses/tracks_spec.rb +18 -0
  55. data/spec/clarify/rest_client_spec.rb +222 -0
  56. data/spec/spec_helper.rb +4 -9
  57. data/src_readme/README_no_output.md +186 -0
  58. data/src_readme/examples/bundle_create.rb +11 -0
  59. data/src_readme/examples/bundle_fetch.rb +9 -0
  60. data/src_readme/examples/bundles_list_fetch.rb +11 -0
  61. data/src_readme/examples/bundles_paged_over.rb +8 -0
  62. data/src_readme/examples/bundles_search.rb +20 -0
  63. data/src_readme/examples/list_bundles.rb +6 -0
  64. data/src_readme/examples/searches_paged_over.rb +10 -0
  65. data/src_readme/examples/setup.rb +5 -0
  66. data/src_readme/make.rb +56 -0
  67. data/src_readme/readme.md.erb +55 -0
  68. metadata +127 -62
  69. data/LICENSE.txt +0 -24
  70. data/examples/create.rb +0 -14
  71. data/examples/delete.rb +0 -12
  72. data/examples/list.rb +0 -14
  73. data/examples/search.rb +0 -26
  74. data/examples/test.rb +0 -15
  75. data/lib/clarify/bundle.rb +0 -40
  76. data/lib/clarify/metadata.rb +0 -26
  77. data/lib/clarify/request.rb +0 -37
  78. data/lib/clarify/search.rb +0 -10
  79. data/lib/clarify/track.rb +0 -40
  80. data/spec/lib/clarify/bundle_spec.rb +0 -43
  81. data/spec/lib/clarify/configuration_spec.rb +0 -19
  82. data/spec/lib/clarify/metadata_spec.rb +0 -36
  83. data/spec/lib/clarify/search_spec.rb +0 -22
  84. data/spec/lib/clarify/track_spec.rb +0 -81
@@ -0,0 +1,27 @@
1
+
2
+ module Clarify
3
+ # Iterate over a paginated collection
4
+ class CollectionIterator
5
+ include Enumerable
6
+
7
+ def initialize(restclient, collection)
8
+ @restclient = restclient
9
+ @collection = collection
10
+ end
11
+
12
+ def each
13
+ collections.each { |collection| collection.each { |*i| yield(*i) } }
14
+ end
15
+
16
+ def collections
17
+ Enumerator.new do |y|
18
+ current = @collection
19
+ loop do
20
+ y << current
21
+ break unless current.more?
22
+ current = @restclient.get(current.next)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,16 +1,38 @@
1
+
1
2
  module Clarify
3
+ # A Configuration class represents the server to communicate with, and the
4
+ # credentials to consume. You can use multiple Configuration objects with
5
+ # multiple RestClient objects to support multiple accounts in the same
6
+ # application.
2
7
  class Configuration
3
-
4
- attr_accessor :api_key, :version_name, :version
8
+ attr_reader :api_key
9
+ attr_reader :server
10
+
11
+ def initialize(conf = {})
12
+ @api_key = conf.fetch(:api_key)
13
+ @server = conf.fetch(:server, 'https://api.clarify.io')
14
+ end
15
+
16
+ def api_key?
17
+ return false unless api_key
18
+ return false if api_key.empty?
19
+ true
20
+ end
21
+
22
+ def ssl?
23
+ uri.scheme == 'https'
24
+ end
5
25
 
6
- def initialize
7
- @api_key = 'api key here'
8
- @version = '1' # latest version is default
26
+ def host
27
+ uri.host
9
28
  end
10
29
 
11
- def version_name
12
- "v#{@version}"
30
+ def port
31
+ uri.port
13
32
  end
14
33
 
34
+ def uri
35
+ URI(server)
36
+ end
15
37
  end
16
38
  end
@@ -0,0 +1,22 @@
1
+
2
+ module Clarify
3
+ class StandardError < Exception
4
+ end
5
+
6
+ # This Response error is to be raised when a particular Response has a status
7
+ # code of 401
8
+ class UnauthenticatedError < Clarify::StandardError
9
+ def initialize(response)
10
+ @response = response
11
+
12
+ super 'Response had code 401'
13
+ end
14
+ end
15
+
16
+ # Handles errors where we are unable to understand the response from the API.
17
+ class UnrecognizedResponseError < Clarify::StandardError
18
+ def initialize(type)
19
+ super "Unrecognized response class #{type}"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ module Clarify
2
+ # A general purpose response which handles status code access and accessing
3
+ # link relations.
4
+ class Response
5
+ attr_reader :body
6
+ attr_reader :response
7
+
8
+ def initialize(body, response)
9
+ @body = body
10
+ @response = response
11
+ end
12
+
13
+ def http_status_code
14
+ response.code.to_i
15
+ end
16
+
17
+ def relation!(link)
18
+ url = relation(link)
19
+
20
+ fail ArgumentError, "Link '#{link}' not present" if url.nil?
21
+
22
+ url
23
+ end
24
+
25
+ def relation(link)
26
+ body.fetch('_links', {}).fetch(link, {}).fetch('href', nil)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+
2
+ module Clarify
3
+ # Handles the HTTP response and returns responses. Also raises exceptions
4
+ # around API failures.
5
+ class ResponseFactory
6
+ def make_result(response)
7
+ raise_on_code!(response)
8
+
9
+ return Clarify::Responses::NoBody.new(response) if response.body.nil?
10
+
11
+ data = JSON.parse(response.body || '')
12
+ klass_for_class(data['_class']).new(data, response)
13
+ rescue KeyError
14
+ raise Clarify::UnrecognizedResponseError, data['_class']
15
+ end
16
+
17
+ def raise_on_code!(response)
18
+ return unless response.code.to_i == 401
19
+ fail Clarify::UnauthenticatedError, response
20
+ end
21
+
22
+ def klass_for_class(name)
23
+ klasses = {
24
+ 'Collection' => Clarify::Responses::Collection,
25
+ 'SearchCollection' => Clarify::Responses::SearchCollection,
26
+ 'Bundle' => Clarify::Responses::Bundle,
27
+ 'Tracks' => Clarify::Responses::Tracks,
28
+ 'Ref' => Clarify::Response
29
+ }
30
+
31
+ klasses.fetch name
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+
2
+ module Clarify
3
+ module Responses
4
+ # Represents an individual bundle.
5
+ class Bundle < Clarify::Response
6
+ def name
7
+ body['name']
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+
2
+ require 'json'
3
+
4
+ module Clarify
5
+ module Responses
6
+ # A Collection represents a list of results from the API, primarily used in
7
+ # the Bundle list and the Search results (a SearchCollection.)
8
+ #
9
+ # A collection can be paged through using next / prev / first / last link
10
+ # relations.
11
+ class Collection < Clarify::Response
12
+ include Enumerable
13
+
14
+ def each
15
+ items.each { |i| yield i['href'] }
16
+ end
17
+
18
+ def items
19
+ body['_links']['items']
20
+ end
21
+
22
+ def more?
23
+ !self.next.nil?
24
+ end
25
+
26
+ def next
27
+ relation('next')
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+
2
+ module Clarify
3
+ module Responses
4
+ # The Tracks class represents a collection of tracks within a bundle. A
5
+ # bundle may have zero or more tracks. Iterating over a Tracks object will
6
+ # yield each track in the tracks resource.
7
+ class NoBody < Clarify::Response
8
+ def initialize(response)
9
+ super response, nil
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+
2
+ module Clarify
3
+ module Responses
4
+ # A SearchCollection represents a list of search results from the API.
5
+ #
6
+ # A collection can be paged through using next / prev / first / last link
7
+ # relations.
8
+ class SearchCollection < Clarify::Responses::Collection
9
+ def each
10
+ items.each { |result, ref| yield result, ref['href'] }
11
+ end
12
+
13
+ def items
14
+ body['item_results'].zip(body['_links']['items'])
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+
2
+ module Clarify
3
+ module Responses
4
+ # The Tracks class represents a collection of tracks within a bundle. A
5
+ # bundle may have zero or more tracks. Iterating over a Tracks object will
6
+ # yield each track in the tracks resource.
7
+ class Tracks < Clarify::Response
8
+ include Enumerable
9
+
10
+ def each
11
+ body['tracks'].each { |i| yield i }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,129 @@
1
+
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'uri'
5
+
6
+ module Clarify
7
+ # Implement the HTTP layer. All the communication in this restclient goes over
8
+ # the one HTTP connection for performance. The connection is created by the
9
+ # input of the Configuration object which is passed in by default.
10
+ #
11
+ # This is a bit over complex, but deals with:
12
+ #
13
+ # 1. Authenticating and identifying (via UserAgent) requests ("blessing")
14
+ # 2. Actually calling the API over HTTP
15
+ # 3. Converting the resulting responses into Response objects
16
+ #
17
+ # In other words, this does too many things, but it is a start.
18
+ class RestClient
19
+ attr_reader :config
20
+
21
+ def initialize(config)
22
+ @config = config
23
+ @cached = {}
24
+ end
25
+
26
+ def get(url, params = {})
27
+ request(get_request(url, params))
28
+ end
29
+
30
+ def post(url, params = {})
31
+ request(post_request(url, params))
32
+ end
33
+
34
+ def put(url, params = {})
35
+ request(put_request(url, params))
36
+ end
37
+
38
+ def delete(url, params = {})
39
+ request(delete_request(url, params))
40
+ end
41
+
42
+ def get_request(url, params = {})
43
+ url = make_get_url(url, params)
44
+ Net::HTTP::Get.new(url)
45
+ end
46
+
47
+ def post_request(url, body = {})
48
+ request = Net::HTTP::Post.new(url)
49
+ request.set_form_data(body)
50
+ request
51
+ end
52
+
53
+ def put_request(url, body = {})
54
+ request = Net::HTTP::Put.new(url)
55
+ request.set_form_data(body)
56
+ request
57
+ end
58
+
59
+ def delete_request(url, body = {})
60
+ request = Net::HTTP::Delete.new(url)
61
+ request.set_form_data(body)
62
+ request
63
+ end
64
+
65
+ def make_get_url(url, parameters)
66
+ # Convert the URL to a URI object we can manipulate
67
+ uri = URI.parse(url)
68
+
69
+ # Code a=b to [['a', 'b']] -- this array of arrays format is used
70
+ # so you can represent (valid) URLs which look like:
71
+ # a=b&a=c via [['a', 'b'], ['a', 'c']]
72
+ original_params = URI.decode_www_form(uri.query.to_s)
73
+
74
+ # Now we convert the incoming URL parameters which could either look like:
75
+ # 1. { b: 'c' }
76
+ # 2. { b: ['c', 'd'] }
77
+ # 3. { b: :c }
78
+ # 4. [['b', 'c']]
79
+ # to consistently be in the fourth format by encoding (flexible on input)
80
+ # and decoding again (which always produces the fourth format.)
81
+ new_params = URI.decode_www_form(URI.encode_www_form(parameters))
82
+
83
+ # Merge the two arrays [['b', 'c']] and [['a', 'b']] into
84
+ # [['a', 'b'], ['b', 'c']] and put it into the uri object for returning
85
+ uri.query = URI.encode_www_form(original_params + new_params)
86
+
87
+ # Set the query to nil if the query is empty, otherwise the generated URL
88
+ # will have a trailing ?.
89
+ uri.query = nil if uri.query.empty?
90
+
91
+ uri.to_s
92
+ end
93
+
94
+ def request(request)
95
+ request = bless_request(request)
96
+ response = connection.request(request)
97
+ make_result(response)
98
+ end
99
+
100
+ def bless_request(request)
101
+ request['Authorization'] = "Bearer #{config.api_key}" if config.api_key?
102
+ request['User-Agent'] = user_agent
103
+
104
+ request
105
+ end
106
+
107
+ def user_agent
108
+ ruby_version = "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}"
109
+ "clarify-ruby/#{Clarify::VERSION}/#{ruby_version}"
110
+ end
111
+
112
+ def make_result(response)
113
+ response_factory.make_result(response)
114
+ end
115
+
116
+ def response_factory
117
+ @response_factory ||= Clarify::ResponseFactory.new
118
+ end
119
+
120
+ def connection
121
+ unless @connection
122
+ @connection ||= Net::HTTP.new(config.host, config.port)
123
+ @connection.use_ssl = config.ssl?
124
+ end
125
+
126
+ @connection
127
+ end
128
+ end
129
+ end
@@ -1,3 +1,5 @@
1
+
2
+ # Stub for the gemspec, tick the version on each release.
1
3
  module Clarify
2
- VERSION = "1.1.1"
4
+ VERSION = '2.0.0.alpha.1'
3
5
  end
@@ -0,0 +1,37 @@
1
+
2
+ describe Clarify::BundleRepository do
3
+ let(:restclient) { double(:restclient) }
4
+ let(:repo) { Clarify::BundleRepository.new(restclient) }
5
+
6
+ describe '#fetch' do
7
+ it 'performs a get on the restclient' do
8
+ expect(restclient).to receive(:get).with('/v1/bundles')
9
+ repo.fetch
10
+ end
11
+ end
12
+
13
+ describe '#search' do
14
+ it 'performs a get on the restclient' do
15
+ expect(restclient).to receive(:get).with('/v1/search', query: 'abc')
16
+ repo.search 'abc'
17
+ end
18
+ end
19
+
20
+ describe '#create!' do
21
+ it 'performs a post on the restclient' do
22
+ body = { hey: :there }
23
+ expect(restclient).to receive(:post).with('/v1/bundles', body)
24
+ repo.create! body
25
+ end
26
+ end
27
+
28
+ describe '#delete!' do
29
+ it 'performs a delete on the restclient' do
30
+ bundle = double(:bundle)
31
+ expect(bundle).to receive(:relation!)
32
+ .with('self').and_return('/v1/bundle/thisone')
33
+ expect(restclient).to receive(:delete).with('/v1/bundle/thisone')
34
+ repo.delete! bundle
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,93 @@
1
+
2
+ describe Clarify::Client do
3
+ let(:opts) { {} }
4
+ let(:config) { { api_key: 'abc123' } }
5
+ let(:client) { Clarify::Client.new(config, opts) }
6
+
7
+ context 'with a fake restclient' do
8
+ let(:url) { double(:url) }
9
+ let(:params) { double(:params) }
10
+ let(:restclient) { double(:restclient) }
11
+ before(:each) do
12
+ allow(client).to receive(:restclient).and_return(restclient)
13
+ end
14
+ describe '#get' do
15
+ it 'calls get on the restclient' do
16
+ expect(restclient).to receive(:get).with(url, params)
17
+ client.get(url, params)
18
+ end
19
+ end
20
+
21
+ describe '#put' do
22
+ it 'calls put on the restclient' do
23
+ expect(restclient).to receive(:put).with(url, params)
24
+ client.put(url, params)
25
+ end
26
+ end
27
+
28
+ describe '#post' do
29
+ it 'calls post on the restclient' do
30
+ expect(restclient).to receive(:post).with(url, params)
31
+ client.post(url, params)
32
+ end
33
+ end
34
+
35
+ describe '#delete' do
36
+ it 'calls delete on the restclient' do
37
+ expect(restclient).to receive(:delete).with(url, params)
38
+ client.delete(url, params)
39
+ end
40
+ end
41
+ end
42
+
43
+ describe '#pager' do
44
+ let(:collection) { double(:collection) }
45
+ let(:restclient) { double(:restclient) }
46
+ let(:iterator_klass) { double(:iterator_klass) }
47
+ let(:opts) { { iterator: iterator_klass } }
48
+ it 'creates an iterator with the restclient' do
49
+ expect(client).to receive(:restclient).and_return(restclient)
50
+ expect(iterator_klass).to receive(:new).with(restclient, collection)
51
+ client.pager(collection)
52
+ end
53
+ end
54
+
55
+ describe '#bundles' do
56
+ let(:repo) { double(:repo) }
57
+ it 'calls the bundle_repository method' do
58
+ expect(client).to receive(:bundle_repository).and_return(repo)
59
+ expect(client.bundles).to eq(repo)
60
+ end
61
+ end
62
+
63
+ describe '#bundle_repository' do
64
+ let(:restclient) { double(:restclient) }
65
+ let(:bundle_klass) { double(:bundle_klass) }
66
+ let(:opts) { { bundle_repository: bundle_klass } }
67
+ it 'creates a bundle repository with the restclient' do
68
+ expect(client).to receive(:restclient).and_return(restclient)
69
+ expect(bundle_klass).to receive(:new).with(restclient)
70
+ client.bundle_repository
71
+ end
72
+ end
73
+
74
+ describe '#restclient' do
75
+ let(:configuration) { double(:configuration) }
76
+ let(:restclient_klass) { double(:restclient_klass) }
77
+ let(:opts) { { rest_client: restclient_klass } }
78
+ it 'creates a new restclient with the configuration' do
79
+ expect(client).to receive(:configuration).and_return(configuration)
80
+ expect(restclient_klass).to receive(:new).with(configuration)
81
+ client.restclient
82
+ end
83
+ end
84
+
85
+ describe '#configuration' do
86
+ let(:config_klass) { double(:config_klass) }
87
+ let(:opts) { { configuration: config_klass } }
88
+ it 'should create a new Configuration from the opts' do
89
+ expect(config_klass).to receive(:new).with(config)
90
+ client.configuration
91
+ end
92
+ end
93
+ end