package yocaml

  1. Overview
  2. Docs

Source file eff.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
(* YOCaml a static blog generator.
   Copyright (C) 2024 The Funkyworkers and The YOCaml's developers

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>. *)

type 'a t = unit -> 'a

let return x () = x
let bind f x = f (x ())
let map f x = bind (fun m -> return @@ f m) x
let join x = bind (fun x -> x) x
let compose f g x = bind g (f x)
let rcompose f g x = bind f (g x)
let apply ft xt = map (ft ()) xt
let zip x y = apply (map (fun a b -> (a, b)) x) y
let replace x e = map (fun _ -> x) e
let void e = replace () e

let select x y =
  bind
    (function
      | Either.Right v -> return v | Either.Left v -> map (fun f -> f v) y)
    x

let branch s l r =
  let a = map Either.(map_right left) s
  and b = map (fun f x -> Either.right (f x)) l in
  select (select a b) r

let map2 fu a b = apply (map fu a) b
let map3 fu a b c = apply (map2 fu a b) c
let map4 fu a b c d = apply (map3 fu a b c) d
let map5 fu a b c d e = apply (map4 fu a b c d) e
let map6 fu a b c d e f = apply (map5 fu a b c d e) f
let map7 fu a b c d e f g = apply (map6 fu a b c d e f) g
let map8 fu a b c d e f g h = apply (map7 fu a b c d e f g) h

module List = struct
  let traverse f l =
    let rec aux acc = function
      | [] -> map Stdlib.List.rev acc
      | x :: xs -> (aux [@tailcall]) (map2 Stdlib.List.cons (f x) acc) xs
    in
    aux (return []) l

  let sequence l = traverse Fun.id l

  let filter_map f l =
    let rec aux acc = function
      | [] -> return @@ Stdlib.List.rev acc
      | x :: xs ->
          bind (function
            | None -> (aux [@tailcall]) acc xs
            | Some x -> (aux [@tailcall]) (x :: acc) xs)
          @@ f x
    in
    aux [] l

  let fold_left f default list =
    let rec aux acc = function
      | [] -> acc
      | x :: xs -> (aux [@tailcall]) (bind (fun m -> f acc m) x) xs
    in
    aux default list
end

module Infix = struct
  let ( <$> ) = map
  let ( <*> ) = apply
  let ( <*? ) = select
  let ( =<< ) = bind
  let ( >>= ) x f = f =<< x
  let ( >|= ) x f = f <$> x
  let ( =|< ) = map
  let ( >=> ) = compose
  let ( <=< ) = rcompose
end

module Syntax = struct
  let ( let+ ) x f = map f x
  let ( and+ ) = zip
  let ( let* ) x f = bind f x
end

include Infix
include Syntax

type filesystem = [ `Source | `Target ]

