atum 0.5.0

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 (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +9 -0
  5. data/.rubocop_todo.yml +25 -0
  6. data/.travis.yml +9 -0
  7. data/Appraisals +9 -0
  8. data/CHANGELOG.md +5 -0
  9. data/CONTRIBUTING.md +9 -0
  10. data/CONTRIBUTORS.md +15 -0
  11. data/Gemfile +4 -0
  12. data/Guardfile +23 -0
  13. data/LICENSE.txt +22 -0
  14. data/README.md +95 -0
  15. data/Rakefile +13 -0
  16. data/TODO +3 -0
  17. data/atum.gemspec +37 -0
  18. data/bin/atum +41 -0
  19. data/circle.yml +7 -0
  20. data/gemfiles/faraday_0.8.9.gemfile +8 -0
  21. data/gemfiles/faraday_0.9.gemfile +8 -0
  22. data/lib/atum.rb +15 -0
  23. data/lib/atum/core.rb +13 -0
  24. data/lib/atum/core/client.rb +45 -0
  25. data/lib/atum/core/errors.rb +20 -0
  26. data/lib/atum/core/link.rb +77 -0
  27. data/lib/atum/core/paginator.rb +32 -0
  28. data/lib/atum/core/request.rb +55 -0
  29. data/lib/atum/core/resource.rb +15 -0
  30. data/lib/atum/core/response.rb +53 -0
  31. data/lib/atum/core/schema.rb +12 -0
  32. data/lib/atum/core/schema/api_schema.rb +62 -0
  33. data/lib/atum/core/schema/link_schema.rb +121 -0
  34. data/lib/atum/core/schema/parameter.rb +27 -0
  35. data/lib/atum/core/schema/parameter_choice.rb +28 -0
  36. data/lib/atum/core/schema/resource_schema.rb +51 -0
  37. data/lib/atum/generation.rb +15 -0
  38. data/lib/atum/generation/erb_context.rb +17 -0
  39. data/lib/atum/generation/errors.rb +6 -0
  40. data/lib/atum/generation/generator_link.rb +39 -0
  41. data/lib/atum/generation/generator_resource.rb +31 -0
  42. data/lib/atum/generation/generator_service.rb +73 -0
  43. data/lib/atum/generation/generators/base_generator.rb +57 -0
  44. data/lib/atum/generation/generators/client_generator.rb +16 -0
  45. data/lib/atum/generation/generators/module_generator.rb +17 -0
  46. data/lib/atum/generation/generators/resource_generator.rb +23 -0
  47. data/lib/atum/generation/generators/views/client.erb +26 -0
  48. data/lib/atum/generation/generators/views/module.erb +104 -0
  49. data/lib/atum/generation/generators/views/resource.erb +33 -0
  50. data/lib/atum/generation/options_parameter.rb +12 -0
  51. data/lib/atum/version.rb +3 -0
  52. data/spec/atum/core/client_spec.rb +26 -0
  53. data/spec/atum/core/errors_spec.rb +19 -0
  54. data/spec/atum/core/link_spec.rb +80 -0
  55. data/spec/atum/core/paginator_spec.rb +72 -0
  56. data/spec/atum/core/request_spec.rb +110 -0
  57. data/spec/atum/core/resource_spec.rb +66 -0
  58. data/spec/atum/core/response_spec.rb +127 -0
  59. data/spec/atum/core/schema/api_schema_spec.rb +49 -0
  60. data/spec/atum/core/schema/link_schema_spec.rb +91 -0
  61. data/spec/atum/core/schema/parameter_choice_spec.rb +40 -0
  62. data/spec/atum/core/schema/parameter_spec.rb +24 -0
  63. data/spec/atum/core/schema/resource_schema_spec.rb +24 -0
  64. data/spec/atum/generation/generator_link_spec.rb +62 -0
  65. data/spec/atum/generation/generator_resource_spec.rb +44 -0
  66. data/spec/atum/generation/generator_service_spec.rb +41 -0
  67. data/spec/atum/generation/generators/base_generator_spec.rb +75 -0
  68. data/spec/atum/generation/generators/client_generator_spec.rb +30 -0
  69. data/spec/atum/generation/generators/module_generator_spec.rb +37 -0
  70. data/spec/atum/generation/generators/resource_generator_spec.rb +46 -0
  71. data/spec/atum/generation/options_parameter_spec.rb +27 -0
  72. data/spec/fixtures/fruity_schema.json +161 -0
  73. data/spec/fixtures/sample_schema.json +139 -0
  74. data/spec/integration/client_integration_spec.rb +91 -0
  75. data/spec/spec_helper.rb +11 -0
  76. metadata +303 -0
@@ -0,0 +1,104 @@
1
+ # encoding: utf-8
2
+ #
3
+ # WARNING: Do not edit by hand, this file was generated by Atum:
4
+ #
5
+ # https://github.com/gocardless/atum
6
+ #
7
+ require 'atum'
8
+ require 'uri'
9
+
10
+ # Ensure module namespacing is set up
11
+ <% @module_name.split("::").size.times do |i| %>
12
+ module <%= @module_name.split("::")[0..i].join("::") %>; end
13
+ <% end %>
14
+
15
+ version_file = '<%= @module_name.downcase.underscore.split("/").last %>/version'
16
+ if File.file?(version_file)
17
+ require version_file
18
+ else
19
+ <%= "#{@module_name}::VERSION" %> = ''
20
+ end
21
+ require_relative '<%= @module_name.downcase.underscore.split("/").last %>/client'
22
+ <% @resources.each do |resource| %>
23
+ require_relative '<%= @module_name.downcase.underscore.split("/").last %>/resources/<%= resource.name.underscore %>'
24
+ <% end %>
25
+
26
+ module <%= @module_name %>
27
+ extend Forwardable
28
+
29
+ <% @resources.each do |resource| %>
30
+ def self.<%= resource.name %>(*args)
31
+ self.client.<%= resource.name %>(*args)
32
+ end
33
+ <% end %>
34
+
35
+ # Get a Client configured to use HTTP Basic authentication.
36
+ #
37
+ # @param api_key [String] The API key to use when connecting.
38
+ # @param options [Hash<Symbol,String>] Optionally, custom settings
39
+ # to use with the client. Allowed options are `default_headers`,
40
+ # `cache`, `user` and `url`.
41
+ # @return [Client] A client configured to use the API with HTTP Basic
42
+ # authentication.
43
+ def self.connect(api_key, options = nil)
44
+ options = self.custom_options(options)
45
+ uri = URI.parse(options[:url])
46
+ uri.user = options.fetch(:user, 'user').gsub('@', '%40')
47
+ uri.password = api_key
48
+ client = Atum::Core::Client.client_from_schema(SCHEMA, uri.to_s, options)
49
+ @client = Client.new(client)
50
+ end
51
+
52
+ def self.client
53
+ if @client.nil?
54
+ raise "Client not initialized, call <%= @module_name %>.connect(...)"
55
+ end
56
+ @client
57
+ end
58
+
59
+ # Get customized options.
60
+ def self.custom_options(options)
61
+ return self.default_options if options.nil?
62
+
63
+ return self.default_options.merge(options) unless options[:default_headers]
64
+
65
+ opts = self.default_options.merge(options)
66
+ opts[:default_headers] = self.default_options[:default_headers].merge(options[:default_headers])
67
+
68
+ opts
69
+ end
70
+
71
+ # Get the default options.
72
+ def self.default_options
73
+ {
74
+ default_headers: {
75
+ <%= @default_headers.merge(
76
+ 'User-Agent' => '#{user_agent}',
77
+ 'Content-Type' => 'application/json'
78
+ ).map do |k, v|
79
+ "\"#{k.to_s}\" => \"#{v.to_s}\""
80
+ end.join(",\n ") %>
81
+ } }
82
+ end
83
+
84
+ def self.user_agent
85
+ @user_agent ||=
86
+ begin
87
+ gem_name = "<%= @module_name.downcase.underscore.gsub("/", "-") %>"
88
+ gem_info = "#{gem_name}"
89
+ gem_info += "/v#{<%= @module_name %>::VERSION}" if defined?(<%= @module_name %>::VERSION)
90
+ ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
91
+ ruby_version = RUBY_VERSION
92
+ ruby_version += " p#{RUBY_PATCHLEVEL}" if defined?(RUBY_PATCHLEVEL)
93
+ comment = ["#{ruby_engine} #{ruby_version}"]
94
+ comment << "atum v#{Atum::VERSION}"
95
+ comment << "faraday v#{Faraday::VERSION}"
96
+ comment << RUBY_PLATFORM if defined?(RUBY_PLATFORM)
97
+ "#{gem_info} (#{comment.join("; ")})"
98
+ end
99
+ end
100
+
101
+ SCHEMA = Atum::Core::Schema::ApiSchema.new(JSON.load(<<-'JSON'))
102
+ <%= @schema %>
103
+ JSON
104
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: utf-8
2
+ #
3
+ # WARNING: Do not edit by hand, this file was generated by Atum:
4
+ #
5
+ # https://github.com/gocardless/atum
6
+ #
7
+ require 'atum'
8
+ require 'uri'
9
+
10
+ module <%= @module_name %>
11
+ <%= commentify(@description, 1) %>
12
+ class <%= @class_name %>
13
+ def initialize(client)
14
+ @client = client
15
+ end
16
+ <% @links.each do |link| %>
17
+
18
+ <%= commentify(link.description, 2) %>
19
+ <%=
20
+ unless link.parameters.empty?
21
+ param_comments = link.parameters.map do |parameter|
22
+ "@param #{parameter.name}: #{parameter.description}"
23
+ end.join("\n")
24
+
25
+ commentify("\n" + param_comments, 2)
26
+ end
27
+ %>
28
+ def <%= method(link.name, link.parameter_names_with_defaults) %>
29
+ @client.<%= @resource_name %>.<%= method(link.name, link.parameter_names) %>
30
+ end
31
+ <% end %>
32
+ end
33
+ end
@@ -0,0 +1,12 @@
1
+ module Atum
2
+ module Generation
3
+ class OptionsParameter < Atum::Core::Schema::Parameter
4
+ attr_reader :default
5
+
6
+ def initialize
7
+ super(nil, 'options', 'any query parameters in the form of a hash')
8
+ @default = '{}'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Atum
2
+ VERSION = '0.5.0'
3
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Atum::Core::Client do
4
+ let(:client) { described_class.new({}, 'http://foo:bar@example.com') }
5
+
6
+ describe '#inspect' do
7
+ it 'should redact user password credentials' do
8
+ expect(client.inspect).to include('url="http://foo:REDACTED@example.com"')
9
+ end
10
+ end
11
+
12
+ describe '#unknown_resource' do
13
+ let(:client) { described_class.new({}, 'http://example.com') }
14
+ subject(:command) { -> { client.unknown_resource } }
15
+
16
+ context 'without a matching resource' do
17
+ it { is_expected.to raise_error(NoMethodError) }
18
+ end
19
+ end
20
+
21
+ describe '.client_from_schema' do
22
+ let(:schema) { Atum::Core::Schema::ApiSchema.new(SAMPLE_SCHEMA) }
23
+ subject { described_class.client_from_schema(schema, 'https://example.com') }
24
+ it { is_expected.to be_an(described_class) }
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Atum::Core::ApiError do
4
+ let(:message) { 'Foo failed' }
5
+ let(:url) { 'http://example.com/how-to-fix' }
6
+
7
+ subject { described_class.new(error).message }
8
+
9
+ context 'without a documentation url' do
10
+ let(:error) { { 'message' => message } }
11
+ it { is_expected.to eq(message) }
12
+ end
13
+
14
+ context 'with a documentation url' do
15
+ let(:error) { { 'message' => message, 'documentation_url' => url } }
16
+ it { is_expected.to include(message) }
17
+ it { is_expected.to include(url) }
18
+ end
19
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe Atum::Core::Link do
4
+ describe '.new' do
5
+ let(:url) { 'http://example.com' }
6
+ let(:link_schema) { double }
7
+ let(:options) { {} }
8
+ subject(:method) { -> { described_class.new(url, link_schema, options) } }
9
+
10
+ shared_examples_for 'creating a Faraday instance' do
11
+ it 'generates a new Faraday connection with the root url' do
12
+ expect(Faraday).to receive(:new).with(url: 'http://example.com')
13
+ method.call
14
+ end
15
+ end
16
+ end
17
+
18
+ describe '#run' do
19
+ let(:url) { 'http://example.com' }
20
+ let(:link_schema) { double(method: :get) }
21
+ let(:options) { {} }
22
+ let(:param_one) { double }
23
+ let(:envelope_name) { double }
24
+ let(:params) { [param_one, options] }
25
+ let(:fake_request) { double }
26
+ let(:fake_api_request) { double(request: fake_request) }
27
+ let(:link) { described_class.new(url, link_schema, options) }
28
+
29
+ before do
30
+ allow(link_schema).to receive_message_chain(
31
+ :resource_schema, :name, :pluralize).and_return(envelope_name)
32
+ allow(Atum::Core::Request).to receive(:new).and_return(fake_api_request)
33
+ allow(link_schema).to receive(:construct_path).and_return('/things')
34
+ end
35
+
36
+ context 'options' do
37
+ let(:headers) { { foo: 'bar' } }
38
+ let(:options) { { headers: headers } }
39
+
40
+ before do
41
+ allow(link).to receive(:default_options)
42
+ .and_return(headers: { bish: 'bash' })
43
+ end
44
+
45
+ it 'deep merges the options' do
46
+ expect(Atum::Core::Request).to receive(:new)
47
+ .with(anything, anything, anything,
48
+ hash_including(headers: { bish: 'bash', foo: 'bar' }))
49
+ link.run(param_one, options)
50
+ end
51
+
52
+ context 'when user headers would clash with the defaults' do
53
+ let(:headers) { { bish: 'bosh' } }
54
+ it 'overrides the default headers with user headers' do
55
+ expect(Atum::Core::Request).to receive(:new)
56
+ .with(anything, anything, anything,
57
+ hash_including(headers: { bish: 'bosh' }))
58
+ link.run(param_one, options)
59
+ end
60
+ end
61
+ end
62
+
63
+ context 'the passed http method' do
64
+ let(:http_method) { double }
65
+ before { allow(link_schema).to receive(:method).and_return(http_method) }
66
+ it 'should be from the link schema' do
67
+ expect(Atum::Core::Request).to receive(:new)
68
+ .with(anything, http_method, anything, anything)
69
+ link.run(param_one, options)
70
+ end
71
+ end
72
+
73
+ context 'when the last parameter is not an options hash' do
74
+ subject { -> { link.run(*params) } }
75
+ let(:params) { [] }
76
+ it { is_expected.to raise_error(ArgumentError) }
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe Atum::Core::Paginator do
4
+ let(:items) do
5
+ (1..100).map do |i|
6
+ { name: "item_#{i}" }
7
+ end
8
+ end
9
+ let(:initial_limit) { 50 }
10
+ let(:limit_increment) { 50 }
11
+ let(:request) { double(make_request: response) }
12
+ let(:response) { double(body: anything) }
13
+ let(:options) { {} }
14
+ let(:paginator) { described_class.new(request, response, options) }
15
+ let(:after) { double }
16
+
17
+ describe '#enumerator' do
18
+ subject(:enumerator) { paginator.enumerator }
19
+ before do
20
+ allow(request).to receive(:unenvelope)
21
+ .and_return(items[0...initial_limit],
22
+ items[initial_limit...initial_limit * 2])
23
+ allow(response).to receive(:limit)
24
+ .and_return(initial_limit, initial_limit,
25
+ initial_limit + limit_increment,
26
+ initial_limit + limit_increment)
27
+ allow(response).to receive(:meta)
28
+ .and_return('cursors' => { 'after' => after })
29
+ end
30
+
31
+ it { is_expected.to be_an(Enumerator) }
32
+
33
+ context 'when the number of items required is < LIMIT_INCREMENT' do
34
+ before { stub_const('Atum::Core::Paginator::LIMIT_INCREMENT', limit_increment) }
35
+
36
+ it "doesn't fetch any more pages of data" do
37
+ expect(request).to_not receive(:make_request)
38
+ enumerator.take(limit_increment - 1).to_a
39
+ end
40
+ end
41
+
42
+ context 'when the number of items required is = LIMIT_INCREMENT' do
43
+ before { stub_const('Atum::Core::Paginator::LIMIT_INCREMENT', limit_increment) }
44
+
45
+ it "doesn't fetch any more pages of data" do
46
+ expect(request).to_not receive(:make_request)
47
+ enumerator.take(limit_increment).to_a
48
+ end
49
+ end
50
+
51
+ context 'when the number of items required is > LIMIT_INCREMENT' do
52
+ before { stub_const('Atum::Core::Paginator::LIMIT_INCREMENT', limit_increment) }
53
+
54
+ it 'fetches another page of data' do
55
+ expect(request).to receive(:make_request).once
56
+ enumerator.take(limit_increment + 1).to_a
57
+ end
58
+
59
+ it 'sends after as a query param' do
60
+ expect(request).to receive(:make_request)
61
+ .with(query: hash_including(after: after))
62
+ enumerator.take(limit_increment + 1).to_a
63
+ end
64
+
65
+ it 'increases the requested limit on each successive fetch' do
66
+ expect(request).to receive(:make_request)
67
+ .with(query: hash_including(limit: initial_limit + limit_increment))
68
+ enumerator.take(limit_increment + 1).to_a
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+
3
+ describe Atum::Core::Request do
4
+ let(:root_url) { 'http://example.com' }
5
+ let(:path) { '/things' }
6
+ let(:querystring) { '' }
7
+ let(:full_path) { "#{root_url}#{path}#{querystring}" }
8
+ let(:method) { :get }
9
+ let(:connection) { Faraday.new(url: root_url) }
10
+ let(:pre_processor) { nil }
11
+ let(:headers) { {} }
12
+ let(:body) { nil }
13
+ let(:query) { {} }
14
+ let(:options) do
15
+ {
16
+ headers: headers,
17
+ body: body,
18
+ query: query,
19
+ envelope_name: 'things'
20
+ }
21
+ end
22
+ subject(:requestor) { described_class.new(connection, method, path, options) }
23
+
24
+ describe '#request' do
25
+ let(:make_request) { -> { requestor.request } }
26
+ before { stub }
27
+
28
+ context 'passing headers' do
29
+ let(:headers) { { 'Foo' => 'BarBaz' } }
30
+ let(:stub) { stub_request(:get, full_path).with(headers: headers) }
31
+
32
+ it 'works as expected' do
33
+ make_request.call
34
+ expect(stub).to have_been_requested
35
+ end
36
+ end
37
+
38
+ context 'passing a body' do
39
+ let(:method) { :post }
40
+ let(:body) { { 'Foo' => 'BarBaz' } }
41
+ let(:stub) { stub_request(:post, full_path).with(body: body) }
42
+
43
+ it 'works as expected' do
44
+ make_request.call
45
+ expect(stub).to have_been_requested
46
+ end
47
+ end
48
+
49
+ context 'passing a query' do
50
+ let(:query) { { 'a' => 'b' } }
51
+ let(:stub) { stub_request(:get, full_path).with(query: query) }
52
+
53
+ it 'works as expected' do
54
+ make_request.call
55
+ expect(stub).to have_been_requested
56
+ end
57
+ end
58
+
59
+ context 'when the response is not JSON' do
60
+ let(:response) { 'ABCDEFGH' }
61
+ let(:stub) do
62
+ stub_request(:get, full_path)
63
+ .with(query: query)
64
+ .to_return(body: response,
65
+ headers: { 'Content-Type' => 'application/text' })
66
+ end
67
+
68
+ it 'returns the raw response' do
69
+ expect(make_request.call).to eq(response)
70
+ end
71
+ end
72
+
73
+ context 'when the response is json' do
74
+ context 'and is not paginated' do
75
+ let(:response_body) { { 'ABC' => 'DEF' } }
76
+ let(:response) { { 'things' => response_body } }
77
+ let(:stub) do
78
+ stub_request(:get, full_path)
79
+ .with(query: query)
80
+ .to_return(body: response.to_json,
81
+ headers: { 'Content-Type' => 'application/json' })
82
+ end
83
+
84
+ it 'returns an unenveloped body' do
85
+ expect(requestor).to receive(:unenvelope).with(response)
86
+ .and_return(response_body)
87
+ make_request.call
88
+ end
89
+ end
90
+
91
+ context 'and is paginated' do
92
+ let(:limit) { 50 }
93
+ let(:response) { { 'things' => (0..limit).map { |i| "item_#{i}" } } }
94
+ let(:stub) do
95
+ stub_request(:get, full_path)
96
+ .to_return(body: {
97
+ resources: response,
98
+ meta: {
99
+ limit: limit, cursors: { before: 'beforeID', after: 'afterID' }
100
+ }
101
+ }.to_json, headers: { 'Content-Type' => 'application/json' })
102
+ end
103
+
104
+ it 'returns an Enumerator' do
105
+ expect(make_request.call).to be_an(Enumerator)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end