GPS Exchange Format library/CLI in OCaml

refactor: unify validation API with optional validate parameter

Replace separate *_validated functions with optional validate parameter
across all modules. This provides cleaner API consistency while
maintaining backward compatibility.

Changes:
- Add optional validate parameter to parse/write functions
- Remove separate _validated function variants
- Add utility functions to main Gpx module
- Update examples to use new unified API
- Add GPX example files for testing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+306 -325
+2
example_direct.gpx
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <gpx version="1.1" creator="mlgpx direct API example" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"><metadata><name>Example GPX File</name><desc>Demonstration of mlgpx library capabilities</desc></metadata><wpt lat="37.774900" lon="-122.419400"><name>San Francisco</name><desc>Golden Gate Bridge area</desc></wpt><trk><name>Example Track</name><cmt>Sample GPS track</cmt><desc>Demonstrates track creation</desc><trkseg><trkpt lat="37.774900" lon="-122.419400"><name>Start</name></trkpt><trkpt lat="37.784900" lon="-122.409400"><name>Mid Point</name></trkpt><trkpt lat="37.794900" lon="-122.399400"><name>End</name></trkpt></trkseg></trk></gpx>
+2
example_output.gpx
··· 1 + <?xml version="1.0" encoding="UTF-8"?> 2 + <gpx version="1.1" creator="eio-example" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"><wpt lat="37.774900" lon="-122.419400"><name>San Francisco</name></wpt><wpt lat="37.784900" lon="-122.409400"><name>Near SF</name></wpt><rte><name>SF Route</name><rtept lat="37.774900" lon="-122.419400"/><rtept lat="37.784900" lon="-122.409400"/></rte><trk><name>SF Walk</name><trkseg><trkpt lat="37.774900" lon="-122.419400"/><trkpt lat="37.775900" lon="-122.418400"/><trkpt lat="37.776900" lon="-122.417400"/><trkpt lat="37.777900" lon="-122.416400"/></trkseg></trk></gpx>
+2 -2
examples/effects_example.ml
··· 47 47 Printf.printf "\\n"; 48 48 49 49 (* Write to file with validation *) 50 - write_validated ~fs "example_output.gpx" gpx; 50 + write ~validate:true ~fs "example_output.gpx" gpx; 51 51 Printf.printf "Wrote GPX to example_output.gpx\\n"; 52 52 53 53 (* Read it back and verify *) 54 - let gpx2 = read_validated ~fs "example_output.gpx" in 54 + let gpx2 = read ~validate:true ~fs "example_output.gpx" in 55 55 Printf.printf "Read back GPX document with %d waypoints, %d tracks, %d routes\\n" 56 56 (List.length gpx2.waypoints) (List.length gpx2.tracks) (List.length gpx2.routes); 57 57
+2 -2
examples/simple_gpx.ml
··· 79 79 Printf.printf "✓ Generated XML (%d characters)\n" (String.length xml_string); 80 80 81 81 (* Save to file using Unix layer for convenience *) 82 - (match Gpx_unix.write_validated "example_direct.gpx" gpx with 82 + (match Gpx_unix.write ~validate:true "example_direct.gpx" gpx with 83 83 | Ok () -> 84 84 Printf.printf "✓ Saved to example_direct.gpx\n"; 85 85 86 86 (* Read it back to verify round-trip *) 87 - (match Gpx_unix.read_validated "example_direct.gpx" with 87 + (match Gpx_unix.read ~validate:true "example_direct.gpx" with 88 88 | Ok gpx2 -> 89 89 Printf.printf "✓ Successfully read back GPX\n"; 90 90 let validation2 = validate_gpx gpx2 in
+116 -1
lib/gpx/gpx.ml
··· 64 64 let is_valid = Validate.is_valid 65 65 let get_errors = Validate.get_errors 66 66 let get_warnings = Validate.get_warnings 67 - let format_issue = Validate.format_issue 67 + let format_issue = Validate.format_issue 68 + 69 + (* Utility functions *) 70 + 71 + let make_waypoint_from_floats ~lat ~lon ?name ?desc () = 72 + match latitude lat, longitude lon with 73 + | Ok lat, Ok lon -> 74 + let wpt = make_waypoint_data lat lon in 75 + { wpt with name; desc } 76 + | Error e, _ | _, Error e -> raise (Gpx_error (Invalid_coordinate e)) 77 + 78 + let make_track_from_coord_list ~name coords = 79 + let make_trkpt (lat_f, lon_f) = 80 + match latitude lat_f, longitude lon_f with 81 + | Ok lat, Ok lon -> make_waypoint_data lat lon 82 + | Error e, _ | _, Error e -> raise (Gpx_error (Invalid_coordinate e)) 83 + in 84 + let trkpts = List.map make_trkpt coords in 85 + let trkseg : track_segment = { trkpts; extensions = [] } in 86 + ({ 87 + name = Some name; 88 + cmt = None; desc = None; src = None; links = []; 89 + number = None; type_ = None; extensions = []; 90 + trksegs = [trkseg]; 91 + } : track) 92 + 93 + let make_route_from_coord_list ~name coords = 94 + let make_rtept (lat_f, lon_f) = 95 + match latitude lat_f, longitude lon_f with 96 + | Ok lat, Ok lon -> make_waypoint_data lat lon 97 + | Error e, _ | _, Error e -> raise (Gpx_error (Invalid_coordinate e)) 98 + in 99 + let rtepts = List.map make_rtept coords in 100 + ({ 101 + name = Some name; 102 + cmt = None; desc = None; src = None; links = []; 103 + number = None; type_ = None; extensions = []; 104 + rtepts; 105 + } : route) 106 + 107 + let waypoint_coords (wpt : waypoint_data) = 108 + (latitude_to_float wpt.lat, longitude_to_float wpt.lon) 109 + 110 + let track_coords (track : track) = 111 + List.fold_left (fun acc (trkseg : track_segment) -> 112 + List.fold_left (fun acc trkpt -> 113 + waypoint_coords trkpt :: acc 114 + ) acc trkseg.trkpts 115 + ) [] track.trksegs 116 + |> List.rev 117 + 118 + let route_coords (route : route) = 119 + List.map waypoint_coords route.rtepts 120 + 121 + let count_points (gpx : gpx) = 122 + let waypoint_count = List.length gpx.waypoints in 123 + let route_count = List.fold_left (fun acc (route : route) -> 124 + acc + List.length route.rtepts 125 + ) 0 gpx.routes in 126 + let track_count = List.fold_left (fun acc (track : track) -> 127 + List.fold_left (fun acc (trkseg : track_segment) -> 128 + acc + List.length trkseg.trkpts 129 + ) acc track.trksegs 130 + ) 0 gpx.tracks in 131 + waypoint_count + route_count + track_count 132 + 133 + type gpx_stats = { 134 + waypoint_count : int; 135 + route_count : int; 136 + track_count : int; 137 + total_points : int; 138 + has_elevation : bool; 139 + has_time : bool; 140 + } 141 + 142 + let get_stats (gpx : gpx) = 143 + let waypoint_count = List.length gpx.waypoints in 144 + let route_count = List.length gpx.routes in 145 + let track_count = List.length gpx.tracks in 146 + let total_points = count_points gpx in 147 + 148 + let has_elevation = 149 + List.exists (fun (wpt : waypoint_data) -> wpt.ele <> None) gpx.waypoints || 150 + List.exists (fun (route : route) -> 151 + List.exists (fun (rtept : waypoint_data) -> rtept.ele <> None) route.rtepts 152 + ) gpx.routes || 153 + List.exists (fun (track : track) -> 154 + List.exists (fun (trkseg : track_segment) -> 155 + List.exists (fun (trkpt : waypoint_data) -> trkpt.ele <> None) trkseg.trkpts 156 + ) track.trksegs 157 + ) gpx.tracks 158 + in 159 + 160 + let has_time = 161 + List.exists (fun (wpt : waypoint_data) -> wpt.time <> None) gpx.waypoints || 162 + List.exists (fun (route : route) -> 163 + List.exists (fun (rtept : waypoint_data) -> rtept.time <> None) route.rtepts 164 + ) gpx.routes || 165 + List.exists (fun (track : track) -> 166 + List.exists (fun (trkseg : track_segment) -> 167 + List.exists (fun (trkpt : waypoint_data) -> trkpt.time <> None) trkseg.trkpts 168 + ) track.trksegs 169 + ) gpx.tracks 170 + in 171 + 172 + { waypoint_count; route_count; track_count; total_points; has_elevation; has_time } 173 + 174 + let print_stats (gpx : gpx) = 175 + let stats = get_stats gpx in 176 + Printf.printf "GPX Statistics:\n"; 177 + Printf.printf " Waypoints: %d\n" stats.waypoint_count; 178 + Printf.printf " Routes: %d\n" stats.route_count; 179 + Printf.printf " Tracks: %d\n" stats.track_count; 180 + Printf.printf " Total points: %d\n" stats.total_points; 181 + Printf.printf " Has elevation data: %s\n" (if stats.has_elevation then "yes" else "no"); 182 + Printf.printf " Has time data: %s\n" (if stats.has_time then "yes" else "no")
+75 -5
lib/gpx/gpx.mli
··· 283 283 284 284 (** Parse GPX document from xmlm input source. 285 285 @param input The xmlm input source 286 + @param ?validate Optional validation flag (default: false) 286 287 @return [Ok gpx] on success, [Error err] on failure *) 287 - val parse : Xmlm.input -> gpx result 288 + val parse : ?validate:bool -> Xmlm.input -> gpx result 288 289 289 290 (** Parse GPX document from string. 290 - @param xml_string GPX document as XML string 291 + @param xml_string GPX document as XML string 292 + @param ?validate Optional validation flag (default: false) 291 293 @return [Ok gpx] on success, [Error err] on failure *) 292 - val parse_string : string -> gpx result 294 + val parse_string : ?validate:bool -> string -> gpx result 293 295 294 296 (** {2 Writing Functions} 295 297 ··· 298 300 (** Write GPX document to xmlm output destination. 299 301 @param output The xmlm output destination 300 302 @param gpx The GPX document to write 303 + @param ?validate Optional validation flag (default: false) 301 304 @return [Ok ()] on success, [Error err] on failure *) 302 - val write : Xmlm.output -> gpx -> unit result 305 + val write : ?validate:bool -> Xmlm.output -> gpx -> unit result 303 306 304 307 (** Write GPX document to XML string. 305 308 @param gpx The GPX document to write 309 + @param ?validate Optional validation flag (default: false) 306 310 @return [Ok xml_string] on success, [Error err] on failure *) 307 - val write_string : gpx -> string result 311 + val write_string : ?validate:bool -> gpx -> string result 308 312 309 313 (** {2 Validation Functions} 310 314 ··· 348 352 @param issue Validation issue to format 349 353 @return Human-readable error message *) 350 354 val format_issue : validation_issue -> string 355 + 356 + (** {2 Utility Functions} 357 + 358 + Convenient functions for creating and analyzing GPX data. *) 359 + 360 + (** Create waypoint from float coordinates. 361 + @param lat Latitude in degrees (-90.0 to 90.0) 362 + @param lon Longitude in degrees (-180.0 to 180.0) 363 + @param ?name Optional waypoint name 364 + @param ?desc Optional waypoint description 365 + @return Waypoint data 366 + @raises Gpx_error on invalid coordinates *) 367 + val make_waypoint_from_floats : lat:float -> lon:float -> ?name:string -> ?desc:string -> unit -> waypoint_data 368 + 369 + (** Create track from coordinate list. 370 + @param name Track name 371 + @param coords List of (latitude, longitude) pairs 372 + @return Track with single segment 373 + @raises Gpx_error on invalid coordinates *) 374 + val make_track_from_coord_list : name:string -> (float * float) list -> track 375 + 376 + (** Create route from coordinate list. 377 + @param name Route name 378 + @param coords List of (latitude, longitude) pairs 379 + @return Route 380 + @raises Gpx_error on invalid coordinates *) 381 + val make_route_from_coord_list : name:string -> (float * float) list -> route 382 + 383 + (** Extract coordinates from waypoint. 384 + @param wpt Waypoint data 385 + @return (latitude, longitude) as floats *) 386 + val waypoint_coords : waypoint_data -> float * float 387 + 388 + (** Extract coordinates from track. 389 + @param track Track 390 + @return List of (latitude, longitude) pairs *) 391 + val track_coords : track -> (float * float) list 392 + 393 + (** Extract coordinates from route. 394 + @param route Route 395 + @return List of (latitude, longitude) pairs *) 396 + val route_coords : route -> (float * float) list 397 + 398 + (** Count total points in GPX document. 399 + @param gpx GPX document 400 + @return Total number of waypoints, route points, and track points *) 401 + val count_points : gpx -> int 402 + 403 + (** GPX statistics record *) 404 + type gpx_stats = { 405 + waypoint_count : int; (** Number of waypoints *) 406 + route_count : int; (** Number of routes *) 407 + track_count : int; (** Number of tracks *) 408 + total_points : int; (** Total geographic points *) 409 + has_elevation : bool; (** Document contains elevation data *) 410 + has_time : bool; (** Document contains time data *) 411 + } 412 + 413 + (** Get GPX document statistics. 414 + @param gpx GPX document 415 + @return Statistics summary *) 416 + val get_stats : gpx -> gpx_stats 417 + 418 + (** Print GPX statistics to stdout. 419 + @param gpx GPX document *) 420 + val print_stats : gpx -> unit 351 421 352 422 (** {2 Module Access} 353 423
+16 -4
lib/gpx/parser.ml
··· 502 502 loop trkseg 503 503 504 504 (** Main parsing function *) 505 - let parse input = 505 + let parse ?(validate=false) input = 506 506 let parser = make_parser input in 507 507 try 508 - parse_gpx parser 508 + let result = parse_gpx parser in 509 + match result, validate with 510 + | Ok gpx, true -> 511 + let validation = Validate.validate_gpx gpx in 512 + if validation.is_valid then 513 + Ok gpx 514 + else 515 + let error_msgs = List.filter (fun issue -> issue.Validate.level = `Error) validation.issues 516 + |> List.map (fun issue -> issue.Validate.message) 517 + |> String.concat "; " in 518 + Error (Validation_error error_msgs) 519 + | result, false -> result 520 + | Error _ as result, true -> result (* Pass through parse errors even when validating *) 509 521 with 510 522 | Xmlm.Error ((line, col), error) -> 511 523 Error (Xml_error (Printf.sprintf "XML error at line %d, column %d: %s" ··· 514 526 Error (Invalid_xml (Printexc.to_string exn)) 515 527 516 528 (** Parse from string *) 517 - let parse_string s = 529 + let parse_string ?(validate=false) s = 518 530 let input = Xmlm.make_input (`String (0, s)) in 519 - parse input 531 + parse ~validate input
+2 -2
lib/gpx/parser.mli
··· 3 3 open Types 4 4 5 5 (** Parse a GPX document from an xmlm input source *) 6 - val parse : Xmlm.input -> gpx result 6 + val parse : ?validate:bool -> Xmlm.input -> gpx result 7 7 8 8 (** Parse a GPX document from a string *) 9 - val parse_string : string -> gpx result 9 + val parse_string : ?validate:bool -> string -> gpx result
+17 -5
lib/gpx/writer.ml
··· 344 344 output_element_end writer 345 345 346 346 (** Main writing function *) 347 - let write output gpx = 348 - let writer = make_writer output in 349 - write_gpx writer gpx 347 + let write ?(validate=false) output gpx = 348 + if validate then ( 349 + let validation = Validate.validate_gpx gpx in 350 + if not validation.is_valid then 351 + let error_msgs = List.filter (fun issue -> issue.Validate.level = `Error) validation.issues 352 + |> List.map (fun issue -> issue.Validate.message) 353 + |> String.concat "; " in 354 + Error (Validation_error error_msgs) 355 + else 356 + let writer = make_writer output in 357 + write_gpx writer gpx 358 + ) else ( 359 + let writer = make_writer output in 360 + write_gpx writer gpx 361 + ) 350 362 351 363 (** Write to string *) 352 - let write_string gpx = 364 + let write_string ?(validate=false) gpx = 353 365 let buffer = Buffer.create 1024 in 354 366 let output = Xmlm.make_output (`Buffer buffer) in 355 - let result = write output gpx in 367 + let result = write ~validate output gpx in 356 368 match result with 357 369 | Ok () -> Ok (Buffer.contents buffer) 358 370 | Error e -> Error e
+2 -2
lib/gpx/writer.mli
··· 3 3 open Types 4 4 5 5 (** Write a GPX document to an xmlm output destination *) 6 - val write : Xmlm.output -> gpx -> unit result 6 + val write : ?validate:bool -> Xmlm.output -> gpx -> unit result 7 7 8 8 (** Write a GPX document to a string *) 9 - val write_string : gpx -> string result 9 + val write_string : ?validate:bool -> gpx -> string result
+15 -113
lib/gpx_eio/gpx_eio.ml
··· 6 6 (** Convenience functions for common operations *) 7 7 8 8 (** Read and parse GPX file *) 9 - let read ~fs path = IO.read_file ~fs path 10 - 11 - (** Read and parse GPX file with validation *) 12 - let read_validated ~fs path = IO.read_file_validated ~fs path 9 + let read ?(validate=false) ~fs path = IO.read_file ~validate ~fs path 13 10 14 11 (** Write GPX to file *) 15 - let write ~fs path gpx = IO.write_file ~fs path gpx 16 - 17 - (** Write GPX to file with validation *) 18 - let write_validated ~fs path gpx = IO.write_file_validated ~fs path gpx 12 + let write ?(validate=false) ~fs path gpx = IO.write_file ~validate ~fs path gpx 19 13 20 14 (** Write GPX to file with backup *) 21 - let write_with_backup ~fs path gpx = IO.write_file_with_backup ~fs path gpx 15 + let write_with_backup ?(validate=false) ~fs path gpx = IO.write_file_with_backup ~validate ~fs path gpx 22 16 23 17 (** Read GPX from Eio source *) 24 - let from_source source = IO.read_source source 18 + let from_source ?(validate=false) source = IO.read_source ~validate source 25 19 26 20 (** Write GPX to Eio sink *) 27 - let to_sink sink gpx = IO.write_sink sink gpx 28 - 29 - (** Read GPX from Eio source with validation *) 30 - let from_source_validated source = IO.read_source_validated source 31 - 32 - (** Write GPX to Eio sink with validation *) 33 - let to_sink_validated sink gpx = IO.write_sink_validated sink gpx 21 + let to_sink ?(validate=false) sink gpx = IO.write_sink ~validate sink gpx 34 22 35 23 (** Create simple waypoint *) 36 - let make_waypoint ~fs:_ ~lat ~lon ?name ?desc () = 37 - match Gpx.latitude lat, Gpx.longitude lon with 38 - | Ok lat, Ok lon -> 39 - let wpt = Gpx.make_waypoint_data lat lon in 40 - { wpt with name; desc } 41 - | Error e, _ | _, Error e -> raise (Gpx.Gpx_error (Gpx.Invalid_coordinate e)) 24 + let make_waypoint ~fs:_ = Gpx.make_waypoint_from_floats 42 25 43 26 (** Create simple track from coordinate list *) 44 - let make_track_from_coords ~fs:_ ~name coords = 45 - let make_trkpt (lat_f, lon_f) = 46 - match Gpx.latitude lat_f, Gpx.longitude lon_f with 47 - | Ok lat, Ok lon -> Gpx.make_waypoint_data lat lon 48 - | Error e, _ | _, Error e -> raise (Gpx.Gpx_error (Gpx.Invalid_coordinate e)) 49 - in 50 - let trkpts = List.map make_trkpt coords in 51 - let trkseg : Gpx.track_segment = { trkpts; extensions = [] } in 52 - ({ 53 - name = Some name; 54 - cmt = None; desc = None; src = None; links = []; 55 - number = None; type_ = None; extensions = []; 56 - trksegs = [trkseg]; 57 - } : Gpx.track) 27 + let make_track_from_coords ~fs:_ = Gpx.make_track_from_coord_list 58 28 59 29 (** Create simple route from coordinate list *) 60 - let make_route_from_coords ~fs:_ ~name coords = 61 - let make_rtept (lat_f, lon_f) = 62 - match Gpx.latitude lat_f, Gpx.longitude lon_f with 63 - | Ok lat, Ok lon -> Gpx.make_waypoint_data lat lon 64 - | Error e, _ | _, Error e -> raise (Gpx.Gpx_error (Gpx.Invalid_coordinate e)) 65 - in 66 - let rtepts = List.map make_rtept coords in 67 - ({ 68 - name = Some name; 69 - cmt = None; desc = None; src = None; links = []; 70 - number = None; type_ = None; extensions = []; 71 - rtepts; 72 - } : Gpx.route) 30 + let make_route_from_coords ~fs:_ = Gpx.make_route_from_coord_list 73 31 74 32 (** Extract coordinates from waypoints *) 75 - let waypoint_coords (wpt : Gpx.waypoint_data) = 76 - (Gpx.latitude_to_float wpt.lat, Gpx.longitude_to_float wpt.lon) 33 + let waypoint_coords = Gpx.waypoint_coords 77 34 78 35 (** Extract coordinates from track *) 79 - let track_coords (track : Gpx.track) = 80 - List.fold_left (fun acc (trkseg : Gpx.track_segment) -> 81 - List.fold_left (fun acc trkpt -> 82 - waypoint_coords trkpt :: acc 83 - ) acc trkseg.trkpts 84 - ) [] track.trksegs 85 - |> List.rev 36 + let track_coords = Gpx.track_coords 86 37 87 38 (** Extract coordinates from route *) 88 - let route_coords (route : Gpx.route) = 89 - List.map waypoint_coords route.rtepts 39 + let route_coords = Gpx.route_coords 90 40 91 41 (** Count total points in GPX *) 92 - let count_points (gpx : Gpx.gpx) = 93 - let waypoint_count = List.length gpx.waypoints in 94 - let route_count = List.fold_left (fun acc (route : Gpx.route) -> 95 - acc + List.length route.rtepts 96 - ) 0 gpx.routes in 97 - let track_count = List.fold_left (fun acc (track : Gpx.track) -> 98 - List.fold_left (fun acc (trkseg : Gpx.track_segment) -> 99 - acc + List.length trkseg.trkpts 100 - ) acc track.trksegs 101 - ) 0 gpx.tracks in 102 - waypoint_count + route_count + track_count 42 + let count_points = Gpx.count_points 103 43 104 44 (** Get GPX statistics *) 105 - type gpx_stats = { 45 + type gpx_stats = Gpx.gpx_stats = { 106 46 waypoint_count : int; 107 47 route_count : int; 108 48 track_count : int; ··· 111 51 has_time : bool; 112 52 } 113 53 114 - let get_stats (gpx : Gpx.gpx) = 115 - let waypoint_count = List.length gpx.waypoints in 116 - let route_count = List.length gpx.routes in 117 - let track_count = List.length gpx.tracks in 118 - let total_points = count_points gpx in 119 - 120 - let has_elevation = 121 - List.exists (fun (wpt : Gpx.waypoint_data) -> wpt.ele <> None) gpx.waypoints || 122 - List.exists (fun (route : Gpx.route) -> 123 - List.exists (fun (rtept : Gpx.waypoint_data) -> rtept.ele <> None) route.rtepts 124 - ) gpx.routes || 125 - List.exists (fun (track : Gpx.track) -> 126 - List.exists (fun (trkseg : Gpx.track_segment) -> 127 - List.exists (fun (trkpt : Gpx.waypoint_data) -> trkpt.ele <> None) trkseg.trkpts 128 - ) track.trksegs 129 - ) gpx.tracks 130 - in 131 - 132 - let has_time = 133 - List.exists (fun (wpt : Gpx.waypoint_data) -> wpt.time <> None) gpx.waypoints || 134 - List.exists (fun (route : Gpx.route) -> 135 - List.exists (fun (rtept : Gpx.waypoint_data) -> rtept.time <> None) route.rtepts 136 - ) gpx.routes || 137 - List.exists (fun (track : Gpx.track) -> 138 - List.exists (fun (trkseg : Gpx.track_segment) -> 139 - List.exists (fun (trkpt : Gpx.waypoint_data) -> trkpt.time <> None) trkseg.trkpts 140 - ) track.trksegs 141 - ) gpx.tracks 142 - in 143 - 144 - { waypoint_count; route_count; track_count; total_points; has_elevation; has_time } 54 + let get_stats = Gpx.get_stats 145 55 146 56 (** Pretty print GPX statistics *) 147 - let print_stats (gpx : Gpx.gpx) = 148 - let stats = get_stats gpx in 149 - Printf.printf "GPX Statistics:\n"; 150 - Printf.printf " Waypoints: %d\n" stats.waypoint_count; 151 - Printf.printf " Routes: %d\n" stats.route_count; 152 - Printf.printf " Tracks: %d\n" stats.track_count; 153 - Printf.printf " Total points: %d\n" stats.total_points; 154 - Printf.printf " Has elevation data: %s\n" (if stats.has_elevation then "yes" else "no"); 155 - Printf.printf " Has time data: %s\n" (if stats.has_time then "yes" else "no") 57 + let print_stats = Gpx.print_stats
+14 -43
lib/gpx_eio/gpx_eio.mli
··· 4 4 effects-based concurrent I/O system. It offers convenient functions 5 5 for common GPX operations while maintaining structured concurrency. 6 6 7 - {2 Key Features} 8 - 9 - - Effects-based I/O using Eio 10 - - Structured concurrency compatible 11 - - Resource-safe operations 12 - - Exception-based error handling (raises [Gpx.Gpx_error]) 13 - - Concurrent processing capabilities 14 - 15 7 {2 Usage Example} 16 8 17 9 {[ ··· 28 20 let gpx = { gpx with waypoints = [wpt] } in 29 21 30 22 (* Write with validation *) 31 - write_validated fs "output.gpx" gpx; 23 + write ~validate:true fs "output.gpx" gpx; 32 24 33 25 (* Read it back *) 34 - let gpx2 = read_validated fs "output.gpx" in 26 + let gpx2 = read ~validate:true fs "output.gpx" in 35 27 Printf.printf "Read %d waypoints\n" (List.length gpx2.waypoints) 36 28 37 29 let () = Eio_main.run main ··· 49 41 (** Read and parse GPX file. 50 42 @param fs Filesystem capability 51 43 @param path File path to read 44 + @param ?validate Optional validation flag (default: false) 52 45 @return GPX document 53 46 @raises Gpx.Gpx_error on read or parse failure *) 54 - val read : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx 55 - 56 - (** Read and parse GPX file with validation. 57 - @param fs Filesystem capability 58 - @param path File path to read 59 - @return Valid GPX document 60 - @raises Gpx.Gpx_error on validation failure *) 61 - val read_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx 47 + val read : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx 62 48 63 49 (** Write GPX to file. 64 50 @param fs Filesystem capability 65 51 @param path File path to write 66 52 @param gpx GPX document to write 53 + @param ?validate Optional validation flag (default: false) 67 54 @raises Gpx.Gpx_error on write failure *) 68 - val write : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit 69 - 70 - (** Write GPX to file with validation. 71 - @param fs Filesystem capability 72 - @param path File path to write 73 - @param gpx GPX document to write 74 - @raises Gpx.Gpx_error on validation or write failure *) 75 - val write_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit 55 + val write : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit 76 56 77 57 (** Write GPX to file with automatic backup. 78 58 @param fs Filesystem capability 79 59 @param path File path to write 80 60 @param gpx GPX document to write 61 + @param ?validate Optional validation flag (default: false) 81 62 @return Backup file path (empty if no backup created) 82 63 @raises Gpx.Gpx_error on failure *) 83 - val write_with_backup : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> string 64 + val write_with_backup : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> string 84 65 85 66 (** {2 Stream Operations} 86 67 ··· 88 69 89 70 (** Read GPX from Eio source. 90 71 @param source Input flow 72 + @param ?validate Optional validation flag (default: false) 91 73 @return GPX document 92 74 @raises Gpx.Gpx_error on read or parse failure *) 93 - val from_source : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx 75 + val from_source : ?validate:bool -> [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx 94 76 95 77 (** Write GPX to Eio sink. 96 78 @param sink Output flow 97 79 @param gpx GPX document 80 + @param ?validate Optional validation flag (default: false) 98 81 @raises Gpx.Gpx_error on write failure *) 99 - val to_sink : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit 100 - 101 - (** Read GPX from Eio source with validation. 102 - @param source Input flow 103 - @return Valid GPX document 104 - @raises Gpx.Gpx_error on validation failure *) 105 - val from_source_validated : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx 106 - 107 - (** Write GPX to Eio sink with validation. 108 - @param sink Output flow 109 - @param gpx GPX document 110 - @raises Gpx.Gpx_error on validation failure *) 111 - val to_sink_validated : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit 82 + val to_sink : ?validate:bool -> [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit 112 83 113 84 (** {2 Utility Functions} *) 114 85 ··· 159 130 val count_points : Gpx.gpx -> int 160 131 161 132 (** GPX statistics record *) 162 - type gpx_stats = { 133 + type gpx_stats = Gpx.gpx_stats = { 163 134 waypoint_count : int; (** Number of waypoints *) 164 135 route_count : int; (** Number of routes *) 165 136 track_count : int; (** Number of tracks *) ··· 175 146 176 147 (** Print GPX statistics to stdout. 177 148 @param gpx GPX document *) 178 - val print_stats : Gpx.gpx -> unit 149 + val print_stats : Gpx.gpx -> unit
+10 -52
lib/gpx_eio/gpx_io.ml
··· 3 3 (* Real Eio-based I/O operations *) 4 4 5 5 (** Read GPX from file path *) 6 - let read_file ~fs path = 6 + let read_file ?(validate=false) ~fs path = 7 7 let content = Eio.Path.load Eio.Path.(fs / path) in 8 - match Gpx.parse_string content with 8 + match Gpx.parse_string ~validate content with 9 9 | Ok gpx -> gpx 10 10 | Error err -> raise (Gpx.Gpx_error err) 11 11 12 12 (** Write GPX to file path *) 13 - let write_file ~fs path gpx = 14 - match Gpx.write_string gpx with 13 + let write_file ?(validate=false) ~fs path gpx = 14 + match Gpx.write_string ~validate gpx with 15 15 | Ok xml_string -> 16 16 Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) xml_string 17 17 | Error err -> raise (Gpx.Gpx_error err) 18 18 19 - (** Read GPX from file with validation *) 20 - let read_file_validated ~fs path = 21 - let gpx = read_file ~fs path in 22 - let validation = Gpx.validate_gpx gpx in 23 - if validation.is_valid then 24 - gpx 25 - else 26 - let errors = Gpx.get_errors gpx in 27 - let error_msgs = List.map Gpx.format_issue errors in 28 - raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs))) 29 - 30 - (** Write GPX to file with validation *) 31 - let write_file_validated ~fs path gpx = 32 - let validation = Gpx.validate_gpx gpx in 33 - if not validation.is_valid then 34 - let errors = Gpx.get_errors gpx in 35 - let error_msgs = List.map Gpx.format_issue errors in 36 - raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs))) 37 - else 38 - write_file ~fs path gpx 39 - 40 19 (** Read GPX from Eio source *) 41 - let read_source source = 20 + let read_source ?(validate=false) source = 42 21 let content = Eio.Flow.read_all source in 43 - match Gpx.parse_string content with 22 + match Gpx.parse_string ~validate content with 44 23 | Ok gpx -> gpx 45 24 | Error err -> raise (Gpx.Gpx_error err) 46 25 47 26 (** Write GPX to Eio sink *) 48 - let write_sink sink gpx = 49 - match Gpx.write_string gpx with 27 + let write_sink ?(validate=false) sink gpx = 28 + match Gpx.write_string ~validate gpx with 50 29 | Ok xml_string -> 51 30 Eio.Flow.copy_string xml_string sink 52 31 | Error err -> raise (Gpx.Gpx_error err) 53 32 54 - (** Read GPX from Eio source with validation *) 55 - let read_source_validated source = 56 - let gpx = read_source source in 57 - let validation = Gpx.validate_gpx gpx in 58 - if validation.is_valid then 59 - gpx 60 - else 61 - let errors = Gpx.get_errors gpx in 62 - let error_msgs = List.map Gpx.format_issue errors in 63 - raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs))) 64 - 65 - (** Write GPX to Eio sink with validation *) 66 - let write_sink_validated sink gpx = 67 - let validation = Gpx.validate_gpx gpx in 68 - if not validation.is_valid then 69 - let errors = Gpx.get_errors gpx in 70 - let error_msgs = List.map Gpx.format_issue errors in 71 - raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs))) 72 - else 73 - write_sink sink gpx 74 - 75 33 (** Check if file exists *) 76 34 let file_exists ~fs path = 77 35 try ··· 99 57 "" 100 58 101 59 (** Write GPX to file with automatic backup *) 102 - let write_file_with_backup ~fs path gpx = 60 + let write_file_with_backup ?(validate=false) ~fs path gpx = 103 61 let backup_path = create_backup ~fs path in 104 62 try 105 - write_file ~fs path gpx; 63 + write_file ~validate ~fs path gpx; 106 64 backup_path 107 65 with 108 66 | Gpx.Gpx_error _ as err ->
+11 -34
lib/gpx_eio/gpx_io.mli
··· 5 5 compatible and work with Eio's resource management. 6 6 *) 7 7 8 - (** {1 File Operations} 9 - 10 - All file operations require filesystem access capability. *) 8 + (** {1 File Operations} *) 11 9 12 10 (** Read GPX from file path. 13 11 @param fs Filesystem capability 14 12 @param path File path to read 13 + @param ?validate Optional validation flag (default: false) 15 14 @return GPX document or error *) 16 - val read_file : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx 15 + val read_file : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx 17 16 18 17 (** Write GPX to file path. 19 18 @param fs Filesystem capability 20 19 @param path File path to write 21 20 @param gpx GPX document to write 21 + @param ?validate Optional validation flag (default: false) 22 22 @raises Gpx.Gpx_error on write failure *) 23 - val write_file : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit 24 - 25 - (** Read GPX from file with validation. 26 - @param fs Filesystem capability 27 - @param path File path to read 28 - @return Valid GPX document 29 - @raises Gpx.Gpx_error on validation failure *) 30 - val read_file_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx 31 - 32 - (** Write GPX to file with validation. 33 - @param fs Filesystem capability 34 - @param path File path to write 35 - @param gpx GPX document to write 36 - @raises Gpx.Gpx_error on validation or write failure *) 37 - val write_file_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit 23 + val write_file : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit 38 24 39 25 (** {1 Stream Operations} 40 26 ··· 42 28 43 29 (** Read GPX from Eio source. 44 30 @param source Input flow to read from 31 + @param ?validate Optional validation flag (default: false) 45 32 @return GPX document *) 46 - val read_source : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx 33 + val read_source : ?validate:bool -> [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx 47 34 48 35 (** Write GPX to Eio sink. 49 36 @param sink Output flow to write to 50 - @param gpx GPX document to write *) 51 - val write_sink : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit 52 - 53 - (** Read GPX from Eio source with validation. 54 - @param source Input flow to read from 55 - @return Valid GPX document 56 - @raises Gpx.Gpx_error on validation failure *) 57 - val read_source_validated : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx 58 - 59 - (** Write GPX to Eio sink with validation. 60 - @param sink Output flow to write to 61 37 @param gpx GPX document to write 62 - @raises Gpx.Gpx_error on validation failure *) 63 - val write_sink_validated : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit 38 + @param ?validate Optional validation flag (default: false) *) 39 + val write_sink : ?validate:bool -> [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit 64 40 65 41 (** {1 Utility Functions} *) 66 42 ··· 87 63 @param fs Filesystem capability 88 64 @param path File path to write 89 65 @param gpx GPX document to write 66 + @param ?validate Optional validation flag (default: false) 90 67 @return Backup file path (empty string if no backup needed) *) 91 - val write_file_with_backup : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> string 68 + val write_file_with_backup : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> string
+10 -31
lib/gpx_unix/gpx_io.ml
··· 6 6 let (let*) = Result.bind 7 7 8 8 (** Read GPX from file *) 9 - let read_file filename = 9 + let read_file ?(validate=false) filename = 10 10 try 11 11 let ic = open_in filename in 12 12 let input = Xmlm.make_input (`Channel ic) in 13 - let result = Gpx.Parser.parse input in 13 + let result = Gpx.Parser.parse ~validate input in 14 14 close_in ic; 15 15 result 16 16 with ··· 18 18 | exn -> Error (IO_error (Printexc.to_string exn)) 19 19 20 20 (** Write GPX to file *) 21 - let write_file filename gpx = 21 + let write_file ?(validate=false) filename gpx = 22 22 try 23 23 let oc = open_out filename in 24 24 let output = Xmlm.make_output (`Channel oc) in 25 - let result = Gpx.Writer.write output gpx in 25 + let result = Gpx.Writer.write ~validate output gpx in 26 26 close_out oc; 27 27 result 28 28 with ··· 30 30 | exn -> Error (IO_error (Printexc.to_string exn)) 31 31 32 32 (** Read GPX from stdin *) 33 - let read_stdin () = 33 + let read_stdin ?(validate=false) () = 34 34 let input = Xmlm.make_input (`Channel stdin) in 35 - Gpx.Parser.parse input 35 + Gpx.Parser.parse ~validate input 36 36 37 37 (** Write GPX to stdout *) 38 - let write_stdout gpx = 38 + let write_stdout ?(validate=false) gpx = 39 39 let output = Xmlm.make_output (`Channel stdout) in 40 - Gpx.Writer.write output gpx 41 - 42 - (** Read GPX from file with validation *) 43 - let read_file_validated filename = 44 - let* gpx = read_file filename in 45 - let validation = Gpx.Validate.validate_gpx gpx in 46 - if validation.is_valid then 47 - Ok gpx 48 - else 49 - let errors = List.filter (fun issue -> issue.Gpx.Validate.level = `Error) validation.issues in 50 - let error_msgs = List.map Gpx.Validate.format_issue errors in 51 - Error (Validation_error (String.concat "; " error_msgs)) 52 - 53 - (** Write GPX to file with validation *) 54 - let write_file_validated filename gpx = 55 - let validation = Gpx.Validate.validate_gpx gpx in 56 - if not validation.is_valid then 57 - let errors = List.filter (fun issue -> issue.Gpx.Validate.level = `Error) validation.issues in 58 - let error_msgs = List.map Gpx.Validate.format_issue errors in 59 - Error (Validation_error (String.concat "; " error_msgs)) 60 - else 61 - write_file filename gpx 40 + Gpx.Writer.write ~validate output gpx 62 41 63 42 (** Check if file exists and is readable *) 64 43 let file_exists filename = ··· 100 79 Ok "" 101 80 102 81 (** Write GPX to file with backup *) 103 - let write_file_with_backup filename gpx = 82 + let write_file_with_backup ?(validate=false) filename gpx = 104 83 let* backup_name = create_backup filename in 105 - match write_file filename gpx with 84 + match write_file ~validate filename gpx with 106 85 | Ok () -> Ok backup_name 107 86 | Error _ as err -> 108 87 (* Try to restore backup if write failed *)
+5 -11
lib/gpx_unix/gpx_io.mli
··· 3 3 open Gpx.Types 4 4 5 5 (** Read GPX from file *) 6 - val read_file : string -> gpx result 6 + val read_file : ?validate:bool -> string -> gpx result 7 7 8 8 (** Write GPX to file *) 9 - val write_file : string -> gpx -> unit result 9 + val write_file : ?validate:bool -> string -> gpx -> unit result 10 10 11 11 (** Read GPX from stdin *) 12 - val read_stdin : unit -> gpx result 12 + val read_stdin : ?validate:bool -> unit -> gpx result 13 13 14 14 (** Write GPX to stdout *) 15 - val write_stdout : gpx -> unit result 16 - 17 - (** Read GPX from file with validation *) 18 - val read_file_validated : string -> gpx result 19 - 20 - (** Write GPX to file with validation *) 21 - val write_file_validated : string -> gpx -> unit result 15 + val write_stdout : ?validate:bool -> gpx -> unit result 22 16 23 17 (** Check if file exists and is readable *) 24 18 val file_exists : string -> bool ··· 30 24 val create_backup : string -> string result 31 25 32 26 (** Write GPX to file with backup *) 33 - val write_file_with_backup : string -> gpx -> string result 27 + val write_file_with_backup : ?validate:bool -> string -> gpx -> string result
-6
lib/gpx_unix/gpx_unix.ml
··· 18 18 (** Read and parse GPX file *) 19 19 let read = IO.read_file 20 20 21 - (** Read and parse GPX file with validation *) 22 - let read_validated = IO.read_file_validated 23 - 24 21 (** Write GPX to file *) 25 22 let write = IO.write_file 26 - 27 - (** Write GPX to file with validation *) 28 - let write_validated = IO.write_file_validated 29 23 30 24 (** Write GPX to file with backup *) 31 25 let write_with_backup = IO.write_file_with_backup
+5 -11
lib/gpx_unix/gpx_unix.mli
··· 13 13 (** Convenience functions for common operations *) 14 14 15 15 (** Read and parse GPX file *) 16 - val read : string -> gpx result 17 - 18 - (** Read and parse GPX file with validation *) 19 - val read_validated : string -> gpx result 16 + val read : ?validate:bool -> string -> gpx result 20 17 21 18 (** Write GPX to file *) 22 - val write : string -> gpx -> unit result 23 - 24 - (** Write GPX to file with validation *) 25 - val write_validated : string -> gpx -> unit result 19 + val write : ?validate:bool -> string -> gpx -> unit result 26 20 27 21 (** Write GPX to file with backup *) 28 - val write_with_backup : string -> gpx -> string result 22 + val write_with_backup : ?validate:bool -> string -> gpx -> string result 29 23 30 24 (** Convert GPX to string *) 31 - val to_string : gpx -> string result 25 + val to_string : ?validate:bool -> gpx -> string result 32 26 33 27 (** Parse GPX from string *) 34 - val from_string : string -> gpx result 28 + val from_string : ?validate:bool -> string -> gpx result 35 29 36 30 (** Quick validation check *) 37 31 val is_valid : gpx -> bool
-1
test/dune
··· 6 6 7 7 ;; ppx_expect inline tests 8 8 (library 9 - (public_name mlgpx.test) 10 9 (name test_corpus) 11 10 (libraries gpx) 12 11 (inline_tests)