eval_in 0.1.6 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ccfaf7503c99eaaece5c964e2dcbe82271ed1eb
4
- data.tar.gz: 87787a87e7f4540c0c021219a6959efb6f2cc247
3
+ metadata.gz: 9a9e273c4e6a93761fc1f35b47e35242397e8559
4
+ data.tar.gz: d78b600e459c3adece521d7c2540209e6c138955
5
5
  SHA512:
6
- metadata.gz: 3fa266438bdf528ec9d025bb63a858beb5aeea37e215d5ba0ace45badd654dd7eb88049c74af616104f58857c6dcb85c910d2386ce817816d041f19d7ebe93d5
7
- data.tar.gz: 350cc67567a1f13b103641cdb7517d2cc802b3e797167a34a622bd8447a4b87f2306d1f92093d3e3664731b57df75a692c963c12e7187d4f5d7fa8ea3ae0be27
6
+ metadata.gz: 3d6b167a6a021c1208dfac00a9de3078f90ee026bdf51198323afd14f2c2429ae00d49b63556ae1f092261a815852ac36c73bae57828d78bb8ab652fac62693e
7
+ data.tar.gz: 558dd782b6838e5c74945636bc9721cc684fd9ab3db11613de40f394eee39a6b888269a0c6bb40eb715f4547ebce905243c5dc25c2734fc9ab98369d6cd99ffb
data/Readme.md CHANGED
@@ -153,6 +153,138 @@ What languages can this run?
153
153
  </tr>
154
154
  </table>
155
155
 
