payload-api 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/lib/payload/arm/attr.rb +169 -0
- data/lib/payload/arm/object.rb +37 -2
- data/lib/payload/arm/request.rb +53 -5
- data/lib/payload/arm/session.rb +8 -4
- data/lib/payload/exceptions.rb +15 -0
- data/lib/payload/objects.rb +10 -2
- data/lib/payload/version.rb +2 -2
- data/spec/payload/arm/arm_request_query_spec.rb +226 -0
- data/spec/payload/arm/attr_spec.rb +216 -0
- data/spec/payload/arm/object_spec.rb +114 -0
- data/spec/payload/arm/request_format_integration_spec.rb +166 -0
- data/spec/payload/arm/request_spec.rb +1 -1
- data/spec/payload/arm/session_spec.rb +40 -0
- data/spec/payload/exceptions_spec.rb +82 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74c2fd7f99fba4cf6b0f1950d013debc81c56629a5b280e7ac79e4e4abd88838
|
|
4
|
+
data.tar.gz: abf8b305b33a0e9a5241f5664807842989b7d423781f35606607298c9297d35a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0e34c4a1a585078565d876af179d8e9b3344888ca44a08f9465ac2e740f5f112990d8b7564b7c0394e21dc4eae22c9b5e0e804850c23cb39d5ffd789ad055319
|
|
7
|
+
data.tar.gz: ad8721f83c061113c6abcf4e26f7077e9c07b7a4d34b60d8cdaa9d4f61ad599443cc1e8511d46983d7acd368601b6596b42cd6cef9509dd3cc4a0a89c9492202
|
data/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Payload (http://payload.com)
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Payload
|
|
4
|
+
class ARMFilter
|
|
5
|
+
attr_reader :attr, :opval, :val
|
|
6
|
+
|
|
7
|
+
def initialize(attr, val)
|
|
8
|
+
@attr = attr.to_s
|
|
9
|
+
@val = val
|
|
10
|
+
@opval = self.class.op + val.to_s
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.op
|
|
14
|
+
''
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def |(other)
|
|
18
|
+
raise TypeError, 'invalid type' unless other.is_a?(Payload::ARMFilter)
|
|
19
|
+
raise ArgumentError, '`or` only works on the same attribute' if other.attr != @attr
|
|
20
|
+
joined = [@opval, other.opval].join('|')
|
|
21
|
+
Payload::ARMEqual.new(@attr, joined)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class ARMEqual < ARMFilter
|
|
26
|
+
def self.op
|
|
27
|
+
''
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class ARMNotEqual < ARMFilter
|
|
32
|
+
def self.op
|
|
33
|
+
'!'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class ARMGreaterThan < ARMFilter
|
|
38
|
+
def self.op
|
|
39
|
+
'>'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class ARMLessThan < ARMFilter
|
|
44
|
+
def self.op
|
|
45
|
+
'<'
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class ARMGreaterThanEqual < ARMFilter
|
|
50
|
+
def self.op
|
|
51
|
+
'>='
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class ARMLessThanEqual < ARMFilter
|
|
56
|
+
def self.op
|
|
57
|
+
'<='
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class ARMContains < ARMFilter
|
|
62
|
+
def self.op
|
|
63
|
+
'?*'
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Root proxy for pl.attr so that pl.attr.name returns an Attr (not Class#name).
|
|
68
|
+
# Session#attr returns this instead of the Attr class to avoid Class/Module methods (e.g. .name) shadowing attribute names.
|
|
69
|
+
class AttrRoot
|
|
70
|
+
def method_missing(name, *args)
|
|
71
|
+
if args.size == 1 && args[0].is_a?(Symbol)
|
|
72
|
+
inner = Attr.new(name.to_s)
|
|
73
|
+
Attr.new(args[0].to_s, inner).call
|
|
74
|
+
else
|
|
75
|
+
Attr.new(name.to_s)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def respond_to_missing?(name, include_private = false)
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Attribute DSL for select/group_by/order_by and filter expressions.
|
|
85
|
+
# - pl.attr.id -> "id"
|
|
86
|
+
# - pl.attr.created_at(:month) -> "month(created_at)"
|
|
87
|
+
# - pl.attr.amount(:sum) -> "sum(amount)"
|
|
88
|
+
# - pl.attr.sender.account_id -> "sender[account_id]"
|
|
89
|
+
class Attr
|
|
90
|
+
attr_reader :param, :parent
|
|
91
|
+
|
|
92
|
+
class << self
|
|
93
|
+
def method_missing(name, *args)
|
|
94
|
+
new(name.to_s)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def respond_to_missing?(name, include_private = false)
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def initialize(param, parent = nil)
|
|
103
|
+
@param = param.to_s
|
|
104
|
+
@parent = parent
|
|
105
|
+
@is_method = false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def key
|
|
109
|
+
@parent ? "#{@parent.key}[#{@param}]" : @param
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def to_s
|
|
113
|
+
@is_method ? "#{@param}(#{@parent.key})" : key
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def strip
|
|
117
|
+
to_s.strip
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def method_missing(name, *args)
|
|
121
|
+
raise "cannot get attr of method" if @is_method
|
|
122
|
+
|
|
123
|
+
if args.size == 1 && args[0].is_a?(Symbol)
|
|
124
|
+
inner = Attr.new(name.to_s, self)
|
|
125
|
+
Attr.new(args[0].to_s, inner).call
|
|
126
|
+
else
|
|
127
|
+
Attr.new(name.to_s, self)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def respond_to_missing?(name, include_private = false)
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Mark attribute as a function call (e.g. .month(), .sum())
|
|
136
|
+
def call
|
|
137
|
+
@is_method = true
|
|
138
|
+
self
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def ==(other)
|
|
142
|
+
ARMEqual.new(self, other)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def !=(other)
|
|
146
|
+
ARMNotEqual.new(self, other)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def >(other)
|
|
150
|
+
ARMGreaterThan.new(self, other)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def <(other)
|
|
154
|
+
ARMLessThan.new(self, other)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def >=(other)
|
|
158
|
+
ARMGreaterThanEqual.new(self, other)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def <=(other)
|
|
162
|
+
ARMLessThanEqual.new(self, other)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def contains(other)
|
|
166
|
+
ARMContains.new(self, other)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
data/lib/payload/arm/object.rb
CHANGED
|
@@ -29,11 +29,26 @@ module Payload
|
|
|
29
29
|
def get(id)
|
|
30
30
|
return @cls.get(id, :session => @session)
|
|
31
31
|
end
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
def select(*args, **data)
|
|
34
34
|
@cls.select(*args, **data, session: @session)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def order_by(*args, **data)
|
|
38
|
+
@cls.order_by(*args, **data, session: @session)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def limit(n, **data)
|
|
42
|
+
@cls.limit(n, **data, session: @session)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def offset(n, **data)
|
|
46
|
+
@cls.offset(n, **data, session: @session)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def group_by(*args, **data)
|
|
50
|
+
@cls.group_by(*args, **data, session: @session)
|
|
51
|
+
end
|
|
37
52
|
end
|
|
38
53
|
|
|
39
54
|
class ARMObject
|
|
@@ -103,7 +118,7 @@ module Payload
|
|
|
103
118
|
end
|
|
104
119
|
|
|
105
120
|
def [](key)
|
|
106
|
-
return @data[key]
|
|
121
|
+
return @data[key.to_s]
|
|
107
122
|
end
|
|
108
123
|
|
|
109
124
|
def _get_request()
|
|
@@ -118,6 +133,22 @@ module Payload
|
|
|
118
133
|
return self._get_request().select(*args, **data)
|
|
119
134
|
end
|
|
120
135
|
|
|
136
|
+
def self.order_by(*args, **data)
|
|
137
|
+
self._get_request().order_by(*args, **data)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.limit(n, **data)
|
|
141
|
+
self._get_request().limit(n)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.offset(n, **data)
|
|
145
|
+
self._get_request().offset(n)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.group_by(*args, **data)
|
|
149
|
+
self._get_request().group_by(*args, **data)
|
|
150
|
+
end
|
|
151
|
+
|
|
121
152
|
def self.filter_by(*args, **data)
|
|
122
153
|
session = data[:session]
|
|
123
154
|
data.delete(:session)
|
|
@@ -154,6 +185,10 @@ module Payload
|
|
|
154
185
|
return _get_request()._request('Delete', id: self.id)
|
|
155
186
|
end
|
|
156
187
|
|
|
188
|
+
def json
|
|
189
|
+
to_json
|
|
190
|
+
end
|
|
191
|
+
|
|
157
192
|
def to_json(*args)
|
|
158
193
|
serialized = {}
|
|
159
194
|
if self.class.poly
|
data/lib/payload/arm/request.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "payload/exceptions"
|
|
2
2
|
require "payload/utils"
|
|
3
|
+
require "payload/arm/attr"
|
|
3
4
|
require "net/http"
|
|
4
5
|
require "uri"
|
|
5
6
|
require "json"
|
|
@@ -12,21 +13,46 @@ module Payload
|
|
|
12
13
|
@cls = cls
|
|
13
14
|
@session = session || Payload::Session.new(Payload::api_key, Payload::api_url)
|
|
14
15
|
@filters = {}
|
|
16
|
+
@group_by = []
|
|
17
|
+
@order_by = []
|
|
18
|
+
@limit = nil
|
|
19
|
+
@offset = nil
|
|
20
|
+
@filter_objects = []
|
|
15
21
|
end
|
|
16
22
|
|
|
17
23
|
def select(*args, **data)
|
|
18
|
-
@filters['fields'] = args.map {|a| a.strip }.join(',')
|
|
19
|
-
|
|
24
|
+
@filters['fields'] = args.map { |a| a.strip }.join(',')
|
|
20
25
|
return self
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
def group_by(*args, **data)
|
|
29
|
+
@group_by.concat(args)
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def order_by(*args, **data)
|
|
34
|
+
@order_by.concat(args)
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def limit(n)
|
|
39
|
+
@limit = n
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def offset(n)
|
|
44
|
+
@offset = n
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
23
48
|
def filter_by(*args, **data)
|
|
49
|
+
args.each do |f|
|
|
50
|
+
@filter_objects << f if f.respond_to?(:attr) && f.respond_to?(:opval)
|
|
51
|
+
end
|
|
24
52
|
if !@cls.nil? && @cls.poly
|
|
25
53
|
data = data.merge(@cls.poly)
|
|
26
54
|
end
|
|
27
|
-
|
|
28
55
|
@filters = @filters.merge(data)
|
|
29
|
-
|
|
30
56
|
return self
|
|
31
57
|
end
|
|
32
58
|
|
|
@@ -35,6 +61,17 @@ module Payload
|
|
|
35
61
|
return self._request('Get')
|
|
36
62
|
end
|
|
37
63
|
|
|
64
|
+
def [](key)
|
|
65
|
+
case key
|
|
66
|
+
when Range
|
|
67
|
+
raise ArgumentError, 'Negative slice indices not supported' if key.begin && key.begin < 0
|
|
68
|
+
raise ArgumentError, 'Negative slice indices not supported' if key.end && key.end < 0
|
|
69
|
+
offset(key.begin).limit(key.size).all()
|
|
70
|
+
else
|
|
71
|
+
raise TypeError, "invalid key or index: #{key.inspect}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
38
75
|
def get(id)
|
|
39
76
|
if id.nil? || id.empty?
|
|
40
77
|
throw 'id cannot be empty'
|
|
@@ -127,6 +164,16 @@ module Payload
|
|
|
127
164
|
http.request(request)
|
|
128
165
|
end
|
|
129
166
|
|
|
167
|
+
def request_params
|
|
168
|
+
params = @filters.dup
|
|
169
|
+
@filter_objects.each { |f| params[f.attr] = f.opval }
|
|
170
|
+
@group_by.each_with_index { |v, i| params["group_by[#{i}]"] = v.to_s }
|
|
171
|
+
@order_by.each_with_index { |v, i| params["order_by[#{i}]"] = v.to_s }
|
|
172
|
+
params['limit'] = @limit.to_s if @limit
|
|
173
|
+
params['offset'] = @offset.to_s if @offset
|
|
174
|
+
params
|
|
175
|
+
end
|
|
176
|
+
|
|
130
177
|
def _request(method, id: nil, json: nil)
|
|
131
178
|
if !@cls.nil?
|
|
132
179
|
if @cls.spec.key?("endpoint")
|
|
@@ -146,8 +193,9 @@ module Payload
|
|
|
146
193
|
endpoint = File.join(endpoint, id)
|
|
147
194
|
end
|
|
148
195
|
|
|
196
|
+
params = request_params
|
|
149
197
|
url = URI.join(@session.api_url, endpoint)
|
|
150
|
-
url.query = URI.encode_www_form(
|
|
198
|
+
url.query = URI.encode_www_form(params)
|
|
151
199
|
|
|
152
200
|
http = Net::HTTP.new(url.host, url.port)
|
|
153
201
|
|
data/lib/payload/arm/session.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
require 'payload/arm/request'
|
|
2
|
-
|
|
2
|
+
require 'payload/arm/attr'
|
|
3
3
|
|
|
4
4
|
module Payload
|
|
5
5
|
class Session
|
|
6
6
|
attr_accessor :api_key, :api_url, :api_version
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
def initialize(api_key = nil, api_url = nil, api_version = nil, **kwargs)
|
|
9
9
|
@api_key = kwargs[:api_key] || api_key
|
|
10
10
|
@api_url = kwargs[:api_url] || api_url || Payload.URL
|
|
11
11
|
@api_version = kwargs[:api_version] || api_version || Payload.api_version
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
Payload.constants.each do |c|
|
|
14
14
|
val = Payload.const_get(c)
|
|
15
15
|
if val.is_a?(Class) && val < Payload::ARMObject
|
|
@@ -17,11 +17,15 @@ module Payload
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
def _get_request(cls = nil)
|
|
22
22
|
return Payload::ARMRequest.new(cls, self)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def attr
|
|
26
|
+
Payload::AttrRoot.new
|
|
27
|
+
end
|
|
28
|
+
|
|
25
29
|
def query(cls)
|
|
26
30
|
return self._get_request(cls)
|
|
27
31
|
end
|
data/lib/payload/exceptions.rb
CHANGED
|
@@ -18,6 +18,21 @@ module Payload
|
|
|
18
18
|
@code='400'
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
class TransactionDeclined < BadRequest
|
|
22
|
+
attr_reader :transaction
|
|
23
|
+
|
|
24
|
+
def initialize(msg, data = nil)
|
|
25
|
+
super(msg, data)
|
|
26
|
+
@transaction = if data && data['details'].is_a?(Hash)
|
|
27
|
+
cls = Payload.get_cls(data['details'])
|
|
28
|
+
cls = Payload::Transaction if cls.nil?
|
|
29
|
+
cls.new(data['details'], nil)
|
|
30
|
+
else
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
21
36
|
class InvalidAttributes < PayloadError
|
|
22
37
|
@code='400'
|
|
23
38
|
end
|
data/lib/payload/objects.rb
CHANGED
|
@@ -62,10 +62,18 @@ module Payload
|
|
|
62
62
|
@spec = { 'object' => 'invoice' }
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
class InvoiceAttachment < ARMObject
|
|
66
|
+
@spec = { 'object' => 'invoice_attachment' }
|
|
67
|
+
end
|
|
68
|
+
|
|
65
69
|
class PaymentActivation < ARMObject
|
|
66
70
|
@spec = { 'object' => 'payment_activation' }
|
|
67
71
|
end
|
|
68
72
|
|
|
73
|
+
class ProcessingSettings < ARMObject
|
|
74
|
+
@spec = { 'object' => 'processing_settings' }
|
|
75
|
+
end
|
|
76
|
+
|
|
69
77
|
class Webhook < ARMObject
|
|
70
78
|
@spec = { 'object' => 'webhook' }
|
|
71
79
|
end
|
|
@@ -96,7 +104,7 @@ module Payload
|
|
|
96
104
|
class ProcessingAccount < ARMObject
|
|
97
105
|
@spec = { 'object' => 'processing_account' }
|
|
98
106
|
end
|
|
99
|
-
|
|
107
|
+
|
|
100
108
|
class Org < ARMObject
|
|
101
109
|
@spec = { 'object' => 'org', 'endpoint' => '/account/orgs' }
|
|
102
110
|
end
|
|
@@ -123,7 +131,7 @@ module Payload
|
|
|
123
131
|
end
|
|
124
132
|
|
|
125
133
|
class Entity < ARMObject
|
|
126
|
-
@spec = { 'object' => 'entity' }
|
|
134
|
+
@spec = { 'object' => 'entity', 'endpoint' => 'entities' }
|
|
127
135
|
end
|
|
128
136
|
|
|
129
137
|
class Stakeholder < ARMObject
|
data/lib/payload/version.rb
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
module Payload
|
|
2
|
-
VERSION = '0.
|
|
3
|
-
end
|
|
2
|
+
VERSION = '0.6.0'
|
|
3
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "payload"
|
|
4
|
+
require "payload/arm/object"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Payload::ARMRequest do
|
|
7
|
+
describe "#group_by" do
|
|
8
|
+
let(:instance) { described_class.new(Payload::Invoice, nil) }
|
|
9
|
+
|
|
10
|
+
it "appends to group_by and returns self" do
|
|
11
|
+
year_attr = Payload::Attr.new("year", Payload::Attr.new("created_at"))
|
|
12
|
+
year_attr.call
|
|
13
|
+
result = instance.group_by(year_attr, Payload::Attr.status)
|
|
14
|
+
expect(result).to be(instance)
|
|
15
|
+
expect(instance.instance_variable_get(:@group_by).map(&:to_s)).to eq(["year(created_at)", "status"])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "includes group_by in request params when all() is called" do
|
|
19
|
+
Payload::api_key = "test_key"
|
|
20
|
+
instance.instance_variable_set(:@cls, Payload::Invoice)
|
|
21
|
+
|
|
22
|
+
expect(instance).to receive(:_execute_request) do |_http, request|
|
|
23
|
+
query = request.path.split("?")[1]
|
|
24
|
+
expect(query).to include("group_by%5B0%5D=") # group_by[0]=
|
|
25
|
+
expect(query).to include("group_by%5B1%5D=") # group_by[1]=
|
|
26
|
+
QuerySpecMockResponse.new('{"object":"list","values":[]}')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
instance.group_by("year(created_at)", "status").all()
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe "#order_by" do
|
|
34
|
+
let(:instance) { described_class.new(Payload::Account, nil) }
|
|
35
|
+
|
|
36
|
+
it "appends to order_by and returns self" do
|
|
37
|
+
result = instance.order_by("created_at", "desc(id)")
|
|
38
|
+
expect(result).to be(instance)
|
|
39
|
+
expect(instance.instance_variable_get(:@order_by)).to eq(["created_at", "desc(id)"])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "includes order_by in request params when all() is called" do
|
|
43
|
+
Payload::api_key = "test_key"
|
|
44
|
+
instance.instance_variable_set(:@cls, Payload::Account)
|
|
45
|
+
|
|
46
|
+
expect(instance).to receive(:_execute_request) do |_http, request|
|
|
47
|
+
query = request.path.split("?")[1]
|
|
48
|
+
expect(query).to include("order_by%5B0%5D=created_at")
|
|
49
|
+
expect(query).to include("order_by%5B1%5D=desc%28id%29")
|
|
50
|
+
QuerySpecMockResponse.new('{"object":"list","values":[]}')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
instance.order_by("created_at", "desc(id)").all()
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "#limit and #offset" do
|
|
58
|
+
let(:instance) { described_class.new(Payload::Account, nil) }
|
|
59
|
+
|
|
60
|
+
it "sets limit and offset and returns self" do
|
|
61
|
+
result = instance.limit(10).offset(20)
|
|
62
|
+
expect(result).to be(instance)
|
|
63
|
+
expect(instance.instance_variable_get(:@limit)).to eq(10)
|
|
64
|
+
expect(instance.instance_variable_get(:@offset)).to eq(20)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "includes limit and offset in request params when all() is called" do
|
|
68
|
+
Payload::api_key = "test_key"
|
|
69
|
+
instance.instance_variable_set(:@cls, Payload::Account)
|
|
70
|
+
|
|
71
|
+
expect(instance).to receive(:_execute_request) do |_http, request|
|
|
72
|
+
query = request.path.split("?")[1]
|
|
73
|
+
expect(query).to include("limit=10")
|
|
74
|
+
expect(query).to include("offset=20")
|
|
75
|
+
QuerySpecMockResponse.new('{"object":"list","values":[]}')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
instance.limit(10).offset(20).all()
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "#request_params" do
|
|
83
|
+
it "merges filters, filter_objects, group_by, order_by, limit, offset into query params" do
|
|
84
|
+
instance = described_class.new(Payload::Transaction, nil)
|
|
85
|
+
instance.instance_variable_set(:@filters, { "fields" => "id,amount" })
|
|
86
|
+
instance.instance_variable_set(:@group_by, ["status"])
|
|
87
|
+
instance.instance_variable_set(:@order_by, ["desc(created_at)"])
|
|
88
|
+
instance.instance_variable_set(:@limit, 5)
|
|
89
|
+
instance.instance_variable_set(:@offset, 10)
|
|
90
|
+
|
|
91
|
+
filter_obj = Payload::ARMGreaterThan.new(Payload::Attr.amount, 100)
|
|
92
|
+
instance.instance_variable_set(:@filter_objects, [filter_obj])
|
|
93
|
+
|
|
94
|
+
params = instance.request_params
|
|
95
|
+
|
|
96
|
+
expect(params["fields"]).to eq("id,amount")
|
|
97
|
+
expect(params["group_by[0]"]).to eq("status")
|
|
98
|
+
expect(params["order_by[0]"]).to eq("desc(created_at)")
|
|
99
|
+
expect(params["limit"]).to eq("5")
|
|
100
|
+
expect(params["offset"]).to eq("10")
|
|
101
|
+
expect(params[filter_obj.attr]).to eq(filter_obj.opval)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "is encoded as URL query string in _request (url.query = URI.encode_www_form(params))" do
|
|
105
|
+
Payload::api_key = "test_key"
|
|
106
|
+
session = Payload::Session.new("test_key", "https://api.test.com", "v2")
|
|
107
|
+
instance = described_class.new(Payload::Invoice, session)
|
|
108
|
+
instance.select("id", "status").filter_by(session.attr.status == "open").order_by("created_at").limit(5).offset(1)
|
|
109
|
+
|
|
110
|
+
expected_params = instance.request_params.dup
|
|
111
|
+
|
|
112
|
+
expect(instance).to receive(:_execute_request) do |_http, request|
|
|
113
|
+
query_str = request.path.split("?", 2)[1]
|
|
114
|
+
expect(query_str).not_to be_nil
|
|
115
|
+
decoded = URI.decode_www_form(query_str || "").to_h
|
|
116
|
+
expected_params.each do |key, value|
|
|
117
|
+
expect(decoded[key]).to eq(value.to_s)
|
|
118
|
+
end
|
|
119
|
+
expect(decoded).to include("fields" => "id,status", "limit" => "5", "offset" => "1")
|
|
120
|
+
expect(decoded.keys).to include("status", "order_by[0]")
|
|
121
|
+
QuerySpecMockResponse.new('{"object":"list","values":[]}')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
instance.all()
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
describe "#[] (slice)" do
|
|
129
|
+
let(:instance) { described_class.new(Payload::Account, nil) }
|
|
130
|
+
|
|
131
|
+
it "raises TypeError for non-Range key" do
|
|
132
|
+
expect { instance["foo"] }.to raise_error(TypeError, /invalid key or index/)
|
|
133
|
+
expect { instance[5] }.to raise_error(TypeError, /invalid key or index/)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "raises ArgumentError for negative begin" do
|
|
137
|
+
expect { instance[-1..10] }.to raise_error(ArgumentError, /Negative slice indices not supported/)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "raises ArgumentError for negative end" do
|
|
141
|
+
expect { instance[0..-5] }.to raise_error(ArgumentError, /Negative slice indices not supported/)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "calls offset(begin).limit(size).all() for a range and returns result" do
|
|
145
|
+
Payload::api_key = "test_key"
|
|
146
|
+
instance.instance_variable_set(:@cls, Payload::Account)
|
|
147
|
+
|
|
148
|
+
expect(instance).to receive(:_execute_request) do |_http, request|
|
|
149
|
+
query = request.path.split("?")[1]
|
|
150
|
+
expect(query).to include("offset=10")
|
|
151
|
+
expect(query).to include("limit=10")
|
|
152
|
+
QuerySpecMockResponse.new('{"object":"list","values":[{"id":"acct_1","object":"customer"}]}')
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
result = instance[10..19]
|
|
156
|
+
expect(result).to be_an(Array)
|
|
157
|
+
expect(result.size).to eq(1)
|
|
158
|
+
expect(result[0].id).to eq("acct_1")
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe "#filter_by with pl.attr filter objects" do
|
|
163
|
+
it "extracts ARM filter objects and merges their attr/opval into request params" do
|
|
164
|
+
session = Payload::Session.new("test_key", "https://api.test.com", "v2")
|
|
165
|
+
instance = described_class.new(Payload::Transaction, session)
|
|
166
|
+
pl = session
|
|
167
|
+
filter_expr = (pl.attr.amount > 100) | (pl.attr.amount < 200)
|
|
168
|
+
|
|
169
|
+
instance.filter_by(filter_expr)
|
|
170
|
+
|
|
171
|
+
expect(instance.instance_variable_get(:@filter_objects)).to include(be_a(Payload::ARMFilter))
|
|
172
|
+
params = instance.request_params
|
|
173
|
+
expect(params.keys).to include(filter_expr.attr)
|
|
174
|
+
expect(params[filter_expr.attr]).to eq(filter_expr.opval)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "builds GET request with filter object serialized in query string" do
|
|
178
|
+
Payload::api_key = "test_key"
|
|
179
|
+
session = Payload::Session.new("test_key", "https://api.test.com", "v2")
|
|
180
|
+
instance = described_class.new(Payload::Transaction, session)
|
|
181
|
+
filter_expr = session.attr.amount > 100
|
|
182
|
+
|
|
183
|
+
instance.filter_by(filter_expr)
|
|
184
|
+
|
|
185
|
+
expect(instance).to receive(:_execute_request) do |_http, request|
|
|
186
|
+
query = request.path.split("?")[1]
|
|
187
|
+
expect(query).to include("amount=")
|
|
188
|
+
expect(query).to include("%3E100") # URL-encoded ">100"
|
|
189
|
+
QuerySpecMockResponse.new('{"object":"list","values":[]}')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
instance.all()
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "#filter_by with multiple filters" do
|
|
197
|
+
it "accumulates multiple filter objects and merges keyword filters into @filters" do
|
|
198
|
+
instance = described_class.new(Payload::Invoice, nil)
|
|
199
|
+
f1 = Payload::ARMEqual.new(Payload::Attr.status, "open")
|
|
200
|
+
f2 = Payload::ARMGreaterThan.new(Payload::Attr.amount, 50)
|
|
201
|
+
|
|
202
|
+
instance.filter_by(f1).filter_by(f2).filter_by(custom_key: "value")
|
|
203
|
+
|
|
204
|
+
fo = instance.instance_variable_get(:@filter_objects)
|
|
205
|
+
expect(fo).to include(f1, f2)
|
|
206
|
+
params = instance.request_params
|
|
207
|
+
expect(params["status"]).to eq("open")
|
|
208
|
+
expect(params["amount"]).to eq(">50")
|
|
209
|
+
expect(instance.instance_variable_get(:@filters)[:custom_key]).to eq("value")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
class QuerySpecMockResponse
|
|
215
|
+
def initialize(body = '{"object":"list","values":[]}')
|
|
216
|
+
@body = body
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def code
|
|
220
|
+
"200"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def body
|
|
224
|
+
@body
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "payload"
|
|
4
|
+
require "payload/arm/attr"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Payload::AttrRoot do
|
|
7
|
+
let(:root) { described_class.new }
|
|
8
|
+
|
|
9
|
+
describe "attribute access" do
|
|
10
|
+
it "returns an Attr for any method name" do
|
|
11
|
+
expect(root.id).to be_a(Payload::Attr)
|
|
12
|
+
expect(root.id.to_s).to eq("id")
|
|
13
|
+
expect(root.created_at).to be_a(Payload::Attr)
|
|
14
|
+
expect(root.created_at.to_s).to eq("created_at")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "returns an Attr for chained property access (non-callable names stay as nested key)" do
|
|
18
|
+
chained = root.sender.account_id
|
|
19
|
+
expect(chained).to be_a(Payload::Attr)
|
|
20
|
+
expect(chained.to_s).to eq("sender[account_id]")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "treats no-arg access as nested attribute" do
|
|
24
|
+
expect(root.created_at.month.to_s).to eq("created_at[month]")
|
|
25
|
+
expect(root.created_at.year.to_s).to eq("created_at[year]")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "supports pl.attr.created_at(:month) style (function as symbol arg)" do
|
|
29
|
+
expect(root.created_at(:month).to_s).to eq("month(created_at)")
|
|
30
|
+
expect(root.created_at(:year).to_s).to eq("year(created_at)")
|
|
31
|
+
expect(root.amount(:sum).to_s).to eq("sum(amount)")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "supports nested attr with symbol: pl.attr.totals.total(:sum) => sum(totals[total])" do
|
|
35
|
+
expect(root.totals.total(:sum).to_s).to eq("sum(totals[total])")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
RSpec.describe Payload::Attr do
|
|
41
|
+
describe "simple attribute" do
|
|
42
|
+
it "has key and to_s as the param when no parent" do
|
|
43
|
+
attr = Payload::Attr.new("amount")
|
|
44
|
+
expect(attr.key).to eq("amount")
|
|
45
|
+
expect(attr.to_s).to eq("amount")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "nested attribute" do
|
|
50
|
+
it "builds key and to_s as parent[param]" do
|
|
51
|
+
parent = Payload::Attr.new("totals")
|
|
52
|
+
attr = Payload::Attr.new("total", parent)
|
|
53
|
+
expect(attr.key).to eq("totals[total]")
|
|
54
|
+
expect(attr.to_s).to eq("totals[total]")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "function form (after .call)" do
|
|
59
|
+
it "serializes as name(parent_key) when marked as method" do
|
|
60
|
+
parent = Payload::Attr.new("created_at")
|
|
61
|
+
attr = Payload::Attr.new("month", parent)
|
|
62
|
+
attr.call
|
|
63
|
+
expect(attr.to_s).to eq("month(created_at)")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "serializes nested path in function form" do
|
|
67
|
+
parent = Payload::Attr.new("total", Payload::Attr.new("totals"))
|
|
68
|
+
attr = Payload::Attr.new("sum", parent)
|
|
69
|
+
attr.call
|
|
70
|
+
expect(attr.to_s).to eq("sum(totals[total])")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "raises when chaining after a method" do
|
|
74
|
+
parent = Payload::Attr.new("created_at")
|
|
75
|
+
attr = Payload::Attr.new("month", parent)
|
|
76
|
+
attr.call
|
|
77
|
+
expect { attr.day }.to raise_error(RuntimeError, /cannot get attr of method/)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "#strip" do
|
|
82
|
+
it "returns to_s.strip so Attr works with request.select(*args, **data) and args.map(&:strip)" do
|
|
83
|
+
inner = Payload::Attr.new("created_at")
|
|
84
|
+
year_attr = Payload::Attr.new("year", inner)
|
|
85
|
+
year_attr.call
|
|
86
|
+
expect(year_attr.strip).to eq("year(created_at)")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe "comparisons (return ARMFilter subclasses)" do
|
|
91
|
+
let(:attr_amount) { Payload::Attr.new("amount") }
|
|
92
|
+
let(:attr_status) { Payload::Attr.new("status") }
|
|
93
|
+
|
|
94
|
+
it "== returns ARMEqual with correct attr and opval" do
|
|
95
|
+
f = attr_status == "processed"
|
|
96
|
+
expect(f).to be_a(Payload::ARMEqual)
|
|
97
|
+
expect(f.attr).to eq("status")
|
|
98
|
+
expect(f.opval).to eq("processed")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "!= returns ARMNotEqual with ! prefix" do
|
|
102
|
+
f = attr_status != "draft"
|
|
103
|
+
expect(f).to be_a(Payload::ARMNotEqual)
|
|
104
|
+
expect(f.attr).to eq("status")
|
|
105
|
+
expect(f.opval).to eq("!draft")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "> returns ARMGreaterThan" do
|
|
109
|
+
f = attr_amount > 100
|
|
110
|
+
expect(f).to be_a(Payload::ARMGreaterThan)
|
|
111
|
+
expect(f.attr).to eq("amount")
|
|
112
|
+
expect(f.opval).to eq(">100")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "< returns ARMLessThan" do
|
|
116
|
+
f = attr_amount < 500
|
|
117
|
+
expect(f).to be_a(Payload::ARMLessThan)
|
|
118
|
+
expect(f.opval).to eq("<500")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it ">= returns ARMGreaterThanEqual" do
|
|
122
|
+
f = attr_amount >= 100
|
|
123
|
+
expect(f).to be_a(Payload::ARMGreaterThanEqual)
|
|
124
|
+
expect(f.opval).to eq(">=100")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "<= returns ARMLessThanEqual" do
|
|
128
|
+
f = attr_amount <= 100
|
|
129
|
+
expect(f).to be_a(Payload::ARMLessThanEqual)
|
|
130
|
+
expect(f.opval).to eq("<=100")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "contains returns ARMContains with ?* op" do
|
|
134
|
+
attr = Payload::Attr.new("description")
|
|
135
|
+
f = attr.contains("INV -")
|
|
136
|
+
expect(f).to be_a(Payload::ARMContains)
|
|
137
|
+
expect(f.attr).to eq("description")
|
|
138
|
+
expect(f.opval).to eq("?*INV -")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "comparisons use Attr key for nested paths" do
|
|
142
|
+
nested = Payload::Attr.new("total", Payload::Attr.new("totals"))
|
|
143
|
+
f = nested > 0
|
|
144
|
+
expect(f.attr).to eq("totals[total]")
|
|
145
|
+
expect(f.opval).to eq(">0")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe "Attr class-level (Payload::Attr.name)" do
|
|
150
|
+
it "returns Attr via method_missing" do
|
|
151
|
+
expect(Payload::Attr.created_at).to be_a(Payload::Attr)
|
|
152
|
+
expect(Payload::Attr.created_at.to_s).to eq("created_at")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
RSpec.describe Payload::ARMFilter do
|
|
158
|
+
describe "#| (OR)" do
|
|
159
|
+
it "combines two filters on the same attribute and returns ARMEqual with joined opval" do
|
|
160
|
+
left = Payload::ARMGreaterThan.new(Payload::Attr.amount, 100)
|
|
161
|
+
right = Payload::ARMLessThan.new(Payload::Attr.amount, 50)
|
|
162
|
+
combined = left | right
|
|
163
|
+
expect(combined).to be_a(Payload::ARMEqual)
|
|
164
|
+
expect(combined.attr).to eq("amount")
|
|
165
|
+
expect(combined.opval).to eq(">100|<50")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "raises TypeError when other is not an ARMFilter" do
|
|
169
|
+
f = Payload::ARMEqual.new(Payload::Attr.status, "active")
|
|
170
|
+
expect { f | "invalid" }.to raise_error(TypeError, /invalid type/)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "raises ArgumentError when attributes differ" do
|
|
174
|
+
f1 = Payload::ARMEqual.new(Payload::Attr.status, "active")
|
|
175
|
+
f2 = Payload::ARMEqual.new(Payload::Attr.type, "payment")
|
|
176
|
+
expect { f1 | f2 }.to raise_error(ArgumentError, /only works on the same attribute/)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
describe "filter subclasses op values" do
|
|
181
|
+
it "ARMEqual has empty op" do
|
|
182
|
+
f = Payload::ARMEqual.new("status", "active")
|
|
183
|
+
expect(f.opval).to eq("active")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "ARMNotEqual has ! prefix" do
|
|
187
|
+
f = Payload::ARMNotEqual.new("status", "draft")
|
|
188
|
+
expect(f.opval).to eq("!draft")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it "ARMGreaterThan has > prefix" do
|
|
192
|
+
f = Payload::ARMGreaterThan.new("amount", 100)
|
|
193
|
+
expect(f.opval).to eq(">100")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "ARMLessThan has < prefix" do
|
|
197
|
+
f = Payload::ARMLessThan.new("amount", 200)
|
|
198
|
+
expect(f.opval).to eq("<200")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it "ARMGreaterThanEqual has >= prefix" do
|
|
202
|
+
f = Payload::ARMGreaterThanEqual.new("amount", 100)
|
|
203
|
+
expect(f.opval).to eq(">=100")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
it "ARMLessThanEqual has <= prefix" do
|
|
207
|
+
f = Payload::ARMLessThanEqual.new("amount", 100)
|
|
208
|
+
expect(f.opval).to eq("<=100")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "ARMContains has ?* prefix" do
|
|
212
|
+
f = Payload::ARMContains.new("email", "example.com")
|
|
213
|
+
expect(f.opval).to eq("?*example.com")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "payload"
|
|
4
|
+
require "payload/arm/object"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Payload::ARMObject do
|
|
7
|
+
describe "method_missing (attribute access)" do
|
|
8
|
+
let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
|
|
9
|
+
|
|
10
|
+
it "returns value for present key" do
|
|
11
|
+
obj = Payload::Invoice.new({ "id" => "inv_1", "amount" => 100 }, session)
|
|
12
|
+
expect(obj.id).to eq("inv_1")
|
|
13
|
+
expect(obj.amount).to eq(100)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "raises NoMethodError for missing key (strict behavior preserved)" do
|
|
17
|
+
obj = Payload::Invoice.new({ "id" => "inv_1" }, session)
|
|
18
|
+
expect { obj.nonexistent_key }.to raise_error(NoMethodError, /nonexistent_key/)
|
|
19
|
+
expect { obj.amount }.to raise_error(NoMethodError, /amount/)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "strips trailing = for setter-like names and returns value" do
|
|
23
|
+
obj = Payload::Account.new({ "name" => "Acme" }, session)
|
|
24
|
+
expect(obj.name).to eq("Acme")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "#json" do
|
|
29
|
+
let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
|
|
30
|
+
|
|
31
|
+
it "returns same as to_json" do
|
|
32
|
+
obj = Payload::Invoice.new({ "id" => "inv_1", "object" => "invoice" }, session)
|
|
33
|
+
expect(obj.json).to eq(obj.to_json)
|
|
34
|
+
expect(obj.json).to include("inv_1")
|
|
35
|
+
expect(obj.json).to include("invoice")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe ".order_by, .limit, .offset" do
|
|
40
|
+
it "delegates to ARMRequest and returns chainable request" do
|
|
41
|
+
req = Payload::Invoice.order_by("created_at")
|
|
42
|
+
expect(req).to be_a(Payload::ARMRequest)
|
|
43
|
+
expect(req.instance_variable_get(:@order_by)).to include("created_at")
|
|
44
|
+
|
|
45
|
+
req = Payload::Invoice.limit(10)
|
|
46
|
+
expect(req.instance_variable_get(:@limit)).to eq(10)
|
|
47
|
+
|
|
48
|
+
req = Payload::Invoice.offset(20)
|
|
49
|
+
expect(req.instance_variable_get(:@offset)).to eq(20)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe ".select" do
|
|
54
|
+
it "delegates to ARMRequest and sets fields filter" do
|
|
55
|
+
req = Payload::Invoice.select("id", "amount")
|
|
56
|
+
expect(req).to be_a(Payload::ARMRequest)
|
|
57
|
+
expect(req.instance_variable_get(:@filters)["fields"]).to eq("id,amount")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "#[] (bracket access)" do
|
|
62
|
+
let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
|
|
63
|
+
|
|
64
|
+
it "returns value for string key" do
|
|
65
|
+
obj = Payload::Invoice.new({ "id" => "inv_1", "status" => "open" }, session)
|
|
66
|
+
expect(obj["id"]).to eq("inv_1")
|
|
67
|
+
expect(obj["status"]).to eq("open")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "returns value for symbol key (data keys are stored as strings)" do
|
|
71
|
+
obj = Payload::Invoice.new({ "id" => "inv_1" }, session)
|
|
72
|
+
expect(obj[:id]).to eq("inv_1")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "returns nil for missing key" do
|
|
76
|
+
obj = Payload::Invoice.new({ "id" => "inv_1" }, session)
|
|
77
|
+
expect(obj["missing"]).to be_nil
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe "#to_json" do
|
|
82
|
+
let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
|
|
83
|
+
|
|
84
|
+
it "includes @data in JSON output" do
|
|
85
|
+
obj = Payload::Invoice.new({ "id" => "inv_1", "object" => "invoice", "amount" => 99 }, session)
|
|
86
|
+
json = obj.to_json
|
|
87
|
+
expect(json).to include("inv_1")
|
|
88
|
+
expect(json).to include("invoice")
|
|
89
|
+
expect(json).to include("99")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "merges class poly when present" do
|
|
93
|
+
obj = Payload::Payment.new({ "id" => "txn_1", "object" => "transaction", "type" => "payment" }, session)
|
|
94
|
+
json = obj.to_json
|
|
95
|
+
expect(json).to include("payment")
|
|
96
|
+
expect(json).to include("txn_1")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "#respond_to_missing?" do
|
|
101
|
+
let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
|
|
102
|
+
|
|
103
|
+
it "returns true for key present in data" do
|
|
104
|
+
obj = Payload::Invoice.new({ "id" => "inv_1", "amount" => 100 }, session)
|
|
105
|
+
expect(obj.respond_to?(:id)).to be true
|
|
106
|
+
expect(obj.respond_to?(:amount)).to be true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "returns false for key missing from data (so method_missing will call super)" do
|
|
110
|
+
obj = Payload::Invoice.new({ "id" => "inv_1" }, session)
|
|
111
|
+
expect(obj.respond_to?(:nonexistent_key)).to be false
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "payload"
|
|
4
|
+
require "payload/arm/object"
|
|
5
|
+
require_relative "../../support/helpers"
|
|
6
|
+
|
|
7
|
+
RSpec.describe "confirm request format is valid" do
|
|
8
|
+
include_context "test helpers"
|
|
9
|
+
|
|
10
|
+
[1, 2].each do |api_version|
|
|
11
|
+
context "API v#{api_version}" do
|
|
12
|
+
let(:session) { Payload::Session.new(Payload.api_key, Payload.api_url, api_version) }
|
|
13
|
+
let(:pl) { session }
|
|
14
|
+
|
|
15
|
+
context "date functions" do
|
|
16
|
+
it "filter with year(attr) ==" do
|
|
17
|
+
results = pl.Transaction.filter_by(
|
|
18
|
+
pl.attr.type == "payment",
|
|
19
|
+
pl.attr.created_at(:year) == Time.now.year
|
|
20
|
+
).limit(5).all
|
|
21
|
+
expect(results).to be_an(Array)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "filter with month(attr) ==" do
|
|
25
|
+
results = pl.Transaction.filter_by(
|
|
26
|
+
pl.attr.created_at(:month) == Time.now.month
|
|
27
|
+
).limit(5).all
|
|
28
|
+
expect(results).to be_an(Array)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "select with dayname(attr)" do
|
|
32
|
+
results = pl.Transaction.select(
|
|
33
|
+
pl.attr.id,
|
|
34
|
+
pl.attr.created_at(:dayname)
|
|
35
|
+
).limit(5).all
|
|
36
|
+
expect(results).to be_an(Array)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "group_by year(attr) with count" do
|
|
40
|
+
yearly = pl.Transaction.select(
|
|
41
|
+
pl.attr.created_at(:year),
|
|
42
|
+
pl.attr.id(:count)
|
|
43
|
+
).group_by(pl.attr.created_at(:year)).limit(5).all
|
|
44
|
+
expect(yearly).to be_an(Array)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context "order_by" do
|
|
49
|
+
it "order_by ascending" do
|
|
50
|
+
results = pl.Transaction.select(pl.attr.id).order_by(pl.attr.created_at).limit(5).all
|
|
51
|
+
expect(results).to be_an(Array)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "order_by descending" do
|
|
55
|
+
results = pl.Transaction.select(pl.attr.id).order_by(pl.attr.created_at(:desc)).limit(5).all
|
|
56
|
+
expect(results).to be_an(Array)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context "limit, offset, and range" do
|
|
61
|
+
it "limit" do
|
|
62
|
+
results = pl.Transaction.select(pl.attr.id).limit(2).all
|
|
63
|
+
expect(results).to be_an(Array)
|
|
64
|
+
expect(results.length).to be <= 2
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "offset" do
|
|
68
|
+
page1 = pl.Transaction.select(pl.attr.id).limit(3).offset(0).all
|
|
69
|
+
page2 = pl.Transaction.select(pl.attr.id).limit(3).offset(3).all
|
|
70
|
+
expect(page1).to be_an(Array)
|
|
71
|
+
expect(page2).to be_an(Array)
|
|
72
|
+
expect(page1.length).to be <= 3
|
|
73
|
+
expect(page2.length).to be <= 3
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "range operator []" do
|
|
77
|
+
results = pl.Transaction.select(pl.attr.id)[0..2]
|
|
78
|
+
expect(results).to be_an(Array)
|
|
79
|
+
expect(results.length).to be <= 3
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
context "nested attr" do
|
|
84
|
+
it "select with nested attr (sender.account_id)" do
|
|
85
|
+
results = pl.Transaction.select(
|
|
86
|
+
pl.attr.id,
|
|
87
|
+
pl.attr.sender.account_id
|
|
88
|
+
).filter_by(pl.attr.type == "payment").limit(5).all
|
|
89
|
+
expect(results).to be_an(Array)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context "filter operators" do
|
|
94
|
+
it "filter with >" do
|
|
95
|
+
results = pl.Transaction.filter_by(
|
|
96
|
+
pl.attr.type == "payment",
|
|
97
|
+
pl.attr.amount > 0
|
|
98
|
+
).select(pl.attr.id, pl.attr.amount).limit(5).all
|
|
99
|
+
expect(results).to be_an(Array)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "filter with <" do
|
|
103
|
+
results = pl.Transaction.filter_by(
|
|
104
|
+
pl.attr.type == "payment",
|
|
105
|
+
pl.attr.amount < 1_000_000
|
|
106
|
+
).select(pl.attr.id, pl.attr.amount).limit(5).all
|
|
107
|
+
expect(results).to be_an(Array)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "filter with !=" do
|
|
111
|
+
results = pl.Transaction.filter_by(
|
|
112
|
+
pl.attr.type != "nonexistent_type"
|
|
113
|
+
).select(pl.attr.id, pl.attr.type).limit(5).all
|
|
114
|
+
expect(results).to be_an(Array)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "filter with contains" do
|
|
118
|
+
results = pl.Transaction.filter_by(
|
|
119
|
+
pl.attr.type == "payment",
|
|
120
|
+
pl.attr.description.contains("")
|
|
121
|
+
).select(pl.attr.id, pl.attr.description).limit(5).all
|
|
122
|
+
expect(results).to be_an(Array)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
context "filter OR (chained conditions)" do
|
|
127
|
+
it "filter with OR (|) on same attribute" do
|
|
128
|
+
or_filter = (pl.attr.amount > 0) | (pl.attr.amount < 1_000_000)
|
|
129
|
+
results = pl.Transaction.filter_by(
|
|
130
|
+
pl.attr.type == "payment",
|
|
131
|
+
or_filter
|
|
132
|
+
).select(pl.attr.id, pl.attr.amount).limit(5).all
|
|
133
|
+
expect(results).to be_an(Array)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context "group_by and aggregates" do
|
|
138
|
+
it "group_by with sum and count" do
|
|
139
|
+
results = pl.Transaction.select(
|
|
140
|
+
pl.attr.type,
|
|
141
|
+
pl.attr.amount(:sum),
|
|
142
|
+
pl.attr.id(:count)
|
|
143
|
+
).filter_by(pl.attr.type == "payment").group_by(pl.attr.type).limit(5).all
|
|
144
|
+
expect(results).to be_an(Array)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "group_by month(attr) with sum and count" do
|
|
148
|
+
results = pl.Transaction.select(
|
|
149
|
+
pl.attr.created_at(:month),
|
|
150
|
+
pl.attr.amount(:sum),
|
|
151
|
+
pl.attr.id(:count)
|
|
152
|
+
).filter_by(pl.attr.type == "payment").group_by(pl.attr.created_at(:month)).limit(5).all
|
|
153
|
+
expect(results).to be_an(Array)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "group_by year(attr) with count" do
|
|
157
|
+
results = pl.Transaction.select(
|
|
158
|
+
pl.attr.created_at(:year),
|
|
159
|
+
pl.attr.id(:count)
|
|
160
|
+
).filter_by(pl.attr.type == "payment").group_by(pl.attr.created_at(:year)).limit(5).all
|
|
161
|
+
expect(results).to be_an(Array)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -11,7 +11,7 @@ RSpec.describe Payload::ARMRequest do
|
|
|
11
11
|
|
|
12
12
|
context "when the user selects custom fields" do
|
|
13
13
|
it "selects the requested fields" do
|
|
14
|
-
instance.select('
|
|
14
|
+
instance.select('name', 'age')
|
|
15
15
|
expect(instance.instance_variable_get(:@filters)).to eq({ "fields" => "name,age" })
|
|
16
16
|
instance.select('count(id)', 'sum(amount)')
|
|
17
17
|
expect(instance.instance_variable_get(:@filters)).to eq({ "fields" => "count(id),sum(amount)" })
|
|
@@ -24,6 +24,46 @@ RSpec.describe Payload::Session do
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
describe "#attr" do
|
|
28
|
+
it "returns an AttrRoot so pl.attr.name returns an Attr (not shadowed by Class#name)" do
|
|
29
|
+
instance = described_class.new("test_key", "https://api.hello.co")
|
|
30
|
+
root = instance.attr
|
|
31
|
+
|
|
32
|
+
expect(root).to be_a(Payload::AttrRoot)
|
|
33
|
+
expect(root.id).to be_a(Payload::Attr)
|
|
34
|
+
expect(root.id.to_s).to eq("id")
|
|
35
|
+
|
|
36
|
+
expect(root.created_at(:year)).to be_a(Payload::Attr)
|
|
37
|
+
expect(root.created_at(:year).to_s).to eq("year(created_at)")
|
|
38
|
+
|
|
39
|
+
expect(root.created_at.year.to_s).to eq("created_at[year]")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "query chaining with session" do
|
|
44
|
+
it "passes session through query -> select -> order_by -> limit chain" do
|
|
45
|
+
instance = described_class.new("session_key", "https://api.test.com", "v2")
|
|
46
|
+
req = instance.query(Payload::Invoice).select("id", "amount").order_by("created_at").limit(5)
|
|
47
|
+
|
|
48
|
+
expect(req).to be_a(Payload::ARMRequest)
|
|
49
|
+
expect(req.instance_variable_get(:@session)).to eq(instance)
|
|
50
|
+
expect(req.instance_variable_get(:@filters)["fields"]).to eq("id,amount")
|
|
51
|
+
expect(req.instance_variable_get(:@order_by)).to include("created_at")
|
|
52
|
+
expect(req.instance_variable_get(:@limit)).to eq(5)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "filter_by with session.attr uses AttrRoot from same session" do
|
|
56
|
+
instance = described_class.new("test_key", "https://api.test.com", "v2")
|
|
57
|
+
filter_expr = instance.attr.status == "processed"
|
|
58
|
+
req = instance.query(Payload::Transaction).filter_by(filter_expr)
|
|
59
|
+
|
|
60
|
+
expect(req.instance_variable_get(:@session)).to eq(instance)
|
|
61
|
+
expect(req.instance_variable_get(:@filter_objects)).to include(be_a(Payload::ARMEqual))
|
|
62
|
+
params = req.request_params
|
|
63
|
+
expect(params["status"]).to eq("processed")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
27
67
|
describe "#query" do
|
|
28
68
|
|
|
29
69
|
context "when the user queries an ARMObject with a session" do
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "payload"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Payload::TransactionDeclined do
|
|
6
|
+
def api_error_payload(message:, details: nil)
|
|
7
|
+
{
|
|
8
|
+
"object" => "error",
|
|
9
|
+
"error_type" => "TransactionDeclined",
|
|
10
|
+
"error_description" => message,
|
|
11
|
+
"details" => details,
|
|
12
|
+
}.compact
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "inherits from BadRequest (HTTP 400)" do
|
|
16
|
+
expect(described_class.superclass).to eq(Payload::BadRequest)
|
|
17
|
+
expect(Payload::BadRequest.code).to eq("400")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "sets message from first argument (same as request.rb: data['error_description'])" do
|
|
21
|
+
e = described_class.new("Card declined", nil)
|
|
22
|
+
expect(e.message).to eq("Card declined")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "exposes #transaction as nil when data is nil" do
|
|
26
|
+
e = described_class.new("Declined", nil)
|
|
27
|
+
expect(e.transaction).to be_nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "exposes #transaction as nil when details is missing" do
|
|
31
|
+
e = described_class.new("Declined", api_error_payload(message: "Declined"))
|
|
32
|
+
expect(e.transaction).to be_nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "exposes #transaction as nil when details is not a Hash" do
|
|
36
|
+
e = described_class.new("Declined", api_error_payload(message: "Declined", details: "string"))
|
|
37
|
+
expect(e.transaction).to be_nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "builds a transaction object from data['details'] when present (API-realistic payload)" do
|
|
41
|
+
details = {
|
|
42
|
+
"id" => "txn_123",
|
|
43
|
+
"object" => "transaction",
|
|
44
|
+
"type" => "payment",
|
|
45
|
+
"status" => "declined",
|
|
46
|
+
"status_code" => "do_not_honor",
|
|
47
|
+
"amount" => 100.0,
|
|
48
|
+
}
|
|
49
|
+
data = api_error_payload(message: "Transaction was declined", details: details)
|
|
50
|
+
e = described_class.new(data["error_description"], data)
|
|
51
|
+
|
|
52
|
+
expect(e.transaction).to be_a(Payload::ARMObject)
|
|
53
|
+
expect(e.transaction.id).to eq("txn_123")
|
|
54
|
+
expect(e.transaction["status"]).to eq("declined")
|
|
55
|
+
expect(e.transaction["status_code"]).to eq("do_not_honor")
|
|
56
|
+
expect(e.transaction["amount"]).to eq(100.0)
|
|
57
|
+
expect(e.transaction).to be_a(Payload::Payment)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "uses Transaction when get_cls returns nil for details (minimal details, no object key)" do
|
|
61
|
+
details = { "id" => "txn_456", "status" => "declined" }
|
|
62
|
+
data = api_error_payload(message: "Declined", details: details)
|
|
63
|
+
e = described_class.new(data["error_description"], data)
|
|
64
|
+
|
|
65
|
+
expect(e.transaction).to be_a(Payload::Transaction)
|
|
66
|
+
expect(e.transaction.id).to eq("txn_456")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "matches how ARM request raises: message from error_description, data = full response" do
|
|
70
|
+
data = {
|
|
71
|
+
"object" => "error",
|
|
72
|
+
"error_type" => "TransactionDeclined",
|
|
73
|
+
"error_description" => "There was an issue processing the payment",
|
|
74
|
+
"details" => { "id" => "txn_789", "object" => "transaction", "status" => "declined" },
|
|
75
|
+
}
|
|
76
|
+
e = described_class.new(data["error_description"], data)
|
|
77
|
+
|
|
78
|
+
expect(e.message).to eq("There was an issue processing the payment")
|
|
79
|
+
expect(e.transaction).to be_a(Payload::ARMObject)
|
|
80
|
+
expect(e.transaction.id).to eq("txn_789")
|
|
81
|
+
end
|
|
82
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: payload-api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Payload
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-02-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A simple library to interface with the Payload API. See https://docs.payload.com
|
|
14
14
|
for details.
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- LICENSE
|
|
23
23
|
- README.md
|
|
24
24
|
- lib/payload.rb
|
|
25
|
+
- lib/payload/arm/attr.rb
|
|
25
26
|
- lib/payload/arm/object.rb
|
|
26
27
|
- lib/payload/arm/request.rb
|
|
27
28
|
- lib/payload/arm/session.rb
|
|
@@ -43,8 +44,13 @@ files:
|
|
|
43
44
|
- spec/objects/v2/invoice_spec.rb
|
|
44
45
|
- spec/objects/v2/payment_method_spec.rb
|
|
45
46
|
- spec/objects/v2/transaction_spec.rb
|
|
47
|
+
- spec/payload/arm/arm_request_query_spec.rb
|
|
48
|
+
- spec/payload/arm/attr_spec.rb
|
|
49
|
+
- spec/payload/arm/object_spec.rb
|
|
50
|
+
- spec/payload/arm/request_format_integration_spec.rb
|
|
46
51
|
- spec/payload/arm/request_spec.rb
|
|
47
52
|
- spec/payload/arm/session_spec.rb
|
|
53
|
+
- spec/payload/exceptions_spec.rb
|
|
48
54
|
- spec/support/helpers.rb
|
|
49
55
|
- spec/support/helpers/v1_helpers.rb
|
|
50
56
|
- spec/support/helpers/v2_helpers.rb
|