gh-akerl 0.15.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.
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