gh-akerl 0.15.1

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