GPS Exchange Format library/CLI in OCaml

Add comprehensive test corpus and Eio implementation with XML fixes

This commit adds a complete test infrastructure covering both Unix and Eio
implementations, along with XML writer/parser fixes for proper round-trip
functionality.

**New Test Infrastructure:**
- Created 9 synthetic GPX test files covering diverse features:
* simple_waypoints.gpx, detailed_waypoints.gpx, simple_route.gpx
* simple_track.gpx, multi_segment_track.gpx, comprehensive.gpx
* minimal.gpx, edge_cases.gpx, invalid.gpx
- Added comprehensive alcotest suite (58 passing tests):
* Unix vs Eio equivalence testing
* Round-trip testing (write→parse→write consistency)
* Validation testing across all test files
* Performance comparison between backends
* Error handling validation
- Added ppx_expect inline tests for parser validation

**New Eio Implementation:**
- Complete Eio-based I/O layer using real Eio APIs
- Exception-based error handling (vs result-based Unix layer)
- Proper Eio.Path and Eio.Flow integration
- Effects-style example with structured concurrency
- Optional compilation when eio_main available

**Critical XML Fixes:**
- Fixed XML writer namespace declaration format
- Corrected XML header generation (removed double declaration)
- Ensured generated XML correctly parses (round-trip compatibility)
- Fixed attribute namespace handling for GPX schema validation

**Enhanced Documentation:**
- Updated README with three-layer architecture description
- Added comprehensive API examples for both Unix and Eio layers
- Documented all major features and usage patterns

All tests pass: Unix parsing, Eio parsing, cross-backend equivalence,
round-trip validation, and error handling work correctly.

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

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

