Dduvall has submitted this change and it was merged. 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 (Client#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, 278 insertions(+), 43 deletions(-) Approvals: Dduvall: Looks good to me, approved diff --git a/lib/mediawiki_api/client.rb b/lib/mediawiki_api/client.rb index 3f48a9d..5f1279b 100644 --- a/lib/mediawiki_api/client.rb +++ b/lib/mediawiki_api/client.rb @@ -3,12 +3,15 @@ require "json" require "mediawiki_api/exceptions" +require "mediawiki_api/response" module MediawikiApi class Client FORMAT = "json" attr_accessor :logged_in + + alias logged_in? logged_in def initialize(url, log = false) @conn = Faraday.new(url: url) do |faraday| @@ -23,51 +26,54 @@ end def log_in(username, password, token = nil) - params = { action: "login", lgname: username, lgpassword: password, format: FORMAT } + params = { lgname: username, lgpassword: password, token_type: false } params[:lgtoken] = token unless token.nil? - resp = @conn.post "", params - data = JSON.parse(resp.body)["login"] + data = action(:login, params).data case data["result"] when "Success" @logged_in = true @tokens.clear when "NeedToken" - log_in username, password, data["token"] + data = log_in(username, password, data["token"]) else raise LoginError, data["result"] end + + data end def create_account(username, password, token = nil) - params = { action: "createaccount", name: username, password: password, format: FORMAT } + params = { name: username, password: password, token_type: false } params[:token] = token unless token.nil? - resp = @conn.post "", params - data = JSON.parse(resp.body)["createaccount"] + data = action(:createaccount, params).data case data["result"] when "Success" @logged_in = true + @tokens.clear 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) + 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(:upload, token_type: "edit", filename: filename, file: file, comment: comment, ignorewarnings: ignorewarnings) end def get_wikitext(title) @@ -75,41 +81,81 @@ end def protect_page(title, reason, protections = "edit=sysop|move=sysop") - action("protect", title: title, reason: reason, protections: protections) + action(:protect, title: title, reason: reason, protections: protections) end def watch_page(title) - action("watch", titles: title) + action(:watch, titles: title) end def unwatch_page(title) - action("watch", titles: title, unwatch: true) + action(:watch, titles: title, unwatch: true) + end + + def list(type, params = {}) + subquery(:list, type, params) + end + + def meta(type, params = {}) + subquery(:meta, type, params) + end + + def prop(type, params = {}) + subquery(:prop, type, params) + end + + def query(params = {}) + action(:query, { token_type: false, http_method: :get }.merge(params)) 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 - @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"]) + method = params.delete(:http_method) || :post + token_type = params.delete(:token_type) + envelope = (params.delete(:envelope) || [name]).map(&:to_s) + + params[:token] = get_token(token_type || name) unless token_type == false + params = compile_parameters(params) + + response = @conn.send(method, "", 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 } - token_data = JSON.parse(resp.body) + response = action(:tokens, type: type, http_method: :get, token_type: false) - raise TokenError, token_data["warnings"] if token_data.include?("warnings") + raise TokenError, response.warnings.join(", ") if response.warnings? - @tokens[type] = token_data["tokens"][type + "token"] + @tokens[type] = response.data["#{type}token"] end @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..21bc6e9 100644 --- a/lib/mediawiki_api/exceptions.rb +++ b/lib/mediawiki_api/exceptions.rb @@ -1,14 +1,27 @@ 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 message - "#{info} (#{code})" + def code + data["code"] + end + + def info + data["info"] + end + + def to_s + "#{self.class.name}: #{info} (#{code})" + end + + private + + def data + @response.data || {} end end diff --git a/lib/mediawiki_api/response.rb b/lib/mediawiki_api/response.rb new file mode 100644 index 0000000..f0e0090 --- /dev/null +++ b/lib/mediawiki_api/response.rb @@ -0,0 +1,88 @@ +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 + + # Set of warning messages from the response. + # + # @return [Array] + # + def warnings + if response_object["warnings"] + response_object["warnings"].values.map(&:values).flatten + else + [] + end + end + + # Whether the response contains warnings. + # + # @return [true, false] + # + def warnings? + !warnings.empty? + end + + private + + def open_envelope(obj, env = @envelope) + if !obj.is_a?(Hash) || env.nil? || env.empty? || !obj.include?(env.first) + obj + else + open_envelope(obj[env.first], env[1..-1]) + end + 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..1f47e16 --- /dev/null +++ b/spec/response_spec.rb @@ -0,0 +1,90 @@ +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 + + context "and a multi-level envelope that doesn't completely match" do + let(:envelope) { ["query", "something"] } + let(:partially_nested_object) { response_object["query"] } + + it { is_expected.to eq(partially_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: merged Gerrit-Change-Id: I500ef8e153b431d31492fc6a43ff63b54003a00e Gerrit-PatchSet: 8 Gerrit-Project: mediawiki/ruby/api Gerrit-Branch: master Gerrit-Owner: Dduvall <dduv...@wikimedia.org> Gerrit-Reviewer: Cmcmahon <cmcma...@wikimedia.org> Gerrit-Reviewer: Dduvall <dduv...@wikimedia.org> Gerrit-Reviewer: JGonera <jgon...@wikimedia.org> Gerrit-Reviewer: Zfilipin <zfili...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits