tests and docs
This commit is contained in:
parent
c3e1d26ab3
commit
1afe333a4e
7 changed files with 175 additions and 107 deletions
|
@ -22,7 +22,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nunit" Version="4.2.1" />
|
||||
<PackageReference Include="Nunit" Version="3.14.0" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
|
|
211
Pentole/path.fs
211
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<IPath>
|
||||
inherit IComparable
|
||||
|
||||
[<Struct>]
|
||||
[<CustomEquality>]
|
||||
[<CustomComparison>]
|
||||
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<IPath> with
|
||||
member _.Equals (o: IPath) =
|
||||
Internal.trim o.string_value = path
|
||||
|
||||
[<Struct>]
|
||||
[<CustomEquality>]
|
||||
[<CustomComparison>]
|
||||
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<IPath> with
|
||||
member _.Equals (o: IPath) =
|
||||
Internal.trim o.string_value = path
|
||||
|
||||
module Path =
|
||||
let of_string (path: string): Result<IPath, string> =
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Return the parent path
|
||||
|
@ -36,46 +153,12 @@ let private string_apply fun_ = function
|
|||
let parent (path: Path) =
|
||||
failwith "TODO"
|
||||
|
||||
/// <summary>
|
||||
/// Return the extension of the given path
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
let extension = function
|
||||
| Absolute p ->System.IO.Path.GetExtension p
|
||||
| Relative p ->System.IO.Path.GetExtension p
|
||||
|
||||
/// <summary>
|
||||
/// Return the basename of the given path
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
let basename = function
|
||||
| Absolute p -> System.IO.Path.GetFileNameWithoutExtension p
|
||||
| Relative p -> System.IO.Path.GetFileNameWithoutExtension p
|
||||
|
||||
/// <summary>
|
||||
/// Concatenates `path` and `other` and adds a directory separator character between any of the path components if one is not already present.
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="other"></param>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Change the extension of the given path
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="extension"></param>
|
||||
let with_extension (path: Path) (extension: string) =
|
||||
path
|
||||
|> string_apply (fun path -> System.IO.Path.ChangeExtension (path, extension))
|
||||
|
||||
/// <summary>
|
||||
/// 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..])
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
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
|
||||
*)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<bool, 'a>) =
|
||||
static member inline ok_is_true (r: Result<bool, 'a>) =
|
||||
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<bool, 'e>) =
|
||||
static member inline ok_is_false (r: Result<bool, 'e>) =
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
[<Test>]
|
||||
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/")
|
||||
|
||||
[<Test>]
|
||||
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/../")
|
||||
|
|
|
@ -9,4 +9,3 @@ open Pentole
|
|||
let split_test () =
|
||||
let target = "a/b/c"
|
||||
Assert.are_seq_equal (target.Split("/")) (String.split "/" target)
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="NUnit" Version="4.2.1" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
|
|
Loading…
Reference in a new issue