Source file archetype.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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
let alt_opt a b = match a with Some a -> Some a | None -> b
let is_empty_list = function [] -> true | _ -> false
module Datetime = struct
type month =
| Jan
| Feb
| Mar
| Apr
| May
| Jun
| Jul
| Aug
| Sep
| Oct
| Nov
| Dec
type day_of_week = Mon | Tue | Wed | Thu | Fri | Sat | Sun
type year = int
type day = int
type hour = int
type min = int
type sec = int
type t = {
year : year
; month : month
; day : day
; hour : hour
; min : min
; sec : sec
}
let invalid_int x message =
Data.Validation.fail_with ~given:(string_of_int x) message
let month_from_int x =
if x > 0 && x <= 12 then
Result.ok
[| Jan; Feb; Mar; Apr; May; Jun; Jul; Aug; Sep; Oct; Nov; Dec |].(x - 1)
else invalid_int x "Invalid month value"
let year_from_int x =
if x >= 0 then Result.ok x else invalid_int x "Invalid year value"
let is_leap year =
if year mod 100 = 0 then year mod 400 = 0 else year mod 4 = 0
let days_in_month year month =
match month with
| Jan | Mar | May | Jul | Aug | Oct | Dec -> 31
| Feb -> if is_leap year then 29 else 28
| _ -> 30
let day_from_int year month x =
let dim = days_in_month year month in
if x >= 1 && x <= dim then Result.ok x
else invalid_int x "Invalid day value"
let hour_from_int x =
if x >= 0 && x < 24 then Result.ok x else invalid_int x "Invalid hour value"
let min_from_int x =
if x >= 0 && x < 60 then Result.ok x else invalid_int x "Invalid min value"
let sec_from_int x =
if x >= 0 && x < 60 then Result.ok x else invalid_int x "Invalid sec value"
let make_raw ?(time = (0, 0, 0)) ~year ~month ~day () =
let hour, min, sec = time in
{ year; month; day; hour; min; sec }
let dummy = make_raw ~time:(0, 0, 0) ~year:1970 ~month:Jan ~day:1 ()
let ( let* ) = Result.bind
let make ?(time = (0, 0, 0)) ~year ~month ~day () =
let hour, min, sec = time in
let* year = year_from_int year in
let* month = month_from_int month in
let* day = day_from_int year month day in
let* hour = hour_from_int hour in
let* min = min_from_int min in
let* sec = sec_from_int sec in
Result.ok { year; month; day; hour; min; sec }
let validate_from_datetime_str str =
let str = String.trim str in
match
Scanf.sscanf_opt str "%04d%c%02d%c%02d%c%02d%c%02d%c%02d"
(fun year _ month _ day _ hour _ min _ sec ->
((hour, min, sec), year, month, day))
with
| None -> Data.Validation.fail_with ~given:str "Invalid date format"
| Some (time, year, month, day) -> make ~time ~year ~month ~day ()
let validate_from_date_str str =
let str = String.trim str in
match
Scanf.sscanf_opt str "%04d%c%02d%c%02d" (fun year _ month _ day ->
(year, month, day))
with
| None -> Data.Validation.fail_with ~given:str "Invalid date format"
| Some (year, month, day) -> make ~year ~month ~day ()
let validate =
let open Data.Validation in
string & (validate_from_datetime_str / validate_from_date_str)
let month_to_int = function
| Jan -> 1
| Feb -> 2
| Mar -> 3
| Apr -> 4
| May -> 5
| Jun -> 6
| Jul -> 7
| Aug -> 8
| Sep -> 9
| Oct -> 10
| Nov -> 11
| Dec -> 12
let month_to_string = function
| Jan -> "jan"
| Feb -> "feb"
| Mar -> "mar"
| Apr -> "apr"
| May -> "may"
| Jun -> "jun"
| Jul -> "jul"
| Aug -> "aug"
| Sep -> "sep"
| Oct -> "oct"
| Nov -> "nov"
| Dec -> "dec"
let dow_to_int = function
| Mon -> 0
| Tue -> 1
| Wed -> 2
| Thu -> 3
| Fri -> 4
| Sat -> 5
| Sun -> 6
let dow_to_string = function
| Mon -> "mon"
| Tue -> "tue"
| Wed -> "wed"
| Thu -> "thu"
| Fri -> "fri"
| Sat -> "sat"
| Sun -> "sun"
let compare_date a b =
let cmp = Int.compare a.year b.year in
if Int.equal cmp 0 then
let cmp = Int.compare (month_to_int a.month) (month_to_int b.month) in
if Int.equal cmp 0 then Int.compare a.day b.day else cmp
else cmp
let compare_time a b =
let cmp = Int.compare a.hour b.hour in
if Int.equal cmp 0 then
let cmp = Int.compare a.min b.min in
if Int.equal cmp 0 then Int.compare a.sec b.sec else cmp
else cmp
let compare a b =
let cmp = compare_date a b in
if Int.equal cmp 0 then compare_time a b else cmp
let equal a b = Int.equal 0 (compare a b)
let pp_date ppf { year; month; day; _ } =
Format.fprintf ppf "%04d-%02d-%02d" year (month_to_int month) day
let pp_time ppf { hour; min; sec; _ } =
Format.fprintf ppf "%02d:%02d:%02d" hour min sec
let pp ppf dt = Format.fprintf ppf "%a %a" pp_date dt pp_time dt
let month_value = function
| Jan -> 0
| Feb -> 3
| Mar -> 3
| Apr -> 6
| May -> 1
| Jun -> 4
| Jul -> 6
| Aug -> 2
| Sep -> 5
| Oct -> 0
| Nov -> 3
| Dec -> 5
let day_of_week { year; month; day; _ } =
let yy = year mod 100 in
let cc = (year - yy) / 100 in
let c_code = [| 6; 4; 2; 0 |].(cc mod 4) in
let y_code = (yy + (yy / 4)) mod 7 in
let m_code =
let v = month_value month in
if is_leap year && (month = Jan || month = Feb) then v - 1 else v
in
let index = (c_code + y_code + m_code + day) mod 7 in
[| Sun; Mon; Tue; Wed; Thu; Fri; Sat |].(index)
let pp_rfc822 ?(tz = "gmt") () ppf dt =
let tz = String.uppercase_ascii tz in
let dow = dt |> day_of_week |> dow_to_string |> String.capitalize_ascii in
let mon = dt.month |> month_to_string |> String.capitalize_ascii in
Format.fprintf ppf "%s, %02d %s %04d %a %s" dow dt.day mon dt.year pp_time
dt tz
let pp_rfc3339 ?(tz = "Z") () ppf dt =
let mon = dt.month |> month_to_int in
Format.fprintf ppf "%04d-%02d-%02dT%02d:%02d:%02d%s" dt.year mon dt.day
dt.hour dt.min dt.sec tz
let normalize ({ year; month; day; hour; min; sec } as dt) =
let has_time = not (Int.equal (compare_time dt dummy) 0) in
let datetime_repr = Format.asprintf "%a" pp dt in
let date_repr = Format.asprintf "%a" pp_date dt in
let time_repr = Format.asprintf "%a" pp_time dt in
let day_of_week = day_of_week dt in
let open Data in
record
[
("year", int year)
; ("month", int (month_to_int month))
; ("day", int day)
; ("hour", int hour)
; ("min", int min)
; ("sec", int sec)
; ("has_time", bool has_time)
; ("day_of_week", int (dow_to_int day_of_week))
; ( "repr"
, record
[
("month", string (month_to_string month))
; ("datetime", string datetime_repr)
; ("date", string date_repr)
; ("time", string time_repr)
; ("day_of_week", string (dow_to_string day_of_week))
] )
]
module Infix = struct
let ( = ) = equal
let ( <> ) x y = not (equal x y)
let ( > ) x y = compare x y > 0
let ( >= ) x y = compare x y >= 0
let ( < ) x y = compare x y < 0
let ( <= ) x y = compare x y <= 0
end
let min a b = if Infix.(b > a) then a else b
let max a b = if Infix.(a < b) then a else b
include Infix
end
module Page = struct
let entity_name = "Page"
class type t = object ('a)
method page_title : string option
method page_charset : string option
method description : string option
method tags : string list
method display_toc : bool
method toc : string option
method with_toc : string option -> 'a
end
class page ?title ?description ?charset ?(tags = []) ?(display_toc = false)
?toc () =
object (_ : #t)
val toc_value = toc
method page_title = title
method page_charset = charset
method description = description
method tags = tags
method display_toc = display_toc
method toc = toc_value
method with_toc toc_value = {<toc_value>}
end
let title p = p#page_title
let charset p = p#page_charset
let description p = p#description
let tags p = p#tags
let neutral = Result.ok @@ new page ()
let validate_page fields =
let open Data.Validation in
let+ title = optional fields "page_title" string
and+ description = optional fields "description" string
and+ charset = optional fields "page_charset" string
and+ tags = optional_or fields ~default:[] "tags" (list_of string)
and+ display_toc = optional fields "display_toc" bool in
new page ?title ?description ?charset ~tags ?display_toc ()
let with_toc m toc = m#with_toc toc
let validate =
let open Data.Validation in
record validate_page
let to_meta name = function
| None -> []
| Some x ->
[ Data.(record [ ("name", string name); ("content", string x) ]) ]
let to_meta_kwd = function
| [] -> []
| tags ->
[
Data.(
record
[
("name", string "keywords")
; ("content", string @@ String.concat ", " tags)
])
]
let meta_list p = to_meta "description" p#description @ to_meta_kwd p#tags
let has_toc obj = obj#display_toc && Option.is_some obj#toc
let normalize_parameters obj =
Data.
[
("page_title", option string obj#page_title)
; ("description", option string obj#description)
; ("page_charset", option string obj#page_charset)
; ("tags", list_of string obj#tags)
; ("has_tags", bool (not (is_empty_list obj#tags)))
; ("has_page_title", bool @@ Option.is_some obj#page_title)
; ("has_description", bool @@ Option.is_some obj#description)
; ("has_page_charset", bool @@ Option.is_some obj#page_charset)
; ("has_toc", bool @@ has_toc obj)
; ("toc", option string obj#toc)
]
let normalize_meta obj = Data.[ ("meta", list @@ meta_list obj) ]
let normalize obj = normalize_parameters obj @ normalize_meta obj
end
module Article = struct
let entity_name = "Article"
class type t = object
inherit Page.t
method title : string
method synopsis : string option
method date : Datetime.t
end
class article page ?synopsis ~title ~date () =
let page_title = alt_opt page#page_title (Some title) in
let description = alt_opt page#description synopsis in
object (_ : #t)
inherit
Page.page
?title:page_title ?description ?charset:page#page_charset
~tags:page#tags ~display_toc:page#display_toc ?toc:page#toc ()
method title = title
method synopsis = synopsis
method date = date
end
let page a = (a :> Page.t)
let title a = a#title
let synopsis a = a#synopsis
let date a = a#date
let with_toc a = a#with_toc
let neutral =
Data.Validation.fail_with ~given:"null" "Cannot be null"
|> Result.map_error (fun error ->
Required.Validation_error { entity = entity_name; error })
let validate =
let open Data.Validation in
record (fun fields ->
let+ page = Page.validate_page fields
and+ title = required fields "title" string
and+ synopsis = optional fields "synopsis" string
and+ date = required fields "date" Datetime.validate in
new article page ?synopsis ~title ~date ())
let normalize obj =
Page.normalize obj
@ Data.
[
("title", string obj#title)
; ("synopsis", option string obj#synopsis)
; ("date", Datetime.normalize obj#date)
; ("has_synopsis", bool @@ Option.is_some obj#synopsis)
]
end
module Articles = struct
class type t = object
inherit Page.t
method articles : (Path.t * Article.t) list
end
class articles page articles =
object (_ : #t)
inherit
Page.page
?title:page#page_title ?description:page#description
?charset:page#page_charset ~tags:page#tags ?toc:page#toc
~display_toc:page#display_toc ()
method articles = articles
end
let with_toc a = a#with_toc
let from_page = Task.lift (fun (page, articles) -> new articles page articles)
let sort_by_date ?(increasing = false) articles =
List.sort
(fun (_, articleA) (_, articleB) ->
let r = Datetime.compare articleA#date articleB#date in
if increasing then r else ~-r)
articles
let fetch (module P : Required.DATA_PROVIDER) ?increasing
?(filter = fun x -> x) ?(on = `Source) ~where ~compute_link path =
Task.from_effect (fun () ->
let open Eff in
let* files = read_directory ~on ~only:`Files ~where path in
let+ articles =
List.traverse
(fun file ->
let url = compute_link file in
let+ metadata, _content =
Eff.read_file_with_metadata (module P) (module Article) ~on file
in
(url, metadata))
files
in
articles |> sort_by_date ?increasing |> filter)
let compute_index (module P : Required.DATA_PROVIDER) ?increasing
?(filter = fun x -> x) ?(on = `Source) ~where ~compute_link path =
let open Task in
(fun x -> (x, ()))
|>> second
(fetch (module P) ?increasing ~filter ~on ~where ~compute_link path)
>>> from_page
let normalize_article (ident, article) =
let open Data in
record (("url", string @@ Path.to_string ident) :: Article.normalize article)
let normalize obj =
let open Data in
("articles", list_of normalize_article obj#articles)
:: ("has_articles", bool @@ is_empty_list obj#articles)
:: Page.normalize obj
end