atum 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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