agent2agent 1.0.8 → 1.1.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/lib/a2a/agent.rb +165 -117
- data/lib/a2a/client.rb +470 -51
- data/lib/a2a/errors/json_rpc_error.rb +71 -0
- data/lib/a2a/errors/rest_error.rb +68 -0
- data/lib/a2a/errors.rb +535 -0
- data/lib/a2a/faraday/middleware/json_rpc/request.rb +96 -0
- data/lib/a2a/faraday/middleware/json_rpc/response.rb +131 -0
- data/lib/a2a/faraday/middleware/rest/request.rb +166 -0
- data/lib/a2a/faraday/middleware/rest/response.rb +144 -0
- data/lib/a2a/faraday/middleware/schema_request.rb +69 -0
- data/lib/a2a/middleware/extract_message.rb +120 -0
- data/lib/a2a/middleware/fetch_task.rb +228 -0
- data/lib/a2a/middleware/limit_history_length.rb +123 -0
- data/lib/a2a/middleware/limit_pagination_size.rb +133 -0
- data/lib/a2a/middleware/sse_stream.rb +235 -0
- data/lib/a2a/middleware.rb +7 -0
- data/lib/a2a/schema/definition.rb +35 -1
- data/lib/a2a/schema.rb +126 -0
- data/lib/a2a/{bindings → server/bindings}/json_rpc.rb +12 -8
- data/lib/a2a/{bindings → server/bindings}/rest.rb +12 -8
- data/lib/a2a/server/dispatcher.rb +52 -54
- data/lib/a2a/server/env.rb +4 -6
- data/lib/a2a/server/triage.rb +1 -1
- data/lib/a2a/server.rb +10 -10
- data/lib/a2a/sse/event_parser.rb +202 -0
- data/lib/a2a/sse/json_rpc_stream.rb +27 -5
- data/lib/a2a/sse/rest_stream.rb +17 -5
- data/lib/a2a/sse/stream.rb +135 -7
- data/lib/a2a/sse.rb +1 -0
- data/lib/a2a/test_helpers.rb +89 -0
- data/lib/a2a/version.rb +1 -1
- data/lib/a2a.rb +6 -2
- data/lib/traces/provider/a2a/{bindings → server/bindings}/json_rpc.rb +2 -2
- data/lib/traces/provider/a2a/{bindings → server/bindings}/rest.rb +2 -2
- data/lib/traces/provider/a2a.rb +2 -2
- metadata +49 -22
- data/lib/a2a/server/cancel_task.rb +0 -14
- data/lib/a2a/server/create_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/delete_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/get_extended_agent_card.rb +0 -15
- data/lib/a2a/server/get_task.rb +0 -14
- data/lib/a2a/server/get_task_push_notification_config.rb +0 -14
- data/lib/a2a/server/list_task_push_notification_configs.rb +0 -14
- data/lib/a2a/server/list_tasks.rb +0 -14
- data/lib/a2a/server/send_message.rb +0 -14
- data/lib/a2a/server/send_streaming_message.rb +0 -14
- data/lib/a2a/server/subscribe_to_task.rb +0 -14
- data/lib/a2a/store/processor.rb +0 -136
- data/lib/a2a/store/pub_sub.rb +0 -149
- data/lib/a2a/store/sqlite.rb +0 -533
- data/lib/a2a/store/webhooks.rb +0 -94
- data/lib/a2a/store.rb +0 -6
- data/lib/a2a/task_store.rb +0 -315
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "a2a"
|
|
5
|
+
|
|
6
|
+
module A2A
|
|
7
|
+
module Middleware
|
|
8
|
+
# Loads a task from the store and places it on `env["a2a.task"]`.
|
|
9
|
+
# Sets `env["a2a.task"]` to nil when the ID is absent or the task
|
|
10
|
+
# does not exist. Does not raise.
|
|
11
|
+
#
|
|
12
|
+
# The task ID is read from the request — by default from `request.id`.
|
|
13
|
+
# Use `id_field:` to read from a different field, and `from:` to
|
|
14
|
+
# read from a nested object on the request.
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
#
|
|
18
|
+
# on "SendMessage" do
|
|
19
|
+
# use A2A::Middleware::FetchTask, store: sqlite_store, id_field: :task_id, from: :message
|
|
20
|
+
# respond_with -> (env) {
|
|
21
|
+
# existing = env["a2a.task"] # nil if new task
|
|
22
|
+
# }
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
class FetchTask
|
|
26
|
+
def initialize(app, store:, id_field: :id, from: nil)
|
|
27
|
+
@app = app
|
|
28
|
+
@store = store
|
|
29
|
+
@id_field = id_field
|
|
30
|
+
@from = from
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def call(env)
|
|
34
|
+
request = env["a2a.request"]
|
|
35
|
+
source = @from ? request.public_send(@from) : request
|
|
36
|
+
id = source.public_send(@id_field)
|
|
37
|
+
|
|
38
|
+
env["a2a.task"] = id.to_s.empty? ? nil : @store.get(id)
|
|
39
|
+
|
|
40
|
+
@app.call(env)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Loads a task from the store and places it on `env["a2a.task"]`.
|
|
45
|
+
# Raises `A2A::TaskNotFoundError` if the task does not exist.
|
|
46
|
+
#
|
|
47
|
+
# The task ID is read from the request — by default from `request.id`.
|
|
48
|
+
# Use `id_field:` to read from a different field, and `from:` to
|
|
49
|
+
# read from a nested object on the request.
|
|
50
|
+
#
|
|
51
|
+
# Usage:
|
|
52
|
+
#
|
|
53
|
+
# on "GetTask" do
|
|
54
|
+
# use A2A::Middleware::FetchTaskOrRaise, store: sqlite_store
|
|
55
|
+
# respond_with -> (env) {
|
|
56
|
+
# task = env["a2a.task"]
|
|
57
|
+
# }
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# on "GetTaskPushNotificationConfig" do
|
|
61
|
+
# use A2A::Middleware::FetchTaskOrRaise, store: sqlite_store, id_field: :task_id
|
|
62
|
+
# respond_with -> (env) { ... }
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
class FetchTaskOrRaise
|
|
66
|
+
def initialize(app, store:, id_field: :id, from: nil)
|
|
67
|
+
@app = app
|
|
68
|
+
@store = store
|
|
69
|
+
@id_field = id_field
|
|
70
|
+
@from = from
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def call(env)
|
|
74
|
+
request = env["a2a.request"]
|
|
75
|
+
source = @from ? request.public_send(@from) : request
|
|
76
|
+
id = source.public_send(@id_field)
|
|
77
|
+
|
|
78
|
+
task = @store.get(id)
|
|
79
|
+
raise A2A::TaskNotFoundError.new(id) unless task
|
|
80
|
+
|
|
81
|
+
env["a2a.task"] = task
|
|
82
|
+
|
|
83
|
+
@app.call(env)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
test do
|
|
90
|
+
describe "A2A::Middleware::FetchTask" do
|
|
91
|
+
before do
|
|
92
|
+
@store = Object.new
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "sets env[\"a2a.task\"] when task exists" do
|
|
96
|
+
task_data = { id: "t-1", state: "TASK_STATE_COMPLETED" }
|
|
97
|
+
@store.define_singleton_method(:get) { |id| id == "t-1" ? task_data : nil }
|
|
98
|
+
|
|
99
|
+
request = Object.new
|
|
100
|
+
request.define_singleton_method(:id) { "t-1" }
|
|
101
|
+
|
|
102
|
+
downstream = -> (env) { env["a2a.task"] }
|
|
103
|
+
mw = A2A::Middleware::FetchTask.new(downstream, store: @store)
|
|
104
|
+
env = { "a2a.request" => request }
|
|
105
|
+
|
|
106
|
+
mw.call(env).should == task_data
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "sets env[\"a2a.task\"] to nil when task does not exist" do
|
|
110
|
+
@store.define_singleton_method(:get) { |id| nil }
|
|
111
|
+
|
|
112
|
+
request = Object.new
|
|
113
|
+
request.define_singleton_method(:id) { "missing" }
|
|
114
|
+
|
|
115
|
+
downstream = -> (env) { env["a2a.task"] }
|
|
116
|
+
mw = A2A::Middleware::FetchTask.new(downstream, store: @store)
|
|
117
|
+
env = { "a2a.request" => request }
|
|
118
|
+
|
|
119
|
+
mw.call(env).should.be.nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "sets env[\"a2a.task\"] to nil when id is empty" do
|
|
123
|
+
request = Object.new
|
|
124
|
+
request.define_singleton_method(:id) { "" }
|
|
125
|
+
|
|
126
|
+
downstream = -> (env) { env["a2a.task"] }
|
|
127
|
+
mw = A2A::Middleware::FetchTask.new(downstream, store: @store)
|
|
128
|
+
env = { "a2a.request" => request }
|
|
129
|
+
|
|
130
|
+
mw.call(env).should.be.nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "sets env[\"a2a.task\"] to nil when id is nil" do
|
|
134
|
+
message = Object.new
|
|
135
|
+
message.define_singleton_method(:task_id) { nil }
|
|
136
|
+
|
|
137
|
+
request = Object.new
|
|
138
|
+
request.define_singleton_method(:message) { message }
|
|
139
|
+
|
|
140
|
+
downstream = -> (env) { env["a2a.task"] }
|
|
141
|
+
mw = A2A::Middleware::FetchTask.new(downstream, store: @store, id_field: :task_id, from: :message)
|
|
142
|
+
env = { "a2a.request" => request }
|
|
143
|
+
|
|
144
|
+
mw.call(env).should.be.nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "reads id from a nested object via from:" do
|
|
148
|
+
task_data = { id: "t-2", state: "TASK_STATE_WORKING" }
|
|
149
|
+
@store.define_singleton_method(:get) { |id| id == "t-2" ? task_data : nil }
|
|
150
|
+
|
|
151
|
+
message = Object.new
|
|
152
|
+
message.define_singleton_method(:task_id) { "t-2" }
|
|
153
|
+
|
|
154
|
+
request = Object.new
|
|
155
|
+
request.define_singleton_method(:message) { message }
|
|
156
|
+
|
|
157
|
+
downstream = -> (env) { env["a2a.task"] }
|
|
158
|
+
mw = A2A::Middleware::FetchTask.new(downstream, store: @store, id_field: :task_id, from: :message)
|
|
159
|
+
env = { "a2a.request" => request }
|
|
160
|
+
|
|
161
|
+
mw.call(env).should == task_data
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe "A2A::Middleware::FetchTaskOrRaise" do
|
|
166
|
+
before do
|
|
167
|
+
@store = Object.new
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "sets env[\"a2a.task\"] when task exists" do
|
|
171
|
+
task_data = { id: "t-1", state: "TASK_STATE_COMPLETED" }
|
|
172
|
+
@store.define_singleton_method(:get) { |id| id == "t-1" ? task_data : nil }
|
|
173
|
+
|
|
174
|
+
request = Object.new
|
|
175
|
+
request.define_singleton_method(:id) { "t-1" }
|
|
176
|
+
|
|
177
|
+
downstream = -> (env) { env["a2a.task"] }
|
|
178
|
+
mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store)
|
|
179
|
+
env = { "a2a.request" => request }
|
|
180
|
+
|
|
181
|
+
mw.call(env).should == task_data
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "raises TaskNotFoundError when task does not exist" do
|
|
185
|
+
@store.define_singleton_method(:get) { |id| nil }
|
|
186
|
+
|
|
187
|
+
request = Object.new
|
|
188
|
+
request.define_singleton_method(:id) { "missing" }
|
|
189
|
+
|
|
190
|
+
downstream = -> (env) { :should_not_reach }
|
|
191
|
+
mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store)
|
|
192
|
+
env = { "a2a.request" => request }
|
|
193
|
+
|
|
194
|
+
lambda { mw.call(env) }.should.raise(A2A::TaskNotFoundError)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "reads task_id when id_field: :task_id is specified" do
|
|
198
|
+
task_data = { id: "t-2", state: "TASK_STATE_WORKING" }
|
|
199
|
+
@store.define_singleton_method(:get) { |id| id == "t-2" ? task_data : nil }
|
|
200
|
+
|
|
201
|
+
request = Object.new
|
|
202
|
+
request.define_singleton_method(:task_id) { "t-2" }
|
|
203
|
+
|
|
204
|
+
downstream = -> (env) { env["a2a.task"] }
|
|
205
|
+
mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store, id_field: :task_id)
|
|
206
|
+
env = { "a2a.request" => request }
|
|
207
|
+
|
|
208
|
+
mw.call(env).should == task_data
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "reads id from a nested object via from:" do
|
|
212
|
+
task_data = { id: "t-3", state: "TASK_STATE_WORKING" }
|
|
213
|
+
@store.define_singleton_method(:get) { |id| id == "t-3" ? task_data : nil }
|
|
214
|
+
|
|
215
|
+
message = Object.new
|
|
216
|
+
message.define_singleton_method(:task_id) { "t-3" }
|
|
217
|
+
|
|
218
|
+
request = Object.new
|
|
219
|
+
request.define_singleton_method(:message) { message }
|
|
220
|
+
|
|
221
|
+
downstream = -> (env) { env["a2a.task"] }
|
|
222
|
+
mw = A2A::Middleware::FetchTaskOrRaise.new(downstream, store: @store, id_field: :task_id, from: :message)
|
|
223
|
+
env = { "a2a.request" => request }
|
|
224
|
+
|
|
225
|
+
mw.call(env).should == task_data
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "a2a"
|
|
5
|
+
|
|
6
|
+
module A2A
|
|
7
|
+
module Middleware
|
|
8
|
+
# Resolves the effective history length limit and sets
|
|
9
|
+
# `env["a2a.history_length"]` to an integer.
|
|
10
|
+
#
|
|
11
|
+
# The server max is required. The effective limit is the *minimum*
|
|
12
|
+
# of the client-requested value and the server cap. When the client
|
|
13
|
+
# doesn't specify a history_length, the server cap is used.
|
|
14
|
+
#
|
|
15
|
+
# `env["a2a.history_length"]` is always an Integer (0..max).
|
|
16
|
+
# The handler applies it unconditionally:
|
|
17
|
+
#
|
|
18
|
+
# result["history"] = task[:history]&.last(limit)
|
|
19
|
+
#
|
|
20
|
+
# Usage:
|
|
21
|
+
#
|
|
22
|
+
# on "GetTask" do
|
|
23
|
+
# use A2A::Middleware::FetchTaskOrRaise, store: sqlite_store
|
|
24
|
+
# use A2A::Middleware::LimitHistoryLength, 20
|
|
25
|
+
# respond_with -> (env) {
|
|
26
|
+
# task = env["a2a.task"]
|
|
27
|
+
# limit = env["a2a.history_length"]
|
|
28
|
+
# # ...
|
|
29
|
+
# result["history"] = task[:history]&.last(limit)
|
|
30
|
+
# }
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
class LimitHistoryLength
|
|
34
|
+
def initialize(app, max)
|
|
35
|
+
@app = app
|
|
36
|
+
@max = max
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call(env)
|
|
40
|
+
request = env["a2a.request"]
|
|
41
|
+
limit = @max
|
|
42
|
+
|
|
43
|
+
if request.respond_to?(:history_length) && request.history_length
|
|
44
|
+
limit = [request.history_length.to_i, @max].min
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
env["a2a.history_length"] = limit
|
|
48
|
+
|
|
49
|
+
@app.call(env)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
test do
|
|
56
|
+
describe "A2A::Middleware::LimitHistoryLength" do
|
|
57
|
+
it "defaults to server max when client does not specify" do
|
|
58
|
+
request = Object.new
|
|
59
|
+
request.define_singleton_method(:history_length) { nil }
|
|
60
|
+
|
|
61
|
+
downstream = -> (env) { env["a2a.history_length"] }
|
|
62
|
+
mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
|
|
63
|
+
env = { "a2a.request" => request }
|
|
64
|
+
|
|
65
|
+
mw.call(env).should == 20
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "uses client value when smaller than max" do
|
|
69
|
+
request = Object.new
|
|
70
|
+
request.define_singleton_method(:history_length) { 5 }
|
|
71
|
+
|
|
72
|
+
downstream = -> (env) { env["a2a.history_length"] }
|
|
73
|
+
mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
|
|
74
|
+
env = { "a2a.request" => request }
|
|
75
|
+
|
|
76
|
+
mw.call(env).should == 5
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "caps client value to server max" do
|
|
80
|
+
request = Object.new
|
|
81
|
+
request.define_singleton_method(:history_length) { 100 }
|
|
82
|
+
|
|
83
|
+
downstream = -> (env) { env["a2a.history_length"] }
|
|
84
|
+
mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
|
|
85
|
+
env = { "a2a.request" => request }
|
|
86
|
+
|
|
87
|
+
mw.call(env).should == 20
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "returns 0 when client requests 0" do
|
|
91
|
+
request = Object.new
|
|
92
|
+
request.define_singleton_method(:history_length) { 0 }
|
|
93
|
+
|
|
94
|
+
downstream = -> (env) { env["a2a.history_length"] }
|
|
95
|
+
mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
|
|
96
|
+
env = { "a2a.request" => request }
|
|
97
|
+
|
|
98
|
+
mw.call(env).should == 0
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "handles string values" do
|
|
102
|
+
request = Object.new
|
|
103
|
+
request.define_singleton_method(:history_length) { "3" }
|
|
104
|
+
|
|
105
|
+
downstream = -> (env) { env["a2a.history_length"] }
|
|
106
|
+
mw = A2A::Middleware::LimitHistoryLength.new(downstream, 20)
|
|
107
|
+
env = { "a2a.request" => request }
|
|
108
|
+
|
|
109
|
+
mw.call(env).should == 3
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "always returns an integer" do
|
|
113
|
+
request = Object.new
|
|
114
|
+
request.define_singleton_method(:history_length) { nil }
|
|
115
|
+
|
|
116
|
+
downstream = -> (env) { env["a2a.history_length"] }
|
|
117
|
+
mw = A2A::Middleware::LimitHistoryLength.new(downstream, 10)
|
|
118
|
+
env = { "a2a.request" => request }
|
|
119
|
+
|
|
120
|
+
mw.call(env).should.be.kind_of(Integer)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "a2a"
|
|
5
|
+
|
|
6
|
+
module A2A
|
|
7
|
+
module Middleware
|
|
8
|
+
# Clamps `request.page_size` to a valid range and sets
|
|
9
|
+
# `env["a2a.page_size"]` for downstream handlers.
|
|
10
|
+
#
|
|
11
|
+
# Accepts a single integer — the maximum page size (also used as the
|
|
12
|
+
# default when the client doesn't specify one). Clamps to [1, max].
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
#
|
|
16
|
+
# on "ListTasks" do
|
|
17
|
+
# use A2A::Middleware::LimitPaginationSize, 50
|
|
18
|
+
# respond_with -> (env) {
|
|
19
|
+
# page_size = env["a2a.page_size"]
|
|
20
|
+
# # ...
|
|
21
|
+
# }
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
class LimitPaginationSize
|
|
25
|
+
def initialize(app, max = 100)
|
|
26
|
+
@app = app
|
|
27
|
+
@max = max
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(env)
|
|
31
|
+
request = env["a2a.request"]
|
|
32
|
+
|
|
33
|
+
page_size = @max
|
|
34
|
+
if request.respond_to?(:page_size) && request.page_size
|
|
35
|
+
ps = request.page_size.to_i
|
|
36
|
+
page_size = [[ps, 1].max, @max].min
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
env["a2a.page_size"] = page_size
|
|
40
|
+
|
|
41
|
+
@app.call(env)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
test do
|
|
48
|
+
describe "A2A::Middleware::LimitPaginationSize" do
|
|
49
|
+
it "uses max as default page size when not specified by client" do
|
|
50
|
+
request = Object.new
|
|
51
|
+
request.define_singleton_method(:page_size) { nil }
|
|
52
|
+
|
|
53
|
+
downstream = -> (env) { env["a2a.page_size"] }
|
|
54
|
+
mw = A2A::Middleware::LimitPaginationSize.new(downstream)
|
|
55
|
+
env = { "a2a.request" => request }
|
|
56
|
+
|
|
57
|
+
result = mw.call(env)
|
|
58
|
+
result.should == 100
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "uses a custom max as the default" do
|
|
62
|
+
request = Object.new
|
|
63
|
+
request.define_singleton_method(:page_size) { nil }
|
|
64
|
+
|
|
65
|
+
downstream = -> (env) { env["a2a.page_size"] }
|
|
66
|
+
mw = A2A::Middleware::LimitPaginationSize.new(downstream, 25)
|
|
67
|
+
env = { "a2a.request" => request }
|
|
68
|
+
|
|
69
|
+
result = mw.call(env)
|
|
70
|
+
result.should == 25
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "clamps to minimum of 1" do
|
|
74
|
+
request = Object.new
|
|
75
|
+
request.define_singleton_method(:page_size) { 0 }
|
|
76
|
+
|
|
77
|
+
downstream = -> (env) { env["a2a.page_size"] }
|
|
78
|
+
mw = A2A::Middleware::LimitPaginationSize.new(downstream)
|
|
79
|
+
env = { "a2a.request" => request }
|
|
80
|
+
|
|
81
|
+
result = mw.call(env)
|
|
82
|
+
result.should == 1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "clamps to the max" do
|
|
86
|
+
request = Object.new
|
|
87
|
+
request.define_singleton_method(:page_size) { 999 }
|
|
88
|
+
|
|
89
|
+
downstream = -> (env) { env["a2a.page_size"] }
|
|
90
|
+
mw = A2A::Middleware::LimitPaginationSize.new(downstream)
|
|
91
|
+
env = { "a2a.request" => request }
|
|
92
|
+
|
|
93
|
+
result = mw.call(env)
|
|
94
|
+
result.should == 100
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "clamps to a custom max" do
|
|
98
|
+
request = Object.new
|
|
99
|
+
request.define_singleton_method(:page_size) { 999 }
|
|
100
|
+
|
|
101
|
+
downstream = -> (env) { env["a2a.page_size"] }
|
|
102
|
+
mw = A2A::Middleware::LimitPaginationSize.new(downstream, 50)
|
|
103
|
+
env = { "a2a.request" => request }
|
|
104
|
+
|
|
105
|
+
result = mw.call(env)
|
|
106
|
+
result.should == 50
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "passes through a valid page size" do
|
|
110
|
+
request = Object.new
|
|
111
|
+
request.define_singleton_method(:page_size) { 30 }
|
|
112
|
+
|
|
113
|
+
downstream = -> (env) { env["a2a.page_size"] }
|
|
114
|
+
mw = A2A::Middleware::LimitPaginationSize.new(downstream)
|
|
115
|
+
env = { "a2a.request" => request }
|
|
116
|
+
|
|
117
|
+
result = mw.call(env)
|
|
118
|
+
result.should == 30
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "handles string values" do
|
|
122
|
+
request = Object.new
|
|
123
|
+
request.define_singleton_method(:page_size) { "20" }
|
|
124
|
+
|
|
125
|
+
downstream = -> (env) { env["a2a.page_size"] }
|
|
126
|
+
mw = A2A::Middleware::LimitPaginationSize.new(downstream)
|
|
127
|
+
env = { "a2a.request" => request }
|
|
128
|
+
|
|
129
|
+
result = mw.call(env)
|
|
130
|
+
result.should == 20
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|