greeve 0.0.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/greeve.rb ADDED
@@ -0,0 +1,29 @@
1
+ require "bigdecimal"
2
+ require "ox"
3
+ require "typhoeus"
4
+
5
+ require_relative "greeve/version"
6
+
7
+ require_relative "greeve/base_item"
8
+ require_relative "greeve/row"
9
+ require_relative "greeve/rowset"
10
+ require_relative "greeve/eve/character_info"
11
+
12
+ # A Ruby wrapper for the EVE Online XML API.
13
+ module Greeve
14
+ # Base URL of the EVE XML API.
15
+ EVE_API_BASE_URL = "https://api.eveonline.com".freeze
16
+
17
+ # API resources.
18
+ module API; end
19
+ # Character resources.
20
+ module Character; end
21
+ # Corporation resources.
22
+ module Corporation; end
23
+ # Eve resources.
24
+ module Eve; end
25
+ # Map resources.
26
+ module Map; end
27
+ # Server resources.
28
+ module Server; end
29
+ end
@@ -0,0 +1,237 @@
1
+ require "bigdecimal"
2
+ require "time"
3
+
4
+ require_relative "rowset"
5
+
6
+ module Greeve
7
+ # An abstract class used to map XML responses from the EVE XML API into Ruby
8
+ # objects. This class is designed to be subclassed by classes representing
9
+ # the specific EVE API resources.
10
+ class BaseItem
11
+ # A DSL method to map an XML attribute to a Ruby object.
12
+ #
13
+ # @param name [Symbol] the Ruby name for this attribute
14
+ #
15
+ # @option opts [Symbol] :xpath the xpath string used to locate the attribute
16
+ # in the XML element
17
+ # @option opts [:integer, :numeric, :string] :type method used to coerce the
18
+ # XML value
19
+ #
20
+ # @example
21
+ # attribute :character_id, xpath: "characterID/?[0]", type: :integer
22
+ def self.attribute(name, opts = {})
23
+ name = name.to_sym
24
+ @attributes ||= {}
25
+
26
+ raise "Attribute `#{name}` defined more than once" if @attributes[name]
27
+ raise "`:xpath` not specified for `#{name}`" unless opts[:xpath]
28
+
29
+ @attributes[name] = {
30
+ xpath: opts[:xpath],
31
+ type: opts[:type],
32
+ }
33
+
34
+ define_method(name) do
35
+ ivar = instance_variable_get(:"@#{name}")
36
+ return ivar unless ivar.nil?
37
+
38
+ value = @xml_element.locate(opts[:xpath]).first
39
+
40
+ unless value.nil?
41
+ value =
42
+ case opts[:type]
43
+ when :integer
44
+ value.to_i
45
+ when :numeric
46
+ BigDecimal.new(value)
47
+ when :string
48
+ value.to_s
49
+ when :datetime
50
+ Time.parse(value + " UTC")
51
+ end
52
+ end
53
+
54
+ instance_variable_set(:"@#{name}", value)
55
+ end
56
+ end
57
+
58
+ # A DSL method to map an XML rowset to a Ruby object.
59
+ #
60
+ # @param name [Symbol] the Ruby name for this attribute
61
+ #
62
+ # @option opts [Symbol] :xpath the xpath string used to locate the attribute
63
+ # in the XML element
64
+ #
65
+ # @example
66
+ # rowset :employment_history, xpath: "eveapi/result/rowset[@name='employmentHistory']" do
67
+ # attribute :record_id, xpath: "@recordID", type: :integer
68
+ # attribute :corporation_id, xpath: "@corporationID", type: :integer
69
+ # attribute :corporation_name, xpath: "@corporationName", type: :string
70
+ # attribute :start_date, xpath: "@startDate", type: :datetime
71
+ # end
72
+ def self.rowset(name, opts = {}, &block)
73
+ name = name.to_sym
74
+ @attributes ||= {}
75
+
76
+ raise "Attribute `#{name}` defined more than once" if @attributes[name]
77
+ raise "`:xpath` not specified for `#{name}`" unless opts[:xpath]
78
+
79
+ @attributes[name] = {
80
+ xpath: opts[:xpath],
81
+ type: :rowset,
82
+ }
83
+
84
+ define_method(name) do
85
+ ivar = instance_variable_get(:"@#{name}")
86
+ return ivar unless ivar.nil?
87
+
88
+ # Since Ox doesn't support the xpath [@k='v'] syntax, parse it out
89
+ # with a regex (captures :path, :attr, :attr_value).
90
+ attr_regex = %r{\A(?<path>.*?)\[@(?<attr>\w+)=['"](?<attr_value>\w+)['"]\]\z}
91
+ match = opts[:xpath].match(attr_regex)
92
+
93
+ rowset_element =
94
+ @xml_element
95
+ .locate(match[:path])
96
+ .find { |e| e.attributes[match[:attr].to_sym] == match[:attr_value] }
97
+
98
+ rowset = Rowset.new(name, rowset_element, &block)
99
+
100
+ instance_variable_set(:"@#{name}", rowset)
101
+ end
102
+ end
103
+
104
+ # A DSL method to specify the API endpoint.
105
+ #
106
+ # @param path [String] path of the API endpoint. Doesn't need to include the
107
+ # leading slash `/`, or the extension `.xml.aspx`
108
+ #
109
+ # @example
110
+ # endpoint "eve/CharacterInfo"
111
+ def self.endpoint(path)
112
+ # Remove leading slash and file extension.
113
+ @endpoint = path.gsub(/\A\/*(.*?)(?:\.xml)?(?:\.aspx)?\z/, '\1')
114
+ end
115
+
116
+ # @abstract Subclass and use the DSL methods to map API endpoints to objects
117
+ #
118
+ # @option opts [String, Fixnum] :key API key
119
+ # @option opts [String] :vcode API vCode
120
+ # @option opts [Hash<String, String>] :query_params a hash of HTTP query
121
+ # params that specify how a value maps to the API request
122
+ #
123
+ # @example
124
+ # super(query_params: {
125
+ # "characterID" => character_id,
126
+ # })
127
+ def initialize(opts = {})
128
+ raise TypeError, "Cannot instantiate an abstract class" \
129
+ if self.class.superclass != Greeve::BaseItem
130
+
131
+ @api_key = opts[:key]
132
+ @api_vcode = opts[:vcode]
133
+ @query_params = opts[:query_params] || {}
134
+
135
+ if @api_key && @api_vcode
136
+ @query_params.merge!({
137
+ "keyID" => @api_key,
138
+ "vCode" => @api_vcode,
139
+ })
140
+ end
141
+
142
+ refresh
143
+ end
144
+
145
+ # Query the API, refreshing this object's data.
146
+ #
147
+ # @return true if the endpoint was fetched (HTTP request sent), false if
148
+ # the cache hasn't expired
149
+ def refresh
150
+ return false unless cache_expired?
151
+
152
+ fetch
153
+ true
154
+ end
155
+
156
+ # @return true if the API cache timer has expired and this object can
157
+ # be refreshed
158
+ def cache_expired?
159
+ !(cached_until && cached_until > Time.now)
160
+ end
161
+
162
+ # @return [Time, nil] time when the cache expires and the resource can be
163
+ # refreshed (sends an HTTP request)
164
+ def cached_until
165
+ return unless @xml_element
166
+
167
+ _cached_until = @xml_element.locate("eveapi/cachedUntil/?[0]").first
168
+ _cached_until ? Time.parse(_cached_until + " UTC") : nil
169
+ end
170
+
171
+ # :nodoc:
172
+ def inspect
173
+ attrs = to_s
174
+
175
+ unless attrs.empty?
176
+ attrs = attrs.split("\n").map { |l| " #{l}" }.join("\n")
177
+ attrs = "\n#{attrs}\n"
178
+ end
179
+
180
+ "#<#{self.class.name}:#{object_id}#{attrs}>"
181
+ end
182
+
183
+ # @return [String] a string representation of the non-nil attributes
184
+ def to_s
185
+ to_h
186
+ .map { |k, v|
187
+ v = v.to_s("F") if v.is_a?(BigDecimal)
188
+ "#{k}: #{v}"
189
+ }
190
+ .join("\n")
191
+ end
192
+
193
+ # @return [Hash] a hash of non-nil attributes
194
+ def to_h
195
+ attributes
196
+ .keys
197
+ .map { |name|
198
+ value = __send__(name)
199
+ value = value.to_a if value.is_a?(Rowset)
200
+
201
+ [name, value]
202
+ }
203
+ .to_h
204
+ .reject { |k, v| v.nil? }
205
+ end
206
+
207
+ private
208
+
209
+ # @return [Hash] the hash of attributes for this object
210
+ def attributes
211
+ self.class.instance_variable_get(:@attributes) || {}
212
+ end
213
+
214
+ # @return [String] the class's endpoint path
215
+ def endpoint
216
+ self.class.instance_variable_get(:@endpoint)
217
+ end
218
+
219
+ # Fetch data from the API HTTP endpoint.
220
+ def fetch
221
+ url = "#{Greeve::EVE_API_BASE_URL}/#{endpoint}.xml.aspx"
222
+ response = Typhoeus.get(url, params: @query_params)
223
+
224
+ # TODO: Use a better error class.
225
+ raise TypeError, "HTTP error #{response.code}" \
226
+ unless (200..299).include?(response.code.to_i)
227
+
228
+ @xml_element = Ox.parse(response.body)
229
+
230
+ attributes.keys.each do |name|
231
+ instance_variable_set(:"@#{name}", nil)
232
+ end
233
+
234
+ @xml_element
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,45 @@
1
+ require_relative "../base_item"
2
+
3
+ module Greeve
4
+ module Eve
5
+ # Public character info.
6
+ class CharacterInfo < Greeve::BaseItem
7
+ endpoint "eve/CharacterInfo"
8
+
9
+ attribute :character_id, xpath: "eveapi/result/characterID/?[0]", type: :integer
10
+ attribute :character_name, xpath: "eveapi/result/characterName/?[0]", type: :string
11
+ attribute :race, xpath: "eveapi/result/race/?[0]", type: :string
12
+ attribute :bloodline_id, xpath: "eveapi/result/bloodlineID/?[0]", type: :integer
13
+ attribute :bloodline, xpath: "eveapi/result/bloodline/?[0]", type: :string
14
+ attribute :ancestry_id, xpath: "eveapi/result/ancestryID/?[0]", type: :integer
15
+ attribute :ancestry, xpath: "eveapi/result/ancestry/?[0]", type: :string
16
+ attribute :account_balance, xpath: "eveapi/result/accountBalance/?[0]", type: :numeric
17
+ attribute :skill_points, xpath: "eveapi/result/skillPoints/?[0]", type: :integer
18
+ attribute :next_training_ends, xpath: "eveapi/result/nextTrainingEnds/?[0]", type: :datetime
19
+ attribute :ship_name, xpath: "eveapi/result/shipName/?[0]", type: :string
20
+ attribute :ship_type_id, xpath: "eveapi/result/shipTypeID/?[0]", type: :integer
21
+ attribute :ship_type_name, xpath: "eveapi/result/shipTypeName/?[0]", type: :string
22
+ attribute :corporation_id, xpath: "eveapi/result/corporationID/?[0]", type: :integer
23
+ attribute :corporation, xpath: "eveapi/result/corporation/?[0]", type: :string
24
+ attribute :corporation_date, xpath: "eveapi/result/corporationDate/?[0]", type: :datetime
25
+ attribute :alliance_id, xpath: "eveapi/result/allianceID/?[0]", type: :integer
26
+ attribute :alliance, xpath: "eveapi/result/alliance/?[0]", type: :string
27
+ attribute :alliance_date, xpath: "eveapi/result/allianceDate/?[0]", type: :datetime
28
+ attribute :last_known_location, xpath: "eveapi/result/lastKnownLocation/?[0]", type: :string
29
+ attribute :security_status, xpath: "eveapi/result/securityStatus/?[0]", type: :numeric
30
+
31
+ rowset :employment_history, xpath: "eveapi/result/rowset[@name='employmentHistory']" do
32
+ attribute :record_id, xpath: "@recordID", type: :integer
33
+ attribute :corporation_id, xpath: "@corporationID", type: :integer
34
+ attribute :corporation_name, xpath: "@corporationName", type: :string
35
+ attribute :start_date, xpath: "@startDate", type: :datetime
36
+ end
37
+
38
+ # @param character_id [Integer] EVE character ID
39
+ def initialize(character_id, opts = {})
40
+ opts[:query_params] = { "characterID" => character_id }
41
+ super(opts)
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/greeve/row.rb ADDED
@@ -0,0 +1,72 @@
1
+ module Greeve
2
+ # Represents an XML `row` element, contained in a {Rowset}.
3
+ class Row
4
+ # @param xml_element [Ox::Element] the xml row element for this item
5
+ # @param attributes [Hash] the hash of attribute definitions for this row
6
+ def initialize(xml_element, attributes)
7
+ @xml_element = xml_element
8
+ @attributes = attributes
9
+
10
+ attributes.each do |name, opts|
11
+ define_singleton_method(name) do
12
+ ivar = instance_variable_get(:"@#{name}")
13
+ return ivar unless ivar.nil?
14
+
15
+ value = @xml_element.locate(opts[:xpath]).first
16
+
17
+ unless value.nil?
18
+ value =
19
+ case opts[:type]
20
+ when :integer
21
+ value.to_i
22
+ when :numeric
23
+ BigDecimal.new(value)
24
+ when :string
25
+ value.to_s
26
+ when :datetime
27
+ Time.parse(value + " UTC")
28
+ end
29
+ end
30
+
31
+ instance_variable_set(:"@#{name}", value)
32
+ end
33
+ end
34
+ end
35
+
36
+ # :nodoc:
37
+ def inspect
38
+ attrs = to_s
39
+
40
+ unless attrs.empty?
41
+ attrs = attrs.split("\n").map { |l| " #{l}" }.join("\n")
42
+ attrs = "\n#{attrs}\n"
43
+ end
44
+
45
+ "#<#{self.class.name}:#{object_id}#{attrs}>"
46
+ end
47
+
48
+ # @return [String] a string representation of the non-nil attributes
49
+ def to_s
50
+ to_h
51
+ .map { |k, v|
52
+ v = v.to_s("F") if v.is_a?(BigDecimal)
53
+ "#{k}: #{v}"
54
+ }
55
+ .join("\n")
56
+ end
57
+
58
+ # @return [Hash] a hash of non-nil attributes
59
+ def to_h
60
+ @attributes
61
+ .keys
62
+ .map { |name|
63
+ value = __send__(name)
64
+ value = value.to_a if value.is_a?(Rowset)
65
+
66
+ [name, value]
67
+ }
68
+ .to_h
69
+ .reject { |k, v| v.nil? }
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,71 @@
1
+ require_relative "row"
2
+
3
+ module Greeve
4
+ # Represents an XML `rowset` element.
5
+ class Rowset
6
+ include Enumerable
7
+
8
+ # @return [Symbol] name of the rowset
9
+ attr_reader :name
10
+
11
+ # @param name [Symbol] name of the rowset
12
+ # @param xml_element [Ox::Element] the xml rowset element for this item
13
+ # @yield a block containing the attribute definitions for {Row}
14
+ def initialize(name, xml_element, &block)
15
+ @name = name
16
+ @xml_element = xml_element
17
+ @attributes = {}
18
+ @rows = nil
19
+
20
+ # Load the attribute configuration in the rowset block.
21
+ instance_eval(&block)
22
+
23
+ # Disable the #attribute method since the attributes have been configured.
24
+ define_singleton_method(:attribute) { raise NoMethodError, "private method" }
25
+ end
26
+
27
+ # :nodoc:
28
+ def each(&block)
29
+ rows.each(&block)
30
+ end
31
+
32
+ # @return [Array] an array of rows and their non-nil attributes
33
+ def to_a
34
+ map(&:to_h)
35
+ end
36
+
37
+ # :nodoc:
38
+ def inspect
39
+ "#<#{self.class.name}:#{object_id} name: #{@name}>"
40
+ end
41
+
42
+ # Map an XML attribute to a Ruby object.
43
+ #
44
+ # @!visibility private
45
+ # @see {Greeve::BaseItem.attribute}
46
+ def attribute(name, opts = {})
47
+ name = name.to_sym
48
+
49
+ raise "Attribute `#{name}` defined more than once" if @attributes[name]
50
+ raise "`:xpath` not specified for `#{name}`" unless opts[:xpath]
51
+
52
+ @attributes[name] = {
53
+ xpath: opts[:xpath],
54
+ type: opts[:type],
55
+ }
56
+ end
57
+
58
+ private
59
+
60
+ # @return [Array<Row>] an array of rows in the rowset
61
+ def rows
62
+ return @rows unless @rows.nil?
63
+
64
+ rows = @xml_element.locate("row").map do |row_element|
65
+ Row.new(row_element, @attributes)
66
+ end
67
+
68
+ @rows = rows
69
+ end
70
+ end
71
+ end