1use std::collections::BTreeMap;
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
20pub struct ApiResponse<T: Default> {
21 pub data: T,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub includes: Option<Includes>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
29 pub meta: Option<ResponseMeta>,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub errors: Option<Vec<ApiError>>,
33 #[serde(flatten)]
36 pub extra: BTreeMap<String, Value>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
41pub struct Includes {
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub users: Option<Vec<User>>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub tweets: Option<Vec<Tweet>>,
48 #[serde(flatten)]
50 pub extra: BTreeMap<String, Value>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
55pub struct ResponseMeta {
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub result_count: Option<u64>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub next_token: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub previous_token: Option<String>,
66 #[serde(flatten)]
68 pub extra: BTreeMap<String, Value>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
73pub struct ApiError {
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub message: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub title: Option<String>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub detail: Option<String>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub r#type: Option<String>,
86 #[serde(flatten)]
88 pub extra: BTreeMap<String, Value>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
98pub struct Tweet {
99 pub id: String,
101 pub text: String,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub created_at: Option<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub author_id: Option<String>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub conversation_id: Option<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub in_reply_to_user_id: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub public_metrics: Option<TweetPublicMetrics>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub referenced_tweets: Option<Vec<ReferencedTweet>>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub entities: Option<Value>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub attachments: Option<Value>,
127 #[serde(flatten)]
129 pub extra: BTreeMap<String, Value>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
134pub struct TweetPublicMetrics {
135 #[serde(default)]
137 pub retweet_count: u64,
138 #[serde(default)]
140 pub reply_count: u64,
141 #[serde(default)]
143 pub like_count: u64,
144 #[serde(default)]
146 pub quote_count: u64,
147 #[serde(default)]
149 pub bookmark_count: u64,
150 #[serde(default)]
152 pub impression_count: u64,
153 #[serde(flatten)]
155 pub extra: BTreeMap<String, Value>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
160pub struct ReferencedTweet {
161 pub id: String,
163 pub r#type: String,
165 #[serde(flatten)]
167 pub extra: BTreeMap<String, Value>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
177pub struct User {
178 pub id: String,
180 pub name: String,
182 pub username: String,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub created_at: Option<String>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub description: Option<String>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub verified: Option<bool>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub profile_image_url: Option<String>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub public_metrics: Option<UserPublicMetrics>,
199 #[serde(flatten)]
201 pub extra: BTreeMap<String, Value>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
206pub struct UserPublicMetrics {
207 #[serde(default)]
209 pub followers_count: u64,
210 #[serde(default)]
212 pub following_count: u64,
213 #[serde(default)]
215 pub tweet_count: u64,
216 #[serde(default)]
218 pub listed_count: u64,
219 #[serde(flatten)]
221 pub extra: BTreeMap<String, Value>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
228pub struct DmEvent {
229 pub id: String,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub text: Option<String>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub event_type: Option<String>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub created_at: Option<String>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub dm_conversation_id: Option<String>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub sender_id: Option<String>,
246 #[serde(flatten)]
248 pub extra: BTreeMap<String, Value>,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
255pub struct LikedResult {
256 pub liked: bool,
258 #[serde(flatten)]
260 pub extra: BTreeMap<String, Value>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
265pub struct FollowingResult {
266 pub following: bool,
268 #[serde(flatten)]
270 pub extra: BTreeMap<String, Value>,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
275pub struct DeletedResult {
276 pub deleted: bool,
278 #[serde(flatten)]
280 pub extra: BTreeMap<String, Value>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
285pub struct RetweetedResult {
286 pub retweeted: bool,
288 #[serde(flatten)]
290 pub extra: BTreeMap<String, Value>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
295pub struct BookmarkedResult {
296 pub bookmarked: bool,
298 #[serde(flatten)]
300 pub extra: BTreeMap<String, Value>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
305pub struct BlockingResult {
306 pub blocking: bool,
308 #[serde(flatten)]
310 pub extra: BTreeMap<String, Value>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
315pub struct MutingResult {
316 pub muting: bool,
318 #[serde(flatten)]
320 pub extra: BTreeMap<String, Value>,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
327pub struct MediaUploadResponse {
328 pub id: String,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub media_key: Option<String>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub expires_after_secs: Option<u64>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub processing_info: Option<MediaProcessingInfo>,
339 #[serde(flatten)]
341 pub extra: BTreeMap<String, Value>,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
346pub struct MediaProcessingInfo {
347 pub state: String,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
351 pub check_after_secs: Option<u64>,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub progress_percent: Option<u64>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub error: Option<Value>,
358 #[serde(flatten)]
360 pub extra: BTreeMap<String, Value>,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
370pub struct UsageData {
371 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub project_cap: Option<String>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub project_id: Option<String>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub project_usage: Option<String>,
380 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub cap_reset_day: Option<u64>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
385 pub daily_project_usage: Option<Value>,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
388 pub daily_client_app_usage: Option<Value>,
389 #[serde(flatten)]
391 pub extra: BTreeMap<String, Value>,
392}
393
394pub fn deserialize_response<T: Default + serde::de::DeserializeOwned>(
407 value: Value,
408) -> crate::error::Result<ApiResponse<T>> {
409 if value.as_object().is_some_and(|m| m.is_empty()) {
410 return Err(crate::error::XurlError::Json(
411 "empty response body — expected JSON with a \"data\" field".to_string(),
412 ));
413 }
414 if let Some(obj) = value.as_object()
419 && !obj.contains_key("data")
420 && obj.contains_key("errors")
421 {
422 return Err(crate::error::XurlError::validation(value.to_string()));
423 }
424 Ok(serde_json::from_value(value)?)
425}
426
427#[cfg(test)]
428mod tests {
429 use serde_json::json;
430
431 use super::*;
432
433 #[test]
436 fn deserialize_single_tweet() {
437 let json = json!({
438 "data": {
439 "id": "123",
440 "text": "Hello world",
441 "created_at": "2026-01-01T00:00:00.000Z",
442 "public_metrics": {
443 "retweet_count": 5,
444 "reply_count": 2,
445 "like_count": 10,
446 "quote_count": 1,
447 "bookmark_count": 0,
448 "impression_count": 100
449 }
450 }
451 });
452 let resp: ApiResponse<Tweet> =
453 serde_json::from_value(json).expect("Tweet response must deserialize");
454 assert_eq!(resp.data.id, "123");
455 assert_eq!(resp.data.text, "Hello world");
456 assert_eq!(
457 resp.data.created_at.as_deref(),
458 Some("2026-01-01T00:00:00.000Z")
459 );
460 let metrics = resp
461 .data
462 .public_metrics
463 .expect("public_metrics must be present");
464 assert_eq!(metrics.like_count, 10);
465 assert_eq!(metrics.impression_count, 100);
466 }
467
468 #[test]
469 fn deserialize_tweet_list() {
470 let json = json!({
471 "data": [
472 {"id": "1", "text": "first"},
473 {"id": "2", "text": "second"}
474 ],
475 "meta": {"result_count": 2}
476 });
477 let resp: ApiResponse<Vec<Tweet>> =
478 serde_json::from_value(json).expect("Tweet list response must deserialize");
479 assert_eq!(resp.data.len(), 2);
480 assert_eq!(resp.data[0].id, "1");
481 assert_eq!(resp.data[1].text, "second");
482 assert_eq!(
483 resp.meta.expect("meta must be present").result_count,
484 Some(2)
485 );
486 }
487
488 #[test]
489 fn deserialize_action_liked() {
490 let json = json!({"data": {"liked": true}});
491 let resp: ApiResponse<LikedResult> =
492 serde_json::from_value(json).expect("LikedResult response must deserialize");
493 assert!(resp.data.liked);
494 }
495
496 #[test]
497 fn deserialize_with_includes_and_meta() {
498 let json = json!({
499 "data": [{"id": "1", "text": "tweet"}],
500 "includes": {
501 "users": [{"id": "42", "name": "Bot", "username": "bot"}]
502 },
503 "meta": {"result_count": 1, "next_token": "abc123"}
504 });
505 let resp: ApiResponse<Vec<Tweet>> =
506 serde_json::from_value(json).expect("Tweet list with includes/meta must deserialize");
507 let includes = resp.includes.expect("includes must be present");
508 let users = includes.users.expect("includes.users must be present");
509 assert_eq!(users[0].id, "42");
510 assert_eq!(
511 resp.meta
512 .expect("meta must be present")
513 .next_token
514 .as_deref(),
515 Some("abc123")
516 );
517 }
518
519 #[test]
520 fn deserialize_user() {
521 let json = json!({
522 "data": {
523 "id": "42",
524 "name": "Test User",
525 "username": "testuser",
526 "verified": true,
527 "public_metrics": {
528 "followers_count": 100,
529 "following_count": 50,
530 "tweet_count": 1000,
531 "listed_count": 5
532 }
533 }
534 });
535 let resp: ApiResponse<User> =
536 serde_json::from_value(json).expect("User response must deserialize");
537 assert_eq!(resp.data.id, "42");
538 assert_eq!(resp.data.username, "testuser");
539 assert_eq!(resp.data.verified, Some(true));
540 let metrics = resp
541 .data
542 .public_metrics
543 .expect("user.public_metrics must be present");
544 assert_eq!(metrics.followers_count, 100);
545 }
546
547 #[test]
548 fn deserialize_dm_event() {
549 let json = json!({
550 "data": {
551 "id": "dm1",
552 "text": "hello",
553 "event_type": "MessageCreate",
554 "dm_conversation_id": "conv1",
555 "sender_id": "42"
556 }
557 });
558 let resp: ApiResponse<DmEvent> =
559 serde_json::from_value(json).expect("DmEvent response must deserialize");
560 assert_eq!(resp.data.id, "dm1");
561 assert_eq!(resp.data.text.as_deref(), Some("hello"));
562 assert_eq!(resp.data.sender_id.as_deref(), Some("42"));
563 }
564
565 #[test]
566 fn deserialize_usage_data() {
567 let json = json!({
568 "data": {
569 "project_cap": "2000000",
570 "project_id": "123",
571 "project_usage": "399",
572 "cap_reset_day": 19
573 }
574 });
575 let resp: ApiResponse<UsageData> =
576 serde_json::from_value(json).expect("UsageData response must deserialize");
577 assert_eq!(resp.data.project_cap.as_deref(), Some("2000000"));
578 assert_eq!(resp.data.cap_reset_day, Some(19));
579 }
580
581 #[test]
582 fn deserialize_media_upload_response() {
583 let json = json!({
584 "data": {
585 "id": "media_123",
586 "media_key": "key_456",
587 "expires_after_secs": 3600
588 }
589 });
590 let resp: ApiResponse<MediaUploadResponse> =
591 serde_json::from_value(json).expect("MediaUploadResponse must deserialize");
592 assert_eq!(resp.data.id, "media_123");
593 assert_eq!(resp.data.media_key.as_deref(), Some("key_456"));
594 assert_eq!(resp.data.expires_after_secs, Some(3600));
595 }
596
597 #[test]
598 fn deserialize_media_with_processing_info() {
599 let json = json!({
600 "data": {
601 "id": "media_123",
602 "processing_info": {
603 "state": "in_progress",
604 "check_after_secs": 5,
605 "progress_percent": 45
606 }
607 }
608 });
609 let resp: ApiResponse<MediaUploadResponse> = serde_json::from_value(json)
610 .expect("MediaUploadResponse with processing_info must deserialize");
611 let info = resp
612 .data
613 .processing_info
614 .expect("processing_info must be present");
615 assert_eq!(info.state, "in_progress");
616 assert_eq!(info.check_after_secs, Some(5));
617 assert_eq!(info.progress_percent, Some(45));
618 }
619
620 #[test]
623 fn unknown_fields_captured_in_extra() {
624 let json = json!({
625 "data": {
626 "id": "123",
627 "text": "hello",
628 "brand_new_field": "surprise"
629 },
630 "top_level_extra": 42
631 });
632 let resp: ApiResponse<Tweet> = serde_json::from_value(json)
633 .expect("Tweet response with unknown fields must deserialize");
634 assert_eq!(resp.data.extra["brand_new_field"], "surprise");
635 assert_eq!(resp.extra["top_level_extra"], 42);
636 }
637
638 #[test]
639 fn unknown_fields_round_trip() {
640 let json = json!({
641 "data": {
642 "id": "123",
643 "text": "hello",
644 "new_field": 42
645 },
646 "top_extra": "value"
647 });
648 let resp: ApiResponse<Tweet> =
649 serde_json::from_value(json).expect("Tweet round-trip fixture must deserialize");
650 let serialized = serde_json::to_value(&resp).expect("Tweet response must serialize");
651 assert_eq!(serialized["data"]["new_field"], 42);
652 assert_eq!(serialized["top_extra"], "value");
653 }
654
655 #[test]
656 fn nested_unknown_fields_both_captured() {
657 let json = json!({
658 "data": {
659 "id": "123",
660 "text": "hello",
661 "tweet_extra": "a",
662 "public_metrics": {
663 "retweet_count": 0,
664 "reply_count": 0,
665 "like_count": 0,
666 "quote_count": 0,
667 "bookmark_count": 0,
668 "impression_count": 0,
669 "metrics_extra": "b"
670 }
671 }
672 });
673 let resp: ApiResponse<Tweet> = serde_json::from_value(json)
674 .expect("Tweet response with nested unknown fields must deserialize");
675 assert_eq!(resp.data.extra["tweet_extra"], "a");
676 let metrics = resp
677 .data
678 .public_metrics
679 .expect("public_metrics must be present");
680 assert_eq!(metrics.extra["metrics_extra"], "b");
681 }
682
683 #[test]
684 fn extra_is_empty_when_no_unknown_fields() {
685 let json = json!({"data": {"id": "1", "text": "hi"}});
686 let resp: ApiResponse<Tweet> =
687 serde_json::from_value(json).expect("minimal Tweet response must deserialize");
688 assert!(resp.extra.is_empty());
689 assert!(resp.data.extra.is_empty());
690 let out = serde_json::to_value(&resp).expect("Tweet response must serialize");
692 let data = out["data"]
693 .as_object()
694 .expect("serialized data must be a JSON object");
695 assert!(!data.contains_key("extra"));
696 }
697
698 #[test]
699 fn missing_optional_fields_are_none() {
700 let json = json!({"data": {"id": "1", "text": "minimal"}});
701 let resp: ApiResponse<Tweet> =
702 serde_json::from_value(json).expect("minimal Tweet must deserialize");
703 assert!(resp.data.created_at.is_none());
704 assert!(resp.data.public_metrics.is_none());
705 assert!(resp.data.author_id.is_none());
706 assert!(resp.includes.is_none());
707 assert!(resp.meta.is_none());
708 }
709
710 #[test]
711 fn default_produces_valid_structs() {
712 let tweet = Tweet {
713 id: "test".into(),
714 text: "hello".into(),
715 ..Default::default()
716 };
717 assert_eq!(tweet.id, "test");
718 assert!(tweet.created_at.is_none());
719
720 let user = User {
721 id: "42".into(),
722 name: "Bot".into(),
723 username: "bot".into(),
724 ..Default::default()
725 };
726 assert_eq!(user.username, "bot");
727
728 let _resp: ApiResponse<Tweet> = ApiResponse {
729 data: tweet,
730 ..Default::default()
731 };
732 }
733
734 #[test]
735 fn all_action_types_default_and_deserialize() {
736 let _: LikedResult = Default::default();
738 let _: FollowingResult = Default::default();
739 let _: DeletedResult = Default::default();
740 let _: RetweetedResult = Default::default();
741 let _: BookmarkedResult = Default::default();
742 let _: BlockingResult = Default::default();
743 let _: MutingResult = Default::default();
744
745 for (field, ty) in [
746 ("liked", "LikedResult"),
747 ("following", "FollowingResult"),
748 ("deleted", "DeletedResult"),
749 ("retweeted", "RetweetedResult"),
750 ("bookmarked", "BookmarkedResult"),
751 ("blocking", "BlockingResult"),
752 ("muting", "MutingResult"),
753 ] {
754 let json = json!({"data": {field: true}});
755 match ty {
757 "LikedResult" => {
758 let r: ApiResponse<LikedResult> = serde_json::from_value(json)
759 .expect("LikedResult action response must deserialize");
760 assert!(r.data.liked);
761 }
762 "FollowingResult" => {
763 let r: ApiResponse<FollowingResult> = serde_json::from_value(json)
764 .expect("FollowingResult action response must deserialize");
765 assert!(r.data.following);
766 }
767 "DeletedResult" => {
768 let r: ApiResponse<DeletedResult> = serde_json::from_value(json)
769 .expect("DeletedResult action response must deserialize");
770 assert!(r.data.deleted);
771 }
772 "RetweetedResult" => {
773 let r: ApiResponse<RetweetedResult> = serde_json::from_value(json)
774 .expect("RetweetedResult action response must deserialize");
775 assert!(r.data.retweeted);
776 }
777 "BookmarkedResult" => {
778 let r: ApiResponse<BookmarkedResult> = serde_json::from_value(json)
779 .expect("BookmarkedResult action response must deserialize");
780 assert!(r.data.bookmarked);
781 }
782 "BlockingResult" => {
783 let r: ApiResponse<BlockingResult> = serde_json::from_value(json)
784 .expect("BlockingResult action response must deserialize");
785 assert!(r.data.blocking);
786 }
787 "MutingResult" => {
788 let r: ApiResponse<MutingResult> = serde_json::from_value(json)
789 .expect("MutingResult action response must deserialize");
790 assert!(r.data.muting);
791 }
792 _ => unreachable!(),
793 }
794 }
795 }
796
797 #[test]
800 fn invalid_json_missing_required_field() {
801 let json = json!({"data": {"text": "no id"}});
803 let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
804 assert!(result.is_err());
805 }
806
807 #[test]
808 fn round_trip_serialize_deserialize() {
809 let tweet = Tweet {
810 id: "456".into(),
811 text: "round trip".into(),
812 created_at: Some("2026-01-01T00:00:00Z".into()),
813 ..Default::default()
814 };
815 let resp = ApiResponse {
816 data: tweet,
817 ..Default::default()
818 };
819 let value = serde_json::to_value(&resp).expect("Tweet must serialize");
820 let back: ApiResponse<Tweet> =
821 serde_json::from_value(value).expect("round-tripped Tweet must deserialize");
822 assert_eq!(back.data.id, "456");
823 assert_eq!(back.data.text, "round trip");
824 }
825
826 #[test]
827 fn deserialize_helper_rejects_empty_object() {
828 let value = json!({});
829 let result = deserialize_response::<Tweet>(value);
830 assert!(result.is_err());
831 let err = result.unwrap_err().to_string();
832 assert!(err.contains("empty response body"), "Got: {err}");
833 }
834
835 #[test]
838 fn adversarial_array_where_object_expected() {
839 let json = json!({"data": [{"id": "1", "text": "oops"}]});
841 let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
842 assert!(result.is_err(), "Should fail: array where object expected");
843 }
844
845 #[test]
846 fn adversarial_string_where_object_expected() {
847 let json = json!({"data": "not an object"});
848 let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
849 assert!(result.is_err(), "Should fail: string where object expected");
850 }
851
852 #[test]
853 fn adversarial_null_data_field() {
854 let json = json!({"data": null});
855 let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
856 assert!(result.is_err(), "Should fail: null data");
857 }
858
859 #[test]
860 fn adversarial_numeric_overflow_u64() {
861 let json = json!({
863 "data": {
864 "id": "123",
865 "text": "hi",
866 "public_metrics": {
867 "like_count": 99_999_999_999_999u64,
868 "retweet_count": 0,
869 "reply_count": 0,
870 "quote_count": 0,
871 "bookmark_count": 0,
872 "impression_count": 0
873 }
874 }
875 });
876 let resp: ApiResponse<Tweet> =
878 serde_json::from_value(json).expect("Tweet with large u64 must deserialize");
879 assert_eq!(
880 resp.data
881 .public_metrics
882 .expect("public_metrics must be present")
883 .like_count,
884 99_999_999_999_999
885 );
886 }
887
888 #[test]
889 fn adversarial_negative_count() {
890 let json = json!({
892 "data": {
893 "id": "123",
894 "text": "hi",
895 "public_metrics": {
896 "like_count": -1,
897 "retweet_count": 0,
898 "reply_count": 0,
899 "quote_count": 0,
900 "bookmark_count": 0,
901 "impression_count": 0
902 }
903 }
904 });
905 let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
906 assert!(result.is_err(), "Should fail: negative u64");
907 }
908
909 #[test]
910 fn adversarial_deeply_nested_unknown_fields() {
911 let json = json!({
912 "data": {"id": "123", "text": "hi"},
913 "extra_field": {
914 "a": {"b": {"c": [1, 2, 3]}}
915 }
916 });
917 let resp: ApiResponse<Tweet> =
918 serde_json::from_value(json).expect("Tweet with deep unknown fields must deserialize");
919 assert!(resp.extra.contains_key("extra_field"));
920 }
921
922 #[test]
923 fn adversarial_empty_string_required_fields() {
924 let json = json!({"data": {"id": "", "text": ""}});
926 let resp: ApiResponse<Tweet> = serde_json::from_value(json)
927 .expect("Tweet with empty string required fields must deserialize");
928 assert_eq!(resp.data.id, "");
929 assert_eq!(resp.data.text, "");
930 }
931
932 #[test]
933 fn adversarial_errors_field_no_data_raw_serde() {
934 let json = json!({"errors": [{"message": "forbidden"}]});
936 let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
937 assert!(result.is_err(), "Should fail: no data field");
938 }
939
940 #[test]
941 fn errors_only_response_returns_validation_error() {
942 let json = json!({
945 "errors": [{
946 "detail": "Could not find tweet with id: [123].",
947 "title": "Not Found Error",
948 "resource_type": "tweet",
949 "type": "https://api.twitter.com/2/problems/resource-not-found"
950 }]
951 });
952 let result = deserialize_response::<Tweet>(json);
953 assert!(result.is_err());
954 let err = result.unwrap_err();
955 assert!(err.is_validation(), "Expected Validation error, got: {err}");
956 let msg = err.to_string();
957 assert!(
958 msg.contains("Not Found Error"),
959 "Error should contain API message: {msg}"
960 );
961 }
962
963 #[test]
964 fn adversarial_extra_top_level_on_action() {
965 let json = json!({
966 "data": {"liked": true},
967 "extra_top_level": "ignored_by_consumer"
968 });
969 let resp: ApiResponse<LikedResult> = serde_json::from_value(json)
970 .expect("LikedResult with extra top-level fields must deserialize");
971 assert!(resp.data.liked);
972 assert_eq!(resp.extra["extra_top_level"], "ignored_by_consumer");
973 }
974
975 #[test]
976 fn adversarial_wrong_bool_type_for_action() {
977 let json = json!({"data": {"liked": "true"}});
979 let result = serde_json::from_value::<ApiResponse<LikedResult>>(json);
980 assert!(result.is_err(), "Should fail: string instead of bool");
981 }
982
983 #[test]
984 fn adversarial_partial_errors_with_valid_data() {
985 let json = json!({
987 "data": {"id": "123", "text": "partial"},
988 "errors": [{"message": "some field unavailable", "title": "Partial Error"}]
989 });
990 let resp: ApiResponse<Tweet> = serde_json::from_value(json)
991 .expect("Tweet with partial errors payload must deserialize");
992 assert_eq!(resp.data.id, "123");
993 let errors = resp.errors.expect("errors field must be present");
994 assert_eq!(errors.len(), 1);
995 assert_eq!(errors[0].message.as_deref(), Some("some field unavailable"));
996 }
997
998 #[test]
999 fn adversarial_huge_array_in_data() {
1000 let tweets: Vec<Value> = (0..1000)
1002 .map(|i| json!({"id": i.to_string(), "text": format!("tweet {i}")}))
1003 .collect();
1004 let json = json!({"data": tweets});
1005 let resp: ApiResponse<Vec<Tweet>> =
1006 serde_json::from_value(json).expect("1000-element Tweet list must deserialize");
1007 assert_eq!(resp.data.len(), 1000);
1008 }
1009
1010 #[test]
1011 fn adversarial_completely_wrong_shape() {
1012 let json = json!({"data": 42});
1014 let result = serde_json::from_value::<ApiResponse<Tweet>>(json);
1015 assert!(result.is_err());
1016 }
1017
1018 #[test]
1019 fn adversarial_api_error_with_extra_fields() {
1020 let json = json!({
1021 "data": {"id": "1", "text": "ok"},
1022 "errors": [{
1023 "message": "oops",
1024 "new_error_field": "surprise"
1025 }]
1026 });
1027 let resp: ApiResponse<Tweet> = serde_json::from_value(json)
1028 .expect("Tweet with errors containing extra fields must deserialize");
1029 let error = &resp.errors.expect("errors field must be present")[0];
1030 assert_eq!(error.extra["new_error_field"], "surprise");
1031 }
1032}