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

Reply via email to