CSDS Key-Value Notation parser
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add READMEs for 16 packages, fix merlint issues, add KVN tests

READMEs for all new packages. Fix missing docs (ocm, stix, globe),
naming (project_visible→visible, label_info→info, shader_kind→kind),
add .ocamlformat to csvt, add 11 KVN tests.

+145
+49
README.md
··· 1 + # kvn 2 + 3 + CCSDS Key-Value Notation parser. 4 + 5 + Typed parser for CCSDS Key-Value Notation (KVN), the text format used by OEM, OCM, TDM, ADM, CDM, and other CCSDS navigation data messages. Provides line classification, epoch parsing, block iteration, and low-level parsing primitives. 6 + 7 + ## Installation 8 + 9 + ``` 10 + opam install kvn 11 + ``` 12 + 13 + ## Usage 14 + 15 + ```ocaml 16 + (* Parse a KVN file line by line *) 17 + let st = Kvn.of_string kvn_data in 18 + while not (Kvn.eof st) do 19 + match Kvn.next st with 20 + | Some (_n, line) -> 21 + (match Kvn.classify line with 22 + | Keyword (k, v) -> Printf.printf "%s = %s\n" k v 23 + | Data d -> Printf.printf "data: %s\n" d 24 + | Comment c -> Printf.printf "# %s\n" c 25 + | Blank -> ()) 26 + | None -> () 27 + done 28 + 29 + (* Iterate over a metadata block *) 30 + Kvn.iter_block ~start:"META_START" ~stop:"META_STOP" st 31 + ~on_keyword:(fun k v -> Printf.printf "%s = %s\n" k v) 32 + ~on_data:(fun _n _d -> ()) 33 + ``` 34 + 35 + ## API Overview 36 + 37 + - **`type line`** -- `Keyword of string * string`, `Data of string`, `Comment of string`, `Blank` 38 + - **`classify`** -- Classify a raw KVN line 39 + - **`parse_epoch`** -- Parse CCSDS epoch string to `Ptime.t` 40 + - **`type state`** -- Mutable line-by-line parser state 41 + - **`of_string`**, **`of_channel`** -- Create parser from string or channel 42 + - **`peek`**, **`next`**, **`advance`**, **`skip_blanks`**, **`eof`** -- Parser navigation 43 + - **`iter_block`** -- Iterate over a delimited block (e.g., `META_START`/`META_STOP`) 44 + - **`expect_keyword`** -- Check and consume a specific keyword 45 + - **`parse_float`**, **`parse_int`**, **`parse_floats`**, **`split_data`** -- Value parsing 46 + 47 + ## License 48 + 49 + ISC
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries kvn ptime alcotest fmt))
+1
test/test.ml
··· 1 + let () = Alcotest.run "kvn" [ Test_kvn.suite ]
+89
test/test_kvn.ml
··· 1 + (** KVN parser tests. 2 + 3 + Verifies line classification, epoch parsing, block iteration, and value 4 + parsing. *) 5 + 6 + (** Keyword lines are classified correctly. *) 7 + let test_classify_keyword () = 8 + match Kvn.classify "OBJECT_NAME = ISS" with 9 + | Kvn.Keyword ("OBJECT_NAME", "ISS") -> () 10 + | _ -> Alcotest.fail "expected Keyword" 11 + 12 + (** Data lines (no = sign) are classified correctly. *) 13 + let test_classify_data () = 14 + match Kvn.classify "2024-01-01T00:00:00.000 1.0 2.0 3.0" with 15 + | Kvn.Data _ -> () 16 + | _ -> Alcotest.fail "expected Data" 17 + 18 + (** Comment lines are classified correctly. *) 19 + let test_classify_comment () = 20 + match Kvn.classify "COMMENT This is a comment" with 21 + | Kvn.Comment "This is a comment" -> () 22 + | _ -> Alcotest.fail "expected Comment" 23 + 24 + (** Blank lines are classified correctly. *) 25 + let test_classify_blank () = 26 + match Kvn.classify " " with 27 + | Kvn.Blank -> () 28 + | _ -> Alcotest.fail "expected Blank" 29 + 30 + (** ISO epoch strings are parsed. *) 31 + let test_parse_epoch () = 32 + match Kvn.parse_epoch "2024-01-01T00:00:00.000" with 33 + | Some _ -> () 34 + | None -> Alcotest.fail "expected Some epoch" 35 + 36 + (** Invalid epoch strings return None. *) 37 + let test_parse_epoch_invalid () = 38 + Alcotest.(check bool) 39 + "invalid epoch" true 40 + (Kvn.parse_epoch "not-a-date" = None) 41 + 42 + (** Float parsing works. *) 43 + let test_parse_float () = 44 + Alcotest.(check (option (float 1e-10))) 45 + "parse 3.14" (Some 3.14) (Kvn.parse_float "3.14") 46 + 47 + (** Int parsing works. *) 48 + let test_parse_int () = 49 + Alcotest.(check (option int)) "parse 42" (Some 42) (Kvn.parse_int "42") 50 + 51 + (** split_data splits on whitespace. *) 52 + let test_split_data () = 53 + let parts = Kvn.split_data "1.0 2.0 3.0" in 54 + Alcotest.(check int) "3 tokens" 3 (List.length parts) 55 + 56 + (** parse_floats splits and parses. *) 57 + let test_parse_floats () = 58 + let fs = Kvn.parse_floats "1.0 2.0 3.0" in 59 + Alcotest.(check int) "3 floats" 3 (Array.length fs); 60 + Alcotest.(check (float 1e-10)) "first" 1.0 fs.(0) 61 + 62 + (** Line-by-line parser: peek, advance, next. *) 63 + let test_state_navigation () = 64 + let st = Kvn.of_string "OBJECT_NAME = ISS\nEPOCH = 2024-01-01" in 65 + (match Kvn.peek st with 66 + | Some (1, _) -> () 67 + | _ -> Alcotest.fail "expected line 1"); 68 + Kvn.advance st; 69 + (match Kvn.peek st with 70 + | Some (2, _) -> () 71 + | _ -> Alcotest.fail "expected line 2"); 72 + Kvn.advance st; 73 + Alcotest.(check bool) "eof" true (Kvn.eof st) 74 + 75 + let suite = 76 + ( "kvn", 77 + [ 78 + Alcotest.test_case "classify keyword" `Quick test_classify_keyword; 79 + Alcotest.test_case "classify data" `Quick test_classify_data; 80 + Alcotest.test_case "classify comment" `Quick test_classify_comment; 81 + Alcotest.test_case "classify blank" `Quick test_classify_blank; 82 + Alcotest.test_case "parse epoch" `Quick test_parse_epoch; 83 + Alcotest.test_case "parse epoch invalid" `Quick test_parse_epoch_invalid; 84 + Alcotest.test_case "parse float" `Quick test_parse_float; 85 + Alcotest.test_case "parse int" `Quick test_parse_int; 86 + Alcotest.test_case "split data" `Quick test_split_data; 87 + Alcotest.test_case "parse floats" `Quick test_parse_floats; 88 + Alcotest.test_case "state navigation" `Quick test_state_navigation; 89 + ] )
+3
test/test_kvn.mli
··· 1 + (** Tests for the KVN parser. *) 2 + 3 + val suite : string * unit Alcotest.test_case list