OCaml HTML5 parser/serialiser based on Python's JustHTML

Squashed 'ocaml-zulip/' changes from feaee1f..c074b19

c074b19 Add Zulip standard [api] section config support and daily changelog
98a5561 Store poe config under unified XDG app name

git-subtree-dir: ocaml-zulip
git-subtree-split: c074b199446d2065bf0ce23b5ebe54e7d11f5245

+80 -35
+51 -14
lib/zulip_bot/config.ml
··· 14 14 api_key : string; 15 15 description : string option; 16 16 usage : string option; 17 + xdg_app : string option; 17 18 } 18 19 19 - let create ~name ~site ~email ~api_key ?description ?usage () = 20 - { name; site; email; api_key; description; usage } 20 + let create ~name ~site ~email ~api_key ?description ?usage ?xdg_app () = 21 + { name; site; email; api_key; description; usage; xdg_app } 21 22 22 23 (** Convert bot name to environment variable prefix. "my-bot" -> "ZULIP_MY_BOT" 23 24 *) ··· 53 54 |> opt_mem "usage" Init.string ~enc:(fun c -> c.ini_usage) 54 55 |> skip_unknown |> finish) 55 56 57 + (** Codec for Zulip's standard [api] section format (uses "key" not "api_key") *) 58 + let zulip_api_section_codec = 59 + Init.Section.( 60 + obj (fun site email key -> 61 + { 62 + ini_site = site; 63 + ini_email = email; 64 + ini_api_key = key; 65 + ini_description = None; 66 + ini_usage = None; 67 + }) 68 + |> mem "site" Init.string ~enc:(fun c -> c.ini_site) 69 + |> mem "email" Init.string ~enc:(fun c -> c.ini_email) 70 + |> mem "key" Init.string ~enc:(fun c -> c.ini_api_key) 71 + |> skip_unknown |> finish) 72 + 56 73 (** Document codec that accepts a [bot] section or bare options at top level *) 57 74 let ini_doc_codec = 58 75 Init.Document.( ··· 60 77 |> section "bot" ini_section_codec ~enc:Fun.id 61 78 |> skip_unknown |> finish) 62 79 80 + (** Document codec for Zulip's standard [api] section format *) 81 + let zulip_api_doc_codec = 82 + Init.Document.( 83 + obj (fun api -> api) 84 + |> section "api" zulip_api_section_codec ~enc:Fun.id 85 + |> skip_unknown |> finish) 86 + 63 87 (** Codec for configs without section headers (bare key=value pairs) *) 64 88 let bare_section_codec = 65 89 Init.Document.( ··· 67 91 |> defaults ini_section_codec ~enc:Fun.id 68 92 |> skip_unknown |> finish) 69 93 70 - let load ~fs name = 94 + let load ?xdg_app ~fs name = 71 95 Log.info (fun m -> m "Loading config for bot: %s" name); 72 - let xdg = Xdge.create fs ("zulip-bot/" ^ name) in 73 - let config_file = Eio.Path.(Xdge.config_dir xdg / "config") in 96 + let app = match xdg_app with Some app -> app | None -> "zulip-bot/" ^ name in 97 + let xdg = Xdge.create fs app in 98 + let config_file = Eio.Path.(Xdge.config_dir xdg / "zulip.config") in 74 99 Log.debug (fun m -> m "Looking for config at: %a" Eio.Path.pp config_file); 75 - (* Try parsing with [bot] section first, fall back to bare config *) 100 + (* Try parsing with [bot] section, then Zulip [api] section, then bare config *) 76 101 let ini_config = 77 102 match Init_eio.decode_path ini_doc_codec config_file with 78 103 | Ok c -> c 79 104 | Error _ -> ( 80 - (* Try bare config format (no section headers) *) 81 - match Init_eio.decode_path bare_section_codec config_file with 105 + (* Try Zulip's standard [api] section format *) 106 + match Init_eio.decode_path zulip_api_doc_codec config_file with 82 107 | Ok c -> c 83 - | Error e -> raise (Init_eio.err e)) 108 + | Error _ -> ( 109 + (* Try bare config format (no section headers) *) 110 + match Init_eio.decode_path bare_section_codec config_file with 111 + | Ok c -> c 112 + | Error e -> raise (Init_eio.err e))) 84 113 in 85 114 { 86 115 name; ··· 89 118 api_key = ini_config.ini_api_key; 90 119 description = ini_config.ini_description; 91 120 usage = ini_config.ini_usage; 121 + xdg_app; 92 122 } 93 123 94 - let from_env name = 124 + let from_env ?xdg_app name = 95 125 Log.info (fun m -> m "Loading config for bot %s from environment" name); 96 126 let prefix = env_prefix name in 97 127 let get_env key = Sys.getenv_opt (prefix ^ key) in ··· 110 140 api_key = get_required "API_KEY"; 111 141 description = get_env "DESCRIPTION"; 112 142 usage = get_env "USAGE"; 143 + xdg_app; 113 144 } 114 145 115 - let load_or_env ~fs name = 116 - try load ~fs name 146 + let load_or_env ?xdg_app ~fs name = 147 + try load ?xdg_app ~fs name 117 148 with _ -> 118 149 Log.debug (fun m -> 119 150 m "Config file not found, falling back to environment variables"); 120 - from_env name 151 + from_env ?xdg_app name 121 152 122 - let xdg ~fs config = Xdge.create fs ("zulip-bot/" ^ config.name) 153 + let xdg ~fs config = 154 + let app = 155 + match config.xdg_app with 156 + | Some app -> app 157 + | None -> "zulip-bot/" ^ config.name 158 + in 159 + Xdge.create fs app 123 160 let data_dir ~fs config = Xdge.data_dir (xdg ~fs config) 124 161 let state_dir ~fs config = Xdge.state_dir (xdg ~fs config) 125 162 let cache_dir ~fs config = Xdge.cache_dir (xdg ~fs config)
+29 -21
lib/zulip_bot/config.mli
··· 5 5 6 6 (** Bot configuration with XDG Base Directory support. 7 7 8 - Configuration is loaded from XDG-compliant locations using the bot's name to 9 - locate the appropriate configuration file. The configuration file should be 10 - in INI format with the following structure: 8 + Configuration is loaded from XDG-compliant locations. The configuration 9 + file should be in INI format with the following structure: 11 10 12 11 {v 13 12 [bot] ··· 21 20 v} 22 21 23 22 Configuration files are searched in XDG config directories: 24 - - [$XDG_CONFIG_HOME/zulip-bot/<name>/config] (typically 25 - [~/.config/zulip-bot/<name>/config]) 26 - - System directories as fallback 23 + - Default: [$XDG_CONFIG_HOME/zulip-bot/<name>/zulip.config] 24 + - With [~xdg_app:"myapp"]: [$XDG_CONFIG_HOME/myapp/zulip.config] 27 25 28 26 Environment variables can override file configuration: 29 27 - [ZULIP_<NAME>_SITE], [ZULIP_<NAME>_EMAIL], [ZULIP_<NAME>_API_KEY] ··· 38 36 api_key : string; (** Bot API key *) 39 37 description : string option; (** Optional bot description *) 40 38 usage : string option; (** Optional usage help text *) 39 + xdg_app : string option; (** Custom XDG app name for config/data paths *) 41 40 } 42 41 (** Bot configuration record. *) 43 42 ··· 48 47 api_key:string -> 49 48 ?description:string -> 50 49 ?usage:string -> 50 + ?xdg_app:string -> 51 51 unit -> 52 52 t 53 - (** [create ~name ~site ~email ~api_key ?description ?usage ()] creates a 54 - configuration programmatically. *) 53 + (** [create ~name ~site ~email ~api_key ?description ?usage ?xdg_app ()] 54 + creates a configuration programmatically. If [xdg_app] is provided, 55 + XDG directories will use that app name instead of [zulip-bot/<name>]. *) 55 56 56 - val load : fs:Eio.Fs.dir_ty Eio.Path.t -> string -> t 57 - (** [load ~fs name] loads configuration for a named bot from XDG config 58 - directory. 57 + val load : ?xdg_app:string -> fs:Eio.Fs.dir_ty Eio.Path.t -> string -> t 58 + (** [load ?xdg_app ~fs name] loads configuration for a named bot from XDG 59 + config directory. 59 60 60 61 Searches for configuration in: 61 - - [$XDG_CONFIG_HOME/zulip-bot/<name>/config] 62 - - System config directories as fallback 62 + - Default: [$XDG_CONFIG_HOME/zulip-bot/<name>/zulip.config] 63 + - With [~xdg_app:"myapp"]: [$XDG_CONFIG_HOME/myapp/zulip.config] 63 64 65 + @param xdg_app Custom XDG app name (overrides default [zulip-bot/<name>]) 64 66 @param fs The Eio filesystem 65 67 @param name The bot name 66 68 @raise Eio.Io if configuration file cannot be read or parsed 67 69 @raise Failure if required fields are missing *) 68 70 69 - val from_env : string -> t 70 - (** [from_env name] loads configuration from environment variables. 71 + val from_env : ?xdg_app:string -> string -> t 72 + (** [from_env ?xdg_app name] loads configuration from environment variables. 71 73 72 74 Reads the following environment variables (where [NAME] is the uppercase bot 73 75 name with hyphens replaced by underscores): ··· 77 79 - [ZULIP_<NAME>_DESCRIPTION] (optional) 78 80 - [ZULIP_<NAME>_USAGE] (optional) 79 81 82 + @param xdg_app Custom XDG app name for data/state/cache directories 80 83 @raise Failure if required environment variables are not set *) 81 84 82 - val load_or_env : fs:Eio.Fs.dir_ty Eio.Path.t -> string -> t 83 - (** [load_or_env ~fs name] loads config from XDG location, falling back to 84 - environment. 85 + val load_or_env : ?xdg_app:string -> fs:Eio.Fs.dir_ty Eio.Path.t -> string -> t 86 + (** [load_or_env ?xdg_app ~fs name] loads config from XDG location, falling 87 + back to environment. 85 88 86 89 Attempts to load from the XDG config file first. If that fails (file not 87 90 found or unreadable), falls back to environment variables. 88 91 92 + @param xdg_app Custom XDG app name (overrides default [zulip-bot/<name>]) 89 93 @raise Failure if neither source provides valid configuration *) 90 94 91 95 val xdg : fs:Eio.Fs.dir_ty Eio.Path.t -> t -> Xdge.t 92 96 (** [xdg ~fs config] returns the XDG context for this bot. 93 97 98 + Uses the [xdg_app] field if set, otherwise defaults to [zulip-bot/<name>]. 94 99 Useful for accessing data and state directories for the bot: 95 100 - [Xdge.data_dir (xdg ~fs config)] for persistent data 96 101 - [Xdge.state_dir (xdg ~fs config)] for runtime state ··· 99 104 val data_dir : fs:Eio.Fs.dir_ty Eio.Path.t -> t -> Eio.Fs.dir_ty Eio.Path.t 100 105 (** [data_dir ~fs config] returns the XDG data directory for this bot. 101 106 102 - Returns [$XDG_DATA_HOME/zulip-bot/<name>], creating it if necessary. *) 107 + Returns [$XDG_DATA_HOME/<app>] where [<app>] is either [config.xdg_app] 108 + or [zulip-bot/<name>]. Creates the directory if necessary. *) 103 109 104 110 val state_dir : fs:Eio.Fs.dir_ty Eio.Path.t -> t -> Eio.Fs.dir_ty Eio.Path.t 105 111 (** [state_dir ~fs config] returns the XDG state directory for this bot. 106 112 107 - Returns [$XDG_STATE_HOME/zulip-bot/<name>], creating it if necessary. *) 113 + Returns [$XDG_STATE_HOME/<app>] where [<app>] is either [config.xdg_app] 114 + or [zulip-bot/<name>]. Creates the directory if necessary. *) 108 115 109 116 val cache_dir : fs:Eio.Fs.dir_ty Eio.Path.t -> t -> Eio.Fs.dir_ty Eio.Path.t 110 117 (** [cache_dir ~fs config] returns the XDG cache directory for this bot. 111 118 112 - Returns [$XDG_CACHE_HOME/zulip-bot/<name>], creating it if necessary. *) 119 + Returns [$XDG_CACHE_HOME/<app>] where [<app>] is either [config.xdg_app] 120 + or [zulip-bot/<name>]. Creates the directory if necessary. *)