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

Reply via email to