atproto libraries implementation in ocaml

Migrate test fixture loading from Yojson to Atproto_json

- Replace Yojson with Atproto_json (simdjson-backed) in all test files:
- test/mst/test_mst.ml
- test/crypto/test_crypto.ml
- test/sync/test_sync.ml
- test/ipld/test_ipld.ml
- test/compliance/compliance_report.ml
- test/compliance/run_compliance.ml
- Remove yojson dependency from all test dune files
- Add encode_pretty function to lib/json/atproto_json.ml for formatted JSON output
- Update test/lexicon/dune to remove stale yojson dependency

All tests pass except pre-existing crypto signature verification failures.

Changed files
+457 -395
.beads
lib
test
+1
.beads/issues.jsonl
··· 50 50 {"id":"atproto-q0h","title":"Add firehose commit-proof-fixtures.json tests","description":"Add tests for the commit-proof-fixtures.json file which contains 6 test cases for MST proof verification:\n\n1. two deep split\n2. two deep leafless split\n3. add on edge with neighbor two layers down\n4. merge and split in multi-op commit\n5. complex multi-op commit\n6. split with earlier leaves on same layer\n\nEach fixture includes:\n- keys (existing keys in MST)\n- adds (keys to add)\n- dels (keys to delete)\n- rootBeforeCommit / rootAfterCommit (expected CIDs)\n- blocksInProof (CIDs of blocks needed for proof)\n\nThis tests the commit proof verification needed for firehose sync.","acceptance_criteria":"- All 6 commit-proof fixtures are tested\n- MST operations (add/delete) produce correct root CIDs\n- Proof blocks are correctly identified\n- Tests verify rootBeforeCommit and rootAfterCommit match","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T12:12:34.999268893+01:00","updated_at":"2025-12-28T12:58:39.408679225+01:00","closed_at":"2025-12-28T12:58:39.408679225+01:00","labels":["conformance","firehose","testing"]} 51 51 {"id":"atproto-s19","title":"Add CIDv0 parsing support","description":"Detect base58btc strings starting with \"Qm\", decode as multihash (sha2-256), implicit dag-pb codec","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T15:47:26.962896421+01:00","updated_at":"2025-12-28T15:58:38.151319563+01:00","closed_at":"2025-12-28T15:58:38.151319563+01:00","labels":["compliance","ipld"]} 52 52 {"id":"atproto-udz","title":"Add missing data-model conformance tests","description":"Add tests for data-model fixtures that are not currently covered:\n\n1. **data-model-valid.json** (5 entries) - Valid AT Protocol data model examples:\n - trivial record\n - float but integer-like (123.0)\n - empty list and object\n - list of nullable\n - list of lists\n\n2. **data-model-invalid.json** (12 entries) - Invalid examples that must be rejected:\n - top-level not an object\n - non-integer float\n - record with $type null/wrong type/empty\n - blob with string size/missing key\n - bytes with wrong field type/extra fields\n - link with wrong field type/bogus CID/extra fields","acceptance_criteria":"- test_data_model_valid() tests all 5 valid entries\n- test_data_model_invalid() tests all 12 invalid entries\n- Valid entries encode/decode correctly\n- Invalid entries are rejected with appropriate errors","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T12:12:14.579573063+01:00","updated_at":"2025-12-28T12:42:16.291981859+01:00","closed_at":"2025-12-28T12:42:16.291981859+01:00","labels":["conformance","ipld","testing"]} 53 + {"id":"atproto-w30","title":"Migrate test fixture loading from Yojson to Atproto_json","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T22:43:58.821369697+01:00","updated_at":"2026-01-01T22:53:56.562250588+01:00","closed_at":"2026-01-01T22:53:56.562250588+01:00"} 53 54 {"id":"atproto-w5i","title":"Create example applications","description":"Create example applications demonstrating the AT Protocol libraries:\n1. Simple client - authenticate and make posts\n2. Firehose consumer - subscribe to real-time events\n3. Bot example - automated posting/interactions","notes":"Added firehose_demo example showing how to use the firehose module with OCaml 5 effects. Additional examples (client, bot) can be added in future iterations.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T13:34:11.928213055+01:00","updated_at":"2025-12-28T13:36:39.963890666+01:00","closed_at":"2025-12-28T13:36:39.963890666+01:00","labels":["documentation","examples"],"dependencies":[{"issue_id":"atproto-w5i","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T13:34:17.10878+01:00","created_by":"daemon"}]} 54 55 {"id":"atproto-xfg","title":"Add Float support to DAG-CBOR","description":"Add Float of float variant to value type, encode as 64-bit double (CBOR major type 7, additional info 27), reject NaN and Infinity values, keep AT Protocol mode that rejects all floats","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-28T15:47:24.836568239+01:00","updated_at":"2025-12-28T15:53:03.229484165+01:00","closed_at":"2025-12-28T15:53:03.229484165+01:00","labels":["compliance","ipld"]} 55 56 {"id":"atproto-y4h","title":"Add JSON output mode to firehose demo","description":"Add --json flag to output events as JSON lines (JSONL format) instead of human-readable format. This enables piping to jq or other tools for further processing.","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-12-28T21:08:22.085375231+01:00","updated_at":"2025-12-28T21:13:33.122865944+01:00","closed_at":"2025-12-28T21:13:33.122865944+01:00","labels":["demo","enhancement","firehose"]}
+62
lib/json/atproto_json.ml
··· 76 76 let to_object_opt : t -> (string * t) list option = function 77 77 | Simdjsont.Json.Object pairs -> Some pairs 78 78 | _ -> None 79 + 80 + let escape_string s = 81 + let buf = Buffer.create (String.length s * 2) in 82 + String.iter 83 + (fun c -> 84 + match c with 85 + | '"' -> Buffer.add_string buf "\\\"" 86 + | '\\' -> Buffer.add_string buf "\\\\" 87 + | '\n' -> Buffer.add_string buf "\\n" 88 + | '\r' -> Buffer.add_string buf "\\r" 89 + | '\t' -> Buffer.add_string buf "\\t" 90 + | c when Char.code c < 32 -> 91 + Buffer.add_string buf (Printf.sprintf "\\u%04x" (Char.code c)) 92 + | c -> Buffer.add_char buf c) 93 + s; 94 + Buffer.contents buf 95 + 96 + let rec encode_pretty_aux buf indent t = 97 + let indent_str = String.make (indent * 2) ' ' in 98 + let next_indent = indent + 1 in 99 + match t with 100 + | Simdjsont.Json.Null -> Buffer.add_string buf "null" 101 + | Simdjsont.Json.Bool b -> 102 + Buffer.add_string buf (if b then "true" else "false") 103 + | Simdjsont.Json.Int i -> Buffer.add_string buf (Int64.to_string i) 104 + | Simdjsont.Json.Float f -> Buffer.add_string buf (Printf.sprintf "%g" f) 105 + | Simdjsont.Json.String s -> 106 + Buffer.add_char buf '"'; 107 + Buffer.add_string buf (escape_string s); 108 + Buffer.add_char buf '"' 109 + | Simdjsont.Json.Array [] -> Buffer.add_string buf "[]" 110 + | Simdjsont.Json.Array items -> 111 + Buffer.add_string buf "[\n"; 112 + List.iteri 113 + (fun i item -> 114 + Buffer.add_string buf (String.make (next_indent * 2) ' '); 115 + encode_pretty_aux buf next_indent item; 116 + if i < List.length items - 1 then Buffer.add_char buf ','; 117 + Buffer.add_char buf '\n') 118 + items; 119 + Buffer.add_string buf indent_str; 120 + Buffer.add_char buf ']' 121 + | Simdjsont.Json.Object [] -> Buffer.add_string buf "{}" 122 + | Simdjsont.Json.Object pairs -> 123 + Buffer.add_string buf "{\n"; 124 + List.iteri 125 + (fun i (k, v) -> 126 + Buffer.add_string buf (String.make (next_indent * 2) ' '); 127 + Buffer.add_char buf '"'; 128 + Buffer.add_string buf (escape_string k); 129 + Buffer.add_string buf "\": "; 130 + encode_pretty_aux buf next_indent v; 131 + if i < List.length pairs - 1 then Buffer.add_char buf ','; 132 + Buffer.add_char buf '\n') 133 + pairs; 134 + Buffer.add_string buf indent_str; 135 + Buffer.add_char buf '}' 136 + 137 + let encode_pretty (t : t) : string = 138 + let buf = Buffer.create 1024 in 139 + encode_pretty_aux buf 0 t; 140 + Buffer.contents buf
+40 -36
test/compliance/compliance_report.ml
··· 118 118 report_pass_rate = pass_rate; 119 119 } 120 120 121 - (** Convert report to JSON *) 122 121 let result_to_json (r : test_result) = 123 - `Assoc 122 + Atproto_json.object_ 124 123 [ 125 - ("input", `String r.input); 124 + ("input", Atproto_json.string r.input); 126 125 ( "expected", 127 - `String 126 + Atproto_json.string 128 127 (match r.expected with `Valid -> "valid" | `Invalid -> "invalid") ); 129 128 ( "actual", 130 - `String 129 + Atproto_json.string 131 130 (match r.actual with `Valid -> "valid" | `Invalid -> "invalid") ); 132 - ("passed", `Bool r.passed); 133 - ("error", match r.error_msg with Some s -> `String s | None -> `Null); 131 + ("passed", Atproto_json.bool r.passed); 132 + ( "error", 133 + match r.error_msg with 134 + | Some s -> Atproto_json.string s 135 + | None -> Atproto_json.null ); 134 136 ] 135 137 136 138 let category_to_json (c : category_result) = 137 - `Assoc 139 + Atproto_json.object_ 138 140 [ 139 - ("name", `String c.cat_name); 140 - ("description", `String c.cat_description); 141 - ("fixture_file", `String c.cat_fixture_file); 142 - ("total", `Int c.cat_total); 143 - ("passed", `Int c.cat_passed); 144 - ("failed", `Int c.cat_failed); 141 + ("name", Atproto_json.string c.cat_name); 142 + ("description", Atproto_json.string c.cat_description); 143 + ("fixture_file", Atproto_json.string c.cat_fixture_file); 144 + ("total", Atproto_json.int c.cat_total); 145 + ("passed", Atproto_json.int c.cat_passed); 146 + ("failed", Atproto_json.int c.cat_failed); 145 147 ( "pass_rate", 146 - `Float 148 + Atproto_json.float 147 149 (if c.cat_total > 0 then 148 150 float_of_int c.cat_passed /. float_of_int c.cat_total *. 100.0 149 151 else 0.0) ); 150 - ("results", `List (List.map result_to_json c.cat_results)); 152 + ("results", Atproto_json.array (List.map result_to_json c.cat_results)); 151 153 ] 152 154 153 155 let suite_to_json (s : suite_result) = 154 - `Assoc 156 + Atproto_json.object_ 155 157 [ 156 - ("name", `String s.suite_name); 158 + ("name", Atproto_json.string s.suite_name); 157 159 ( "spec_url", 158 - match s.suite_spec_url with Some u -> `String u | None -> `Null ); 159 - ("total", `Int s.suite_total); 160 - ("passed", `Int s.suite_passed); 161 - ("failed", `Int s.suite_failed); 160 + match s.suite_spec_url with 161 + | Some u -> Atproto_json.string u 162 + | None -> Atproto_json.null ); 163 + ("total", Atproto_json.int s.suite_total); 164 + ("passed", Atproto_json.int s.suite_passed); 165 + ("failed", Atproto_json.int s.suite_failed); 162 166 ( "pass_rate", 163 - `Float 167 + Atproto_json.float 164 168 (if s.suite_total > 0 then 165 169 float_of_int s.suite_passed /. float_of_int s.suite_total *. 100.0 166 170 else 0.0) ); 167 - ("categories", `List (List.map category_to_json s.suite_categories)); 171 + ( "categories", 172 + Atproto_json.array (List.map category_to_json s.suite_categories) ); 168 173 ] 169 174 170 175 let report_to_json (r : report) = 171 - `Assoc 176 + Atproto_json.object_ 172 177 [ 173 - ("title", `String r.report_title); 174 - ("version", `String r.report_version); 175 - ("generated_at", `String r.report_generated_at); 176 - ("repository", `String r.report_repository); 177 - ("total_tests", `Int r.report_total_tests); 178 - ("total_passed", `Int r.report_total_passed); 179 - ("total_failed", `Int r.report_total_failed); 180 - ("pass_rate", `Float r.report_pass_rate); 181 - ("suites", `List (List.map suite_to_json r.report_suites)); 178 + ("title", Atproto_json.string r.report_title); 179 + ("version", Atproto_json.string r.report_version); 180 + ("generated_at", Atproto_json.string r.report_generated_at); 181 + ("repository", Atproto_json.string r.report_repository); 182 + ("total_tests", Atproto_json.int r.report_total_tests); 183 + ("total_passed", Atproto_json.int r.report_total_passed); 184 + ("total_failed", Atproto_json.int r.report_total_failed); 185 + ("pass_rate", Atproto_json.float r.report_pass_rate); 186 + ("suites", Atproto_json.array (List.map suite_to_json r.report_suites)); 182 187 ] 183 188 184 - (** Write report to JSON file *) 185 189 let write_json_report filename (report : report) = 186 190 let json = report_to_json report in 187 191 let oc = open_out filename in 188 - output_string oc (Yojson.Safe.pretty_to_string json); 192 + output_string oc (Atproto_json.encode_pretty json); 189 193 output_char oc '\n'; 190 194 close_out oc 191 195
+2 -2
test/compliance/dune
··· 1 1 (library 2 2 (name compliance_report) 3 3 (modules compliance_report) 4 - (libraries yojson unix)) 4 + (libraries atproto_json unix)) 5 5 6 6 (executable 7 7 (name run_compliance) ··· 13 13 atproto-ipld 14 14 atproto-mst 15 15 mirage-crypto-rng.unix 16 - yojson 16 + atproto_json 17 17 unix))
+72 -117
test/compliance/run_compliance.ml
··· 38 38 let ic = open_in filename in 39 39 let content = In_channel.input_all ic in 40 40 close_in ic; 41 - Yojson.Safe.from_string content 41 + match Atproto_json.decode content with 42 + | Ok json -> json 43 + | Error e -> failwith ("JSON parse error: " ^ e) 42 44 43 45 (** Base64 decode *) 44 46 let base64_decode s = ··· 187 189 188 190 (* ==================== Crypto Tests ==================== *) 189 191 192 + let get_string_field key pairs = 193 + match Atproto_json.get_string_opt key pairs with 194 + | Some s -> s 195 + | None -> failwith ("missing " ^ key) 196 + 197 + let get_bool_field key pairs = 198 + match Atproto_json.get key pairs with 199 + | Some v -> ( 200 + match Atproto_json.to_bool_opt v with 201 + | Some b -> b 202 + | None -> failwith ("invalid bool: " ^ key)) 203 + | None -> failwith ("missing " ^ key) 204 + 205 + let get_int_field key pairs = 206 + match Atproto_json.get_int_opt key pairs with 207 + | Some i -> i 208 + | None -> failwith ("missing or invalid int: " ^ key) 209 + 190 210 let run_crypto_tests () = 191 - (* Signature verification tests *) 192 211 let sig_fixtures = 193 212 load_json_fixture (fixture_dir ^ "/crypto/signature-fixtures.json") 194 213 in 195 214 let sig_results = 196 - match sig_fixtures with 197 - | `List items -> 215 + match Atproto_json.to_array_opt sig_fixtures with 216 + | Some items -> 198 217 List.map 199 218 (fun item -> 200 - match item with 201 - | `Assoc fields -> 219 + match Atproto_json.to_object_opt item with 220 + | Some fields -> 202 221 let comment = 203 - match List.assoc_opt "comment" fields with 204 - | Some (`String s) -> s 205 - | _ -> "unknown" 222 + match Atproto_json.get_string_opt "comment" fields with 223 + | Some s -> s 224 + | None -> "unknown" 206 225 in 207 - let message_b64 = 208 - match List.assoc_opt "messageBase64" fields with 209 - | Some (`String s) -> s 210 - | _ -> failwith "missing messageBase64" 211 - in 212 - let algorithm = 213 - match List.assoc_opt "algorithm" fields with 214 - | Some (`String s) -> s 215 - | _ -> failwith "missing algorithm" 216 - in 217 - let public_key_did = 218 - match List.assoc_opt "publicKeyDid" fields with 219 - | Some (`String s) -> s 220 - | _ -> failwith "missing publicKeyDid" 221 - in 222 - let signature_b64 = 223 - match List.assoc_opt "signatureBase64" fields with 224 - | Some (`String s) -> s 225 - | _ -> failwith "missing signatureBase64" 226 - in 227 - let valid_signature = 228 - match List.assoc_opt "validSignature" fields with 229 - | Some (`Bool b) -> b 230 - | _ -> failwith "missing validSignature" 231 - in 226 + let message_b64 = get_string_field "messageBase64" fields in 227 + let algorithm = get_string_field "algorithm" fields in 228 + let public_key_did = get_string_field "publicKeyDid" fields in 229 + let signature_b64 = get_string_field "signatureBase64" fields in 230 + let valid_signature = get_bool_field "validSignature" fields in 232 231 233 232 let message = base64_decode message_b64 in 234 233 let signature = base64_decode signature_b64 in ··· 262 261 with _ -> `Invalid 263 262 in 264 263 make_result ~input:comment ~expected ~actual () 265 - | _ -> failwith "invalid fixture format") 264 + | None -> failwith "invalid fixture format") 266 265 items 267 - | _ -> failwith "invalid fixtures format" 266 + | None -> failwith "invalid fixtures format" 268 267 in 269 268 270 269 let sig_category = ··· 273 272 ~fixture_file:"signature-fixtures.json" sig_results 274 273 in 275 274 276 - (* did:key encoding tests for P-256 *) 277 275 let p256_fixtures = 278 276 load_json_fixture (fixture_dir ^ "/crypto/w3c_didkey_P256.json") 279 277 in 280 278 let p256_results = 281 - match p256_fixtures with 282 - | `List items -> 279 + match Atproto_json.to_array_opt p256_fixtures with 280 + | Some items -> 283 281 List.map 284 282 (fun item -> 285 - match item with 286 - | `Assoc fields -> 287 - let did = 288 - match List.assoc_opt "publicDidKey" fields with 289 - | Some (`String s) -> s 290 - | _ -> failwith "missing publicDidKey" 291 - in 283 + match Atproto_json.to_object_opt item with 284 + | Some fields -> 285 + let did = get_string_field "publicDidKey" fields in 292 286 let actual = 293 287 try 294 288 match Atproto_crypto.Did_key.decode did with ··· 297 291 with _ -> `Invalid 298 292 in 299 293 make_result ~input:did ~expected:`Valid ~actual () 300 - | _ -> failwith "invalid fixture format") 294 + | None -> failwith "invalid fixture format") 301 295 items 302 - | _ -> [] 296 + | None -> [] 303 297 in 304 298 305 299 let p256_category = ··· 308 302 ~fixture_file:"w3c_didkey_P256.json" p256_results 309 303 in 310 304 311 - (* did:key encoding tests for K-256 *) 312 305 let k256_fixtures = 313 306 load_json_fixture (fixture_dir ^ "/crypto/w3c_didkey_K256.json") 314 307 in 315 308 let k256_results = 316 - match k256_fixtures with 317 - | `List items -> 309 + match Atproto_json.to_array_opt k256_fixtures with 310 + | Some items -> 318 311 List.map 319 312 (fun item -> 320 - match item with 321 - | `Assoc fields -> 322 - let did = 323 - match List.assoc_opt "publicDidKey" fields with 324 - | Some (`String s) -> s 325 - | _ -> failwith "missing publicDidKey" 326 - in 313 + match Atproto_json.to_object_opt item with 314 + | Some fields -> 315 + let did = get_string_field "publicDidKey" fields in 327 316 let actual = 328 317 try 329 318 match Atproto_crypto.Did_key.decode did with ··· 332 321 with _ -> `Invalid 333 322 in 334 323 make_result ~input:did ~expected:`Valid ~actual () 335 - | _ -> failwith "invalid fixture format") 324 + | None -> failwith "invalid fixture format") 336 325 items 337 - | _ -> [] 326 + | None -> [] 338 327 in 339 328 340 329 let k256_category = ··· 350 339 (* ==================== Data Model Tests ==================== *) 351 340 352 341 let run_data_model_tests () = 353 - (* DAG-CBOR encoding and CID computation *) 354 342 let fixtures = 355 343 load_json_fixture (fixture_dir ^ "/data-model/data-model-fixtures.json") 356 344 in 357 345 let results = 358 - match fixtures with 359 - | `List items -> 346 + match Atproto_json.to_array_opt fixtures with 347 + | Some items -> 360 348 List.mapi 361 349 (fun i item -> 362 - match item with 363 - | `Assoc fields -> 364 - let expected_cid = 365 - match List.assoc_opt "cid" fields with 366 - | Some (`String s) -> s 367 - | _ -> failwith "missing cid" 368 - in 369 - let cbor_b64 = 370 - match List.assoc_opt "cbor_base64" fields with 371 - | Some (`String s) -> s 372 - | _ -> failwith "missing cbor_base64" 373 - in 374 - let json_value = 375 - match List.assoc_opt "json" fields with 376 - | Some j -> j 377 - | _ -> failwith "missing json" 378 - in 379 - 380 - let _ = json_value in 381 - (* We'll use cbor_base64 directly *) 350 + match Atproto_json.to_object_opt item with 351 + | Some fields -> 352 + let expected_cid = get_string_field "cid" fields in 353 + let cbor_b64 = get_string_field "cbor_base64" fields in 382 354 let cbor_str = base64_decode cbor_b64 in 383 355 384 356 let actual = ··· 391 363 make_result 392 364 ~input:(Printf.sprintf "fixture[%d]" i) 393 365 ~expected:`Valid ~actual () 394 - | _ -> failwith "invalid fixture format") 366 + | None -> failwith "invalid fixture format") 395 367 items 396 - | _ -> [] 368 + | None -> [] 397 369 in 398 370 399 371 let cbor_category = ··· 402 374 ~fixture_file:"data-model-fixtures.json" results 403 375 in 404 376 405 - (* CID syntax validation - preserve whitespace to test whitespace handling *) 406 377 let cid_valid = 407 378 load_test_vectors (fixture_dir ^ "/syntax/cid_syntax_valid.txt") 408 379 in ··· 415 386 List.map 416 387 (fun input -> 417 388 let actual = 418 - (* Use syntax validation for CID syntax tests - this matches the Go 419 - implementation which does lenient validation without fully decoding *) 420 389 if Atproto_ipld.Cid.is_valid_syntax input then `Valid else `Invalid 421 390 in 422 391 make_result ~input ~expected:`Valid ~actual ()) ··· 446 415 (* ==================== MST Tests ==================== *) 447 416 448 417 let run_mst_tests () = 449 - (* Key height tests *) 450 418 let height_fixtures = 451 419 load_json_fixture (fixture_dir ^ "/mst/key_heights.json") 452 420 in 453 421 let height_results = 454 - match height_fixtures with 455 - | `Assoc items -> 422 + match Atproto_json.to_object_opt height_fixtures with 423 + | Some items -> 456 424 List.map 457 425 (fun (key, expected_height) -> 458 426 let expected_h = 459 - match expected_height with 460 - | `Int h -> h 461 - | _ -> failwith "invalid height" 427 + match Atproto_json.to_int_opt expected_height with 428 + | Some h -> h 429 + | None -> failwith "invalid height" 462 430 in 463 431 let actual = 464 432 try ··· 468 436 in 469 437 make_result ~input:key ~expected:`Valid ~actual ()) 470 438 items 471 - | _ -> [] 439 + | None -> [] 472 440 in 473 441 474 442 let height_category = ··· 476 444 ~fixture_file:"key_heights.json" height_results 477 445 in 478 446 479 - (* Common prefix tests *) 480 447 let prefix_fixtures = 481 448 load_json_fixture (fixture_dir ^ "/mst/common_prefix.json") 482 449 in 483 450 let prefix_results = 484 - match prefix_fixtures with 485 - | `List items -> 451 + match Atproto_json.to_array_opt prefix_fixtures with 452 + | Some items -> 486 453 List.mapi 487 454 (fun i item -> 488 - match item with 489 - | `Assoc fields -> 490 - let left = 491 - match List.assoc_opt "left" fields with 492 - | Some (`String s) -> s 493 - | _ -> failwith "missing left" 494 - in 495 - let right = 496 - match List.assoc_opt "right" fields with 497 - | Some (`String s) -> s 498 - | _ -> failwith "missing right" 499 - in 500 - let expected_len = 501 - match List.assoc_opt "len" fields with 502 - | Some (`Int n) -> n 503 - | _ -> failwith "missing len" 504 - in 455 + match Atproto_json.to_object_opt item with 456 + | Some fields -> 457 + let left = get_string_field "left" fields in 458 + let right = get_string_field "right" fields in 459 + let expected_len = get_int_field "len" fields in 505 460 let actual = 506 461 try 507 462 let len = Atproto_mst.common_prefix_len left right in ··· 511 466 make_result 512 467 ~input:(Printf.sprintf "prefix[%d]: %s, %s" i left right) 513 468 ~expected:`Valid ~actual () 514 - | _ -> failwith "invalid fixture format") 469 + | None -> failwith "invalid fixture format") 515 470 items 516 - | _ -> [] 471 + | None -> [] 517 472 in 518 473 519 474 let prefix_category =
+1 -1
test/crypto/dune
··· 1 1 (test 2 2 (name test_crypto) 3 3 (package atproto-crypto) 4 - (libraries atproto_crypto alcotest yojson mirage-crypto-rng.unix) 4 + (libraries atproto_crypto atproto_json alcotest mirage-crypto-rng.unix) 5 5 (deps 6 6 (source_tree ../fixtures/crypto)) 7 7 (preprocess no_preprocessing))
+61 -55
test/crypto/test_crypto.ml
··· 13 13 let ic = open_in path in 14 14 let content = In_channel.input_all ic in 15 15 close_in ic; 16 - Yojson.Safe.from_string content 16 + match Atproto_json.decode content with 17 + | Ok json -> json 18 + | Error e -> failwith ("JSON parse error: " ^ e) 17 19 18 20 (** Base64 decode *) 19 21 let base64_decode s = ··· 80 82 81 83 let test_signature_verification () = 82 84 let fixtures = read_fixture "signature-fixtures.json" in 83 - match fixtures with 84 - | `List items -> 85 + match Atproto_json.to_array_opt fixtures with 86 + | Some items -> 85 87 List.iter 86 88 (fun item -> 87 - match item with 88 - | `Assoc fields -> 89 + match Atproto_json.to_object_opt item with 90 + | Some fields -> 89 91 let comment = 90 - match List.assoc_opt "comment" fields with 91 - | Some (`String s) -> s 92 - | _ -> "unknown" 92 + match Atproto_json.get_string_opt "comment" fields with 93 + | Some s -> s 94 + | None -> "unknown" 93 95 in 94 96 let message_b64 = 95 - match List.assoc_opt "messageBase64" fields with 96 - | Some (`String s) -> s 97 - | _ -> failwith "missing messageBase64" 97 + match Atproto_json.get_string_opt "messageBase64" fields with 98 + | Some s -> s 99 + | None -> failwith "missing messageBase64" 98 100 in 99 101 let algorithm = 100 - match List.assoc_opt "algorithm" fields with 101 - | Some (`String s) -> s 102 - | _ -> failwith "missing algorithm" 102 + match Atproto_json.get_string_opt "algorithm" fields with 103 + | Some s -> s 104 + | None -> failwith "missing algorithm" 103 105 in 104 106 let public_key_did = 105 - match List.assoc_opt "publicKeyDid" fields with 106 - | Some (`String s) -> s 107 - | _ -> failwith "missing publicKeyDid" 107 + match Atproto_json.get_string_opt "publicKeyDid" fields with 108 + | Some s -> s 109 + | None -> failwith "missing publicKeyDid" 108 110 in 109 111 let signature_b64 = 110 - match List.assoc_opt "signatureBase64" fields with 111 - | Some (`String s) -> s 112 - | _ -> failwith "missing signatureBase64" 112 + match Atproto_json.get_string_opt "signatureBase64" fields with 113 + | Some s -> s 114 + | None -> failwith "missing signatureBase64" 113 115 in 114 116 let valid_signature = 115 - match List.assoc_opt "validSignature" fields with 116 - | Some (`Bool b) -> b 117 - | _ -> failwith "missing validSignature" 117 + match Atproto_json.get "validSignature" fields with 118 + | Some json -> ( 119 + match Atproto_json.to_bool_opt json with 120 + | Some b -> b 121 + | None -> failwith "missing validSignature") 122 + | None -> failwith "missing validSignature" 118 123 in 119 124 let tags = 120 - match List.assoc_opt "tags" fields with 121 - | Some (`List tags) -> 122 - List.filter_map 123 - (function `String s -> Some s | _ -> None) 124 - tags 125 - | _ -> [] 125 + match Atproto_json.get_array_opt "tags" fields with 126 + | Some tags -> List.filter_map Atproto_json.to_string_opt tags 127 + | None -> [] 126 128 in 127 129 128 130 (* Decode inputs *) ··· 168 170 (Printf.sprintf "signature validity: %s" comment) 169 171 valid_signature is_valid 170 172 end 171 - | _ -> failwith "expected object in fixture array") 173 + | None -> failwith "expected object in fixture array") 172 174 items 173 - | _ -> failwith "expected array in fixture file" 175 + | None -> failwith "expected array in fixture file" 174 176 175 177 (* === did:key encoding tests for K-256 === *) 176 178 177 179 let test_didkey_k256 () = 178 180 let fixtures = read_fixture "w3c_didkey_K256.json" in 179 - match fixtures with 180 - | `List items -> 181 + match Atproto_json.to_array_opt fixtures with 182 + | Some items -> 181 183 List.iter 182 184 (fun item -> 183 - match item with 184 - | `Assoc fields -> ( 185 + match Atproto_json.to_object_opt item with 186 + | Some fields -> ( 185 187 let private_key_hex = 186 - match List.assoc_opt "privateKeyBytesHex" fields with 187 - | Some (`String s) -> s 188 - | _ -> failwith "missing privateKeyBytesHex" 188 + match 189 + Atproto_json.get_string_opt "privateKeyBytesHex" fields 190 + with 191 + | Some s -> s 192 + | None -> failwith "missing privateKeyBytesHex" 189 193 in 190 194 let expected_did = 191 - match List.assoc_opt "publicDidKey" fields with 192 - | Some (`String s) -> s 193 - | _ -> failwith "missing publicDidKey" 195 + match Atproto_json.get_string_opt "publicDidKey" fields with 196 + | Some s -> s 197 + | None -> failwith "missing publicDidKey" 194 198 in 195 199 196 200 (* Decode private key and derive public key *) ··· 215 219 | Ok (K256 _pub') -> () 216 220 | Ok (P256 _) -> 217 221 Alcotest.fail "decoded as P256 instead of K256")) 218 - | _ -> failwith "expected object in fixture array") 222 + | None -> failwith "expected object in fixture array") 219 223 items 220 - | _ -> failwith "expected array in fixture file" 224 + | None -> failwith "expected array in fixture file" 221 225 222 226 (* === did:key encoding tests for P-256 === *) 223 227 224 228 let test_didkey_p256 () = 225 229 let fixtures = read_fixture "w3c_didkey_P256.json" in 226 - match fixtures with 227 - | `List items -> 230 + match Atproto_json.to_array_opt fixtures with 231 + | Some items -> 228 232 List.iter 229 233 (fun item -> 230 - match item with 231 - | `Assoc fields -> ( 234 + match Atproto_json.to_object_opt item with 235 + | Some fields -> ( 232 236 let private_key_b58 = 233 - match List.assoc_opt "privateKeyBytesBase58" fields with 234 - | Some (`String s) -> s 235 - | _ -> failwith "missing privateKeyBytesBase58" 237 + match 238 + Atproto_json.get_string_opt "privateKeyBytesBase58" fields 239 + with 240 + | Some s -> s 241 + | None -> failwith "missing privateKeyBytesBase58" 236 242 in 237 243 let expected_did = 238 - match List.assoc_opt "publicDidKey" fields with 239 - | Some (`String s) -> s 240 - | _ -> failwith "missing publicDidKey" 244 + match Atproto_json.get_string_opt "publicDidKey" fields with 245 + | Some s -> s 246 + | None -> failwith "missing publicDidKey" 241 247 in 242 248 243 249 (* Decode private key and derive public key *) ··· 266 272 | Ok (P256 _pub') -> () 267 273 | Ok (K256 _) -> 268 274 Alcotest.fail "decoded as K256 instead of P256"))) 269 - | _ -> failwith "expected object in fixture array") 275 + | None -> failwith "expected object in fixture array") 270 276 items 271 - | _ -> failwith "expected array in fixture file" 277 + | None -> failwith "expected array in fixture file" 272 278 273 279 (* === Basic P256 signing tests === *) 274 280
+1 -1
test/ipld/dune
··· 4 4 (deps 5 5 (source_tree ../fixtures/syntax) 6 6 (source_tree ../fixtures/data-model)) 7 - (libraries atproto_ipld alcotest yojson base64)) 7 + (libraries atproto_ipld alcotest atproto_json base64))
+173 -132
test/ipld/test_ipld.ml
··· 255 255 let ic = open_in path in 256 256 let content = really_input_string ic (in_channel_length ic) in 257 257 close_in ic; 258 - Yojson.Basic.from_string content 258 + match Atproto_json.decode content with 259 + | Ok json -> json 260 + | Error e -> failwith ("JSON parse error: " ^ e) 259 261 260 262 (** Base64 decode helper using the base64 library *) 261 263 let base64_decode_test s = ··· 264 266 | Ok decoded -> decoded 265 267 | Error _ -> failwith ("base64 decode failed: " ^ s) 266 268 267 - (** Convert Yojson.Basic.t to Dag_cbor.json *) 268 - let rec yojson_to_dag_cbor_json (j : Yojson.Basic.t) : Dag_cbor.json = 269 - match j with 270 - | `Null -> `Null 271 - | `Bool b -> `Bool b 272 - | `Int i -> `Int i 273 - | `Float f -> `Float f 274 - | `String s -> `String s 275 - | `List l -> `List (List.map yojson_to_dag_cbor_json l) 276 - | `Assoc pairs -> 277 - `Assoc (List.map (fun (k, v) -> (k, yojson_to_dag_cbor_json v)) pairs) 269 + (** Convert Atproto_json.t to Dag_cbor.json *) 270 + let rec atproto_json_to_dag_cbor_json (j : Atproto_json.t) : Dag_cbor.json = 271 + match Atproto_json.to_null_opt j with 272 + | Some () -> `Null 273 + | None -> ( 274 + match Atproto_json.to_bool_opt j with 275 + | Some b -> `Bool b 276 + | None -> ( 277 + match Atproto_json.to_int_opt j with 278 + | Some i -> `Int i 279 + | None -> ( 280 + match Atproto_json.to_float_opt j with 281 + | Some f -> `Float f 282 + | None -> ( 283 + match Atproto_json.to_string_opt j with 284 + | Some s -> `String s 285 + | None -> ( 286 + match Atproto_json.to_array_opt j with 287 + | Some l -> 288 + `List (List.map atproto_json_to_dag_cbor_json l) 289 + | None -> ( 290 + match Atproto_json.to_object_opt j with 291 + | Some pairs -> 292 + `Assoc 293 + (List.map 294 + (fun (k, v) -> 295 + (k, atproto_json_to_dag_cbor_json v)) 296 + pairs) 297 + | None -> failwith "Unknown JSON type")))))) 278 298 279 299 let test_dag_cbor_fixtures () = 280 300 let fixtures = read_fixture_json "data-model/data-model-fixtures.json" in 281 - match fixtures with 282 - | `List items -> 301 + match Atproto_json.to_array_opt fixtures with 302 + | Some items -> 283 303 List.iteri 284 304 (fun idx item -> 285 - match item with 286 - | `Assoc pairs -> ( 305 + match Atproto_json.to_object_opt item with 306 + | Some pairs -> ( 287 307 let json_val = 288 - List.assoc_opt "json" pairs 289 - |> Option.map yojson_to_dag_cbor_json 290 - in 291 - let cbor_b64 = 292 - match List.assoc_opt "cbor_base64" pairs with 293 - | Some (`String s) -> Some s 294 - | _ -> None 295 - in 296 - let expected_cid = 297 - match List.assoc_opt "cid" pairs with 298 - | Some (`String s) -> Some s 299 - | _ -> None 308 + Atproto_json.get "json" pairs 309 + |> Option.map atproto_json_to_dag_cbor_json 300 310 in 311 + let cbor_b64 = Atproto_json.get_string_opt "cbor_base64" pairs in 312 + let expected_cid = Atproto_json.get_string_opt "cid" pairs in 301 313 match (json_val, cbor_b64, expected_cid) with 302 314 | Some json, Some b64, Some cid_str -> ( 303 - (* Parse JSON to value *) 304 315 match Dag_cbor.of_json json with 305 316 | Ok value -> ( 306 - (* Encode to CBOR *) 307 317 let encoded = Dag_cbor.encode value in 308 - (* Decode expected CBOR *) 309 318 let expected_cbor = base64_decode_test b64 in 310 - (* Check CBOR matches *) 311 319 Alcotest.(check string) 312 320 (Printf.sprintf "fixture %d: CBOR encoding" idx) 313 321 expected_cbor encoded; 314 - (* Check CID matches *) 315 322 let cid = Cid.of_dag_cbor encoded in 316 323 Alcotest.(check string) 317 324 (Printf.sprintf "fixture %d: CID" idx) 318 325 cid_str (Cid.to_string cid); 319 - (* Test decode roundtrip *) 320 326 match Dag_cbor.decode encoded with 321 327 | Ok decoded -> 322 328 Alcotest.(check bool) ··· 331 337 Alcotest.fail 332 338 (Printf.sprintf "fixture %d: JSON parse failed: %s" idx 333 339 (Dag_cbor.error_to_string e))) 334 - | _ -> () (* Skip incomplete fixtures *)) 335 - | _ -> ()) 340 + | _ -> ()) 341 + | None -> ()) 336 342 items 337 - | _ -> Alcotest.fail "Expected JSON array" 343 + | None -> Alcotest.fail "Expected JSON array" 338 344 339 345 let test_dag_cbor_key_sorting () = 340 346 (* Test that map keys are sorted by length first, then lexicographically *) ··· 1010 1016 1011 1017 (** Validate AT Protocol data model JSON. This validates the structural rules 1012 1018 beyond basic JSON parsing. *) 1013 - let rec validate_data_model (j : Yojson.Basic.t) : 1019 + let rec validate_data_model (j : Atproto_json.t) : 1014 1020 (unit, data_model_error) result = 1015 - match j with 1016 - | `Null | `Bool _ | `String _ -> Ok () 1017 - | `Int _ -> Ok () 1018 - | `Float f -> 1019 - (* Floats must be integer-like in AT Protocol *) 1020 - if Float.is_integer f then Ok () else Error Float_not_integer 1021 - | `List items -> 1022 - (* Validate each item in the list *) 1023 - List.fold_left 1024 - (fun acc item -> 1025 - match acc with 1026 - | Error e -> Error e 1027 - | Ok () -> validate_data_model item) 1028 - (Ok ()) items 1029 - | `Assoc pairs -> 1030 - (* Check for special AT Protocol objects *) 1031 - let keys = List.map fst pairs in 1032 - if List.mem "$link" keys then validate_link pairs 1033 - else if List.mem "$bytes" keys then validate_bytes pairs 1034 - else if List.mem "$type" keys then validate_typed_object pairs 1035 - else 1036 - (* Regular object - validate all values *) 1037 - List.fold_left 1038 - (fun acc (_, v) -> 1039 - match acc with Error e -> Error e | Ok () -> validate_data_model v) 1040 - (Ok ()) pairs 1021 + match Atproto_json.to_null_opt j with 1022 + | Some () -> Ok () 1023 + | None -> ( 1024 + match Atproto_json.to_bool_opt j with 1025 + | Some _ -> Ok () 1026 + | None -> ( 1027 + match Atproto_json.to_string_opt j with 1028 + | Some _ -> Ok () 1029 + | None -> ( 1030 + match Atproto_json.to_int_opt j with 1031 + | Some _ -> Ok () 1032 + | None -> ( 1033 + match Atproto_json.to_float_opt j with 1034 + | Some f -> 1035 + if Float.is_integer f then Ok () 1036 + else Error Float_not_integer 1037 + | None -> ( 1038 + match Atproto_json.to_array_opt j with 1039 + | Some items -> 1040 + List.fold_left 1041 + (fun acc item -> 1042 + match acc with 1043 + | Error e -> Error e 1044 + | Ok () -> validate_data_model item) 1045 + (Ok ()) items 1046 + | None -> ( 1047 + match Atproto_json.to_object_opt j with 1048 + | Some pairs -> 1049 + let keys = List.map fst pairs in 1050 + if List.mem "$link" keys then validate_link pairs 1051 + else if List.mem "$bytes" keys then 1052 + validate_bytes pairs 1053 + else if List.mem "$type" keys then 1054 + validate_typed_object pairs 1055 + else 1056 + List.fold_left 1057 + (fun acc (_, v) -> 1058 + match acc with 1059 + | Error e -> Error e 1060 + | Ok () -> validate_data_model v) 1061 + (Ok ()) pairs 1062 + | None -> Ok ())))))) 1041 1063 1042 1064 and validate_link pairs = 1043 - match pairs with 1044 - | [ ("$link", `String cid_str) ] -> 1045 - (* Validate CID string *) 1046 - if Cid.is_valid_syntax cid_str then Ok () else Error Link_invalid_cid 1047 - | [ ("$link", _) ] -> Error Link_wrong_type 1048 - | _ when List.length pairs > 1 -> Error Link_extra_fields 1049 - | _ -> Error Link_wrong_type 1065 + let link_val = Atproto_json.get "$link" pairs in 1066 + match link_val with 1067 + | Some v -> ( 1068 + if List.length pairs > 1 then Error Link_extra_fields 1069 + else 1070 + match Atproto_json.to_string_opt v with 1071 + | Some cid_str -> 1072 + if Cid.is_valid_syntax cid_str then Ok () 1073 + else Error Link_invalid_cid 1074 + | None -> Error Link_wrong_type) 1075 + | None -> Error Link_wrong_type 1050 1076 1051 1077 and validate_bytes pairs = 1052 - match pairs with 1053 - | [ ("$bytes", `String _) ] -> Ok () 1054 - | [ ("$bytes", _) ] -> Error Bytes_wrong_type 1055 - | _ when List.length pairs > 1 -> Error Bytes_extra_fields 1056 - | _ -> Error Bytes_wrong_type 1078 + let bytes_val = Atproto_json.get "$bytes" pairs in 1079 + match bytes_val with 1080 + | Some v -> ( 1081 + if List.length pairs > 1 then Error Bytes_extra_fields 1082 + else 1083 + match Atproto_json.to_string_opt v with 1084 + | Some _ -> Ok () 1085 + | None -> Error Bytes_wrong_type) 1086 + | None -> Error Bytes_wrong_type 1057 1087 1058 1088 and validate_typed_object pairs = 1059 - (* Check $type field *) 1060 - let type_val = List.assoc_opt "$type" pairs in 1089 + let type_val = Atproto_json.get "$type" pairs in 1061 1090 match type_val with 1062 - | Some `Null -> Error Type_null 1063 - | Some (`String s) when String.length s = 0 -> Error Type_empty 1064 - | Some (`String s) when s = "blob" -> 1065 - (* Validate blob structure *) 1066 - validate_blob pairs 1067 - | Some (`String _) -> 1068 - (* Valid record - validate all values *) 1069 - List.fold_left 1070 - (fun acc (_, v) -> 1071 - match acc with Error e -> Error e | Ok () -> validate_data_model v) 1072 - (Ok ()) pairs 1073 - | Some _ -> Error Type_not_string 1074 - | None -> Ok () (* No $type is fine for non-records *) 1091 + | Some v -> ( 1092 + match Atproto_json.to_null_opt v with 1093 + | Some () -> Error Type_null 1094 + | None -> ( 1095 + match Atproto_json.to_string_opt v with 1096 + | Some s when String.length s = 0 -> Error Type_empty 1097 + | Some s when s = "blob" -> validate_blob pairs 1098 + | Some _ -> 1099 + List.fold_left 1100 + (fun acc (_, v) -> 1101 + match acc with 1102 + | Error e -> Error e 1103 + | Ok () -> validate_data_model v) 1104 + (Ok ()) pairs 1105 + | None -> Error Type_not_string)) 1106 + | None -> Ok () 1075 1107 1076 1108 and validate_blob pairs = 1077 - (* Blob must have: $type = "blob", ref (CID link), mimeType (string), size (int) *) 1078 - let size_val = List.assoc_opt "size" pairs in 1079 - let ref_val = List.assoc_opt "ref" pairs in 1080 - match (size_val, ref_val) with 1081 - | Some (`String _), _ -> Error Blob_size_not_int 1082 - | _, None -> Error Blob_missing_ref 1083 - | Some (`Int _), Some ref_json -> 1084 - (* Validate the ref is a proper link *) 1085 - validate_data_model ref_json 1086 - | _ -> 1087 - (* Validate all fields *) 1088 - List.fold_left 1089 - (fun acc (_, v) -> 1090 - match acc with Error e -> Error e | Ok () -> validate_data_model v) 1091 - (Ok ()) pairs 1109 + let size_val = Atproto_json.get "size" pairs in 1110 + let ref_val = Atproto_json.get "ref" pairs in 1111 + match ref_val with 1112 + | None -> Error Blob_missing_ref 1113 + | Some ref_json -> ( 1114 + match size_val with 1115 + | Some sv -> ( 1116 + match Atproto_json.to_string_opt sv with 1117 + | Some _ -> Error Blob_size_not_int 1118 + | None -> ( 1119 + match Atproto_json.to_int_opt sv with 1120 + | Some _ -> validate_data_model ref_json 1121 + | None -> 1122 + List.fold_left 1123 + (fun acc (_, v) -> 1124 + match acc with 1125 + | Error e -> Error e 1126 + | Ok () -> validate_data_model v) 1127 + (Ok ()) pairs)) 1128 + | None -> 1129 + List.fold_left 1130 + (fun acc (_, v) -> 1131 + match acc with 1132 + | Error e -> Error e 1133 + | Ok () -> validate_data_model v) 1134 + (Ok ()) pairs) 1092 1135 1093 - (** Validate top-level - must be an object *) 1094 - let validate_top_level (j : Yojson.Basic.t) : (unit, data_model_error) result = 1095 - match j with 1096 - | `Assoc pairs -> 1097 - (* Validate all values recursively *) 1136 + let validate_top_level (j : Atproto_json.t) : (unit, data_model_error) result = 1137 + match Atproto_json.to_object_opt j with 1138 + | Some pairs -> 1098 1139 List.fold_left 1099 1140 (fun acc (_, v) -> 1100 1141 match acc with Error e -> Error e | Ok () -> validate_data_model v) 1101 1142 (Ok ()) pairs 1102 - | _ -> Error Top_level_not_object 1143 + | None -> Error Top_level_not_object 1103 1144 1104 1145 let test_data_model_valid () = 1105 1146 let fixtures = read_fixture_json "data-model/data-model-valid.json" in 1106 - match fixtures with 1107 - | `List items -> 1147 + match Atproto_json.to_array_opt fixtures with 1148 + | Some items -> 1108 1149 List.iter 1109 1150 (fun item -> 1110 - match item with 1111 - | `Assoc pairs -> ( 1151 + match Atproto_json.to_object_opt item with 1152 + | Some pairs -> ( 1112 1153 let note = 1113 - match List.assoc_opt "note" pairs with 1114 - | Some (`String s) -> s 1115 - | _ -> "unknown" 1154 + match Atproto_json.get_string_opt "note" pairs with 1155 + | Some s -> s 1156 + | None -> "unknown" 1116 1157 in 1117 - match List.assoc_opt "json" pairs with 1158 + match Atproto_json.get "json" pairs with 1118 1159 | Some json -> 1119 1160 let result = validate_top_level json in 1120 1161 Alcotest.(check bool) 1121 1162 (Printf.sprintf "valid: %s" note) 1122 1163 true (Result.is_ok result) 1123 1164 | None -> ()) 1124 - | _ -> ()) 1165 + | None -> ()) 1125 1166 items 1126 - | _ -> Alcotest.fail "Expected JSON array" 1167 + | None -> Alcotest.fail "Expected JSON array" 1127 1168 1128 1169 let test_data_model_invalid () = 1129 1170 let fixtures = read_fixture_json "data-model/data-model-invalid.json" in 1130 - match fixtures with 1131 - | `List items -> 1171 + match Atproto_json.to_array_opt fixtures with 1172 + | Some items -> 1132 1173 List.iter 1133 1174 (fun item -> 1134 - match item with 1135 - | `Assoc pairs -> ( 1175 + match Atproto_json.to_object_opt item with 1176 + | Some pairs -> ( 1136 1177 let note = 1137 - match List.assoc_opt "note" pairs with 1138 - | Some (`String s) -> s 1139 - | _ -> "unknown" 1178 + match Atproto_json.get_string_opt "note" pairs with 1179 + | Some s -> s 1180 + | None -> "unknown" 1140 1181 in 1141 - match List.assoc_opt "json" pairs with 1182 + match Atproto_json.get "json" pairs with 1142 1183 | Some json -> 1143 1184 let result = validate_top_level json in 1144 1185 Alcotest.(check bool) 1145 1186 (Printf.sprintf "invalid: %s" note) 1146 1187 true (Result.is_error result) 1147 1188 | None -> ()) 1148 - | _ -> ()) 1189 + | None -> ()) 1149 1190 items 1150 - | _ -> Alcotest.fail "Expected JSON array" 1191 + | None -> Alcotest.fail "Expected JSON array" 1151 1192 1152 1193 let data_model_tests = 1153 1194 [
+1 -1
test/lexicon/dune
··· 3 3 (package atproto-lexicon) 4 4 (deps 5 5 (source_tree ../fixtures/lexicon)) 6 - (libraries atproto-lexicon yojson alcotest)) 6 + (libraries atproto-lexicon alcotest))
+1 -1
test/mst/dune
··· 3 3 (package atproto-mst) 4 4 (deps 5 5 (source_tree ../fixtures/mst)) 6 - (libraries atproto_mst atproto_ipld alcotest yojson)) 6 + (libraries atproto_mst atproto_ipld atproto_json alcotest)) 7 7 8 8 (executable 9 9 (name debug_mst)
+20 -38
test/mst/test_mst.ml
··· 12 12 let ic = open_in path in 13 13 let content = really_input_string ic (in_channel_length ic) in 14 14 close_in ic; 15 - Yojson.Basic.from_string content 15 + match Atproto_json.decode content with 16 + | Ok json -> json 17 + | Error e -> failwith ("JSON parse error: " ^ e) 16 18 17 19 (* === Key height tests === *) 18 20 19 21 let test_key_heights () = 20 22 let fixtures = read_fixture_json "key_heights.json" in 21 - match fixtures with 22 - | `List items -> 23 + match Atproto_json.to_array_opt fixtures with 24 + | Some items -> 23 25 List.iter 24 26 (fun item -> 25 - match item with 26 - | `Assoc pairs -> ( 27 - let key = 28 - match List.assoc_opt "key" pairs with 29 - | Some (`String s) -> Some s 30 - | _ -> None 31 - in 32 - let expected_height = 33 - match List.assoc_opt "height" pairs with 34 - | Some (`Int h) -> Some h 35 - | _ -> None 36 - in 27 + match Atproto_json.to_object_opt item with 28 + | Some pairs -> ( 29 + let key = Atproto_json.get_string_opt "key" pairs in 30 + let expected_height = Atproto_json.get_int_opt "height" pairs in 37 31 match (key, expected_height) with 38 32 | Some k, Some expected -> 39 33 let actual = key_height k in ··· 41 35 (Printf.sprintf "height of %S" k) 42 36 expected actual 43 37 | _ -> ()) 44 - | _ -> ()) 38 + | None -> ()) 45 39 items 46 - | _ -> Alcotest.fail "Expected JSON array" 40 + | None -> Alcotest.fail "Expected JSON array" 47 41 48 42 (* === Common prefix tests === *) 49 43 50 44 let test_common_prefix () = 51 45 let fixtures = read_fixture_json "common_prefix.json" in 52 - match fixtures with 53 - | `List items -> 46 + match Atproto_json.to_array_opt fixtures with 47 + | Some items -> 54 48 List.iter 55 49 (fun item -> 56 - match item with 57 - | `Assoc pairs -> ( 58 - let left = 59 - match List.assoc_opt "left" pairs with 60 - | Some (`String s) -> Some s 61 - | _ -> None 62 - in 63 - let right = 64 - match List.assoc_opt "right" pairs with 65 - | Some (`String s) -> Some s 66 - | _ -> None 67 - in 68 - let expected_len = 69 - match List.assoc_opt "len" pairs with 70 - | Some (`Int n) -> Some n 71 - | _ -> None 72 - in 50 + match Atproto_json.to_object_opt item with 51 + | Some pairs -> ( 52 + let left = Atproto_json.get_string_opt "left" pairs in 53 + let right = Atproto_json.get_string_opt "right" pairs in 54 + let expected_len = Atproto_json.get_int_opt "len" pairs in 73 55 match (left, right, expected_len) with 74 56 | Some l, Some r, Some expected -> 75 57 let actual = common_prefix_len l r in ··· 77 59 (Printf.sprintf "prefix(%S, %S)" l r) 78 60 expected actual 79 61 | _ -> ()) 80 - | _ -> ()) 62 + | None -> ()) 81 63 items 82 - | _ -> Alcotest.fail "Expected JSON array" 64 + | None -> Alcotest.fail "Expected JSON array" 83 65 84 66 (* === MST operations tests === *) 85 67
+1 -1
test/sync/dune
··· 1 1 (test 2 2 (name test_sync) 3 - (libraries atproto-sync atproto-ipld atproto-mst yojson alcotest)) 3 + (libraries atproto-sync atproto-ipld atproto-mst atproto-json alcotest))
+21 -10
test/sync/test_sync.ml
··· 635 635 content) 636 636 else try_paths rest 637 637 in 638 - match Yojson.Safe.from_string (try_paths paths) with 639 - | `List fixtures -> fixtures 640 - | _ -> failwith "Expected array of fixtures" 638 + match Atproto_json.decode (try_paths paths) with 639 + | Ok json -> ( 640 + match Atproto_json.to_array_opt json with 641 + | Some fixtures -> fixtures 642 + | None -> failwith "Expected array of fixtures") 643 + | Error e -> failwith ("JSON parse error: " ^ e) 641 644 642 645 (** Extract string from JSON *) 643 - let json_string = function `String s -> s | _ -> failwith "Expected string" 646 + let json_string json = 647 + match Atproto_json.to_string_opt json with 648 + | Some s -> s 649 + | None -> failwith "Expected string" 644 650 645 651 (** Extract string list from JSON *) 646 - let json_string_list = function 647 - | `List items -> List.map json_string items 648 - | _ -> failwith "Expected array of strings" 652 + let json_string_list json = 653 + match Atproto_json.to_array_opt json with 654 + | Some items -> List.map json_string items 655 + | None -> failwith "Expected array of strings" 649 656 650 657 (** Get field from JSON object *) 651 - let json_field name = function 652 - | `Assoc pairs -> List.assoc name pairs 653 - | _ -> failwith ("Expected object with field " ^ name) 658 + let json_field name json = 659 + match Atproto_json.to_object_opt json with 660 + | Some pairs -> ( 661 + match Atproto_json.get name pairs with 662 + | Some v -> v 663 + | None -> failwith ("Expected object with field " ^ name)) 664 + | None -> failwith ("Expected object with field " ^ name) 654 665 655 666 module Mst = Atproto_mst 656 667