travis-gh 0.21.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.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+ require 'backports/basic_object' unless defined? BasicObject
5
+
6
+ module GH
7
+ # Public: ...
8
+ class Parallel < Wrapper
9
+ attr_accessor :parallelize
10
+
11
+ class Dummy < BasicObject
12
+ attr_accessor :__delegate__
13
+
14
+ def method_missing(*args)
15
+ ::Kernel.raise ::RuntimeError, 'response not yet loaded' if __delegate__.nil?
16
+ __delegate__.__send__(*args)
17
+ end
18
+
19
+ def respond_to_missing?(method, *)
20
+ super
21
+ end
22
+ end
23
+
24
+ def setup(*)
25
+ @parallelize = true if @parallelize.nil?
26
+ @in_parallel = false
27
+ @mutex = Mutex.new
28
+ @queue = []
29
+ super
30
+ end
31
+
32
+ def generate_response(key, response)
33
+ return super unless in_parallel?
34
+
35
+ dummy = Dummy.new
36
+ @mutex.synchronize { @queue << [dummy, key, response] }
37
+ dummy
38
+ end
39
+
40
+ def in_parallel
41
+ return yield if in_parallel? || !@parallelize
42
+
43
+ was = @in_parallel
44
+ @in_parallel = true
45
+ result = nil
46
+ connection.in_parallel { result = yield }
47
+ @mutex.synchronize do
48
+ @queue.each { |dummy, key, response| dummy.__delegate__ = backend.generate_response(key, response) }
49
+ @queue.clear
50
+ end
51
+ result
52
+ ensure
53
+ @in_parallel = was unless was.nil?
54
+ end
55
+
56
+ def in_parallel?
57
+ @in_parallel
58
+ end
59
+
60
+ def connection
61
+ @connection ||= begin
62
+ layer = backend
63
+ layer = layer.backend until layer.respond_to? :connection
64
+ layer.connection
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/gh/remote.rb ADDED
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'gh'
4
+ require 'faraday'
5
+ require 'faraday/retry'
6
+ require 'faraday/typhoeus'
7
+ require 'active_support/core_ext/string'
8
+
9
+ module GH
10
+ # Public: This class deals with HTTP requests to Github. It is the base Wrapper you always want to use.
11
+ # Note that it is usually used implicitely by other wrapper classes if not specified.
12
+ class Remote < Wrapper
13
+ attr_reader :api_host, :connection, :headers, :prefix
14
+
15
+ # Public: Generates a new Remote instance.
16
+ #
17
+ # api_host - HTTP host to send requests to, has to include schema (https or http)
18
+ # options - Hash with configuration options:
19
+ # :token - OAuth token to use (optional).
20
+ # :username - Github user used for login (optional).
21
+ # :password - Github password used for login (optional).
22
+ # :origin - Value of the origin request header (optional).
23
+ # :headers - HTTP headers to be send on every request (optional).
24
+ #
25
+ # It is highly recommended to set origin, but not to set headers.
26
+ # If you set the username, you should also set the password.
27
+ def setup(api_host, options)
28
+ token, username, password = options.values_at :token, :username, :password
29
+
30
+ api_host = api_host.api_host if api_host.respond_to? :api_host
31
+ @api_host = Addressable::URI.parse(api_host)
32
+ @headers = {
33
+ 'User-Agent' => options[:user_agent] || "GH/#{GH::VERSION}",
34
+ 'Accept' => options[:accept] || 'application/vnd.github.v3+json',
35
+ 'Accept-Charset' => 'utf-8'
36
+ }
37
+
38
+ @headers.merge! options[:headers] if options[:headers]
39
+ @headers['Origin'] = options[:origin] if options[:origin]
40
+
41
+ @prefix = ''
42
+ @prefix << "#{token}@" if token
43
+ @prefix << "#{username}:#{password}@" if username && password
44
+ @prefix << @api_host.host
45
+
46
+ faraday_options = { url: api_host }
47
+ faraday_options[:ssl] = options[:ssl] if options[:ssl]
48
+ faraday_options.merge! options[:faraday_options] if options[:faraday_options]
49
+
50
+ @connection = Faraday.new(faraday_options) do |builder|
51
+ builder.request(:authorization, :token, token) if token
52
+ builder.request(:basic_auth, username, password) if username && password
53
+ builder.request(:retry)
54
+ builder.adapter(:typhoeus)
55
+ builder.response(:raise_error)
56
+ builder.use :instrumentation if defined? FaradayMiddleware::Instrumentation
57
+ builder.response(:logger, nil, formatter: GH.const_get(options[:formatter].camelize)) if options[:formatter]
58
+ end
59
+ end
60
+
61
+ # Public: ...
62
+ def inspect
63
+ "#<#{self.class}: #{api_host}>"
64
+ end
65
+
66
+ # Internal: ...
67
+ def fetch_resource(key)
68
+ frontend.http(:get, frontend.path_for(key), headers)
69
+ end
70
+
71
+ # Internal: ...
72
+ def generate_response(key, response)
73
+ body = response.body
74
+ headers = response.headers
75
+ url = response.env[:url] if response.respond_to?(:env) && response.env
76
+ url = response.url if response.respond_to?(:url)
77
+ url = frontend.full_url(key) if url.to_s.empty?
78
+ modify(body, headers, url)
79
+ end
80
+
81
+ # Internal: ...
82
+ def http(verb, url, headers = {}, &block)
83
+ body = headers.delete :body
84
+ connection.run_request(verb, url, body, headers, &block)
85
+ rescue StandardError => e
86
+ raise Error.new(e, nil, verb:, url:, headers:)
87
+ end
88
+
89
+ # Internal: ...
90
+ def request(verb, key, body = nil)
91
+ response = frontend.http(verb, path_for(key), headers) do |req|
92
+ req.body = Response.new(body).to_s if body
93
+ end
94
+ frontend.generate_response(key, response)
95
+ rescue GH::Error => e
96
+ e.info[:payload] = Response.new(body).to_s if body
97
+ raise e
98
+ end
99
+
100
+ # Public: ...
101
+ def post(key, body)
102
+ frontend.request(:post, key, body)
103
+ end
104
+
105
+ # Public: ...
106
+ def delete(key, body = nil)
107
+ frontend.request(:delete, key, body)
108
+ end
109
+
110
+ # Public: ...
111
+ def head(key)
112
+ frontend.request(:head, key)
113
+ end
114
+
115
+ # Public: ...
116
+ def patch(key, body)
117
+ frontend.request(:patch, key, body)
118
+ end
119
+
120
+ # Public: ...
121
+ def put(key, body)
122
+ frontend.request(:put, key, body)
123
+ end
124
+
125
+ # Public: ...
126
+ def reset; end
127
+
128
+ # Public: ...
129
+ def load(data)
130
+ modify(data)
131
+ end
132
+
133
+ # Public: ...
134
+ def in_parallel
135
+ raise 'use GH::Parallel middleware for #in_parallel support'
136
+ end
137
+
138
+ def full_url(key)
139
+ uri = Addressable::URI.parse(key)
140
+ uri.path = File.join(api_host.path, uri.path) unless uri.absolute? || uri.path.start_with?(api_host.path)
141
+ uri = api_host + uri
142
+ raise ArgumentError, "URI out of scope: #{key}" if uri.host != api_host.host
143
+
144
+ uri
145
+ end
146
+
147
+ def path_for(key)
148
+ frontend.full_url(key).request_uri
149
+ end
150
+
151
+ private
152
+
153
+ def identifier(key)
154
+ path_for(key)
155
+ end
156
+
157
+ def modify(body, headers = {}, url = nil)
158
+ return body if body.is_a? Response
159
+
160
+ Response.new(body, headers, url)
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: false
2
+
3
+ require 'gh'
4
+ require 'multi_json'
5
+
6
+ module GH
7
+ # Public: Class wrapping low level Github responses.
8
+ #
9
+ # Delegates safe methods to the parsed body (expected to be an Array or Hash).
10
+ class Response
11
+ include Enumerable
12
+ include GH::Case
13
+ attr_accessor :headers, :data, :body, :url
14
+
15
+ # subset of safe methods that both Array and Hash implement
16
+ extend Forwardable
17
+ def_delegators(:@data, :[], :assoc, :each, :empty?, :flatten, :include?, :index, :inspect, :length,
18
+ :pretty_print, :pretty_print_cycle, :rassoc, :select, :size, :to_a, :values_at)
19
+
20
+ # Internal: Initializes a new instance.
21
+ #
22
+ # headers - HTTP headers as a Hash
23
+ # body - HTTP body as a String
24
+ def initialize(body = '{}', headers = {}, url = nil)
25
+ @url = url
26
+ @headers = headers.transform_keys { |k| k.downcase }
27
+
28
+ case body
29
+ when nil, '' then @data = {}
30
+ when respond_to(:to_str) then @body = body.to_str
31
+ when respond_to(:to_hash) then @data = body.to_hash
32
+ when respond_to(:to_ary) then @data = body.to_ary
33
+ else raise ArgumentError, "cannot parse #{body.inspect}"
34
+ end
35
+
36
+ @body.force_encoding('utf-8') if @body.respond_to? :force_encoding
37
+ @body ||= MultiJson.encode(@data)
38
+ @data ||= MultiJson.decode(@body)
39
+ rescue EncodingError
40
+ raise "Invalid encoding in #{url}, please contact github."
41
+ end
42
+
43
+ # Public: Duplicates the instance. Will also duplicate some instance variables to behave as expected.
44
+ #
45
+ # Returns new Response instance.
46
+ def dup
47
+ super.dup_ivars
48
+ end
49
+
50
+ # Public: Returns the response body as a String.
51
+ def to_s
52
+ @body.dup
53
+ end
54
+
55
+ # Public: Returns true or false indicating whether it supports method.
56
+ def respond_to?(method, *)
57
+ return super unless (method.to_s == 'to_hash') || (method.to_s == 'to_ary')
58
+
59
+ data.respond_to? method
60
+ end
61
+
62
+ # Public: Implements to_hash conventions, please check respond_to?(:to_hash).
63
+ def to_hash
64
+ return method_missing(__method__) unless respond_to? __method__
65
+
66
+ @data.dup.to_hash
67
+ end
68
+
69
+ # Public: Implements to_ary conventions, please check respond_to?(:to_hash).
70
+ def to_ary
71
+ return method_missing(__method__) unless respond_to? __method__
72
+
73
+ @data.dup.to_ary
74
+ end
75
+
76
+ # Public: ...
77
+ def to_gh
78
+ self
79
+ end
80
+
81
+ # Public: ...
82
+ def ==(other)
83
+ super or @data == other
84
+ end
85
+
86
+ protected
87
+
88
+ def dup_ivars
89
+ @headers = @headers.dup
90
+ @data = @data.dup
91
+ @body = @body.dup
92
+ self
93
+ end
94
+
95
+ private
96
+
97
+ def content_type
98
+ headers['content-type']
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+ require 'delegate'
5
+
6
+ module GH
7
+ ResponseWrapper = DelegateClass(Response) unless const_defined?
8
+
9
+ class ResponseWrapper
10
+ def to_gh
11
+ self
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday/logging/formatter'
4
+
5
+ module GH
6
+ class ResponseXHeaderFormatter < Faraday::Logging::Formatter
7
+ def request(env); end
8
+
9
+ def response(env)
10
+ info('Response') { env.response_headers.select { |k, _v| k =~ /^x-/ }.sort.to_h }
11
+ end
12
+ end
13
+ end
data/lib/gh/stack.rb ADDED
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+
5
+ module GH
6
+ # Public: Exposes DSL for stacking wrappers.
7
+ #
8
+ # Examples
9
+ #
10
+ # api = GH::Stack.build do
11
+ # use GH::Cache, cache: Rails.cache
12
+ # use GH::Normalizer
13
+ # use GH::Remote, username: "admin", password: "admin"
14
+ # end
15
+ class Stack
16
+ attr_reader :options
17
+
18
+ # Public: Generates a new wrapper stack from the given block.
19
+ #
20
+ # options - Hash of options that will be passed to all layers upon initialization.
21
+ #
22
+ # Returns top most Wrapper instance.
23
+ def self.build(options = {}, &block)
24
+ new(&block).build(options)
25
+ end
26
+
27
+ # Public: Generates a new Stack instance.
28
+ #
29
+ # options - Hash of options that will be passed to all layers upon initialization.
30
+ #
31
+ # Can be used for easly stacking layers.
32
+ def initialize(_options = {}, &block)
33
+ @options = {}
34
+ @stack = []
35
+ instance_eval(&block) if block
36
+ end
37
+
38
+ # Public: Adds a new layer to the stack.
39
+ #
40
+ # Layer will be wrapped by layers already on the stack.
41
+ def use(klass, options = {})
42
+ @stack << [klass, options]
43
+ self
44
+ end
45
+
46
+ # Public: Generates wrapper instances for stack configuration.
47
+ #
48
+ # options - Hash of options that will be passed to all layers upon initialization.
49
+ #
50
+ # Returns top most Wrapper instance.
51
+ def build(options = {})
52
+ @stack.reverse.inject(nil) do |backend, (klass, opts)|
53
+ klass.new backend, @options.merge(opts).merge(options)
54
+ end
55
+ end
56
+
57
+ # Public: ...
58
+ def replace(old_class, new_class)
59
+ @stack.map! { |klass, options| [old_class == klass ? new_class : klass, options] }
60
+ end
61
+
62
+ alias new build
63
+ end
64
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh/error'
4
+ require 'base64'
5
+
6
+ module GH
7
+ class TokenCheck < Wrapper
8
+ attr_accessor :client_id, :client_secret, :token
9
+
10
+ def setup(backend, options)
11
+ @client_secret = options[:client_secret]
12
+ @client_id = options[:client_id]
13
+ @token = options[:token]
14
+ @check_token = true
15
+ super
16
+ end
17
+
18
+ def check_token
19
+ return unless @check_token && client_id && client_secret && token
20
+
21
+ @check_token = false
22
+
23
+ auth_header = 'Basic %s' % Base64.strict_encode64("#{client_id}:#{client_secret}")
24
+ http :post, path_for("/applications/#{client_id}/token"), body: "{\"access_token\": \"#{token}\"}",
25
+ 'Authorization' => auth_header
26
+ rescue GH::Error(response_status: 404) => e
27
+ raise GH::TokenInvalid, e
28
+ end
29
+
30
+ def http(*)
31
+ check_token
32
+ super
33
+ end
34
+ end
35
+ end
data/lib/gh/version.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GH
4
+ # Public: Library version.
5
+ VERSION = '0.21.0'
6
+ end
data/lib/gh/wrapper.rb ADDED
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gh'
4
+ require 'addressable/uri'
5
+
6
+ module GH
7
+ # Public: Simple base class for low level layers.
8
+ # Handy if you want to manipulate resources coming in from Github.
9
+ #
10
+ # Examples
11
+ #
12
+ # class IndifferentAccess
13
+ # def [](key) super.tap { |r| r.data.with_indifferent_access! } end
14
+ # end
15
+ #
16
+ # gh = IndifferentAccess.new
17
+ # gh['users/rkh'][:name] # => "Konstantin Haase"
18
+ #
19
+ # # easy to use in the low level stack
20
+ # gh = Github.build do
21
+ # use GH::Cache
22
+ # use IndifferentAccess
23
+ # use GH::Normalizer
24
+ # end
25
+ class Wrapper
26
+ extend Forwardable
27
+ include Case
28
+
29
+ # Public: Get wrapped layer.
30
+ attr_reader :backend
31
+
32
+ # Public: ...
33
+ attr_reader :options
34
+
35
+ # Public: Returns the URI used for sending out web request.
36
+ def_delegator :backend, :api_host
37
+
38
+ # Internal: ...
39
+ def_delegator :backend, :http
40
+
41
+ # Internal: ...
42
+ def_delegator :backend, :request
43
+
44
+ # Public: ...
45
+ def_delegator :backend, :post
46
+
47
+ # Public: ...
48
+ def_delegator :backend, :delete
49
+
50
+ # Public: ...
51
+ def_delegator :backend, :head
52
+
53
+ # Public: ...
54
+ def_delegator :backend, :patch
55
+
56
+ # Public: ...
57
+ def_delegator :backend, :put
58
+
59
+ # Public: ...
60
+ def_delegator :backend, :fetch_resource
61
+
62
+ # Public: ...
63
+ def_delegator :backend, :in_parallel
64
+
65
+ # Public: ...
66
+ def_delegator :backend, :in_parallel?
67
+
68
+ # Public: ...
69
+ def_delegator :backend, :full_url
70
+
71
+ # Public: ...
72
+ def_delegator :backend, :path_for
73
+
74
+ def self.double_dispatch
75
+ define_method(:modify) { |data| double_dispatch(data) }
76
+ end
77
+
78
+ # Public: Retrieves resources from Github.
79
+ def self.[](key)
80
+ new[key]
81
+ end
82
+
83
+ # Public: Retrieves resources from Github.
84
+ #
85
+ # By default, this method is delegated to the next layer on the stack
86
+ # and modify is called.
87
+ def [](key)
88
+ generate_response key, fetch_resource(key)
89
+ end
90
+
91
+ # Internal: ...
92
+ def generate_response(key, resource)
93
+ modify backend.generate_response(key, resource)
94
+ end
95
+
96
+ # Internal: Get/set default layer to wrap when creating a new instance.
97
+ def self.wraps(klass = nil)
98
+ @wraps = klass if klass
99
+ @wraps ||= Remote
100
+ end
101
+
102
+ # Public: Initialize a new Wrapper.
103
+ #
104
+ # backend - layer to be wrapped
105
+ # options - config options
106
+ def initialize(backend = nil, options = {})
107
+ backend, @options = normalize_options(backend, options)
108
+ @options.each_pair { |key, value| public_send("#{key}=", value) if respond_to? "#{key}=" }
109
+ setup(backend, @options)
110
+ end
111
+
112
+ # Public: Set wrapped layer.
113
+ def backend=(layer)
114
+ reset if backend
115
+ layer.frontend = self
116
+ @backend = layer
117
+ end
118
+
119
+ # Internal: ...
120
+ attr_writer :frontend
121
+
122
+ # Internal: ...
123
+ def frontend
124
+ @frontend ? @frontend.frontend : self
125
+ end
126
+
127
+ # Public: ...
128
+ def inspect
129
+ "#<#{self.class}: #{backend.inspect}>"
130
+ end
131
+
132
+ # Internal: ...
133
+ def prefixed(key)
134
+ "#{prefix}##{identifier(key)}"
135
+ end
136
+
137
+ # Public: ...
138
+ def reset
139
+ backend&.reset
140
+ end
141
+
142
+ # Public: ...
143
+ def load(data)
144
+ modify backend.load(data)
145
+ end
146
+
147
+ private
148
+
149
+ def identifier(key)
150
+ backend.prefixed(key)
151
+ end
152
+
153
+ def prefix
154
+ self.class.name
155
+ end
156
+
157
+ def double_dispatch(data)
158
+ case data
159
+ when respond_to(:to_gh) then modify_response(data)
160
+ when respond_to(:to_hash) then modify_hash(data)
161
+ when respond_to(:to_ary) then modify_array(data)
162
+ when respond_to(:to_str) then modify_string(data)
163
+ when respond_to(:to_int) then modify_integer(data)
164
+ else modify_unknown data
165
+ end
166
+ rescue StandardError => e
167
+ raise Error.new(e, data)
168
+ end
169
+
170
+ def modify_response(response)
171
+ result = double_dispatch response.data
172
+ result.respond_to?(:to_gh) ? result.to_gh : Response.new(result, response.headers, response.url)
173
+ end
174
+
175
+ def modify(data, *)
176
+ data
177
+ rescue StandardError => e
178
+ raise Error.new(e, data)
179
+ end
180
+
181
+ def modify_array(array)
182
+ array.map { |e| modify(e) }
183
+ end
184
+
185
+ def modify_hash(hash)
186
+ corrected = {}
187
+ hash.each_pair { |k, v| corrected[k] = modify(v) }
188
+ corrected.default_proc = hash.default_proc if hash.default_proc
189
+ corrected
190
+ end
191
+
192
+ alias modify_string modify
193
+ alias modify_integer modify
194
+ alias modify_unknown modify
195
+
196
+ def setup(backend, options)
197
+ self.backend = backend.is_a?(Wrapper) ? backend : self.class.wraps.new(backend, options)
198
+ end
199
+
200
+ def normalize_options(backend, options)
201
+ if backend.is_a?(Hash)
202
+ options = backend
203
+ backend = nil
204
+ end
205
+ options ||= {}
206
+ backend ||= options[:backend] || options[:api_url] || 'https://api.github.com'
207
+ [backend, options]
208
+ end
209
+
210
+ def setup_default_proc(hash, &block)
211
+ old_proc = hash.default_proc
212
+ hash.default_proc = proc do |h, key|
213
+ value = old_proc.call(h, key) if old_proc
214
+ value = block[h, key] if value.nil?
215
+ value
216
+ end
217
+ end
218
+
219
+ def setup_lazy_loading(hash, *args)
220
+ loaded = false
221
+ setup_default_proc(hash) do |h, key|
222
+ next if loaded
223
+
224
+ fields = lazy_load(h, key, *args)
225
+ if fields
226
+ modify_hash(fields)
227
+ h.merge!(fields)
228
+ loaded = true
229
+ fields[key]
230
+ end
231
+ end
232
+ hash
233
+ end
234
+ end
235
+ end