karatekit 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,78 @@
1
+ require 'karatekit/connection'
2
+ require 'karatekit/warnable'
3
+ require 'karatekit/arguments'
4
+ require 'karatekit/configurable'
5
+ require 'karatekit/authentication'
6
+ require 'karatekit/rate_limit'
7
+
8
+ require 'karatekit/client/locations'
9
+ require 'karatekit/client/products'
10
+ require 'karatekit/client/sessions'
11
+ require 'karatekit/client/events'
12
+ require 'karatekit/client/event_details'
13
+ require 'karatekit/client/event_parts'
14
+ require 'karatekit/client/posts'
15
+ require 'karatekit/client/instructors'
16
+ require 'karatekit/client/instructor_details'
17
+ require 'karatekit/client/instructor_groups'
18
+ require 'karatekit/client/instructor_group_members'
19
+ require 'karatekit/client/rate_limit'
20
+
21
+ module Karatekit
22
+
23
+ # Client for the kampfsport.center API
24
+ #
25
+ # @see https://developer.kampfsport.center
26
+ class Client
27
+
28
+ include Karatekit::Authentication
29
+ include Karatekit::Configurable
30
+ include Karatekit::Connection
31
+ include Karatekit::Warnable
32
+
33
+ include Karatekit::Client::Locations
34
+ include Karatekit::Client::Products
35
+ include Karatekit::Client::Sessions
36
+ include Karatekit::Client::Events
37
+ include Karatekit::Client::EventDetails
38
+ include Karatekit::Client::EventParts
39
+ include Karatekit::Client::Posts
40
+ include Karatekit::Client::Instructors
41
+ include Karatekit::Client::InstructorDetails
42
+ include Karatekit::Client::InstructorGroups
43
+ include Karatekit::Client::InstructorGroupMembers
44
+ include Karatekit::Client::RateLimit
45
+
46
+ # Header keys that can be passed in options hash to {#get},{#head}
47
+ CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
48
+
49
+ def initialize(options = {})
50
+ # Use options passed in, but fall back to module defaults
51
+ Karatekit::Configurable.keys.each do |key|
52
+ value = options.key?(key) ? options[key] : Karatekit.instance_variable_get(:"@#{key}")
53
+ instance_variable_set(:"@#{key}", value)
54
+ end
55
+ end
56
+
57
+ # Text representation of the client, masking tokens and passwords
58
+ #
59
+ # @return [String]
60
+ def inspect
61
+ inspected = super
62
+
63
+ if @access_token
64
+ inspected = inspected.gsub! @access_token, "#{'*'*36}#{@access_token[36..-1]}"
65
+ end
66
+
67
+ inspected
68
+ end
69
+
70
+ # Set OAuth access token for authentication
71
+ #
72
+ # @param value [String] 40 character kampfsport.center OAuth access token
73
+ def access_token=(value)
74
+ reset_agent
75
+ @access_token = value
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,80 @@
1
+ module Karatekit
2
+
3
+ # Configuration options for {Client}, defaulting to values
4
+ # in {Default}
5
+ module Configurable
6
+ # @!attribute [w] access_token
7
+ # @see https://developer.github.com/v3/oauth/
8
+ # @return [String] OAuth2 access token for authentication
9
+ # @!attribute api_endpoint
10
+ # @return [String] Base URL for API requests. default: https://api.github.com/
11
+ # @!attribute auto_paginate
12
+ # @return [Boolean] Auto fetch next page of results until rate limit reached
13
+ # @!attribute connection_options
14
+ # @see https://github.com/lostisland/faraday
15
+ # @return [Hash] Configure connection options for Faraday
16
+ # @!attribute middleware
17
+ # @see https://github.com/lostisland/faraday
18
+ # @return [Faraday::Builder or Faraday::RackBuilder] Configure middleware for Faraday
19
+ # @!attribute per_page
20
+ # @return [String] Configure page size for paginated results. API default: 30
21
+ # @!attribute user_agent
22
+ # @return [String] Configure User-Agent header for requests.
23
+
24
+ attr_accessor :access_token, :auto_paginate,
25
+ :connection_options, :default_media_type,
26
+ :middleware,
27
+ :per_page, :user_agent
28
+ attr_writer :api_endpoint
29
+
30
+ class << self
31
+
32
+ # List of configurable keys for {Karatekit::Client}
33
+ # @return [Array] of option keys
34
+ def keys
35
+ @keys ||= [
36
+ :access_token,
37
+ :api_endpoint,
38
+ :default_media_type,
39
+ :auto_paginate,
40
+ :connection_options,
41
+ :middleware,
42
+ :per_page,
43
+ :user_agent,
44
+ ]
45
+ end
46
+ end
47
+
48
+ # Set configuration options using a block
49
+ def configure
50
+ yield self
51
+ end
52
+
53
+ # Reset configuration options to default values
54
+ def reset!
55
+ Karatekit::Configurable.keys.each do |key|
56
+ instance_variable_set(:"@#{key}", Karatekit::Default.options[key])
57
+ end
58
+ self
59
+ end
60
+ alias setup reset!
61
+
62
+ # Compares client options to a Hash of requested options
63
+ #
64
+ # @param opts [Hash] Options to compare with current client options
65
+ # @return [Boolean]
66
+ def same_options?(opts)
67
+ opts.hash == options.hash
68
+ end
69
+
70
+ def api_endpoint
71
+ File.join(@api_endpoint, "")
72
+ end
73
+
74
+ private
75
+
76
+ def options
77
+ Hash[Karatekit::Configurable.keys.map{|key| [key, instance_variable_get(:"@#{key}")]}]
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,191 @@
1
+ require 'sawyer'
2
+ require 'karatekit/authentication'
3
+ module Karatekit
4
+
5
+ # Network layer for API clients.
6
+ module Connection
7
+
8
+ include Karatekit::Authentication
9
+
10
+ # Header keys that can be passed in options hash to {#get},{#head}
11
+ CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
12
+
13
+ # Make a HTTP GET request
14
+ #
15
+ # @param url [String] The path, relative to {#api_endpoint}
16
+ # @param options [Hash] Query and header params for request
17
+ # @return [Sawyer::Resource]
18
+ def get(url, options = {})
19
+ request :get, url, parse_query_and_convenience_headers(options)
20
+ end
21
+
22
+ # Make a HTTP POST request
23
+ #
24
+ # @param url [String] The path, relative to {#api_endpoint}
25
+ # @param options [Hash] Body and header params for request
26
+ # @return [Sawyer::Resource]
27
+ def post(url, options = {})
28
+ request :post, url, options
29
+ end
30
+
31
+ # Make a HTTP PUT request
32
+ #
33
+ # @param url [String] The path, relative to {#api_endpoint}
34
+ # @param options [Hash] Body and header params for request
35
+ # @return [Sawyer::Resource]
36
+ def put(url, options = {})
37
+ request :put, url, options
38
+ end
39
+
40
+ # Make a HTTP PATCH request
41
+ #
42
+ # @param url [String] The path, relative to {#api_endpoint}
43
+ # @param options [Hash] Body and header params for request
44
+ # @return [Sawyer::Resource]
45
+ def patch(url, options = {})
46
+ request :patch, url, options
47
+ end
48
+
49
+ # Make a HTTP DELETE request
50
+ #
51
+ # @param url [String] The path, relative to {#api_endpoint}
52
+ # @param options [Hash] Query and header params for request
53
+ # @return [Sawyer::Resource]
54
+ def delete(url, options = {})
55
+ request :delete, url, options
56
+ end
57
+
58
+ # Make a HTTP HEAD request
59
+ #
60
+ # @param url [String] The path, relative to {#api_endpoint}
61
+ # @param options [Hash] Query and header params for request
62
+ # @return [Sawyer::Resource]
63
+ def head(url, options = {})
64
+ request :head, url, parse_query_and_convenience_headers(options)
65
+ end
66
+
67
+ # Make one or more HTTP GET requests, optionally fetching
68
+ # the next page of results from URL in Link response header based
69
+ # on value in {#auto_paginate}.
70
+ #
71
+ # @param url [String] The path, relative to {#api_endpoint}
72
+ # @param options [Hash] Query and header params for request
73
+ # @param block [Block] Block to perform the data concatination of the
74
+ # multiple requests. The block is called with two parameters, the first
75
+ # contains the contents of the requests so far and the second parameter
76
+ # contains the latest response.
77
+ # @return [Sawyer::Resource]
78
+ def paginate(url, options = {}, &block)
79
+ opts = parse_query_and_convenience_headers(options)
80
+ if @auto_paginate || @per_page
81
+ opts[:query][:per_page] ||= @per_page || (@auto_paginate ? 100 : nil)
82
+ end
83
+
84
+ data = request(:get, url, opts.dup)
85
+
86
+ if @auto_paginate
87
+ while @last_response.rels[:next] && rate_limit.remaining > 0
88
+ @last_response = @last_response.rels[:next].get(:headers => opts[:headers])
89
+ if block_given?
90
+ yield(data, @last_response)
91
+ else
92
+ data.concat(@last_response.data) if @last_response.data.is_a?(Array)
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ data
99
+ end
100
+
101
+ # Hypermedia agent for the kampfsport.center API
102
+ #
103
+ # @return [Sawyer::Agent]
104
+ def agent
105
+ @agent ||= Sawyer::Agent.new(endpoint, sawyer_options) do |http|
106
+ http.headers[:accept] = default_media_type
107
+ http.headers[:content_type] = "application/json"
108
+ http.headers[:user_agent] = user_agent
109
+ http.authorization 'Token', @access_token
110
+ end
111
+ end
112
+
113
+ # Fetch the root resource for the API
114
+ #
115
+ # @return [Sawyer::Resource]
116
+ def root
117
+ get "/"
118
+ end
119
+
120
+ # Response for last HTTP request
121
+ #
122
+ # @return [Sawyer::Response]
123
+ def last_response
124
+ @last_response if defined? @last_response
125
+ end
126
+
127
+ protected
128
+
129
+ def endpoint
130
+ api_endpoint
131
+ end
132
+
133
+ private
134
+
135
+ def reset_agent
136
+ @agent = nil
137
+ end
138
+
139
+ def request(method, path, data, options = {})
140
+ if data.is_a?(Hash)
141
+ options[:query] = data.delete(:query) || {}
142
+ options[:headers] = data.delete(:headers) || {}
143
+ if accept = data.delete(:accept)
144
+ options[:headers][:accept] = accept
145
+ end
146
+ end
147
+
148
+ @last_response = response = agent.call(method, Addressable::URI.parse(path.to_s).normalize.to_s, data, options)
149
+ response.data
150
+ end
151
+
152
+ # Executes the request, checking if it was successful
153
+ #
154
+ # @return [Boolean] True on success, false otherwise
155
+ def boolean_from_response(method, path, options = {})
156
+ request(method, path, options)
157
+ @last_response.status == 204
158
+ rescue Karatekit::NotFound
159
+ false
160
+ end
161
+
162
+ def sawyer_options
163
+ opts = {
164
+ :links_parser => Sawyer::LinkParsers::Simple.new
165
+ }
166
+ conn_opts = @connection_options
167
+ conn_opts[:builder] = @middleware if @middleware
168
+ conn_opts[:proxy] = @proxy if @proxy
169
+ conn_opts[:ssl] = { :verify_mode => @ssl_verify_mode } if @ssl_verify_mode
170
+ opts[:faraday] = Faraday.new(conn_opts)
171
+
172
+ opts
173
+ end
174
+
175
+ def parse_query_and_convenience_headers(options)
176
+ options = options.dup
177
+ headers = options.delete(:headers) { Hash.new }
178
+ CONVENIENCE_HEADERS.each do |h|
179
+ if header = options.delete(h)
180
+ headers[h] = header
181
+ end
182
+ end
183
+ query = options.delete(:query)
184
+ opts = {:query => options}
185
+ opts[:query].merge!(query) if query && query.is_a?(Hash)
186
+ opts[:headers] = headers unless headers.empty?
187
+
188
+ opts
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,97 @@
1
+ # require 'karatekit/middleware/follow_redirects'
2
+ require 'karatekit/response/raise_error'
3
+ # require 'karatekit/response/feed_parser'
4
+ require 'karatekit/version'
5
+
6
+ module Karatekit
7
+
8
+ # Default configuration options for {Client}
9
+ module Default
10
+
11
+ # Default API endpoint
12
+ API_ENDPOINT = "https://api.kampfsport.center".freeze
13
+
14
+ # Default User Agent header string
15
+ USER_AGENT = "Karatekit Ruby Gem #{Karatekit::VERSION}".freeze
16
+
17
+ # Default media type
18
+ MEDIA_TYPE = "application/json".freeze
19
+
20
+ # In Faraday 0.9, Faraday::Builder was renamed to Faraday::RackBuilder
21
+ RACK_BUILDER_CLASS = defined?(Faraday::RackBuilder) ? Faraday::RackBuilder : Faraday::Builder
22
+
23
+ # Default Faraday middleware stack
24
+ MIDDLEWARE = RACK_BUILDER_CLASS.new do |builder|
25
+ builder.use Faraday::Request::Retry, exceptions: [Karatekit::ServerError]
26
+ # builder.use Karatekit::Middleware::FollowRedirects
27
+ builder.use Karatekit::Response::RaiseError
28
+ # builder.use Karatekit::Response::FeedParser
29
+ builder.adapter Faraday.default_adapter
30
+ end
31
+
32
+ class << self
33
+
34
+ # Configuration options
35
+ # @return [Hash]
36
+ def options
37
+ Hash[Karatekit::Configurable.keys.map{|key| [key, send(key)]}]
38
+ end
39
+
40
+ # Default access token from ENV
41
+ # @return [String]
42
+ def access_token
43
+ ENV['KARATEKIT_ACCESS_TOKEN']
44
+ end
45
+
46
+ # Default API endpoint from ENV or {API_ENDPOINT}
47
+ # @return [String]
48
+ def api_endpoint
49
+ ENV['KARATEKIT_API_ENDPOINT'] || API_ENDPOINT
50
+ end
51
+
52
+ # Default pagination preference from ENV
53
+ # @return [String]
54
+ def auto_paginate
55
+ ENV['KARATEKIT_AUTO_PAGINATE']
56
+ end
57
+
58
+ # Default options for Faraday::Connection
59
+ # @return [Hash]
60
+ def connection_options
61
+ {
62
+ :headers => {
63
+ :accept => default_media_type,
64
+ :user_agent => user_agent
65
+ }
66
+ }
67
+ end
68
+
69
+ # Default media type from {MEDIA_TYPE}
70
+ # @return [String]
71
+ def default_media_type
72
+ MEDIA_TYPE
73
+ end
74
+
75
+ # Default middleware stack for Faraday::Connection
76
+ # from {MIDDLEWARE}
77
+ # @return [Faraday::RackBuilder or Faraday::Builder]
78
+ def middleware
79
+ MIDDLEWARE
80
+ end
81
+
82
+ # Default pagination page size from ENV
83
+ # @return [Integer] Page size
84
+ def per_page
85
+ page_size = ENV['KARATEKIT_PER_PAGE']
86
+
87
+ page_size.to_i if page_size
88
+ end
89
+
90
+ # Default User-Agent header string from ENV or {USER_AGENT}
91
+ # @return [String]
92
+ def user_agent
93
+ ENV['KARATEKIT_USER_AGENT'] || USER_AGENT
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,244 @@
1
+ module Karatekit
2
+ # Custom error class for rescuing from all kampfsport.center errors
3
+ class Error < StandardError
4
+
5
+ # Returns the appropriate Karatekit::Error subclass based
6
+ # on status and response message
7
+ #
8
+ # @param [Hash] response HTTP response
9
+ # @return [Karatekit::Error]
10
+ def self.from_response(response)
11
+ status = response[:status].to_i
12
+ body = response[:body].to_s
13
+ headers = response[:response_headers]
14
+
15
+ if klass = case status
16
+ when 400 then Karatekit::BadRequest
17
+ when 401 then error_for_401(headers)
18
+ when 403 then error_for_403(body)
19
+ when 404 then error_for_404(body)
20
+ when 405 then Karatekit::MethodNotAllowed
21
+ when 406 then Karatekit::NotAcceptable
22
+ when 409 then Karatekit::Conflict
23
+ when 415 then Karatekit::UnsupportedMediaType
24
+ when 422 then Karatekit::UnprocessableEntity
25
+ when 451 then Karatekit::UnavailableForLegalReasons
26
+ when 400..499 then Karatekit::ClientError
27
+ when 500 then Karatekit::InternalServerError
28
+ when 501 then Karatekit::NotImplemented
29
+ when 502 then Karatekit::BadGateway
30
+ when 503 then Karatekit::ServiceUnavailable
31
+ when 500..599 then Karatekit::ServerError
32
+ end
33
+ klass.new(response)
34
+ end
35
+ end
36
+
37
+ def initialize(response=nil)
38
+ @response = response
39
+ super(build_error_message)
40
+ end
41
+
42
+ # Documentation URL returned by the API for some errors
43
+ #
44
+ # @return [String]
45
+ def documentation_url
46
+ data[:documentation_url] if data.is_a? Hash
47
+ end
48
+
49
+ # Returns most appropriate error for 401 HTTP status code
50
+ # @private
51
+ def self.error_for_401(headers)
52
+ Karatekit::Unauthorized
53
+ end
54
+
55
+ # Returns most appropriate error for 403 HTTP status code
56
+ # @private
57
+ def self.error_for_403(body)
58
+ if body =~ /rate limit exceeded/i
59
+ Karatekit::TooManyRequests
60
+ elsif body =~ /login attempts exceeded/i
61
+ Karatekit::TooManyLoginAttempts
62
+ elsif body =~ /abuse/i
63
+ Karatekit::AbuseDetected
64
+ elsif body =~ /email address must be verified/i
65
+ Karatekit::UnverifiedEmail
66
+ elsif body =~ /account was suspended/i
67
+ Karatekit::AccountSuspended
68
+ else
69
+ Karatekit::Forbidden
70
+ end
71
+ end
72
+
73
+ # Return most appropriate error for 404 HTTP status code
74
+ # @private
75
+ def self.error_for_404(body)
76
+ Karatekit::NotFound
77
+ end
78
+
79
+ # Array of validation errors
80
+ # @return [Array<Hash>] Error info
81
+ def errors
82
+ if data && data.is_a?(Hash)
83
+ data[:errors] || []
84
+ else
85
+ []
86
+ end
87
+ end
88
+
89
+ # Status code returned by the kampfsport.center server.
90
+ #
91
+ # @return [Integer]
92
+ def response_status
93
+ @response[:status]
94
+ end
95
+
96
+ # Headers returned by the kampfsport.center server.
97
+ #
98
+ # @return [Hash]
99
+ def response_headers
100
+ @response[:response_headers]
101
+ end
102
+
103
+ # Body returned by the kampfsport.center server.
104
+ #
105
+ # @return [String]
106
+ def response_body
107
+ @response[:body]
108
+ end
109
+
110
+ private
111
+
112
+ def data
113
+ @data ||=
114
+ if (body = @response[:body]) && !body.empty?
115
+ if body.is_a?(String) &&
116
+ @response[:response_headers] &&
117
+ @response[:response_headers][:content_type] =~ /json/
118
+
119
+ Sawyer::Agent.serializer.decode(body)
120
+ else
121
+ body
122
+ end
123
+ else
124
+ nil
125
+ end
126
+ end
127
+
128
+ def response_message
129
+ case data
130
+ when Hash
131
+ data[:message]
132
+ when String
133
+ data
134
+ end
135
+ end
136
+
137
+ def response_error
138
+ "Error: #{data[:error]}" if data.is_a?(Hash) && data[:error]
139
+ end
140
+
141
+ def response_error_summary
142
+ return nil unless data.is_a?(Hash) && !Array(data[:errors]).empty?
143
+
144
+ summary = "\nError summary:\n"
145
+ summary << data[:errors].map do |error|
146
+ if error.is_a? Hash
147
+ error.map { |k,v| " #{k}: #{v}" }
148
+ else
149
+ " #{error}"
150
+ end
151
+ end.join("\n")
152
+
153
+ summary
154
+ end
155
+
156
+ def build_error_message
157
+ return nil if @response.nil?
158
+
159
+ message = "#{@response[:method].to_s.upcase} "
160
+ message << redact_url(@response[:url].to_s) + ": "
161
+ message << "#{@response[:status]} - "
162
+ message << "#{response_message}" unless response_message.nil?
163
+ message << "#{response_error}" unless response_error.nil?
164
+ message << "#{response_error_summary}" unless response_error_summary.nil?
165
+ message << " // See: #{documentation_url}" unless documentation_url.nil?
166
+ message
167
+ end
168
+
169
+ def redact_url(url_string)
170
+ %w[client_secret access_token].each do |token|
171
+ url_string.gsub!(/#{token}=\S+/, "#{token}=(redacted)") if url_string.include? token
172
+ end
173
+ url_string
174
+ end
175
+ end
176
+
177
+ # Raised on errors in the 400-499 range
178
+ class ClientError < Error; end
179
+
180
+ # Raised when kampfsport.center returns a 400 HTTP status code
181
+ class BadRequest < ClientError; end
182
+
183
+ # Raised when kampfsport.center returns a 401 HTTP status code
184
+ class Unauthorized < ClientError; end
185
+
186
+ # Raised when kampfsport.center returns a 403 HTTP status code
187
+ class Forbidden < ClientError; end
188
+
189
+ # Raised when kampfsport.center returns a 403 HTTP status code
190
+ # and body matches 'rate limit exceeded'
191
+ class TooManyRequests < Forbidden; end
192
+
193
+ # Raised when kampfsport.center returns a 403 HTTP status code
194
+ # and body matches 'login attempts exceeded'
195
+ class TooManyLoginAttempts < Forbidden; end
196
+
197
+ # Raised when kampfsport.center returns a 403 HTTP status code
198
+ # and body matches 'abuse'
199
+ class AbuseDetected < Forbidden; end
200
+
201
+ # Raised when kampfsport.center returns a 403 HTTP status code
202
+ # and body matches 'email address must be verified'
203
+ class UnverifiedEmail < Forbidden; end
204
+
205
+ # Raised when kampfsport.center returns a 403 HTTP status code
206
+ # and body matches 'account was suspended'
207
+ class AccountSuspended < Forbidden; end
208
+
209
+ # Raised when kampfsport.center returns a 404 HTTP status code
210
+ class NotFound < ClientError; end
211
+
212
+ # Raised when kampfsport.center returns a 405 HTTP status code
213
+ class MethodNotAllowed < ClientError; end
214
+
215
+ # Raised when kampfsport.center returns a 406 HTTP status code
216
+ class NotAcceptable < ClientError; end
217
+
218
+ # Raised when kampfsport.center returns a 409 HTTP status code
219
+ class Conflict < ClientError; end
220
+
221
+ # Raised when kampfsport.center returns a 414 HTTP status code
222
+ class UnsupportedMediaType < ClientError; end
223
+
224
+ # Raised when kampfsport.center returns a 422 HTTP status code
225
+ class UnprocessableEntity < ClientError; end
226
+
227
+ # Raised on errors in the 500-599 range
228
+ class ServerError < Error; end
229
+
230
+ # Raised when kampfsport.center returns a 500 HTTP status code
231
+ class InternalServerError < ServerError; end
232
+
233
+ # Raised when kampfsport.center returns a 501 HTTP status code
234
+ class NotImplemented < ServerError; end
235
+
236
+ # Raised when kampfsport.center returns a 502 HTTP status code
237
+ class BadGateway < ServerError; end
238
+
239
+ # Raised when kampfsport.center returns a 503 HTTP status code
240
+ class ServiceUnavailable < ServerError; end
241
+
242
+ # Raised when client fails to provide valid Content-Type
243
+ class MissingContentType < ArgumentError; end
244
+ end