My agentic slop goes here. Not intended for anyone else!
at main 13 kB view raw
1(** Sortal - Username to metadata mapping with XDG storage 2 3 This library provides a system for mapping usernames to various metadata 4 including URLs, emails, ORCID identifiers, and social media handles. 5 It uses XDG Base Directory Specification for storage locations and 6 jsont for JSON encoding/decoding. 7 8 {b Storage:} 9 10 Contact metadata is stored as JSON files in the XDG data directory, 11 with one file per contact using the handle as the filename. 12 13 {b Typical Usage:} 14 15 {[ 16 let store = Sortal.create env#fs "myapp" in 17 let contact = Sortal.Contact.make 18 ~handle:"avsm" 19 ~names:["Anil Madhavapeddy"] 20 ~email:"anil@recoil.org" 21 ~github:"avsm" 22 ~orcid:"0000-0002-7890-1234" 23 () in 24 Sortal.save store contact; 25 26 match Sortal.lookup store "avsm" with 27 | Some c -> Printf.printf "Found: %s\n" (Sortal.Contact.name c) 28 | None -> Printf.printf "Not found\n" 29 ]} 30*) 31 32(** {1 Feed Metadata} *) 33 34module Feed : sig 35 (** Feed subscription with type and URL. 36 37 A feed represents a subscription to a content source (Atom, RSS, or JSONFeed). *) 38 type t 39 40 (** Feed type identifier. *) 41 type feed_type = 42 | Atom (** Atom feed format *) 43 | Rss (** RSS feed format *) 44 | Json (** JSON Feed format *) 45 46 (** [make ~feed_type ~url ?name ()] creates a new feed. 47 48 @param feed_type The type of feed (Atom, RSS, or JSON) 49 @param url The feed URL 50 @param name Optional human-readable name/label for the feed 51 *) 52 val make : feed_type:feed_type -> url:string -> ?name:string -> unit -> t 53 54 (** [feed_type t] returns the feed type. *) 55 val feed_type : t -> feed_type 56 57 (** [url t] returns the feed URL. *) 58 val url : t -> string 59 60 (** [name t] returns the feed name if set. *) 61 val name : t -> string option 62 63 (** [set_name t name] returns a new feed with the name updated. *) 64 val set_name : t -> string -> t 65 66 (** [feed_type_to_string ft] converts a feed type to a string. *) 67 val feed_type_to_string : feed_type -> string 68 69 (** [feed_type_of_string s] parses a feed type from a string. 70 Returns [None] if the string is not recognized. *) 71 val feed_type_of_string : string -> feed_type option 72 73 (** [json_t] is the jsont encoder/decoder for feeds. *) 74 val json_t : t Jsont.t 75 76 (** [pp ppf t] pretty prints a feed. *) 77 val pp : Format.formatter -> t -> unit 78end 79 80(** {1 Contact Metadata} *) 81 82module Contact : sig 83 (** Individual contact metadata. 84 85 A contact represents metadata about a person, including their name(s), 86 social media handles, professional identifiers, and other contact information. *) 87 type t 88 89 (** [make ~handle ~names ?email ?icon ?thumbnail ?github ?twitter ?bluesky ?mastodon 90 ?orcid ?url ?feeds ()] creates a new contact. 91 92 @param handle A unique identifier/username for this contact (required) 93 @param names A list of names for this contact, with the first being primary (required) 94 @param email Email address 95 @param icon URL to an avatar/icon image 96 @param thumbnail Path to a local thumbnail image file 97 @param github GitHub username (without the [\@] prefix) 98 @param twitter Twitter/X username (without the [\@] prefix) 99 @param bluesky Bluesky handle 100 @param mastodon Mastodon handle (including instance) 101 @param orcid ORCID identifier 102 @param url Personal or professional website URL (primary URL) 103 @param urls Additional website URLs 104 @param feeds List of feed subscriptions (Atom/RSS/JSON) associated with this contact 105 *) 106 val make : 107 handle:string -> 108 names:string list -> 109 ?email:string -> 110 ?icon:string -> 111 ?thumbnail:string -> 112 ?github:string -> 113 ?twitter:string -> 114 ?bluesky:string -> 115 ?mastodon:string -> 116 ?orcid:string -> 117 ?url:string -> 118 ?urls:string list -> 119 ?feeds:Feed.t list -> 120 unit -> 121 t 122 123 (** {2 Accessors} *) 124 125 (** [handle t] returns the unique handle/username. *) 126 val handle : t -> string 127 128 (** [names t] returns all names associated with this contact. *) 129 val names : t -> string list 130 131 (** [name t] returns the primary (first) name. *) 132 val name : t -> string 133 134 (** [primary_name t] returns the primary (first) name. 135 This is an alias for {!name} for clarity. *) 136 val primary_name : t -> string 137 138 (** [email t] returns the email address if available. *) 139 val email : t -> string option 140 141 (** [icon t] returns the icon/avatar URL if available. *) 142 val icon : t -> string option 143 144 (** [thumbnail t] returns the path to the local thumbnail image if available. 145 This is a relative path from the Sortal data directory. *) 146 val thumbnail : t -> string option 147 148 (** [github t] returns the GitHub username if available. *) 149 val github : t -> string option 150 151 (** [twitter t] returns the Twitter/X username if available. *) 152 val twitter : t -> string option 153 154 (** [bluesky t] returns the Bluesky handle if available. *) 155 val bluesky : t -> string option 156 157 (** [mastodon t] returns the Mastodon handle if available. *) 158 val mastodon : t -> string option 159 160 (** [orcid t] returns the ORCID identifier if available. *) 161 val orcid : t -> string option 162 163 (** [url t] returns the primary URL if available. 164 165 Returns the [url] field if set, otherwise returns the first element 166 of [urls] if available, or [None] if neither is set. *) 167 val url : t -> string option 168 169 (** [urls t] returns all URLs associated with this contact. 170 171 Combines the [url] field (if set) with the [urls] list (if set). 172 The primary [url] appears first if present. Returns an empty list 173 if neither [url] nor [urls] is set. *) 174 val urls : t -> string list 175 176 (** [feeds t] returns the list of feed subscriptions if available. *) 177 val feeds : t -> Feed.t list option 178 179 (** [add_feed t feed] returns a new contact with the feed added. *) 180 val add_feed : t -> Feed.t -> t 181 182 (** [remove_feed t url] returns a new contact with the feed matching the URL removed. *) 183 val remove_feed : t -> string -> t 184 185 (** {2 Derived Information} *) 186 187 (** [best_url t] returns the best available URL for this contact. 188 189 Priority order: 190 1. Personal URL (if set) 191 2. GitHub profile URL (if GitHub username is set) 192 3. Email as mailto: link (if email is set) 193 4. None if no URL-like information is available 194 *) 195 val best_url : t -> string option 196 197 (** {2 JSON Encoding} *) 198 199 (** [json_t] is the jsont encoder/decoder for contacts. 200 201 The JSON schema includes all contact fields with optional values 202 omitted when not present: 203 {[ 204 { 205 "handle": "avsm", 206 "names": ["Anil Madhavapeddy"], 207 "email": "anil@recoil.org", 208 "github": "avsm", 209 "orcid": "0000-0002-7890-1234" 210 } 211 ]} 212 *) 213 val json_t : t Jsont.t 214 215 (** {2 Utilities} *) 216 217 (** [compare a b] compares two contacts by their handles. *) 218 val compare : t -> t -> int 219 220 (** [pp ppf t] pretty prints a contact with formatting. *) 221 val pp : Format.formatter -> t -> unit 222end 223 224(** {1 Contact Store} *) 225 226(** The contact store manages reading and writing contact metadata 227 using XDG-compliant storage locations. *) 228type t 229 230(** [create fs app_name] creates a new contact store. 231 232 The store will use XDG data directories for persistent storage 233 of contact metadata. Each contact is stored as a separate JSON 234 file named after its handle. 235 236 @param fs Eio filesystem for file operations 237 @param app_name Application name for XDG directory structure 238 *) 239val create : Eio.Fs.dir_ty Eio.Path.t -> string -> t 240 241(** {2 Storage Operations} *) 242 243(** [save t contact] saves a contact to the store. 244 245 The contact is serialized to JSON and written to a file 246 named "handle.json" in the XDG data directory. 247 248 If a contact with the same handle already exists, it is overwritten. 249 *) 250val save : t -> Contact.t -> unit 251 252(** [lookup t handle] retrieves a contact by handle. 253 254 Searches for a file named "handle.json" in the XDG data directory 255 and deserializes it if found. 256 257 @return [Some contact] if found, [None] if not found or deserialization fails 258 *) 259val lookup : t -> string -> Contact.t option 260 261(** [delete t handle] removes a contact from the store. 262 263 Deletes the file "handle.json" from the XDG data directory. 264 Does nothing if the contact does not exist. 265 *) 266val delete : t -> string -> unit 267 268(** [list t] returns all contacts in the store. 269 270 Scans the XDG data directory for all .json files and attempts 271 to deserialize them as contacts. Files that fail to parse are 272 silently skipped. 273 274 @return A list of all successfully loaded contacts 275 *) 276val list : t -> Contact.t list 277 278(** [thumbnail_path t contact] returns the absolute filesystem path to the contact's thumbnail. 279 280 Returns [None] if the contact has no thumbnail set, or [Some path] with 281 the full path to the thumbnail file in Sortal's data directory. 282 283 @param t The Sortal store 284 @param contact The contact whose thumbnail path to retrieve *) 285val thumbnail_path : t -> Contact.t -> Eio.Fs.dir_ty Eio.Path.t option 286 287(** {2 Searching} *) 288 289(** [find_by_name t name] searches for contacts by name. 290 291 Performs a case-insensitive search through all contacts, 292 checking if any of their names match the provided name. 293 294 @param name The name to search for (case-insensitive) 295 @return The matching contact if exactly one match is found 296 @raise Not_found if no contacts match the name 297 @raise Invalid_argument if multiple contacts match the name 298 *) 299val find_by_name : t -> string -> Contact.t 300 301(** [find_by_name_opt t name] searches for contacts by name, returning an option. 302 303 Like {!find_by_name} but returns [None] instead of raising exceptions 304 when no match or multiple matches are found. 305 306 @param name The name to search for (case-insensitive) 307 @return [Some contact] if exactly one match is found, [None] otherwise 308 *) 309val find_by_name_opt : t -> string -> Contact.t option 310 311(** {2 Utilities} *) 312 313(** [handle_of_name name] generates a handle from a full name. 314 315 Creates a handle by concatenating the initials of all words 316 in the name with the full last name, all in lowercase. 317 318 Examples: 319 - "Anil Madhavapeddy" -> "ammadhavapeddy" 320 - "John Smith" -> "jssmith" 321 322 @param name The full name to convert 323 @return A suggested handle 324 *) 325val handle_of_name : string -> string 326 327(** {2 Convenience Functions} *) 328 329(** [create_from_xdg xdg] creates a contact store from an XDG context. 330 331 This is a convenience function for creating a store when you already 332 have an XDG context (e.g., from eiocmd or your own XDG initialization). 333 The store will use the XDG data directory for the application. 334 335 @param xdg An existing XDG context 336 @return A contact store using the XDG data directory 337 *) 338val create_from_xdg : Xdge.t -> t 339 340(** [search_all t query] searches for contacts matching a query string. 341 342 Performs a flexible search through all contact names, looking for: 343 - Exact matches (case-insensitive) 344 - Names that start with the query 345 - Multi-word names where any word starts with the query 346 347 This is useful for autocomplete or fuzzy search functionality. 348 349 @param t The contact store 350 @param query The search query (case-insensitive) 351 @return A list of matching contacts, sorted by handle 352 *) 353val search_all : t -> string -> Contact.t list 354 355(** {2 Pretty Printing} *) 356 357(** [pp ppf t] pretty prints the contact store showing statistics. *) 358val pp : Format.formatter -> t -> unit 359 360(** {1 Cmdliner Integration} *) 361 362module Cmd : sig 363 (** Cmdliner terms and commands for contact management. 364 365 This module provides ready-to-use Cmdliner terms for building 366 CLI applications that work with contact metadata. *) 367 368 (** [list_cmd] is a Cmdliner command that lists all contacts. 369 370 Usage: Integrate into your CLI with [Cmd.group] or use standalone. 371 Requires eiocmd setup (env, xdg, profile parameters). *) 372 val list_cmd : unit -> (Eio_unix.Stdenv.base -> Xdge.t -> 'a -> int) 373 374 (** [show_cmd handle] creates a command to show detailed contact information. 375 376 @param handle The contact handle to display *) 377 val show_cmd : string -> (Eio_unix.Stdenv.base -> Xdge.t -> 'a -> int) 378 379 (** [search_cmd query] creates a command to search contacts by name. 380 381 @param query The search query string *) 382 val search_cmd : string -> (Eio_unix.Stdenv.base -> Xdge.t -> 'a -> int) 383 384 (** [stats_cmd] is a command that shows database statistics. *) 385 val stats_cmd : unit -> (Eio_unix.Stdenv.base -> Xdge.t -> 'a -> int) 386 387 (** {2 Cmdliner Info Objects} *) 388 389 (** [list_info] is the command info for the list command. *) 390 val list_info : Cmdliner.Cmd.info 391 392 (** [show_info] is the command info for the show command. *) 393 val show_info : Cmdliner.Cmd.info 394 395 (** [search_info] is the command info for the search command. *) 396 val search_info : Cmdliner.Cmd.info 397 398 (** [stats_info] is the command info for the stats command. *) 399 val stats_info : Cmdliner.Cmd.info 400 401 (** {2 Cmdliner Argument Definitions} *) 402 403 (** [handle_arg] is the positional argument for a contact handle. *) 404 val handle_arg : string Cmdliner.Term.t 405 406 (** [query_arg] is the positional argument for a search query. *) 407 val query_arg : string Cmdliner.Term.t 408end