My agentic slop goes here. Not intended for anyone else!
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
103 @param feeds List of feed subscriptions (Atom/RSS/JSON) associated with this contact
104 *)
105 val make :
106 handle:string ->
107 names:string list ->
108 ?email:string ->
109 ?icon:string ->
110 ?thumbnail:string ->
111 ?github:string ->
112 ?twitter:string ->
113 ?bluesky:string ->
114 ?mastodon:string ->
115 ?orcid:string ->
116 ?url:string ->
117 ?feeds:Feed.t list ->
118 unit ->
119 t
120
121 (** {2 Accessors} *)
122
123 (** [handle t] returns the unique handle/username. *)
124 val handle : t -> string
125
126 (** [names t] returns all names associated with this contact. *)
127 val names : t -> string list
128
129 (** [name t] returns the primary (first) name. *)
130 val name : t -> string
131
132 (** [primary_name t] returns the primary (first) name.
133 This is an alias for {!name} for clarity. *)
134 val primary_name : t -> string
135
136 (** [email t] returns the email address if available. *)
137 val email : t -> string option
138
139 (** [icon t] returns the icon/avatar URL if available. *)
140 val icon : t -> string option
141
142 (** [thumbnail t] returns the path to the local thumbnail image if available.
143 This is a relative path from the Sortal data directory. *)
144 val thumbnail : t -> string option
145
146 (** [github t] returns the GitHub username if available. *)
147 val github : t -> string option
148
149 (** [twitter t] returns the Twitter/X username if available. *)
150 val twitter : t -> string option
151
152 (** [bluesky t] returns the Bluesky handle if available. *)
153 val bluesky : t -> string option
154
155 (** [mastodon t] returns the Mastodon handle if available. *)
156 val mastodon : t -> string option
157
158 (** [orcid t] returns the ORCID identifier if available. *)
159 val orcid : t -> string option
160
161 (** [url t] returns the personal/professional website URL if available. *)
162 val url : t -> string option
163
164 (** [feeds t] returns the list of feed subscriptions if available. *)
165 val feeds : t -> Feed.t list option
166
167 (** [add_feed t feed] returns a new contact with the feed added. *)
168 val add_feed : t -> Feed.t -> t
169
170 (** [remove_feed t url] returns a new contact with the feed matching the URL removed. *)
171 val remove_feed : t -> string -> t
172
173 (** {2 Derived Information} *)
174
175 (** [best_url t] returns the best available URL for this contact.
176
177 Priority order:
178 1. Personal URL (if set)
179 2. GitHub profile URL (if GitHub username is set)
180 3. Email as mailto: link (if email is set)
181 4. None if no URL-like information is available
182 *)
183 val best_url : t -> string option
184
185 (** {2 JSON Encoding} *)
186
187 (** [json_t] is the jsont encoder/decoder for contacts.
188
189 The JSON schema includes all contact fields with optional values
190 omitted when not present:
191 {[
192 {
193 "handle": "avsm",
194 "names": ["Anil Madhavapeddy"],
195 "email": "anil@recoil.org",
196 "github": "avsm",
197 "orcid": "0000-0002-7890-1234"
198 }
199 ]}
200 *)
201 val json_t : t Jsont.t
202
203 (** {2 Utilities} *)
204
205 (** [compare a b] compares two contacts by their handles. *)
206 val compare : t -> t -> int
207
208 (** [pp ppf t] pretty prints a contact with formatting. *)
209 val pp : Format.formatter -> t -> unit
210end
211
212(** {1 Contact Store} *)
213
214(** The contact store manages reading and writing contact metadata
215 using XDG-compliant storage locations. *)
216type t
217
218(** [create fs app_name] creates a new contact store.
219
220 The store will use XDG data directories for persistent storage
221 of contact metadata. Each contact is stored as a separate JSON
222 file named after its handle.
223
224 @param fs Eio filesystem for file operations
225 @param app_name Application name for XDG directory structure
226 *)
227val create : Eio.Fs.dir_ty Eio.Path.t -> string -> t
228
229(** {2 Storage Operations} *)
230
231(** [save t contact] saves a contact to the store.
232
233 The contact is serialized to JSON and written to a file
234 named "handle.json" in the XDG data directory.
235
236 If a contact with the same handle already exists, it is overwritten.
237 *)
238val save : t -> Contact.t -> unit
239
240(** [lookup t handle] retrieves a contact by handle.
241
242 Searches for a file named "handle.json" in the XDG data directory
243 and deserializes it if found.
244
245 @return [Some contact] if found, [None] if not found or deserialization fails
246 *)
247val lookup : t -> string -> Contact.t option
248
249(** [delete t handle] removes a contact from the store.
250
251 Deletes the file "handle.json" from the XDG data directory.
252 Does nothing if the contact does not exist.
253 *)
254val delete : t -> string -> unit
255
256(** [list t] returns all contacts in the store.
257
258 Scans the XDG data directory for all .json files and attempts
259 to deserialize them as contacts. Files that fail to parse are
260 silently skipped.
261
262 @return A list of all successfully loaded contacts
263 *)
264val list : t -> Contact.t list
265
266(** [thumbnail_path t contact] returns the absolute filesystem path to the contact's thumbnail.
267
268 Returns [None] if the contact has no thumbnail set, or [Some path] with
269 the full path to the thumbnail file in Sortal's data directory.
270
271 @param t The Sortal store
272 @param contact The contact whose thumbnail path to retrieve *)
273val thumbnail_path : t -> Contact.t -> Eio.Fs.dir_ty Eio.Path.t option
274
275(** {2 Searching} *)
276
277(** [find_by_name t name] searches for contacts by name.
278
279 Performs a case-insensitive search through all contacts,
280 checking if any of their names match the provided name.
281
282 @param name The name to search for (case-insensitive)
283 @return The matching contact if exactly one match is found
284 @raise Not_found if no contacts match the name
285 @raise Invalid_argument if multiple contacts match the name
286 *)
287val find_by_name : t -> string -> Contact.t
288
289(** [find_by_name_opt t name] searches for contacts by name, returning an option.
290
291 Like {!find_by_name} but returns [None] instead of raising exceptions
292 when no match or multiple matches are found.
293
294 @param name The name to search for (case-insensitive)
295 @return [Some contact] if exactly one match is found, [None] otherwise
296 *)
297val find_by_name_opt : t -> string -> Contact.t option
298
299(** {2 Utilities} *)
300
301(** [handle_of_name name] generates a handle from a full name.
302
303 Creates a handle by concatenating the initials of all words
304 in the name with the full last name, all in lowercase.
305
306 Examples:
307 - "Anil Madhavapeddy" -> "ammadhavapeddy"
308 - "John Smith" -> "jssmith"
309
310 @param name The full name to convert
311 @return A suggested handle
312 *)
313val handle_of_name : string -> string
314
315(** {2 Convenience Functions} *)
316
317(** [create_from_xdg xdg] creates a contact store from an XDG context.
318
319 This is a convenience function for creating a store when you already
320 have an XDG context (e.g., from eiocmd or your own XDG initialization).
321 The store will use the XDG data directory for the application.
322
323 @param xdg An existing XDG context
324 @return A contact store using the XDG data directory
325 *)
326val create_from_xdg : Xdge.t -> t
327
328(** [search_all t query] searches for contacts matching a query string.
329
330 Performs a flexible search through all contact names, looking for:
331 - Exact matches (case-insensitive)
332 - Names that start with the query
333 - Multi-word names where any word starts with the query
334
335 This is useful for autocomplete or fuzzy search functionality.
336
337 @param t The contact store
338 @param query The search query (case-insensitive)
339 @return A list of matching contacts, sorted by handle
340 *)
341val search_all : t -> string -> Contact.t list
342
343(** {2 Pretty Printing} *)
344
345(** [pp ppf t] pretty prints the contact store showing statistics. *)
346val pp : Format.formatter -> t -> unit
347
348(** {1 Cmdliner Integration} *)
349
350module Cmd : sig
351 (** Cmdliner terms and commands for contact management.
352
353 This module provides ready-to-use Cmdliner terms for building
354 CLI applications that work with contact metadata. *)
355
356 (** [list_cmd] is a Cmdliner command that lists all contacts.
357
358 Usage: Integrate into your CLI with [Cmd.group] or use standalone.
359 Requires eiocmd setup (env, xdg, profile parameters). *)
360 val list_cmd : unit -> (Eio_unix.Stdenv.base -> Xdge.t -> 'a -> int)
361
362 (** [show_cmd handle] creates a command to show detailed contact information.
363
364 @param handle The contact handle to display *)
365 val show_cmd : string -> (Eio_unix.Stdenv.base -> Xdge.t -> 'a -> int)
366
367 (** [search_cmd query] creates a command to search contacts by name.
368
369 @param query The search query string *)
370 val search_cmd : string -> (Eio_unix.Stdenv.base -> Xdge.t -> 'a -> int)
371
372 (** [stats_cmd] is a command that shows database statistics. *)
373 val stats_cmd : unit -> (Eio_unix.Stdenv.base -> Xdge.t -> 'a -> int)
374
375 (** {2 Cmdliner Info Objects} *)
376
377 (** [list_info] is the command info for the list command. *)
378 val list_info : Cmdliner.Cmd.info
379
380 (** [show_info] is the command info for the show command. *)
381 val show_info : Cmdliner.Cmd.info
382
383 (** [search_info] is the command info for the search command. *)
384 val search_info : Cmdliner.Cmd.info
385
386 (** [stats_info] is the command info for the stats command. *)
387 val stats_info : Cmdliner.Cmd.info
388
389 (** {2 Cmdliner Argument Definitions} *)
390
391 (** [handle_arg] is the positional argument for a contact handle. *)
392 val handle_arg : string Cmdliner.Term.t
393
394 (** [query_arg] is the positional argument for a search query. *)
395 val query_arg : string Cmdliner.Term.t
396end