diff --git a/.claude/rules/code-style.md b/.claude/rules/code-style.md index 897a65382f..ca765f24de 100644 --- a/.claude/rules/code-style.md +++ b/.claude/rules/code-style.md @@ -11,6 +11,30 @@ - Fields that are optional use the `maybe_` prefix: `maybe_creator_user_token`, `maybe_prompt_token` - When implementing `fmt::Display` for error types, use the pattern: `write!(f, "{:?}", self)` +## File Layout (Top-to-Bottom Reading Order) + +Organize code so a human reading top-to-bottom encounters the most important things first and +can drill down into details only when curious. Think of it like a newspaper: headline first, +then the story, then the fine print. + +### Ordering within a file + +1. **Constants** at the top, right after imports. +2. **Structs, enums, and type definitions** next. If a struct contains another struct, place + the outer/larger struct above the inner/smaller one. +3. **Type ordering for API types**: Request, then Response (success), then Error. +4. **`impl` blocks** after their type definitions. + +### Ordering within an `impl` block + +1. **Constructors** (`new`, `from_*`, `builder`) at the top. +2. **Public methods** next — these are the main entry points a reader cares about. +3. **Private helper methods** below public methods. + - Among helpers: place "meaty" helpers (substantial logic, direct continuations of the + public entry point) above "leaf" helpers (small formatters, simple lookups). + - A method that calls another method in the same file should appear *above* the method + it calls. This lets readers follow the call chain downward. + ## Naming - Handler functions: `{verb}_{noun}_handler` (e.g. `get_health_check_handler`, `login_handler`) diff --git a/CLAUDE.md b/CLAUDE.md index 96da01713a..4a32af226e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,3 +62,14 @@ artcraft/ - A mix of wreq and reqwest for Rust HTTP clients - TypeScript with Nx, React, Vite, Zustand, and Three.js - Use two spaces for indentation + +### File Layout + +Organize for top-to-bottom reading. Important things first, details later. + +- **Constants** at the top (after imports) +- **Structs/enums** next; outer structs above inner sub-structs +- **API types** in order: Request, Response, Error +- **In impl blocks**: constructors first, then public methods, then private helpers +- Private helpers go *below* the methods that call them +- Among helpers: meatier logic above leaf-level formatters diff --git a/crates/lib/pager/src/client/pager_client.rs b/crates/lib/pager/src/client/pager_client.rs index daf547a800..5f279f4d81 100644 --- a/crates/lib/pager/src/client/pager_client.rs +++ b/crates/lib/pager/src/client/pager_client.rs @@ -66,7 +66,11 @@ pub struct PageSentResult { pub short_id: Option, } +// =============== Implementation =============== + impl PagerClient { + // --- Constructor --- + pub fn new( client_config: PagerClientConfig, application_name: Option, @@ -77,45 +81,12 @@ impl PagerClient { Self { client_config, application_name, environment, hostname, service_id } } + // --- Public interface --- + pub fn is_noop(&self) -> bool { matches!(self.client_config, PagerClientConfig::NoOp) } - /// Build structured labels from the client config and notification fields. - fn build_labels(&self, notification: &NotificationDetails) -> Option> { - let mut labels: Vec<(String, String)> = Vec::new(); - - if let Some(name) = &self.application_name { - labels.push(("application_name".to_string(), name.clone())); - } - - if let Some(id) = &self.service_id { - labels.push(("service_id".to_string(), id.clone())); - } - - if let Some(env) = &self.environment { - labels.push(("environment".to_string(), env.clone())); - } - - if let Some(h) = &self.hostname { - labels.push(("hostname".to_string(), h.clone())); - } - - if let Some(method) = ¬ification.http_method { - labels.push(("http_method".to_string(), method.clone())); - } - - if let Some(path) = ¬ification.http_path { - labels.push(("http_path".to_string(), path.clone())); - } - - if let Some(status_code) = notification.http_status_code { - labels.push(("http_status_code".to_string(), status_code.to_string())); - } - - if labels.is_empty() { None } else { Some(labels) } - } - /// Send a page immediately. Returns `Ok(None)` for NoOp. pub async fn send_page(&self, notification: &NotificationDetails) -> Result, PagerError> { match &self.client_config { @@ -129,6 +100,8 @@ impl PagerClient { } } + // --- Private helpers (called by send_page) --- + async fn send_page_via_rootly( &self, notification: &NotificationDetails, @@ -150,10 +123,8 @@ impl PagerClient { debug!("Sending page via Rootly (source={}): {}", source, notification.title); - // Build labels for Rootly's structured metadata. let labels = self.build_labels(notification); - // Build the enriched description from the notification's atomic sections. let description = notification.build_enriched_description( self.application_name.as_deref(), self.service_id.as_deref(), @@ -199,4 +170,39 @@ impl PagerClient { } } } + + /// Build structured labels from the client config and notification fields. + fn build_labels(&self, notification: &NotificationDetails) -> Option> { + let mut labels: Vec<(String, String)> = Vec::new(); + + if let Some(name) = &self.application_name { + labels.push(("application_name".to_string(), name.clone())); + } + + if let Some(id) = &self.service_id { + labels.push(("service_id".to_string(), id.clone())); + } + + if let Some(env) = &self.environment { + labels.push(("environment".to_string(), env.clone())); + } + + if let Some(h) = &self.hostname { + labels.push(("hostname".to_string(), h.clone())); + } + + if let Some(method) = ¬ification.http_method { + labels.push(("http_method".to_string(), method.clone())); + } + + if let Some(path) = ¬ification.http_path { + labels.push(("http_path".to_string(), path.clone())); + } + + if let Some(status_code) = notification.http_status_code { + labels.push(("http_status_code".to_string(), status_code.to_string())); + } + + if labels.is_empty() { None } else { Some(labels) } + } }