myna_eventmachine 1.0.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.
- data/Gemfile +5 -0
- data/Gemfile.lock +19 -0
- data/README.md +16 -0
- data/Rakefile +12 -0
- data/lib/myna/future.rb +90 -0
- data/lib/myna/myna.rb +230 -0
- data/lib/myna/response.rb +141 -0
- data/myna_eventmachine.gemspec +20 -0
- data/test/test_authorized.rb +78 -0
- data/test/test_future.rb +54 -0
- data/test/test_response.rb +101 -0
- data/test/test_suggest.rb +62 -0
- metadata +61 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
addressable (2.2.6)
|
5
|
+
em-http-request (0.3.0)
|
6
|
+
addressable (>= 2.0.0)
|
7
|
+
escape_utils
|
8
|
+
eventmachine (>= 0.12.9)
|
9
|
+
escape_utils (0.2.4)
|
10
|
+
eventmachine (0.12.10)
|
11
|
+
json (1.6.1)
|
12
|
+
|
13
|
+
PLATFORMS
|
14
|
+
ruby
|
15
|
+
|
16
|
+
DEPENDENCIES
|
17
|
+
em-http-request
|
18
|
+
eventmachine
|
19
|
+
json
|
data/README.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Myna Ruby/EventMachine Client
|
2
|
+
|
3
|
+
This is a Ruby client for the v1 Myna API, using EventMachine for asychronous goodness.
|
4
|
+
|
5
|
+
Tested in Ruby 1.9.2
|
6
|
+
|
7
|
+
## Development Notes
|
8
|
+
|
9
|
+
The easiest way to install the dependencies is to install [Bundler](http://gembundler.com/) and then run `bundle install`
|
10
|
+
|
11
|
+
Run `rake test` to run the tests.
|
12
|
+
|
13
|
+
|
14
|
+
## TODO
|
15
|
+
|
16
|
+
Futures need to handle failures, and need to rigourously trap exceptions and propagate them
|
data/Rakefile
ADDED
data/lib/myna/future.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Copyright 2011 by Untyped Ltd. All Rights Reserved\
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# No portion of this Software shall be used in any application which does not
|
14
|
+
# use the ReportGrid platform to provide some subset of its functionality.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'thread'
|
26
|
+
|
27
|
+
|
28
|
+
module Future
|
29
|
+
class Future
|
30
|
+
def initialize()
|
31
|
+
@lock = Mutex.new
|
32
|
+
@signal = ConditionVariable.new
|
33
|
+
@value = nil
|
34
|
+
@delivered = false
|
35
|
+
@listeners = []
|
36
|
+
end
|
37
|
+
|
38
|
+
def deliver(value)
|
39
|
+
@lock.synchronize {
|
40
|
+
if @delivered
|
41
|
+
raise ArgumentError, "This future has already been delivered a value"
|
42
|
+
else
|
43
|
+
@value = value
|
44
|
+
@delivered = true
|
45
|
+
@signal.broadcast
|
46
|
+
end
|
47
|
+
}
|
48
|
+
@listeners.each do |block|
|
49
|
+
block.call(value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def get
|
54
|
+
@lock.synchronize {
|
55
|
+
if !@delivered
|
56
|
+
@signal.wait(@lock)
|
57
|
+
@signal.broadcast
|
58
|
+
end
|
59
|
+
|
60
|
+
@value
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_complete(&block)
|
65
|
+
return_value = false
|
66
|
+
@lock.synchronize {
|
67
|
+
if @delivered
|
68
|
+
return_value = true
|
69
|
+
else
|
70
|
+
@listeners.push(block)
|
71
|
+
end
|
72
|
+
}
|
73
|
+
|
74
|
+
# We can't call the block inside the synchronise block as the
|
75
|
+
# block may itself block on another lock. In this case it won't
|
76
|
+
# exit the synchronise block and we can get deadlock
|
77
|
+
if return_value
|
78
|
+
block.call(@value)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def map(&block)
|
83
|
+
future = Future.new
|
84
|
+
self.on_complete do |value|
|
85
|
+
future.deliver(block.call(value))
|
86
|
+
end
|
87
|
+
future
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/myna/myna.rb
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
# Copyright 2011 by Untyped Ltd. All Rights Reserved\
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# No portion of this Software shall be used in any application which does not
|
14
|
+
# use the ReportGrid platform to provide some subset of its functionality.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'eventmachine'
|
26
|
+
require 'em-http-request'
|
27
|
+
require 'json'
|
28
|
+
require 'myna/future'
|
29
|
+
require 'myna/response'
|
30
|
+
|
31
|
+
module Myna
|
32
|
+
# Run EventMachine, if it is not already running. Returns a future.
|
33
|
+
# Block on the future to wait till EventMachine is up
|
34
|
+
def Myna.run()
|
35
|
+
future = Future::Future.new
|
36
|
+
|
37
|
+
if EventMachine::reactor_running?
|
38
|
+
future.deliver(true)
|
39
|
+
else
|
40
|
+
Thread.new do
|
41
|
+
EventMachine::run {}
|
42
|
+
end
|
43
|
+
|
44
|
+
EventMachine::next_tick do
|
45
|
+
future.deliver(true)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
future
|
50
|
+
end
|
51
|
+
|
52
|
+
def Myna.stop()
|
53
|
+
EventMachine::stop_event_loop
|
54
|
+
end
|
55
|
+
|
56
|
+
module AuthorizedHost
|
57
|
+
SCHEME = "https"
|
58
|
+
HOST = 'api.mynaweb.com'
|
59
|
+
PORT = 443
|
60
|
+
end
|
61
|
+
|
62
|
+
module UnauthorizedHost
|
63
|
+
SCHEME = "http"
|
64
|
+
HOST = 'api.mynaweb.com'
|
65
|
+
PORT = 80
|
66
|
+
end
|
67
|
+
|
68
|
+
class Api
|
69
|
+
def initialize(api)
|
70
|
+
@api = api
|
71
|
+
end
|
72
|
+
|
73
|
+
def make_request(uri)
|
74
|
+
# puts "Contacting "+uri
|
75
|
+
future = Future::Future.new
|
76
|
+
|
77
|
+
EventMachine::schedule do
|
78
|
+
client = EventMachine::HttpRequest.new(uri)
|
79
|
+
|
80
|
+
http = yield(client)
|
81
|
+
http.errback { future.deliver(Response::NetworkError.new) }
|
82
|
+
http.callback { future.deliver(Response.parse(http.response)) }
|
83
|
+
end
|
84
|
+
|
85
|
+
future
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_uri(path)
|
89
|
+
"#{@api::SCHEME}://#{@api::HOST}:#{@api::PORT}#{path}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def create
|
93
|
+
build_uri("/v1/experiment/new")
|
94
|
+
end
|
95
|
+
|
96
|
+
def suggest(uuid)
|
97
|
+
build_uri("/v1/experiment/#{uuid}/suggest")
|
98
|
+
end
|
99
|
+
|
100
|
+
def reward(uuid)
|
101
|
+
build_uri("/v1/experiment/#{uuid}/reward")
|
102
|
+
end
|
103
|
+
|
104
|
+
def delete(uuid)
|
105
|
+
build_uri("/v1/experiment/#{uuid}/delete")
|
106
|
+
end
|
107
|
+
|
108
|
+
def info(uuid)
|
109
|
+
build_uri("/v1/experiment/#{uuid}/info")
|
110
|
+
end
|
111
|
+
|
112
|
+
def create_variant(uuid)
|
113
|
+
build_uri("/v1/experiment/#{uuid}/new-variant")
|
114
|
+
end
|
115
|
+
|
116
|
+
def delete_variant(uuid)
|
117
|
+
build_uri("/v1/experiment/#{uuid}/delete-variant")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# The simple API for unauthorized access
|
122
|
+
class Experiment
|
123
|
+
def initialize(uuid, host = UnauthorizedHost)
|
124
|
+
@uuid = uuid
|
125
|
+
@host = host
|
126
|
+
@api = Api.new(host)
|
127
|
+
end
|
128
|
+
|
129
|
+
attr_reader :uuid
|
130
|
+
|
131
|
+
def suggest()
|
132
|
+
@api.make_request(@api.suggest(@uuid)) do |client|
|
133
|
+
client.get(:head => {'Accept' => 'application/json'})
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def reward(token, amount = 1.0)
|
138
|
+
@api.make_request(@api.reward(@uuid)) do |client|
|
139
|
+
client.post(:head => {'Accept' => 'application/json'},
|
140
|
+
:body => {:token => token, :amount => amount}.to_json)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def Myna.authorize(email, password, host = AuthorizedHost)
|
146
|
+
ExperimentFactory.new(email, password, host)
|
147
|
+
end
|
148
|
+
|
149
|
+
# The more complex API for authorized access
|
150
|
+
class ExperimentFactory
|
151
|
+
def initialize(email, password, host = AuthorizedHost)
|
152
|
+
@email = email
|
153
|
+
@password = password
|
154
|
+
@host = host
|
155
|
+
@api = Api.new(host)
|
156
|
+
end
|
157
|
+
|
158
|
+
def create(name)
|
159
|
+
future =
|
160
|
+
@api.make_request(@api.create()) do |client|
|
161
|
+
client.post(:head => {
|
162
|
+
'Accept' => 'application/json',
|
163
|
+
'Authorization' => [@email, @password]
|
164
|
+
},
|
165
|
+
:body => {:experiment => name}.to_json)
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
future.map do |response|
|
170
|
+
if response.kind_of? Response::Uuid
|
171
|
+
AuthorizedExperiment.new(response.uuid, @email, @password, @host)
|
172
|
+
else
|
173
|
+
response
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def experiment(uuid)
|
179
|
+
AuthorizedExperiment.new(uuid, @email, @password, @host)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class AuthorizedExperiment < Experiment
|
184
|
+
def initialize(uuid, email, password, host)
|
185
|
+
super(uuid, host)
|
186
|
+
@email = email
|
187
|
+
@password = password
|
188
|
+
end
|
189
|
+
|
190
|
+
def delete
|
191
|
+
@api.make_request(@api.delete(@uuid)) do |client|
|
192
|
+
client.get(:head => {
|
193
|
+
'Accept' => 'application/json',
|
194
|
+
'Authorization' => [@email, @password]
|
195
|
+
})
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def info
|
200
|
+
@api.make_request(@api.info(@uuid)) do |client|
|
201
|
+
client.get(:head => {
|
202
|
+
'Accept' => 'application/json',
|
203
|
+
'Authorization' => [@email, @password]
|
204
|
+
})
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def create_variant(name)
|
209
|
+
@api.make_request(@api.create_variant(@uuid)) do |client|
|
210
|
+
client.post(:head => {
|
211
|
+
'Accept' => 'application/json',
|
212
|
+
'Authorization' => [@email, @password]
|
213
|
+
},
|
214
|
+
:body => {:variant => name}.to_json)
|
215
|
+
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def delete_variant(name)
|
220
|
+
@api.make_request(@api.delete_variant(@uuid)) do |client|
|
221
|
+
client.post(:head => {
|
222
|
+
'Accept' => 'application/json',
|
223
|
+
'Authorization' => [@email, @password]
|
224
|
+
},
|
225
|
+
:body => {:variant => name}.to_json)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# Copyright 2011 by Untyped Ltd. All Rights Reserved\
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# No portion of this Software shall be used in any application which does not
|
14
|
+
# use the ReportGrid platform to provide some subset of its functionality.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'json'
|
26
|
+
require 'singleton'
|
27
|
+
|
28
|
+
module Response
|
29
|
+
# String -> Response
|
30
|
+
#
|
31
|
+
# Parse a JSON string into a Response object
|
32
|
+
def Response.parse(str)
|
33
|
+
json = JSON.parse str
|
34
|
+
case json['typename']
|
35
|
+
when 'problem'
|
36
|
+
ApiError.from_json(json)
|
37
|
+
when 'ok'
|
38
|
+
Ok.instance
|
39
|
+
when 'suggestion'
|
40
|
+
Suggestion.from_json(json)
|
41
|
+
when 'uuid'
|
42
|
+
Uuid.from_json(json)
|
43
|
+
when 'experiment'
|
44
|
+
Experiment.from_json(json)
|
45
|
+
else
|
46
|
+
raise ArgumentError, "Typename "+json['typename']+" is not a recognized response.", caller
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
class Response
|
52
|
+
end
|
53
|
+
|
54
|
+
class MynaError < Response
|
55
|
+
end
|
56
|
+
|
57
|
+
class NetworkError < MynaError
|
58
|
+
end
|
59
|
+
|
60
|
+
class ApiError < MynaError
|
61
|
+
def self.from_json(json)
|
62
|
+
error = ApiError.new(json['subtype'], json['messages'])
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_reader :subtype
|
66
|
+
attr_reader :messages
|
67
|
+
|
68
|
+
def initialize(subtype, messages)
|
69
|
+
@subtype = subtype
|
70
|
+
@messages = messages
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Singleton
|
75
|
+
class Ok < Response
|
76
|
+
include Singleton
|
77
|
+
end
|
78
|
+
|
79
|
+
class Suggestion < Response
|
80
|
+
def self.from_json(json)
|
81
|
+
suggestion = Suggestion.new(json['token'], json['choice'])
|
82
|
+
end
|
83
|
+
|
84
|
+
attr_reader :token
|
85
|
+
attr_reader :choice
|
86
|
+
|
87
|
+
def initialize(token, choice)
|
88
|
+
@token = token
|
89
|
+
@choice = choice
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Uuid < Response
|
94
|
+
def self.from_json(json)
|
95
|
+
uuid = Uuid.new(json['uuid'])
|
96
|
+
end
|
97
|
+
|
98
|
+
attr_reader :uuid
|
99
|
+
|
100
|
+
def initialize(uuid)
|
101
|
+
@uuid = uuid
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Experiment < Response
|
106
|
+
def self.from_json(json)
|
107
|
+
variants = json['variants'].map { |variant| Variant.from_json(variant) }
|
108
|
+
experiment = Experiment.new(json['name'], json['uuid'], json['accountId'], variants)
|
109
|
+
end
|
110
|
+
|
111
|
+
attr_reader :name
|
112
|
+
attr_reader :uuid
|
113
|
+
attr_reader :accountId
|
114
|
+
attr_reader :variants
|
115
|
+
|
116
|
+
def initialize(name, uuid, accountId, variants)
|
117
|
+
@name = name
|
118
|
+
@uuid = uuid
|
119
|
+
@accountId = accountId
|
120
|
+
@variants = variants
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class Variant < Response
|
125
|
+
def self.from_json(json)
|
126
|
+
variant = Variant.new(json['name'], json['views'], json['totalReward'], json['confidenceBound'])
|
127
|
+
end
|
128
|
+
|
129
|
+
attr_reader :name
|
130
|
+
attr_reader :views
|
131
|
+
attr_reader :totalReward
|
132
|
+
attr_reader :confidenceBound
|
133
|
+
|
134
|
+
def initialize(name, views, totalReward, confidenceBound)
|
135
|
+
@name = name
|
136
|
+
@views = views
|
137
|
+
@totalReward = totalReward
|
138
|
+
@confidenceBound = confidenceBound
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "myna/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'myna_eventmachine'
|
7
|
+
s.version = Myna::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.date = '2012-05-11'
|
10
|
+
s.summary = 'Ruby/EventMachine client library for Myna (http://www.mynaweb.com)'
|
11
|
+
s.authors = ["Noel Welsh"]
|
12
|
+
s.description = 'An EventMachine based client for '
|
13
|
+
s.email = ["noel [at] untyped [dot] com"]
|
14
|
+
s.files = ["lib/myna.rb"]
|
15
|
+
s.homepage = 'http://www.mynaweb.com/'
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# Copyright 2011 by Untyped Ltd. All Rights Reserved\
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# No portion of this Software shall be used in any application which does not
|
14
|
+
# use the ReportGrid platform to provide some subset of its functionality.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'minitest/autorun'
|
26
|
+
require 'myna'
|
27
|
+
|
28
|
+
describe Myna do
|
29
|
+
|
30
|
+
before do
|
31
|
+
Myna.run.get
|
32
|
+
end
|
33
|
+
|
34
|
+
module TestHost
|
35
|
+
SCHEME = "http"
|
36
|
+
HOST = 'localhost'
|
37
|
+
PORT = 8080
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "create and delete" do
|
41
|
+
it "must return an Experiment and delete same experiment" do
|
42
|
+
myna = Myna.authorize("test@example.com", "themostsecretpassword")
|
43
|
+
|
44
|
+
expt = myna.create("dummy experimento").get
|
45
|
+
expt.must_be_kind_of Myna::AuthorizedExperiment
|
46
|
+
|
47
|
+
expt.delete().get.must_be_kind_of Response::Ok
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "create_variant and delete_variant" do
|
52
|
+
it "must create and delete variants" do
|
53
|
+
myna = Myna.authorize("test@example.com", "themostsecretpassword")
|
54
|
+
|
55
|
+
expt = myna.create("dummy experimento").get
|
56
|
+
|
57
|
+
expt.create_variant("An awesome variant").get.must_be_kind_of Response::Ok
|
58
|
+
expt.delete_variant("An awesome variant").get.must_be_kind_of Response::Ok
|
59
|
+
|
60
|
+
expt.delete().get.must_be_kind_of Response::Ok
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "info" do
|
65
|
+
it "must return the expected info" do
|
66
|
+
myna = Myna.authorize("test@example.com", "themostsecretpassword")
|
67
|
+
|
68
|
+
info = myna.experiment('45923780-80ed-47c6-aa46-15e2ae7a0e8c').info().get
|
69
|
+
|
70
|
+
info.must_be_kind_of Response::Experiment
|
71
|
+
info.name.must_equal "test"
|
72
|
+
info.uuid.must_equal '45923780-80ed-47c6-aa46-15e2ae7a0e8c'
|
73
|
+
info.variants.length.must_equal 2
|
74
|
+
info.variants[0].name.must_equal 'variant1'
|
75
|
+
info.variants[1].name.must_equal 'variant2'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/test/test_future.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# Copyright 2011 by Untyped Ltd. All Rights Reserved\
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# No portion of this Software shall be used in any application which does not
|
14
|
+
# use the ReportGrid platform to provide some subset of its functionality.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'minitest/autorun'
|
26
|
+
require 'myna/future'
|
27
|
+
|
28
|
+
describe Future do
|
29
|
+
describe "Future.map" do
|
30
|
+
it "must run the given block and complete the returned future" do
|
31
|
+
f1 = Future::Future.new
|
32
|
+
f2 = f1.map do |v|
|
33
|
+
v + 3
|
34
|
+
end
|
35
|
+
|
36
|
+
f1.deliver(1)
|
37
|
+
f2.get.must_equal 4
|
38
|
+
end
|
39
|
+
|
40
|
+
it "must run the given block and complete the returned future from another thread" do
|
41
|
+
f1 = Future::Future.new
|
42
|
+
f2 = f1.map do |v|
|
43
|
+
v + 3
|
44
|
+
end
|
45
|
+
|
46
|
+
Thread.new do
|
47
|
+
sleep(1)
|
48
|
+
f1.deliver(1)
|
49
|
+
end
|
50
|
+
|
51
|
+
f2.get.must_equal 4
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# Copyright 2011 by Untyped Ltd. All Rights Reserved\
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# No portion of this Software shall be used in any application which does not
|
14
|
+
# use the ReportGrid platform to provide some subset of its functionality.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'minitest/autorun'
|
26
|
+
require 'myna/response'
|
27
|
+
|
28
|
+
describe Response do
|
29
|
+
|
30
|
+
describe "parse" do
|
31
|
+
it "must parse an error" do
|
32
|
+
txt = '{"typename":"problem","subtype":400,"messages":{"typename":"message", "info":"Experiment cannot be found"}}'
|
33
|
+
msg = {"typename" => "message", "info" => "Experiment cannot be found"}
|
34
|
+
ans = Response.parse(txt)
|
35
|
+
|
36
|
+
|
37
|
+
ans.must_be_kind_of Response::ApiError
|
38
|
+
ans.subtype.must_equal 400
|
39
|
+
ans.messages.must_equal msg
|
40
|
+
end
|
41
|
+
|
42
|
+
it "must parse ok" do
|
43
|
+
ans = Response.parse('{"typename":"ok"}')
|
44
|
+
|
45
|
+
ans.must_be_kind_of Response::Ok
|
46
|
+
end
|
47
|
+
|
48
|
+
it "must parse a suggestion" do
|
49
|
+
txt = '{"typename":"suggestion","token":"871ec6f8-bea7-4a51-ba48-33bdfea9feb2","choice":"variant2"}'
|
50
|
+
ans = Response.parse(txt)
|
51
|
+
|
52
|
+
ans.must_be_kind_of Response::Suggestion
|
53
|
+
ans.token.must_equal '871ec6f8-bea7-4a51-ba48-33bdfea9feb2'
|
54
|
+
ans.choice.must_equal 'variant2'
|
55
|
+
end
|
56
|
+
|
57
|
+
it "must parse a uuid" do
|
58
|
+
ans = Response.parse('{"typename":"uuid","uuid":"f8d9eb79-3d8f-41b0-9044-605faddd959c"}')
|
59
|
+
|
60
|
+
ans.must_be_kind_of Response::Uuid
|
61
|
+
ans.uuid.must_equal 'f8d9eb79-3d8f-41b0-9044-605faddd959c'
|
62
|
+
end
|
63
|
+
|
64
|
+
it "must parse an experiment" do
|
65
|
+
txt = '{"typename":"experiment",
|
66
|
+
"name":"test",
|
67
|
+
"uuid":"45923780-80ed-47c6-aa46-15e2ae7a0e8c",
|
68
|
+
"accountId":"eb1fb383-4172-422a-b680-8031cf26a23e",
|
69
|
+
"variants":[
|
70
|
+
{"typename":"variant",
|
71
|
+
"name":"variant1",
|
72
|
+
"views":36,
|
73
|
+
"totalReward":1.0,
|
74
|
+
"confidenceBound":0.252904521564191},
|
75
|
+
{"typename":"variant",
|
76
|
+
"name":"variant2",
|
77
|
+
"views":9672,
|
78
|
+
"totalReward":0.0,
|
79
|
+
"confidenceBound":0.015429423531830728}]}'
|
80
|
+
ans = Response.parse(txt)
|
81
|
+
|
82
|
+
ans.must_be_kind_of Response::Experiment
|
83
|
+
ans.name.must_equal "test"
|
84
|
+
ans.uuid.must_equal "45923780-80ed-47c6-aa46-15e2ae7a0e8c"
|
85
|
+
ans.accountId.must_equal "eb1fb383-4172-422a-b680-8031cf26a23e"
|
86
|
+
|
87
|
+
v0 = ans.variants[0]
|
88
|
+
v1 = ans.variants[1]
|
89
|
+
|
90
|
+
v0.name.must_equal "variant1"
|
91
|
+
v0.views.must_equal 36
|
92
|
+
v0.totalReward.must_equal 1.0
|
93
|
+
v0.confidenceBound.must_equal 0.252904521564191
|
94
|
+
|
95
|
+
v1.name.must_equal "variant2"
|
96
|
+
v1.views.must_equal 9672
|
97
|
+
v1.totalReward.must_equal 0.0
|
98
|
+
v1.confidenceBound.must_equal 0.015429423531830728
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Copyright 2011 by Untyped Ltd. All Rights Reserved\
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# No portion of this Software shall be used in any application which does not
|
14
|
+
# use the ReportGrid platform to provide some subset of its functionality.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'minitest/autorun'
|
26
|
+
require 'myna'
|
27
|
+
|
28
|
+
describe Myna do
|
29
|
+
|
30
|
+
before do
|
31
|
+
Myna.run.get
|
32
|
+
end
|
33
|
+
|
34
|
+
after do
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "suggest" do
|
38
|
+
it "must return a suggestion for a valid experiment" do
|
39
|
+
expt = Myna::Experiment.new('45923780-80ed-47c6-aa46-15e2ae7a0e8c')
|
40
|
+
ans = expt.suggest
|
41
|
+
ans.get.must_be_kind_of Response::Suggestion
|
42
|
+
end
|
43
|
+
|
44
|
+
it "must return an error for an invalid experiment" do
|
45
|
+
expt = Myna::Experiment.new('bogus')
|
46
|
+
ans = expt.suggest
|
47
|
+
ans.get.must_be_kind_of Response::ApiError
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "reward" do
|
52
|
+
it "must succeed when given correct token and amount" do
|
53
|
+
expt = Myna::Experiment.new('45923780-80ed-47c6-aa46-15e2ae7a0e8c')
|
54
|
+
suggestion = expt.suggest.get
|
55
|
+
|
56
|
+
ans = expt.reward(suggestion.token, 0.5)
|
57
|
+
ans.get
|
58
|
+
ans.get.must_be_kind_of Response::Ok
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: myna_eventmachine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Noel Welsh
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-11 00:00:00.000000000Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! 'An EventMachine based client for '
|
15
|
+
email:
|
16
|
+
- noel [at] untyped [dot] com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- Gemfile
|
22
|
+
- Gemfile.lock
|
23
|
+
- README.md
|
24
|
+
- Rakefile
|
25
|
+
- lib/myna/future.rb
|
26
|
+
- lib/myna/myna.rb
|
27
|
+
- lib/myna/response.rb
|
28
|
+
- myna_eventmachine.gemspec
|
29
|
+
- test/test_authorized.rb
|
30
|
+
- test/test_future.rb
|
31
|
+
- test/test_response.rb
|
32
|
+
- test/test_suggest.rb
|
33
|
+
homepage: http://www.mynaweb.com/
|
34
|
+
licenses: []
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
none: false
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 1.8.10
|
54
|
+
signing_key:
|
55
|
+
specification_version: 3
|
56
|
+
summary: Ruby/EventMachine client library for Myna (http://www.mynaweb.com)
|
57
|
+
test_files:
|
58
|
+
- test/test_authorized.rb
|
59
|
+
- test/test_future.rb
|
60
|
+
- test/test_response.rb
|
61
|
+
- test/test_suggest.rb
|