type _ Effect.t +=
  | Yocaml_log :
      ([ `App | `Error | `Warning | `Info | `Debug ] * string)
      -> unit Effect.t
  | Yocaml_failwith : exn -> 'a Effect.t
  | Yocaml_get_time : unit -> int Effect.t
  | Yocaml_file_exists : filesystem * Path.t -> bool Effect.t
  | Yocaml_read_file : filesystem * Path.t -> string Effect.t
  | Yocaml_get_mtime : filesystem * Path.t -> int Effect.t
  | Yocaml_hash_content : string -> string Effect.t
  | Yocaml_write_file : filesystem * Path.t * string -> unit Effect.t
  | Yocaml_is_directory : filesystem * Path.t -> bool Effect.t
  | Yocaml_read_dir : filesystem * Path.t -> Path.fragment list Effect.t
  | Yocaml_create_dir : filesystem * Path.t -> unit Effect.t
  | Yocaml_exec_command :
      string * string list * (int -> bool)
      -> string Effect.t

let perform raw_effect = return @@ Effect.perform raw_effect

let run handler arrow input =
  Effect.Deep.match_with (fun input -> arrow input ()) input handler

exception File_not_exists of filesystem * Path.t
exception Invalid_path of filesystem * Path.t
exception File_is_a_directory of filesystem * Path.t
exception Directory_is_a_file of filesystem * Path.t
exception Directory_not_exists of filesystem * Path.t
exception Provider_error of Required.provider_error

let log ?(level = `Debug) message = perform @@ Yocaml_log (level, message)
let raise exn = perform @@ Yocaml_failwith exn
let failwith message = perform @@ Yocaml_failwith (Failure message)
let get_time () = perform @@ Yocaml_get_time ()
let file_exists ~on path = perform @@ Yocaml_file_exists (on, path)
let logf ?(level = `Debug) = Format.kasprintf (fun result -> log ~level result)
let is_directory ~on path = perform @@ Yocaml_is_directory (on, path)

let exec ?(is_success = Int.equal 0) exec_name ?(args = []) =
  perform @@ Yocaml_exec_command (exec_name, args, is_success)

let exec_cmd ?is_success cmd =
  let command, args = Cmd.normalize cmd in
  exec ?is_success ~args command

let is_file ~on path =
  let* file_exists = file_exists ~on path in
  if file_exists then
    let+ is_dir = is_directory ~on path in
    not is_dir
  else return false

let ensure_file_exists ~on f path =
  let* exists = file_exists ~on path in
  if exists then f path else raise (File_not_exists (on, path))

let read_file ~on =
  ensure_file_exists ~on (fun path ->
      let* is_file = is_file ~on path in
      if is_file then perform @@ Yocaml_read_file (on, path)
      else raise @@ File_is_a_directory (on, path))

let read_file_as_metadata (type a) (module P : Required.DATA_PROVIDER)
    (module R : Required.DATA_READABLE with type t = a) ~on path =
  let* file = read_file ~on path in
  file
  |> Option.some
  |> Metadata.validate (module P) (module R)
  |> Result.fold
       ~error:(fun err -> raise @@ Provider_error err)
       ~ok:(fun metadata -> return metadata)

let read_file_with_metadata (type a) (module P : Required.DATA_PROVIDER)
    (module R : Required.DATA_READABLE with type t = a)
    ?(extraction_strategy = Metadata.jekyll) ~on path =
  let* file = read_file ~on path in
  let raw_metadata, content =
    Metadata.extract_from_content ~strategy:extraction_strategy file
  in
  raw_metadata
  |> Metadata.validate (module P) (module R)
  |> Result.fold
       ~error:(fun err -> raise @@ Provider_error err)
       ~ok:(fun metadata -> return (metadata, content))

let get_mtime ~on =
  ensure_file_exists ~on (fun path -> perform @@ Yocaml_get_mtime (on, path))

let hash str = perform @@ Yocaml_hash_content str

let create_directory ~on path =
  let rec aux path =
    let* is_file = is_file ~on path in
    if is_file then raise (Directory_is_a_file (on, path))
    else
      let* is_directory = is_directory ~on path in
      if not is_directory then
        let parent = Path.dirname path in
        let* () = aux parent in
        perform @@ Yocaml_create_dir (on, path)
      else return ()
  in
  aux path

let write_file ~on path content =
  let parent = Path.dirname path in
  let* () = create_directory ~on parent in
  perform @@ Yocaml_write_file (on, path, content)

let read_directory ~on ?(only = `Both) ?(where = fun _ -> true) path =
  let* is_dir = is_directory ~on path in
  if is_dir then
    let predicate child =
      let file = Path.(path / child) in
      let* exists = file_exists ~on file in
      let+ is_dir = is_directory ~on file in
      let predicate =
        match only with
        | `Files -> exists && (not is_dir) && where file
        | `Directories -> exists && is_dir && where file
        | `Both -> exists && where file
      in
      if predicate then Some file else None
    in
    let* children = perform @@ Yocaml_read_dir (on, path) in
    List.filter_map predicate children
  else
    let+ () = logf ~level:`Warning "%a does not exists" Path.pp path in
    []

let mtime ~on path =
  let rec aux path =
    let* t = get_mtime ~on path in
    let* d = is_directory path ~on in
    if d then
      let* children = read_directory ~on ~only:`Both path in
      Stdlib.List.fold_left
        (fun max_time f ->
          let* a = max_time in
          let+ b = aux f in
          Int.max a b)
        (return t) children
    else return t
  in
  aux path

let get_basename source =
  match Path.basename source with
  | None -> raise (Invalid_path (`Source, source))
  | Some fragment -> return fragment

let copy_file into source =
  let* fragment = get_basename source in
  let dest = Path.(into / fragment) in
  let* content = read_file ~on:`Source source in
  write_file ~on:`Target dest content

let copy_recursive ?new_name ~into source =
  let rec aux ?new_name into source =
    let* is_dir = is_directory ~on:`Target into in
    if is_dir then
      let* source_is_file = is_file ~on:`Source source in
      if source_is_file then
        let* () =
          log ~level:`Debug @@ Lexicon.copy_file ?new_name ~into source
        in
        copy_file into source
      else
        let* source_is_directory = is_directory ~on:`Source source in
        if source_is_directory then
          let* () =
            log ~level:`Debug @@ Lexicon.copy_directory ?new_name ~into source
          in
          let* name = get_basename source in
          let name = Option.value new_name ~default:name in
          let name = Path.(into / name) in
          let* () = create_directory ~on:`Target name in
          let* children = read_directory ~on:`Source ~only:`Both source in
          let* _ = List.traverse (fun child -> aux name child) children in
          return ()
        else raise (File_not_exists (`Source, source))
    else
      let* is_file = is_file ~on:`Target into in
      if is_file then raise (Directory_is_a_file (`Target, into))
      else
        let* () = create_directory ~on:`Target into in
        aux ?new_name into source
  in
  aux ?new_name into source
OCaml

Innovation. Community. Security.