From 1afe333a4e8b8772e8d7f7948a62a740f71d0cd1 Mon Sep 17 00:00:00 2001 From: Francesco Mecca Date: Sat, 7 Dec 2024 13:55:55 +0100 Subject: [PATCH] tests and docs --- Pentole/Pentole.fsproj | 2 +- Pentole/path.fs | 211 +++++++++++++++++++++++-------------- Pentole/string.fs | 6 +- Pentole/test_extensions.fs | 31 +++--- tests/path_tests.fs | 29 +++-- tests/string_tests.fs | 1 - tests/tests.fsproj | 2 +- 7 files changed, 175 insertions(+), 107 deletions(-) diff --git a/Pentole/Pentole.fsproj b/Pentole/Pentole.fsproj index c4ba52d..57bb8b0 100644 --- a/Pentole/Pentole.fsproj +++ b/Pentole/Pentole.fsproj @@ -22,7 +22,7 @@ - + diff --git a/Pentole/path.fs b/Pentole/path.fs index 030baa9..4e4e183 100644 --- a/Pentole/path.fs +++ b/Pentole/path.fs @@ -1,33 +1,150 @@ -module Pentole.Path +namespace Pentole + +open System -let private invalid_chars = System.IO.Path.GetInvalidPathChars() +module Internal = + let private invalid_chars = System.IO.Path.GetInvalidPathChars () -type Path = | Absolute of string | Relative of string -with - static member of_string (path: string) = + let trim (a: string) = + System.IO.Path.TrimEndingDirectorySeparator a + + let construct (path: string): Result<(string * bool), string> = let is_absolute (path: string) = System.IO.Path.IsPathFullyQualified path - if System.String.IsNullOrEmpty path then Error "Can't create a path from an empty string" elif invalid_chars |> Seq.exists (fun c -> path.Contains c) then Error $"The string contains invalid characters: {path}" elif is_absolute path then path - |> System.IO.Path.TrimEndingDirectorySeparator - |> Absolute - |> Ok + |> trim + |> fun p -> Ok (p, true) else path - |> System.IO.Path.TrimEndingDirectorySeparator - |> Relative - |> Ok + |> trim + |> fun p -> Ok (p, false) + +type IPath = + abstract member ToString: unit -> string + abstract member string_value: string + inherit IEquatable + inherit IComparable + +[] +[] +[] +type AbsolutePath internal (path: string) = + interface IPath with + member _.ToString () = $"AbsolutePath {path}" + member _.string_value = path + + member x.CompareTo (o: obj) = + match o with + | :? IPath as abs -> + let a = abs.string_value + a.CompareTo (x :> IPath |> _.string_value) + | _ -> 1 + + + interface IEquatable with + member _.Equals (o: IPath) = + Internal.trim o.string_value = path + +[] +[] +[] +type RelativePath internal (path: string) = + interface IPath with + member _.ToString() = $"RelativePath {path}" + member _.string_value = path + + member x.CompareTo (o: obj) = + match o with + | :? IPath as abs -> + let a = abs.string_value + a.CompareTo (x :> IPath |> _.string_value) + | _ -> 1 + + interface IEquatable with + member _.Equals (o: IPath) = + Internal.trim o.string_value = path + +module Path = + let of_string (path: string): Result = + match Internal.construct path with + | Ok (path, true) -> AbsolutePath path :> IPath |> Ok + | Ok (path, false) -> RelativePath path :> IPath |> Ok + | Error e -> Error e + + let private should_not_fail (st: string) = + st + |> of_string + |> Result.defaultWith (fun exn -> failwith exn) + + /// Returns the extension of the given path + let extension (path: IPath) = + path.string_value + |> IO.Path.GetExtension + + /// Return the basename of the given path + let basename (path: IPath) = + path.string_value |> IO.Path.GetFileNameWithoutExtension + + /// Concatenates `path` and `other` and adds a directory separator character between any of the path components if one is not already present. + let join (path: IPath) (other: IPath) = + (path.string_value, other.string_value) + |> System.IO.Path.Join + |> should_not_fail + + /// Change the extension of the given path + let with_extension (path: IPath) (extension: string) = + System.IO.Path.ChangeExtension (path.string_value, extension) + |> should_not_fail + + /// + /// Determines wheter the path is normalized. + /// A normalized path is a path that does not contain any "." or ".." path segments and has no trailing or duplicate slashes + /// + let is_normalized (path: IPath) = + + let path: string = path.string_value + + let rec checkSegments = function + | [] -> true + | ("." | "..")::_ -> false + | _::tail -> checkSegments tail + + not (path.Contains "//") && (String.split "/" path |> checkSegments) + + /// Returns the parent directory of the given path. + /// + /// If the path is the root directory or does not have a parent, the original path is returned. + /// + /// # Arguments + /// + /// * `path`: The path to get the parent directory of. + /// + /// # Returns + /// The parent directory of the given path, or the original path if it has no parent + let parent (path: IPath) = + let parent = IO.Path.GetDirectoryName path.string_value + if parent = null then + path + else + parent |> should_not_fail + + +// impure functions +// https://docs.python.org/3.8/library/pathlib.html +module FileSystem = + let resolve (path: IPath) = + Native.realpath path.string_value |> Result.bind Path.of_string +(* +module Pentole.Path + -let private string_apply fun_ = function - | Absolute p -> fun_ p |> Absolute - | Relative p -> fun_ p |> Relative /// /// Return the parent path @@ -36,46 +153,12 @@ let private string_apply fun_ = function let parent (path: Path) = failwith "TODO" -/// -/// Return the extension of the given path -/// -/// -let extension = function - | Absolute p ->System.IO.Path.GetExtension p - | Relative p ->System.IO.Path.GetExtension p - -/// -/// Return the basename of the given path -/// -/// -let basename = function - | Absolute p -> System.IO.Path.GetFileNameWithoutExtension p - | Relative p -> System.IO.Path.GetFileNameWithoutExtension p - -/// -/// Concatenates `path` and `other` and adds a directory separator character between any of the path components if one is not already present. -/// -/// -/// -let join (path: Path) (other: Path) = - match (path, other) with - | (Absolute path | Relative path), (Absolute other| Relative other) -> - System.IO.Path.Join (path, other) |> Path.of_string - let relative_to (parent: Path) (child: Path) = match (parent, child) with | (Absolute parent | Relative parent), (Absolute child| Relative child) -> System.IO.Path.GetRelativePath (parent, child) |> Path.of_string -/// -/// Change the extension of the given path -/// -/// -/// -let with_extension (path: Path) (extension: string) = - path - |> string_apply (fun path -> System.IO.Path.ChangeExtension (path, extension)) /// /// Change the basename of the path @@ -86,33 +169,5 @@ let with_name (path: Path) (new_: string) = failwith "t" -let (|Nil|Cons|) (arr: 'a array) = - match arr.Length with - | 0 -> Nil - | 1 -> Cons(arr.[0], [||]) - | _ -> Cons(arr.[0], arr.[1..]) -/// -/// Determines wheter the path is normalized. -/// A normalized path is a path that does not contain any "." or ".." path segments and has no trailing or duplicate slashes -/// -/// -let is_normalized (path_: Path) = - - let path: string = path_ |> function | Absolute a -> a | Relative a -> a - - let rec checkSegments = function - | [] -> true - | ("." | "..")::_ -> false - | _::tail -> checkSegments tail - - not (path.Contains "//") && (String.split "/" path |> checkSegments) - -// impure functions -// https://docs.python.org/3.8/library/pathlib.html -module FileSystem = - let resolve = function - | Absolute path -> - Native.realpath path |> Result.bind Path.of_string - | Relative path -> - Native.realpath path |> Result.bind Path.of_string +*) diff --git a/Pentole/string.fs b/Pentole/string.fs index 96c7154..e732a4c 100644 --- a/Pentole/string.fs +++ b/Pentole/string.fs @@ -10,9 +10,9 @@ let split(separator: string) (target: string) : string list = | -1 -> if start_idx < target.Length then target.Substring(start_idx) :: acc - else - acc + else acc | index -> let part = target.Substring (start_idx, index - start_idx) split (part :: acc) (index + slen) - split [] 0 + + split [] 0 |> List.rev diff --git a/Pentole/test_extensions.fs b/Pentole/test_extensions.fs index 2622775..b7eae5a 100644 --- a/Pentole/test_extensions.fs +++ b/Pentole/test_extensions.fs @@ -6,27 +6,34 @@ type Assert with static member inline ok_is_equal<'ok, 'err> (expected: 'ok) (got: Result<'ok, 'err>) = match got with - | Ok got -> Assert.That (got, Is.EqualTo(expected)) + | Ok got -> Assert.AreEqual (expected, got) | Error e -> Assert.Fail $"Expected 'ok, got: {e}" - static member is_true (got: bool) = - Assert.That (got, Is.True) + static member inline is_true (got: bool) = + Assert.IsTrue got - static member is_false (got: bool) = - Assert.That (got, Is.False) + static member inline is_false (got: bool) = + Assert.IsFalse got - static member ok_is_true (r: Result) = + static member inline ok_is_true (r: Result) = match r with - | Ok got -> Assert.That (got, Is.True) + | Ok got -> Assert.is_true got | Error e -> Assert.Fail $"Expected truth value, got: {e}" - static member ok_is_false (r: Result) = + static member inline ok_is_false (r: Result) = match r with - | Ok got -> Assert.That (got, Is.False) + | Ok got -> Assert.is_false got | Error e -> Assert.Fail $"Expected truth value, got: {e}" static member inline are_equal expected (got: 'a) = - Assert.That (got, Is.EqualTo(expected)) + Assert.AreEqual (expected, got) - static member are_seq_equal (expected: 'a seq) (got: 'a seq) = - Assert.That (got, Is.EquivalentTo(expected)) + static member inline are_seq_equal (expected: 'a seq) (got: 'a seq) = + let s_exp = Seq.map id expected + let g_exp = Seq.map id got + CollectionAssert.AreEqual (s_exp, g_exp) + + static member inline are_not_equal expected (got: 'a) = + Assert.AreNotEqual (expected, got) + + static member inline fail = Assert.Fail diff --git a/tests/path_tests.fs b/tests/path_tests.fs index 989737e..19b83c0 100644 --- a/tests/path_tests.fs +++ b/tests/path_tests.fs @@ -10,11 +10,14 @@ open Pentole.Path let constructor_test () = "/" |> Path.of_string |> Result.isOk |> Assert.is_true '\000' |> string |> Path.of_string |> Result.isOk |> Assert.is_false + [] let absolute_test () = - let is_absolute = function - | (Absolute path | Relative path) -> - System.IO.Path.IsPathFullyQualified path + + + let is_absolute (p: IPath) = + p.string_value + |> System.IO.Path.IsPathFullyQualified let test (s:string) = s @@ -53,18 +56,22 @@ let resolve_test () = let equality_test () = let p (n: string ) = Path.of_string n |> Result.Unsafe.get Assert.are_equal (p "/etc") (p "/etc/") -(* + Assert.are_not_equal (p "etc") (p "/etc/") + Assert.are_not_equal (p "etc") (p "/etc") + Assert.are_equal (Path.of_string "/tmp" ) ( Path.of_string "/tmp/") + Assert.is_true (Path.of_string "/tmp" = Path.of_string "/tmp/") + [] let parent_test () = - let p (n: string ) = Path.of_string n |> Result.get + let p (n: string ) = Path.of_string n |> Result.Unsafe.get let test (s:string) = s |> Path.of_string - |> Result.get + |> Result.Unsafe.get |> parent - "/" |> test |> Assert.are_equal [p "/"] - "/etc/" |> test |> Assert.are_seq_equal ["/"] - "/etc/conf" |> test |> Assert.are_seq_equal [p "/etc"] - "/etc/../etc" |> test |> Assert.ok_is_equal (p "/etc") -*) + "/" |> test |> Assert.are_equal (p "/") + "/etc/" |> test |> Assert.are_equal (p "/") + "/etc/conf" |> test |> Assert.are_equal (p "/etc") + "/etc/../etc" |> test |> Assert.are_equal (p "/etc/../") + "etc/../etc" |> test |> Assert.are_equal (p "etc/../") diff --git a/tests/string_tests.fs b/tests/string_tests.fs index ad2cb71..97e7617 100644 --- a/tests/string_tests.fs +++ b/tests/string_tests.fs @@ -9,4 +9,3 @@ open Pentole let split_test () = let target = "a/b/c" Assert.are_seq_equal (target.Split("/")) (String.split "/" target) - diff --git a/tests/tests.fsproj b/tests/tests.fsproj index 971a5cc..5995f4a 100644 --- a/tests/tests.fsproj +++ b/tests/tests.fsproj @@ -18,7 +18,7 @@ - +