From 01412cfaac844f1fe4f56cce1f0183dcbecf4f2c Mon Sep 17 00:00:00 2001 From: Kenny Meyer Date: Sat, 19 Sep 2026 10:33:55 -0300 Subject: [PATCH] Release 3.0.0: modernize for Ruby 3.4/4.0 Replace Jeweler and Travis with a maintained gemspec and GitHub Actions. Call Ideone over HTTPS, require Ruby 3.4+, and cover the SOAP client with WebMock tests. The live Ideone SOAP API is still compatible. --- .document | 5 - .github/workflows/ci.yml | 25 +++ .gitignore | 70 +++----- .ruby-version | 1 + .travis.yml | 5 - CHANGELOG.md | 17 ++ Gemfile | 15 +- LICENSE.txt | 2 +- README.md | 130 ++++++++++++++ README.rdoc | 85 --------- Rakefile | 49 +----- VERSION | 1 - ideone-ruby-api.gemspec | 81 +++------ lib/ideone.rb | 196 +++++++++++---------- lib/ideone/exceptions.rb | 5 +- lib/ideone/version.rb | 12 +- test/client_test.rb | 112 ++++++++++++ test/fixtures/create_submission.xml | 11 ++ test/fixtures/get_languages.xml | 18 ++ test/fixtures/get_submission_details.xml | 25 +++ test/fixtures/get_submission_status.xml | 12 ++ test/fixtures/paste_not_found.xml | 10 ++ test/fixtures/test_function_auth_error.xml | 10 ++ test/fixtures/test_function_ok.xml | 14 ++ test/fixtures/wsdl.xml | 3 + test/helper.rb | 17 -- test/live_test.rb | 39 ++++ test/test_helper.rb | 21 +++ test/test_ideone-ruby-api.rb | 79 --------- 29 files changed, 621 insertions(+), 449 deletions(-) delete mode 100644 .document create mode 100644 .github/workflows/ci.yml create mode 100644 .ruby-version delete mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 README.md delete mode 100644 README.rdoc delete mode 100644 VERSION create mode 100644 test/client_test.rb create mode 100644 test/fixtures/create_submission.xml create mode 100644 test/fixtures/get_languages.xml create mode 100644 test/fixtures/get_submission_details.xml create mode 100644 test/fixtures/get_submission_status.xml create mode 100644 test/fixtures/paste_not_found.xml create mode 100644 test/fixtures/test_function_auth_error.xml create mode 100644 test/fixtures/test_function_ok.xml create mode 100644 test/fixtures/wsdl.xml delete mode 100644 test/helper.rb create mode 100644 test/live_test.rb create mode 100644 test/test_helper.rb delete mode 100644 test/test_ideone-ruby-api.rb diff --git a/.document b/.document deleted file mode 100644 index 3d618dd..0000000 --- a/.document +++ /dev/null @@ -1,5 +0,0 @@ -lib/**/*.rb -bin/* -- -features/**/*.feature -LICENSE.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d0ba384 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: ["3.4", "4.0"] + steps: + - uses: actions/checkout@v7 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run tests + run: bundle exec rake diff --git a/.gitignore b/.gitignore index 1553036..a750a9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,49 +1,21 @@ -# rcov generated -coverage - -# rdoc generated -rdoc - -# yard generated -doc -.yardoc - -# bundler -.bundle - -# jeweler generated -pkg - -# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: -# -# * Create a file at ~/.gitignore -# * Include files you want ignored -# * Run: git config --global core.excludesfile ~/.gitignore -# -# After doing this, these files will be ignored in all your git projects, -# saving you from having to 'pollute' every project you touch with them -# -# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) -# -# For MacOS: -# -#.DS_Store - -# For TextMate -#*.tmproj -#tmtags - -# For emacs: -#*~ -#\#* -#.\#* - -# For vim: -#*.swp - -# For redcar: -#.redcar - -# For rubinius: -#*.rbc -Gemfile.lock +*.gem +*.rbc +/Gemfile.lock +/.bundle/ +/.config +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/rdoc/ +/spec/reports/ +/tmp/ +/vendor/bundle +.DS_Store +.idea/ +.serena/ +.planning/ +task_plan.md +findings.md +progress.md diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..5186d07 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +4.0 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f6efc37..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -language: ruby -rvm: - - 1.9.2 - - 1.9.3 - - 2.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6403cfe --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +# Changelog + +## 3.0.0 + +- Require Ruby 3.4 or newer. Developed and CI-tested on Ruby 4.0. +- Upgrade Savon to `~> 2.17` and add the `cgi` gem (removed from default gems in Ruby 4.0). +- Call the Ideone SOAP API over HTTPS (`https://ideone.com/api/1/service.wsdl`). +- Replace Jeweler-generated packaging with a hand-maintained gemspec. +- Replace Travis CI with GitHub Actions (Ruby 3.4 and 4.0). +- Use Minitest 6 and WebMock for the test suite. +- Stop treating every Ideone error as `Ideone::AuthError`. Authentication failures still raise `AuthError`; other API errors raise `Ideone::Error`. +- Stop mutating shared request state between calls. +- Read credentials from `IDEONE_USER` / `IDEONE_PASSWORD` in optional live tests instead of hardcoding them. + +## 2.1.2 + +- Last Jeweler-era release (2014-05-10). diff --git a/Gemfile b/Gemfile index 5039c7a..be173b2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,5 @@ -source "http://rubygems.org" -# Add dependencies required to use your gem here. -# Example: -# gem "activesupport", ">= 2.3.5" -gem "savon", "~> 2.3.3" +# frozen_string_literal: true -# Add dependencies to develop your gem here. -# Include everything needed to run rake, tests, features, etc. -group :development do - gem "jeweler", "~> 2.0.1" - gem "test-unit", "~> 2.3" -end +source "https://rubygems.org" + +gemspec diff --git a/LICENSE.txt b/LICENSE.txt index f8363c3..210e8ee 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2011 Kenny Meyer +Copyright (c) 2011-2026 Kenny Meyer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md new file mode 100644 index 0000000..67b93bc --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# ideone-ruby-api + +[![CI](https://github.com/kennym/ideone-ruby-api/actions/workflows/ci.yml/badge.svg)](https://github.com/kennym/ideone-ruby-api/actions/workflows/ci.yml) +[![Gem Version](https://badge.fury.io/rb/ideone-ruby-api.svg)](https://badge.fury.io/rb/ideone-ruby-api) + +Ruby bindings for the [Ideone](https://ideone.com) SOAP API — an online compiler and pastebin. + +Requires **Ruby 3.4+**. Developed on **Ruby 4.0**. + +## Ideone API status + +The SOAP service this gem wraps is still live. The original operations +(`testFunction`, `getLanguages`, `createSubmission`, `getSubmissionStatus`, +`getSubmissionDetails`) still work: + +| | | +|---|---| +| WSDL | `https://ideone.com/api/1/service.wsdl` | +| Endpoint | `https://ideone.com/api/1/service` | + +[Sphere Engine](https://sphere-engine.com) no longer develops this SOAP API. +They recommend the [Compilers REST API v4](https://docs.sphere-engine.com/compilers/api/overview-version-4) +for new products. This gem stays on Ideone SOAP so existing callers keep working. + +The old PDF (`http://ideone.com/files/ideone-api.pdf`) is gone. + +## Installation + +```ruby +# Gemfile +gem "ideone-ruby-api" +``` + +```bash +gem install ideone-ruby-api +``` + +## Usage + +```ruby +require "ideone" + +client = Ideone.new("username", "password") + +# Log SOAP traffic +client = Ideone.new("username", "password", true) + +link = client.create_submission("puts 'hello, world'", 17) # 17 = Ruby +# => "TOKEN" + +client.submission_status(link) +# => { status: 0, result: 15 } + +client.submission_details(link) +# => { "error" => "OK", "langName" => "Ruby", "output" => "hello, world\n", ... } + +client.languages +# => { "1" => "C++ (gcc 8.3)", "17" => "Ruby (ruby 2.5.5)", "116" => "Python 3 (python 3.12)", ... } + +client.test +# => { "error" => "OK", "moreHelp" => "ideone.com", "pi" => "3.14", +# "answerToLifeAndEverything" => "42", "oOok" => true } +``` + +### Language ids + +Call `languages` for the current list. As of 2026-09 these still match the old docs: + +| Id | Language | +|---:|---| +| 1 | C++ (gcc 8.3) | +| 17 | Ruby (ruby 2.5.5) | +| 116 | Python 3 (python 3.12) | + +### Submission status and result + +`submission_status` returns integers: + +**status** (execution stage) + +| Value | Meaning | +|---:|---| +| < 0 | waiting in queue | +| 0 | finished | +| 1 | compiling | +| 3 | running | + +**result** (when status is 0) + +| Value | Meaning | +|---:|---| +| 11 | compilation error | +| 12 | runtime error | +| 13 | time limit exceeded | +| 15 | success | +| 17 | memory limit exceeded | +| 19 | illegal system call | +| 20 | internal error | + +Poll until `status == 0` before reading output from `submission_details`. + +### Errors + +- `Ideone::AuthError` — Ideone returned `AUTH_ERROR` +- `Ideone::Error` — any other API or transport failure + +## Development + +```bash +bundle install +bundle exec rake +``` + +Default tests are mocked (WebMock) and do not call Ideone. + +```bash +IDEONE_USER=you IDEONE_PASSWORD=secret bundle exec rake +``` + +CI runs on Ruby 3.4 and 4.0. + +## Similar projects + +- https://github.com/maveonair/ccpacona +- https://github.com/jonathanperret/ideone.rb +- https://github.com/Pistos/ideone-gem + +## License + +Copyright (c) 2011-2026 Kenny Meyer. See [LICENSE.txt](LICENSE.txt). diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index 09a7b6c..0000000 --- a/README.rdoc +++ /dev/null @@ -1,85 +0,0 @@ -= ideone-ruby-api - -{}[http://travis-ci.org/kennym/ideone-ruby-api] -{Gem Version}[http://badge.fury.io/rb/ideone-ruby-api] -{GA analytics}[https://ga-beacon.appspot.com/UA-48308186-1/ideone-ruby-api/README.rdoc] - -Ideone is a pastebin, as well as an online compiler and debugger. This -project is a Ruby binding to the Ideone API. - -Requires Ruby =>1.9.2 - -== Usage - - require 'ideone' - - # Authenticate with Ideone - i = Ideone.new('username', 'password') - => # - - # Enable logging by setting flag to `true` - i = Ideone.new('username', 'password', true) - => # - - # Create submission - source_code = "puts 'hello, world'" - i.create_submission(source_code, - 17) # Ruby - => 'TOKEN' - - # Get submission status - i.submission_status("TOKEN") - => {:status=>0, :result=>15} - - # Get all available languages - i.languages - => {"1"=>"C++ (gcc-4.3.4)", "2"=>"Pascal (gpc) (gpc 20070904)", - "3"=>"Perl (perl 5.12.1)", "4"=>"Python (python 2.6.4)" - - # Test that everything's working - i.test - => {"error"=>"OK", - "moreHelp"=>"ideone.com", - "pi"=>"3.14", - "answerToLifeAndEverything"=>"42", - "oOok"=>true} - -== Similar projects - -* https://github.com/maveonair/ccpacona -* https://github.com/jonathanperret/ideone.rb -* https://github.com/Pistos/ideone-gem/blob/master/lib/ideone.rb - -I stole the best ideas from each of the above projects, which, I -think, makes it much more robust, and it includes tests (which -actually pass). - -== Running the test suite - -To run the test suite, please fill out USER and PASS in test/test_ideone-ruby-api.rb - -Then, you can run `rake test` - -== In the wild - -* Nejupy ( https://github.com/kennym/nejupy-ruby ) - -== Contributing to ideone-ruby-api - -* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet -* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it -* Fork the project -* Start a feature/bugfix branch -* Commit and push until you are happy with your contribution -* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. -* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. - -== References - -* http://ideone.com/files/ideone-api.pdf - -== Copyright - -Copyright (c) 2011-2014 Kenny Meyer. See LICENSE.txt for -further details. - diff --git a/Rakefile b/Rakefile index 5559c6f..bd893bd 100644 --- a/Rakefile +++ b/Rakefile @@ -1,47 +1,12 @@ -# encoding: utf-8 +# frozen_string_literal: true -require 'rubygems' -require 'bundler' -begin - Bundler.setup(:default, :development) -rescue Bundler::BundlerError => e - $stderr.puts e.message - $stderr.puts "Run `bundle install` to install missing gems" - exit e.status_code -end -require 'rake' - -require 'jeweler' -require './lib/ideone/version.rb' -Jeweler::Tasks.new do |gem| - # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options - gem.version = Ideone::Version::STRING - gem.name = "ideone-ruby-api" - gem.homepage = "http://github.com/kennym/ideone-ruby-api" - gem.license = "MIT" - gem.summary = "Ruby binding for Ideone API" - gem.description = "A better Ruby binding for the Ideone API " - gem.email = "kenny@kennymeyer.net" - gem.authors = ["Kenny Meyer"] - # dependencies defined in Gemfile -end -Jeweler::RubygemsDotOrgTasks.new +require "bundler/gem_tasks" +require "rake/testtask" -require 'rake/testtask' Rake::TestTask.new(:test) do |test| - test.libs << 'lib' << 'test' - test.pattern = 'test/**/test_*.rb' - test.verbose = true + test.libs << "lib" << "test" + test.pattern = "test/**/*_test.rb" + test.warning = true end -task :default => :test - -require 'rdoc/task' -Rake::RDocTask.new do |rdoc| - version = File.exist?('VERSION') ? File.read('VERSION') : "" - - rdoc.rdoc_dir = 'rdoc' - rdoc.title = "ideone-ruby-api #{version}" - rdoc.rdoc_files.include('README*') - rdoc.rdoc_files.include('lib/**/*.rb') -end +task default: :test diff --git a/VERSION b/VERSION deleted file mode 100644 index 359a5b9..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.0.0 \ No newline at end of file diff --git a/ideone-ruby-api.gemspec b/ideone-ruby-api.gemspec index f34671e..4429069 100644 --- a/ideone-ruby-api.gemspec +++ b/ideone-ruby-api.gemspec @@ -1,59 +1,34 @@ -# Generated by jeweler -# DO NOT EDIT THIS FILE DIRECTLY -# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' -# -*- encoding: utf-8 -*- -# stub: ideone-ruby-api 2.1.2 ruby lib +# frozen_string_literal: true -Gem::Specification.new do |s| - s.name = "ideone-ruby-api" - s.version = "2.1.2" +require_relative "lib/ideone/version" - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.require_paths = ["lib"] - s.authors = ["Kenny Meyer"] - s.date = "2014-05-10" - s.description = "A better Ruby binding for the Ideone API " - s.email = "kenny@kennymeyer.net" - s.extra_rdoc_files = [ - "LICENSE.txt", - "README.rdoc" - ] - s.files = [ - ".document", - ".travis.yml", - "Gemfile", - "LICENSE.txt", - "README.rdoc", - "Rakefile", - "VERSION", - "ideone-ruby-api.gemspec", - "lib/ideone.rb", - "lib/ideone/exceptions.rb", - "lib/ideone/version.rb", - "test/helper.rb", - "test/test_ideone-ruby-api.rb" - ] - s.homepage = "http://github.com/kennym/ideone-ruby-api" - s.licenses = ["MIT"] - s.rubygems_version = "2.2.2" - s.summary = "Ruby binding for Ideone API" +Gem::Specification.new do |spec| + spec.name = "ideone-ruby-api" + spec.version = Ideone::Version::STRING + spec.authors = ["Kenny Meyer"] + spec.email = ["kenny@kennymeyer.net"] - if s.respond_to? :specification_version then - s.specification_version = 4 + spec.summary = "Ruby binding for the Ideone API" + spec.description = "A Ruby binding for the Ideone SOAP API (online compiler / pastebin)." + spec.homepage = "https://github.com/kennym/ideone-ruby-api" + spec.license = "MIT" + spec.required_ruby_version = ">= 3.4" - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, ["~> 2.3.3"]) - s.add_development_dependency(%q, ["~> 2.0.1"]) - s.add_development_dependency(%q, ["~> 2.3"]) - else - s.add_dependency(%q, ["~> 2.3.3"]) - s.add_dependency(%q, ["~> 2.0.1"]) - s.add_dependency(%q, ["~> 2.3"]) - end - else - s.add_dependency(%q, ["~> 2.3.3"]) - s.add_dependency(%q, ["~> 2.0.1"]) - s.add_dependency(%q, ["~> 2.3"]) + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/master" + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues" + spec.metadata["rubygems_mfa_required"] = "true" + + spec.files = Dir.chdir(__dir__) do + Dir["lib/**/*.rb", "LICENSE.txt", "README.md", "CHANGELOG.md"] end -end + spec.require_paths = ["lib"] + spec.add_dependency "savon", "~> 2.17" + spec.add_dependency "cgi", "~> 0.5" # Ruby 4.0 removed cgi from default gems; Savon/Gyoku still need it + + spec.add_development_dependency "minitest", "~> 6.0" + spec.add_development_dependency "rake", "~> 13.4" + spec.add_development_dependency "webmock", "~> 3.26" +end diff --git a/lib/ideone.rb b/lib/ideone.rb index 8eba036..072da0b 100644 --- a/lib/ideone.rb +++ b/lib/ideone.rb @@ -1,139 +1,145 @@ -# This program is a Ruby API to the Ideone web service. For more -# information about the Ideone API consult -# http://ideone.com/files/ideone-api.pdf +# frozen_string_literal: true + +# Ruby bindings for the Ideone SOAP API. +# See https://ideone.com/api/1/service.wsdl # -# Author:: Kenny Meyer (kenny@kennymeyer.net) -# Copyright:: Copyright (c) 2014 -# License:: Distributes under the same terms as Ruby +# Author:: Kenny Meyer (kenny@kennymeyer.net) +# Copyright:: Copyright (c) 2011-2026 +# License:: MIT -require_relative 'ideone/exceptions' -require 'savon' # SOAP Client +require "savon" +require_relative "ideone/exceptions" +require_relative "ideone/version" module Ideone - def self.new(username, password, verbose=false) - return Ideone::Client.new(username, password, verbose) + def self.new(username, password, verbose = false, **options) + Client.new(username, password, verbose, **options) end class Client - def initialize(username=nil, password=nil, verbose=false) - @client = Savon.client(wsdl: "http://ideone.com/api/1/service.wsdl", log: verbose) + WSDL_URL = "https://ideone.com/api/1/service.wsdl" + + def initialize(username = nil, password = nil, verbose = false, wsdl: WSDL_URL) + @client = Savon.client( + wsdl: wsdl, + log: verbose, + convert_request_keys_to: :none, + follow_redirects: true + ) @languages_cache = nil - @request_body = { - :user => username, - :pass => password, + @credentials = { + user: username, + pass: password } end - def create_submission(source_code, lang_id, std_input="", run=true, - is_private=false) - request_body = @request_body - request_body[:sourceCode] = source_code - request_body[:language] = lang_id - request_body[:input] = std_input - request_body[:run] = run - request_body[:private] = is_private - - response = call_request(:create_submission) - - return response.to_hash[:create_submission_response][:return][:item][1][:value] + def create_submission(source_code, lang_id, std_input = "", run = true, + is_private = false) + map = response_map( + call_request(:create_submission, { + sourceCode: source_code, + language: lang_id, + input: std_input, + run: run, + private: is_private + }), + :create_submission_response + ) + map["link"] end def submission_status(link) - request_body = @request_body - request_body[:link] = link + map = response_map( + call_request(:get_submission_status, { link: link }), + :get_submission_status_response + ) - response = call_request(:get_submission_status) - - status = response.to_hash[:get_submission_status_response][:return][:item][1][:value].to_i - result = response.to_hash[:get_submission_status_response][:return][:item][2][:value].to_i - - if status < 0 - status = -1 - end + status = map["status"].to_i + result = map["result"].to_i + status = -1 if status < 0 - return { :status => status, :result => result } + { status: status, result: result } end def submission_details(link, - with_source=true, - with_input=true, - with_output=true, - with_stderr=true, - with_cmpinfo=true) - request_body = @request_body - request_body[:link] = link - request_body[:withSource] = with_source - request_body[:withInput] = with_input - request_body[:withOutput] = with_output - request_body[:withStderr] = with_stderr - request_body[:withCmpinfo] = with_cmpinfo - - response = call_request(:get_submission_details) - - details = response.to_hash[:get_submission_details_response][:return][:item] - - create_dict(details) + with_source = true, + with_input = true, + with_output = true, + with_stderr = true, + with_cmpinfo = true) + response_map( + call_request(:get_submission_details, { + link: link, + withSource: with_source, + withInput: with_input, + withOutput: with_output, + withStderr: with_stderr, + withCmpinfo: with_cmpinfo + }), + :get_submission_details_response + ) end - # Get a list of supported languages and cache it. def languages - unless @languages_cache - response = call_request(:get_languages) - - languages = response.to_hash[:get_languages_response][:return][:item][1][:value][:item] - # Create a sorted hash - @languages_cache = Hash[create_dict(languages).sort_by{|k,v| k.to_i}] + @languages_cache ||= begin + map = response_map(call_request(:get_languages), :get_languages_response) + create_dict(array_wrap(nested_items(map["languages"]))).sort_by { |k, _| k.to_i }.to_h end - return @languages_cache end - # A test function that always returns the same thing. def test - response = call_request(:test_function) - - items = response.to_hash[:test_function_response][:return][:item] - - return create_dict(items) + response_map(call_request(:test_function), :test_function_response) end private + def call_request(api_endpoint, extra = {}) + response = @client.call(api_endpoint, message: @credentials.merge(extra)) + check_error(response, :"#{api_endpoint}_response") + response + rescue Savon::Error => e + raise Error, e.message + end + def check_error(response, function_response) - error = get_error(response.to_hash, function_response) - if error != 'OK' - raise Ideone::AuthError, "Invalid Ideone credentials provided" + error = response_map(response, function_response)["error"] + return if error == "OK" + + if error == "AUTH_ERROR" + raise AuthError, "Invalid Ideone credentials provided" end + + raise Error, error.to_s end - def call_request(api_endpoint) - begin - response = @client.call(api_endpoint, :message => @request_body) - rescue Exception => e - raise e - end - check_error(response, "#{api_endpoint}_response".to_sym) - return response + def response_map(response, function_response) + items = response.to_hash.dig(function_response, :return, :item) + raise Error, "Unexpected response from Ideone API" if items.nil? + + create_dict(array_wrap(items)) end - def get_error(response, function_response) - begin - return response[function_response][:return][:item][0][:value] - rescue - return response[function_response][:return][:item][:value] + def create_dict(items) + items.each_with_object({}) do |item, dict| + dict[item[:key]] = normalize_value(item[:value]) end end - def create_dict(items) - dict = {} + def nested_items(value) + return value[:item] if value.is_a?(Hash) && value.key?(:item) - items.each do |item| - key = item[:key] - value = item[:value] - value = "" if value == {:"@xsi:type"=>"xsd:string"} - dict[key] = value - end + value + end + + def array_wrap(value) + value.is_a?(Array) ? value : [value] + end + + def normalize_value(value) + return "" if value.nil? + return "" if value.is_a?(Hash) && value.keys.all? { |key| key.to_s.start_with?("@") } - dict + value end end end diff --git a/lib/ideone/exceptions.rb b/lib/ideone/exceptions.rb index 45186a2..bd0ea06 100644 --- a/lib/ideone/exceptions.rb +++ b/lib/ideone/exceptions.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + module Ideone - class AuthError < StandardError; end + class Error < StandardError; end + class AuthError < Error; end end diff --git a/lib/ideone/version.rb b/lib/ideone/version.rb index e39aa77..c40a29d 100644 --- a/lib/ideone/version.rb +++ b/lib/ideone/version.rb @@ -1,9 +1,11 @@ -class Ideone +# frozen_string_literal: true + +module Ideone module Version - MAJOR = 2 - MINOR = 1 - PATCH = 2 + MAJOR = 3 + MINOR = 0 + PATCH = 0 - STRING = [MAJOR, MINOR, PATCH].compact.join('.') + STRING = [MAJOR, MINOR, PATCH].join(".") end end diff --git a/test/client_test.rb b/test/client_test.rb new file mode 100644 index 0000000..6b15b9a --- /dev/null +++ b/test/client_test.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require "test_helper" + +class ClientTest < Minitest::Test + def setup + WebMock.disable_net_connect! + @client = Ideone::Client.new("user", "pass", wsdl: fixture_path("wsdl.xml")) + end + + def test_wsdl_uses_https + assert_equal "https://ideone.com/api/1/service.wsdl", Ideone::Client::WSDL_URL + end + + def test_new_factory + client = Ideone.new("user", "pass", wsdl: fixture_path("wsdl.xml")) + assert_instance_of Ideone::Client, client + end + + def test_test_function + stub_soap("test_function_ok.xml") + + result = @client.test + + assert_equal "OK", result["error"] + assert_equal "ideone.com", result["moreHelp"] + assert_equal "3.14", result["pi"].to_s + assert_equal "42", result["answerToLifeAndEverything"].to_s + assert result["oOok"] + end + + def test_auth_error + stub_soap("test_function_auth_error.xml") + + error = assert_raises(Ideone::AuthError) { @client.test } + assert_match(/credentials/i, error.message) + end + + def test_generic_api_error + stub_soap("paste_not_found.xml") + + error = assert_raises(Ideone::Error) { @client.submission_status("missing") } + assert_equal "PASTE_NOT_FOUND", error.message + refute_instance_of Ideone::AuthError, error + end + + def test_languages + stub_soap("get_languages.xml") + + result = @client.languages + + assert_equal "C++ (gcc 8.3)", result["1"] + assert_equal "Ruby (ruby 2.5.5)", result["17"] + assert_equal "Python 3 (python 3.12)", result["116"] + assert_equal %w[1 17 116], result.keys + end + + def test_create_submission + stub_soap("create_submission.xml") + + link = @client.create_submission("puts 'hello'", 17) + + assert_equal "VmyZyN", link + assert_requested :post, "https://ideone.com/api/1/service" do |req| + req.body.include?("hello") && req.body.include?("17") + end + end + + def test_submission_status + stub_soap("get_submission_status.xml") + + result = @client.submission_status("PD2kqM") + + assert_equal 0, result[:status] + assert_equal 15, result[:result] + end + + def test_submission_details + stub_soap("get_submission_details.xml") + + result = @client.submission_details("PD2kqM") + + assert_equal "OK", result["error"] + assert_equal "Ruby", result["langName"] + assert_equal "puts \"hello\"", result["source"] + assert_equal "", result["stderr"] + assert_equal 16, result.size + end + + def test_does_not_leak_request_fields_across_calls + stub_soap("create_submission.xml") + @client.create_submission("puts 1", 17) + + stub_soap("test_function_ok.xml") + @client.test + + assert_requested :post, "https://ideone.com/api/1/service" do |req| + req.body.include?("testFunction") && !req.body.include?("sourceCode") + end + end + + private + + def stub_soap(fixture_name) + stub_request(:post, "https://ideone.com/api/1/service") + .to_return( + status: 200, + body: fixture(fixture_name), + headers: { "Content-Type" => "text/xml;charset=UTF-8" } + ) + end +end diff --git a/test/fixtures/create_submission.xml b/test/fixtures/create_submission.xml new file mode 100644 index 0000000..e5e524d --- /dev/null +++ b/test/fixtures/create_submission.xml @@ -0,0 +1,11 @@ + + + + + + errorOK + linkVmyZyN + + + + diff --git a/test/fixtures/get_languages.xml b/test/fixtures/get_languages.xml new file mode 100644 index 0000000..5930c26 --- /dev/null +++ b/test/fixtures/get_languages.xml @@ -0,0 +1,18 @@ + + + + + + errorOK + + languages + + 1C++ (gcc 8.3) + 17Ruby (ruby 2.5.5) + 116Python 3 (python 3.12) + + + + + + diff --git a/test/fixtures/get_submission_details.xml b/test/fixtures/get_submission_details.xml new file mode 100644 index 0000000..6b4a312 --- /dev/null +++ b/test/fixtures/get_submission_details.xml @@ -0,0 +1,25 @@ + + + + + + errorOK + langId17 + langNameRuby + langVersionruby 2.5.5 + time0.02 + date2026-09-19 13:15:00 + status0 + result15 + memory12345 + signal0 + publicfalse + sourceputs "hello" + input + outputhello + stderr + cmpinfo + + + + diff --git a/test/fixtures/get_submission_status.xml b/test/fixtures/get_submission_status.xml new file mode 100644 index 0000000..deaf4ef --- /dev/null +++ b/test/fixtures/get_submission_status.xml @@ -0,0 +1,12 @@ + + + + + + errorOK + status0 + result15 + + + + diff --git a/test/fixtures/paste_not_found.xml b/test/fixtures/paste_not_found.xml new file mode 100644 index 0000000..d5de899 --- /dev/null +++ b/test/fixtures/paste_not_found.xml @@ -0,0 +1,10 @@ + + + + + + errorPASTE_NOT_FOUND + + + + diff --git a/test/fixtures/test_function_auth_error.xml b/test/fixtures/test_function_auth_error.xml new file mode 100644 index 0000000..9adc213 --- /dev/null +++ b/test/fixtures/test_function_auth_error.xml @@ -0,0 +1,10 @@ + + + + + + errorAUTH_ERROR + + + + diff --git a/test/fixtures/test_function_ok.xml b/test/fixtures/test_function_ok.xml new file mode 100644 index 0000000..a6cabe4 --- /dev/null +++ b/test/fixtures/test_function_ok.xml @@ -0,0 +1,14 @@ + + + + + + errorOK + moreHelpideone.com + pi3.14 + answerToLifeAndEverything42 + oOoktrue + + + + diff --git a/test/fixtures/wsdl.xml b/test/fixtures/wsdl.xml new file mode 100644 index 0000000..35b7481 --- /dev/null +++ b/test/fixtures/wsdl.xml @@ -0,0 +1,3 @@ + +Creates a new submission.Returns status and result of a submission in an associative array.Returns information about the submission in an associative array.Returns list of supported languages.This is a test function. If you can call it successfully, +then you will also be able to call the other functions. diff --git a/test/helper.rb b/test/helper.rb deleted file mode 100644 index 77dd42c..0000000 --- a/test/helper.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'rubygems' -require 'bundler' -begin - Bundler.setup(:default, :development) -rescue Bundler::BundlerError => e - $stderr.puts e.message - $stderr.puts "Run `bundle install` to install missing gems" - exit e.status_code -end -require 'test/unit' - -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -$LOAD_PATH.unshift(File.dirname(__FILE__)) -require 'ideone' - -class Test::Unit::TestCase -end diff --git a/test/live_test.rb b/test/live_test.rb new file mode 100644 index 0000000..871c3b3 --- /dev/null +++ b/test/live_test.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "test_helper" + +class LiveTest < Minitest::Test + def setup + skip "Set IDEONE_USER and IDEONE_PASSWORD to run live API tests" unless live? + WebMock.allow_net_connect! + @client = Ideone::Client.new(ENV.fetch("IDEONE_USER"), ENV.fetch("IDEONE_PASSWORD")) + end + + def teardown + WebMock.disable_net_connect! + end + + def test_test_function + result = @client.test + assert_equal "OK", result["error"] + assert_equal "42", result["answerToLifeAndEverything"].to_s + end + + def test_languages_include_ruby + result = @client.languages + assert_operator result.size, :>, 10 + assert_match(/Ruby/i, result["17"].to_s) + end + + def test_submission_status_for_known_link + result = @client.submission_status("PD2kqM") + assert_equal 0, result[:status] + assert_equal 15, result[:result] + end + + private + + def live? + !ENV["IDEONE_USER"].to_s.empty? && !ENV["IDEONE_PASSWORD"].to_s.empty? + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..f1d5cfd --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +$LOAD_PATH.unshift File.expand_path("../lib", __dir__) + +require "ideone" +require "minitest/autorun" +require "webmock/minitest" + +module FixtureHelper + def fixture_path(name) + File.expand_path("fixtures/#{name}", __dir__) + end + + def fixture(name) + File.read(fixture_path(name)) + end +end + +class Minitest::Test + include FixtureHelper +end diff --git a/test/test_ideone-ruby-api.rb b/test/test_ideone-ruby-api.rb deleted file mode 100644 index 4d408d8..0000000 --- a/test/test_ideone-ruby-api.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'helper' - -USER = 'humpydumpy' # YOUR IDEONE USER -PASS = 'testing' # YOUR IDEONE PASSWORD - -class TestIdeoneRubyApi < Test::Unit::TestCase - def test_initialize - omit_if(USER.empty? || PASS.empty?, "Specify ideone USER and PASS") - - instance = Ideone::Client.new - assert_not_nil(instance) - - instance = Ideone::Client.new(USER, PASS) - assert_not_nil(instance) - end - - def test_ideone_test - omit_if(USER.empty? || PASS.empty?, "Specify ideone USER and PASS") - - instance = Ideone::Client.new(USER, PASS) - - result = instance.test - - def compare_intersecting_keys(a, b) - (a.keys & b.keys).all? {|k| a[k] == b[k]} - end - - assert compare_intersecting_keys(result,{"error"=>"OK", "moreHelp"=>"ideone.com", "pi"=>"3.14", "answerToLifeAndEverything"=>"42", "oOok"=>true}) == true - assert_not_nil result - end - - def test_languages - omit_if(USER.empty? || PASS.empty?, "Specify ideone USER and PASS") - - instance = Ideone::Client.new(USER, PASS) - - result = instance.languages - - assert result.count > 10 - assert_not_nil result - end - - def test_create_submission - omit_if(USER.empty? || PASS.empty?, "Specify ideone USER and PASS") - - instance = Ideone::Client.new(USER, PASS) - - code = <<-eos -puts "This is a test submission created from ideone-ruby-api. https://github.com/kennym/ideone-gem/blob/master/lib/ideone.rb" - eos - result = instance.create_submission(code, 17) - - assert result.is_a?(String) - assert_not_nil result - end - - def test_submission_status - omit_if(USER.empty? || PASS.empty?, "Specify ideone USER and PASS") - - instance = Ideone::Client.new(USER, PASS) - - result = instance.submission_status("PD2kqM") - - assert_not_nil result - assert_not_nil result[:status] - assert_not_nil result[:result] - end - - def test_submission_details - omit_if(USER.empty? || PASS.empty?, "Specify ideone USER and PASS") - - instance = Ideone::Client.new(USER, PASS) - - result = instance.submission_details("PD2kqM") - - assert result.count == 16 - assert_not_nil result - end -end