Compare changes

Choose any two refs to compare.

+1 -1
bin/dune
···
(executable
(public_name mlgpx)
(name mlgpx_cli)
-
(libraries gpx gpx_eio cmdliner eio_main fmt fmt.tty fmt.cli))
···
(executable
(public_name mlgpx)
(name mlgpx_cli)
+
(libraries gpx gpx_eio cmdliner eio_main fmt fmt.tty fmt.cli))
+6 -3
bin/mlgpx_cli.ml
···
open Cmdliner
open Gpx
(* Terminal and formatting setup *)
let setup_fmt style_renderer =
Fmt_tty.setup_std_outputs ?style_renderer ();
···
log_info "Reading GPX file: %a" (bold_style Fmt.string) input_file;
(* Read input GPX *)
-
let gpx = Gpx_eio.read ~fs input_file in
if verbose then
log_info "Found %d waypoints and %d existing tracks"
···
log_info "Writing output to: %a" (bold_style Fmt.string) output_file;
(* Write output GPX *)
-
Gpx_eio.write ~fs output_file output_gpx;
if verbose then (
Fmt.pf Format.std_formatter "%a\n" (success_style Fmt.string) "Conversion completed successfully!";
···
if verbose then
log_info "Analyzing GPX file: %a" (bold_style Fmt.string) input_file;
-
let gpx = Gpx_eio.read ~fs input_file in
(* Header *)
Fmt.pf Format.std_formatter "%a\n" (bold_style Fmt.string) "GPX File Information";
···
let duration_span = Ptime.diff stop_time start_time in
match Ptime.Span.to_int_s duration_span with
| Some seconds ->
let days = seconds / 86400 in
let hours = (seconds mod 86400) / 3600 in
let minutes = (seconds mod 3600) / 60 in
···
open Cmdliner
open Gpx
+
let ( / ) = Eio.Path.( / )
+
(* Terminal and formatting setup *)
let setup_fmt style_renderer =
Fmt_tty.setup_std_outputs ?style_renderer ();
···
log_info "Reading GPX file: %a" (bold_style Fmt.string) input_file;
(* Read input GPX *)
+
let gpx = Gpx_eio.read (fs / input_file) in
if verbose then
log_info "Found %d waypoints and %d existing tracks"
···
log_info "Writing output to: %a" (bold_style Fmt.string) output_file;
(* Write output GPX *)
+
Gpx_eio.write (fs / output_file) output_gpx;
if verbose then (
Fmt.pf Format.std_formatter "%a\n" (success_style Fmt.string) "Conversion completed successfully!";
···
if verbose then
log_info "Analyzing GPX file: %a" (bold_style Fmt.string) input_file;
+
let gpx = Gpx_eio.read (fs / input_file) in
(* Header *)
Fmt.pf Format.std_formatter "%a\n" (bold_style Fmt.string) "GPX File Information";
···
let duration_span = Ptime.diff stop_time start_time in
match Ptime.Span.to_int_s duration_span with
| Some seconds ->
+
let ( / ) = Int.div in
let days = seconds / 86400 in
let hours = (seconds mod 86400) / 3600 in
let minutes = (seconds mod 3600) / 60 in
+1 -1
dune-project
···
(package
(name mlgpx)
-
(depends ocaml dune xmlm ptime eio ppx_expect alcotest eio_main cmdliner fmt logs)
(synopsis "Library and CLI for parsing and generating GPS Exchange (GPX) formats")
(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.")
···
(package
(name mlgpx)
+
(depends ocaml dune xmlm ptime (eio (>= 1.2)) ppx_expect alcotest eio_main cmdliner fmt logs)
(synopsis "Library and CLI for parsing and generating GPS Exchange (GPX) formats")
(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.")
+14 -1
lib/gpx/dune
···
(public_name mlgpx.core)
(name gpx)
(libraries xmlm ptime)
-
(modules gpx parser writer validate coordinate link extension waypoint metadata route track error doc))
···
(public_name mlgpx.core)
(name gpx)
(libraries xmlm ptime)
+
(modules
+
gpx
+
parser
+
writer
+
validate
+
coordinate
+
link
+
extension
+
waypoint
+
metadata
+
route
+
track
+
error
+
doc))
+1 -1
lib/gpx_eio/dune
···
(public_name mlgpx.eio)
(name gpx_eio)
(libraries eio xmlm ptime gpx)
-
(modules gpx_io gpx_eio))
···
(public_name mlgpx.eio)
(name gpx_eio)
(libraries eio xmlm ptime gpx)
+
(modules gpx_io gpx_eio))
+3 -3
lib/gpx_eio/gpx_eio.ml
···
module IO = Gpx_io
(** Read and parse GPX file *)
-
let read ?(validate=false) ~fs path = IO.read_file ~validate ~fs path
(** Write GPX to file *)
-
let write ?(validate=false) ~fs path gpx = IO.write_file ~validate ~fs path gpx
(** Write GPX to file with backup *)
-
let write_with_backup ?(validate=false) ~fs path gpx = IO.write_file_with_backup ~validate ~fs path gpx
(** Read GPX from Eio source *)
let from_source ?(validate=false) source = IO.read_source ~validate source
···
module IO = Gpx_io
(** Read and parse GPX file *)
+
let read ?(validate=false) path = IO.read_file ~validate path
(** Write GPX to file *)
+
let write ?(validate=false) path gpx = IO.write_file ~validate path gpx
(** Write GPX to file with backup *)
+
let write_with_backup ?(validate=false) path gpx = IO.write_file_with_backup ~validate path gpx
(** Read GPX from Eio source *)
let from_source ?(validate=false) source = IO.read_source ~validate source
+7 -10
lib/gpx_eio/gpx_eio.mli
···
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
@param ?validate Optional validation flag (default: false)
@return GPX document
@raises Gpx.Gpx_error on read or parse failure *)
-
val read : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t
(** Write GPX to file.
-
@param fs Filesystem capability
@param path File path to write
@param gpx GPX document to write
@param ?validate Optional validation flag (default: false)
@raises Gpx.Gpx_error on write failure *)
-
val write : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t -> unit
(** Write GPX to file with automatic backup.
-
@param fs Filesystem capability
@param path File path to write
@param gpx GPX document to write
@param ?validate Optional validation flag (default: false)
-
@return Backup file path (empty if no backup created)
@raises Gpx.Gpx_error on failure *)
-
val write_with_backup : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t -> string
(** {2 Stream Operations}
···
@param ?validate Optional validation flag (default: false)
@return GPX document
@raises Gpx.Gpx_error on read or parse failure *)
-
val from_source : ?validate:bool -> [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.t
(** Write GPX to Eio sink.
@param sink Output flow
@param gpx GPX document
@param ?validate Optional validation flag (default: false)
@raises Gpx.Gpx_error on write failure *)
-
val to_sink : ?validate:bool -> [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.t -> unit
(** Print GPX statistics to sink.
@param sink Output sink
@param gpx GPX document *)
-
val print_stats : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.t -> unit
···
These functions provide simple file I/O with the filesystem from {!Eio.Stdenv.fs}. *)
(** Read and parse GPX file.
@param path File path to read
@param ?validate Optional validation flag (default: false)
@return GPX document
@raises Gpx.Gpx_error on read or parse failure *)
+
val read : ?validate:bool -> _ Eio.Path.t -> Gpx.t
(** Write GPX to file.
@param path File path to write
@param gpx GPX document to write
@param ?validate Optional validation flag (default: false)
@raises Gpx.Gpx_error on write failure *)
+
val write : ?validate:bool -> _ Eio.Path.t -> Gpx.t -> unit
(** Write GPX to file with automatic backup.
@param path File path to write
@param gpx GPX document to write
@param ?validate Optional validation flag (default: false)
+
@return Backup file path ([None] if no backup created)
@raises Gpx.Gpx_error on failure *)
+
val write_with_backup : ?validate:bool -> 'a Eio.Path.t -> Gpx.t -> 'a Eio.Path.t option
(** {2 Stream Operations}
···
@param ?validate Optional validation flag (default: false)
@return GPX document
@raises Gpx.Gpx_error on read or parse failure *)
+
val from_source : ?validate:bool -> _ Eio.Flow.source -> Gpx.t
(** Write GPX to Eio sink.
@param sink Output flow
@param gpx GPX document
@param ?validate Optional validation flag (default: false)
@raises Gpx.Gpx_error on write failure *)
+
val to_sink : ?validate:bool -> _ Eio.Flow.sink -> Gpx.t -> unit
(** Print GPX statistics to sink.
@param sink Output sink
@param gpx GPX document *)
+
val print_stats : _ Eio.Flow.sink -> Gpx.t -> unit
+27 -29
lib/gpx_eio/gpx_io.ml
···
(** GPX Eio I/O operations *)
(** Read GPX from file path *)
-
let read_file ?(validate=false) ~fs path =
-
let content = Eio.Path.load Eio.Path.(fs / path) in
match Gpx.parse_string ~validate content with
| Ok gpx -> gpx
| Error err -> raise (Gpx.Gpx_error err)
(** Write GPX to file path *)
-
let write_file ?(validate=false) ~fs path gpx =
match Gpx.write_string ~validate 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 Eio source *)
···
| Error err -> raise (Gpx.Gpx_error err)
(** 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.Error.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 ?(validate=false) ~fs path gpx =
-
let backup_path = create_backup ~fs path in
try
-
write_file ~validate ~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
···
(** GPX Eio I/O operations *)
(** Read GPX from file path *)
+
let read_file ?(validate=false) path =
+
let content = Eio.Path.load path in
match Gpx.parse_string ~validate content with
| Ok gpx -> gpx
| Error err -> raise (Gpx.Gpx_error err)
(** Write GPX to file path *)
+
let write_file ?(validate=false) path gpx =
match Gpx.write_string ~validate gpx with
| Ok xml_string ->
+
Eio.Path.save ~create:(`Or_truncate 0o644) path xml_string
| Error err -> raise (Gpx.Gpx_error err)
(** Read GPX from Eio source *)
···
| Error err -> raise (Gpx.Gpx_error err)
(** Check if file exists *)
+
let file_exists = Eio.Path.is_file
(** Get file size *)
+
let file_size path =
try
+
let stat = Eio.Path.stat ~follow:true path in
Optint.Int63.to_int stat.size
with
| exn -> raise (Gpx.Gpx_error (Gpx.Error.io_error (Printexc.to_string exn)))
(** Create backup of existing file *)
+
let create_backup ((fs, inner_path) as path) =
+
if file_exists path then
+
let backup_path = inner_path ^ ".backup" in
+
let content = Eio.Path.load path in
+
Eio.Path.save ~create:(`Or_truncate 0o644) (fs, backup_path) content;
+
Some (fs, backup_path)
else
+
None
(** Write GPX to file with automatic backup *)
+
let write_file_with_backup ?(validate=false) path gpx =
+
let backup_path = create_backup path in
try
+
write_file ~validate path gpx;
backup_path
with
| Gpx.Gpx_error _ as err ->
(* Try to restore backup if write failed *)
+
match backup_path with
+
| Some backup_path ->
+
if file_exists backup_path then (
+
try
+
let backup_content = Eio.Path.load backup_path in
+
Eio.Path.save ~create:(`Or_truncate 0o644) path backup_content
+
with _ -> () (* Ignore restore errors *)
+
);
+
raise err
+
| _ -> raise err
+7 -13
lib/gpx_eio/gpx_io.mli
···
(** {1 File Operations} *)
(** Read GPX from file path.
-
@param fs Filesystem capability
@param path File path to read
@param ?validate Optional validation flag (default: false)
@return GPX document or error *)
-
val read_file : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t
(** Write GPX to file path.
-
@param fs Filesystem capability
@param path File path to write
@param gpx GPX document to write
@param ?validate Optional validation flag (default: false)
@raises Gpx.Gpx_error on write failure *)
-
val write_file : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t -> unit
(** {1 Stream Operations}
···
(** {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
@param ?validate Optional validation flag (default: false)
-
@return Backup file path (empty string if no backup needed) *)
-
val write_file_with_backup : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t -> string
···
(** {1 File Operations} *)
(** Read GPX from file path.
@param path File path to read
@param ?validate Optional validation flag (default: false)
@return GPX document or error *)
+
val read_file : ?validate:bool -> _ Eio.Path.t -> Gpx.t
(** Write GPX to file path.
@param path File path to write
@param gpx GPX document to write
@param ?validate Optional validation flag (default: false)
@raises Gpx.Gpx_error on write failure *)
+
val write_file : ?validate:bool -> _ Eio.Path.t -> Gpx.t -> unit
(** {1 Stream Operations}
···
(** {1 Utility Functions} *)
(** Check if file exists.
@param path File path to check
@return [true] if file exists and is readable *)
+
val file_exists : _ Eio.Path.t -> bool
(** Get file size.
@param path File path
@return File size in bytes *)
+
val file_size : _ Eio.Path.t -> int
(** Create backup of existing file.
@param path Original file path
@return Backup file path *)
+
val create_backup : 'a Eio.Path.t -> 'a Eio.Path.t option
(** Write GPX to file with automatic backup.
Creates a backup of existing file before writing new content.
@param path File path to write
@param gpx GPX document to write
@param ?validate Optional validation flag (default: false)
+
@return Backup file path ([None] if no backup needed) *)
+
val write_file_with_backup : ?validate:bool -> 'a Eio.Path.t -> Gpx.t -> 'a Eio.Path.t option
+1 -1
lib/gpx_unix/dune
···
(public_name mlgpx.unix)
(name gpx_unix)
(libraries unix xmlm ptime gpx)
-
(modules gpx_io gpx_unix))
···
(public_name mlgpx.unix)
(name gpx_unix)
(libraries unix xmlm ptime gpx)
+
(modules gpx_io gpx_unix))
+2 -1
mlgpx.opam
···
"dune" {>= "3.18"}
"xmlm"
"ptime"
-
"eio"
"ppx_expect"
"alcotest"
"eio_main"
···
]
]
x-maintenance-intent: ["(latest)"]
···
"dune" {>= "3.18"}
"xmlm"
"ptime"
+
"eio" {>= "1.2"}
"ppx_expect"
"alcotest"
"eio_main"
···
]
]
x-maintenance-intent: ["(latest)"]
+
dev-repo: "git+https://tangled.sh/@anil.recoil.org/ocaml-gpx"
+1
mlgpx.opam.template
···
···
+
dev-repo: "git+https://tangled.sh/@anil.recoil.org/ocaml-gpx"
+5 -2
test/dune
···
(modules test_gpx))
;; ppx_expect inline tests
(library
(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))
···
(modules test_gpx))
;; ppx_expect inline tests
+
(library
(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))
+7 -5
test/test_corpus_unix_eio.ml
···
open Alcotest
let test_data_dir = "test/data"
let test_files = [
···
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 (
···
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
···
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 ->
···
(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 _ ->
···
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
···
open Alcotest
+
let ( / ) = Eio.Path. ( / )
+
let test_data_dir = "test/data"
let test_files = [
···
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 (
···
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
···
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 ->
···
(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 _ ->
···
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