diff --git a/lib/xero-ruby/api_client.rb b/lib/xero-ruby/api_client.rb index dc9d679a..14e3966d 100644 --- a/lib/xero-ruby/api_client.rb +++ b/lib/xero-ruby/api_client.rb @@ -95,6 +95,17 @@ def payroll_au_api XeroRuby::PayrollAuApi.new(self) end + # HAND-MAINTAINED: this accessor, the PayrollAuV2Api branches in `call_api` + # and `deserialize`, and `payroll_au_v2_url` in configuration.rb are not yet + # emitted by OpenAPI Generator. Regeneration overwrites this file (see commit + # d400b016), so any codegen bump must also update the generator templates or + # these are silently dropped and `payroll_au_v2_api` raises NoMethodError. + # spec/configuration_spec.rb and spec/api_client_spec.rb guard against that. + def payroll_au_v2_api + @config.base_url = @config.payroll_au_v2_url + XeroRuby::PayrollAuV2Api.new(self) + end + def payroll_nz_api @config.base_url = @config.payroll_nz_url XeroRuby::PayrollNzApi.new(self) @@ -289,6 +300,8 @@ def call_api(http_method, path, api_client, opts = {}) method_base_url = @config.files_url when "PayrollAuApi" method_base_url = @config.payroll_au_url + when "PayrollAuV2Api" + method_base_url = @config.payroll_au_v2_url when "PayrollNzApi" method_base_url = @config.payroll_nz_url when "PayrollUkApi" @@ -300,7 +313,9 @@ def call_api(http_method, path, api_client, opts = {}) when "FinanceApi" method_base_url = @config.finance_url else - method_base_url = @config.accounting_url + # `connections` and `disconnect` call through with no API class and set + # @config.base_url themselves, so honour it before falling back. + method_base_url = @config.base_url || @config.accounting_url end connection = Faraday.new(:url => method_base_url, :ssl => ssl_options) do |conn| @@ -314,7 +329,7 @@ def call_api(http_method, path, api_client, opts = {}) begin response = connection.public_send(http_method.to_sym.downcase) do |req| - build_request(http_method, path, req, opts) + build_request(http_method, path, req, opts, method_base_url) end if @config.debugging @@ -359,8 +374,8 @@ def return_error(response) # @option opts [Hash] :form_params Query parameters # @option opts [Object] :body HTTP body (JSON/XML) # @return A Faraday Request - def build_request(http_method, path, request, opts = {}) - url = build_request_url(path) + def build_request(http_method, path, request, opts = {}, base_url = nil) + url = build_request_url(path, base_url) http_method = http_method.to_sym.downcase header_params = @default_headers.merge(opts[:header_params] || {}) @@ -517,6 +532,8 @@ def convert_to_type(data, return_type, api_client) XeroRuby::Files.const_get(return_type).build_from_hash(data) when 'PayrollAuApi' XeroRuby::PayrollAu.const_get(return_type).build_from_hash(data) + when 'PayrollAuV2Api' + XeroRuby::PayrollAuV2.const_get(return_type).build_from_hash(data) when 'PayrollNzApi' XeroRuby::PayrollNz.const_get(return_type).build_from_hash(data) when 'PayrollUkApi' @@ -567,11 +584,16 @@ def sanitize_filename(filename) filename.gsub(/.*[\/\\]/, '') end - def build_request_url(path) - if @config.base_url + # `base_url` is resolved per call from the API class that issued the request. + # Falling back to @config.base_url would read shared mutable state that any + # later `client._api` call has already overwritten, which sends the + # request to the wrong API set while still deserialising into this one. + def build_request_url(path, base_url = nil) + base_url ||= @config.base_url + if base_url # Add leading and trailing slashes to path path = "/#{path}".gsub(/\/+/, '/') - @config.base_url + path + base_url + path else path end diff --git a/lib/xero-ruby/configuration.rb b/lib/xero-ruby/configuration.rb index 7710f557..b384c1cc 100644 --- a/lib/xero-ruby/configuration.rb +++ b/lib/xero-ruby/configuration.rb @@ -31,6 +31,10 @@ class Configuration attr_accessor :project_url attr_accessor :files_url attr_accessor :payroll_au_url + # HAND-MAINTAINED: not emitted by OpenAPI Generator. Regeneration overwrites + # this file, so a codegen bump must also update the generator templates. + # Guarded by spec/configuration_spec.rb. + attr_accessor :payroll_au_v2_url attr_accessor :payroll_nz_url attr_accessor :payroll_uk_url attr_accessor :app_store_url @@ -149,6 +153,7 @@ def initialize @project_url = 'https://api.xero.com/projects.xro/2.0/' @files_url = 'https://api.xero.com/files.xro/1.0/' @payroll_au_url = 'https://api.xero.com/payroll.xro/1.0/' + @payroll_au_v2_url = 'https://api.xero.com/payroll.xro/2.0/' @payroll_nz_url = 'https://api.xero.com/payroll.xro/2.0/' @payroll_uk_url = 'https://api.xero.com/payroll.xro/2.0/' @app_store_url = 'https://api.xero.com/appstore/2.0/' diff --git a/spec/api_client_spec.rb b/spec/api_client_spec.rb index 617433b7..6169e549 100644 --- a/spec/api_client_spec.rb +++ b/spec/api_client_spec.rb @@ -168,6 +168,9 @@ api_client.payroll_au_api expect(api_client.config.base_url).to eq('https://api.xero.com/payroll.xro/1.0/') + + expect(api_client.payroll_au_v2_api).to be_a(XeroRuby::PayrollAuV2Api) + expect(api_client.config.base_url).to eq('https://api.xero.com/payroll.xro/2.0/') api_client.payroll_nz_api expect(api_client.config.base_url).to eq('https://api.xero.com/payroll.xro/2.0/') @@ -221,6 +224,16 @@ expect(data).to be_instance_of(Hash) expect(data).to eq(:message => 'Hello') end + + it 'deserializes Payroll AU v2 models in their own namespace' do + api_client = XeroRuby::ApiClient.new + headers = { 'Content-Type' => 'application/json' } + response = double('response', headers: headers, body: '{}') + + data = api_client.deserialize(response, 'TimesheetObject', 'PayrollAuV2Api') + + expect(data).to be_instance_of(XeroRuby::PayrollAuV2::TimesheetObject) + end end describe "#object_to_hash modifies a hash from snake_case to PascalCase" do @@ -553,4 +566,31 @@ end end end + + # Regression guards for hand-maintained Payroll AU v2 wiring. OpenAPI Generator + # does not emit these and regeneration overwrites api_client.rb, so if a codegen + # bump drops them these examples fail instead of the SDK silently losing v2. + describe 'payroll AU v2 wiring' do + let(:api_client) { XeroRuby::ApiClient.new } + + it 'exposes the payroll_au_v2_api accessor' do + expect(api_client).to respond_to(:payroll_au_v2_api) + expect(api_client.payroll_au_v2_api).to be_a(XeroRuby::PayrollAuV2Api) + end + + it 'resolves the request url from the calling API class, not the last-set base_url' do + api_client.payroll_au_v2_api + api_client.payroll_au_api + expect(api_client.config.base_url).to eq('https://api.xero.com/payroll.xro/1.0/') + + url = api_client.build_request_url('/Timesheets', api_client.config.payroll_au_v2_url) + expect(url).to start_with('https://api.xero.com/payroll.xro/2.0/') + expect(url).not_to start_with('https://api.xero.com/payroll.xro/1.0/') + end + + it 'still falls back to the shared base_url when no per-call base url is given' do + api_client.config.base_url = 'https://api.xero.com' + expect(api_client.build_request_url('/connections/')).to eq('https://api.xero.com/connections/') + end + end end diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb index 3daa2666..590de41f 100644 --- a/spec/configuration_spec.rb +++ b/spec/configuration_spec.rb @@ -12,12 +12,29 @@ expect(config.project_url).to eq('https://api.xero.com/projects.xro/2.0/') expect(config.files_url).to eq('https://api.xero.com/files.xro/1.0/') expect(config.payroll_au_url).to eq('https://api.xero.com/payroll.xro/1.0/') + expect(config.payroll_au_v2_url).to eq('https://api.xero.com/payroll.xro/2.0/') expect(config.payroll_nz_url).to eq('https://api.xero.com/payroll.xro/2.0/') expect(config.payroll_uk_url).to eq('https://api.xero.com/payroll.xro/2.0/') expect(config.finance_url).to eq('https://api.xero.com/finance.xro/1.0/') end end + # Regression guard for hand-maintained Payroll AU v2 wiring. OpenAPI Generator + # does not emit payroll_au_v2_url and regeneration overwrites configuration.rb, + # so a codegen bump that drops it fails here instead of only surfacing later as + # a NoMethodError from ApiClient#payroll_au_v2_api. + describe 'payroll_au_v2_url' do + it 'is exposed as a reader and a writer' do + expect(config).to respond_to(:payroll_au_v2_url) + expect(config).to respond_to(:payroll_au_v2_url=) + end + + it 'defaults to the payroll 2.0 base url and not the v1 one' do + expect(config.payroll_au_v2_url).to eq('https://api.xero.com/payroll.xro/2.0/') + expect(config.payroll_au_v2_url).not_to eq(config.payroll_au_url) + end + end + describe 'config' do it 'should apply the default configuration options' do client = XeroRuby::ApiClient.new(credentials: {})