156
+
157
+ Mocking for non-prod environments
158
+ ---------------------------------
159
+
160
+ ### Wiring the mock in
161
+
162
+ The mock that is provided will need to be set into place.
163
+ If the code is directly doing `EvalIn.call(...)`, then it is
164
+ a hard dependency, and there is no ablity to set the mock into place.
165
+
166
+ You will need to structure your code such that it receives the `eval_in`
167
+ service as an argument, or looks it up in some configuration or something
168
+ (I strongly prefer the former).
169
+ This will allow your test environment to give it a mock that tests that it
170
+ works in all the edge cases, or is just generally benign (doesn't make real http request in tests),
171
+ your dev environment to give it an `EvalIn` that looks so close to the real one you wouldn't even know,
172
+ and your prod environment to give it the actual `EvalIn` that actually uses the service.
173
+
174
+ If this is still unclear, here is an example:
175
+
176
+ ```ruby
177
+ # == before: hard dependency prevents you from giving a mock ==
178
+ def get_output(code)
179
+ EvalIn.call(code, language: 'ruby/mri-2.1').output
180
+ end
181
+
182
+ # == after: receive the thing you're talking to ==
183
+ def get_output(eval_in, code)
184
+ eval_in.call(code, language: 'ruby/mri-2.1').output
185
+ end
186
+
187
+ # in test, you call like this
188
+ output = 'the output'
189
+ assert_equal output, get_output(EvalIn::Mock.new(result: EvalIn::Result.new(output: output)), 'some code')
190
+
191
+ # in prod you call like this:
192
+ get_output(EvalIn, params[:code_sample])
193
+
194
+ # in dev you call like this:
195
+ get_output(EvalIn::Mock.new(languages: {'ruby/mri-2.1' => {program: RbConfig.ruby, args: []}}),
196
+ params[:code_sample])
197
+
198
+ # Now those last_two are probably in the same controller, right?
199
+ # How do you get it to do the right thing in the right env, then?
200
+ # well, you pass it to your controller, the same way you passed the `get_output`
201
+ # for example, you might do this in a rack middleware,
202
+ # then have it get the service from the `env` hash
203
+ # or set it in an environment initializer
204
+ ```
205
+
206
+ ### The provided mock
207
+
208
+ For a test or dev env where you don't care about correctness,
209
+ just that it does something that looks real,
210
+ you can make a mock that has a `Result`,
211
+ instantiated with any values you care about.
212
+
213
+ ```ruby
214
+ require 'eval_in/mock'
215
+ eval_in = EvalIn::Mock.new(result: EvalIn::Result.new(code: 'injected code', output: 'the output'))
216
+
217
+ eval_in.call('overridden code', language: 'irrelevant')
218
+ # => #<EvalIn::Result:0x007fb503a7a5e8
219
+ # @code="injected code",
220
+ # @exitstatus=-1,
221
+ # @language="",
222
+ # @language_friendly="",
223
+ # @output="the output",
224
+ # @status="",
225
+ # @url="">
226
+ ```
227
+
228
+ If you want your environment to behave approximately like the real `eval_in`,
229
+ you can instantiate a mock that knows how to evaluate code locally.
230
+ This is necessary, because it doesn't know how to execute these languages
231
+ (eval.in does that, it just knows how to talk to eval.in).
232
+ So you must provide it with a list of languages and how to execute them
233
+
234
+ This is probably idea for a dev environment, the results will be the most realistic.
235
+
236
+ **NOTE THAT THIS DOES NOT WORK ON JRUBY**
237
+
238
+ ```ruby
239
+ require 'eval_in/mock'
240
+
241
+ # a mock that can execute Ruby code and C code
242
+ eval_in = EvalIn::Mock.new(languages: {
243
+ 'ruby/mri-2.1' => {program: RbConfig.ruby,
244
+ args: []
245
+ },
246
+ 'c/gcc-4.9.1' => {program: RbConfig.ruby,
247
+ args: ['-e',
248
+ 'system "gcc -x c -o /tmp/eval_in_c_example #{ARGV.first}"
249
+ exec "/tmp/eval_in_c_example"']
250
+ },
251
+ })
252
+
253
+ eval_in.call 'puts "hello from ruby!"; exit 123', language: 'ruby/mri-2.1'
254
+ # => #<EvalIn::Result:0x007fb503a7d518
255
+ # @code="puts \"hello from ruby!\"; exit 123",
256
+ # @exitstatus=123,
257
+ # @language="ruby/mri-2.1",
258
+ # @language_friendly="ruby/mri-2.1",
259
+ # @output="hello from ruby!\n",
260
+ # @status="OK (0.072 sec real, 0.085 sec wall, 8 MB, 19 syscalls)",
261
+ # @url="https://eval.in/207744.json">
262
+
263
+ eval_in.call '#include <stdio.h>
264
+ int main() {
265
+ puts("hello from c!");
266
+ }', language: 'c/gcc-4.9.1'
267
+ # => #<EvalIn::Result:0x007fb503a850b0
268
+ # @code="#include <stdio.h>\nint main() {\n puts(\"hello from c!\");\n}",
269
+ # @exitstatus=0,
270
+ # @language="c/gcc-4.9.1",
271
+ # @language_friendly="c/gcc-4.9.1",
272
+ # @output="hello from c!\n",
273
+ # @status="OK (0.072 sec real, 0.085 sec wall, 8 MB, 19 syscalls)",
274
+ # @url="https://eval.in/207744.json">
275
+ ```
276
+
277
+ You can also provide a callback that will be invoked to handle the request.
278
+ This is probably ideal for testing more nuanced edge cases that the mock
279
+ doesn't inherently provide the ability to do.
280
+
281
+ ```ruby
282
+ require 'eval_in/mock'
283
+ eval_in = EvalIn::Mock.new on_call: -> code, options { raise EvalIn::RequestError, 'does my code do the right thing in the event of an exception?' }
284
+
285
+ eval_in.call('code', language: 'any') rescue $! # => #<EvalIn::RequestError: does my code do the right thing in the event of an exception?>
286
+ ```
287
+
156
288
  Attribution
157
289
  -----------
158
290
 
@@ -1,6 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  $:.push File.expand_path("../lib", __FILE__)
3
- require "eval_in/version"
3
+ require "eval_in/constants"
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "eval_in"
@@ -1,80 +1,7 @@
1
- # encoding: utf-8
2
-
3
- require 'uri'
4
- require 'json'
5
- require 'net/http'
6
- require 'pathname'
7
- require 'eval_in/version'
1
+ require 'eval_in/http'
2
+ require 'eval_in/client'
8
3
 
9
4
  module EvalIn
10
- EvalInError = Class.new StandardError
11
- RequestError = Class.new EvalInError
12
- ResultNotFound = Class.new EvalInError
13
-
14
- # @example Generated with
15
- # nokogiri https://eval.in -e 'puts $_.xpath("//option/@value")'
16
- KNOWN_LANGUAGES = %w[
17
- c/gcc-4.4.3
18
- c/gcc-4.9.1
19
- c++/c++11-gcc-4.9.1
20
- c++/gcc-4.4.3
21
- c++/gcc-4.9.1
22
- coffeescript/node-0.10.29-coffee-1.7.1
23
- fortran/f95-4.4.3
24
- haskell/hugs98-sep-2006
25
- io/io-20131204
26
- javascript/node-0.10.29
27
- lua/lua-5.1.5
28
- lua/lua-5.2.3
29
- ocaml/ocaml-4.01.0
30
- php/php-5.5.14
31
- pascal/fpc-2.6.4
32
- perl/perl-5.20.0
33
- python/cpython-2.7.8
34
- python/cpython-3.4.1
35
- ruby/mri-1.0
36
- ruby/mri-1.8.7
37
- ruby/mri-1.9.3
38
- ruby/mri-2.0.0
39
- ruby/mri-2.1
40
- slash/slash-head
41
- assembly/nasm-2.07
42
- ]
43
-
44
- # The data structure containing the final result
45
- # its attributes default to null-objects for their given type
46
- class Result
47
- @attribute_names = [:exitstatus, :language, :language_friendly, :code, :output, :status, :url].freeze
48
- attr_accessor *@attribute_names
49
- class << self
50
- attr_reader :attribute_names
51
- end
52
-
53
- def initialize(attributes={})
54
- attributes = attributes.dup
55
- self.exitstatus = attributes.delete(:exitstatus) || -1
56
- self.language = attributes.delete(:language) || ""
57
- self.language_friendly = attributes.delete(:language_friendly) || ""
58
- self.code = attributes.delete(:code) || ""
59
- self.output = attributes.delete(:output) || ""
60
- self.status = attributes.delete(:status) || ""
61
- self.url = attributes.delete(:url) || ""
62
- stderr = attributes.delete(:stderr) || $stderr
63
- stderr.puts "Unexpected attributes! #{attributes.keys.inspect}" if attributes.any?
64
- end
65
-
66
- # Returns representation of the result built out of JSON primitives (hash, string, int)
67
- def as_json
68
- self.class.attribute_names.each_with_object Hash.new do |name, attributes|
69
- attributes[name.to_s] = public_send name
70
- end
71
- end
72
-
73
- def to_json
74
- JSON.dump(as_json)
75
- end
76
- end
77
-
78
5
  # @param code [String] the code to evaluate.
79
6
  # @option options [String] :language Mandatory, a language recognized by eval.in, such as any value in {KNOWN_LANGUAGES}.
80
7
  # @option options [String] :url Override the url to post the code to
@@ -86,7 +13,7 @@ module EvalIn
86
13
  # result = EvalIn.call 'puts "hello, #{gets}"', stdin: 'world', language: "ruby/mri-2.1"
87
14
  # result.output # => "hello, world\n"
88
15
  def self.call(code, options={})
89
- url = post_code(code, options)
16
+ url = Client.post_code(code, options)
90
17
  fetch_result url, options
91
18
  end
92
19
 
@@ -97,97 +24,7 @@ module EvalIn
97
24
  # result = EvalIn.fetch_result "https://eval.in/147.json"
98
25
  # result.output # => "Hello Charlie! "
99
26
  def self.fetch_result(raw_url, options={})
100
- raw_json_url = jsonify_url(raw_url)
101
- build_result fetch_result_json(raw_json_url, options)
102
- end
103
-
104
- # @api private
105
- def self.post_code(code, options)
106
- url = options.fetch(:url, "https://eval.in/")
107
- input = options.fetch(:stdin, "")
108
- language = options.fetch(:language) { raise ArgumentError, ":language is mandatory, but options only has #{options.keys.inspect}" }
109
- form_data = {"utf8" => "√", "code" => code, "execute" => "on", "lang" => language, "input" => input}
110
-
111
- result = post_request url, form_data, user_agent_for(options[:context])
112
-
113
- if result.code == '302'
114
- jsonify_url result['location']
115
- elsif KNOWN_LANGUAGES.include? language
116
- raise RequestError, "There was an unexpected error, we got back a response code of #{result.code}"
117
- else
118
- raise RequestError, "Perhaps language is wrong, you provided: #{language.inspect}\n"\
119
- "Known languages are: #{KNOWN_LANGUAGES.inspect}"
120
- end
121
- end
122
-
123
- # @api private
124
- def self.fetch_result_json(raw_url, options={})
125
- result = get_request raw_url, user_agent_for(options[:context])
126
- return JSON.parse(result.body).merge('url' => raw_url) if result.body
127
- raise ResultNotFound, "No json at #{raw_url.inspect}"
128
- end
129
-
130
- # @api private
131
- def self.build_result(response_json)
132
- exitstatus = case response_json['status']
133
- when nil then nil # let it choose default
134
- when /status (\d+)$/ then $1.to_i
135
- when /^Forbidden/ then 1
136
- else 0
137
- end
138
-
139
- Result.new exitstatus: exitstatus,
140
- language: response_json['lang'],
141
- language_friendly: response_json['lang_friendly'],
142
- code: response_json['code'],
143
- output: response_json['output'],
144
- status: response_json['status'],
145
- url: response_json['url']
146
- end
147
-
148
- # @api private
149
- def self.jsonify_url(url)
150
- uri = URI(url)
151
- uri.path = Pathname.new(uri.path).sub_ext('.json').to_s
152
- uri.to_s
153
- end
154
-
155
- # @private
156
- def self.user_agent_for(context)
157
- 'http://rubygems.org/gems/eval_in'.tap do |agent|
158
- context && agent.concat(" (#{context})")
159
- end
160
- end
161
-
162
- # @api private
163
- # Can't just use Net::HTTP.get, b/c it doesn't use ssl on 1.9.3
164
- # https://github.com/ruby/ruby/blob/v2_1_2/lib/net/http.rb#L478-479
165
- # https://github.com/ruby/ruby/blob/v1_9_3_547/lib/net/http.rb#L454
166
- def self.get_request(raw_url, user_agent)
167
- generic_request_for raw_url: raw_url,
168
- request_type: Net::HTTP::Get,
169
- user_agent: user_agent
170
- end
171
-
172
- # @private
173
- def self.post_request(raw_url, form_data, user_agent)
174
- generic_request_for raw_url: raw_url,
175
- request_type: Net::HTTP::Post,
176
- user_agent: user_agent,
177
- form_data: form_data
178
- end
179
-
180
- # @private
181
- # stole this out of implementation for post_form https://github.com/ruby/ruby/blob/2afed6eceff2951b949db7ded8167a75b431bad6/lib/net/http.rb#L503
182
- # can use this to view the request: http.set_debug_output $stdout
183
- def self.generic_request_for(params)
184
- uri = URI params.fetch(:raw_url)
185
- path = uri.path
186
- path = '/' if path.empty?
187
- request = params.fetch(:request_type).new(path)
188
- request['User-Agent'] = params[:user_agent] if params.key? :user_agent
189
- request.form_data = params[:form_data] if params.key? :form_data
190
- request.basic_auth uri.user, uri.password if uri.user
191
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: (uri.scheme == 'https')) { |http| http.request request }
27
+ raw_json_url = HTTP.jsonify_url(raw_url)
28
+ Client.build_result Client.fetch_result_json(raw_json_url, options)
192
29
  end
