Skip to main content

max / balanced_breakfast

Decompose register_host_functions into grouped helpers Split the single ~215-line register_host_functions into five focused helpers it now delegates to: register_http_fns (http_get/http_get_json, capturing the request counter, deadline, and shared ureq agent), register_parse_fns, register_string_fns, register_util_fns, and register_error_fns. Same file, same module-level consts and validate_url/ is_cgnat helpers; a readability pass, not a behavior change. All 21 registered builtin names preserved, build clean, clippy unchanged, 153 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-13 17:34 UTC
Commit: 5b96b693a6bbafd8e11f1a5257282c5506ef9ed6
Parent: 75a1795
1 file changed, +76 insertions, -46 deletions
@@ -217,6 +217,20 @@ pub(super) fn register_host_functions(
217 217 request_counter: Arc<AtomicUsize>,
218 218 fetch_deadline: Arc<AtomicU64>,
219 219 ) {
220 + register_http_fns(engine, request_counter, fetch_deadline);
221 + register_parse_fns(engine);
222 + register_string_fns(engine);
223 + register_util_fns(engine);
224 + register_error_fns(engine);
225 + }
226 +
227 + /// Register the HTTP fetch builtins (`http_get`, `http_get_json`). These capture the
228 + /// per-fetch request counter, deadline, and a shared `ureq` agent.
229 + fn register_http_fns(
230 + engine: &mut Engine,
231 + request_counter: Arc<AtomicUsize>,
232 + fetch_deadline: Arc<AtomicU64>,
233 + ) {
220 234 // HTTP GET returning string (see trust model above)
221 235 let counter = request_counter.clone();
222 236 let deadline = fetch_deadline.clone();
@@ -267,7 +281,11 @@ pub(super) fn register_host_functions(
267 281 json_to_dynamic(json)
268 282 },
269 283 );
284 + }
270 285
286 + /// Register the parsing builtins (`parse_json`, `parse_xml`, `parse_feed`,
287 + /// `parse_datetime`, `parse_int`).
288 + fn register_parse_fns(engine: &mut Engine) {
271 289 // Parse JSON string to Dynamic
272 290 engine.register_fn(
273 291 "parse_json",
@@ -303,14 +321,30 @@ pub(super) fn register_host_functions(
303 321 },
304 322 );
305 323
306 - // Current UTC timestamp as Unix epoch seconds (i64).
307 - engine.register_fn("timestamp_now", || -> i64 { chrono::Utc::now().timestamp() });
324 + // Parse ISO 8601 date to timestamp
325 + engine.register_fn(
326 + "parse_datetime",
327 + |date_str: &str| -> Result<i64, Box<rhai::EvalAltResult>> {
328 + chrono::DateTime::parse_from_rfc3339(date_str)
329 + .or_else(|_| chrono::DateTime::parse_from_rfc2822(date_str))
330 + .map(|dt| dt.timestamp())
331 + .map_err(|e| format!("Date parse error: {}", e).into())
332 + },
333 + );
308 334
309 - // Convert HTML to readable markdown.
310 - engine.register_fn("html_to_text", |html: &str| -> String {
311 - pter::convert(html)
335 + // Parse string to integer. Returns Dynamic::UNIT (Rhai's nil) on failure
336 + // instead of Option<i64> because Rhai doesn't natively handle Rust Options.
337 + engine.register_fn("parse_int", |text: &str| -> Dynamic {
338 + match text.parse::<i64>() {
339 + Ok(n) => n.into(),
340 + Err(_) => Dynamic::UNIT,
341 + }
312 342 });
343 + }
313 344
345 + /// Register the string-manipulation builtins (`truncate`, `str_contains`, `str_split`,
346 + /// `str_replace`, `str_trim`).
347 + fn register_string_fns(engine: &mut Engine) {
314 348 // Truncate text with ellipsis (character-count aware for multibyte UTF-8)
315 349 engine.register_fn("truncate", |text: &str, max_len: i64| -> String {
316 350 let max = max_len.max(0) as usize;
@@ -342,17 +376,18 @@ pub(super) fn register_host_functions(
342 376
343 377 // String trim
344 378 engine.register_fn("str_trim", |text: &str| -> String { text.trim().to_string() });
379 + }
345 380
346 - // Parse ISO 8601 date to timestamp
347 - engine.register_fn(
348 - "parse_datetime",
349 - |date_str: &str| -> Result<i64, Box<rhai::EvalAltResult>> {
350 - chrono::DateTime::parse_from_rfc3339(date_str)
351 - .or_else(|_| chrono::DateTime::parse_from_rfc2822(date_str))
352 - .map(|dt| dt.timestamp())
353 - .map_err(|e| format!("Date parse error: {}", e).into())
354 - },
355 - );
381 + /// Register the remaining utility builtins (`timestamp_now`, `html_to_text`,
382 + /// `debug_print`, `strip_tracking`, `extract_article`).
383 + fn register_util_fns(engine: &mut Engine) {
384 + // Current UTC timestamp as Unix epoch seconds (i64).
385 + engine.register_fn("timestamp_now", || -> i64 { chrono::Utc::now().timestamp() });
386 +
387 + // Convert HTML to readable markdown.
388 + engine.register_fn("html_to_text", |html: &str| -> String {
389 + pter::convert(html)
390 + });
356 391
357 392 // Debug print — outputs to tracing at debug level, visible in dev console.
358 393 engine.register_fn("debug_print", |val: Dynamic| {
@@ -364,15 +399,33 @@ pub(super) fn register_host_functions(
364 399 url_cleaner::strip_tracking_params(url)
365 400 });
366 401
367 - // Parse string to integer. Returns Dynamic::UNIT (Rhai's nil) on failure
368 - // instead of Option<i64> because Rhai doesn't natively handle Rust Options.
369 - engine.register_fn("parse_int", |text: &str| -> Dynamic {
370 - match text.parse::<i64>() {
371 - Ok(n) => n.into(),
372 - Err(_) => Dynamic::UNIT,
373 - }
402 + // Extract main article content from HTML using the readability algorithm.
403 + // Returns a map with "title", "content" (cleaned HTML), and "text" (plain text).
404 + engine.register_fn("extract_article", |html: String| -> rhai::Map {
405 + let mut map = rhai::Map::new();
406 + let mut readability = readable_readability::Readability::new();
407 + let (node, metadata) = readability.parse(&html);
408 +
409 + // Serialize the extracted DOM node to an HTML string.
410 + let mut content = Vec::new();
411 + node.serialize(&mut content).unwrap_or_default();
412 + let content_html = String::from_utf8_lossy(&content).to_string();
413 +
414 + // Readable markdown via pter
415 + let text = pter::convert(&content_html);
416 +
417 + map.insert(
418 + "title".into(),
419 + Dynamic::from(metadata.article_title.unwrap_or_default()),
420 + );
421 + map.insert("content".into(), Dynamic::from(content_html));
422 + map.insert("text".into(), Dynamic::from(text));
423 + map
374 424 });
425 + }
375 426
427 + /// Register the structured-error signaling builtins (`throw_*`).
428 + fn register_error_fns(engine: &mut Engine) {
376 429 // ── Structured error signaling ──────────────────────────────────
377 430 // Plugins can throw categorized errors that the host recognizes.
378 431
@@ -403,32 +456,9 @@ pub(super) fn register_host_functions(
403 456 Err(format!("{BB_ERR_PREFIX}parse:{msg}").into())
404 457 },
405 458 );
406 -
407 - // Extract main article content from HTML using the readability algorithm.
408 - // Returns a map with "title", "content" (cleaned HTML), and "text" (plain text).
409 - engine.register_fn("extract_article", |html: String| -> rhai::Map {
410 - let mut map = rhai::Map::new();
411 - let mut readability = readable_readability::Readability::new();
412 - let (node, metadata) = readability.parse(&html);
413 -
414 - // Serialize the extracted DOM node to an HTML string.
415 - let mut content = Vec::new();
416 - node.serialize(&mut content).unwrap_or_default();
417 - let content_html = String::from_utf8_lossy(&content).to_string();
418 -
419 - // Readable markdown via pter
420 - let text = pter::convert(&content_html);
421 -
422 - map.insert(
423 - "title".into(),
424 - Dynamic::from(metadata.article_title.unwrap_or_default()),
425 - );
426 - map.insert("content".into(), Dynamic::from(content_html));
427 - map.insert("text".into(), Dynamic::from(text));
428 - map
429 - });
430 459 }
431 460
461 +
432 462 #[cfg(test)]
433 463 mod tests {
434 464 use std::sync::atomic::{AtomicUsize, Ordering};