diff --git a/CHANGELOG.md b/CHANGELOG.md index 481a829..e5b298c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## Unreleased +### Added +- Added support for nested search fields using dot notation in Payrix search query builder (e.g. `merchant.entity.login.division`) + +### Changed +- Increased Payrix API timeout to 60 seconds to improve reliability for slower requests + ### Breaking Changes - Change API for setting request environment. diff --git a/Gemfile.lock b/Gemfile.lock index 99bbbfc..c94131a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - payrix (1.0.0) + payrix (1.1.0) faraday (~> 2.0.1) faraday-follow_redirects diff --git a/README.md b/README.md index d1ddd73..4d76afe 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,8 @@ Here is a list of all simple operators available. - `Payrix::Search.greater` - `Payrix::Search.less` +> Note: Field names can be provided as either symbols (`:status`) or dot-notation strings (`'merchant.entity.login.division'`) when querying nested attributes. + The interface of all operators follows `.operator(:field, value)`. Use compound operators to make combinations of simple and other compound operators. diff --git a/lib/payrix/http/request.rb b/lib/payrix/http/request.rb index 7c1d431..02e388a 100644 --- a/lib/payrix/http/request.rb +++ b/lib/payrix/http/request.rb @@ -11,7 +11,7 @@ module Http class Request # rubocop:disable Style/Documentation - Legacy file, which will be removed eventually include Singleton - def send_http(method, base_url, endpoint, data = {}, headers = {}, timeout = 30) + def send_http(method, base_url, endpoint, data = {}, headers = {}, timeout = 60) conn = Faraday.new(url: base_url) do |connection| connection.response :follow_redirects, limit: 3 diff --git a/lib/payrix/request_options/search/atom.rb b/lib/payrix/request_options/search/atom.rb index 69ae95f..145bbb1 100644 --- a/lib/payrix/request_options/search/atom.rb +++ b/lib/payrix/request_options/search/atom.rb @@ -22,12 +22,14 @@ def initialize(field, operator, value) def construct(prefix = '') raise ArgumentError, 'Prefix parameter must be a string' unless prefix.is_a?(String) - field = Payrix::Util.camel_case(@field.to_s) + formatted_field = rendered_field(prefix) - if prefix == '' - "#{field}[#{@operator}]=#{@value}" + if prefix.empty? + "#{formatted_field}[#{@operator}]=#{@value}" + elsif formatted_field.start_with?('[') + "#{prefix}#{formatted_field}[#{@operator}]=#{@value}" else - "#{prefix}[#{field}][#{@operator}]=#{@value}" + "#{prefix}[#{formatted_field}][#{@operator}]=#{@value}" end end @@ -35,9 +37,16 @@ def construct(prefix = '') def validate_field return if @field.is_a?(Symbol) - return if @field.is_a?(String) && @field != '' + raise ArgumentError, 'Field parameter must be a symbol or a non-empty string' unless valid_string_field? + raise ArgumentError, 'Field parameter must not contain empty dot notation segments' if empty_dot_segment? + end + + def valid_string_field? + @field.is_a?(String) && @field != '' + end - raise ArgumentError, 'Field parameter must be a symbol or a non-empty string' + def empty_dot_segment? + @field.include?('.') && @field.split('.').any?(&:empty?) end def validate_operator @@ -52,6 +61,20 @@ def validate_value raise ArgumentError, 'Value parameter must be a non-empty string' end + + def rendered_field(prefix) + return Payrix::Util.camel_case(@field.to_s) unless @field.is_a?(String) && @field.include?('.') + + dot_field(prefix.empty?) + end + + def dot_field(unprefixed) + parts = @field.split('.').map { |part| Payrix::Util.camel_case(part) } + + return parts.map { |part| "[#{part}]" }.join unless unprefixed + + [parts.first, *parts.drop(1).map { |part| "[#{part}]" }].join + end end end end diff --git a/lib/payrix/version.rb b/lib/payrix/version.rb index ea7aed4..f436739 100644 --- a/lib/payrix/version.rb +++ b/lib/payrix/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Payrix - VERSION = '1.0.0' + VERSION = '1.1.0' end diff --git a/spec/lib/payrix/request_options/search/atom_spec.rb b/spec/lib/payrix/request_options/search/atom_spec.rb index c9f4ac4..e67835b 100644 --- a/spec/lib/payrix/request_options/search/atom_spec.rb +++ b/spec/lib/payrix/request_options/search/atom_spec.rb @@ -38,6 +38,18 @@ end end + context 'when the field is dot notation with empty segments at the beginning' do + it 'raises ArgumentError' do + expect { described_class.new('.field', :operator, 'value') }.to raise_error(ArgumentError) + end + end + + context 'when the field is dot notation with empty segments at middle' do + it 'raises ArgumentError' do + expect { described_class.new('a..field', :operator, 'value') }.to raise_error(ArgumentError) + end + end + context 'when the operator is nil' do it 'raises ArgumentError' do expect { described_class.new(:field, nil, 'value') }.to raise_error(ArgumentError) @@ -251,5 +263,20 @@ expect { atom.construct({}) }.to raise_error(ArgumentError) end end + + context 'when the field is a dotted string' do + it 'returns a valid nested search argument' do + atom = described_class.new('a.field', :operator, 'value') + + expect(atom.construct).to eq('a[field][operator]=value') + end + end + + context 'when the field is a dotted string and the prefix is a non-empty string' do + it 'returns a valid nested search argument with a prefix' do + atom = described_class.new('a.field', :operator, 'value') + expect(atom.construct('prefix')).to eq('prefix[a][field][operator]=value') + end + end end end