payload-api 0.5.0 → 0.6.1

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.
@@ -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