193
30
  end
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+
3
+ require 'json'
4
+ require 'eval_in/http'
5
+ require 'eval_in/result'
6
+ require 'eval_in/constants'
7
+
8
+ module EvalIn
9
+
10
+ # Code that does the integration with the actual eval.in service
11
+ # It is primarily in place to help the toplevel methods wire things together
12
+ # in the ways that they need to.
13
+ #
14
+ # **This should be assumed volatile, and you should avoid depending on it.**
15
+ module Client
16
+ extend self
17
+
18
+ # @api private
19
+ def post_code(code, options)
20
+ url = options.fetch(:url, "https://eval.in/")
21
+ input = options.fetch(:stdin, "")
22
+ language = language_or_error_from options
23
+ form_data = {"utf8" => "√", "code" => code, "execute" => "on", "lang" => language, "input" => input}
24
+
25
+ result = HTTP.post_request url, form_data, user_agent_for(options[:context])
26
+
27
+ if result.code == '302'
28
+ HTTP.jsonify_url result['location']
29
+ elsif KNOWN_LANGUAGES.include? language
30
+ raise RequestError, "There was an unexpected error, we got back a response code of #{result.code}"
31
+ else
32
+ raise RequestError, "Perhaps language is wrong, you provided: #{language.inspect}\n"\
33
+ "Known languages are: #{KNOWN_LANGUAGES.inspect}"
34
+ end
35
+ end
36
+
37
+ # @api private
38
+ def fetch_result_json(raw_url, options={})
39
+ result = HTTP.get_request raw_url, user_agent_for(options[:context])
40
+ return JSON.parse(result.body).merge('url' => raw_url) if result.body
41
+ raise ResultNotFound, "No json at #{raw_url.inspect}"
42
+ end
43
+
44
+ # @api private
45
+ def build_result(response_json)
46
+ exitstatus = case response_json['status']
47
+ when nil then nil # let Result choose default
48
+ when /status (\d+)$/ then $1.to_i
49
+ when /^Forbidden/ then 1
50
+ else 0
51
+ end
52
+
53
+ Result.new exitstatus: exitstatus,
54
+ language: response_json['lang'],
55
+ language_friendly: response_json['lang_friendly'],
56
+ code: response_json['code'],
57
+ output: response_json['output'],
58
+ status: response_json['status'],
59
+ url: response_json['url']
60
+ end
61
+
62
+ # @api private
63
+ def user_agent_for(context)
64
+ 'http://rubygems.org/gems/eval_in'.tap do |agent|
65
+ agent << " (#{context})" if context
66
+ end
67
+ end
68
+
69
+ # @api private
70
+ def language_or_error_from(options)
71
+ options.fetch :language do
72
+ raise ArgumentError, ":language is mandatory, but options only has #{options.keys.inspect}"
73
+ end
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,37 @@
1
+ module EvalIn
2
+ VERSION = '0.2.0'
3
+
4
+ EvalInError = Class.new StandardError
5
+ RequestError = Class.new EvalInError
6
+ ResultNotFound = Class.new EvalInError
7
+
8
+ # @example Generated with
9
+ # nokogiri https://eval.in -e 'puts $_.xpath("//option/@value")'
10
+ KNOWN_LANGUAGES = %w[
11
+ c/gcc-4.4.3
12
+ c/gcc-4.9.1
13
+ c++/c++11-gcc-4.9.1
14
+ c++/gcc-4.4.3
15
+ c++/gcc-4.9.1
16
+ coffeescript/node-0.10.29-coffee-1.7.1
17
+ fortran/f95-4.4.3
18
+ haskell/hugs98-sep-2006
19
+ io/io-20131204
20
+ javascript/node-0.10.29
21
+ lua/lua-5.1.5
22
+ lua/lua-5.2.3
23
+ ocaml/ocaml-4.01.0
24
+ php/php-5.5.14
25
+ pascal/fpc-2.6.4
26
+ perl/perl-5.20.0
27
+ python/cpython-2.7.8
28
+ python/cpython-3.4.1
29
+ ruby/mri-1.0
30
+ ruby/mri-1.8.7
31
+ ruby/mri-1.9.3
32
+ ruby/mri-2.0.0
33
+ ruby/mri-2.1
34
+ slash/slash-head
35
+ assembly/nasm-2.07
36
+ ]
37
+ end