[OCaml] Mobile-friendly clone of cgit.
1
(** Reading and validating the TOML configuration.
2
3
The file is looked up at [$OGIT_CONFIG], then
4
[$XDG_CONFIG_HOME/ogit/config.toml], then [/etc/ogit/config.toml]. When no
5
file is chosen explicitly a missing one is fine and {!default} applies; a
6
file that exists but is malformed or invalid is always an error, so a
7
mistake never silently degrades into defaults.
8
9
[config.toml] at the repository root documents every variable and doubles as
10
the reference for its default. Adding a field here means updating that file
11
in the same commit.
12
13
{!to_table} and {!write_file} support the [ogit-write-config] executable,
14
which emits a config file pre-filled with the defaults. *)
15
16
open Toml
17
18
type t = {
19
user_name : string;
20
default_branch : string;
21
git_project_root : string;
22
commits_max_displayed : int;
23
root_title : string;
24
nav_logo : string;
25
host : string;
26
port : int;
27
favorite_repositories : string list;
28
archived_repositories : string list;
29
}
30
31
type load_error =
32
| Not_found of string
33
| Parse_error of string
34
| Invalid_value of string
35
| Io_error of string
36
37
let environment_value name =
38
match Sys.getenv_opt name with Some "" | None -> None | value -> value
39
40
let default =
41
{
42
user_name = "";
43
default_branch = "main";
44
git_project_root = "/srv/git";
45
commits_max_displayed = 10;
46
root_title = "";
47
nav_logo = "/static/git_icon.svg";
48
host = "127.0.0.1";
49
port = 8081;
50
favorite_repositories = [];
51
archived_repositories = [];
52
}
53
54
let locate_config_file () =
55
match environment_value "OGIT_CONFIG" with
56
| Some file -> file
57
| None -> (
58
match environment_value "XDG_CONFIG_HOME" with
59
| Some config_home ->
60
Filename.concat (Filename.concat config_home "ogit") "config.toml"
61
| None -> "/etc/ogit/config.toml")
62
63
let to_table t =
64
let open Types in
65
let string_list_to_value lst =
66
TArray (if lst = [] then NodeEmpty else NodeString lst)
67
in
68
List.map
69
(fun (key, value) -> (Min.key key, value))
70
[
71
("user_name", TString t.user_name);
72
("default_branch", TString t.default_branch);
73
("git_project_root", TString t.git_project_root);
74
("commits_max_displayed", TInt t.commits_max_displayed);
75
("ogit_root_title", TString t.root_title);
76
("nav_logo", TString t.nav_logo);
77
("host", TString t.host);
78
("port", TInt t.port);
79
("favorite_repositories", string_list_to_value t.favorite_repositories);
80
("archived_repositories", string_list_to_value t.archived_repositories);
81
]
82
|> Min.of_key_values
83
84
let write_file ?file table =
85
let file = Option.value file ~default:(locate_config_file ()) in
86
Out_channel.with_open_text file (fun channel ->
87
Printer.string_of_table table |> Printf.fprintf channel "%s\n")
88
89
(* Key lookups fall back to [~default] when the key is absent, so a partial
90
file overriding one setting is valid. A key that is present but holds the
91
wrong type is always an error, so a typo is never silently ignored.
92
93
These are deliberately not named [*_opt]: by OCaml convention that suffix
94
means the function returns an option, whereas here the optionality is the
95
key's, not the result's. *)
96
97
let optional_string table key ~default =
98
match Types.Table.find_opt (Min.key key) table with
99
| Some (Types.TString value) -> Ok value
100
| Some _ -> Error (Invalid_value ("expected string for key: " ^ key))
101
| None -> Ok default
102
103
let optional_int table key ~default =
104
match Types.Table.find_opt (Min.key key) table with
105
| Some (Types.TInt value) -> Ok value
106
| Some _ -> Error (Invalid_value ("expected int for key: " ^ key))
107
| None -> Ok default
108
109
(* An absent list and an empty list mean the same thing here, so this needs no
110
[~default]. *)
111
let optional_string_list table key =
112
match Types.Table.find_opt (Min.key key) table with
113
| Some (Types.TArray (Types.NodeString values)) -> Ok values
114
| Some (Types.TArray Types.NodeEmpty) -> Ok []
115
| Some _ ->
116
Error (Invalid_value ("expected array of strings for key: " ^ key))
117
| None -> Ok []
118
119
let of_table table =
120
let ( let* ) = Result.bind in
121
let* git_project_root =
122
optional_string table "git_project_root" ~default:default.git_project_root
123
in
124
let* user_name =
125
optional_string table "user_name" ~default:default.user_name
126
in
127
let* default_branch =
128
optional_string table "default_branch" ~default:default.default_branch
129
in
130
let* commits_max_displayed =
131
optional_int table "commits_max_displayed"
132
~default:default.commits_max_displayed
133
in
134
(* [ogit_root_title] is the current key; a bare [title] is still accepted so
135
that configuration files predating the rename keep working. *)
136
let* root_title =
137
match Types.Table.find_opt (Min.key "ogit_root_title") table with
138
| Some _ ->
139
optional_string table "ogit_root_title" ~default:default.root_title
140
| None -> optional_string table "title" ~default:default.root_title
141
in
142
let* nav_logo = optional_string table "nav_logo" ~default:default.nav_logo in
143
let* host = optional_string table "host" ~default:default.host in
144
let* port = optional_int table "port" ~default:default.port in
145
let* favorite_repositories =
146
optional_string_list table "favorite_repositories"
147
in
148
let* archived_repositories =
149
optional_string_list table "archived_repositories"
150
in
151
if commits_max_displayed <= 0 then
152
Error (Invalid_value "commits_max_displayed must be positive")
153
else if port < 1 || port > 65535 then
154
Error (Invalid_value "port must be between 1 and 65535")
155
else
156
Ok
157
{
158
git_project_root;
159
user_name;
160
default_branch;
161
commits_max_displayed;
162
root_title;
163
nav_logo;
164
host;
165
port;
166
favorite_repositories;
167
archived_repositories;
168
}
169
170
let io_error file error =
171
Io_error (Printf.sprintf "%s: %s" file (Unix.error_message error))
172
173
let file_exists file =
174
try
175
ignore (Unix.stat file);
176
Ok ()
177
with
178
| Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) ->
179
Error (Not_found file)
180
| Unix.Unix_error (error, _, _) -> Error (io_error file error)
181
182
let read_file ?file () =
183
let file = Option.value file ~default:(locate_config_file ()) in
184
let ( let* ) = Result.bind in
185
let* () = file_exists file in
186
try
187
match Toml.Parser.from_filename file with
188
| `Error (message, location) ->
189
Error
190
(Parse_error
191
(Printf.sprintf "%s: %s at line %d" location.source message
192
location.line))
193
| `Ok table -> of_table table
194
with Sys_error message -> Error (Io_error message)
195
196
let implicit_config_files () =
197
match environment_value "XDG_CONFIG_HOME" with
198
| Some config_home ->
199
[
200
Filename.concat (Filename.concat config_home "ogit") "config.toml";
201
"/etc/ogit/config.toml";
202
]
203
| None -> [ "/etc/ogit/config.toml" ]
204
205
let load () =
206
match environment_value "OGIT_CONFIG" with
207
| Some file -> read_file ~file ()
208
| None ->
209
let rec first_existing = function
210
| [] -> Ok default
211
| file :: rest -> (
212
match read_file ~file () with
213
| Error (Not_found _) -> first_existing rest
214
| result -> result)
215
in
216
first_existing (implicit_config_files ())
217
218
(* [pp_load_error] and [show_load_error] follow the convention established by
219
ppx_deriving: [pp_] is the Format-based printer that composes with "%a", and
220
[show_] is the string-producing convenience built on it. Naming them as a
221
pair signals that relationship, which [load_error_to_string] did not. *)
222
223
let pp_load_error formatter = function
224
| Not_found file ->
225
Format.fprintf formatter "configuration file not found: %s" file
226
| Parse_error message ->
227
Format.fprintf formatter "invalid configuration: %s" message
228
| Invalid_value message ->
229
Format.fprintf formatter "invalid configuration value: %s" message
230
| Io_error message ->
231
Format.fprintf formatter "could not read configuration: %s" message
232
233
let show_load_error error = Format.asprintf "%a" pp_load_error error
234