aboutsummaryrefslogblamecommitdiffstats
path: root/lib/configuration.ml
blob: 3f66ac0aa02c19aae74614882e95aaeb1e370537 (plain) (tree)



























                                                                      

                                                                              
        

                                                                             




















































                                                                                
module type CONFIGURABLE = sig
  type configuration

  val parse : Sexplib.Sexp.t -> (configuration, string) result
end

type t = Sexplib.Sexp.t Utilities.StringMap.t
(** Section name -> raw sexp subtree. *)

let find_section (key : string) (config : t) : Sexplib.Sexp.t option =
  Utilities.StringMap.find_opt key config

let errorf fmt = Printf.ksprintf (fun s -> Error s) fmt

(** Check that [key] is expected and not duplicate, then add it. *)
let insert_section ~expected acc key body =
  let ( let* ) = Result.bind in
  let* () =
    if Utilities.StringSet.mem key expected then Ok ()
    else errorf "Unknown configuration section: %s" key
  in
  let* () =
    if Utilities.StringMap.mem key acc then
      errorf "Duplicate configuration section: %s" key
    else Ok ()
  in
  match body with
  | [ subtree ] -> Ok (Utilities.StringMap.add key subtree acc)
  | [] ->
      errorf "Configuration section %s is empty (expected a tagged value)" key
  | _ ->
      errorf
        "Configuration section %s has multiple values (expected exactly one)"
        key

(** [(server ...)] sexp -> section map. Rejects unknown/duplicate keys. *)
let parse_server ~expected_keys (sexp : Sexplib.Sexp.t) : (t, string) result =
  let open Sexplib.Sexp in
  match sexp with
  | List (Atom "server" :: sections) ->
      let expected =
        List.fold_right Utilities.StringSet.add expected_keys
          Utilities.StringSet.empty
      in
      let rec go acc = function
        | [] -> Ok acc
        | List (Atom key :: body) :: rest ->
            Result.bind (insert_section ~expected acc key body) (fun acc ->
                go acc rest)
        | bad :: _ ->
            errorf "Malformed configuration section: %s" (to_string bad)
      in
      go Utilities.StringMap.empty sections
  | _ -> Error "Configuration must be a (server ...) s-expression"

(** Read a file from disk and run [parse_server] on it. *)
let load ~expected_keys (path : string) : (t, string) result =
  match Sexplib.Sexp.load_sexp path with
  | sexp -> parse_server ~expected_keys sexp
  | exception Sys_error msg ->
      errorf "Failed to load configuration file %s: %s" path msg
  | exception Failure msg ->
      errorf "Failed to parse configuration file %s: %s" path msg

(** [(tag field1 ...)] -> [(tag, List [field1; ...])]. *)
let extract_tagged_section (sexp : Sexplib.Sexp.t) :
    (string * Sexplib.Sexp.t, string) result =
  let open Sexplib.Sexp in
  match sexp with
  | List (Atom tag :: body) -> Ok (tag, List body)
  | _ -> errorf "Expected (tag ...) but got: %s" (to_string sexp)

(** Look up a section by name, extract its tag, and check the tag is allowed. *)
let require_section ~(name : string) ~(valid_tags : string list) (config : t) :
    (string * Sexplib.Sexp.t, string) result =
  let ( let* ) = Result.bind in
  let* sexp =
    find_section name config
    |> Option.to_result
         ~none:(Printf.sprintf "Missing (%s ...) section in configuration" name)
  in
  let* tag, body = extract_tagged_section sexp in
  let* () =
    if List.mem tag valid_tags then Ok () else errorf "Unknown %s: %s" name tag
  in
  Ok (tag, body)