From a396924a82505c64f0ab549900eafbe8729b97fb Mon Sep 17 00:00:00 2001 From: gr-im Date: Fri, 13 Feb 2026 13:15:49 +0100 Subject: [PATCH] Make `Item` less abstract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `item` type was a little too abstract in my opinion, and made expressing the system in the manner of `Simple` laborious outside the ‘friends’ context of Simple and Tree. --- lib/tree.ml | 212 ++++++++++++---------- lib/tree.mli | 157 +++++++++++------ test/read_dir_test.ml | 2 +- test/simple_rewriting.ml | 367 +++++++++++++++++++++++++++++++++++++++ test/tree_core_test.ml | 4 +- 5 files changed, 591 insertions(+), 151 deletions(-) create mode 100644 test/simple_rewriting.ml diff --git a/lib/tree.ml b/lib/tree.ml index c7a5666..36f3e53 100644 --- a/lib/tree.ml +++ b/lib/tree.ml @@ -3,73 +3,106 @@ SPDX-License-Identifier: BSD-3-Clause *) -type ('a, 'metadata) t = - { children : ('a, 'metadata) item list - ; scope : Path.t - } +let path_to_list p = + let prefix, f = if Path.is_absolute p then "", Path.abs else ".", Path.rel + and fragments = Path.to_list p in + f, prefix :: fragments +;; -and ('a, 'metadata) elt = - { name : string - ; content : 'a - ; metadata : 'metadata option - } +module Item = struct + type ('a, 'metadata) t = + | File of + { name : string + ; content : 'a + ; metadata : 'metadata option + } + | Directory of + { name : string + ; children : ('a, 'metadata) t list + ; metadata : 'metadata option + } + + let compare a b = + match a, b with + | File { name = a; _ }, File { name = b; _ } + | Directory { name = a; _ }, Directory { name = b; _ } -> String.compare a b + | File _, Directory _ -> 1 + | Directory _, File _ -> -1 + ;; -and ('a, 'metadata) item = - | File of ('a, 'metadata) elt - | Directory of (('a, 'metadata) item list, 'metadata) elt + (* HACK: To ensure that trees are ordered consistently.*) + let sort xs = List.sort_uniq compare xs -let content = function - | File { content; _ } -> `File content - | Directory { content; _ } -> `Directory content -;; + let dir ?metadata ~name children = + let children = sort children in + Directory { name; children; metadata } + ;; -let set_name name = function - | File elt -> File { elt with name } - | Directory elt -> Directory { elt with name } -;; + let file ?metadata ~name content = File { name; content; metadata } -let metadata = function - | File { metadata; _ } | Directory { metadata; _ } -> metadata -;; + let name_to_string = function + | File { name; _ } -> name + | Directory { name; _ } -> name ^ "/" + ;; -let is_file = function - | File _ -> true - | Directory _ -> false -;; + let name = function + | File { name; _ } -> name + | Directory { name; _ } -> name + ;; -let is_directory = function - | Directory _ -> true - | File _ -> false -;; + let has_name ~name:given = function + | File { name; _ } | Directory { name; _ } -> String.equal name given + ;; -let children = function - | Directory { content; _ } -> content - | File _ -> [] -;; + let content = function + | File { content; _ } -> `File content + | Directory { children; _ } -> `Directory children + ;; -let compare_item a b = - match a, b with - | File { name = a; _ }, File { name = b; _ } - | Directory { name = a; _ }, Directory { name = b; _ } -> String.compare a b - | File _, Directory _ -> 1 - | Directory _, File _ -> -1 -;; + let rename name = function + | File elt -> File { elt with name } + | Directory elt -> Directory { elt with name } + ;; -(* HACK: To ensure that trees are ordered consistently.*) -let sort_items xs = List.sort_uniq compare_item xs + let rec map_content on_file = function + | File elt -> File { elt with content = on_file elt.content } + | Directory elt -> + Directory + { elt with children = List.map (map_content on_file) elt.children } + ;; -let dir ?metadata ~name children = - let content = sort_items children in - Directory { name; content; metadata } -;; + let on_metadata f = function + | File elt -> File { elt with metadata = f elt.metadata } + | Directory elt -> Directory { elt with metadata = f elt.metadata } + ;; -let path_to_list p = - let prefix, f = if Path.is_absolute p then "", Path.abs else ".", Path.rel - and fragments = Path.to_list p in - f, prefix :: fragments -;; + let metadata = function + | File { metadata; _ } | Directory { metadata; _ } -> metadata + ;; + + let is_file = function + | File _ -> true + | Directory _ -> false + ;; + + let is_directory = function + | Directory _ -> true + | File _ -> false + ;; + + let children = function + | Directory { children; _ } -> children + | File _ -> [] + ;; +end + +type ('a, 'metadata) t = + { children : ('a, 'metadata) Item.t list + ; scope : Path.t + } -let file ?metadata ~name content = File { name; content; metadata } +let dir = Item.dir +let file = Item.file let make ?(scope_metadata = fun _ -> None) ~scope list = let rec aux s = function @@ -87,17 +120,6 @@ let make ?(scope_metadata = fun _ -> None) ~scope list = let from_root list = make ~scope:Path.root list let from_cwd list = make ~scope:Path.cwd list -let name_to_string = function - | File { name; _ } -> name - | Directory { name; _ } -> name ^ "/" -;; - -let name = name_to_string - -let has_name ~name:given = function - | File { name; _ } | Directory { name; _ } -> String.equal name given -;; - let resolve_path scope path = if Path.is_absolute scope then Path.resolve ~from:scope path @@ -112,16 +134,16 @@ let fetch ~path fs = | x :: xs, [ name ] -> (* We are on the [basename] of the path; if the names are equivalent, we return the item. *) - if has_name ~name x + if Item.has_name ~name x then Some x else (* Otherwise, we continue to traverse the tree. *) aux xs path - | (Directory { content; _ } as x) :: xs, name :: ps -> + | (Item.Directory { children; _ } as x) :: xs, name :: ps -> (* In a directory case, if the nases are equivalent, we traverse into the directory. *) - if has_name ~name x - then aux content ps + if Item.has_name ~name x + then aux children ps else (* Otherwise, we continue to traverse the tree. *) aux xs path @@ -134,8 +156,8 @@ let fetch ~path fs = let prism ~scope fs = match fetch fs ~path:scope with | None -> make ~scope [] - | Some (File _ as f) -> make ~scope [ f ] - | Some (Directory { content; _ }) -> make ~scope content + | Some (Item.File _ as f) -> make ~scope [ f ] + | Some (Directory { children; _ }) -> make ~scope children ;; let fold_callback acc @@ -143,8 +165,8 @@ let fold_callback acc (* HACK: Ensure the order preservation after inserting an element. *) function - | None -> sort_items acc - | Some x -> sort_items (x :: acc) + | None -> Item.sort acc + | Some x -> Item.sort (x :: acc) ;; let update ~path:in_path callback fs = @@ -161,7 +183,7 @@ let update ~path:in_path callback fs = callback ~previous:None ~path:in_path |> fold_callback acc | item :: fs_xs, [ name ] -> (* We crossed the path. *) - if has_name ~name item + if Item.has_name ~name item then ( (* If the item has the correct name, we apply the callback. *) let new_acc = acc @ fs_xs in @@ -175,15 +197,15 @@ let update ~path:in_path callback fs = (* The file does not have the correct name; we must continue traversing. *) aux (item :: acc) fs_xs [ name ] - | ( (Directory { metadata; content; name = dirname } as cdir) :: fs_xs + | ( (Item.Directory { metadata; children; name = dirname } as cdir) :: fs_xs , name :: xs ) -> (* We arrive in a directory and the path is not complete. *) - if has_name ~name cdir + if Item.has_name ~name cdir then ( (* The item has the right name, so we can dive into the crossing. *) - let new_dir = dir ?metadata ~name:dirname (aux [] content xs) in - new_dir :: (acc @ fs_xs) |> sort_items) + let new_dir = dir ?metadata ~name:dirname (aux [] children xs) in + new_dir :: (acc @ fs_xs) |> Item.sort) else (* The name is invalid, so we continue browsing the current directory. *) @@ -191,7 +213,7 @@ let update ~path:in_path callback fs = | [], name :: path_xs -> (* We need continue to create a tree structure. *) let new_dir = dir ~name (aux [] [] path_xs) in - new_dir :: acc |> sort_items + new_dir :: acc |> Item.sort | x :: fs_xs, path -> (* Not in the right position, let's continue the iteration. *) aux (x :: acc) fs_xs path @@ -235,7 +257,7 @@ let mv ~target ~source fs = and name = Path.basename target in update ~path:target - (fun ~previous:_ ~path:_ -> Some (set_name name item)) + (fun ~previous:_ ~path:_ -> Some (Item.rename name item)) new_fs ;; @@ -246,25 +268,25 @@ let mv ~target ~source fs = let ls ?scope fs = match Option.bind scope (fun path -> fetch ~path fs) with - | None -> fs.children |> List.map name_to_string - | Some (File _ as f) -> [ name_to_string f ] - | Some (Directory { content; _ }) -> List.map name_to_string content + | None -> fs.children |> List.map Item.name_to_string + | Some (Item.File _ as f) -> [ Item.name_to_string f ] + | Some (Directory { children; _ }) -> List.map Item.name_to_string children ;; let nested_print level term = let c = String.make (level * 2) ' ' in - c ^ "└─" ^ name_to_string term + c ^ "└─" ^ Item.name_to_string term ;; let tree fs = let rec aux level acc = function | [] -> acc - | (File _ as term) :: xs -> + | (Item.File _ as term) :: xs -> let f = nested_print level term in aux level (acc ^ "\n" ^ f) xs - | (Directory { content; _ } as term) :: xs -> + | (Directory { children; _ } as term) :: xs -> let f = nested_print level term in - let a = aux (succ level) (acc ^ "\n" ^ f) content in + let a = aux (succ level) (acc ^ "\n" ^ f) children in aux level a xs in aux 0 "" fs.children @@ -290,7 +312,7 @@ module Simple = struct type 'a clock = 'a -> time type metadata = { mtime : time } type content = string - type nonrec item = (content, metadata) item + type nonrec item = (content, metadata) Item.t type nonrec t = (content, metadata) t type error = @@ -387,7 +409,7 @@ module Simple = struct match fetch ~path fs with | Some item -> item - |> metadata + |> Item.metadata |> Option.fold ~none:0.0 (* OKAY: having [0.0] as a default result seems ok. *) ~some:(fun { mtime } -> mtime) @@ -433,33 +455,33 @@ module Simple = struct let is_directory ~path fs = match fetch ~path fs with | None -> false - | Some item -> is_directory item + | Some item -> Item.is_directory item ;; let is_file ~path fs = match fetch ~path fs with | None -> false - | Some item -> is_file item + | Some item -> Item.is_file item ;; let read_dir ~path fs = match fetch ~path fs with | None -> raise_error (Read_dir (path, err_no_such_target)) | Some (File _) -> raise_error (Read_dir (path, err_is_file)) - | Some (Directory { content; _ }) -> + | Some (Directory { children; _ }) -> List.fold_left (fun map elt -> - let key = Path.(path / name elt) in + let key = Path.(path / Item.name elt) in Path.Map.add key elt map) Path.Map.empty - content + children ;; let is_empty_dir ~path fs = match fetch ~path fs with | None -> raise_error (Read_dir (path, err_no_such_target)) | Some (File _) -> raise_error (Read_dir (path, err_is_file)) - | Some (Directory { content = []; _ }) -> true + | Some (Directory { children = []; _ }) -> true | Some (Directory _) -> false ;; diff --git a/lib/tree.mli b/lib/tree.mli index c5e5bc8..fd962d5 100644 --- a/lib/tree.mli +++ b/lib/tree.mli @@ -16,12 +16,100 @@ the contents of files and the metadata associated with [files] and [directories]. *) +(** Describes a tree (a directory or file list). *) +type ('a, 'metadata) t + (** A tree is a list of items, where an item can be a file or a directory (which is a list of files). *) -type ('a, 'metadata) item -(** Describes a tree (a directory or file list). *) -type ('a, 'metadata) t +module Item : sig + (** Describes the contents of a file system, which may be files or + directories. *) + + (** {1 Representation} *) + + (** Describes an element of the file system. *) + type ('a, 'metadata) t = private + | File of + { name : string + ; content : 'a + ; metadata : 'metadata option + } + | Directory of + { name : string + ; children : ('a, 'metadata) t list + ; metadata : 'metadata option + } + + (** {1 Building items} + + The core of a tree is its items. [Tree] allows you to describe + files and directories, abstracting away the contents of the files + as well as the metadata of the directories or files. *) + + (** [dir ?metadata ~name children] creates a directory and takes a + list of children. *) + val dir + : ?metadata:'metadata + -> name:string + -> ('a, 'metadata) t list + -> ('a, 'metadata) t + + (** [file ?metadata ~name content] creates a file and takes a content. *) + val file : ?metadata:'metadata -> name:string -> 'a -> ('a, 'metadata) t + + (** {1 On Items} + + Information about items. *) + + (** [is_file item] returns [true] if the given [item] is a + file. [false] otherwise. *) + val is_file : ('a, 'metadata) t -> bool + + (** [is_directory item] returns [true] if the given [item] is a + directory. [false] otherwise. *) + val is_directory : ('a, 'metadata) t -> bool + + (** [name item] returns the name of the given [item]. *) + val name : ('a, 'metadata) t -> string + + (** Same of {!val:name} but add a trailing slash if the item is a + directory. *) + val name_to_string : ('a, 'metadata) t -> string + + (** [has_name ~name item] returns [true] if the given [item] as the + given [name]. *) + val has_name : name:string -> ('a, 'metadata) t -> bool + + (** [rename new_name item] change the name of the given [item] by + [new_name]. *) + val rename : string -> ('a, 'metadata) t -> ('a, 'metadata) t + + (** [children item] returns the children of the given [item]. If + [item] is a file, it returns an empty list. *) + val children : ('a, 'metadata) t -> ('a, 'metadata) t list + + (** [content item] returns the content of the given [item]. Since the + content of a directory is a [tree], it use a polymorphic variant + to manage the different kind of content. *) + val content + : ('a, 'metadata) t + -> [ `File of 'a | `Directory of ('a, 'metadata) t list ] + + (** [map_content f item] map [f] on every nested information of the + given [item]. *) + val map_content : ('a -> 'b) -> ('a, 'metadata) t -> ('b, 'metadata) t + + (** [metadata item] returns the metadata associated to the given + [item]. *) + val metadata : ('a, 'metadata) t -> 'metadata option + + (** [on_metadata f item] apply [f] on [item] metadata. *) + val on_metadata + : ('metadata option -> 'metadata option) + -> ('a, 'metadata) t + -> ('a, 'metadata) t +end (** {1 Building Trees} @@ -35,66 +123,29 @@ type ('a, 'metadata) t val make : ?scope_metadata:(Path.t -> 'metadata option) -> scope:Path.t - -> ('a, 'metadata) item list + -> ('a, 'metadata) Item.t list -> ('a, 'metadata) t (** [from_root] is like {!val:make} but using {!val:Path.root} as {i scope}. *) -val from_root : ('a, 'metadata) item list -> ('a, 'metadata) t +val from_root : ('a, 'metadata) Item.t list -> ('a, 'metadata) t (** [from_cwd] is like {!val:make} but using {!val:Path.cwd} as {i scope}. *) -val from_cwd : ('a, 'metadata) item list -> ('a, 'metadata) t - -(** {2 Building Items} - - The core of a tree is its items. [Tree] allows you to describe - files and directories, abstracting away the contents of the files - as well as the metadata of the directories or files. *) +val from_cwd : ('a, 'metadata) Item.t list -> ('a, 'metadata) t -(** [dir ?metadata ~name children] creates a directory and takes a - list of children. *) +(** See {!val:Item.dir} *) val dir : ?metadata:'metadata -> name:string - -> ('a, 'metadata) item list - -> ('a, 'metadata) item - -(** [file ?metadata ~name content] creates a file and takes a content. *) -val file : ?metadata:'metadata -> name:string -> 'a -> ('a, 'metadata) item - -(** {1 On items} - - Information about items. *) - -(** [is_file item] returns [true] if the given [item] is a - file. [false] otherwise. *) -val is_file : ('a, 'metadata) item -> bool - -(** [is_directory item] returns [true] if the given [item] is a - directory. [false] otherwise. *) -val is_directory : ('a, 'metadata) item -> bool - -(** [name item] returns the name of the given [item]. *) -val name : ('a, 'metadata) item -> string - -(** [children item] returns the children of the given [item]. If - [item] is a file, it returns an empty list. *) -val children : ('a, 'metadata) item -> ('a, 'metadata) item list - -(** [content item] returns the content of the given [item]. Since the - content of a directory is a [tree], it use a polymorphic variant - to manage the different kind of content. *) -val content - : ('a, 'metadata) item - -> [ `File of 'a | `Directory of ('a, 'metadata) item list ] + -> ('a, 'metadata) Item.t list + -> ('a, 'metadata) Item.t -(** [metadata item] returns the metadata associated to the given - [item]. *) -val metadata : ('a, 'metadata) item -> 'metadata option +(** See {!val:Item.file} *) +val file : ?metadata:'metadata -> name:string -> 'a -> ('a, 'metadata) Item.t (** {1 Operation on Trees} *) (** [fetch ~path fs] try to reach the [item] at the position [path]. *) -val fetch : path:Path.t -> ('a, 'metadata) t -> ('a, 'metadata) item option +val fetch : path:Path.t -> ('a, 'metadata) t -> ('a, 'metadata) Item.t option (** [prism fs scope] returns a sub-tree based on a path ([scope]).*) val prism : scope:Path.t -> ('a, 'metadata) t -> ('a, 'metadata) t @@ -104,9 +155,9 @@ val prism : scope:Path.t -> ('a, 'metadata) t -> ('a, 'metadata) t whether the file should be created or deleted. *) val update : path:Path.t - -> (previous:('a, 'metadata) item option + -> (previous:('a, 'metadata) Item.t option -> path:Path.t - -> ('a, 'metadata) item option) + -> ('a, 'metadata) Item.t option) -> ('a, 'metadata) t -> ('a, 'metadata) t @@ -115,7 +166,7 @@ val update created. *) val touch : path:Path.t - -> ?if_exists:(('a, 'metadata) item -> ('a, 'metadata) item) + -> ?if_exists:(('a, 'metadata) Item.t -> ('a, 'metadata) Item.t) -> ?metadata:'metadata -> 'a -> ('a, 'metadata) t @@ -187,7 +238,7 @@ module Simple : sig type content = string (** Items of the file system. *) - type nonrec item = (content, metadata) item + type nonrec item = (content, metadata) Item.t (** Items of the file system. *) type nonrec t = (content, metadata) t diff --git a/test/read_dir_test.ml b/test/read_dir_test.ml index a9423af..54622a3 100644 --- a/test/read_dir_test.ml +++ b/test/read_dir_test.ml @@ -34,7 +34,7 @@ let fs = let finalizer = Path.Map.iter (fun path elt -> let ps = Path.to_string path in - let es = T.name elt in + let es = T.Item.name_to_string elt in ps ^ " -> " ^ es |> print_endline) ;; diff --git a/test/simple_rewriting.ml b/test/simple_rewriting.ml new file mode 100644 index 0000000..5f5134b --- /dev/null +++ b/test/simple_rewriting.ml @@ -0,0 +1,367 @@ +(* Copyright (c) 2026, Cargocut and the Virtfs developers. + All rights reserved. + + SPDX-License-Identifier: BSD-3-Clause *) + +(* Ensure that the [Tree] (and [Item]) module is not too abstract. *) + +module Simple = struct + type time = float + type 'a clock = 'a -> time + type metadata = { mtime : time } + type content = string + type nonrec item = (content, metadata) Tree.Item.t + type nonrec t = (content, metadata) Tree.t + + type error = + | Mkdir of Path.t * string + | Stat of Path.t * string + | Write_file of Path.t * string + | Read_file of Path.t * string + | Read_dir of Path.t * string + | Remove of Path.t * string + + exception Simple_error of error + + let err_file_exists = "File exists" + let err_no_such_target = "No such file or directory" + let err_is_file = "Is a file" + let err_is_directory = "Is a directory" + let err_overriden = "Cannot be overridden" + let err_not_empty = "Directory not empty" + + let error_s path prim err reason = + prim ^ ": " ^ err ^ " '" ^ Path.to_string path ^ "': " ^ reason + ;; + + let raise_error error = raise (Simple_error error) + let const_clock x _ = x + + let mount ?(clock = const_clock 1.0) ~scope children = + Tree.make + ~scope_metadata:(fun path -> Some { mtime = clock path }) + ~scope + children + ;; + + let file ?(clock = const_clock 1.0) ~name content = + Tree.file ~metadata:{ mtime = clock (name, content) } ~name content + ;; + + let dir ?(clock = const_clock 1.0) ~name children = + Tree.dir ~metadata:{ mtime = clock name } ~name children + ;; + + let error_to_string = function + | Mkdir (p, reason) -> error_s p "mkdir" "cannot create directory" reason + | Stat (p, reason) -> error_s p "stat" "cannot statx" reason + | Write_file (p, reason) -> + error_s p "create_file" "cannot create file" reason + | Read_file (p, reason) -> error_s p "read_file" "cannot read file" reason + | Read_dir (p, reason) -> + error_s p "read_dir" "cannot read directory" reason + | Remove (p, reason) -> error_s p "rm" "cannot remove" reason + ;; + + let run ?(finalizer = fun _ -> ()) callback = + try finalizer (callback ()) with + | Simple_error err -> err |> error_to_string |> prerr_endline + ;; + + let create_dir ?(clock = const_clock 1.0) ~path fs = + let dname = Path.dirname path in + match Tree.fetch ~path:dname fs, Tree.fetch ~path fs with + | Some _, None -> + Tree.update + ~path + (fun ~previous:_ ~path -> + let bname = Path.basename path in + let item = dir ~clock ~name:bname [] in + Some item) + fs + | _, Some _ -> raise_error (Mkdir (path, err_file_exists)) + | None, _ -> raise_error (Mkdir (path, err_no_such_target)) + ;; + + let create_dir_rec ?(clock = const_clock 1.0) ~path fs = + let rec aux path fs = + let file = Tree.fetch ~path fs in + match file with + | Some (File _) -> raise_error (Mkdir (path, err_file_exists)) + | Some (Directory _) -> fs + | None -> + let p = Path.dirname path in + let fs = aux p fs in + create_dir ~clock ~path fs + in + aux path fs + ;; + + let mkdir ?(recursive = false) ?(clock = const_clock 1.0) ~path fs = + if recursive + then create_dir_rec ~clock ~path fs + else create_dir ~clock ~path fs + ;; + + let mtime ~path fs = + match Tree.fetch ~path fs with + | Some item -> + item + |> Tree.Item.metadata + |> Option.fold + ~none:0.0 (* OKAY: having [0.0] as a default result seems ok. *) + ~some:(fun { mtime } -> mtime) + | _ -> raise_error (Stat (path, err_no_such_target)) + ;; + + let write_file + ?(overwrite = false) + ?(clock = const_clock 1.0) + ~path + content + fs + = + let parent = Path.dirname path in + match Tree.fetch ~path:parent fs, Tree.fetch ~path fs with + | None, _ -> raise_error (Write_file (path, err_no_such_target)) + | Some _, Some (Directory _) -> + raise_error (Write_file (path, err_is_directory)) + | Some _, Some (File _) when not overwrite -> + raise_error (Write_file (path, err_overriden)) + | Some _, (Some _ | None) -> + Tree.update + ~path + (fun ~previous:_ ~path -> + let name = Path.basename path in + Some (file ~clock ~name content)) + fs + ;; + + let read_file ~path fs = + match Tree.fetch ~path fs with + | None -> raise_error (Read_file (path, err_no_such_target)) + | Some (Directory _) -> raise_error (Read_file (path, err_is_directory)) + | Some (File { content; _ }) -> content + ;; + + let file_exists ~path fs = + match Tree.fetch ~path fs with + | None -> false + | Some _ -> true + ;; + + let is_directory ~path fs = + match Tree.fetch ~path fs with + | None -> false + | Some item -> Tree.Item.is_directory item + ;; + + let is_file ~path fs = + match Tree.fetch ~path fs with + | None -> false + | Some item -> Tree.Item.is_file item + ;; + + let read_dir ~path fs = + match Tree.fetch ~path fs with + | None -> raise_error (Read_dir (path, err_no_such_target)) + | Some (File _) -> raise_error (Read_dir (path, err_is_file)) + | Some (Directory { children; _ }) -> + List.fold_left + (fun map elt -> + let key = Path.(path / Tree.Item.name elt) in + Path.Map.add key elt map) + Path.Map.empty + children + ;; + + let is_empty_dir ~path fs = + match Tree.fetch ~path fs with + | None -> raise_error (Read_dir (path, err_no_such_target)) + | Some (File _) -> raise_error (Read_dir (path, err_is_file)) + | Some (Directory { children = []; _ }) -> true + | Some (Directory _) -> false + ;; + + let rm_file ~path fs = + match Tree.fetch ~path fs with + | None -> raise_error (Remove (path, err_no_such_target)) + | Some (Directory _) -> raise_error (Remove (path, err_is_directory)) + | Some (File _) -> Tree.rm_file ~path fs + ;; + + let generic_rm_dir = Tree.rm_dir + + let rec rm_dir ?(recursive = false) ~path fs = + if recursive + then ( + let rec aux path fs = + if is_file ~path fs + then rm_file ~path fs + else if is_empty_dir ~path fs + then rm_dir ~path fs + else ( + let fs = + Path.Map.fold (fun path _ fs -> aux path fs) (read_dir ~path fs) fs + in + rm_dir ~path fs) + in + aux path fs) + else if is_empty_dir ~path fs + then generic_rm_dir ~path fs + else raise_error (Remove (path, err_not_empty)) + ;; + + let rm ?(recursive = false) ~path fs = + if is_file ~path fs + then rm_file ~path fs + else if is_directory ~path fs + then rm_dir ~recursive ~path fs + else raise_error (Remove (path, err_no_such_target)) + ;; +end + +module T = Tree +module U = T.Simple + +let fs = + let open U in + mount + ~scope:Path.root + [ dir + ~name:"1-foo" + [ dir ~name:"bar" [ file ~name:"index.md" "Hello World" ] ] + ; dir + ~name:"2-foo" + [ dir + ~name:"bar" + [ dir ~name:"baz" [ file ~name:"index.md" "Hello World 2" ] ] + ] + ; dir ~name:"3-foo" [ dir ~name:"bar" [] ] + ] +;; + +let finalizer (fs, tm, ct) = + print_endline ("mtime: " ^ Float.to_string tm); + print_endline ct; + fs |> T.tree |> print_endline +;; + +let%expect_test "writing on a directory" = + let clock _ = 5.0 + and path = Path.abs [ "1-foo"; "bar" ] in + U.run + (fun () -> + let fs = fs |> U.write_file ~clock ~path "My new content" in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect {| create_file: cannot create file '/1-foo/bar': Is a directory |}] +;; + +let%expect_test "writing a file in an invalid path" = + let clock _ = 5.0 + and path = Path.abs [ "4-foo"; "index.md" ] in + U.run + (fun () -> + let fs = fs |> U.write_file ~clock ~path "My new content" in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect + {| create_file: cannot create file '/4-foo/index.md': No such file or directory |}] +;; + +let%expect_test "writing on an existing file (without overwrite flag)" = + let clock _ = 5.0 + and path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run + (fun () -> + let fs = fs |> U.write_file ~clock ~path "My new content" in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect + {| create_file: cannot create file '/1-foo/bar/index.md': Cannot be overridden |}] +;; + +let%expect_test "writing on an existing file (with overwrite flag)" = + let clock _ = 5.0 + and path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run + (fun () -> + let fs = + fs |> U.write_file ~overwrite:true ~clock ~path "My new content" + in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect + {| + mtime: 5. + My new content + + └─/ + └─1-foo/ + └─bar/ + └─index.md + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + |}] +;; + +let%expect_test "writing on a new file" = + let clock _ = 5.0 + and path = Path.abs [ "1-foo"; "bar"; "index-2.md" ] in + U.run + (fun () -> + let fs = fs |> U.write_file ~clock ~path "My new content" in + let tm = U.mtime ~path fs + and ct = U.read_file ~path fs in + fs, tm, ct) + ~finalizer; + [%expect + {| + mtime: 5. + My new content + + └─/ + └─1-foo/ + └─bar/ + └─index-2.md + └─index.md + └─2-foo/ + └─bar/ + └─baz/ + └─index.md + └─3-foo/ + └─bar/ + |}] +;; + +let%expect_test "read_file on a directory" = + let path = Path.abs [ "1-foo"; "bar" ] in + U.run (fun () -> U.read_file ~path fs) ~finalizer:print_endline; + [%expect {| read_file: cannot read file '/1-foo/bar': Is a directory |}] +;; + +let%expect_test "read_file when not exists" = + let path = Path.abs [ "1-foo"; "bar.md" ] in + U.run (fun () -> U.read_file ~path fs) ~finalizer:print_endline; + [%expect + {| read_file: cannot read file '/1-foo/bar.md': No such file or directory |}] +;; + +let%expect_test "read_file" = + let path = Path.abs [ "1-foo"; "bar"; "index.md" ] in + U.run (fun () -> U.read_file ~path fs) ~finalizer:print_endline; + [%expect {| Hello World |}] +;; diff --git a/test/tree_core_test.ml b/test/tree_core_test.ml index 62fc324..bc593f5 100644 --- a/test/tree_core_test.ml +++ b/test/tree_core_test.ml @@ -7,8 +7,8 @@ let fetch fs path = match Tree.fetch fs ~path with | None -> print_endline (Path.to_filename path ^ ": Not found") | Some item -> - let name = Tree.name item in - (match Tree.content item with + let name = Tree.Item.name item in + (match Tree.Item.content item with | `File s -> name ^ ": " ^ s | `Directory xs -> Tree.tree (Tree.make ~scope:path xs)) |> print_endline