eoat 0.1.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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/.yardopts +8 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +361 -0
  9. data/Rakefile +9 -0
  10. data/eoat.gemspec +37 -0
  11. data/lib/eoat.rb +80 -0
  12. data/lib/eoat/cache/file_cache.rb +109 -0
  13. data/lib/eoat/cache/memcached_cache.rb +54 -0
  14. data/lib/eoat/cache/none_cache.rb +36 -0
  15. data/lib/eoat/cache/redis_cache.rb +61 -0
  16. data/lib/eoat/eve_api.rb +49 -0
  17. data/lib/eoat/exception.rb +63 -0
  18. data/lib/eoat/request.rb +82 -0
  19. data/lib/eoat/result/eve_type.rb +163 -0
  20. data/lib/eoat/version.rb +4 -0
  21. data/lib/eoat/zk_api.rb +33 -0
  22. data/spec/eoat/eve_api_spec.rb +80 -0
  23. data/spec/eoat/eve_result_spec.rb +127 -0
  24. data/spec/eoat/file_cache_spec.rb +29 -0
  25. data/spec/eoat/memcached_cache_spec.rb +14 -0
  26. data/spec/eoat/redis_cache_spec.rb +14 -0
  27. data/spec/eoat/zk_api_spec.rb +55 -0
  28. data/spec/eoat_spec.rb +39 -0
  29. data/spec/fixtures/eve/account/APIKeyInfo.xml.aspx +13 -0
  30. data/spec/fixtures/eve/corp/KillLog.xml.aspx +6 -0
  31. data/spec/fixtures/eve/eve/CertificateTree.xml.aspx +3846 -0
  32. data/spec/fixtures/eve/eve/CharacterInfo.xml.aspx +24 -0
  33. data/spec/fixtures/eve/eve/CharacterName.xml.aspx +11 -0
  34. data/spec/fixtures/eve/eve/ErrorList.xml.aspx +91 -0
  35. data/spec/fixtures/eve/eve/FacWarStats.xml.aspx +31 -0
  36. data/spec/fixtures/eve/eve/FacWarTopStats.xml.aspx +733 -0
  37. data/spec/fixtures/eve/server/ServerStatus.xml.aspx +9 -0
  38. data/spec/fixtures/zkillboard/api-kills-allianceID-99002003-limit-5-xml +1 -0
  39. data/spec/fixtures/zkillboard/api-kills-solo-xml +1 -0
  40. data/spec/spec_helper.rb +86 -0
  41. metadata +259 -0
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'eoat/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'eoat'
8
+ spec.version = EOAT::VERSION
9
+ spec.authors = ['Ivan Kotov']
10
+ spec.email = ['i.spec.kotov@gmail.com']
11
+ spec.homepage = 'https://github.com/elDante/eoat'
12
+ spec.summary = %q{Eve Online API toolbox}
13
+ spec.description = %q{Eve Online API toolbox}
14
+ spec.license = 'MIT'
15
+
16
+ spec.platform = Gem::Platform::RUBY
17
+ spec.required_ruby_version = '>= 2.0.0'
18
+ spec.add_dependency 'httparty', '>= 0.11.0'
19
+
20
+ spec.post_install_message = 'Thank you for choosing EOAT.'
21
+
22
+ spec.files = `git ls-files`.split($/)
23
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_development_dependency 'bundler'
28
+ spec.add_development_dependency 'rake'
29
+ spec.add_development_dependency 'rspec-core', '~> 2.10.0'
30
+ spec.add_development_dependency 'rspec-expectations', '~> 2.10.0'
31
+ spec.add_development_dependency 'rr', '~> 1.0.4'
32
+ spec.add_development_dependency 'yard', '~> 0.8.7'
33
+ spec.add_development_dependency 'redcarpet', '~> 2.1.1'
34
+ spec.add_development_dependency 'webmock', '~> 1.13.0'
35
+ spec.add_development_dependency 'memcache', '~> 1.4.1'
36
+ spec.add_development_dependency 'redis', '~> 3.0.4'
37
+ end
@@ -0,0 +1,80 @@
1
+ require 'httparty'
2
+
3
+ require 'eoat/exception'
4
+ require 'eoat/request'
5
+ require 'eoat/version'
6
+ require 'eoat/cache/file_cache'
7
+ require 'eoat/cache/memcached_cache'
8
+ require 'eoat/cache/none_cache'
9
+ require 'eoat/cache/redis_cache'
10
+ require 'eoat/result/eve_type'
11
+
12
+ # Eve Online API Toolbox (EOAT) module
13
+ # @author Ivan Kotov {mailto:i.s.kotov.ws e-mail}
14
+ module EOAT
15
+ @cache = EOAT::Cache::NoneCache.new
16
+ @headers = {
17
+ 'User-Agent' => "EOAT/#{EOAT::VERSION} (Eve Online Api Toolbox;+https://github.com/elDante/eoat)",
18
+ 'Accept-Encoding' => 'gzip',
19
+ 'Accept-Charset' => 'utf-8'
20
+ }
21
+ @max_ttl = 30*24*60*60
22
+
23
+ # Return current cache storage class instance
24
+ # @example Get current cache storage
25
+ # EOAT.cache #=> #<EOAT::Cache::NoneCache:0x007ff97a8b6bd8>
26
+ # @return [EOAT::Cache #read] the instance of cache class
27
+ def self.cache
28
+ @cache
29
+ end
30
+ #
31
+ # Define new cache store class.
32
+ # @note Available cache classes:
33
+ # {EOAT::Cache::FileCache FileCache},
34
+ # {EOAT::Cache::MemcachedCache MemcachedCache},
35
+ # {EOAT::Cache::NoneCache NoneCache}
36
+ # {EOAT::Cache::RedisCache RedisCache}
37
+ # @example Store cache to memcached
38
+ # EOAT.cache = EOAT::Cache::MemcachedCache.new
39
+ # @param [CacheClass] val
40
+ def self.cache=(val)
41
+ if EOAT::Cache.constants.include? val.class.name.split('::').last.to_sym
42
+ @cache = val
43
+ else
44
+ raise TypeError, "Wrong cache class #{val.class}"
45
+ end
46
+ end
47
+
48
+ # This method allows to control the request headers
49
+ # @example Get current headers
50
+ # EOAT.headers #=> {"User-Agent"=>"EOAT/0.0.1 (Eve Online Api Toolbox;+https://github.com/elDante/eoat)"}
51
+ # @example Set 'From' header
52
+ # EOAT.headers['From'] = 'user@example.com' #=> 'user@example.com'
53
+ # @return [Hash #read] the hash of request headers
54
+ def self.headers
55
+ @headers
56
+ end
57
+
58
+ # Return a current maximum TTL of cache in seconds
59
+ # By default: 30 days. Has been introduced to support the memcached.
60
+ # Since the TTL is calculated from the cached_until - request_time,
61
+ # and it may be ~ 10 years.
62
+ # Example: https://api.eveonline.com/eve/SkillTree.xml.aspx
63
+ # @return [Fixnum, #read]
64
+ def self.max_ttl
65
+ @max_ttl
66
+ end
67
+
68
+ # Allow set maximum TTL of cache
69
+ # @param [Fixnum] val
70
+ def self.max_ttl=(val)
71
+ if val.class == Fixnum
72
+ @max_ttl = val
73
+ else
74
+ raise TypeError, "Wrong class #{val.class} of value, it should be Fixnum"
75
+ end
76
+ end
77
+ end
78
+
79
+ require 'eoat/eve_api'
80
+ require 'eoat/zk_api'
@@ -0,0 +1,109 @@
1
+ module EOAT
2
+ module Cache
3
+ # Store cache to files on disk.
4
+ # Default use `~/.eoat/cache` as destination directory.
5
+ # @author Ivan Kotov {mailto:i.s.kotov.ws e-mail}
6
+ # @example Set FileCache as a cache storage, with default destination directory
7
+ # EOAT.cache = EOAT::Cache::FileCache.new
8
+ # @example Set FileCache as a cache storage, with custom destination directory
9
+ # EOAT.cache = EOAT::Cache::FileCache.new('path/to/cache/dir')
10
+ class FileCache
11
+ require 'yaml'
12
+ require 'fileutils'
13
+
14
+ # @param [String] path destination path
15
+ def initialize(path = "#{ENV['HOME']}/.eoat/cache")
16
+ @path = path.chomp('/')
17
+ FileUtils.mkpath(@path) unless File.exists?(@path)
18
+ perform_cache_dir
19
+ end
20
+
21
+ # Get object from cache
22
+ # @param [String] host the request host string
23
+ # @param [String] uri the query string
24
+ # @return [Object, NilClass] the instance of result class
25
+ # or nil if key not does not exist
26
+ def get(host, uri)
27
+ perform_cache_dir
28
+ # Perform host string to md5 string
29
+ host = EOAT::Cache.md5hash(host)
30
+ # Perform uri string to md5 string
31
+ uri = EOAT::Cache.md5hash(uri)
32
+ # Combine parameters to path
33
+ request_path = "#{@path}/#{host}/#{uri}"
34
+ if File.exists?(request_path)
35
+ # Set now in UTC timezone to timestamp
36
+ now = Time.now.utc.to_i
37
+ # Get all directories in request_path
38
+ Dir["#{request_path}/*"].each do |dir|
39
+ # Check timestamp
40
+ if dir.split('/').last.to_i > now
41
+ return YAML::load_file("#{dir}/result.yml")
42
+ else
43
+ FileUtils.rmtree("#{dir}")
44
+ end
45
+ end
46
+ end
47
+ # Return false if data not found
48
+ false
49
+ end
50
+
51
+ # Save instance of result class.
52
+ # @param [String] host the request host string
53
+ # @param [String] uri the query string
54
+ # @param [Object] content the result class instance
55
+ def save(host, uri, content)
56
+ # Check expired
57
+ perform_cache_dir
58
+ # Calculate TTL in seconds
59
+ expire = (content.cached_until - content.request_time).to_i
60
+ # If TTL > EOAT.max_ttl set EOAT.max_tt as expire
61
+ expire = expire > EOAT.max_ttl ? EOAT.max_ttl : expire
62
+ # If 0 or a negative value, it does not save
63
+ if expire > 0
64
+ # Set expired as timestamp of date
65
+ expired = (content.request_time + expire).to_i
66
+ # Perform host string to md5 string
67
+ host = EOAT::Cache.md5hash(host)
68
+ # Perform uri string to md5 string
69
+ uri = EOAT::Cache.md5hash(uri)
70
+ # Combine parameters to path
71
+ save_path = "#{@path}/#{host}/#{uri}/#{expired}"
72
+ # This principle is not possible, but should be checked
73
+ if File.exists?(save_path)
74
+ raise EOAT::Exception::CacheSaveError.new("Save path: #{save_path} already exists!")
75
+ end
76
+ # Create directory
77
+ FileUtils.mkpath(save_path)
78
+ # Save instance to result.yml
79
+ File.write("#{save_path}/result.yml", content.to_yaml)
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # Validation of the file structure and the removal of expired data.
86
+ def perform_cache_dir
87
+ timestamps = Dir["#{@path}/*/*/*"]
88
+ unless timestamps.empty?
89
+ timestamp_now = Time.now.utc.to_i
90
+ timestamps.each do |timestamp|
91
+ FileUtils.rmtree(timestamp) if timestamp.split('/').last.to_i < timestamp_now
92
+ end
93
+ end
94
+ uri_hashes = Dir["#{@path}/*/*"]
95
+ unless uri_hashes.empty?
96
+ uri_hashes.each do |uri|
97
+ FileUtils.rmtree(uri) if Dir["#{uri}/*"].empty?
98
+ end
99
+ end
100
+ host_hashes = Dir["#{@path}/*"]
101
+ unless host_hashes.empty?
102
+ host_hashes.each do |host|
103
+ FileUtils.rmtree(host) if Dir["#{host}/*"].empty?
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,54 @@
1
+ module EOAT
2
+ module Cache
3
+ # Memcached cache handler. Used `gem memcache`
4
+ # Default use standard connection parameters.
5
+ # @author Ivan Kotov {mailto:i.s.kotov.ws e-mail}
6
+ # @example Set Memcached as a cache storage, with default Memcached `address` and `port`
7
+ # EOAT.cache = EOAT::Cache::MemcachedCache.new
8
+ # @example Set Redis as a cache storage, with connect to custom server:port and not set key prefix
9
+ # EOAT.cache = EOAT::Cache::MemcachedCache.new('10.0.1.1:11212', '')
10
+ class MemcachedCache
11
+ require 'yaml'
12
+
13
+ # @param [String] server the connection string `<ip-address>:<port>`
14
+ # @param [String] prefix the prefix for keys
15
+ def initialize(server='localhost:11211', prefix='eoat')
16
+ require 'memcache'
17
+
18
+ @backend = Memcache.new(:server => server, :namespace => prefix, :segment_large_values => true)
19
+ end
20
+
21
+ # Get object from cache
22
+ # @param [String] host the request host string
23
+ # @param [String] uri the query string
24
+ # @return [Object, NilClass] the instance of result class
25
+ # or nil if key not does not exist
26
+ def get(host, uri)
27
+ # Set key as md5 string
28
+ key = EOAT::Cache.md5hash(host + uri)
29
+ @backend.get(key)
30
+ end
31
+
32
+ # Save instance of result class.
33
+ # @param [String] host the request host string
34
+ # @param [String] uri the query string
35
+ # @param [Object] content the result class instance
36
+ def save(host, uri, content)
37
+ # Calculate TTL in seconds
38
+ expire = (content.cached_until - content.request_time).to_i
39
+ # If TTL > EOAT.max_ttl set EOAT.max_tt as expire
40
+ expire = expire > EOAT.max_ttl ? EOAT.max_ttl : expire
41
+ # If 0 or a negative value, it does not save.
42
+ if expire > 0
43
+ # Set key as md5 string
44
+ key = EOAT::Cache.md5hash(host + uri)
45
+ @backend.set(
46
+ key,
47
+ content,
48
+ :expiry => expire
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,36 @@
1
+ module EOAT
2
+ # Collection of EOAT cache handlers
3
+ # @author Ivan Kotov {mailto:i.s.kotov.ws e-mail}
4
+ # All handlers have a public methods: #get and #save
5
+ module Cache
6
+ # NoneCache - fake cache class. The default cache handler.
7
+ # @author Ivan Kotov {mailto:i.s.kotov.ws e-mail}
8
+ class NoneCache
9
+
10
+ # Fake get method. Always return false
11
+ # @param [String] host the request host string
12
+ # @param [String] uri the query string
13
+ # @return [FalseClass]
14
+ def get(host, uri)
15
+ return false
16
+ end
17
+
18
+ # Fake save method. It does not make anything
19
+ # @param [String] host the request host string
20
+ # @param [String] uri the query string
21
+ # @param [Object] content the result class instance
22
+ def save(host, uri, content)
23
+ end
24
+ end
25
+
26
+ # Module method. Calculating md5 of string.
27
+ # Used by all cached handlers.
28
+ # @param [String] string
29
+ # @return [String] the md5 hash string
30
+ def self.md5hash(string)
31
+ require 'digest/md5'
32
+
33
+ return Digest::MD5.hexdigest(string.to_s)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,61 @@
1
+ module EOAT
2
+ module Cache
3
+ # Redis cache handler. Used `gem redis`
4
+ # Default use standard connection parameters.
5
+ # @author Ivan Kotov {mailto:i.s.kotov.ws e-mail}
6
+ # @example Set Redis as a cache storage, with default Redis `address` and `port`
7
+ # EOAT.cache = EOAT::Cache::RedisCache.new
8
+ # @example Set Redis as a cache storage, with connect to socket and not set key prefix
9
+ # EOAT.cache = EOAT::Cache::RedisCache.new(:path => '/var/run/redis.sock', :prefix => '')
10
+ class RedisCache
11
+
12
+ # @see https://github.com/redis/redis-rb Official gem website
13
+ # @param [Hash] kwargs the keywords arguments.
14
+ # If not send `:prefix => value` set default `:prefix => 'eoat:'`
15
+ # Allows take the connection parameters, example
16
+ # `:host => "10.0.1.1", :port => 6380` or `:path => "/tmp/redis.sock"`
17
+ def initialize(**kwargs)
18
+ require 'redis'
19
+
20
+ @prefix = kwargs.key?(:prefix) ? kwargs.delete(:prefix) : 'eoat:'
21
+ @backend = Redis.new(kwargs)
22
+ end
23
+
24
+ # Get object from cache
25
+ # @param [String] host the request host string
26
+ # @param [String] uri the query string
27
+ # @return [Object, NilClass] the instance of result class
28
+ # or nil if key not does not exist
29
+ def get(host, uri)
30
+ # Set key as md5 string
31
+ key = @prefix + EOAT::Cache.md5hash(host + uri)
32
+ cache = @backend.get(key)
33
+ # If the data is successfully received,
34
+ # then restore instance from yaml string
35
+ cache ? YAML::load(cache) : false
36
+ end
37
+
38
+ # Save instance of result class.
39
+ # @param [String] host the request host string
40
+ # @param [String] uri the query string
41
+ # @param [Object] content the result class instance
42
+ def save(host, uri, content)
43
+ # Calculate TTL in seconds
44
+ expire = (content.cached_until - content.request_time).to_i
45
+ # If TTL > EOAT.max_ttl set EOAT.max_tt as expire
46
+ expire = expire > EOAT.max_ttl ? EOAT.max_ttl : expire
47
+ # If 0 or a negative value, it does not save.
48
+ if expire > 0
49
+ # Set key as md5 string
50
+ key = @prefix + EOAT::Cache.md5hash(host + uri)
51
+ # Export result instance to yaml string.
52
+ yaml = content.to_yaml
53
+ # Store yaml string in Redis
54
+ @backend.set(key, yaml)
55
+ # Set TTL
56
+ @backend.expire(key, expire)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,49 @@
1
+ module EOAT
2
+ # EveApi class - call class. Collects user input, building a url request
3
+ # and passes it to the Request class.
4
+ # @author Ivan Kotov {mailto:i.s.kotov.ws e-mail}
5
+ # @example Get EVE skills from https://api.eveonline.com/eve/SkillTree.xml.aspx
6
+ # skills = EOAT::EveApi.new.SkillTree
7
+ # skills.result #=> ["skillGroups"]
8
+ # @example Get API key info
9
+ # key_info = EOAT::EveApi.new('keyID', 'vCode', scope: 'account').APIKeyInfo
10
+ # key_info.key.type #=> "Corporation"
11
+ class EveApi
12
+
13
+ # @param [Integer, String] key_id EVE API keyID
14
+ # @param [String] v_code EVE API vCode
15
+ # @param [String] scope part of request uri
16
+ # @param [String] host request host
17
+ def initialize(key_id='', v_code='', scope: 'eve', host: 'https://api.eveonline.com')
18
+ @key_id = key_id.to_s
19
+ @v_code = v_code.to_s
20
+ @scope = scope.to_s
21
+ @host = host.to_s
22
+ unless key_id.to_s.empty?
23
+ raise ArgumentError, 'You do not specify vCode' if v_code.to_s.empty?
24
+ end
25
+ raise ArgumentError, 'vCode must be string' unless v_code.instance_of? String
26
+ end
27
+
28
+ # Create an request according to the method called.
29
+ # This is used to dynamically create api calls.
30
+ # @return [Object] the result class
31
+ def method_missing(method, **kwargs)
32
+ uri = create_uri(method.id2name, kwargs)
33
+ EOAT::Request.new(@host, uri, EOAT::Result::EveType::Result).get
34
+ end
35
+
36
+ private
37
+
38
+ # Collect all request parameters and combine it to query string.
39
+ # @param [String] fake_method the name of missing method
40
+ # @param [Hash] kwargs the keyword arguments
41
+ # @return [String]
42
+ def create_uri(fake_method, **kwargs)
43
+ kwargs = kwargs.merge({:keyID => @key_id, :vCode => @v_code}) unless @key_id.empty?
44
+ query_string = kwargs ? kwargs.map {|k, v| "#{k}=#{v}"}.join('&') : ''
45
+ query_string.insert(0, '?') unless query_string.empty?
46
+ "#{@scope}/#{fake_method}.xml.aspx#{query_string}"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,63 @@
1
+ module EOAT
2
+ # Collection of all EOAT exceptions
3
+ # @author Ivan Kotov {mailto:i.s.kotov.ws e-mail}
4
+ module Exception
5
+ # Standard EOAT error.
6
+ # All other exception inherited from it.
7
+ class EOATError < StandardError
8
+ end
9
+
10
+ # It is used when the HTTP response code different from 200, 404 or 0.
11
+ class HTTPError < EOATError
12
+ attr_reader :status, :headers
13
+
14
+ # @param [Integer] status the HTTP status code
15
+ # @param [Hash] headers the Hash of response headers
16
+ def initialize(status, headers)
17
+ @status = status
18
+ @headers = headers
19
+ end
20
+ end
21
+
22
+ # Raised when response return 404 HTTP error
23
+ class HTTP404Error < EOATError
24
+ end
25
+
26
+ # Used when response data is incorrect.
27
+ class ParseError < EOATError
28
+ end
29
+
30
+ # Very rare error. Made just in case.
31
+ # Used to prevent overwriting the cache.
32
+ class CacheSaveError < EOATError
33
+ end
34
+
35
+ # All EveApi xml errors will be raise this error.
36
+ # It has a method that stores the error number.
37
+ # @see
38
+ # https://api.eveonline.com/eve/ErrorList.xml.aspx Full error list
39
+ class EveApiError < EOATError
40
+ attr_reader :number
41
+
42
+ # @param [Integer] number the custom EVE API error number
43
+ def initialize(number)
44
+ @number = number
45
+ end
46
+ end
47
+
48
+ # TODO: Check parse algorithm
49
+
50
+ # Called when the HTTP response code is 0
51
+ # Parse EveType xml error page and raise EveApiError with parsed number.
52
+ # @param [Hash] page parsed response body
53
+ # @raise [EOAT::Exception::EveApiError] EVE API response custom error
54
+ def self.parse_error_page(page)
55
+ if page.class == Hash
56
+ if page['eveapi']
57
+ error = page['eveapi']['error']
58
+ raise EOAT::Exception::EveApiError.new(error['code'].to_i), "#{error['__content__']}"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end