# encoding: utf-8
require 'github_api/configuration'
require 'github_api/mime_type'
require 'github_api/rate_limit'
require 'github_api/core_ext/hash'
require 'github_api/core_ext/array'
require 'github_api/null_encoder'
require 'github_api/request/verbs'
require 'github_api/api/actions'
require 'github_api/api/factory'
require 'github_api/api/arguments'
module Github
# Core class responsible for api interface operations
class API
extend Github::ClassMethods
include Constants
include Authorization
include MimeType
include Request::Verbs
include RateLimit
attr_reader *Github.configuration.property_names
attr_accessor *Validations::VALID_API_KEYS
attr_accessor :current_options
# Callback to update current configuration options
class_eval do
Github.configuration.property_names.each do |key|
define_method "#{key}=" do |arg|
self.instance_variable_set("@#{key}", arg)
self.current_options.merge!({:"#{key}" => arg})
end
end
end
# Create new API
#
# @api public
def initialize(options={}, &block)
opts = Github.configuration.fetch.merge(options)
@current_options = opts
Github.configuration.property_names.each do |key|
send("#{key}=", opts[key])
end
if opts.key?(:login) && !opts[:login].nil?
@login, @password = opts[:login], opts[:password]
elsif opts.key?(:basic_auth) && !opts[:basic_auth].nil?
@login, @password = extract_basic_auth(opts[:basic_auth])
end
yield_or_eval(&block) if block_given?
end
# Call block with argument
#
# @api private
def yield_or_eval(&block)
return unless block
block.arity > 0 ? yield(self) : self.instance_eval(&block)
end
# Extract login and password from basic_auth parameter
#
# @api private
def extract_basic_auth(auth)
case auth
when String
auth.split(':', 2)
when Hash
[auth[:login], auth[:password]]
end
end
# List of before callbacks
#
# @api public
def self.before_callbacks
@before_callbacks ||= []
end
# List of after callbacks
#
# @api public
def self.after_callbacks
@after_callbacks ||= []
end
# Before request filter
#
# @api public
def self.before_request(callback, params = {})
before_callbacks << params.merge(callback: callback)
end
# After request filter
#
# @api public
def self.after_request(callback, params = {})
after_callbacks << params.merge(callback: callback)
end
class << self
attr_reader :root
alias_method :root?, :root
end
def self.root!
@root = true
end
def self.inherited(child_class)
before_callbacks.reverse_each { |callback|
child_class.before_callbacks.unshift(callback)
}
after_callbacks.reverse_each { |callback|
child_class.after_callbacks.unshift(callback)
}
extend_with_actions(child_class)
unless child_class.instance_variable_defined?(:@root)
child_class.instance_variable_set(:@root, false)
end
super
end
root!
def self.internal_methods
api = self
api = api.superclass until api.root?
api.public_instance_methods(true)
end
def self.extra_methods
['actions']
end
# Find all the api methods that should be considred by
# request callbacks.
#
# @return [Set]
#
# @api private
def self.request_methods
@request_methods ||= begin
methods = (public_instance_methods(true) -
internal_methods +
public_instance_methods(false)).uniq.map(&:to_s)
Set.new(methods - extra_methods)
end
end
def self.clear_request_methods!
@request_methods = nil
end
def self.method_added(method_name)
method_name = method_name.to_s.gsub(/_with(out)?_callback_.*$/, '')
# Only subclasses matter
return if self.root?
return if extra_methods.include?(method_name)
# Only public methods are of interest
return unless request_methods.include?(method_name)
# Do not redefine
return if (@__methods_added ||= []).include?(method_name)
class_name = self.name.to_s.split('::').last.downcase
with_method = "#{method_name}_with_callback_#{class_name}"
without_method = "#{method_name}_without_callback_#{class_name}"
return if public_method_defined?(with_method)
[method_name, with_method, without_method].each do |met|
@__methods_added << met
end
return if public_method_defined?(with_method)
define_method(with_method) do |*args, &block|
send(:execute, without_method, *args, &block)
end
alias_method without_method, method_name
alias_method method_name, with_method
clear_request_methods!
end
# Filter callbacks based on kind
#
# @param [Symbol] kind
# one of :before or :after
#
# @return [Array[Hash]]
#
# @api private
def filter_callbacks(kind, action_name)
matched_callbacks = self.class.send("#{kind}_callbacks").select do |callback|
callback[:only].nil? || callback[:only].include?(action_name)
end
end
# Run all callbacks associated with this action
#
# @apram [Symbol] action_name
#
# @api private
def run_callbacks(action_name, &block)
filter_callbacks(:before, action_name).each { |hook| send hook[:callback] }
yield if block_given?
filter_callbacks(:after, action_name).each { |hook| send hook[:callback] }
end
# Execute action
#
# @param [Symbol] action
#
# @api private
def execute(action, *args, &block)
action_name = action.to_s.gsub(/_with(out)?_callback_.*$/, '')
result = nil
run_callbacks(action_name) do
result = send(action, *args, &block)
end
result
end
# Responds to attribute query or attribute clear
#
# @api private
def method_missing(method_name, *args, &block) # :nodoc:
case method_name.to_s
when /^(.*)\?$/
return !!send($1.to_s)
when /^clear_(.*)$/
send("#{$1.to_s}=", nil)
else
super
end
end
# Acts as setter and getter for api requests arguments parsing.
#
# Returns Arguments instance.
#
def arguments(args=(not_set = true), options={}, &block)
if not_set
@arguments
else
@arguments = Arguments.new(options.merge!(api: self)).parse(*args, &block)
end
end
# Set a configuration option for a given namespace
#
# @param [String] option
# @param [Object] value
# @param [Boolean] ignore_setter
#
# @return [self]
#
# @api public
def set(option, value=(not_set=true), ignore_setter=false, &block)
raise ArgumentError, 'value not set' if block and !not_set
return self if !not_set and value.nil?
if not_set
set_options option
return self
end
if respond_to?("#{option}=") and not ignore_setter
return __send__("#{option}=", value)
end
define_accessors option, value
self
end
# Defines a namespace
#
# @param [Array[Symbol]] names
# the name for the scope
#
# @example
# namespace :scopes
#
# @return [self]
#
# @api public
def self.namespace(*names)
options = names.last.is_a?(Hash) ? names.pop : {}
names = names.map(&:to_sym)
name = names.pop
if public_method_defined?(name)
raise ArgumentError, "namespace '#{name}' is already defined"
end
class_name = extract_class_name(name, options)
define_method(name) do |*args, &block|
options = args.last.is_a?(Hash) ? args.pop : {}
API::Factory.new(class_name, current_options.merge(options), &block)
end
end
# Extracts class name from options
#
# @param [Hash] options
# @option options [String] :full_name
# the full name for the class
# @option options [Boolean] :root
# if the class is at the root or not
#
# @example
# extract_class_name(:stats, class_name: :statistics)
#
# @return [String]
#
# @api private
def self.extract_class_name(name, options)
converted = options.fetch(:full_name, name).to_s
converted = converted.split('_').map(&:capitalize).join
class_name = options.fetch(:root, false) ? '': "#{self.name}::"
class_name += converted
class_name
end
private
# Set multiple options
#
# @api private
def set_options(options)
unless options.respond_to?(:each)
raise ArgumentError, 'cannot iterate over value'
end
options.each { |key, value| set(key, value) }
end
# Define setters and getters
#
# @api private
def define_accessors(option, value)
setter = proc { |val| set option, val, true }
getter = proc { value }
define_singleton_method("#{option}=", setter) if setter
define_singleton_method(option, getter) if getter
end
# Dynamically define a method for setting request option
#
# @api private
def define_singleton_method(method_name, content=Proc.new)
(class << self; self; end).class_eval do
undef_method(method_name) if method_defined?(method_name)
if String === content
class_eval("def #{method_name}() #{content}; end")
else
define_method(method_name, &content)
end
end
end
end # API
end # Github