+71 -10
README.md
···
## Architecture Overview
-
The library is split into two main components:
+
The library is split into three main components:
### Core Library (`gpx`)
- **Portable**: No Unix dependencies, works with js_of_ocaml
···
### Unix Layer (`gpx_unix`)
- **File I/O**: Convenient functions for reading/writing GPX files
+
- **Result-based**: Explicit error handling with `result` types
- **Validation**: Built-in validation with detailed error reporting
- **Utilities**: Helper functions for common GPX operations
+
### Effects-Style Layer (`gpx_eio`)
+
- **Exception-based**: Simplified error handling with exceptions
+
- **Effects-style API**: Similar to Eio patterns but using standard Unix I/O
+
- **Resource-safe**: Automatic file handle management
+
- **High-level**: Convenient functions for common operations
+
## Key Features
- ✅ **Complete GPX 1.1 support**: Waypoints, routes, tracks, metadata, extensions
···
mlgpx/
├── lib/
│ ├── gpx/ # Portable core library
-
│ │ ├── gpx_types.ml # Type definitions with smart constructors
-
│ │ ├── gpx_parser.ml # Streaming XML parser
-
│ │ ├── gpx_writer.ml # Streaming XML writer
-
│ │ └── gpx_validate.ml # Validation and error checking
-
│ └── gpx_unix/ # Unix I/O layer
-
│ ├── gpx_io.ml # File operations with error handling
-
│ └── gpx_unix.ml # High-level convenience API
+
│ │ ├── types.ml # Type definitions with smart constructors
+
│ │ ├── parser.ml # Streaming XML parser
+
│ │ ├── writer.ml # Streaming XML writer
+
│ │ ├── validate.ml # Validation and error checking
+
│ │ └── gpx.ml[i] # Main interface with direct access to all types
+
│ ├── gpx_unix/ # Unix I/O layer (result-based)
+
│ │ ├── gpx_io.ml # File operations with error handling
+
│ │ └── gpx_unix.ml # High-level convenience API
+
│ └── gpx_eio/ # Effects-style layer (exception-based)
+
│ ├── gpx_io.ml # File operations with exceptions
+
│ └── gpx_eio.ml # High-level effects-style API
├── examples/ # Usage examples
└── test/ # Test suite
```
···
val Gpx_writer.write_string : gpx -> string result
```
-
### File Operations
+
### File Operations (Result-based)
```ocaml
(* Simple file I/O *)
val Gpx_unix.read : string -> gpx result
···
val Gpx_unix.write_with_backup : string -> gpx -> string result
```
+
### Effects-Style Operations (Exception-based)
+
```ocaml
+
(* Simple file I/O *)
+
val Gpx_eio.read : unit -> string -> gpx
+
val Gpx_eio.write : unit -> string -> gpx -> unit
+
+
(* With validation *)
+
val Gpx_eio.read_validated : unit -> string -> gpx
+
val Gpx_eio.write_validated : unit -> string -> gpx -> unit
+
+
(* With backup *)
+
val Gpx_eio.write_with_backup : unit -> string -> gpx -> string
+
+
(* Utility functions *)
+
val Gpx_eio.make_waypoint : unit -> lat:float -> lon:float -> ?name:string -> unit -> waypoint_data
+
val Gpx_eio.make_track_from_coords : unit -> name:string -> (float * float) list -> track
+
```
+
### Validation
```ocaml
type validation_result = {
···
- **Validation**: Optional, can be disabled for performance-critical applications
- **Extensions**: Parsed lazily, minimal overhead when unused
-
## Usage Example
+
## Usage Examples
+
+
### Result-based API (Explicit Error Handling)
```ocaml
open Gpx_unix
···
match create_simple_gpx () with
| Ok () -> Printf.printf "GPX created successfully\n"
| Error e -> Printf.eprintf "Error: %s\n" (error_to_string e)
+
```
+
+
### Effects-Style API (Exception-based)
+
+
```ocaml
+
open Gpx_eio
+
+
let create_simple_gpx () =
+
try
+
(* Create waypoints *)
+
let waypoint = make_waypoint () ~lat:37.7749 ~lon:(-122.4194)
+
~name:"San Francisco" () in
+
+
(* Create track from coordinates *)
+
let coords = [(37.7749, -122.4194); (37.7849, -122.4094)] in
+
let track = make_track_from_coords () ~name:"Sample Track" coords in
+
+
(* Create GPX document *)
+
let gpx = Gpx.make_gpx ~creator:"mlgpx example" in
+
let gpx = { gpx with waypoints = [waypoint]; tracks = [track] } in
+
+
(* Validate and write *)
+
write_validated () "output.gpx" gpx;
+
Printf.printf "GPX created successfully\n"
+
+
with
+
| Gpx.Gpx_error err ->
+
Printf.eprintf "GPX Error: %s\n" (Gpx.error_to_string err)
+
+
let () = create_simple_gpx ()
```
## Dependencies
+2 -2
dune-project
···
-
(lang dune 3.0)
+
(lang dune 3.15)
(package
(name mlgpx)
-
(depends ocaml dune xmlm ptime)
+
(depends ocaml dune xmlm ptime eio ppx_expect alcotest eio_main)
(synopsis "OCaml library for parsing and generating GPX files")
(description
"mlgpx is a streaming GPX (GPS Exchange Format) library for OCaml. It provides a portable core library using the xmlm streaming XML parser, with a separate Unix layer for file I/O operations. The library supports the complete GPX 1.1 specification including waypoints, routes, tracks, and metadata with strong type safety and validation.")
+7 -1
examples/dune
···
(executable
(public_name simple_gpx)
(name simple_gpx)
-
(libraries gpx_unix))
+
(libraries gpx_unix))
+
+
(executable
+
(public_name effects_example)
+
(name effects_example)
+
(libraries gpx_eio eio_main)
+
(optional))
+88
examples/effects_example.ml
···
+
(** Example using GPX with real Eio effects-based API
+
+
This demonstrates the real Eio-based API with structured concurrency
+
and proper resource management.
+
**)
+
+
open Gpx_eio
+
+
let main env =
+
try
+
let fs = Eio.Stdenv.fs env in
+
+
(* Create some GPS coordinates *)
+
let lat1 = Gpx.latitude 37.7749 |> Result.get_ok in
+
let lon1 = Gpx.longitude (-122.4194) |> Result.get_ok in
+
let lat2 = Gpx.latitude 37.7849 |> Result.get_ok in
+
let lon2 = Gpx.longitude (-122.4094) |> Result.get_ok in
+
+
(* Create waypoints *)
+
let waypoint1 = make_waypoint ~fs ~lat:(Gpx.latitude_to_float lat1) ~lon:(Gpx.longitude_to_float lon1) ~name:"San Francisco" () in
+
let waypoint2 = make_waypoint ~fs ~lat:(Gpx.latitude_to_float lat2) ~lon:(Gpx.longitude_to_float lon2) ~name:"Near SF" () in
+
+
(* Create a simple track from coordinates *)
+
let track = make_track_from_coords ~fs ~name:"SF Walk" [
+
(37.7749, -122.4194);
+
(37.7759, -122.4184);
+
(37.7769, -122.4174);
+
(37.7779, -122.4164);
+
] in
+
+
(* Create a route *)
+
let route = make_route_from_coords ~fs ~name:"SF Route" [
+
(37.7749, -122.4194);
+
(37.7849, -122.4094);
+
] in
+
+
(* Create GPX document with all elements *)
+
let gpx = Gpx.make_gpx ~creator:"eio-example" in
+
let gpx = { gpx with
+
waypoints = [waypoint1; waypoint2];
+
tracks = [track];
+
routes = [route];
+
} in
+
+
Printf.printf "Created GPX document with:\\n";
+
print_stats gpx;
+
Printf.printf "\\n";
+
+
(* Write to file with validation *)
+
write_validated ~fs "example_output.gpx" gpx;
+
Printf.printf "Wrote GPX to example_output.gpx\\n";
+
+
(* Read it back and verify *)
+
let gpx2 = read_validated ~fs "example_output.gpx" in
+
Printf.printf "Read back GPX document with %d waypoints, %d tracks, %d routes\\n"
+
(List.length gpx2.waypoints) (List.length gpx2.tracks) (List.length gpx2.routes);
+
+
(* Extract coordinates from track *)
+
match gpx2.tracks with
+
| track :: _ ->
+
let coords = track_coords track in
+
Printf.printf "Track coordinates: %d points\\n" (List.length coords);
+
List.iteri (fun i (lat, lon) ->
+
Printf.printf " Point %d: %.4f, %.4f\\n" i lat lon
+
) coords
+
| [] -> Printf.printf "No tracks found\\n";
+
+
Printf.printf "\\nEio example completed successfully!\\n"
+
+
with
+
| Gpx.Gpx_error err ->
+
let error_msg = match err with
+
| Gpx.Invalid_xml s -> "Invalid XML: " ^ s
+
| Gpx.Invalid_coordinate s -> "Invalid coordinate: " ^ s
+
| Gpx.Missing_required_attribute (elem, attr) ->
+
Printf.sprintf "Missing required attribute '%s' in element '%s'" attr elem
+
| Gpx.Missing_required_element s -> "Missing required element: " ^ s
+
| Gpx.Validation_error s -> "Validation error: " ^ s
+
| Gpx.Xml_error s -> "XML error: " ^ s
+
| Gpx.IO_error s -> "I/O error: " ^ s
+
in
+
Printf.eprintf "GPX Error: %s\\n" error_msg;
+
exit 1
+
| exn ->
+
Printf.eprintf "Unexpected error: %s\\n" (Printexc.to_string exn);
+
exit 1
+
+
let () = Eio_main.run main
+111 -72
examples/simple_gpx.ml
···
-
(** Example demonstrating basic GPX operations *)
+
(** Example demonstrating basic GPX operations using the direct API *)
-
open Gpx_unix
+
open Gpx
let () =
-
(* Create a simple GPX document with waypoints and a track *)
-
let creator = "mlgpx example" in
-
let gpx = Types.make_gpx ~creator in
+
Printf.printf "=== MLGpx Library Example ===\n\n";
-
(* Add some waypoints *)
-
let waypoints = [
-
(37.7749, -122.4194, "San Francisco", "Golden Gate Bridge area");
-
(40.7128, -74.0060, "New York", "Manhattan");
-
(51.5074, -0.1278, "London", "Central London");
-
] in
-
-
let create_waypoints acc (lat, lon, name, desc) =
-
match make_waypoint ~lat ~lon ~name ~desc () with
-
| Ok wpt -> wpt :: acc
-
| Error e ->
-
Printf.eprintf "Error creating waypoint %s: %s\n" name
-
(match e with Invalid_coordinate s -> s | _ -> "unknown");
-
acc
+
(* Create coordinates using direct API *)
+
let create_coordinate_pair lat_f lon_f =
+
match latitude lat_f, longitude lon_f with
+
| Ok lat, Ok lon -> Ok (lat, lon)
+
| Error e, _ | _, Error e -> Error (Invalid_coordinate e)
in
-
let wpts = List.fold_left create_waypoints [] waypoints |> List.rev in
-
let gpx = { gpx with waypoints = wpts } in
+
(* Create a simple waypoint *)
+
(match create_coordinate_pair 37.7749 (-122.4194) with
+
| Ok (lat, lon) ->
+
let wpt = make_waypoint_data lat lon in
+
let wpt = { wpt with name = Some "San Francisco"; desc = Some "Golden Gate Bridge area" } in
+
Printf.printf "✓ Created waypoint: %s\n" (Option.value wpt.name ~default:"<unnamed>");
+
+
(* Create GPX document *)
+
let gpx = make_gpx ~creator:"mlgpx direct API example" in
+
let gpx = { gpx with waypoints = [wpt] } in
+
+
(* Add metadata *)
+
let metadata = { empty_metadata with
+
name = Some "Example GPX File";
+
desc = Some "Demonstration of mlgpx library capabilities";
+
time = None (* Ptime_clock not available in this context *)
+
} in
+
let gpx = { gpx with metadata = Some metadata } in
+
+
(* Create a simple track *)
+
let track_points = [
+
(37.7749, -122.4194, Some "Start");
+
(37.7849, -122.4094, Some "Mid Point");
+
(37.7949, -122.3994, Some "End");
+
] in
+
+
let create_track_points acc (lat_f, lon_f, name) =
+
match create_coordinate_pair lat_f lon_f with
+
| Ok (lat, lon) ->
+
let trkpt = make_waypoint_data lat lon in
+
let trkpt = { trkpt with name } in
+
trkpt :: acc
+
| Error _ -> acc
+
in
+
+
let trkpts = List.fold_left create_track_points [] track_points |> List.rev in
+
let trkseg = { trkpts; extensions = [] } in
+
let track = {
+
name = Some "Example Track";
+
cmt = Some "Sample GPS track";
+
desc = Some "Demonstrates track creation";
+
src = None; links = []; number = None; type_ = None; extensions = [];
+
trksegs = [trkseg];
+
} in
+
let gpx = { gpx with tracks = [track] } in
+
+
Printf.printf "✓ Created track with %d points\n" (List.length trkpts);
+
+
(* Validate the document *)
+
let validation = validate_gpx gpx in
+
Printf.printf "✓ GPX validation: %s\n" (if validation.is_valid then "PASSED" else "FAILED");
+
+
if not validation.is_valid then (
+
Printf.printf "Validation issues:\n";
+
List.iter (fun issue ->
+
Printf.printf " %s: %s\n"
+
(match issue.level with `Error -> "ERROR" | `Warning -> "WARNING")
+
issue.message
+
) validation.issues
+
);
+
+
(* Convert to XML string *)
+
(match write_string gpx with
+
| Ok xml_string ->
+
Printf.printf "✓ Generated XML (%d characters)\n" (String.length xml_string);
+
+
(* Save to file using Unix layer for convenience *)
+
(match Gpx_unix.write_validated "example_direct.gpx" gpx with
+
| Ok () ->
+
Printf.printf "✓ Saved to example_direct.gpx\n";
+
+
(* Read it back to verify round-trip *)
+
(match Gpx_unix.read_validated "example_direct.gpx" with
+
| Ok gpx2 ->
+
Printf.printf "✓ Successfully read back GPX\n";
+
let validation2 = validate_gpx gpx2 in
+
Printf.printf "✓ Round-trip validation: %s\n"
+
(if validation2.is_valid then "PASSED" else "FAILED");
+
Printf.printf " Waypoints: %d, Tracks: %d\n"
+
(List.length gpx2.waypoints) (List.length gpx2.tracks)
+
| Error e ->
+
Printf.printf "✗ Error reading back: %s\n"
+
(match e with
+
| Invalid_xml s -> "Invalid XML: " ^ s
+
| Validation_error s -> "Validation: " ^ s
+
| IO_error s -> "I/O: " ^ s
+
| _ -> "Unknown error"))
+
| Error e ->
+
Printf.printf "✗ Error saving file: %s\n"
+
(match e with
+
| IO_error s -> s
+
| Validation_error s -> s
+
| _ -> "Unknown error"))
+
| Error e ->
+
Printf.printf "✗ Error generating XML: %s\n"
+
(match e with
+
| Invalid_xml s -> s
+
| Xml_error s -> s
+
| _ -> "Unknown error"))
+
| Error e ->
+
Printf.printf "✗ Error creating coordinates: %s\n"
+
(match e with Invalid_coordinate s -> s | _ -> "Unknown error"));
-
(* Create a simple track *)
-
let track_coords = [
-
(37.7749, -122.4194);
-
(37.7849, -122.4094);
-
(37.7949, -122.3994);
-
(37.8049, -122.3894);
-
] in
-
-
let track_result = make_track_from_coords ~name:"Sample Track" track_coords in
-
let gpx = match track_result with
-
| Ok track -> { gpx with tracks = [track] }
-
| Error e ->
-
Printf.eprintf "Error creating track: %s\n"
-
(match e with Invalid_coordinate s -> s | _ -> "unknown");
-
gpx
-
in
-
-
(* Validate the GPX *)
-
let validation = validate gpx in
-
Printf.printf "GPX is valid: %s\n" (string_of_bool validation.is_valid);
-
-
if not validation.is_valid then (
-
List.iter (fun issue ->
-
Printf.printf "%s\n" (Validate.format_issue issue)
-
) validation.issues
-
);
-
-
(* Print statistics *)
-
print_stats gpx;
-
-
(* Write to file *)
-
(match write_validated "example.gpx" gpx with
-
| Ok () -> Printf.printf "GPX written to example.gpx\n"
-
| Error e ->
-
Printf.eprintf "Error writing GPX: %s\n"
-
(match e with
-
| IO_error s | Validation_error s -> s
-
| _ -> "unknown"));
-
-
(* Read it back and verify *)
-
(match read_validated "example.gpx" with
-
| Ok gpx2 ->
-
Printf.printf "Successfully read back GPX file\n";
-
let stats2 = get_stats gpx2 in
-
Printf.printf "Read back %d waypoints, %d tracks\n"
-
stats2.waypoint_count stats2.track_count
-
| Error e ->
-
Printf.eprintf "Error reading GPX: %s\n"
-
(match e with
-
| IO_error s | Validation_error s -> s
-
| _ -> "unknown"))
+
Printf.printf "\n=== Example Complete ===\n"
+58 -4
lib/gpx/gpx.ml
···
(** {1 MLGpx - OCaml GPX Library} *)
-
(** Core types and data structures *)
+
(** Core type definitions and utilities *)
module Types = Types
-
(** Streaming parser *)
+
(** Streaming XML parser *)
module Parser = Parser
-
(** Streaming writer *)
+
(** Streaming XML writer *)
module Writer = Writer
(** Validation engine *)
-
module Validate = Validate
+
module Validate = Validate
+
+
(* Re-export core types for direct access *)
+
type latitude = Types.latitude
+
type longitude = Types.longitude
+
type degrees = Types.degrees
+
type fix_type = Types.fix_type = None_fix | Fix_2d | Fix_3d | Dgps | Pps
+
type person = Types.person = { name : string option; email : string option; link : link option }
+
and link = Types.link = { href : string; text : string option; type_ : string option }
+
type copyright = Types.copyright = { author : string; year : int option; license : string option }
+
type bounds = Types.bounds = { minlat : latitude; minlon : longitude; maxlat : latitude; maxlon : longitude }
+
type extension_content = Types.extension_content = Text of string | Elements of extension list | Mixed of string * extension list
+
and extension = Types.extension = { namespace : string option; name : string; attributes : (string * string) list; content : extension_content }
+
type metadata = Types.metadata = { name : string option; desc : string option; author : person option; copyright : copyright option; links : link list; time : Ptime.t option; keywords : string option; bounds : bounds option; extensions : extension list }
+
type waypoint_data = Types.waypoint_data = { lat : latitude; lon : longitude; ele : float option; time : Ptime.t option; magvar : degrees option; geoidheight : float option; name : string option; cmt : string option; desc : string option; src : string option; links : link list; sym : string option; type_ : string option; fix : fix_type option; sat : int option; hdop : float option; vdop : float option; pdop : float option; ageofdgpsdata : float option; dgpsid : int option; extensions : extension list }
+
type waypoint = Types.waypoint
+
type route_point = Types.route_point
+
type track_point = Types.track_point
+
type route = Types.route = { name : string option; cmt : string option; desc : string option; src : string option; links : link list; number : int option; type_ : string option; extensions : extension list; rtepts : route_point list }
+
type track_segment = Types.track_segment = { trkpts : track_point list; extensions : extension list }
+
type track = Types.track = { name : string option; cmt : string option; desc : string option; src : string option; links : link list; number : int option; type_ : string option; extensions : extension list; trksegs : track_segment list }
+
type gpx = Types.gpx = { version : string; creator : string; metadata : metadata option; waypoints : waypoint list; routes : route list; tracks : track list; extensions : extension list }
+
type error = Types.error = Invalid_xml of string | Invalid_coordinate of string | Missing_required_attribute of string * string | Missing_required_element of string | Validation_error of string | Xml_error of string | IO_error of string
+
exception Gpx_error = Types.Gpx_error
+
type 'a result = ('a, error) Result.t
+
type validation_issue = Validate.validation_issue = { level : [`Error | `Warning]; message : string; location : string option }
+
type validation_result = Validate.validation_result = { issues : validation_issue list; is_valid : bool }
+
+
(* Re-export core functions *)
+
let latitude = Types.latitude
+
let longitude = Types.longitude
+
let degrees = Types.degrees
+
let latitude_to_float = Types.latitude_to_float
+
let longitude_to_float = Types.longitude_to_float
+
let degrees_to_float = Types.degrees_to_float
+
let fix_type_to_string = Types.fix_type_to_string
+
let fix_type_of_string = Types.fix_type_of_string
+
let make_waypoint_data = Types.make_waypoint_data
+
let empty_metadata = Types.empty_metadata
+
let make_gpx = Types.make_gpx
+
+
(* Re-export parser functions *)
+
let parse = Parser.parse
+
let parse_string = Parser.parse_string
+
+
(* Re-export writer functions *)
+
let write = Writer.write
+
let write_string = Writer.write_string
+
+
(* Re-export validation functions *)
+
let validate_gpx = Validate.validate_gpx
+
let is_valid = Validate.is_valid
+
let get_errors = Validate.get_errors
+
let get_warnings = Validate.get_warnings
+
let format_issue = Validate.format_issue
+347 -59
lib/gpx/gpx.mli
···
(** {1 MLGpx - OCaml GPX Library}
-
A library for parsing and generating GPX (GPS Exchange Format) files.
+
A high-quality OCaml library for parsing and generating GPX (GPS Exchange Format) files.
+
GPX is a standardized XML format for exchanging GPS data between applications and devices.
-
The library is split into two main components:
-
- {b Core Library (gpx)}: Portable core library with no Unix dependencies
-
- {b Unix Layer (gpx_unix)}: Convenient functions for file I/O and validation
+
{2 Overview}
-
{2 Key Features}
+
The GPX format defines a standard way to describe waypoints, routes, and tracks.
+
This library provides a complete implementation of GPX 1.1 with strong type safety
+
and memory-efficient streaming processing.
-
- ✅ Complete GPX 1.1 support: Waypoints, routes, tracks, metadata, extensions
-
- ✅ Streaming parser/writer: Memory-efficient for large files
-
- ✅ Strong type safety: Validated coordinates, GPS fix types, etc.
-
- ✅ Comprehensive validation: Detailed error and warning reporting
-
- ✅ Extension support: Handle custom XML elements
-
- ✅ Cross-platform: Core library has no Unix dependencies
+
{b Key Features:}
+
- ✅ Complete GPX 1.1 support with all standard elements
+
- ✅ Type-safe coordinate validation (WGS84 datum)
+
- ✅ Memory-efficient streaming parser and writer
+
- ✅ Comprehensive validation with detailed error reporting
+
- ✅ Extension support for custom elements
+
- ✅ Cross-platform (core has no Unix dependencies)
-
{2 Usage Example}
+
{2 Quick Start}
{[
open Gpx
-
let create_simple_gpx () =
-
(* Create waypoints *)
-
let* waypoint = Types.make_waypoint ~lat:37.7749 ~lon:(-122.4194)
-
~name:"San Francisco" () in
-
-
(* Create track from coordinates *)
-
let coords = [(37.7749, -122.4194); (37.7849, -122.4094)] in
-
let* track = make_track_from_coords ~name:"Sample Track" coords in
-
-
(* Create GPX document *)
-
let gpx = Types.make_gpx ~creator:"mlgpx example" in
-
let gpx = { gpx with waypoints = [waypoint]; tracks = [track] } in
-
-
(* Write to string *)
-
Writer.write_string gpx
+
(* Create coordinates *)
+
let* lat = latitude 37.7749 in
+
let* lon = longitude (-122.4194) in
+
+
(* Create a waypoint *)
+
let wpt = make_waypoint_data lat lon in
+
let wpt = { wpt with name = Some "San Francisco" } in
+
+
(* Create GPX document *)
+
let gpx = make_gpx ~creator:"mlgpx" in
+
let gpx = { gpx with waypoints = [wpt] } in
+
+
(* Convert to XML string *)
+
write_string gpx
]}
-
{2 Module Organization} *)
+
{2 Core Types} *)
+
+
(** {3 Geographic Coordinates}
+
+
All coordinates use the WGS84 datum as specified by the GPX standard. *)
+
+
(** Latitude coordinate (-90.0 to 90.0 degrees).
+
Private type ensures validation through smart constructor. *)
+
type latitude = Types.latitude
+
+
(** Longitude coordinate (-180.0 to 180.0 degrees).
+
Private type ensures validation through smart constructor. *)
+
type longitude = Types.longitude
+
+
(** Degrees for magnetic variation (0.0 to 360.0 degrees).
+
Private type ensures validation through smart constructor. *)
+
type degrees = Types.degrees
+
+
(** Create validated latitude coordinate.
+
@param lat Latitude in degrees (-90.0 to 90.0)
+
@return [Ok lat] if valid, [Error msg] if out of range *)
+
val latitude : float -> (latitude, string) result
+
+
(** Create validated longitude coordinate.
+
@param lon Longitude in degrees (-180.0 to 180.0)
+
@return [Ok lon] if valid, [Error msg] if out of range *)
+
val longitude : float -> (longitude, string) result
+
+
(** Create validated degrees value.
+
@param deg Degrees (0.0 to 360.0)
+
@return [Ok deg] if valid, [Error msg] if out of range *)
+
val degrees : float -> (degrees, string) result
+
+
(** Convert latitude back to float *)
+
val latitude_to_float : latitude -> float
+
+
(** Convert longitude back to float *)
+
val longitude_to_float : longitude -> float
+
+
(** Convert degrees back to float *)
+
val degrees_to_float : degrees -> float
+
+
(** {3 GPS Fix Types}
+
+
Standard GPS fix types as defined in the GPX specification. *)
+
+
(** GPS fix type indicating the quality/type of GPS reading *)
+
type fix_type = Types.fix_type =
+
| None_fix (** No fix available *)
+
| Fix_2d (** 2D fix (latitude/longitude) *)
+
| Fix_3d (** 3D fix (latitude/longitude/altitude) *)
+
| Dgps (** Differential GPS *)
+
| Pps (** Precise Positioning Service *)
+
+
(** Convert fix type to string representation *)
+
val fix_type_to_string : fix_type -> string
+
+
(** Parse fix type from string *)
+
val fix_type_of_string : string -> fix_type option
+
+
(** {3 Metadata Elements} *)
+
+
(** Person information for author, copyright holder, etc. *)
+
type person = Types.person = {
+
name : string option; (** Person's name *)
+
email : string option; (** Email address *)
+
link : link option; (** Link to person's website *)
+
}
+
+
(** External link with optional description and type *)
+
and link = Types.link = {
+
href : string; (** URL of the link *)
+
text : string option; (** Text description of link *)
+
type_ : string option; (** MIME type of linked content *)
+
}
+
+
(** Copyright information for the GPX file *)
+
type copyright = Types.copyright = {
+
author : string; (** Copyright holder *)
+
year : int option; (** Year of copyright *)
+
license : string option; (** License terms *)
+
}
+
+
(** Geographic bounds - minimum bounding rectangle *)
+
type bounds = Types.bounds = {
+
minlat : latitude; (** Minimum latitude *)
+
minlon : longitude; (** Minimum longitude *)
+
maxlat : latitude; (** Maximum latitude *)
+
maxlon : longitude; (** Maximum longitude *)
+
}
+
+
(** Extension content for custom elements *)
+
type extension_content = Types.extension_content =
+
| Text of string (** Text content *)
+
| Elements of extension list (** Child elements *)
+
| Mixed of string * extension list (** Mixed text and elements *)
+
+
(** Extension element for custom data *)
+
and extension = Types.extension = {
+
namespace : string option; (** XML namespace *)
+
name : string; (** Element name *)
+
attributes : (string * string) list; (** Element attributes *)
+
content : extension_content; (** Element content *)
+
}
+
+
(** GPX file metadata containing information about the file itself *)
+
type metadata = Types.metadata = {
+
name : string option; (** Name of GPX file *)
+
desc : string option; (** Description of contents *)
+
author : person option; (** Person who created GPX file *)
+
copyright : copyright option; (** Copyright information *)
+
links : link list; (** Related links *)
+
time : Ptime.t option; (** Creation/modification time *)
+
keywords : string option; (** Keywords for searching *)
+
bounds : bounds option; (** Geographic bounds *)
+
extensions : extension list; (** Custom extensions *)
+
}
-
(** {2 Core Types and Data Structures}
-
-
All GPX data types, coordinate validation, and smart constructors. *)
+
(** Create empty metadata record *)
+
val empty_metadata : metadata
+
+
(** {3 Geographic Points}
+
+
All geographic points (waypoints, route points, track points) share the same structure. *)
+
+
(** Base waypoint data structure used for all geographic points.
+
Contains position, time, and various GPS-related fields. *)
+
type waypoint_data = Types.waypoint_data = {
+
lat : latitude; (** Latitude coordinate *)
+
lon : longitude; (** Longitude coordinate *)
+
ele : float option; (** Elevation in meters *)
+
time : Ptime.t option; (** Time of GPS reading *)
+
magvar : degrees option; (** Magnetic variation at point *)
+
geoidheight : float option; (** Height of geoid above WGS84 ellipsoid *)
+
name : string option; (** Point name *)
+
cmt : string option; (** GPS comment *)
+
desc : string option; (** Point description *)
+
src : string option; (** Source of data *)
+
links : link list; (** Related links *)
+
sym : string option; (** GPS symbol name *)
+
type_ : string option; (** Point classification *)
+
fix : fix_type option; (** Type of GPS fix *)
+
sat : int option; (** Number of satellites *)
+
hdop : float option; (** Horizontal dilution of precision *)
+
vdop : float option; (** Vertical dilution of precision *)
+
pdop : float option; (** Position dilution of precision *)
+
ageofdgpsdata : float option; (** Age of DGPS data *)
+
dgpsid : int option; (** DGPS station ID *)
+
extensions : extension list; (** Custom extensions *)
+
}
+
+
(** Create basic waypoint data with required coordinates *)
+
val make_waypoint_data : latitude -> longitude -> waypoint_data
+
+
(** Individual waypoint - a point of interest *)
+
type waypoint = Types.waypoint
+
+
(** Route point - point along a planned route *)
+
type route_point = Types.route_point
+
+
(** Track point - recorded position along an actual path *)
+
type track_point = Types.track_point
+
+
(** {3 Routes}
+
+
A route is an ordered list of waypoints representing a planned path. *)
+
+
(** Route definition - ordered list of waypoints for navigation *)
+
type route = Types.route = {
+
name : string option; (** Route name *)
+
cmt : string option; (** GPS comment *)
+
desc : string option; (** Route description *)
+
src : string option; (** Source of data *)
+
links : link list; (** Related links *)
+
number : int option; (** Route number *)
+
type_ : string option; (** Route classification *)
+
extensions : extension list; (** Custom extensions *)
+
rtepts : route_point list; (** Route points *)
+
}
+
+
(** {3 Tracks}
+
+
A track represents an actual recorded path, consisting of track segments. *)
+
+
(** Track segment - continuous set of track points *)
+
type track_segment = Types.track_segment = {
+
trkpts : track_point list; (** Track points in segment *)
+
extensions : extension list; (** Custom extensions *)
+
}
+
+
(** Track definition - recorded path made up of segments *)
+
type track = Types.track = {
+
name : string option; (** Track name *)
+
cmt : string option; (** GPS comment *)
+
desc : string option; (** Track description *)
+
src : string option; (** Source of data *)
+
links : link list; (** Related links *)
+
number : int option; (** Track number *)
+
type_ : string option; (** Track classification *)
+
extensions : extension list; (** Custom extensions *)
+
trksegs : track_segment list; (** Track segments *)
+
}
+
+
(** {3 Main GPX Document}
+
+
The root GPX element contains metadata and collections of waypoints, routes, and tracks. *)
+
+
(** Main GPX document conforming to GPX 1.1 standard *)
+
type gpx = Types.gpx = {
+
version : string; (** GPX version (always "1.1") *)
+
creator : string; (** Creating application *)
+
metadata : metadata option; (** File metadata *)
+
waypoints : waypoint list; (** Waypoints *)
+
routes : route list; (** Routes *)
+
tracks : track list; (** Tracks *)
+
extensions : extension list; (** Custom extensions *)
+
}
+
+
(** Create GPX document with required creator field *)
+
val make_gpx : creator:string -> gpx
+
+
(** {3 Error Handling} *)
+
+
(** Errors that can occur during GPX processing *)
+
type error = Types.error =
+
| Invalid_xml of string (** XML parsing error *)
+
| Invalid_coordinate of string (** Coordinate validation error *)
+
| Missing_required_attribute of string * string (** Missing XML attribute *)
+
| Missing_required_element of string (** Missing XML element *)
+
| Validation_error of string (** GPX validation error *)
+
| Xml_error of string (** XML processing error *)
+
| IO_error of string (** I/O error *)
+
+
(** Exception type for GPX errors *)
+
exception Gpx_error of error
+
+
(** Result type for operations that may fail *)
+
type 'a result = ('a, error) Result.t
+
+
(** {2 Parsing Functions}
+
+
Parse GPX documents from XML input sources. *)
+
+
(** Parse GPX document from xmlm input source.
+
@param input The xmlm input source
+
@return [Ok gpx] on success, [Error err] on failure *)
+
val parse : Xmlm.input -> gpx result
+
+
(** Parse GPX document from string.
+
@param xml_string GPX document as XML string
+
@return [Ok gpx] on success, [Error err] on failure *)
+
val parse_string : string -> gpx result
+
+
(** {2 Writing Functions}
+
+
Generate GPX XML from document structures. *)
+
+
(** Write GPX document to xmlm output destination.
+
@param output The xmlm output destination
+
@param gpx The GPX document to write
+
@return [Ok ()] on success, [Error err] on failure *)
+
val write : Xmlm.output -> gpx -> unit result
+
+
(** Write GPX document to XML string.
+
@param gpx The GPX document to write
+
@return [Ok xml_string] on success, [Error err] on failure *)
+
val write_string : gpx -> string result
+
+
(** {2 Validation Functions}
+
+
Validate GPX documents for correctness and best practices. *)
+
+
(** Validation issue with severity level *)
+
type validation_issue = Validate.validation_issue = {
+
level : [`Error | `Warning]; (** Severity level *)
+
message : string; (** Issue description *)
+
location : string option; (** Location in document *)
+
}
+
+
(** Result of validation containing all issues found *)
+
type validation_result = Validate.validation_result = {
+
issues : validation_issue list; (** All validation issues *)
+
is_valid : bool; (** True if no errors found *)
+
}
+
+
(** Validate complete GPX document.
+
Checks coordinates, required fields, and best practices.
+
@param gpx GPX document to validate
+
@return Validation result with any issues found *)
+
val validate_gpx : gpx -> validation_result
+
+
(** Quick validation check.
+
@param gpx GPX document to validate
+
@return [true] if document is valid (no errors) *)
+
val is_valid : gpx -> bool
+
+
(** Get only error-level validation issues.
+
@param gpx GPX document to validate
+
@return List of validation errors *)
+
val get_errors : gpx -> validation_issue list
+
+
(** Get only warning-level validation issues.
+
@param gpx GPX document to validate
+
@return List of validation warnings *)
+
val get_warnings : gpx -> validation_issue list
+
+
(** Format validation issue for display.
+
@param issue Validation issue to format
+
@return Human-readable error message *)
+
val format_issue : validation_issue -> string
+
+
(** {2 Module Access}
+
+
Direct access to submodules for advanced usage. *)
+
+
(** Core type definitions and utilities *)
module Types = Types
-
(** {2 Streaming Parser}
-
-
Memory-efficient streaming XML parser for GPX documents.
-
-
Features:
-
- Validates coordinates and GPS fix types during parsing
-
- Handles extensions and custom elements
-
- Reports detailed parsing errors with location information
-
- Works with any [Xmlm.input] source *)
+
(** Streaming XML parser *)
module Parser = Parser
-
(** {2 Streaming Writer}
-
-
Memory-efficient streaming XML writer for GPX documents.
-
-
Features:
-
- Generates compliant GPX 1.1 XML
-
- Handles proper namespace declarations
-
- Supports extensions and custom elements
-
- Works with any [Xmlm.output] destination *)
+
(** Streaming XML writer *)
module Writer = Writer
-
(** {2 Validation Engine}
-
-
Comprehensive validation for GPX documents with detailed error reporting.
-
-
Features:
-
- Validates coordinates are within proper ranges
-
- Checks required fields and proper structure
-
- Provides warnings for best practices
-
- Supports custom validation rules *)
-
module Validate = Validate
+
(** Validation engine *)
+
module Validate = Validate
+2 -2
lib/gpx/parser.ml
···
type parser_state = {
input : Xmlm.input;
mutable current_element : string list; (* Stack of current element names *)
-
mutable text_buffer : Buffer.t;
+
text_buffer : Buffer.t;
}
(** Create a new parser state *)
···
(** Parse from string *)
let parse_string s =
let input = Xmlm.make_input (`String (0, s)) in
-
parse input
+
parse input
+3 -3
lib/gpx/writer.ml
···
(** Write GPX header and DTD *)
let write_header writer =
-
let* () = output_signal writer (`Dtd (Some "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")) in
+
let* () = output_signal writer (`Dtd None) in
Ok ()
(** Write link element *)
···
let attrs = [
(("", "version"), gpx.version);
(("", "creator"), gpx.creator);
-
(("xmlns", "xsi"), "http://www.w3.org/2001/XMLSchema-instance");
(("", "xmlns"), "http://www.topografix.com/GPX/1/1");
-
(("xsi", "schemaLocation"), "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd");
+
(("http://www.w3.org/2000/xmlns/", "xsi"), "http://www.w3.org/2001/XMLSchema-instance");
+
(("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation"), "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd");
] in
let* () = output_element_start writer "gpx" attrs in
let* () = (match gpx.metadata with
+5
lib/gpx_eio/dune
···
+
(library
+
(public_name mlgpx.eio)
+
(name gpx_eio)
+
(libraries eio xmlm ptime gpx)
+
(modules gpx_io gpx_eio))
+155
lib/gpx_eio/gpx_eio.ml
···
+
(** High-level Eio API for GPX operations *)
+
+
(* I/O module *)
+
module IO = Gpx_io
+
+
(** Convenience functions for common operations *)
+
+
(** Read and parse GPX file *)
+
let read ~fs path = IO.read_file ~fs path
+
+
(** Read and parse GPX file with validation *)
+
let read_validated ~fs path = IO.read_file_validated ~fs path
+
+
(** Write GPX to file *)
+
let write ~fs path gpx = IO.write_file ~fs path gpx
+
+
(** Write GPX to file with validation *)
+
let write_validated ~fs path gpx = IO.write_file_validated ~fs path gpx
+
+
(** Write GPX to file with backup *)
+
let write_with_backup ~fs path gpx = IO.write_file_with_backup ~fs path gpx
+
+
(** Read GPX from Eio source *)
+
let from_source source = IO.read_source source
+
+
(** Write GPX to Eio sink *)
+
let to_sink sink gpx = IO.write_sink sink gpx
+
+
(** Read GPX from Eio source with validation *)
+
let from_source_validated source = IO.read_source_validated source
+
+
(** Write GPX to Eio sink with validation *)
+
let to_sink_validated sink gpx = IO.write_sink_validated sink gpx
+
+
(** Create simple waypoint *)
+
let make_waypoint ~fs:_ ~lat ~lon ?name ?desc () =
+
match Gpx.latitude lat, Gpx.longitude lon with
+
| Ok lat, Ok lon ->
+
let wpt = Gpx.make_waypoint_data lat lon in
+
{ wpt with name; desc }
+
| Error e, _ | _, Error e -> raise (Gpx.Gpx_error (Gpx.Invalid_coordinate e))
+
+
(** Create simple track from coordinate list *)
+
let make_track_from_coords ~fs:_ ~name coords =
+
let make_trkpt (lat_f, lon_f) =
+
match Gpx.latitude lat_f, Gpx.longitude lon_f with
+
| Ok lat, Ok lon -> Gpx.make_waypoint_data lat lon
+
| Error e, _ | _, Error e -> raise (Gpx.Gpx_error (Gpx.Invalid_coordinate e))
+
in
+
let trkpts = List.map make_trkpt coords in
+
let trkseg : Gpx.track_segment = { trkpts; extensions = [] } in
+
({
+
name = Some name;
+
cmt = None; desc = None; src = None; links = [];
+
number = None; type_ = None; extensions = [];
+
trksegs = [trkseg];
+
} : Gpx.track)
+
+
(** Create simple route from coordinate list *)
+
let make_route_from_coords ~fs:_ ~name coords =
+
let make_rtept (lat_f, lon_f) =
+
match Gpx.latitude lat_f, Gpx.longitude lon_f with
+
| Ok lat, Ok lon -> Gpx.make_waypoint_data lat lon
+
| Error e, _ | _, Error e -> raise (Gpx.Gpx_error (Gpx.Invalid_coordinate e))
+
in
+
let rtepts = List.map make_rtept coords in
+
({
+
name = Some name;
+
cmt = None; desc = None; src = None; links = [];
+
number = None; type_ = None; extensions = [];
+
rtepts;
+
} : Gpx.route)
+
+
(** Extract coordinates from waypoints *)
+
let waypoint_coords (wpt : Gpx.waypoint_data) =
+
(Gpx.latitude_to_float wpt.lat, Gpx.longitude_to_float wpt.lon)
+
+
(** Extract coordinates from track *)
+
let track_coords (track : Gpx.track) =
+
List.fold_left (fun acc (trkseg : Gpx.track_segment) ->
+
List.fold_left (fun acc trkpt ->
+
waypoint_coords trkpt :: acc
+
) acc trkseg.trkpts
+
) [] track.trksegs
+
|> List.rev
+
+
(** Extract coordinates from route *)
+
let route_coords (route : Gpx.route) =
+
List.map waypoint_coords route.rtepts
+
+
(** Count total points in GPX *)
+
let count_points (gpx : Gpx.gpx) =
+
let waypoint_count = List.length gpx.waypoints in
+
let route_count = List.fold_left (fun acc (route : Gpx.route) ->
+
acc + List.length route.rtepts
+
) 0 gpx.routes in
+
let track_count = List.fold_left (fun acc (track : Gpx.track) ->
+
List.fold_left (fun acc (trkseg : Gpx.track_segment) ->
+
acc + List.length trkseg.trkpts
+
) acc track.trksegs
+
) 0 gpx.tracks in
+
waypoint_count + route_count + track_count
+
+
(** Get GPX statistics *)
+
type gpx_stats = {
+
waypoint_count : int;
+
route_count : int;
+
track_count : int;
+
total_points : int;
+
has_elevation : bool;
+
has_time : bool;
+
}
+
+
let get_stats (gpx : Gpx.gpx) =
+
let waypoint_count = List.length gpx.waypoints in
+
let route_count = List.length gpx.routes in
+
let track_count = List.length gpx.tracks in
+
let total_points = count_points gpx in
+
+
let has_elevation =
+
List.exists (fun (wpt : Gpx.waypoint_data) -> wpt.ele <> None) gpx.waypoints ||
+
List.exists (fun (route : Gpx.route) ->
+
List.exists (fun (rtept : Gpx.waypoint_data) -> rtept.ele <> None) route.rtepts
+
) gpx.routes ||
+
List.exists (fun (track : Gpx.track) ->
+
List.exists (fun (trkseg : Gpx.track_segment) ->
+
List.exists (fun (trkpt : Gpx.waypoint_data) -> trkpt.ele <> None) trkseg.trkpts
+
) track.trksegs
+
) gpx.tracks
+
in
+
+
let has_time =
+
List.exists (fun (wpt : Gpx.waypoint_data) -> wpt.time <> None) gpx.waypoints ||
+
List.exists (fun (route : Gpx.route) ->
+
List.exists (fun (rtept : Gpx.waypoint_data) -> rtept.time <> None) route.rtepts
+
) gpx.routes ||
+
List.exists (fun (track : Gpx.track) ->
+
List.exists (fun (trkseg : Gpx.track_segment) ->
+
List.exists (fun (trkpt : Gpx.waypoint_data) -> trkpt.time <> None) trkseg.trkpts
+
) track.trksegs
+
) gpx.tracks
+
in
+
+
{ waypoint_count; route_count; track_count; total_points; has_elevation; has_time }
+
+
(** Pretty print GPX statistics *)
+
let print_stats (gpx : Gpx.gpx) =
+
let stats = get_stats gpx in
+
Printf.printf "GPX Statistics:\n";
+
Printf.printf " Waypoints: %d\n" stats.waypoint_count;
+
Printf.printf " Routes: %d\n" stats.route_count;
+
Printf.printf " Tracks: %d\n" stats.track_count;
+
Printf.printf " Total points: %d\n" stats.total_points;
+
Printf.printf " Has elevation data: %s\n" (if stats.has_elevation then "yes" else "no");
+
Printf.printf " Has time data: %s\n" (if stats.has_time then "yes" else "no")
+178
lib/gpx_eio/gpx_eio.mli
···
+
(** {1 GPX Eio - High-level Eio API for GPX operations}
+
+
This module provides a high-level API for GPX operations using Eio's
+
effects-based concurrent I/O system. It offers convenient functions
+
for common GPX operations while maintaining structured concurrency.
+
+
{2 Key Features}
+
+
- Effects-based I/O using Eio
+
- Structured concurrency compatible
+
- Resource-safe operations
+
- Exception-based error handling (raises [Gpx.Gpx_error])
+
- Concurrent processing capabilities
+
+
{2 Usage Example}
+
+
{[
+
open Gpx_eio
+
+
let main env =
+
let fs = Eio.Stdenv.fs env in
+
+
(* Create a GPX document *)
+
let lat = Gpx.latitude 37.7749 |> Result.get_ok in
+
let lon = Gpx.longitude (-122.4194) |> Result.get_ok in
+
let wpt = make_waypoint fs ~lat:(Gpx.latitude_to_float lat) ~lon:(Gpx.longitude_to_float lon) ~name:"San Francisco" () in
+
let gpx = Gpx.make_gpx ~creator:"eio-example" in
+
let gpx = { gpx with waypoints = [wpt] } in
+
+
(* Write with validation *)
+
write_validated fs "output.gpx" gpx;
+
+
(* Read it back *)
+
let gpx2 = read_validated fs "output.gpx" in
+
Printf.printf "Read %d waypoints\n" (List.length gpx2.waypoints)
+
+
let () = Eio_main.run main
+
]}
+
+
{2 Module Re-exports} *)
+
+
(* I/O module *)
+
module IO = Gpx_io
+
+
(** {2 Convenience File Operations}
+
+
These functions provide simple file I/O with the filesystem from [Eio.Stdenv.fs]. *)
+
+
(** Read and parse GPX file.
+
@param fs Filesystem capability
+
@param path File path to read
+
@return GPX document
+
@raises Gpx.Gpx_error on read or parse failure *)
+
val read : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx
+
+
(** Read and parse GPX file with validation.
+
@param fs Filesystem capability
+
@param path File path to read
+
@return Valid GPX document
+
@raises Gpx.Gpx_error on validation failure *)
+
val read_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx
+
+
(** Write GPX to file.
+
@param fs Filesystem capability
+
@param path File path to write
+
@param gpx GPX document to write
+
@raises Gpx.Gpx_error on write failure *)
+
val write : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit
+
+
(** Write GPX to file with validation.
+
@param fs Filesystem capability
+
@param path File path to write
+
@param gpx GPX document to write
+
@raises Gpx.Gpx_error on validation or write failure *)
+
val write_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit
+
+
(** Write GPX to file with automatic backup.
+
@param fs Filesystem capability
+
@param path File path to write
+
@param gpx GPX document to write
+
@return Backup file path (empty if no backup created)
+
@raises Gpx.Gpx_error on failure *)
+
val write_with_backup : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> string
+
+
(** {2 Stream Operations}
+
+
Operations for reading/writing GPX from/to Eio flows. *)
+
+
(** Read GPX from Eio source.
+
@param source Input flow
+
@return GPX document
+
@raises Gpx.Gpx_error on read or parse failure *)
+
val from_source : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx
+
+
(** Write GPX to Eio sink.
+
@param sink Output flow
+
@param gpx GPX document
+
@raises Gpx.Gpx_error on write failure *)
+
val to_sink : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit
+
+
(** Read GPX from Eio source with validation.
+
@param source Input flow
+
@return Valid GPX document
+
@raises Gpx.Gpx_error on validation failure *)
+
val from_source_validated : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx
+
+
(** Write GPX to Eio sink with validation.
+
@param sink Output flow
+
@param gpx GPX document
+
@raises Gpx.Gpx_error on validation failure *)
+
val to_sink_validated : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit
+
+
(** {2 Utility Functions} *)
+
+
(** Create simple waypoint with coordinates.
+
@param fs Filesystem capability (unused, for API consistency)
+
@param lat Latitude in degrees
+
@param lon Longitude in degrees
+
@param ?name Optional waypoint name
+
@param ?desc Optional waypoint description
+
@return Waypoint data
+
@raises Gpx.Gpx_error on invalid coordinates *)
+
val make_waypoint : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> lat:float -> lon:float -> ?name:string -> ?desc:string -> unit -> Gpx.waypoint_data
+
+
(** Create track from coordinate list.
+
@param fs Filesystem capability (unused, for API consistency)
+
@param name Track name
+
@param coords List of (latitude, longitude) pairs
+
@return Track with single segment
+
@raises Gpx.Gpx_error on invalid coordinates *)
+
val make_track_from_coords : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> name:string -> (float * float) list -> Gpx.track
+
+
(** Create route from coordinate list.
+
@param fs Filesystem capability (unused, for API consistency)
+
@param name Route name
+
@param coords List of (latitude, longitude) pairs
+
@return Route
+
@raises Gpx.Gpx_error on invalid coordinates *)
+
val make_route_from_coords : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> name:string -> (float * float) list -> Gpx.route
+
+
(** Extract coordinates from waypoint.
+
@param wpt Waypoint data
+
@return (latitude, longitude) as floats *)
+
val waypoint_coords : Gpx.waypoint_data -> float * float
+
+
(** Extract coordinates from track.
+
@param track Track
+
@return List of (latitude, longitude) pairs *)
+
val track_coords : Gpx.track -> (float * float) list
+
+
(** Extract coordinates from route.
+
@param route Route
+
@return List of (latitude, longitude) pairs *)
+
val route_coords : Gpx.route -> (float * float) list
+
+
(** Count total points in GPX document.
+
@param gpx GPX document
+
@return Total number of waypoints, route points, and track points *)
+
val count_points : Gpx.gpx -> int
+
+
(** GPX statistics record *)
+
type gpx_stats = {
+
waypoint_count : int; (** Number of waypoints *)
+
route_count : int; (** Number of routes *)
+
track_count : int; (** Number of tracks *)
+
total_points : int; (** Total geographic points *)
+
has_elevation : bool; (** Document contains elevation data *)
+
has_time : bool; (** Document contains time data *)
+
}
+
+
(** Get GPX document statistics.
+
@param gpx GPX document
+
@return Statistics summary *)
+
val get_stats : Gpx.gpx -> gpx_stats
+
+
(** Print GPX statistics to stdout.
+
@param gpx GPX document *)
+
val print_stats : Gpx.gpx -> unit
+116
lib/gpx_eio/gpx_io.ml
···
+
(** GPX Eio I/O operations *)
+
+
(* Real Eio-based I/O operations *)
+
+
(** Read GPX from file path *)
+
let read_file ~fs path =
+
let content = Eio.Path.load Eio.Path.(fs / path) in
+
match Gpx.parse_string content with
+
| Ok gpx -> gpx
+
| Error err -> raise (Gpx.Gpx_error err)
+
+
(** Write GPX to file path *)
+
let write_file ~fs path gpx =
+
match Gpx.write_string gpx with
+
| Ok xml_string ->
+
Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) xml_string
+
| Error err -> raise (Gpx.Gpx_error err)
+
+
(** Read GPX from file with validation *)
+
let read_file_validated ~fs path =
+
let gpx = read_file ~fs path in
+
let validation = Gpx.validate_gpx gpx in
+
if validation.is_valid then
+
gpx
+
else
+
let errors = Gpx.get_errors gpx in
+
let error_msgs = List.map Gpx.format_issue errors in
+
raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs)))
+
+
(** Write GPX to file with validation *)
+
let write_file_validated ~fs path gpx =
+
let validation = Gpx.validate_gpx gpx in
+
if not validation.is_valid then
+
let errors = Gpx.get_errors gpx in
+
let error_msgs = List.map Gpx.format_issue errors in
+
raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs)))
+
else
+
write_file ~fs path gpx
+
+
(** Read GPX from Eio source *)
+
let read_source source =
+
let content = Eio.Flow.read_all source in
+
match Gpx.parse_string content with
+
| Ok gpx -> gpx
+
| Error err -> raise (Gpx.Gpx_error err)
+
+
(** Write GPX to Eio sink *)
+
let write_sink sink gpx =
+
match Gpx.write_string gpx with
+
| Ok xml_string ->
+
Eio.Flow.copy_string xml_string sink
+
| Error err -> raise (Gpx.Gpx_error err)
+
+
(** Read GPX from Eio source with validation *)
+
let read_source_validated source =
+
let gpx = read_source source in
+
let validation = Gpx.validate_gpx gpx in
+
if validation.is_valid then
+
gpx
+
else
+
let errors = Gpx.get_errors gpx in
+
let error_msgs = List.map Gpx.format_issue errors in
+
raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs)))
+
+
(** Write GPX to Eio sink with validation *)
+
let write_sink_validated sink gpx =
+
let validation = Gpx.validate_gpx gpx in
+
if not validation.is_valid then
+
let errors = Gpx.get_errors gpx in
+
let error_msgs = List.map Gpx.format_issue errors in
+
raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs)))
+
else
+
write_sink sink gpx
+
+
(** Check if file exists *)
+
let file_exists ~fs path =
+
try
+
let _stat = Eio.Path.stat ~follow:true Eio.Path.(fs / path) in
+
true
+
with
+
| _ -> false
+
+
(** Get file size *)
+
let file_size ~fs path =
+
try
+
let stat = Eio.Path.stat ~follow:true Eio.Path.(fs / path) in
+
Optint.Int63.to_int stat.size
+
with
+
| exn -> raise (Gpx.Gpx_error (Gpx.IO_error (Printexc.to_string exn)))
+
+
(** Create backup of existing file *)
+
let create_backup ~fs path =
+
if file_exists ~fs path then
+
let backup_path = path ^ ".backup" in
+
let content = Eio.Path.load Eio.Path.(fs / path) in
+
Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / backup_path) content;
+
backup_path
+
else
+
""
+
+
(** Write GPX to file with automatic backup *)
+
let write_file_with_backup ~fs path gpx =
+
let backup_path = create_backup ~fs path in
+
try
+
write_file ~fs path gpx;
+
backup_path
+
with
+
| Gpx.Gpx_error _ as err ->
+
(* Try to restore backup if write failed *)
+
if backup_path <> "" && file_exists ~fs backup_path then (
+
try
+
let backup_content = Eio.Path.load Eio.Path.(fs / backup_path) in
+
Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) backup_content
+
with _ -> () (* Ignore restore errors *)
+
);
+
raise err
+91
lib/gpx_eio/gpx_io.mli
···
+
(** GPX Eio I/O operations
+
+
This module provides GPX I/O operations using Eio's effects-based
+
concurrent I/O system. All operations are structured concurrency
+
compatible and work with Eio's resource management.
+
*)
+
+
(** {1 File Operations}
+
+
All file operations require filesystem access capability. *)
+
+
(** Read GPX from file path.
+
@param fs Filesystem capability
+
@param path File path to read
+
@return GPX document or error *)
+
val read_file : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx
+
+
(** Write GPX to file path.
+
@param fs Filesystem capability
+
@param path File path to write
+
@param gpx GPX document to write
+
@raises Gpx.Gpx_error on write failure *)
+
val write_file : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit
+
+
(** Read GPX from file with validation.
+
@param fs Filesystem capability
+
@param path File path to read
+
@return Valid GPX document
+
@raises Gpx.Gpx_error on validation failure *)
+
val read_file_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx
+
+
(** Write GPX to file with validation.
+
@param fs Filesystem capability
+
@param path File path to write
+
@param gpx GPX document to write
+
@raises Gpx.Gpx_error on validation or write failure *)
+
val write_file_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit
+
+
(** {1 Stream Operations}
+
+
Read/write GPX from/to Eio flows. *)
+
+
(** Read GPX from Eio source.
+
@param source Input flow to read from
+
@return GPX document *)
+
val read_source : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx
+
+
(** Write GPX to Eio sink.
+
@param sink Output flow to write to
+
@param gpx GPX document to write *)
+
val write_sink : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit
+
+
(** Read GPX from Eio source with validation.
+
@param source Input flow to read from
+
@return Valid GPX document
+
@raises Gpx.Gpx_error on validation failure *)
+
val read_source_validated : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx
+
+
(** Write GPX to Eio sink with validation.
+
@param sink Output flow to write to
+
@param gpx GPX document to write
+
@raises Gpx.Gpx_error on validation failure *)
+
val write_sink_validated : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit
+
+
(** {1 Utility Functions} *)
+
+
(** Check if file exists.
+
@param fs Filesystem capability
+
@param path File path to check
+
@return [true] if file exists and is readable *)
+
val file_exists : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> bool
+
+
(** Get file size.
+
@param fs Filesystem capability
+
@param path File path
+
@return File size in bytes *)
+
val file_size : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> int
+
+
(** Create backup of existing file.
+
@param fs Filesystem capability
+
@param path Original file path
+
@return Backup file path *)
+
val create_backup : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> string
+
+
(** Write GPX to file with automatic backup.
+
Creates a backup of existing file before writing new content.
+
@param fs Filesystem capability
+
@param path File path to write
+
@param gpx GPX document to write
+
@return Backup file path (empty string if no backup needed) *)
+
val write_file_with_backup : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> string
+103
test/data/comprehensive.gpx
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<gpx version="1.1" creator="mlgpx comprehensive test"
+
xmlns="http://www.topografix.com/GPX/1/1">
+
<metadata>
+
<name>Comprehensive GPX Test</name>
+
<desc>Contains waypoints, routes, and tracks together</desc>
+
<author>
+
<name>Test Suite</name>
+
<email id="test" domain="example.com"/>
+
</author>
+
<copyright author="Test Suite">
+
<year>2024</year>
+
<license>MIT</license>
+
</copyright>
+
<time>2024-01-15T12:00:00Z</time>
+
<keywords>test, gpx, comprehensive</keywords>
+
<bounds minlat="37.7" maxlat="37.8" minlon="-122.5" maxlon="-122.4"/>
+
</metadata>
+
+
<!-- Waypoints -->
+
<wpt lat="37.7749" lon="-122.4194">
+
<ele>52.0</ele>
+
<time>2024-01-15T08:00:00Z</time>
+
<name>Start Point</name>
+
<desc>Beginning of our journey</desc>
+
<type>start</type>
+
</wpt>
+
<wpt lat="37.7849" lon="-122.4094">
+
<ele>100.5</ele>
+
<time>2024-01-15T12:00:00Z</time>
+
<name>End Point</name>
+
<desc>End of our journey</desc>
+
<type>finish</type>
+
</wpt>
+
+
<!-- Route -->
+
<rte>
+
<name>Planned Route</name>
+
<desc>The route we intended to take</desc>
+
<rtept lat="37.7749" lon="-122.4194">
+
<name>Start</name>
+
</rtept>
+
<rtept lat="37.7799" lon="-122.4144">
+
<name>Midpoint</name>
+
</rtept>
+
<rtept lat="37.7849" lon="-122.4094">
+
<name>End</name>
+
</rtept>
+
</rte>
+
+
<!-- Track -->
+
<trk>
+
<name>Actual Track</name>
+
<desc>The track we actually took</desc>
+
<type>walking</type>
+
<trkseg>
+
<trkpt lat="37.7749" lon="-122.4194">
+
<ele>52.0</ele>
+
<time>2024-01-15T08:00:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7759" lon="-122.4184">
+
<ele>55.2</ele>
+
<time>2024-01-15T08:05:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7769" lon="-122.4174">
+
<ele>58.8</ele>
+
<time>2024-01-15T08:10:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7779" lon="-122.4164">
+
<ele>62.1</ele>
+
<time>2024-01-15T08:15:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7789" lon="-122.4154">
+
<ele>65.5</ele>
+
<time>2024-01-15T08:20:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7799" lon="-122.4144">
+
<ele>68.9</ele>
+
<time>2024-01-15T08:25:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7809" lon="-122.4134">
+
<ele>72.3</ele>
+
<time>2024-01-15T08:30:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7819" lon="-122.4124">
+
<ele>75.7</ele>
+
<time>2024-01-15T08:35:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7829" lon="-122.4114">
+
<ele>79.1</ele>
+
<time>2024-01-15T08:40:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7839" lon="-122.4104">
+
<ele>82.5</ele>
+
<time>2024-01-15T08:45:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7849" lon="-122.4094">
+
<ele>85.9</ele>
+
<time>2024-01-15T08:50:00Z</time>
+
</trkpt>
+
</trkseg>
+
</trk>
+
</gpx>
+35
test/data/detailed_waypoints.gpx
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<gpx version="1.1" creator="mlgpx test suite"
+
xmlns="http://www.topografix.com/GPX/1/1">
+
<metadata>
+
<name>Detailed Waypoints</name>
+
<desc>Waypoints with elevation, time, and full metadata</desc>
+
<time>2024-01-15T10:30:00Z</time>
+
<bounds minlat="37.7" maxlat="37.8" minlon="-122.5" maxlon="-122.4"/>
+
</metadata>
+
<wpt lat="37.7849" lon="-122.4094">
+
<ele>100.5</ele>
+
<time>2024-01-15T10:00:00Z</time>
+
<name>Golden Gate Bridge</name>
+
<desc>Famous suspension bridge</desc>
+
<type>landmark</type>
+
<fix>3d</fix>
+
<sat>8</sat>
+
<hdop>1.2</hdop>
+
<vdop>1.8</vdop>
+
<pdop>2.1</pdop>
+
<link href="https://www.nps.gov/goga/">
+
<text>Golden Gate National Recreation Area</text>
+
<type>text/html</type>
+
</link>
+
</wpt>
+
<wpt lat="37.7749" lon="-122.4194">
+
<ele>52.0</ele>
+
<time>2024-01-15T10:15:00Z</time>
+
<name>Lombard Street</name>
+
<desc>Most crooked street in the world</desc>
+
<type>attraction</type>
+
<fix>2d</fix>
+
<sat>6</sat>
+
</wpt>
+
</gpx>
+37
test/data/edge_cases.gpx
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<gpx version="1.1" creator="edge case test"
+
xmlns="http://www.topografix.com/GPX/1/1">
+
<metadata>
+
<name>Edge Cases</name>
+
<desc>Testing boundary conditions</desc>
+
</metadata>
+
+
<!-- Edge case coordinates -->
+
<wpt lat="-90.0" lon="-180.0">
+
<name>South Pole</name>
+
<desc>Minimum latitude and longitude</desc>
+
</wpt>
+
<wpt lat="90.0" lon="179.999999">
+
<name>North Pole Area</name>
+
<desc>Maximum latitude, near maximum longitude</desc>
+
</wpt>
+
<wpt lat="0.0" lon="0.0">
+
<name>Null Island</name>
+
<desc>Zero coordinates</desc>
+
</wpt>
+
+
<!-- Track with extreme elevation -->
+
<trk>
+
<name>Extreme Elevations</name>
+
<trkseg>
+
<trkpt lat="28.0" lon="86.9">
+
<ele>8848.86</ele>
+
<name>Everest Summit</name>
+
</trkpt>
+
<trkpt lat="31.5" lon="35.4">
+
<ele>-430.0</ele>
+
<name>Dead Sea</name>
+
</trkpt>
+
</trkseg>
+
</trk>
+
</gpx>
+13
test/data/invalid.gpx
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<gpx version="1.1" creator="invalid test"
+
xmlns="http://www.topografix.com/GPX/1/1">
+
<wpt lat="invalid_lat" lon="-122.4194">
+
<name>Invalid Waypoint</name>
+
</wpt>
+
<wpt lat="91.0" lon="-122.4194">
+
<name>Invalid Latitude</name>
+
</wpt>
+
<wpt lat="45.0" lon="-181.0">
+
<name>Invalid Longitude</name>
+
</wpt>
+
</gpx>
+7
test/data/minimal.gpx
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<gpx version="1.1" creator="minimal test"
+
xmlns="http://www.topografix.com/GPX/1/1">
+
<wpt lat="0.0" lon="0.0">
+
<name>Origin</name>
+
</wpt>
+
</gpx>
+42
test/data/multi_segment_track.gpx
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<gpx version="1.1" creator="mlgpx test suite"
+
xmlns="http://www.topografix.com/GPX/1/1">
+
<metadata>
+
<name>Multi-Segment Track</name>
+
<desc>Track with multiple segments and breaks</desc>
+
</metadata>
+
<trk>
+
<name>Hiking Trail</name>
+
<desc>Mountain hike with rest stops</desc>
+
<type>hiking</type>
+
<trkseg>
+
<trkpt lat="37.7749" lon="-122.4194">
+
<ele>100.0</ele>
+
<time>2024-01-15T09:00:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7759" lon="-122.4184">
+
<ele>125.5</ele>
+
<time>2024-01-15T09:15:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7769" lon="-122.4174">
+
<ele>150.2</ele>
+
<time>2024-01-15T09:30:00Z</time>
+
</trkpt>
+
</trkseg>
+
<!-- Break for lunch -->
+
<trkseg>
+
<trkpt lat="37.7769" lon="-122.4174">
+
<ele>150.2</ele>
+
<time>2024-01-15T10:30:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7779" lon="-122.4164">
+
<ele>175.8</ele>
+
<time>2024-01-15T10:45:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7789" lon="-122.4154">
+
<ele>200.1</ele>
+
<time>2024-01-15T11:00:00Z</time>
+
</trkpt>
+
</trkseg>
+
</trk>
+
</gpx>
+29
test/data/simple_route.gpx
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<gpx version="1.1" creator="mlgpx test suite"
+
xmlns="http://www.topografix.com/GPX/1/1">
+
<metadata>
+
<name>Simple Route Test</name>
+
<desc>Basic route with waypoints</desc>
+
</metadata>
+
<rte>
+
<name>SF to Oakland</name>
+
<desc>Route across the Bay Bridge</desc>
+
<number>1</number>
+
<type>driving</type>
+
<rtept lat="37.7749" lon="-122.4194">
+
<name>San Francisco Start</name>
+
<desc>Starting point in SF</desc>
+
<type>start</type>
+
</rtept>
+
<rtept lat="37.8044" lon="-122.2711">
+
<name>Bay Bridge</name>
+
<desc>Crossing the bay</desc>
+
<type>waypoint</type>
+
</rtept>
+
<rtept lat="37.8044" lon="-122.2711">
+
<name>Oakland End</name>
+
<desc>Destination in Oakland</desc>
+
<type>finish</type>
+
</rtept>
+
</rte>
+
</gpx>
+36
test/data/simple_track.gpx
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<gpx version="1.1" creator="mlgpx test suite"
+
xmlns="http://www.topografix.com/GPX/1/1">
+
<metadata>
+
<name>Simple Track Test</name>
+
<desc>Basic track recording</desc>
+
</metadata>
+
<trk>
+
<name>Morning Jog</name>
+
<desc>Running track around the park</desc>
+
<type>running</type>
+
<number>1</number>
+
<trkseg>
+
<trkpt lat="37.7749" lon="-122.4194">
+
<ele>15.0</ele>
+
<time>2024-01-15T07:00:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7759" lon="-122.4184">
+
<ele>18.2</ele>
+
<time>2024-01-15T07:01:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7769" lon="-122.4174">
+
<ele>21.5</ele>
+
<time>2024-01-15T07:02:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7779" lon="-122.4164">
+
<ele>19.8</ele>
+
<time>2024-01-15T07:03:00Z</time>
+
</trkpt>
+
<trkpt lat="37.7749" lon="-122.4194">
+
<ele>15.0</ele>
+
<time>2024-01-15T07:10:00Z</time>
+
</trkpt>
+
</trkseg>
+
</trk>
+
</gpx>
+28
test/data/simple_waypoints.gpx
···
+
<?xml version="1.0" encoding="UTF-8"?>
+
<gpx version="1.1" creator="mlgpx test suite"
+
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
+
xmlns="http://www.topografix.com/GPX/1/1"
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
<metadata>
+
<name>Simple Waypoints Test</name>
+
<desc>Basic waypoint test with minimal data</desc>
+
<author>
+
<name>GPX Test Suite</name>
+
</author>
+
</metadata>
+
<wpt lat="37.7749" lon="-122.4194">
+
<name>San Francisco</name>
+
<desc>City by the bay</desc>
+
<type>city</type>
+
</wpt>
+
<wpt lat="40.7128" lon="-74.0060">
+
<name>New York</name>
+
<desc>Big Apple</desc>
+
<type>city</type>
+
</wpt>
+
<wpt lat="34.0522" lon="-118.2437">
+
<name>Los Angeles</name>
+
<desc>City of Angels</desc>
+
<type>city</type>
+
</wpt>
+
</gpx>
+19 -1
test/dune
···
(executable
(public_name test_gpx)
(name test_gpx)
-
(libraries gpx gpx_unix))
+
(libraries gpx gpx_unix)
+
(modules test_gpx))
+
+
;; ppx_expect inline tests
+
(library
+
(public_name mlgpx.test)
+
(name test_corpus)
+
(libraries gpx)
+
(inline_tests)
+
(preprocess (pps ppx_expect))
+
(modules test_corpus))
+
+
;; Alcotest suite for Unix and Eio comparison
+
(executable
+
(public_name corpus_test)
+
(name test_corpus_unix_eio)
+
(libraries gpx gpx_unix gpx_eio alcotest eio_main)
+
(optional)
+
(modules test_corpus_unix_eio))
+251
test/test_corpus.ml
···
+
(** Corpus tests using ppx_expect for GPX parsing *)
+
+
open Gpx
+
+
let test_data_dir =
+
let cwd = Sys.getcwd () in
+
let basename = Filename.basename cwd in
+
if basename = "test" then
+
"data" (* Running from test/ directory *)
+
else if basename = "_build" || String.contains cwd '_' then
+
"../test/data" (* Running from _build during tests *)
+
else
+
"test/data" (* Running from project root *)
+
+
let read_test_file filename =
+
let path = Filename.concat test_data_dir filename in
+
In_channel.with_open_text path In_channel.input_all
+
+
let%expect_test "parse simple waypoints" =
+
let content = read_test_file "simple_waypoints.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
Printf.printf "Waypoints count: %d\n" (List.length gpx.waypoints);
+
Printf.printf "First waypoint name: %s\n"
+
(match gpx.waypoints with
+
| wpt :: _ -> (match wpt.name with Some n -> n | None -> "None")
+
| [] -> "None");
+
Printf.printf "Creator: %s\n" gpx.creator;
+
[%expect {|
+
Waypoints count: 3
+
First waypoint name: San Francisco
+
Creator: mlgpx test suite |}]
+
| Error err ->
+
Printf.printf "Error: %s\n" (match err with
+
| Invalid_xml s -> "Invalid XML: " ^ s
+
| Invalid_coordinate s -> "Invalid coordinate: " ^ s
+
| _ -> "Other error");
+
[%expect.unreachable]
+
+
let%expect_test "parse detailed waypoints" =
+
let content = read_test_file "detailed_waypoints.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
Printf.printf "Waypoints count: %d\n" (List.length gpx.waypoints);
+
Printf.printf "Has metadata time: %b\n"
+
(match gpx.metadata with Some md -> md.time <> None | None -> false);
+
Printf.printf "Has bounds: %b\n"
+
(match gpx.metadata with Some md -> md.bounds <> None | None -> false);
+
(match gpx.waypoints with
+
| wpt :: _ ->
+
Printf.printf "First waypoint has elevation: %b\n" (wpt.ele <> None);
+
Printf.printf "First waypoint has time: %b\n" (wpt.time <> None);
+
Printf.printf "First waypoint has links: %b\n" (wpt.links <> [])
+
| [] -> ());
+
[%expect {|
+
Waypoints count: 2
+
Has metadata time: true
+
Has bounds: true
+
First waypoint has elevation: true
+
First waypoint has time: true
+
First waypoint has links: true |}]
+
| Error _ ->
+
Printf.printf "Parse error\n";
+
[%expect.unreachable]
+
+
let%expect_test "parse simple route" =
+
let content = read_test_file "simple_route.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
Printf.printf "Routes count: %d\n" (List.length gpx.routes);
+
(match gpx.routes with
+
| rte :: _ ->
+
Printf.printf "Route name: %s\n"
+
(match rte.name with Some n -> n | None -> "None");
+
Printf.printf "Route points count: %d\n" (List.length rte.rtepts);
+
Printf.printf "Route has number: %b\n" (rte.number <> None)
+
| [] -> ());
+
[%expect {|
+
Routes count: 1
+
Route name: SF to Oakland
+
Route points count: 3
+
Route has number: true |}]
+
| Error _ ->
+
Printf.printf "Parse error\n";
+
[%expect.unreachable]
+
+
let%expect_test "parse simple track" =
+
let content = read_test_file "simple_track.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
Printf.printf "Tracks count: %d\n" (List.length gpx.tracks);
+
(match gpx.tracks with
+
| trk :: _ ->
+
Printf.printf "Track name: %s\n"
+
(match trk.name with Some n -> n | None -> "None");
+
Printf.printf "Track segments: %d\n" (List.length trk.trksegs);
+
(match trk.trksegs with
+
| seg :: _ ->
+
Printf.printf "First segment points: %d\n" (List.length seg.trkpts);
+
(match seg.trkpts with
+
| pt :: _ ->
+
Printf.printf "First point has elevation: %b\n" (pt.ele <> None);
+
Printf.printf "First point has time: %b\n" (pt.time <> None)
+
| [] -> ())
+
| [] -> ())
+
| [] -> ());
+
[%expect {|
+
Tracks count: 1
+
Track name: Morning Jog
+
Track segments: 1
+
First segment points: 5
+
First point has elevation: true
+
First point has time: true |}]
+
| Error _ ->
+
Printf.printf "Parse error\n";
+
[%expect.unreachable]
+
+
let%expect_test "parse multi-segment track" =
+
let content = read_test_file "multi_segment_track.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
Printf.printf "Tracks count: %d\n" (List.length gpx.tracks);
+
(match gpx.tracks with
+
| trk :: _ ->
+
Printf.printf "Track segments: %d\n" (List.length trk.trksegs);
+
let total_points = List.fold_left (fun acc seg ->
+
acc + List.length seg.trkpts) 0 trk.trksegs in
+
Printf.printf "Total track points: %d\n" total_points
+
| [] -> ());
+
[%expect {|
+
Tracks count: 1
+
Track segments: 2
+
Total track points: 6 |}]
+
| Error _ ->
+
Printf.printf "Parse error\n";
+
[%expect.unreachable]
+
+
let%expect_test "parse comprehensive gpx" =
+
let content = read_test_file "comprehensive.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
Printf.printf "Waypoints: %d\n" (List.length gpx.waypoints);
+
Printf.printf "Routes: %d\n" (List.length gpx.routes);
+
Printf.printf "Tracks: %d\n" (List.length gpx.tracks);
+
Printf.printf "Has author: %b\n"
+
(match gpx.metadata with Some md -> md.author <> None | None -> false);
+
Printf.printf "Has copyright: %b\n"
+
(match gpx.metadata with Some md -> md.copyright <> None | None -> false);
+
Printf.printf "Has keywords: %b\n"
+
(match gpx.metadata with Some md -> md.keywords <> None | None -> false);
+
[%expect {|
+
Waypoints: 2
+
Routes: 1
+
Tracks: 1
+
Has author: true
+
Has copyright: true
+
Has keywords: true |}]
+
| Error _ ->
+
Printf.printf "Parse error\n";
+
[%expect.unreachable]
+
+
let%expect_test "parse minimal gpx" =
+
let content = read_test_file "minimal.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
Printf.printf "Minimal GPX parsed successfully\n";
+
Printf.printf "Waypoints: %d\n" (List.length gpx.waypoints);
+
Printf.printf "Routes: %d\n" (List.length gpx.routes);
+
Printf.printf "Tracks: %d\n" (List.length gpx.tracks);
+
[%expect {|
+
Minimal GPX parsed successfully
+
Waypoints: 1
+
Routes: 0
+
Tracks: 0 |}]
+
| Error err ->
+
Printf.printf "Error: %s\n" (match err with
+
| Invalid_xml s -> "Invalid XML: " ^ s
+
| _ -> "Other error");
+
[%expect.unreachable]
+
+
let%expect_test "parse edge cases" =
+
let content = read_test_file "edge_cases.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
Printf.printf "Edge cases parsed successfully\n";
+
Printf.printf "Waypoints: %d\n" (List.length gpx.waypoints);
+
Printf.printf "Tracks: %d\n" (List.length gpx.tracks);
+
(* Check coordinate ranges *)
+
let check_coords () =
+
match gpx.waypoints with
+
| wpt1 :: wpt2 :: wpt3 :: _ ->
+
Printf.printf "South pole coords: %.1f, %.1f\n"
+
(latitude_to_float wpt1.lat) (longitude_to_float wpt1.lon);
+
Printf.printf "North pole coords: %.1f, %.6f\n"
+
(latitude_to_float wpt2.lat) (longitude_to_float wpt2.lon);
+
Printf.printf "Null island coords: %.1f, %.1f\n"
+
(latitude_to_float wpt3.lat) (longitude_to_float wpt3.lon);
+
| _ -> Printf.printf "Unexpected waypoint count\n"
+
in
+
check_coords ();
+
[%expect {|
+
Edge cases parsed successfully
+
Waypoints: 3
+
Tracks: 1
+
South pole coords: -90.0, -180.0
+
North pole coords: 90.0, 180.000000
+
Null island coords: 0.0, 0.0 |}]
+
| Error _ ->
+
Printf.printf "Parse error\n";
+
[%expect.unreachable]
+
+
let%expect_test "test validation" =
+
let content = read_test_file "comprehensive.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
let validation = validate_gpx gpx in
+
Printf.printf "Is valid: %b\n" validation.is_valid;
+
Printf.printf "Issue count: %d\n" (List.length validation.issues);
+
[%expect {|
+
Is valid: true
+
Issue count: 0 |}]
+
| Error _ ->
+
Printf.printf "Parse error\n";
+
[%expect.unreachable]
+
+
let%expect_test "round-trip test" =
+
let content = read_test_file "simple_waypoints.gpx" in
+
match parse_string content with
+
| Ok gpx ->
+
(match write_string gpx with
+
| Ok xml_output ->
+
(match parse_string xml_output with
+
| Ok gpx2 ->
+
Printf.printf "Round-trip successful\n";
+
Printf.printf "Original waypoints: %d\n" (List.length gpx.waypoints);
+
Printf.printf "Round-trip waypoints: %d\n" (List.length gpx2.waypoints);
+
Printf.printf "Creators match: %b\n" (gpx.creator = gpx2.creator);
+
[%expect {|
+
Round-trip successful
+
Original waypoints: 3
+
Round-trip waypoints: 3
+
Creators match: true |}]
+
| Error _ ->
+
Printf.printf "Round-trip parse failed\n";
+
[%expect.unreachable])
+
| Error _ ->
+
Printf.printf "Write failed\n";
+
[%expect.unreachable])
+
| Error _ ->
+
Printf.printf "Initial parse failed\n";
+
[%expect.unreachable]
+272
test/test_corpus_unix_eio.ml
···
+
(** Alcotest suite comparing Unix and Eio implementations *)
+
+
open Alcotest
+
+
let test_data_dir = "test/data"
+
+
let test_files = [
+
"simple_waypoints.gpx";
+
"detailed_waypoints.gpx";
+
"simple_route.gpx";
+
"simple_track.gpx";
+
"multi_segment_track.gpx";
+
"comprehensive.gpx";
+
"minimal.gpx";
+
"edge_cases.gpx";
+
]
+
+
(** Helper to compare GPX documents *)
+
let compare_gpx_basic gpx1 gpx2 =
+
let open Gpx in
+
gpx1.creator = gpx2.creator &&
+
List.length gpx1.waypoints = List.length gpx2.waypoints &&
+
List.length gpx1.routes = List.length gpx2.routes &&
+
List.length gpx1.tracks = List.length gpx2.tracks
+
+
(** Test Unix implementation can read all test files *)
+
let test_unix_parsing filename () =
+
let path = Filename.concat test_data_dir filename in
+
match Gpx_unix.read path with
+
| Ok gpx ->
+
let validation = Gpx.validate_gpx gpx in
+
check bool "GPX is valid" true validation.is_valid;
+
check bool "Has some content" true (
+
List.length gpx.waypoints > 0 ||
+
List.length gpx.routes > 0 ||
+
List.length gpx.tracks > 0
+
)
+
| Error err ->
+
failf "Unix parsing failed for %s: %s" filename
+
(match err with
+
| Gpx.Invalid_xml s -> "Invalid XML: " ^ s
+
| Gpx.Invalid_coordinate s -> "Invalid coordinate: " ^ s
+
| Gpx.Missing_required_attribute (elem, attr) ->
+
Printf.sprintf "Missing attribute %s in %s" attr elem
+
| Gpx.Missing_required_element s -> "Missing element: " ^ s
+
| Gpx.Validation_error s -> "Validation error: " ^ s
+
| Gpx.Xml_error s -> "XML error: " ^ s
+
| Gpx.IO_error s -> "I/O error: " ^ s)
+
+
(** Test Eio implementation can read all test files *)
+
let test_eio_parsing filename () =
+
Eio_main.run @@ fun env ->
+
let fs = Eio.Stdenv.fs env in
+
let path = Filename.concat test_data_dir filename in
+
try
+
let gpx = Gpx_eio.read ~fs path in
+
let validation = Gpx.validate_gpx gpx in
+
check bool "GPX is valid" true validation.is_valid;
+
check bool "Has some content" true (
+
List.length gpx.waypoints > 0 ||
+
List.length gpx.routes > 0 ||
+
List.length gpx.tracks > 0
+
)
+
with
+
| Gpx.Gpx_error err ->
+
failf "Eio parsing failed for %s: %s" filename
+
(match err with
+
| Gpx.Invalid_xml s -> "Invalid XML: " ^ s
+
| Gpx.Invalid_coordinate s -> "Invalid coordinate: " ^ s
+
| Gpx.Missing_required_attribute (elem, attr) ->
+
Printf.sprintf "Missing attribute %s in %s" attr elem
+
| Gpx.Missing_required_element s -> "Missing element: " ^ s
+
| Gpx.Validation_error s -> "Validation error: " ^ s
+
| Gpx.Xml_error s -> "XML error: " ^ s
+
| Gpx.IO_error s -> "I/O error: " ^ s)
+
+
(** Test Unix and Eio implementations produce equivalent results *)
+
let test_unix_eio_equivalence filename () =
+
let path = Filename.concat test_data_dir filename in
+
+
(* Parse with Unix *)
+
let unix_result = Gpx_unix.read path in
+
+
(* Parse with Eio *)
+
let eio_result =
+
try
+
Eio_main.run @@ fun env ->
+
let fs = Eio.Stdenv.fs env in
+
Ok (Gpx_eio.read ~fs path)
+
with
+
| Gpx.Gpx_error err -> Error err
+
in
+
+
match unix_result, eio_result with
+
| Ok gpx_unix, Ok gpx_eio ->
+
check bool "Unix and Eio produce equivalent results" true
+
(compare_gpx_basic gpx_unix gpx_eio);
+
check string "Creators match" gpx_unix.creator gpx_eio.creator;
+
check int "Waypoint counts match"
+
(List.length gpx_unix.waypoints) (List.length gpx_eio.waypoints);
+
check int "Route counts match"
+
(List.length gpx_unix.routes) (List.length gpx_eio.routes);
+
check int "Track counts match"
+
(List.length gpx_unix.tracks) (List.length gpx_eio.tracks)
+
| Error _, Error _ ->
+
(* Both failed - that's consistent *)
+
check bool "Both Unix and Eio failed consistently" true true
+
| Ok _, Error _ ->
+
failf "Unix succeeded but Eio failed for %s" filename
+
| Error _, Ok _ ->
+
failf "Eio succeeded but Unix failed for %s" filename
+
+
(** Test write-read round-trip with Unix *)
+
let test_unix_round_trip filename () =
+
let path = Filename.concat test_data_dir filename in
+
match Gpx_unix.read path with
+
| Ok gpx_original ->
+
(* Write to temporary string *)
+
(match Gpx.write_string gpx_original with
+
| Ok xml_string ->
+
(* Parse the written string *)
+
(match Gpx.parse_string xml_string with
+
| Ok gpx_roundtrip ->
+
check bool "Round-trip preserves basic structure" true
+
(compare_gpx_basic gpx_original gpx_roundtrip);
+
check string "Creator preserved"
+
gpx_original.creator gpx_roundtrip.creator
+
| Error _ ->
+
failf "Round-trip parse failed for %s" filename)
+
| Error _ ->
+
failf "Round-trip write failed for %s" filename)
+
| Error _ ->
+
failf "Initial read failed for %s" filename
+
+
(** Test write-read round-trip with Eio *)
+
let test_eio_round_trip filename () =
+
Eio_main.run @@ fun env ->
+
let fs = Eio.Stdenv.fs env in
+
let path = Filename.concat test_data_dir filename in
+
try
+
let gpx_original = Gpx_eio.read ~fs path in
+
(* Write to temporary string via GPX core *)
+
match Gpx.write_string gpx_original with
+
| Ok xml_string ->
+
(* Parse the written string *)
+
(match Gpx.parse_string xml_string with
+
| Ok gpx_roundtrip ->
+
check bool "Round-trip preserves basic structure" true
+
(compare_gpx_basic gpx_original gpx_roundtrip);
+
check string "Creator preserved"
+
gpx_original.creator gpx_roundtrip.creator
+
| Error _ ->
+
failf "Round-trip parse failed for %s" filename)
+
| Error _ ->
+
failf "Round-trip write failed for %s" filename
+
with
+
| Gpx.Gpx_error _ ->
+
failf "Initial read failed for %s" filename
+
+
(** Test validation works on all files *)
+
let test_validation filename () =
+
let path = Filename.concat test_data_dir filename in
+
match Gpx_unix.read path with
+
| Ok gpx ->
+
let validation = Gpx.validate_gpx gpx in
+
check bool "Validation runs without error" true true;
+
(* All our test files should be valid *)
+
if filename <> "invalid.gpx" then
+
check bool "Test file is valid" true validation.is_valid
+
| Error _ ->
+
(* Invalid.gpx should fail to parse - this is expected *)
+
if filename = "invalid.gpx" then
+
check bool "Invalid file correctly fails to parse" true true
+
else
+
failf "Could not read %s for validation test" filename
+
+
(** Test error handling with invalid file *)
+
let test_error_handling () =
+
let path = Filename.concat test_data_dir "invalid.gpx" in
+
+
(* Test Unix error handling *)
+
(match Gpx_unix.read path with
+
| Ok _ ->
+
failf "Unix should have failed to parse invalid.gpx"
+
| Error _ ->
+
check bool "Unix correctly rejects invalid file" true true);
+
+
(* Test Eio error handling *)
+
(try
+
Eio_main.run @@ fun env ->
+
let fs = Eio.Stdenv.fs env in
+
let _ = Gpx_eio.read ~fs path in
+
failf "Eio should have failed to parse invalid.gpx"
+
with
+
| Gpx.Gpx_error _ ->
+
check bool "Eio correctly rejects invalid file" true true)
+
+
(** Performance comparison test *)
+
let test_performance_comparison filename () =
+
let path = Filename.concat test_data_dir filename in
+
+
(* Time Unix parsing *)
+
let start_unix = Sys.time () in
+
let _ = Gpx_unix.read path in
+
let unix_time = Sys.time () -. start_unix in
+
+
(* Time Eio parsing *)
+
let start_eio = Sys.time () in
+
let _ = Eio_main.run @@ fun env ->
+
let fs = Eio.Stdenv.fs env in
+
try Some (Gpx_eio.read ~fs path)
+
with Gpx.Gpx_error _ -> None
+
in
+
let eio_time = Sys.time () -. start_eio in
+
+
(* Both should complete reasonably quickly (under 1 second for test files) *)
+
check bool "Unix parsing completes quickly" true (unix_time < 1.0);
+
check bool "Eio parsing completes quickly" true (eio_time < 1.0);
+
+
Printf.printf "Performance for %s: Unix=%.3fms, Eio=%.3fms\n"
+
filename (unix_time *. 1000.) (eio_time *. 1000.)
+
+
(** Generate test cases for each file *)
+
let make_unix_tests () =
+
List.map (fun filename ->
+
test_case filename `Quick (test_unix_parsing filename)
+
) test_files
+
+
let make_eio_tests () =
+
List.map (fun filename ->
+
test_case filename `Quick (test_eio_parsing filename)
+
) test_files
+
+
let make_equivalence_tests () =
+
List.map (fun filename ->
+
test_case filename `Quick (test_unix_eio_equivalence filename)
+
) test_files
+
+
let make_unix_round_trip_tests () =
+
List.map (fun filename ->
+
test_case filename `Quick (test_unix_round_trip filename)
+
) test_files
+
+
let make_eio_round_trip_tests () =
+
List.map (fun filename ->
+
test_case filename `Quick (test_eio_round_trip filename)
+
) test_files
+
+
let make_validation_tests () =
+
List.map (fun filename ->
+
test_case filename `Quick (test_validation filename)
+
) (test_files @ ["invalid.gpx"])
+
+
let make_performance_tests () =
+
List.map (fun filename ->
+
test_case filename `Quick (test_performance_comparison filename)
+
) test_files
+
+
(** Main test suite *)
+
let () =
+
run "GPX Corpus Tests" [
+
"Unix parsing", make_unix_tests ();
+
"Eio parsing", make_eio_tests ();
+
"Unix vs Eio equivalence", make_equivalence_tests ();
+
"Unix round-trip", make_unix_round_trip_tests ();
+
"Eio round-trip", make_eio_round_trip_tests ();
+
"Validation", make_validation_tests ();
+
"Error handling", [
+
test_case "invalid file handling" `Quick test_error_handling;
+
];
+
"Performance", make_performance_tests ();
+
]
+19 -19
test/test_gpx.ml
···
let test_coordinate_validation () =
(* Test valid coordinates *)
-
assert (Result.is_ok (Types.latitude 45.0));
-
assert (Result.is_ok (Types.longitude (-122.0)));
-
assert (Result.is_ok (Types.degrees 180.0));
+
assert (Result.is_ok (latitude 45.0));
+
assert (Result.is_ok (longitude (-122.0)));
+
assert (Result.is_ok (degrees 180.0));
(* Test invalid coordinates *)
-
assert (Result.is_error (Types.latitude 91.0));
-
assert (Result.is_error (Types.longitude 180.0));
-
assert (Result.is_error (Types.degrees 360.0));
+
assert (Result.is_error (latitude 91.0));
+
assert (Result.is_error (longitude 180.0));
+
assert (Result.is_error (degrees 360.0));
Printf.printf "✓ Coordinate validation tests passed\n"
let test_fix_type_conversion () =
(* Test fix type string conversion *)
-
assert (Types.fix_type_to_string Types.Fix_2d = "2d");
-
assert (Types.fix_type_of_string "3d" = Some Types.Fix_3d);
-
assert (Types.fix_type_of_string "invalid" = None);
+
assert (fix_type_to_string Fix_2d = "2d");
+
assert (fix_type_of_string "3d" = Some Fix_3d);
+
assert (fix_type_of_string "invalid" = None);
Printf.printf "✓ Fix type conversion tests passed\n"
let test_gpx_creation () =
let creator = "test" in
-
let gpx = Types.make_gpx ~creator in
+
let gpx = make_gpx ~creator in
assert (gpx.creator = creator);
assert (gpx.version = "1.1");
assert (gpx.waypoints = []);
···
</wpt>
</gpx>|} in
-
match Gpx_parser.parse_string gpx_xml with
+
match parse_string gpx_xml with
| Ok gpx ->
assert (gpx.creator = "test");
assert (List.length gpx.waypoints = 1);
···
assert false
let test_simple_writing () =
-
let lat = Result.get_ok (Types.latitude 37.7749) in
-
let lon = Result.get_ok (Types.longitude (-122.4194)) in
-
let wpt = { (Types.make_waypoint_data lat lon) with
+
let lat = Result.get_ok (latitude 37.7749) in
+
let lon = Result.get_ok (longitude (-122.4194)) in
+
let wpt = { (make_waypoint_data lat lon) with
name = Some "Test Point";
desc = Some "A test waypoint" } in
-
let gpx = { (Types.make_gpx ~creator:"test") with
+
let gpx = { (make_gpx ~creator:"test") with
waypoints = [wpt] } in
-
match Writer.write_string gpx with
+
match write_string gpx with
| Ok xml_string ->
assert (try ignore (String.index xml_string 'T'); true with Not_found -> false);
assert (try ignore (String.index xml_string '3'); true with Not_found -> false);
···
assert false
let test_validation () =
-
let gpx = Types.make_gpx ~creator:"" in
-
let validation = Validate.validate_gpx gpx in
+
let gpx = make_gpx ~creator:"" in
+
let validation = validate_gpx gpx in
assert (not validation.is_valid);
-
let errors = List.filter (fun issue -> issue.Validate.level = `Error) validation.issues in
+
let errors = List.filter (fun issue -> issue.level = `Error) validation.issues in
assert (List.length errors > 0);
Printf.printf "✓ Validation tests passed\n"