Dduvall has uploaded a new change for review. https://gerrit.wikimedia.org/r/150977
Change subject: Parsing of responses and additional query actions ...................................................................... Parsing of responses and additional query actions Implemented default response handling to provide easier access to parsed JSON results and warnings. Alphabetized Client methods and refactored remaining methods to delegate through standard request/response handling (#action). Change-Id: I500ef8e153b431d31492fc6a43ff63b54003a00e --- M lib/mediawiki_api/client.rb M lib/mediawiki_api/exceptions.rb A lib/mediawiki_api/response.rb M spec/client_spec.rb A spec/response_spec.rb A spec/spec_helper.rb 6 files changed, 280 insertions(+), 59 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/ruby/api refs/changes/77/150977/1 diff --git a/lib/mediawiki_api/client.rb b/lib/mediawiki_api/client.rb index 3f48a9d..f845bed 100644 --- a/lib/mediawiki_api/client.rb +++ b/lib/mediawiki_api/client.rb @@ -3,6 +3,7 @@ require "json" require "mediawiki_api/exceptions" +require "mediawiki_api/response" module MediawikiApi class Client @@ -22,86 +23,124 @@ @tokens = {} end - def log_in(username, password, token = nil) - params = { action: "login", lgname: username, lgpassword: password, format: FORMAT } - params[:lgtoken] = token unless token.nil? - resp = @conn.post "", params + def create_account(username, password, token = nil) + params = { name: username, password: password, token_type: false } + params[:token] = token unless token.nil? - data = JSON.parse(resp.body)["login"] + data = action(:createaccount, params).data case data["result"] when "Success" @logged_in = true @tokens.clear when "NeedToken" - log_in username, password, data["token"] - else - raise LoginError, data["result"] - end - end - - def create_account(username, password, token = nil) - params = { action: "createaccount", name: username, password: password, format: FORMAT } - params[:token] = token unless token.nil? - resp = @conn.post "", params - - data = JSON.parse(resp.body)["createaccount"] - - case data["result"] - when "Success" - @logged_in = true - when "NeedToken" - create_account username, password, data["token"] + data = create_account(username, password, data["token"]) else raise CreateAccountError, data["result"] end + + data end def create_page(title, content) - action("edit", title: title, text: content) + action(:edit, title: title, text: content) end def delete_page(title, reason) - action("delete", title: title, reason: reason) - end - - def upload_image(filename, path, comment, ignorewarnings) - file = Faraday::UploadIO.new(path, "image/png") - action("upload", token_type: "edit", filename: filename, file: file, comment: comment, ignorewarnings: ignorewarnings) + action(:delete, title: title, reason: reason) end def get_wikitext(title) @conn.get "/w/index.php", { action: "raw", title: title } end - def protect_page(title, reason, protections = "edit=sysop|move=sysop") - action("protect", title: title, reason: reason, protections: protections) + def list(type, params = {}) + subquery(:list, type, params) end - def watch_page(title) - action("watch", titles: title) + def log_in(username, password, token = nil) + params = { lgname: username, lgpassword: password, token_type: false } + params[:lgtoken] = token unless token.nil? + + data = action(:login, params).data + + case data["result"] + when "Success" + @logged_in = true + @tokens.clear + when "NeedToken" + data = log_in(username, password, data["token"]) + else + raise LoginError, data["result"] + end + + data + end + + def meta(type, params = {}) + subquery(:meta, type, params) + end + + def prop(type, params = {}) + subquery(:prop, type, params) + end + + def protect_page(title, reason, protections = "edit=sysop|move=sysop") + action(:protect, title: title, reason: reason, protections: protections) + end + + def query(params = {}) + action(:query, { token_type: false }.merge(params)) end def unwatch_page(title) - action("watch", titles: title, unwatch: true) + action(:watch, titles: title, unwatch: true) + end + + def upload_image(filename, path, comment, ignorewarnings) + file = Faraday::UploadIO.new(path, "image/png") + action(:upload, token_type: "edit", filename: filename, file: file, comment: comment, ignorewarnings: ignorewarnings) + end + + def watch_page(title) + action(:watch, titles: title) end protected - def action(name, options = {}) - options[:token] = get_token(options.delete(:token_type) || name) - options[:titles] = Array(options[:titles]).join("|") if options.include?(:titles) + def action(name, params = {}) + name = name.to_s + token_type = params.delete(:token_type) + envelope = (params.delete(:envelope) || [name]).map(&:to_s) - @conn.post("", options.merge(action: name, format: FORMAT)).tap do |response| - if response.headers.include?("mediawiki-api-error") - raise ApiError.new(JSON.parse(response.body)["error"]) + params[:token] = get_token(token_type || name) unless token_type == false + params = compile_parameters(params) + + response = @conn.post("", params.merge(action: name, format: FORMAT)) + + if response.headers.include?("mediawiki-api-error") + raise ApiError.new(Response.new(response, ["error"])) + end + + Response.new(response, envelope) + end + + def compile_parameters(parameters) + parameters.each.with_object({}) do |(name, value), params| + case value + when false + # omit it entirely + when Array + params[name] = value.join("|") + else + params[name] = value end end end def get_token(type) unless @tokens.include?(type) - resp = @conn.get "", { action: "tokens", type: type, format: FORMAT } + resp = @conn.get("", action: "tokens", type: type, format: FORMAT) token_data = JSON.parse(resp.body) raise TokenError, token_data["warnings"] if token_data.include?("warnings") @@ -111,5 +150,9 @@ @tokens[type] end + + def subquery(type, subtype, params = {}) + query(params.merge(type.to_sym => subtype, :envelope => ["query", subtype])) + end end end diff --git a/lib/mediawiki_api/exceptions.rb b/lib/mediawiki_api/exceptions.rb index 635dd00..fdc6aba 100644 --- a/lib/mediawiki_api/exceptions.rb +++ b/lib/mediawiki_api/exceptions.rb @@ -1,15 +1,28 @@ module MediawikiApi class ApiError < StandardError - attr_reader :code, :info + attr_reader :response - def initialize(error) - @code = error["code"] - @info = error["info"] + def initialize(response) + @response = response + end + + def code + data["code"] + end + + def info + data["info"] end def message "#{info} (#{code})" end + + private + + def data + @response.data || {} + end end class CreateAccountError < StandardError diff --git a/lib/mediawiki_api/response.rb b/lib/mediawiki_api/response.rb new file mode 100644 index 0000000..11febe4 --- /dev/null +++ b/lib/mediawiki_api/response.rb @@ -0,0 +1,84 @@ +require "forwardable" +require "json" + +module MediawikiApi + # Provides access to a parsed MediaWiki API responses. + # + # Some types of responses, depending on the action, contain a level or two + # of addition structure (an envelope) above the actual payload. The {#data} + # method provides a way of easily getting at it. + # + # @example + # # http.body => '{"query": {"userinfo": {"some": "data"}}}' + # response = Response.new(http, ["query", "userinfo"]) + # response.data # => { "some" => "data" } + # + class Response + extend Forwardable + + def_delegators :@response, :status, :success? + + # Constructs a new response. + # + # @param response [Faraday::Response] + # @param envelope [Array] Property names for expected payload nesting. + # + def initialize(response, envelope = []) + @response = response + @envelope = envelope + end + + # Accessor for root response object values. + # + # @param key [String] + # + # @return [Object] + # + def [](key) + response_object[key] + end + + # The main payload from the parsed response, removed from its envelope. + # + # @return [Object] + # + def data + case response_object + when Hash + open_envelope(response_object) + else + response_object + end + end + + # Condensed warning messages from the response. + # + # @return [Array] + # + def warnings + if response_object["warnings"] + response_object["warnings"].values.collect { |warns| warns.values }.flatten + else + [] + end + end + + # Whether the response contains warnings. + # + # @return [true, false] + # + def warnings? + !warnings.empty? + end + + private + + def open_envelope(obj) + @envelope.reduce(obj) { |obj, key| obj[key] } + end + + def response_object + @response_object ||= JSON.parse(@response.body) + end + end +end diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 4adcd02..5028e1b 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -1,4 +1,4 @@ -require "mediawiki_api" +require "spec_helper" require "webmock/rspec" describe MediawikiApi::Client do @@ -37,12 +37,15 @@ end it "logs in" do - subject.log_in "Test", "qwe123" + response = subject.log_in("Test", "qwe123") + + expect(response).to include("result" => "Success") expect(subject.logged_in).to be true end it "sends second request with token and cookies" do - subject.log_in "Test", "qwe123" + response = subject.log_in("Test", "qwe123") + expect(@success_req).to have_been_requested end end @@ -77,14 +80,6 @@ it "creates a page using an edit token" do subject.create_page("Test", "test123") expect(@edit_req).to have_been_requested - end - - context "when API returns Success" do - before do - @edit_req.to_return(body: { result: "Success" }.to_json ) - end - - it "returns a MediawikiApi::Page" end end @@ -122,7 +117,7 @@ with(body: { format: "json", action: "createaccount", name: "Test", password: "qwe123" }). to_return(body: { createaccount: body_base.merge({ result: "Success" }) }.to_json ) - expect(subject.create_account("Test", "qwe123")).to be true + expect(subject.create_account("Test", "qwe123")).to include("result" => "Success") end context "when API returns NeedToken" do @@ -143,7 +138,7 @@ end it "creates an account" do - expect(subject.create_account("Test", "qwe123")).to be true + expect(subject.create_account("Test", "qwe123")).to include("result" => "Success") end it "sends second request with token and cookies" do diff --git a/spec/response_spec.rb b/spec/response_spec.rb new file mode 100644 index 0000000..e2a40a6 --- /dev/null +++ b/spec/response_spec.rb @@ -0,0 +1,83 @@ +require "spec_helper" + +describe MediawikiApi::Response do + let(:response) { MediawikiApi::Response.new(faraday_response, envelope) } + + let(:faraday_response) { double("Faraday::Response", body: body) } + let(:body) { "{}" } + let(:response_object) { JSON.parse(body) } + let(:envelope) { [] } + + describe "#data" do + subject { response.data } + + context "with a JSON object response body" do + let(:body) { '{ "query": { "result": "success" } }' } + + context "and no expected envelope" do + let(:envelope) { [] } + + it { is_expected.to eq(response_object) } + end + + context "and a single-level envelope" do + let(:envelope) { ["query"] } + let(:nested_object) { response_object["query"] } + + it { is_expected.to eq(nested_object) } + end + + context "and a multi-level envelope" do + let(:envelope) { ["query", "result"] } + let(:nested_object) { response_object["query"]["result"] } + + it { is_expected.to eq(nested_object) } + end + end + + context "with a JSON array response body" do + let(:body) { '[ "something" ]' } + + context "with any expected envelope" do + let(:envelope) { ["what", "ever"] } + + it { is_expected.to eq(response_object) } + end + end + end + + describe "#warnings" do + subject { response.warnings } + + context "where the response contains no warnings" do + let(:body) { '{ "query": { "result": "success" } }' } + + it { is_expected.to be_empty } + end + + context "where the response contains warnings" do + let(:body) { '{ "warnings": { "main": { "*": "sorta bad message" } } }' } + + it { is_expected.to_not be_empty } + it { is_expected.to include("sorta bad message") } + end + end + + describe "#warnings?" do + subject { response.warnings? } + + before { allow(response).to receive(:warnings) { warnings } } + + context "where there are warnings" do + let(:warnings) { ["warning"] } + + it { is_expected.to be(true) } + end + + context "where there are no warnings" do + let(:warnings) { [] } + + it { is_expected.to be(false) } + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..5e71d3c --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,3 @@ +require "bundler/setup" + +Bundler.require(:default, :development) -- To view, visit https://gerrit.wikimedia.org/r/150977 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I500ef8e153b431d31492fc6a43ff63b54003a00e Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/ruby/api Gerrit-Branch: master Gerrit-Owner: Dduvall <dduv...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits