Skip to content

Commit 0911e3e

Browse files
authored
Merge pull request #111 from xhtmlboi/add-batch-tree
Add batch tree
2 parents 790cac3 + d1c50c2 commit 0911e3e

File tree

7 files changed

+344
-3
lines changed

7 files changed

+344
-3
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ jobs:
1313
matrix:
1414
os:
1515
- ubuntu-latest
16+
- macos-latest
1617
ocaml-compiler:
17-
- ocaml-base-compiler.5.3.0
18-
- 5.2.x
19-
- 5.1.x
18+
- 5
2019
runs-on: ${{ matrix.os }}
2120

2221
steps:

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Add `Action.remove_residuals` for erasing residuals files (by [xvw](https://xvw.lol))
66
- Add `Yocaml.Data.Validation.sub_record` for validating a complete structure as a record field (by [xvw](https://xvw.lol))
7+
- Add `Batch.iter_tree` and `Batch.fold_tree` (by [gr-im](https://github.com/gr-im))
78

89
#### Yocaml_git
910

lib/core/batch.ml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,27 @@ let fold_files ?where ~state = fold_children ~only:`Files ?where ~state
2424

2525
let fold_directories ?where ~state =
2626
fold_children ~only:`Directories ?where ~state
27+
28+
let fold_tree ?(where = fun _ _ -> true) ~state path action =
29+
let rec aux state path =
30+
fold_children ~only:`Both ~state path (fun path state cache ->
31+
let open Eff in
32+
let* is_file = is_file ~on:`Source path in
33+
if is_file && where `File path then action path state cache
34+
else
35+
let* is_dir = is_directory ~on:`Source path in
36+
if is_dir && where `Directory path then aux state path cache
37+
else return (cache, state))
38+
in
39+
aux state path
40+
41+
let iter_tree ?where path action cache =
42+
let open Eff in
43+
let+ cache, () =
44+
fold_tree ?where ~state:() path
45+
(fun path () cache ->
46+
let+ cache = action path cache in
47+
(cache, ()))
48+
cache
49+
in
50+
cache

lib/core/batch.mli

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,26 @@ val iter_files :
5959
?where:(Path.t -> bool) -> Path.t -> (Path.t -> Action.t) -> Action.t
6060
(** [iter_files] is [iter_children ~only:`Files]. *)
6161

62+
val iter_tree :
63+
?where:([ `Directory | `File ] -> Path.t -> bool)
64+
-> Path.t
65+
-> (Path.t -> Action.t)
66+
-> Action.t
67+
(** [iter_tree ?where path action] will apply the [action] passed as an argument
68+
to all files, recursively in the directory tree located at [path]. You can
69+
use the second parameter of the [where] predicate to distinguish whether you
70+
are observing a file or a directory. *)
71+
72+
val fold_tree :
73+
?where:([ `Directory | `File ] -> Path.t -> bool)
74+
-> state:'a
75+
-> Path.t
76+
-> (Path.t -> 'a -> Cache.t -> (Cache.t * 'a) Eff.t)
77+
-> Cache.t
78+
-> (Cache.t * 'a) Eff.t
79+
(** [fold_tree ?where ~state path action] is like {!val:iter_children} but you
80+
can maintain your own additional state. *)
81+
6282
val fold_files :
6383
?where:(Path.t -> bool)
6484
-> state:'a

test/yocaml/batch_test.ml

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
(* YOCaml a static blog generator.
2+
Copyright (C) 2025 The Funkyworkers and The YOCaml's developers
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>. *)
16+
17+
open Test_lib
18+
module Batch = Yocaml.Batch
19+
module Path = Yocaml.Path
20+
21+
let test_iter_tree_1 =
22+
let open Alcotest in
23+
test_case "iter_tree - without nesting" `Quick (fun () ->
24+
let base_fs =
25+
let open Fs in
26+
from_list
27+
[ dir "." [ dir "content" [ file "a.md" "a"; file "b.md" "b" ] ] ]
28+
in
29+
let trace = Fs.create_trace ~time:10 base_fs in
30+
let program () =
31+
let open Yocaml.Eff in
32+
return Yocaml.Cache.empty
33+
>>= Fs.increase_time_with 1
34+
>>= Batch.iter_tree (Path.rel [ "content" ]) (fun p ->
35+
let into =
36+
p
37+
|> Path.dirname
38+
|> Path.trim ~prefix:(Path.rel [ "content" ])
39+
|> Path.relocate ~into:(Path.rel [ "_www" ])
40+
in
41+
Yocaml.Action.copy_file ~into p)
42+
in
43+
let trace, _cache = Fs.run ~trace program () in
44+
let computed_file_system = Fs.trace_system trace in
45+
let expected_file_system =
46+
let open Fs in
47+
from_list
48+
[
49+
dir "."
50+
[
51+
dir "content" [ file "a.md" "a"; file "b.md" "b" ]
52+
; dir "_www"
53+
[ file ~mtime:11 "a.md" "a"; file ~mtime:11 "b.md" "b" ]
54+
]
55+
]
56+
in
57+
check Testable.fs "should be equal" expected_file_system
58+
computed_file_system)
59+
60+
let test_iter_tree_2 =
61+
let open Alcotest in
62+
test_case "iter_tree - with nesting" `Quick (fun () ->
63+
let base_fs =
64+
let open Fs in
65+
from_list
66+
[
67+
dir "."
68+
[
69+
dir "content"
70+
[
71+
file "a.md" "a"
72+
; file "b.md" "b"
73+
; dir "foo"
74+
[
75+
file "c.md" "c"
76+
; file "d.md" "d"
77+
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
78+
; dir "foobar" [ file "g.txt" "g" ]
79+
]
80+
]
81+
]
82+
]
83+
in
84+
let trace = Fs.create_trace ~time:10 base_fs in
85+
let program () =
86+
let open Yocaml.Eff in
87+
return Yocaml.Cache.empty
88+
>>= Fs.increase_time_with 1
89+
>>= Batch.iter_tree (Path.rel [ "content" ]) (fun p ->
90+
let into =
91+
p
92+
|> Path.dirname
93+
|> Path.trim ~prefix:(Path.rel [ "content" ])
94+
|> Path.relocate ~into:(Path.rel [ "_www" ])
95+
in
96+
Yocaml.Action.copy_file ~into p)
97+
in
98+
let trace, _cache = Fs.run ~trace program () in
99+
let computed_file_system = Fs.trace_system trace in
100+
let expected_file_system =
101+
let open Fs in
102+
from_list
103+
[
104+
dir "."
105+
[
106+
dir "content"
107+
[
108+
file "a.md" "a"
109+
; file "b.md" "b"
110+
; dir "foo"
111+
[
112+
file "c.md" "c"
113+
; file "d.md" "d"
114+
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
115+
; dir "foobar" [ file "g.txt" "g" ]
116+
]
117+
]
118+
; dir "_www"
119+
[
120+
file ~mtime:11 "a.md" "a"
121+
; file ~mtime:11 "b.md" "b"
122+
; dir ~mtime:11 "foo"
123+
[
124+
file ~mtime:11 "c.md" "c"
125+
; file ~mtime:11 "d.md" "d"
126+
; dir "bar"
127+
[
128+
file ~mtime:11 "e.md" "e"; file ~mtime:11 "f.md" "f"
129+
]
130+
; dir "foobar" [ file ~mtime:11 "g.txt" "g" ]
131+
]
132+
]
133+
]
134+
]
135+
in
136+
check Testable.fs "should be equal" expected_file_system
137+
computed_file_system)
138+
139+
let test_fold_tree_1 =
140+
let open Alcotest in
141+
test_case "fold_tree - with nesting" `Quick (fun () ->
142+
let base_fs =
143+
let open Fs in
144+
from_list
145+
[
146+
dir "."
147+
[
148+
dir "content"
149+
[
150+
file "a.md" "a"
151+
; file "b.md" "b"
152+
; dir "foo"
153+
[
154+
file "c.md" "c"
155+
; file "d.md" "d"
156+
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
157+
; dir "foobar" [ file "g.txt" "g" ]
158+
]
159+
]
160+
]
161+
]
162+
in
163+
let trace = Fs.create_trace ~time:10 base_fs in
164+
let program () =
165+
let open Yocaml.Eff in
166+
return Yocaml.Cache.empty
167+
>>= Fs.increase_time_with 1
168+
>>= Batch.fold_tree ~state:"" (Path.rel [ "content" ])
169+
(fun p state cache ->
170+
let+ ctn = read_file ~on:`Source p in
171+
(cache, state ^ ctn))
172+
>>= fun (c, s) ->
173+
Yocaml.Action.write_static_file (Path.rel [ "OUT" ])
174+
(Yocaml.Task.const s) c
175+
in
176+
let trace, _cache = Fs.run ~trace program () in
177+
let computed_file_system = Fs.trace_system trace in
178+
let expected_file_system =
179+
let open Fs in
180+
from_list
181+
[
182+
dir "."
183+
[
184+
dir "content"
185+
[
186+
file "a.md" "a"
187+
; file "b.md" "b"
188+
; dir "foo"
189+
[
190+
file "c.md" "c"
191+
; file "d.md" "d"
192+
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
193+
; dir "foobar" [ file "g.txt" "g" ]
194+
]
195+
]
196+
; file ~mtime:11 "OUT" "abcdefg"
197+
]
198+
]
199+
in
200+
check Testable.fs "should be equal" expected_file_system
201+
computed_file_system)
202+
203+
let test_fold_tree_2 =
204+
let open Alcotest in
205+
test_case "fold_tree - with filtering" `Quick (fun () ->
206+
let base_fs =
207+
let open Fs in
208+
from_list
209+
[
210+
dir "."
211+
[
212+
dir "content"
213+
[
214+
file "a.md" "a"
215+
; file "b.md" "b"
216+
; dir "foo"
217+
[
218+
file "c.md" "c"
219+
; file "d.md" "d"
220+
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
221+
; dir "foobar" [ file "g.txt" "g" ]
222+
]
223+
]
224+
]
225+
]
226+
in
227+
let reject name p =
228+
match Path.basename p with
229+
| None -> true
230+
| Some x -> not (String.equal x name)
231+
in
232+
let trace = Fs.create_trace ~time:10 base_fs in
233+
let program () =
234+
let open Yocaml.Eff in
235+
return Yocaml.Cache.empty
236+
>>= Fs.increase_time_with 1
237+
>>= Batch.fold_tree
238+
~where:(function
239+
| `Directory -> reject "foobar" | `File -> reject "d.md")
240+
~state:"" (Path.rel [ "content" ])
241+
(fun p state cache ->
242+
let+ ctn = read_file ~on:`Source p in
243+
(cache, state ^ ctn))
244+
>>= fun (c, s) ->
245+
Yocaml.Action.write_static_file (Path.rel [ "OUT" ])
246+
(Yocaml.Task.const s) c
247+
in
248+
let trace, _cache = Fs.run ~trace program () in
249+
let computed_file_system = Fs.trace_system trace in
250+
let expected_file_system =
251+
let open Fs in
252+
from_list
253+
[
254+
dir "."
255+
[
256+
dir "content"
257+
[
258+
file "a.md" "a"
259+
; file "b.md" "b"
260+
; dir "foo"
261+
[
262+
file "c.md" "c"
263+
; file "d.md" "d"
264+
; dir "bar" [ file "e.md" "e"; file "f.md" "f" ]
265+
; dir "foobar" [ file "g.txt" "g" ]
266+
]
267+
]
268+
; file ~mtime:11 "OUT" "abcef"
269+
]
270+
]
271+
in
272+
check Testable.fs "should be equal" expected_file_system
273+
computed_file_system)
274+
275+
let cases =
276+
( "Yocaml.Batch"
277+
, [ test_iter_tree_1; test_iter_tree_2; test_fold_tree_1; test_fold_tree_2 ]
278+
)

test/yocaml/batch_test.mli

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
(* YOCaml a static blog generator.
2+
Copyright (C) 2025 The Funkyworkers and The YOCaml's developers
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>. *)
16+
17+
val cases : string * unit Alcotest.test_case list
18+
(** Returns the list of test cases. *)

test/yocaml/yocaml_test.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ let () =
3030
; Exec_command.cases
3131
; Conditional_test.cases
3232
; Trace_test.cases
33+
; Batch_test.cases
3334
]

0 commit comments

Comments
 (0)