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