1use bon::Builder;
81use schemars::JsonSchema;
82use serde::{Deserialize, Serialize};
83use x402_core::types::{AnyJson, ExtensionInfo};
84
85#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
90pub struct BazaarInfo {
91 pub input: BazaarInput,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub output: Option<BazaarOutput>,
97}
98
99impl ExtensionInfo for BazaarInfo {
100 const ID: &'static str = "bazaar";
101
102 fn schema() -> AnyJson {
103 let schema = schemars::schema_for!(BazaarInfo);
104 serde_json::to_value(&schema).expect("BazaarInfo schema generation should not fail")
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
113#[serde(tag = "type")]
114pub enum BazaarInput {
115 #[serde(rename = "http")]
117 Http(BazaarHttpInput),
118
119 #[serde(rename = "mcp")]
121 Mcp(BazaarMcpInput),
122}
123
124#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
129#[serde(rename_all = "camelCase")]
130pub struct BazaarHttpInput {
131 pub method: HttpMethod,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub query_params: Option<AnyJson>,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub headers: Option<AnyJson>,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
145 #[builder(into)]
146 pub body_type: Option<String>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub body: Option<AnyJson>,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
155pub enum HttpMethod {
156 GET,
158 HEAD,
160 DELETE,
162 POST,
164 PUT,
166 PATCH,
168}
169
170#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
174#[serde(rename_all = "camelCase")]
175pub struct BazaarMcpInput {
176 #[builder(into)]
178 pub tool: String,
179
180 pub input_schema: AnyJson,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 #[builder(into)]
186 pub description: Option<String>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
191 pub transport: Option<McpTransport>,
192
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub example: Option<AnyJson>,
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
200#[serde(rename_all = "kebab-case")]
201pub enum McpTransport {
202 StreamableHttp,
204 Sse,
206}
207
208#[derive(Builder, Debug, Clone, Serialize, Deserialize, JsonSchema)]
210pub struct BazaarOutput {
211 #[serde(rename = "type")]
213 #[builder(into)]
214 pub output_type: String,
215
216 #[serde(skip_serializing_if = "Option::is_none")]
218 #[builder(into)]
219 pub format: Option<String>,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub example: Option<AnyJson>,
224}
225
226#[cfg(test)]
227mod tests {
228 use serde_json::json;
229 use x402_core::types::{Extension, ExtensionMapInsert, Record};
230
231 use super::*;
232
233 #[test]
234 fn bazaar_get_endpoint() {
235 let info = BazaarInfo::builder()
236 .input(BazaarInput::Http(
237 BazaarHttpInput::builder()
238 .method(HttpMethod::GET)
239 .query_params(json!({"city": "San Francisco"}))
240 .build(),
241 ))
242 .output(
243 BazaarOutput::builder()
244 .output_type("json")
245 .example(json!({
246 "city": "San Francisco",
247 "weather": "foggy",
248 "temperature": 60
249 }))
250 .build(),
251 )
252 .build();
253
254 let ext = Extension::typed(info);
255 let (key, transport_ext) = ext.into_pair();
256
257 assert_eq!(key, "bazaar");
258
259 let info_json = &transport_ext.info;
260 assert_eq!(info_json["input"]["type"], "http");
261 assert_eq!(info_json["input"]["method"], "GET");
262 assert_eq!(
263 info_json["input"]["queryParams"],
264 json!({"city": "San Francisco"})
265 );
266 assert_eq!(info_json["output"]["type"], "json");
267 }
268
269 #[test]
270 fn bazaar_post_endpoint() {
271 let info = BazaarInfo::builder()
272 .input(BazaarInput::Http(
273 BazaarHttpInput::builder()
274 .method(HttpMethod::POST)
275 .body_type("json")
276 .body(json!({"query": "example"}))
277 .build(),
278 ))
279 .output(
280 BazaarOutput::builder()
281 .output_type("json")
282 .example(json!({"results": []}))
283 .build(),
284 )
285 .build();
286
287 let ext = Extension::typed(info);
288 let (key, transport_ext) = ext.into_pair();
289
290 assert_eq!(key, "bazaar");
291
292 let info_json = &transport_ext.info;
293 assert_eq!(info_json["input"]["type"], "http");
294 assert_eq!(info_json["input"]["method"], "POST");
295 assert_eq!(info_json["input"]["bodyType"], "json");
296 assert_eq!(info_json["input"]["body"], json!({"query": "example"}));
297 }
298
299 #[test]
300 fn bazaar_mcp_tool() {
301 let info = BazaarInfo::builder()
302 .input(BazaarInput::Mcp(
303 BazaarMcpInput::builder()
304 .tool("financial_analysis")
305 .input_schema(json!({
306 "type": "object",
307 "properties": {
308 "ticker": { "type": "string" },
309 "analysis_type": { "type": "string", "enum": ["quick", "deep"] }
310 },
311 "required": ["ticker"]
312 }))
313 .description("Advanced AI-powered financial analysis")
314 .example(json!({
315 "ticker": "AAPL",
316 "analysis_type": "deep"
317 }))
318 .build(),
319 ))
320 .output(
321 BazaarOutput::builder()
322 .output_type("json")
323 .example(json!({
324 "summary": "Strong fundamentals...",
325 "score": 8.5
326 }))
327 .build(),
328 )
329 .build();
330
331 let ext = Extension::typed(info);
332 let (key, transport_ext) = ext.into_pair();
333
334 assert_eq!(key, "bazaar");
335
336 let info_json = &transport_ext.info;
337 assert_eq!(info_json["input"]["type"], "mcp");
338 assert_eq!(info_json["input"]["tool"], "financial_analysis");
339 assert!(info_json["input"]["inputSchema"].is_object());
340 }
341
342 #[test]
343 fn bazaar_mcp_with_transport() {
344 let info = BazaarInfo::builder()
345 .input(BazaarInput::Mcp(
346 BazaarMcpInput::builder()
347 .tool("my_tool")
348 .input_schema(json!({"type": "object"}))
349 .transport(McpTransport::Sse)
350 .build(),
351 ))
352 .build();
353
354 let (_, ext) = Extension::typed(info).into_pair();
355 assert_eq!(ext.info["input"]["transport"], "sse");
356 }
357
358 #[test]
359 fn bazaar_schema_is_generated() {
360 let schema = <BazaarInfo as ExtensionInfo>::schema();
361 assert!(schema.is_object());
362 let schema_obj = schema.as_object().unwrap();
364 assert!(
365 schema_obj.contains_key("properties") || schema_obj.contains_key("$defs"),
366 "Schema should contain properties or definitions"
367 );
368 }
369
370 #[test]
371 fn bazaar_insert_into_extension_map() {
372 let mut extensions: Record<Extension> = Record::new();
373
374 extensions.insert_typed(Extension::typed(
375 BazaarInfo::builder()
376 .input(BazaarInput::Http(
377 BazaarHttpInput::builder().method(HttpMethod::GET).build(),
378 ))
379 .build(),
380 ));
381
382 assert!(extensions.contains_key("bazaar"));
383 assert_eq!(extensions["bazaar"].info["input"]["type"], "http");
384 assert_eq!(extensions["bazaar"].info["input"]["method"], "GET");
385 }
386
387 #[test]
388 fn bazaar_roundtrip_serialization() {
389 let info = BazaarInfo::builder()
390 .input(BazaarInput::Http(
391 BazaarHttpInput::builder()
392 .method(HttpMethod::POST)
393 .body_type("json")
394 .body(json!({"key": "value"}))
395 .headers(json!({"Authorization": "Bearer token"}))
396 .build(),
397 ))
398 .output(
399 BazaarOutput::builder()
400 .output_type("json")
401 .format("utf-8")
402 .build(),
403 )
404 .build();
405
406 let json = serde_json::to_value(&info).unwrap();
408 let deserialized: BazaarInfo = serde_json::from_value(json.clone()).unwrap();
409 let re_serialized = serde_json::to_value(&deserialized).unwrap();
410
411 assert_eq!(json, re_serialized);
412 }
413
414 #[test]
415 fn bazaar_transport_roundtrip() {
416 let info = BazaarInfo::builder()
417 .input(BazaarInput::Mcp(
418 BazaarMcpInput::builder()
419 .tool("test_tool")
420 .input_schema(json!({"type": "object"}))
421 .build(),
422 ))
423 .build();
424
425 let ext = Extension::typed(info);
426 let (key, transport_ext) = ext.into_pair();
427
428 let json = serde_json::to_value(&transport_ext).unwrap();
430
431 let deserialized: Extension = serde_json::from_value(json).unwrap();
433
434 assert_eq!(
435 transport_ext.info, deserialized.info,
436 "Info should roundtrip"
437 );
438 assert_eq!(
439 transport_ext.schema, deserialized.schema,
440 "Schema should roundtrip"
441 );
442 assert_eq!(key, "bazaar");
443 }
444}