OCaml library for JSONfeed parsing and creation

tests for the roundtripping

+189 -4
+7 -2
CHANGES.md
··· 1 - v1.0.0 2 - ------ 1 + v1.1.0 (dev) 2 + ------------ 3 + 4 + - Simplify round trip processing of unknown messages using Jsont combinators (@avsm). 5 + 6 + v1.0.0 (2025-11-12) 7 + ------------------- 3 8 4 9 - Initial public release (@avsm) 5 10
+1 -1
dune-project
··· 25 25 (ptime (>= 1.2.0)) 26 26 bytesrw 27 27 (odoc :with-doc) 28 - (alcotest (and :with-test (>= 1.9.0))))) 28 + (alcotest (and :with-test (>= 1.8.0)))))
+1 -1
jsonfeed.opam
··· 15 15 "ptime" {>= "1.2.0"} 16 16 "bytesrw" 17 17 "odoc" {with-doc} 18 - "alcotest" {with-test & >= "1.9.0"} 18 + "alcotest" {with-test & >= "1.8.0"} 19 19 ] 20 20 build: [ 21 21 ["dune" "subst"] {dev}
+180
test/test_jsonfeed.ml
··· 370 370 test_feed_parse_invalid_missing_content ); 371 371 ] 372 372 373 + (* Unknown fields preservation tests *) 374 + 375 + let test_author_unknown_roundtrip () = 376 + let json = 377 + {|{ 378 + "name": "Test Author", 379 + "custom_field": "custom value", 380 + "another_extension": 42 381 + }|} 382 + in 383 + match Jsont_bytesrw.decode_string' Author.jsont json with 384 + | Error e -> 385 + Alcotest.fail 386 + (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 387 + | Ok author -> 388 + (* Check that unknown fields are preserved *) 389 + let unknown = Author.unknown author in 390 + Alcotest.(check bool) 391 + "has unknown fields" false 392 + (Jsonfeed.Unknown.is_empty unknown); 393 + (* Encode and decode again *) 394 + (match Jsont_bytesrw.encode_string' Author.jsont author with 395 + | Error e -> 396 + Alcotest.fail 397 + (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 398 + | Ok json2 -> ( 399 + match Jsont_bytesrw.decode_string' Author.jsont json2 with 400 + | Error e -> 401 + Alcotest.fail 402 + (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 403 + | Ok author2 -> 404 + (* Verify unknown fields survive roundtrip *) 405 + let unknown2 = Author.unknown author2 in 406 + Alcotest.(check bool) 407 + "unknown fields preserved" false 408 + (Jsonfeed.Unknown.is_empty unknown2))) 409 + 410 + let test_item_unknown_roundtrip () = 411 + let json = 412 + {|{ 413 + "id": "https://example.com/1", 414 + "content_html": "<p>Test</p>", 415 + "custom_metadata": "some custom data", 416 + "x_custom_number": 123.45 417 + }|} 418 + in 419 + match Jsont_bytesrw.decode_string' Item.jsont json with 420 + | Error e -> 421 + Alcotest.fail 422 + (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 423 + | Ok item -> 424 + (* Check that unknown fields are preserved *) 425 + let unknown = Item.unknown item in 426 + Alcotest.(check bool) 427 + "has unknown fields" false 428 + (Jsonfeed.Unknown.is_empty unknown); 429 + (* Encode and decode again *) 430 + (match Jsont_bytesrw.encode_string' Item.jsont item with 431 + | Error e -> 432 + Alcotest.fail 433 + (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 434 + | Ok json2 -> ( 435 + match Jsont_bytesrw.decode_string' Item.jsont json2 with 436 + | Error e -> 437 + Alcotest.fail 438 + (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 439 + | Ok item2 -> 440 + let unknown2 = Item.unknown item2 in 441 + Alcotest.(check bool) 442 + "unknown fields preserved" false 443 + (Jsonfeed.Unknown.is_empty unknown2))) 444 + 445 + let test_feed_unknown_roundtrip () = 446 + let json = 447 + {|{ 448 + "version": "https://jsonfeed.org/version/1.1", 449 + "title": "Test Feed", 450 + "items": [], 451 + "custom_extension": "custom value", 452 + "x_another_field": {"nested": "data"} 453 + }|} 454 + in 455 + match Jsonfeed.of_string json with 456 + | Error e -> 457 + Alcotest.fail 458 + (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 459 + | Ok feed -> 460 + (* Check that unknown fields are preserved *) 461 + let unknown = Jsonfeed.unknown feed in 462 + Alcotest.(check bool) 463 + "has unknown fields" false 464 + (Jsonfeed.Unknown.is_empty unknown); 465 + (* Encode and decode again *) 466 + (match Jsonfeed.to_string feed with 467 + | Error e -> 468 + Alcotest.fail 469 + (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 470 + | Ok json2 -> ( 471 + match Jsonfeed.of_string json2 with 472 + | Error e -> 473 + Alcotest.fail 474 + (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 475 + | Ok feed2 -> 476 + let unknown2 = Jsonfeed.unknown feed2 in 477 + Alcotest.(check bool) 478 + "unknown fields preserved" false 479 + (Jsonfeed.Unknown.is_empty unknown2))) 480 + 481 + let test_hub_unknown_roundtrip () = 482 + let json = {|{ 483 + "type": "WebSub", 484 + "url": "https://example.com/hub", 485 + "custom_field": "test" 486 + }|} in 487 + match Jsont_bytesrw.decode_string' Hub.jsont json with 488 + | Error e -> 489 + Alcotest.fail 490 + (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 491 + | Ok hub -> 492 + let unknown = Hub.unknown hub in 493 + Alcotest.(check bool) 494 + "has unknown fields" false 495 + (Jsonfeed.Unknown.is_empty unknown); 496 + (match Jsont_bytesrw.encode_string' Hub.jsont hub with 497 + | Error e -> 498 + Alcotest.fail 499 + (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 500 + | Ok json2 -> ( 501 + match Jsont_bytesrw.decode_string' Hub.jsont json2 with 502 + | Error e -> 503 + Alcotest.fail 504 + (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 505 + | Ok hub2 -> 506 + let unknown2 = Hub.unknown hub2 in 507 + Alcotest.(check bool) 508 + "unknown fields preserved" false 509 + (Jsonfeed.Unknown.is_empty unknown2))) 510 + 511 + let test_attachment_unknown_roundtrip () = 512 + let json = 513 + {|{ 514 + "url": "https://example.com/file.mp3", 515 + "mime_type": "audio/mpeg", 516 + "x_custom": "value" 517 + }|} 518 + in 519 + match Jsont_bytesrw.decode_string' Attachment.jsont json with 520 + | Error e -> 521 + Alcotest.fail 522 + (Printf.sprintf "Parse failed: %s" (Jsont.Error.to_string e)) 523 + | Ok att -> 524 + let unknown = Attachment.unknown att in 525 + Alcotest.(check bool) 526 + "has unknown fields" false 527 + (Jsonfeed.Unknown.is_empty unknown); 528 + (match Jsont_bytesrw.encode_string' Attachment.jsont att with 529 + | Error e -> 530 + Alcotest.fail 531 + (Printf.sprintf "Encode failed: %s" (Jsont.Error.to_string e)) 532 + | Ok json2 -> ( 533 + match Jsont_bytesrw.decode_string' Attachment.jsont json2 with 534 + | Error e -> 535 + Alcotest.fail 536 + (Printf.sprintf "Re-parse failed: %s" (Jsont.Error.to_string e)) 537 + | Ok att2 -> 538 + let unknown2 = Attachment.unknown att2 in 539 + Alcotest.(check bool) 540 + "unknown fields preserved" false 541 + (Jsonfeed.Unknown.is_empty unknown2))) 542 + 543 + let unknown_fields_tests = 544 + [ 545 + ("author unknown roundtrip", `Quick, test_author_unknown_roundtrip); 546 + ("item unknown roundtrip", `Quick, test_item_unknown_roundtrip); 547 + ("feed unknown roundtrip", `Quick, test_feed_unknown_roundtrip); 548 + ("hub unknown roundtrip", `Quick, test_hub_unknown_roundtrip); 549 + ("attachment unknown roundtrip", `Quick, test_attachment_unknown_roundtrip); 550 + ] 551 + 373 552 (* Main test suite *) 374 553 375 554 let () = ··· 380 559 ("Hub", hub_tests); 381 560 ("Item", item_tests); 382 561 ("Jsonfeed", jsonfeed_tests); 562 + ("Unknown Fields", unknown_fields_tests); 383 563 ]