flox 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.
- checksums.yaml +7 -0
- data/bin/flox +102 -0
- data/lib/flox.rb +203 -0
- data/lib/flox/entity.rb +103 -0
- data/lib/flox/player.rb +43 -0
- data/lib/flox/resource_enumerator.rb +43 -0
- data/lib/flox/rest_service.rb +128 -0
- data/lib/flox/score.rb +34 -0
- data/lib/flox/utils.rb +38 -0
- data/lib/flox/version.rb +10 -0
- data/test/test_entity.rb +52 -0
- data/test/test_flox.rb +152 -0
- data/test/test_utils.rb +34 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 219bdba8d257f7a35c1b9174877fc74775ce0a89
|
4
|
+
data.tar.gz: cc1abbb765aa82c4b6c3d650e308869b9594361e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 77c3fe30874b4bd0531108be4965dfa5178813712f319346cd167fa9b41985737904e095375b7b64792a0135275d1231d1817ba16e023d20b9c4906a5c4d4c86
|
7
|
+
data.tar.gz: 01509872cb9f839e79bd2941894d01eee92f5b238404e75aff60f1dba2a4099a0e05e96936840e617a070172ea5e2f0adc993986c95bb03540af14426bc3c9cf
|
data/bin/flox
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
## Author: Daniel Sperl
|
4
|
+
## Copyright: Copyright 2014 Gamua
|
5
|
+
## License: Simplified BSD
|
6
|
+
|
7
|
+
$LOAD_PATH << '../lib'
|
8
|
+
|
9
|
+
require 'flox'
|
10
|
+
require 'fileutils'
|
11
|
+
require 'trollop'
|
12
|
+
|
13
|
+
class Worker
|
14
|
+
|
15
|
+
attr_reader :flox
|
16
|
+
|
17
|
+
def initialize(game_id, game_key, base_url)
|
18
|
+
@flox = Flox.new(game_id, game_key, base_url)
|
19
|
+
end
|
20
|
+
|
21
|
+
def login_with_key(key)
|
22
|
+
flox.login_with_key(key)
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute(command_name, args)
|
26
|
+
begin
|
27
|
+
method_name = command_name.downcase.gsub("-", "_")
|
28
|
+
self.send(method_name, **args)
|
29
|
+
rescue Exception => e
|
30
|
+
log "Error: " + e.to_s
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def download_logs(args={})
|
37
|
+
query = args[:query]
|
38
|
+
limit = args[:limit]
|
39
|
+
destination = args[:destination] || Dir.pwd
|
40
|
+
FileUtils.mkdir_p(destination)
|
41
|
+
|
42
|
+
log "Fetching Logs ..."
|
43
|
+
log_ids = flox.load_log_ids(query, limit)
|
44
|
+
log_ids.each do |log_id|
|
45
|
+
remote_path = "logs/#{log_id}"
|
46
|
+
local_path = File.join(destination, log_id) + ".json"
|
47
|
+
load_and_save_resource(remote_path, local_path)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def status(args={})
|
52
|
+
log "Fetching Server Status ..."
|
53
|
+
status = flox.status
|
54
|
+
log "Version: #{status['version']}, status: #{status['status']}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def log(message)
|
58
|
+
puts message
|
59
|
+
end
|
60
|
+
|
61
|
+
def fail(message)
|
62
|
+
log message
|
63
|
+
exit
|
64
|
+
end
|
65
|
+
|
66
|
+
def load_and_save_resource(remote_path, local_path)
|
67
|
+
if (File.exists? local_path)
|
68
|
+
log("Skipped #{local_path} (file exists)")
|
69
|
+
else
|
70
|
+
resource = flox.load_resource(remote_path)
|
71
|
+
File.write(local_path, JSON.pretty_generate(resource))
|
72
|
+
log("Saved #{local_path}")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
options = Trollop::options do
|
79
|
+
banner "Administrative utility for the Flox.cc Game Backend"
|
80
|
+
opt :key, "The 'Hero' key used for authentication", :type => :string, :required => true
|
81
|
+
opt :game_id, "The ID of the game", :type => :string, :required => true
|
82
|
+
opt :game_key, "The key of the game", :type => :string, :required => true
|
83
|
+
opt :base_url, "The URL of the Flox service", :type => :string
|
84
|
+
opt :destination, "The directory in which to store the logs", :type => :string
|
85
|
+
opt :query, "Narrows down the list of results", :type => :string
|
86
|
+
opt :limit, "Maximum number of logs to download", :type => :int
|
87
|
+
end
|
88
|
+
|
89
|
+
commands = ARGV.clone
|
90
|
+
# puts "Optons: #{options}"
|
91
|
+
|
92
|
+
hero_key = options[:key]
|
93
|
+
game_id = options[:game_id]
|
94
|
+
game_key = options[:game_key]
|
95
|
+
base_url = options[:base_url] || Flox::DEFAULT_URL
|
96
|
+
|
97
|
+
worker = Worker.new(game_id, game_key, base_url)
|
98
|
+
worker.login_with_key(hero_key)
|
99
|
+
|
100
|
+
commands.each do |command|
|
101
|
+
worker.execute(command, options)
|
102
|
+
end
|
data/lib/flox.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
# The main class used to interact with the Flox cloud service. Create an
|
6
|
+
# instance of Flox using the game ID and key acquired from the web interface,
|
7
|
+
# then login with a "Hero" key. That way, you will be able to access the data
|
8
|
+
# of all players.
|
9
|
+
class Flox
|
10
|
+
|
11
|
+
# The URL where the Flox servers are found.
|
12
|
+
DEFAULT_URL = "https://www.flox.cc"
|
13
|
+
|
14
|
+
# @private
|
15
|
+
attr_reader :service
|
16
|
+
|
17
|
+
# The player that is currently logged in.
|
18
|
+
attr_reader :current_player
|
19
|
+
|
20
|
+
# Creates a new instance with a certain game ID and key. Per default, a guest
|
21
|
+
# player will be logged in. You probably need to call 'login_with_key' with a
|
22
|
+
# Hero-key to access your data.
|
23
|
+
def initialize(game_id, game_key, base_url=Flox::DEFAULT_URL)
|
24
|
+
@service = RestService.new(game_id, game_key, base_url)
|
25
|
+
self.login_guest
|
26
|
+
end
|
27
|
+
|
28
|
+
# Makes a key-login on the server. It is recommended to create a 'hero'
|
29
|
+
# player in the web interface and use that for the login.
|
30
|
+
def login_with_key(key)
|
31
|
+
login(:key, key)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Creates a new guest player and logs it in.
|
35
|
+
def login_guest()
|
36
|
+
login(:guest)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Logging out the current player automatically logs in a new guest.
|
40
|
+
alias_method :logout, :login_guest
|
41
|
+
|
42
|
+
# @private
|
43
|
+
def login(auth_type, auth_id=nil, auth_token=nil)
|
44
|
+
data = service.login(auth_type, auth_id, auth_token)
|
45
|
+
@current_player = Player.new(data['id'], data['entity'])
|
46
|
+
end
|
47
|
+
|
48
|
+
# Loads an entity with a certain type and id from the server.
|
49
|
+
# Normally, the type is the class name you used for the entity in your game.
|
50
|
+
# Returns a Flox::Entity instance.
|
51
|
+
def load_entity(type, id)
|
52
|
+
path = entity_path(type, id)
|
53
|
+
data = service.get(path)
|
54
|
+
if (type == '.player')
|
55
|
+
Player.new(id, data)
|
56
|
+
else
|
57
|
+
Entity.new(type, id, data)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Stores an entity on the server.
|
62
|
+
def save_entity(entity)
|
63
|
+
result = service.put(entity.path, entity)
|
64
|
+
entity['updatedAt'] = result['updatedAt']
|
65
|
+
entity['createdAt'] = result['createdAt']
|
66
|
+
entity
|
67
|
+
end
|
68
|
+
|
69
|
+
# :call-seq:
|
70
|
+
# delete_entity(entity)
|
71
|
+
# delete_entity(type, id)
|
72
|
+
#
|
73
|
+
# Deletes the given entity from the database.
|
74
|
+
def delete_entity(*entity)
|
75
|
+
if entity.length > 1
|
76
|
+
type, id = entity[0], entity[1]
|
77
|
+
else
|
78
|
+
type, id = entity[0].type, entity[0].id
|
79
|
+
end
|
80
|
+
service.delete(entity_path(type, id))
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# Posts a score to a certain leaderboard. Beware that only the top score of
|
85
|
+
# a player will appear on the leaderboard.
|
86
|
+
def post_score(leaderboard_id, score, player_name)
|
87
|
+
path = leaderboard_path(leaderboard_id)
|
88
|
+
data = { playerName: player_name, value: score }
|
89
|
+
service.post(path, data)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Loads all scores of a leaderboard, sorted by rank. 'scope' can either be
|
93
|
+
# one of the symbols +:today, :this_week, :all_time+ or an array of player IDs.
|
94
|
+
def load_scores(leaderboard_id, scope)
|
95
|
+
path = leaderboard_path(leaderboard_id)
|
96
|
+
args = {}
|
97
|
+
|
98
|
+
if scope.is_a?(Array)
|
99
|
+
args['p'] = scope
|
100
|
+
else
|
101
|
+
args['t'] = scope.to_s.to_camelcase
|
102
|
+
end
|
103
|
+
|
104
|
+
raw_scores = service.get(path, args)
|
105
|
+
raw_scores.collect { |raw_score| Score.new(raw_score) }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Loads a JSON object from the given path. This works with any resource
|
109
|
+
# e.g. entities, logs, etc. Always returns a Hash.
|
110
|
+
def load_resource(path, args=nil)
|
111
|
+
service.get(path, args)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Loads a log with a certain ID. A log is a Hash instance.
|
115
|
+
def load_log(log_id)
|
116
|
+
log = service.get log_path(log_id)
|
117
|
+
log['id'] = log_id unless log['id']
|
118
|
+
log
|
119
|
+
end
|
120
|
+
|
121
|
+
# Loads logs, optionally restricted by a certain query.
|
122
|
+
# Here are some sample queries:
|
123
|
+
#
|
124
|
+
# * 'day:2014-02-20' -> all logs of a certain day
|
125
|
+
# * 'severity:warning' -> all logs of type warning & error
|
126
|
+
# * 'severity:error' -> all logs of type error
|
127
|
+
# * 'day:2014-02-20 severity:error' all error logs from February 20th.
|
128
|
+
#
|
129
|
+
# Returns a Flox::ResourceEnumerator you can use to iterate
|
130
|
+
# over the logs.
|
131
|
+
def load_logs(query=nil, limit=nil)
|
132
|
+
log_ids = load_log_ids(query, limit)
|
133
|
+
paths = log_ids.map { |log_id| log_path(log_id) }
|
134
|
+
ResourceEnumerator.new(service, paths)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Loads just the IDs of the logs, restricted by a certain query.
|
138
|
+
def load_log_ids(query=nil, limit=nil)
|
139
|
+
log_ids = []
|
140
|
+
cursor = nil
|
141
|
+
begin
|
142
|
+
args = {}
|
143
|
+
args['q'] = query if query
|
144
|
+
args['l'] = limit if limit
|
145
|
+
args['c'] = cursor if cursor
|
146
|
+
|
147
|
+
result = service.get "logs", args
|
148
|
+
cursor = result["cursor"]
|
149
|
+
log_ids += result["ids"]
|
150
|
+
limit -= log_ids.length if limit
|
151
|
+
end while !cursor.nil? and (limit.nil? or limit > 0)
|
152
|
+
log_ids
|
153
|
+
end
|
154
|
+
|
155
|
+
# Loads the status of the Flox service, which is a Hash with the
|
156
|
+
# keys 'status' and 'version'.
|
157
|
+
def status
|
158
|
+
service.get("")
|
159
|
+
end
|
160
|
+
|
161
|
+
# The ID of the game you are accessing.
|
162
|
+
def game_id
|
163
|
+
service.game_id
|
164
|
+
end
|
165
|
+
|
166
|
+
# The key of the game you are accessing.
|
167
|
+
def game_key
|
168
|
+
service.game_key
|
169
|
+
end
|
170
|
+
|
171
|
+
# The base URL of the Flox service.
|
172
|
+
def base_url
|
173
|
+
service.base_url
|
174
|
+
end
|
175
|
+
|
176
|
+
# @return [String] a string representation of the object.
|
177
|
+
def inspect
|
178
|
+
"[Flox game_id: '#{game_id}', base_url: '#{base_url}']"
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def entity_path(type, id)
|
184
|
+
"entities/#{type}/#{id}"
|
185
|
+
end
|
186
|
+
|
187
|
+
def leaderboard_path(leaderboard_id)
|
188
|
+
"leaderboards/#{leaderboard_id}"
|
189
|
+
end
|
190
|
+
|
191
|
+
def log_path(log_id)
|
192
|
+
"logs/#{log_id}"
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
require 'flox/version'
|
198
|
+
require 'flox/utils'
|
199
|
+
require 'flox/rest_service'
|
200
|
+
require 'flox/entity'
|
201
|
+
require 'flox/player'
|
202
|
+
require 'flox/score'
|
203
|
+
require 'flox/resource_enumerator'
|
data/lib/flox/entity.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
# The base class of all objects that can be stored persistently on the Flox
|
8
|
+
# server.
|
9
|
+
#
|
10
|
+
# The class extends `Hash`. Thus, all properties of the Entity can be accessed
|
11
|
+
# as keys of the Entity instance.
|
12
|
+
#
|
13
|
+
# entity['name'] = 'Donald Duck'
|
14
|
+
#
|
15
|
+
# For convenience, the standard entity properties (e.g. `created_at` and
|
16
|
+
# `updated_at`)can be accessed via Ruby attributes.
|
17
|
+
#
|
18
|
+
# entity.public_access = 'rw'
|
19
|
+
#
|
20
|
+
# To load and save an entity, use the respective methods on the Flox class.
|
21
|
+
#
|
22
|
+
# my_entity = flox.load_entity('SaveGame', '12345') # => Flox::Entity
|
23
|
+
# flox.save_entity(my_entity)
|
24
|
+
#
|
25
|
+
class Flox::Entity < Hash
|
26
|
+
|
27
|
+
# @return [String] the primary identifier of the entity.
|
28
|
+
attr_accessor :id
|
29
|
+
|
30
|
+
# @return [String] the type of the entity. Types group entities together on the server.
|
31
|
+
attr_reader :type
|
32
|
+
|
33
|
+
# @param type [String] Typically the class name of the entity (as used in the other SDKs).
|
34
|
+
# @param id [String] The unique identifier of the entity.
|
35
|
+
# @param data [Hash] The initial contents of the entity.
|
36
|
+
def initialize(type, id=nil, data=nil)
|
37
|
+
@type = type
|
38
|
+
@id = id ? id : String.random_uid
|
39
|
+
self['createdAt'] = self['updatedAt'] = Time.now.utc.to_xs_datetime
|
40
|
+
self.public_access = ''
|
41
|
+
self.merge!(data) if data
|
42
|
+
end
|
43
|
+
|
44
|
+
def created_at
|
45
|
+
Time.parse self['createdAt']
|
46
|
+
end
|
47
|
+
|
48
|
+
def updated_at
|
49
|
+
Time.parse self['updatedAt']
|
50
|
+
end
|
51
|
+
|
52
|
+
def public_access
|
53
|
+
self["publicAccess"]
|
54
|
+
end
|
55
|
+
|
56
|
+
def public_access=(access)
|
57
|
+
self["publicAccess"] = access.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def owner_id
|
61
|
+
self["ownerId"]
|
62
|
+
end
|
63
|
+
|
64
|
+
def owner_id=(value)
|
65
|
+
self["ownerId"] = value.to_s
|
66
|
+
end
|
67
|
+
|
68
|
+
def path
|
69
|
+
"entities/#{@type}/#{@id}"
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [String] provides a simple string representation of the Entity.
|
73
|
+
def inspect
|
74
|
+
description = "[#{self.class} #{@id} (#{@type})\n"
|
75
|
+
each_pair do |key, value|
|
76
|
+
description += " #{key}: #{value}\n"
|
77
|
+
end
|
78
|
+
description += "]"
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# documentation hints
|
83
|
+
#
|
84
|
+
|
85
|
+
# @!attribute owner_id
|
86
|
+
# @return [String] the player ID of the owner of the entity
|
87
|
+
# (referencing a Player entitity).
|
88
|
+
|
89
|
+
# @!attribute created_at
|
90
|
+
# @return [Time] the time the entity was created.
|
91
|
+
|
92
|
+
# @!attribute updated_at
|
93
|
+
# @return [Time] the time the entity was last changed on the server.
|
94
|
+
|
95
|
+
# @!attribute public_access
|
96
|
+
# @return [String] the access rights of all players except the owner
|
97
|
+
# (the owner always has unlimited access). Possible values: '', 'r', 'rw'
|
98
|
+
|
99
|
+
# @!attribute [r] path
|
100
|
+
# @return [String] the path to the REST-resource of the entity, relative
|
101
|
+
# to the game's root.
|
102
|
+
|
103
|
+
end
|
data/lib/flox/player.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
# An Entity that contains information about a Flox Player.
|
6
|
+
# Normally, you don't create instances of `Player` yourself. There's always
|
7
|
+
# a player logged in you can access with
|
8
|
+
#
|
9
|
+
# flox.current_player # => Flox::Player
|
10
|
+
#
|
11
|
+
# To log in as a different player, you'll probably want to use a `key`-login.
|
12
|
+
# When you use the Flox Gem as a maintenance tool, create a "Hero" in the
|
13
|
+
# online interface and use its key to login. That way, you have access to
|
14
|
+
# all entities, regardless of their `public_access` values.
|
15
|
+
#
|
16
|
+
# flox.login_with_key 'hero-key' # => Flox::Player
|
17
|
+
#
|
18
|
+
# The Player class itself is just an entity that adds an `auth_type`
|
19
|
+
# property for your convenience.
|
20
|
+
class Flox::Player < Flox::Entity
|
21
|
+
|
22
|
+
# Creates a player with a certain ID and data. The `type` of a player
|
23
|
+
# is always `.player` in Flox.
|
24
|
+
def initialize(id=nil, data=nil)
|
25
|
+
data ||= {}
|
26
|
+
data["authType"] ||= "guest"
|
27
|
+
data["publicAccess"] ||= "r"
|
28
|
+
super(".player", id, data)
|
29
|
+
self.owner_id ||= self.id
|
30
|
+
end
|
31
|
+
|
32
|
+
def auth_type
|
33
|
+
self["authType"].to_sym
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# documentation hints
|
38
|
+
#
|
39
|
+
|
40
|
+
# @!attribute auth_type
|
41
|
+
# @return [String] the type of authentication the player used to log in.
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
# A helper class that stores the paths to a number of REST resources and
|
8
|
+
# supports iterating over those resources, downloading them lazily from the
|
9
|
+
# server.
|
10
|
+
class Flox::ResourceEnumerator
|
11
|
+
|
12
|
+
include Enumerable
|
13
|
+
|
14
|
+
# @param rest_service [RestService]
|
15
|
+
# the service instance used to download the resources.
|
16
|
+
# @param paths [Array<String>]
|
17
|
+
# the URLs to the resources that need to be accessed, relative to
|
18
|
+
# the game's root.
|
19
|
+
def initialize(rest_service, paths)
|
20
|
+
@service = rest_service
|
21
|
+
@paths = paths
|
22
|
+
end
|
23
|
+
|
24
|
+
# Iterates over the resources provided on intialization, loading them
|
25
|
+
# from the server one by one.
|
26
|
+
def each
|
27
|
+
@paths.each do |path|
|
28
|
+
yield @service.get(path)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def length
|
33
|
+
@paths.length
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# documentation hints
|
38
|
+
#
|
39
|
+
|
40
|
+
# @!attribute length
|
41
|
+
# @return [Fixnum] the total number of objects being enumerated.
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'yaml'
|
7
|
+
require 'net/http'
|
8
|
+
|
9
|
+
# A class that makes it easy to communicate with the Flox server via a REST protocol.
|
10
|
+
class Flox::RestService
|
11
|
+
|
12
|
+
# @return [String] the unique identifier of the game.
|
13
|
+
attr_reader :game_id
|
14
|
+
|
15
|
+
# @return [String] the key that identifies the game.
|
16
|
+
attr_reader :game_key
|
17
|
+
|
18
|
+
# @return [String] the URL pointing to the Flox REST API.
|
19
|
+
attr_reader :base_url
|
20
|
+
|
21
|
+
def initialize(game_id, game_key, base_url)
|
22
|
+
@game_id = game_id
|
23
|
+
@game_key = game_key
|
24
|
+
@base_url = base_url
|
25
|
+
@authentication = { "authType" => "guest" }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Makes a `GET` request at the server. The given data-Hash is URI-encoded
|
29
|
+
# and added to the path.
|
30
|
+
# @return the server response.
|
31
|
+
def get(path, data=nil)
|
32
|
+
path = full_path(path)
|
33
|
+
path += "?" + URI.encode_www_form(data) if data
|
34
|
+
request = Net::HTTP::Get.new(path)
|
35
|
+
execute(request)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Makes a `DELETE` request at the server.
|
39
|
+
# @return the server response.
|
40
|
+
def delete(path)
|
41
|
+
request = Net::HTTP::Delete.new(full_path(path))
|
42
|
+
execute(request)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Makes a `POST` request at the server. The given data-Hash is transferred
|
46
|
+
# in the body of the request.
|
47
|
+
# @return the server response.
|
48
|
+
def post(path, data=nil)
|
49
|
+
request = Net::HTTP::Post.new(full_path(path))
|
50
|
+
execute(request, data)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Makes a `PUT` request at the server. The given data-Hash is transferred
|
54
|
+
# in the body of the request.
|
55
|
+
# @return the server response.
|
56
|
+
def put(path, data=nil)
|
57
|
+
request = Net::HTTP::Put.new(full_path(path))
|
58
|
+
execute(request, data)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Makes a login on the server with the given authentication data.
|
62
|
+
# @return the server response.
|
63
|
+
def login(auth_type, auth_id=nil, auth_token=nil)
|
64
|
+
auth_data = {
|
65
|
+
"authType" => auth_type,
|
66
|
+
"authId" => auth_id,
|
67
|
+
"authToken" => auth_token,
|
68
|
+
"id" => auth_id
|
69
|
+
}
|
70
|
+
|
71
|
+
if (auth_type.to_sym == :guest)
|
72
|
+
response = auth_data
|
73
|
+
else
|
74
|
+
response = post("authenticate", auth_data)
|
75
|
+
auth_data["id"] = response["id"]
|
76
|
+
end
|
77
|
+
|
78
|
+
@authentication = auth_data
|
79
|
+
response
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [String] provides a simple string representation of the service.
|
83
|
+
def inspect
|
84
|
+
"[RestService game_id: #{game_id}, base_url: #{base_url}]"
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def execute(request, data=nil)
|
90
|
+
flox_header = {
|
91
|
+
"sdk" => { "type" => "ruby", "version" => Flox::VERSION },
|
92
|
+
"gameKey" => @game_key,
|
93
|
+
"dispatchTime" => Time.now.utc.to_xs_datetime,
|
94
|
+
"player" => @authentication
|
95
|
+
}
|
96
|
+
|
97
|
+
request["Content-Type"] = "application/json"
|
98
|
+
request["X-Flox"] = flox_header.to_json
|
99
|
+
request.body = data.to_json if data
|
100
|
+
|
101
|
+
uri = URI.parse(@base_url)
|
102
|
+
http = Net::HTTP::new(uri.host, uri.port)
|
103
|
+
|
104
|
+
if uri.scheme == "https" # enable SSL/TLS
|
105
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
106
|
+
http.use_ssl = true
|
107
|
+
end
|
108
|
+
|
109
|
+
http.start do |session|
|
110
|
+
response = session.request(request)
|
111
|
+
if (response.is_a? Net::HTTPSuccess)
|
112
|
+
return JSON.parse(response.body || '{}')
|
113
|
+
else
|
114
|
+
message = begin
|
115
|
+
JSON.parse(response.body)['message']
|
116
|
+
rescue
|
117
|
+
response.body
|
118
|
+
end
|
119
|
+
raise message
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def full_path(path)
|
125
|
+
"/api/games/#{@game_id}/#{path}"
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
data/lib/flox/score.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
# Provides information about the value and origin of one posted score entry.
|
6
|
+
class Flox::Score
|
7
|
+
|
8
|
+
# @return [String] the ID of the player who posted the score.
|
9
|
+
# Note that this could be a guest player unknown to the server.
|
10
|
+
attr_reader :player_id
|
11
|
+
|
12
|
+
# @return [String] the name of the player who posted the score.
|
13
|
+
attr_reader :player_name
|
14
|
+
|
15
|
+
# @return [Fixnum] the actual score.
|
16
|
+
attr_reader :value
|
17
|
+
|
18
|
+
# @return [String] the country from which the score originated, in a
|
19
|
+
# two-letter country code.
|
20
|
+
attr_reader :country
|
21
|
+
|
22
|
+
# @return [Time] the date at which the score was posted.
|
23
|
+
attr_reader :created_at
|
24
|
+
|
25
|
+
# @param data [Hash] the contents of the score as given by the Flox server.
|
26
|
+
def initialize(data)
|
27
|
+
@player_id = data['playerId'].to_s
|
28
|
+
@player_name = data['playerName'].to_s
|
29
|
+
@value = data['value'].to_i
|
30
|
+
@country = data['country'].to_s
|
31
|
+
@created_at = Time.parse(data['createdAt'].to_s)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/lib/flox/utils.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
# Flox-extensions to the standard Time class.
|
8
|
+
class Time
|
9
|
+
|
10
|
+
# @return [String] an XS-DateTime representation of the string, like this:
|
11
|
+
# `2014-02-20T20:15:00.123Z`
|
12
|
+
def to_xs_datetime
|
13
|
+
strftime("%Y-%m-%dT%H:%M:%S.%LZ")
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
# Flox-extensions to the standard Time class.
|
19
|
+
class String
|
20
|
+
|
21
|
+
# @return [String] creates a random alphanumeric string with a given length.
|
22
|
+
def self.random_uid(length=16)
|
23
|
+
SecureRandom.base64(length * 2).gsub(/[\+\/]/, '').slice(0, length)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String] converts a `camelCase` string to its `under_score` equivalent.
|
27
|
+
def to_underscore
|
28
|
+
gsub(/(.)([A-Z])/,'\1_\2').downcase
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [String] converts a string that separates its words with space,
|
32
|
+
# underscore or dash into its `camelCase` equivalent.
|
33
|
+
def to_camelcase
|
34
|
+
words = downcase.split(/[_\-\s]/)
|
35
|
+
words.shift + words.map(&:capitalize).join
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/lib/flox/version.rb
ADDED
data/test/test_entity.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
require 'flox'
|
6
|
+
require 'test/unit'
|
7
|
+
|
8
|
+
class EntityTest < Test::Unit::TestCase
|
9
|
+
|
10
|
+
def test_init
|
11
|
+
type = "type"
|
12
|
+
id = "id"
|
13
|
+
data = { value: true }
|
14
|
+
entity = Flox::Entity.new(type, id, data)
|
15
|
+
assert_equal(type, entity.type)
|
16
|
+
assert_equal(id, entity.id)
|
17
|
+
assert_equal(data[:value], entity[:value])
|
18
|
+
assert_not_nil(entity.created_at)
|
19
|
+
assert_not_nil(entity.updated_at)
|
20
|
+
assert_not_nil(entity.public_access)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_init_without_id
|
24
|
+
entity = Flox::Entity.new("type")
|
25
|
+
assert_kind_of(String, entity.id)
|
26
|
+
assert_not_empty(entity.id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_created_at
|
30
|
+
created_at = Time.parse("2014-02-20T11:00:00Z")
|
31
|
+
created_at_string = created_at.to_xs_datetime
|
32
|
+
entity = Flox::Entity.new('type', 'id')
|
33
|
+
entity['createdAt'] = created_at_string
|
34
|
+
assert_equal(created_at, entity.created_at)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_updated_at
|
38
|
+
updated_at = Time.parse("2014-02-20T11:00:00Z")
|
39
|
+
updated_at_string = updated_at.to_xs_datetime
|
40
|
+
entity = Flox::Entity.new('type', 'id')
|
41
|
+
entity['updatedAt'] = updated_at_string
|
42
|
+
assert_equal(updated_at, entity.updated_at)
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_public_access
|
46
|
+
entity = Flox::Entity.new('type', 'id')
|
47
|
+
assert_equal('', entity.public_access)
|
48
|
+
entity.public_access = 'rw'
|
49
|
+
assert_equal('rw', entity.public_access)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
data/test/test_flox.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
require 'flox'
|
6
|
+
require 'test/unit'
|
7
|
+
require 'mocha/test_unit'
|
8
|
+
|
9
|
+
class FloxTest < Test::Unit::TestCase
|
10
|
+
|
11
|
+
GAME_ID = "game_id"
|
12
|
+
GAME_KEY = "game_key"
|
13
|
+
BASE_URL = "http://url.com"
|
14
|
+
|
15
|
+
attr_reader :flox
|
16
|
+
|
17
|
+
def setup
|
18
|
+
@flox = Flox.new(GAME_ID, GAME_KEY, BASE_URL)
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_init
|
22
|
+
assert_equal(GAME_ID, flox.game_id)
|
23
|
+
assert_equal(GAME_KEY, flox.game_key)
|
24
|
+
assert_equal(BASE_URL, flox.base_url)
|
25
|
+
|
26
|
+
assert_kind_of(Flox::Player, flox.current_player)
|
27
|
+
assert_equal(:guest, flox.current_player.auth_type)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_post_score
|
31
|
+
flox.service.expects(:post).once
|
32
|
+
flox.post_score('leaderboard_id', 123, 'player_name')
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_load_scores
|
36
|
+
leaderboard_id = "dummy"
|
37
|
+
path = "leaderboards/#{leaderboard_id}"
|
38
|
+
raw_score = {
|
39
|
+
'value' => 20,
|
40
|
+
'playerName' => 'hugo',
|
41
|
+
'playerId' => '123',
|
42
|
+
'country' => 'at',
|
43
|
+
'createdAt' => '2014-02-24T20:15:00.123Z'
|
44
|
+
}
|
45
|
+
|
46
|
+
# using time scope (t)
|
47
|
+
flox.service.expects(:get).once.with(path, has_key('t')).returns([])
|
48
|
+
scores = flox.load_scores(leaderboard_id, :today)
|
49
|
+
assert_kind_of(Array, scores)
|
50
|
+
assert_equal(0, scores.length)
|
51
|
+
|
52
|
+
# using player scope (p)
|
53
|
+
flox.service.expects(:get).once.with(path, has_key('p')).returns([raw_score])
|
54
|
+
scores = flox.load_scores(leaderboard_id, %w(1, 2, 3))
|
55
|
+
assert_kind_of(Array, scores)
|
56
|
+
assert_equal(1, scores.length)
|
57
|
+
|
58
|
+
highscore = scores.first
|
59
|
+
assert_kind_of(Flox::Score, highscore)
|
60
|
+
assert_equal(raw_score['value'], highscore.value)
|
61
|
+
assert_equal(raw_score['playerName'], highscore.player_name)
|
62
|
+
assert_equal(raw_score['playerId'], highscore.player_id)
|
63
|
+
assert_equal(raw_score['country'], highscore.country)
|
64
|
+
assert_equal(raw_score['createdAt'], highscore.created_at.to_xs_datetime)
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_login_with_key
|
68
|
+
key = "key"
|
69
|
+
result = { 'id' => '123', 'entity' => { 'authType' => 'key' } }
|
70
|
+
flox.service.expects(:login).with(:key, key, nil).once.returns(result)
|
71
|
+
player = flox.login_with_key(key)
|
72
|
+
assert_not_nil(player)
|
73
|
+
assert_equal(:key, player.auth_type)
|
74
|
+
assert_equal(player, flox.current_player)
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_login_guest
|
78
|
+
player = flox.login_guest
|
79
|
+
assert_not_nil(player)
|
80
|
+
assert_equal(:guest, player.auth_type)
|
81
|
+
assert_equal(player, flox.current_player)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_load_entity
|
85
|
+
type = "type"
|
86
|
+
id = "id"
|
87
|
+
path = "entities/#{type}/#{id}"
|
88
|
+
data = { "name" => "Jean-Luc" }
|
89
|
+
|
90
|
+
flox.service.expects(:get).with(path).once.returns(data)
|
91
|
+
entity = flox.load_entity(type, id)
|
92
|
+
|
93
|
+
assert_kind_of(Flox::Entity, entity)
|
94
|
+
assert_equal(id, entity.id)
|
95
|
+
assert_equal(type, entity.type)
|
96
|
+
assert_equal(path, entity.path)
|
97
|
+
assert_equal(data["name"], entity["name"])
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_save_entity
|
101
|
+
data = { "name" => "Jean-Luc" }
|
102
|
+
entity = Flox::Entity.new("type", "id", data)
|
103
|
+
path = "entities/#{entity.type}/#{entity.id}"
|
104
|
+
result = { "createdAt" => "2014-01-01T12:00:00.000Z",
|
105
|
+
"updatedAt" => "2014-02-01T12:00:00.000Z" }
|
106
|
+
|
107
|
+
flox.service.expects(:put).with(path, entity).once.returns(result)
|
108
|
+
flox.save_entity(entity)
|
109
|
+
|
110
|
+
assert_equal(result["createdAt"], entity.created_at.to_xs_datetime)
|
111
|
+
assert_equal(result["updatedAt"], entity.updated_at.to_xs_datetime)
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_delete_entity
|
115
|
+
entity = Flox::Entity.new("type", "id")
|
116
|
+
path = "entities/#{entity.type}/#{entity.id}"
|
117
|
+
flox.service.expects(:delete).with(path)
|
118
|
+
flox.delete_entity(entity)
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_find_logs
|
122
|
+
log_ids = %w{ 0 1 2 }
|
123
|
+
result = { 'ids' => log_ids, 'cursor' => nil }
|
124
|
+
flox.service.expects(:get).at_most_once.returns(result)
|
125
|
+
logs = flox.load_logs(':warning', 50)
|
126
|
+
assert_kind_of(Flox::ResourceEnumerator, logs)
|
127
|
+
assert_equal(log_ids.length, logs.length)
|
128
|
+
flox.service.expects(:get).times(log_ids.length).returns({})
|
129
|
+
logs.each { |log| assert_kind_of(Hash, log) }
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_find_log_ids
|
133
|
+
log_ids = %w{ 0 1 2 3 4 5 6 7 8 9 }
|
134
|
+
log_ids_a = log_ids.slice 0, 5
|
135
|
+
log_ids_b = log_ids.slice 5, 5
|
136
|
+
result_a = { 'ids' => log_ids_a, 'cursor' => 'a' }
|
137
|
+
result_b = { 'ids' => log_ids_b, 'cursor' => nil }
|
138
|
+
|
139
|
+
# without limit
|
140
|
+
flox.service.expects(:get).twice.returns(result_a, result_b)
|
141
|
+
out_log_ids = flox.load_log_ids
|
142
|
+
assert_equal(log_ids.length, out_log_ids.length)
|
143
|
+
|
144
|
+
# with limit
|
145
|
+
limit = 7
|
146
|
+
result_b['ids'] = %w{ 5 6 }
|
147
|
+
flox.service.expects(:get).twice.returns(result_a, result_b)
|
148
|
+
out_log_ids = flox.load_log_ids(nil, limit)
|
149
|
+
assert_equal(limit, out_log_ids.length)
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
data/test/test_utils.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
## Author: Daniel Sperl
|
2
|
+
## Copyright: Copyright 2014 Gamua
|
3
|
+
## License: Simplified BSD
|
4
|
+
|
5
|
+
require 'flox'
|
6
|
+
require 'test/unit'
|
7
|
+
|
8
|
+
class EntityTest < Test::Unit::TestCase
|
9
|
+
|
10
|
+
def test_random_uid
|
11
|
+
length = 16
|
12
|
+
uid = String.random_uid(length)
|
13
|
+
assert_equal(length, uid.length)
|
14
|
+
assert_match(/^[a-zA-Z0-9]+$/, uid)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_xs_datetime
|
18
|
+
time_xs = "2014-02-20T11:00:00.124Z"
|
19
|
+
time = Time.parse(time_xs)
|
20
|
+
assert_equal(time_xs, time.to_xs_datetime)
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_to_camelcase
|
24
|
+
assert_equal("homeSweetHome", "home_sweet_home".to_camelcase)
|
25
|
+
assert_equal("homeSweetHome", "hOME-sWEET-hOME".to_camelcase)
|
26
|
+
assert_equal("homeSweetHome", "Home Sweet Home".to_camelcase)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_to_underscore
|
30
|
+
assert_equal("home_sweet_home", "HomeSweetHome".to_underscore)
|
31
|
+
assert_equal("home_sweet_home", "homeSweetHome".to_underscore)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: flox
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Sperl
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mocha
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: yard
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.8'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.8'
|
55
|
+
description: |
|
56
|
+
Flox is the no-fuzz backend for game developers. The Ruby SDK allows direct
|
57
|
+
interaction with the Flox servers, e.g. to download log files or update
|
58
|
+
specific entities. It can be used from other Ruby scripts or directly with
|
59
|
+
its bundled command-line utility.
|
60
|
+
email: daniel@gamua.com
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- bin/flox
|
66
|
+
- test/test_entity.rb
|
67
|
+
- test/test_flox.rb
|
68
|
+
- test/test_utils.rb
|
69
|
+
- lib/flox/entity.rb
|
70
|
+
- lib/flox/player.rb
|
71
|
+
- lib/flox/resource_enumerator.rb
|
72
|
+
- lib/flox/rest_service.rb
|
73
|
+
- lib/flox/score.rb
|
74
|
+
- lib/flox/utils.rb
|
75
|
+
- lib/flox/version.rb
|
76
|
+
- lib/flox.rb
|
77
|
+
homepage: https://www.flox.cc
|
78
|
+
licenses:
|
79
|
+
- Simplified BSD
|
80
|
+
metadata: {}
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 2.0.3
|
98
|
+
signing_key:
|
99
|
+
specification_version: 4
|
100
|
+
summary: Ruby SDK for the flox.cc game backend
|
101
|
+
test_files: []
|
102
|
+
has_rdoc:
|