clarify 1.1.1 → 2.0.0.alpha.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 (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