{"language":"rust","files":[{"path":"Cargo.toml","content":"[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\npath = \"src/lib.rs\"\n\n[[bin]]\nname = \"app\"\npath = \"src/main.rs\"\n\n[dependencies]\naxum = { version = \"0.8\", features = [\"ws\"] }\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\", \"net\", \"sync\", \"time\"] }\ntower-http = { version = \"0.6\", features = [\"fs\"] }\nfutures-util = \"0.3\"\nrusqlite = { version = \"0.33\", features = [\"bundled\"] }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nbase64 = \"0.22\"\nregex = \"1\"\nchrono = { version = \"0.4\", features = [\"serde\"] }\n\n[dev-dependencies]\naxum-test = \"18\"\n# Temporary cap: time 0.3.48 (2026-06-12) added an impl that conflicts\n# (E0119) with cookie 0.18.1's blanket `From<T> for Expiration`; cookie\n# reaches this tree only via axum-test. Drop the cap once cookie ships\n# a fix or time yanks/repairs 0.3.48.\ntime = \">=0.3, <0.3.48\"\n"},{"path":"README.md","content":"# Roundhouse → rust\n\nTranspiled from a Rails source app by [Roundhouse](https://rubys.github.io/roundhouse/).\n\nRunning it serves the blog on http://localhost:3000 (set `PORT` to override), with live Turbo Stream updates over the `/cable` WebSocket.\n\n## Prerequisites\n- Rust 1.85+ (`cargo`)\n- SQLite (system library)\n\n## Build\n```sh\ncargo build --release\n```\n\n## Setup\nSeed the database — the Rails-traditional `storage/development.sqlite3`, from the bundled `db/seed.sql` (needs the `sqlite3` CLI):\n```sh\nsqlite3 storage/development.sqlite3 < db/seed.sql\n```\n\n## Run\n```sh\n./target/release/app\n```\n\n## Test\n```sh\ncargo test\n```\n\n## End-to-end\nBrowser smoke tests (Playwright). Needs Node.js 18+ and the `sqlite3` CLI; run after the Build steps above — the test config boots the server and seeds `db/seed.sql` itself:\n```sh\ncd e2e\nnpm install\nnpx playwright install chromium\nnpx playwright test\n```\n\n## Regenerate\n```sh\nroundhouse --target rust -o <output-dir> <input-app>\n```\n"},{"path":"db/seed.sql","content":"-- Seed data, generated from the app's own db/seeds.rb.\n-- The archive is text-only, so no binary database ships; this file\n-- populates a fresh one for any target: `sqlite3 <db> < db/seed.sql`.\n-- INSERTs name their columns explicitly so the file stays valid whatever\n-- order a target emits its schema columns in.\n-- created_at/updated_at are synthesized (seeds.rb sets neither, and the\n-- columns are NOT NULL): a fixed base instant plus one microsecond per\n-- row, in creation order, so ordering by them is deterministic.\nCREATE TABLE IF NOT EXISTS articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, body TEXT, created_at TEXT NOT NULL, title TEXT, updated_at TEXT NOT NULL);\nCREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, article_id INTEGER NOT NULL, body TEXT, commenter TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);\nCREATE INDEX IF NOT EXISTS index_comments_on_article_id ON comments (article_id);\nINSERT INTO articles (id, title, body, created_at, updated_at) VALUES (1,'Getting Started with Rails','Rails is a web application framework running on the Ruby programming language. It makes building web apps faster and easier with conventions over configuration.','2026-01-01 00:00:00.000001','2026-01-01 00:00:00.000001');\nINSERT INTO comments (id, article_id, commenter, body, created_at, updated_at) VALUES (1,1,'Alice','Great introduction! Rails really does make development faster.','2026-01-01 00:00:00.000002','2026-01-01 00:00:00.000002');\nINSERT INTO comments (id, article_id, commenter, body, created_at, updated_at) VALUES (2,1,'Bob','I love how Rails handles database migrations automatically.','2026-01-01 00:00:00.000003','2026-01-01 00:00:00.000003');\nINSERT INTO articles (id, title, body, created_at, updated_at) VALUES (2,'Understanding MVC Architecture','MVC stands for Model-View-Controller. Models handle data and business logic, Views display information to users, and Controllers coordinate between them.','2026-01-01 00:00:00.000004','2026-01-01 00:00:00.000004');\nINSERT INTO comments (id, article_id, commenter, body, created_at, updated_at) VALUES (3,2,'Carol','This pattern really helps keep code organized!','2026-01-01 00:00:00.000005','2026-01-01 00:00:00.000005');\nINSERT INTO articles (id, title, body, created_at, updated_at) VALUES (3,'Ruby2JS: Rails Everywhere','Ruby2JS transpiles Ruby to JavaScript, enabling Rails applications to run in browsers, on Node.js, and at the edge. Same code, different runtimes.','2026-01-01 00:00:00.000006','2026-01-01 00:00:00.000006');\n"},{"path":"e2e/action_cable.spec.js","content":"import { test, expect } from '@playwright/test'\n\n// article_3 is seeded with zero comments. Assertions are scoped to *our*\n// uniquely-worded comment so this can run in parallel with the Turbo Stream\n// test, which also posts comments on the same article.\nconst ARTICLE_PATH = '/articles/3'\nconst COMMENTER = 'Cable Bot'\nconst BODY = 'Action Cable broadcast smoke-test comment'\n\ntest('a new comment broadcasts live to other viewers via Action Cable', async ({ browser }) => {\n  // Two independent contexts = two separate viewers of the same article.\n  const observerCtx = await browser.newContext()\n  const actorCtx = await browser.newContext()\n  const observer = await observerCtx.newPage()\n  const actor = await actorCtx.newPage()\n\n  const observerRow = observer.locator('#comments > div').filter({ hasText: BODY })\n  const actorRow = actor.locator('#comments > div').filter({ hasText: BODY })\n\n  try {\n    await observer.goto(ARTICLE_PATH)\n    await actor.goto(ARTICLE_PATH)\n    await expect(observerRow).toHaveCount(0)\n\n    // Wait until the observer's Turbo Stream subscription is confirmed, so the\n    // broadcast can't be missed by a not-yet-connected websocket.\n    await expect(observer.locator('turbo-cable-stream-source')).toHaveAttribute('connected', '')\n\n    // The observer must never navigate; this marker proves the row arrives over\n    // the websocket rather than via a reload.\n    await observer.evaluate(() => { window.__noNav = true })\n\n    // The actor submits a comment.\n    await actor.getByLabel('Commenter').fill(COMMENTER)\n    await actor.getByLabel('Body').fill(BODY)\n    await actor.getByRole('button', { name: 'Add Comment' }).click()\n\n    // The observer receives it live, exactly once, without having navigated.\n    await expect(observerRow).toHaveCount(1)\n    await expect(observerRow).toBeVisible()\n    expect(await observer.evaluate(() => window.__noNav)).toBe(true)\n\n    // Cleanup: delete the comment from the actor page (accept the Turbo confirm).\n    actor.on('dialog', dialog => dialog.accept())\n    await actorRow.getByRole('button', { name: 'Delete' }).click()\n    await expect(actorRow).toHaveCount(0)\n\n    // The removal broadcasts too — the observer's row disappears, leaving no residue.\n    await expect(observerRow).toHaveCount(0)\n  } finally {\n    await observerCtx.close()\n    await actorCtx.close()\n  }\n})\n"},{"path":"e2e/flash.spec.js","content":"import { test, expect } from '@playwright/test'\n\n// Flash shows exactly once, then is swept. Trigger a `redirect_to … notice:`\n// → the redirect target renders the \"successfully updated\" notice → a later\n// navigation must NOT show it again. The SWEEP (second assertion) is the\n// regression that matters: a sticky flash re-renders the notice on every\n// page (the bug that motivated moving the sweep into ActionDispatch::Flash).\n//\n// Why EDIT (not create): the suite runs `fullyParallel` against ONE shared\n// server + DB, and `index.spec.js` pins the seeded set to exactly three\n// articles with specific titles + comment counts. Creating an article would\n// race that exact-count assertion; editing an existing one keeps the count,\n// every title, and every comment count fixed — only the body (which no spec\n// asserts) changes — so flash.spec stays isolated from its neighbours. The\n// sweep is checked by reloading the article's own show page, not the index,\n// to steer clear of index.spec entirely.\n//\n// Scoping: runs on every target. All targets now back flash with a\n// per-session `rh_flash` cookie (HttpOnly, cleared when empty), so the\n// sweep is isolated per browser and no longer races the comment specs'\n// `redirect_to … notice:` under `fullyParallel` — no target sets E2E_SKIP\n// for flash. (History: crystal/typescript once shared one global in-memory\n// flash slot and were skipped here; that was removed when they gained\n// cookie-backed per-session flash, matching go/rust/kotlin/swift/etc.)\ntest('flash notice shows once then is swept', async ({ page }) => {\n  await page.goto('/articles/1/edit')\n\n  // Keep the title (index.spec pins it to this exact value); only the body\n  // changes, which no spec asserts. An update with a valid body redirects\n  // with the notice regardless of whether any field actually changed.\n  await page.getByLabel('Title').fill('Getting Started with Rails')\n  await page.getByLabel('Body').fill('Edited by the flash e2e spec to exercise the show-once sweep.')\n  await page.getByRole('button', { name: 'Update Article' }).click()\n\n  // Redirected to the article — the notice renders once.\n  await expect(page.locator('#notice')).toHaveText('Article was successfully updated.')\n\n  // Navigate again — the notice must be gone (swept), not sticky.\n  await page.goto('/articles/1')\n  await expect(page.locator('#notice')).toHaveCount(0)\n})\n"},{"path":"e2e/index.spec.js","content":"import { test, expect } from '@playwright/test'\n\n// The seeded database renders three articles, newest-first (prepend order).\n// Each entry pins the DOM id, the title link text, and the comment-count\n// label shown beside the title.\nconst expectedArticles = [\n  { id: 'article_3', title: 'Ruby2JS: Rails Everywhere',      comments: '(0 comments)' },\n  { id: 'article_2', title: 'Understanding MVC Architecture', comments: '(1 comment)' },\n  { id: 'article_1', title: 'Getting Started with Rails',     comments: '(2 comments)' },\n]\n\ntest('index lists the three seeded articles in order with correct comment counts', async ({ page }) => {\n  await page.goto('/')\n\n  // Exactly three articles render in the list.\n  const rows = page.locator('#articles > div')\n  await expect(rows).toHaveCount(expectedArticles.length)\n\n  // Index positionally so the displayed order is asserted, not just presence.\n  for (let i = 0; i < expectedArticles.length; i++) {\n    const { id, title, comments } = expectedArticles[i]\n    const row = rows.nth(i)\n    await expect(row).toHaveAttribute('id', id)\n    await expect(row.locator('h2 a')).toHaveText(title)\n    await expect(row.locator(`#comments_count_${id}`)).toHaveText(comments)\n  }\n})\n"},{"path":"e2e/package.json","content":"{\n  \"name\": \"app-e2e\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"description\": \"Playwright end-to-end smoke tests for this archive — see ../README.md\",\n  \"scripts\": {\n    \"test\": \"playwright test\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.49.0\"\n  }\n}\n"},{"path":"e2e/playwright.config.js","content":"import { defineConfig, devices } from '@playwright/test'\n\n// Generated by Roundhouse. Self-contained: `webServer` boots the app\n// (built per ../README.md) and global-setup.js seeds ../db/seed.sql.\n// E2E_SKIP is a space/comma list of spec basenames to skip.\nconst SKIP = (process.env.E2E_SKIP || '').split(/[\\s,]+/).filter(Boolean)\n\nexport default defineConfig({\n  testDir: '.',\n  testIgnore: SKIP.map(name => `**/${name}*.spec.js`),\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  reporter: process.env.CI ? [['github'], ['list']] : 'list',\n  use: {\n    baseURL: 'http://localhost:3000',\n    trace: 'on-first-retry',\n  },\n  // seed.js runs INSIDE the webServer command (not globalSetup —\n  // Playwright boots the webServer first) so the server opens an\n  // already-seeded DB.\n  webServer: {\n    command: 'node e2e/seed.js && ./target/release/app',\n    cwd: '..',\n    url: 'http://localhost:3000/articles',\n    reuseExistingServer: !process.env.CI,\n    timeout: 120_000,\n  },\n  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],\n})\n"},{"path":"e2e/seed.js","content":"// Generated by Roundhouse. Seeds the server's DB from ../db/seed.sql\n// (sqlite3 CLI). Runs as the first half of playwright.config.js's\n// webServer command, so the server boots against an already-seeded\n// DB (some targets self-seed demo data on an empty one, with\n// different row timestamps than the canonical seed). Idempotent:\n// skips when articles already exist, so re-runs don't double-seed.\n// For a truly fresh run, delete storage/development.sqlite3 (or re-extract the archive).\nimport { execFileSync } from 'node:child_process'\nimport { mkdirSync, readFileSync } from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')\nconst db = path.join(root, 'storage/development.sqlite3')\nconst seed = path.join(root, 'db', 'seed.sql')\n\nmkdirSync(path.dirname(db), { recursive: true })\nlet count = 0\ntry {\n  count = Number(execFileSync('sqlite3', [db, 'SELECT COUNT(*) FROM articles'],\n    { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim())\n} catch { /* missing file or table — seed below */ }\nif (count === 0) {\n  execFileSync('sqlite3', [db], { input: readFileSync(seed, 'utf8') })\n  console.log(`seed.js: seeded ${db} from db/seed.sql`)\n} else {\n  console.log(`seed.js: db already seeded (${count} articles)`)\n}\n"},{"path":"e2e/tailwind.spec.js","content":"import { test, expect } from '@playwright/test'\n\ntest('tailwind is compiled and applied', async ({ page }) => {\n  await page.goto('/')\n\n  // Stronger check: the compiled stylesheet actually serves (catches a broken\n  // asset pipeline directly). Propshaft fingerprints the filename, so derive\n  // the digested href from the page instead of hardcoding it.\n  const hrefs = await page\n    .locator('link[rel=\"stylesheet\"]')\n    .evaluateAll(links => links.map(l => l.getAttribute('href')))\n  const tailwindHref = hrefs.find(h => /tailwind/.test(h)) ?? hrefs[0]\n  expect(tailwindHref, 'expected a stylesheet <link> on the page').toBeTruthy()\n\n  const res = await page.request.get(tailwindHref)\n  expect(res.status()).toBe(200)\n  expect(res.headers()['content-type']).toContain('css')\n\n  // The \"New article\" button (an <a> styled via Tailwind utilities) is the probe.\n  const button = page.getByRole('link', { name: 'New article' })\n  const s = await button.evaluate(el => {\n    const c = getComputedStyle(el)\n    return { display: c.display, background: c.backgroundColor,\n             radius: c.borderRadius, padding: c.paddingTop }\n  })\n\n  // Utilities applied, not browser defaults (an unstyled <a> would be\n  // display:inline, transparent background, no radius, no padding):\n  expect(s.display).toBe('block')                    // `block`\n  expect(s.radius).toBe('6px')                       // `rounded-md`\n  expect(s.padding).toBe('10px')                     // `py-2.5`\n  expect(s.background).not.toBe('rgba(0, 0, 0, 0)')  // not transparent\n\n  // The distinctive color token resolved (bg-blue-600):\n  expect(s.background).toBe('oklch(0.546 0.245 262.881)')\n})\n"},{"path":"e2e/turbo_comment.spec.js","content":"import { test, expect } from '@playwright/test'\n\n// article_3 (\"Ruby2JS: Rails Everywhere\") is seeded with zero comments. The\n// assertions are scoped to *our* uniquely-worded comment (not the whole list)\n// so this test can run in parallel with the Action Cable test, which also\n// posts comments on the same article.\nconst ARTICLE_PATH = '/articles/3'\nconst COMMENTER = 'Playwright Bot'\nconst BODY = 'Turbo stream smoke-test comment'\n\ntest('adding a comment shows the new row via Turbo without a full reload', async ({ page }) => {\n  await page.goto(ARTICLE_PATH)\n\n  const ours = page.locator('#comments > div').filter({ hasText: BODY })\n  await expect(ours).toHaveCount(0) // not present yet\n\n  // Marker on window: a Turbo Drive visit preserves it; a full page reload wipes it.\n  await page.evaluate(() => { window.__noFullReload = true })\n\n  await page.getByLabel('Commenter').fill(COMMENTER)\n  await page.getByLabel('Body').fill(BODY)\n  await page.getByRole('button', { name: 'Add Comment' }).click()\n\n  // Our new comment row appears exactly once (no redirect/broadcast duplicate).\n  await expect(ours).toHaveCount(1)\n  await expect(ours).toBeVisible()\n\n  // Turbo handled the redirect as a Drive visit, not a full browser reload.\n  expect(await page.evaluate(() => window.__noFullReload)).toBe(true)\n\n  // Cleanup: delete the comment we added, accepting the Turbo confirm dialog.\n  page.on('dialog', dialog => dialog.accept())\n  await ours.getByRole('button', { name: 'Delete' }).click()\n  await expect(ours).toHaveCount(0) // gone — no residue left behind\n})\n"},{"path":"e2e/validation.spec.js","content":"import { test, expect } from '@playwright/test'\n\ntest('new article form shows a validation error for a too-short body', async ({ page }) => {\n  await page.goto('/articles/new')\n\n  // Title only needs presence; body must be at least 10 chars. A short body\n  // (9 chars) trips the length validation, so create re-renders :new (422).\n  await page.getByLabel('Title').fill('Hi')\n  await page.getByLabel('Body').fill('too short')\n\n  await page.getByRole('button', { name: 'Create Article' }).click()\n\n  // The error summary appears with the specific failure message.\n  const errors = page.locator('#error_explanation')\n  await expect(errors).toBeVisible()\n  await expect(errors).toContainText('prohibited this article from being saved')\n  await expect(errors).toContainText('Body is too short (minimum is 10 characters)')\n})\n"},{"path":"src/action_controller_base.rs","content":"// Generated from runtime/ruby/action_controller/base.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\nuse crate::flash::Flash;\nuse crate::session::Session;\nuse crate::param_value::ParamValue;\nuse crate::errors_ext::raise;\nuse crate::errors_ext::NotImplementedError;\n\nstatic STATUS_CODES: std::sync::LazyLock<std::collections::HashMap<&'static str, i64>> = std::sync::LazyLock::new(|| std::collections::HashMap::from([(\"continue\", 100_i64), (\"switching_protocols\", 101_i64), (\"processing\", 102_i64), (\"early_hints\", 103_i64), (\"ok\", 200_i64), (\"created\", 201_i64), (\"accepted\", 202_i64), (\"non_authoritative_information\", 203_i64), (\"no_content\", 204_i64), (\"reset_content\", 205_i64), (\"partial_content\", 206_i64), (\"multi_status\", 207_i64), (\"already_reported\", 208_i64), (\"im_used\", 226_i64), (\"multiple_choices\", 300_i64), (\"moved_permanently\", 301_i64), (\"found\", 302_i64), (\"see_other\", 303_i64), (\"not_modified\", 304_i64), (\"use_proxy\", 305_i64), (\"temporary_redirect\", 307_i64), (\"permanent_redirect\", 308_i64), (\"bad_request\", 400_i64), (\"unauthorized\", 401_i64), (\"payment_required\", 402_i64), (\"forbidden\", 403_i64), (\"not_found\", 404_i64), (\"method_not_allowed\", 405_i64), (\"not_acceptable\", 406_i64), (\"proxy_authentication_required\", 407_i64), (\"request_timeout\", 408_i64), (\"conflict\", 409_i64), (\"gone\", 410_i64), (\"length_required\", 411_i64), (\"precondition_failed\", 412_i64), (\"payload_too_large\", 413_i64), (\"content_too_large\", 413_i64), (\"uri_too_long\", 414_i64), (\"unsupported_media_type\", 415_i64), (\"range_not_satisfiable\", 416_i64), (\"expectation_failed\", 417_i64), (\"misdirected_request\", 421_i64), (\"unprocessable_entity\", 422_i64), (\"unprocessable_content\", 422_i64), (\"locked\", 423_i64), (\"failed_dependency\", 424_i64), (\"too_early\", 425_i64), (\"upgrade_required\", 426_i64), (\"precondition_required\", 428_i64), (\"too_many_requests\", 429_i64), (\"request_header_fields_too_large\", 431_i64), (\"unavailable_for_legal_reasons\", 451_i64), (\"internal_server_error\", 500_i64), (\"not_implemented\", 501_i64), (\"bad_gateway\", 502_i64), (\"service_unavailable\", 503_i64), (\"gateway_timeout\", 504_i64), (\"http_version_not_supported\", 505_i64), (\"variant_also_negotiates\", 506_i64), (\"insufficient_storage\", 507_i64), (\"loop_detected\", 508_i64), (\"bandwidth_limit_exceeded\", 509_i64), (\"not_extended\", 510_i64), (\"network_authentication_required\", 511_i64)]));\n\n#[derive(Clone, Default)]\npub struct Base {\n    pub params: std::collections::HashMap<String, ParamValue>,\n    pub session: Session,\n    pub flash: Flash,\n    pub request_method: String,\n    pub request_path: String,\n    pub request_format: String,\n    pub status: i64,\n    pub body: String,\n    pub location: Option<String>,\n    pub content_type: String,\n    pub headers: std::collections::HashMap<String, String>,\n    pub performed: bool,\n    pub cache_control_max_age: i64,\n    pub cache_control_public: bool,\n}\n\nimpl Base {\n    pub fn params(&self) -> std::collections::HashMap<String, ParamValue> {\n        self.params.clone()\n    }\n\n    pub fn set_params(&mut self, value: std::collections::HashMap<String, ParamValue>) {\n        self.params = value\n    }\n\n    pub fn session(&self) -> Session {\n        self.session.clone()\n    }\n\n    pub fn set_session(&mut self, value: Session) {\n        self.session = value\n    }\n\n    pub fn flash(&self) -> Flash {\n        self.flash.clone()\n    }\n\n    pub fn set_flash(&mut self, value: Flash) {\n        self.flash = value\n    }\n\n    pub fn request_method(&self) -> String {\n        self.request_method.clone()\n    }\n\n    pub fn set_request_method(&mut self, value: &str) {\n        self.request_method = (value).to_string()\n    }\n\n    pub fn request_path(&self) -> String {\n        self.request_path.clone()\n    }\n\n    pub fn set_request_path(&mut self, value: &str) {\n        self.request_path = (value).to_string()\n    }\n\n    pub fn request_format(&self) -> String {\n        self.request_format.clone()\n    }\n\n    pub fn set_request_format(&mut self, value: &str) {\n        self.request_format = (value).to_string()\n    }\n\n    pub fn status(&self) -> i64 {\n        self.status.clone()\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn location(&self) -> Option<String> {\n        self.location.clone()\n    }\n\n    pub fn content_type(&self) -> String {\n        self.content_type.clone()\n    }\n\n    pub fn cache_control_max_age(&self) -> i64 {\n        self.cache_control_max_age.clone()\n    }\n\n    pub fn cache_control_public(&self) -> bool {\n        self.cache_control_public.clone()\n    }\n\n    pub fn new() -> Self {\n        let mut params: std::collections::HashMap<String, ParamValue> = std::collections::HashMap::new();\n        let mut session: Session = Session::new();\n        let mut flash: Flash = Flash::new();\n        let mut status: i64 = 200_i64;\n        let mut body: String = (\"\").to_string();\n        let mut location: Option<String> = None;\n        let mut request_format: String = \"html\".to_string();\n        let mut content_type: String = (\"text/html; charset=utf-8\").to_string();\n        let mut headers: std::collections::HashMap<String, String> = std::collections::HashMap::new();\n        let mut performed: bool = false;\n        let mut cache_control_max_age: i64 = 0_i64;\n        let mut cache_control_public: bool = false;\n        let request_method: String = String::new();\n        let request_path: String = String::new();\n        Self { params, session, flash, request_method, request_path, request_format, status, body, location, content_type, headers, performed, cache_control_max_age, cache_control_public }\n    }\n\n    pub fn performed_pred(&self) -> bool {\n        self.performed.clone()\n    }\n\n    pub fn reset_session(&mut self) -> Session {\n        self.session = Session::new();\n        self.session.clone()\n    }\n\n    pub fn assign_http_session(&mut self, value: Session) -> Session {\n        self.session = value;\n        self.session.clone()\n    }\n\n    pub fn process_action(&self, _action_name: &str) {\n        raise(NotImplementedError, \"process_action must be overridden by subclass\");\n    }\n\n    pub fn render(&mut self, body: &str, status: &str, content_type: Option<String>, location: Option<String>) {\n        self.body = (body).to_string();\n        self.status = Self::resolve_status(&(status));\n        self.performed = true;\n        if !(content_type.is_none()) { self.content_type = (content_type.clone().unwrap()).to_string() };\n        if !(location.is_none()) { self.location = Some((location.clone().unwrap()).to_string()) };\n    }\n\n    pub fn redirect_to(&mut self, path: &str, notice: Option<String>, alert: Option<String>, status: &str) {\n        self.location = Some((path).to_string());\n        self.status = Self::resolve_status(&(status));\n        self.performed = true;\n        if !(notice.is_none()) { self.flash.set(\"notice\", Some(notice.clone().unwrap())) };\n        if !(alert.is_none()) { self.flash.set(\"alert\", Some(alert.clone().unwrap())) };\n    }\n\n    pub fn head(&mut self, status: &str, content_type: Option<String>, location: Option<String>) {\n        if !(location.is_none()) { self.location = Some((location.clone().unwrap()).to_string()) };\n        self.status = Self::resolve_status(&(status));\n        self.body = (\"\").to_string();\n        self.performed = true;\n        if !(content_type.is_none()) { self.content_type = (content_type.clone().unwrap()).to_string() };\n    }\n\n    pub fn response(&self) -> Base {\n        self.clone()\n    }\n\n    pub fn fresh_when(subject: serde_json::Value) {\n    }\n\n    pub fn stale_pred(etag: serde_json::Value) -> bool {\n        true\n    }\n\n    pub fn expires_in(&mut self, seconds: i64, public: bool, stale_while_revalidate: i64) {\n        self.cache_control_max_age = seconds;\n        self.cache_control_public = public;\n    }\n\n    pub fn headers(&self) -> std::collections::HashMap<String, String> {\n        self.headers.clone()\n    }\n\n    pub fn send_data(&mut self, data: &str, r#type: &str, disposition: &str) {\n        self.body = (data).to_string();\n        self.content_type = (r#type).to_string();\n        self.performed = true;\n    }\n\n    pub fn resolve_status(s: &str) -> i64 {\n        if !(STATUS_CODES.contains_key(s.clone())) { panic!(\"{}\", format!(\"Invalid HTTP status: {}\", s.clone())) };\n        STATUS_CODES.get(s.clone()).cloned().unwrap_or(200_i64)\n    }\n}\n\n#[derive(Clone, Default)]\npub struct ActionController {\n}\n\nimpl ActionController {\n}\n"},{"path":"src/active_record_adapter.rs","content":"//! Abstract `ActiveRecordAdapter` trait — the rust analog of\n//! crystal's `abstract class ActiveRecordAdapter` and TS's\n//! `interface ActiveRecordAdapter`. Hand-written for Phase 3.\n//!\n//! The 9-method contract `runtime/ruby/active_record/base.rb` calls\n//! against `ActiveRecord.adapter`. Every concrete adapter (production\n//! sqlite, in-memory framework-test, future libsql/D1) implements it.\n//!\n//! Return shapes are `serde_json::Value` because the abstract slot is\n//! polymorphic — concrete adapters produce concrete row types\n//! (`HashMap<String, rusqlite::Value>` for sqlite, an in-memory\n//! `TestRow` for the framework-test adapter), and the only common\n//! surface is the untyped JSON tree. The transpiled `Base` methods\n//! that call into the adapter feed the result through\n//! `instantiate(row)` which subclasses override with concrete-typed\n//! per-column extraction.\n\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n// Row shape: `HashMap<String, Value>` (not `Value`). The transpiled\n// `runtime/ruby/active_record/base.rb` types rows as\n// `Hash[String, untyped]` and feeds them to `Self::instantiate(row)`\n// whose RBS signature is `(Hash[String, untyped]) -> Base`. Returning\n// a bare `Value` would force the transpile to insert an `as_object`-\n// style coercion at every call site — handing the trait the\n// pre-shaped HashMap keeps the body-typer's view aligned with what\n// the runtime delivers.\n//\n// Parameters use owned `String` (not `&str`) so transpiled call sites\n// can pass the result of `Self::table_name() -> String` directly.\n// Rust idiom would prefer `&str`, but emit-side auto-borrow at every\n// call site is a much wider change than the one-time alloc cost here.\npub type Row = HashMap<String, Value>;\n\npub trait ActiveRecordAdapter: Send + Sync {\n    fn all(&self, table_name: String) -> Vec<Row>;\n    fn find(&self, table_name: String, id: i64) -> Option<Row>;\n    fn r#where(&self, table_name: String, conditions: HashMap<String, Value>) -> Vec<Row>;\n    fn count(&self, table_name: String) -> i64;\n    fn exists_pred(&self, table_name: String, id: i64) -> bool;\n    fn insert(&self, table_name: String, attributes: HashMap<String, Value>) -> i64;\n    fn update(&self, table_name: String, id: i64, attributes: HashMap<String, Value>);\n    fn delete(&self, table_name: String, id: i64);\n    fn delete_all(&self, table_name: String);\n    fn truncate(&self, table_name: String);\n}\n"},{"path":"src/active_record_base.rs","content":"// Generated from runtime/ruby/active_record/base.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\nuse crate::active_record_adapter::ActiveRecordAdapter;\nuse crate::adapter_interface::AdapterInterface;\nuse crate::errors_ext::raise;\nuse crate::errors_ext::name;\nuse crate::errors_ext::NotImplementedError;\nuse crate::errors_ext::RecordNotFound;\nuse crate::errors_ext::RecordInvalid;\n\n#[derive(Clone, Default)]\npub struct Base {\n    pub id: i64,\n    pub errors: Vec<String>,\n    pub id_previously_changed: bool,\n    pub persisted: bool,\n    pub destroyed: bool,\n}\n\nimpl Base {\n    pub fn id(&self) -> i64 {\n        self.id.clone()\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn errors(&self) -> Vec<String> {\n        self.errors.clone()\n    }\n\n    pub fn new(_attrs: std::collections::HashMap<String, serde_json::Value>) -> Self {\n        let mut id: i64 = 0_i64;\n        let mut errors: Vec<String> = vec![];\n        let mut id_previously_changed: bool = false;\n        let mut persisted: bool = false;\n        let mut destroyed: bool = false;\n        Self { id, errors, id_previously_changed, persisted, destroyed }\n    }\n\n    pub fn table_name() -> String {\n        raise(NotImplementedError, format!(\"{}.table_name must be overridden\", name()))\n    }\n\n    pub fn schema_columns() -> Vec<String> {\n        raise(NotImplementedError, format!(\"{}.schema_columns must be overridden\", name()))\n    }\n\n    pub fn primary_key() -> String {\n        (\"id\").to_string()\n    }\n\n    pub fn schema_time_columns() -> Vec<String> {\n        vec![]\n    }\n\n    pub fn instantiate(_row: std::collections::HashMap<String, serde_json::Value>) -> Base {\n        raise(NotImplementedError, format!(\"{}.instantiate must be overridden\", name()))\n    }\n\n    pub fn preload_associations(_records: Vec<serde_json::Value>, _specs: Vec<serde_json::Value>) {\n    }\n\n    pub fn _adapter_find_by_id(id: i64) -> Option<Base> {\n        let Some(row) = ActiveRecord::adapter().find(Self::table_name(), id) else { return None };\n        Some(Self::instantiate(row.clone().clone()))\n    }\n\n    pub fn _adapter_all() -> Vec<Base> {\n        ActiveRecord::adapter().all(Self::table_name()).into_iter().map(|row| { Self::instantiate(row.clone()) }).collect::<Vec<_>>()\n    }\n\n    pub fn _adapter_last() -> Option<Base> {\n        let mut records = Self::_adapter_all();\n        if !(records.is_empty()) { Some(records.clone()[records.clone().len() - 1_usize].clone()) } else { None }\n    }\n\n    pub fn _adapter_insert(&self) -> i64 {\n        raise(NotImplementedError, \"_adapter_insert: subclasses must override\")\n    }\n\n    pub fn _adapter_update() {\n    }\n\n    pub fn _adapter_delete() {\n    }\n\n    pub fn _adapter_count() -> i64 {\n        ActiveRecord::adapter().count(Self::table_name())\n    }\n\n    pub fn _adapter_exists_by_id_pred(id: i64) -> bool {\n        ActiveRecord::adapter().exists_pred(Self::table_name(), id)\n    }\n\n    pub fn _adapter_truncate() {\n        ActiveRecord::adapter().truncate(Self::table_name())\n    }\n\n    pub fn _adapter_reload() -> Option<Base> {\n        None\n    }\n\n    pub fn attributes() -> std::collections::HashMap<String, serde_json::Value> {\n        std::collections::HashMap::new()\n    }\n\n    pub fn get_index(&self, _name: &str) -> serde_json::Value {\n        raise(NotImplementedError, \"[] must be overridden by subclass\")\n    }\n\n    pub fn set_index(&self, _name: &str, _value: serde_json::Value) {\n        raise(NotImplementedError, \"[]= must be overridden by subclass\")\n    }\n\n    pub fn has_attribute_pred(&self, name: &str) -> bool {\n        let mut cols = Self::schema_columns();\n        cols.iter().any(|__c| __c == name)\n    }\n\n    pub fn assign_from_row(_row: std::collections::HashMap<String, serde_json::Value>) {\n    }\n\n    pub fn dom_prefix(&self) -> String {\n        raise(NotImplementedError, \"dom_prefix must be overridden by subclass\")\n    }\n\n    pub fn dom_record_key(&self) -> String {\n        raise(NotImplementedError, \"dom_record_key must be overridden by subclass\")\n    }\n\n    pub fn persisted_pred(&self) -> bool {\n        self.persisted.clone()\n    }\n\n    pub fn new_record_pred(&self) -> bool {\n        !(self.persisted)\n    }\n\n    pub fn destroyed_pred(&self) -> bool {\n        self.destroyed.clone()\n    }\n\n    pub fn mark_persisted_bang(&mut self) {\n        self.persisted = true;\n        self.destroyed = false;\n    }\n\n    pub fn all() -> Vec<Base> {\n        Self::_adapter_all()\n    }\n\n    pub fn find(id: i64) -> Base {\n        let Some(result) = Self::_adapter_find_by_id(id) else { raise(RecordNotFound, format!(\"Couldn't find {} with id={}\", name(), id)) };\n        result.clone()\n    }\n\n    pub fn find_by(conditions: std::collections::HashMap<String, serde_json::Value>) -> Option<Base> {\n        let mut rows = ActiveRecord::adapter().r#where(Self::table_name(), conditions.clone());\n        if (rows.len() as i64) == 0_i64 { return None };\n        Some(Self::instantiate(rows.clone()[(0_i64) as usize].clone()))\n    }\n\n    pub fn find_by_bang(conditions: std::collections::HashMap<String, serde_json::Value>) -> Base {\n        let Some(result) = Self::find_by(conditions.clone()) else { raise(RecordNotFound, format!(\"Couldn't find {}\", name())) };\n        result.clone()\n    }\n\n    pub fn r#where(conditions: std::collections::HashMap<String, serde_json::Value>) -> Vec<Base> {\n        ActiveRecord::adapter().r#where(Self::table_name(), conditions.clone()).into_iter().map(|row| { Self::instantiate(row.clone()) }).collect::<Vec<_>>()\n    }\n\n    pub fn count() -> i64 {\n        Self::_adapter_count()\n    }\n\n    pub fn exists_pred(id: i64) -> bool {\n        Self::_adapter_exists_by_id_pred(id)\n    }\n\n    pub fn none_pred() -> bool {\n        Self::count() == 0_i64\n    }\n\n    pub fn any_pred() -> bool {\n        Self::count() > 0_i64\n    }\n\n    pub fn delete_all() {\n        ActiveRecord::adapter().delete_all(Self::table_name());\n    }\n\n    pub fn destroy_all() -> Vec<Base> {\n        let mut records = Self::_adapter_all();\n        records.clone().iter_mut().for_each(|r| { r.destroy(); });\n        records.clone()\n    }\n\n    pub fn create(attrs: std::collections::HashMap<String, serde_json::Value>) -> Base {\n        let mut instance = Self::new(attrs);\n        instance.save();\n        instance.clone()\n    }\n\n    pub fn create_bang(attrs: std::collections::HashMap<String, serde_json::Value>) -> Base {\n        let mut instance = Self::new(attrs);\n        if !(instance.save()) { raise(RecordInvalid, instance.clone()) };\n        instance.clone()\n    }\n\n    pub fn last() -> Option<Base> {\n        Self::_adapter_last()\n    }\n\n    pub fn save(&mut self) -> bool {\n        Self::before_validation();\n        let ok = self.valid_pred();\n        Self::after_validation();\n        if !(ok) { return false };\n        self.save_after_validation()\n    }\n\n    pub fn save_after_validation(&mut self) -> bool {\n        Self::before_save();\n        let was_new = self.new_record_pred();\n        if was_new { Self::before_create();\n        self.fill_timestamps(true);\n        self.id = self._adapter_insert();\n        self.persisted = true;\n        self.__track_saved_changes(was_new);\n        Self::after_create();\n        Self::after_create_commit() } else { Self::before_update();\n        self.fill_timestamps(false);\n        Self::_adapter_update();\n        self.__track_saved_changes(was_new);\n        Self::after_update();\n        Self::after_update_commit() };\n        Self::after_save();\n        Self::after_save_commit();\n        Self::after_commit();\n        true\n    }\n\n    pub fn __track_saved_changes(&mut self, was_new: bool) {\n        self.id_previously_changed = was_new;\n    }\n\n    pub fn saved_changes() -> std::collections::HashMap<String, serde_json::Value> {\n        std::collections::HashMap::new()\n    }\n\n    pub fn _note_hydrated() {\n    }\n\n    pub fn saved_change_to_attribute_pred(&self, name: &str) -> bool {\n        let mut changes = Self::saved_changes();\n        changes.contains_key(name)\n    }\n\n    pub fn saved_change_to_attribute(&self, name: &str) -> serde_json::Value {\n        let mut changes = Self::saved_changes();\n        changes.get(name).cloned().unwrap_or(serde_json::Value::Null)\n    }\n\n    pub fn attribute_previously_was(&self, name: &str) -> serde_json::Value {\n        let mut changes = Self::saved_changes();\n        changes.get(name).cloned().unwrap_or(serde_json::Value::Null)\n    }\n\n    pub fn id_previously_changed_pred(&self) -> bool {\n        self.id_previously_changed.clone()\n    }\n\n    pub fn save_bang(&mut self) -> Base {\n        if !(self.save()) { raise(RecordInvalid, self) };\n        self.clone()\n    }\n\n    pub fn touch(&mut self) -> bool {\n        self.fill_timestamps(false);\n        Self::_adapter_update();\n        Self::after_touch();\n        true\n    }\n\n    pub fn destroy_bang(&mut self) -> Base {\n        self.destroy()\n    }\n\n    pub fn destroy(&mut self) -> Base {\n        if !(self.persisted_pred()) { return self.clone() };\n        Self::before_destroy();\n        Self::_adapter_delete();\n        self.persisted = false;\n        self.destroyed = true;\n        Self::after_destroy();\n        Self::after_destroy_commit();\n        Self::after_commit();\n        self.clone()\n    }\n\n    pub fn reload(&self) -> Base {\n        Self::_adapter_reload();\n        self.clone()\n    }\n\n    pub fn after_initialize() {\n    }\n\n    pub fn before_validation() {\n    }\n\n    pub fn after_validation() {\n    }\n\n    pub fn before_save() {\n    }\n\n    pub fn after_save() {\n    }\n\n    pub fn before_create() {\n    }\n\n    pub fn after_create() {\n    }\n\n    pub fn before_update() {\n    }\n\n    pub fn after_update() {\n    }\n\n    pub fn before_destroy() {\n    }\n\n    pub fn after_destroy() {\n    }\n\n    pub fn after_commit() {\n    }\n\n    pub fn after_create_commit() {\n    }\n\n    pub fn after_update_commit() {\n    }\n\n    pub fn after_destroy_commit() {\n    }\n\n    pub fn after_save_commit() {\n    }\n\n    pub fn after_touch() {\n    }\n\n    pub fn validate() {\n    }\n\n    pub fn fill_timestamps(&mut self, creating: bool) {\n        let mut cols = Self::schema_columns();\n        let now = crate::rh_datetime::db_now();\n        if cols.iter().any(|__c| __c == \"updated_at\") { self.set_index(\"updated_at\", serde_json::Value::from(now.clone())) };\n        if creating && cols.iter().any(|__c| __c == \"created_at\") { self.set_index(\"created_at\", serde_json::Value::from(now.clone())) };\n    }\n\n    pub fn valid_pred(&mut self) -> bool {\n        self.errors = vec![];\n        Self::validate();\n        self.errors.is_empty()\n    }\n}\n\npub struct ActiveRecord;\n\nstatic ADAPTER: std::sync::Mutex<Option<AdapterInterface>> = std::sync::Mutex::new(None);\n\nimpl ActiveRecord {\n    pub fn adapter() -> AdapterInterface {\n        ADAPTER.lock().unwrap().clone().unwrap_or_default()\n    }\n\n    pub fn set_adapter(value: AdapterInterface) {\n        *ADAPTER.lock().unwrap() = Some(value)\n    }\n\n    pub fn enum_int(text: &str, labels: Vec<String>, values: Vec<i64>) -> i64 {\n        let mut result = -1_i64;\n        let mut i = 0_i64;\n        while i < labels.len() as i64 {\n            if labels.clone()[(i) as usize].clone() == text.clone() { result = values[(i) as usize] };\n            /* TODO rust2: ExprNode::Discriminant(16) */\n        };\n        if result == -1_i64 { result = text.parse::<i64>().unwrap_or(0) };\n        result\n    }\n}\n"},{"path":"src/adapter_interface.rs","content":"//! `AdapterInterface` — the concrete type the transpiled\n//! `ActiveRecord.adapter` slot uses. Wraps an `Arc<dyn ActiveRecordAdapter>`\n//! so the module-singleton emit's slot template\n//! (`Mutex<Option<AdapterInterface>>` + `.clone().unwrap_or_default()`)\n//! works without per-target rust2 emit branching.\n//!\n//! Why a wrapper: the `runtime/ruby/active_record/base.rbs` types\n//! `ActiveRecord.adapter` as `AdapterInterface` (the analyzer registers\n//! that class with the 9-method contract — `all/find/where/count/exists?/\n//! insert/update/delete/truncate`). Transpiled call sites\n//! (`ActiveRecord::adapter().find(...)`) need a *single* concrete type\n//! that:\n//!   - Is `Clone` (the slot template does `.clone()` on the mutex guard).\n//!   - Has a `Default` (the template falls back to `Default::default()`\n//!     when the slot is `None`).\n//!   - Forwards every adapter method to whatever concrete impl was\n//!     installed at boot (sqlite, framework-test, libsql, ...).\n//!\n//! A bare `Arc<dyn ActiveRecordAdapter>` lacks `Default`. Wrapping it\n//! lets us provide a panicking-on-call \"not configured\" default\n//! (matches the call-time error you'd get if the boot path forgot to\n//! install an adapter — earlier than e.g. a SQL error).\n//!\n//! Install at boot:\n//!     ActiveRecord::set_adapter(AdapterInterface::new(SqliteAdapter::open(\"./db.sqlite\")));\n\nuse std::collections::HashMap;\nuse std::sync::Arc;\n\nuse serde_json::Value;\n\nuse crate::active_record_adapter::{ActiveRecordAdapter, Row};\n\nstruct NotConfigured;\nimpl ActiveRecordAdapter for NotConfigured {\n    fn all(&self, _t: String) -> Vec<Row> {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn find(&self, _t: String, _id: i64) -> Option<Row> {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn r#where(&self, _t: String, _c: HashMap<String, Value>) -> Vec<Row> {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn count(&self, _t: String) -> i64 {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn exists_pred(&self, _t: String, _id: i64) -> bool {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn insert(&self, _t: String, _a: HashMap<String, Value>) -> i64 {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn update(&self, _t: String, _id: i64, _a: HashMap<String, Value>) {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn delete(&self, _t: String, _id: i64) {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn truncate(&self, _t: String) {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn delete_all(&self, _t: String) {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n}\n\n#[derive(Clone)]\npub struct AdapterInterface(Arc<dyn ActiveRecordAdapter + Send + Sync>);\n\nimpl Default for AdapterInterface {\n    fn default() -> Self {\n        Self(Arc::new(NotConfigured))\n    }\n}\n\nimpl AdapterInterface {\n    pub fn new<A>(adapter: A) -> Self\n    where\n        A: ActiveRecordAdapter + Send + Sync + 'static,\n    {\n        Self(Arc::new(adapter))\n    }\n}\n\nimpl ActiveRecordAdapter for AdapterInterface {\n    fn all(&self, t: String) -> Vec<Row> {\n        self.0.all(t)\n    }\n    fn find(&self, t: String, id: i64) -> Option<Row> {\n        self.0.find(t, id)\n    }\n    fn r#where(&self, t: String, c: HashMap<String, Value>) -> Vec<Row> {\n        self.0.r#where(t, c)\n    }\n    fn count(&self, t: String) -> i64 {\n        self.0.count(t)\n    }\n    fn exists_pred(&self, t: String, id: i64) -> bool {\n        self.0.exists_pred(t, id)\n    }\n    fn insert(&self, t: String, a: HashMap<String, Value>) -> i64 {\n        self.0.insert(t, a)\n    }\n    fn update(&self, t: String, id: i64, a: HashMap<String, Value>) {\n        self.0.update(t, id, a)\n    }\n    fn delete(&self, t: String, id: i64) {\n        self.0.delete(t, id)\n    }\n    fn truncate(&self, t: String) {\n        self.0.truncate(t)\n    }\n    fn delete_all(&self, t: String) {\n        self.0.delete_all(t)\n    }\n}\n"},{"path":"src/broadcasts.rs","content":"//! Turbo Streams broadcasts shim.\n//!\n//! The model lowerer's `broadcasts_to` expansion (see\n//! `src/lower/broadcasts.rs`) produces calls like\n//! `Broadcasts.prepend(stream: \"x\", target: \"y\", html: \"...\")`\n//! from inside model callback methods (`after_create`, etc.).\n//! rust2 emits the kwargs as a `HashMap<String, Value>`, so each\n//! shim method here accepts the unified hash shape and pulls\n//! the named fields out.\n//!\n//! State lives in a thread-local log so framework tests can assert\n//! on what got emitted; production installs a broadcaster via\n//! `install_broadcaster` that fans fragments out to the cable\n//! websocket. Mirrors `runtime/crystal/broadcasts.cr` member-for-\n//! member at the shim level.\n//!\n//! For Phase 5 the production path is a no-op stub — Cable wiring\n//! arrives in a later phase.\n\nuse std::cell::RefCell;\nuse std::collections::HashMap;\n\nuse serde_json::Value;\n\nthread_local! {\n    /// In-memory broadcast log: `(action, stream, target, html)`\n    /// tuples in emission order. Tests inspect this after running\n    /// model callbacks; production reads it through `log()` if a\n    /// fan-out plugin needs the trail.\n    static LOG: RefCell<Vec<(String, String, String, String)>> =\n        const { RefCell::new(Vec::new()) };\n}\n\n/// `Broadcasts` namespace — `pub struct Broadcasts;` + impl gives\n/// the same `Broadcasts::method(...)` call shape the lowered model\n/// callbacks emit.\npub struct Broadcasts;\n\nimpl Broadcasts {\n    // Compose the `<turbo-stream>` element for a `turbo_stream.<action>`\n    // call in a `.turbo_stream.erb` template. Positional and String-typed on\n    // purpose: it is the ONE shape the view lowerer emits on every target\n    // (the older keyword/Symbol `render_fragment` spellings had drifted\n    // apart between targets).\n    pub fn turbo_stream_fragment(action: &str, target: &str, html: &str) -> String {\n        Self::turbo_stream_fragment_with(action, target, html, \"\")\n    }\n\n    /// `turbo_stream_fragment` plus the element's extra attribute text.\n    /// See `cable::turbo_stream_html_with` on why this is a separate\n    /// function rather than a defaulted parameter.\n    pub fn turbo_stream_fragment_with(\n        action: &str,\n        target: &str,\n        html: &str,\n        attributes: &str,\n    ) -> String {\n        if action == \"remove\" {\n            return format!(\n                \"<turbo-stream{attributes} action=\\\"remove\\\" target=\\\"{target}\\\"></turbo-stream>\"\n            );\n        }\n        format!(\n            \"<turbo-stream{attributes} action=\\\"{action}\\\" target=\\\"{target}\\\"><template>{html}</template></turbo-stream>\"\n        )\n    }\n\n    /// Reset the in-memory log. Framework tests call this between\n    /// assertions; production typically doesn't.\n    pub fn reset_log_bang() {\n        LOG.with(|c| c.borrow_mut().clear());\n    }\n\n    /// Snapshot the log as a fresh Vec.\n    pub fn log() -> Vec<(String, String, String, String)> {\n        LOG.with(|c| c.borrow().clone())\n    }\n\n    pub fn append(attrs: HashMap<String, Value>) {\n        Self::record(\"append\", &attrs);\n    }\n\n    pub fn prepend(attrs: HashMap<String, Value>) {\n        Self::record(\"prepend\", &attrs);\n    }\n\n    pub fn replace(attrs: HashMap<String, Value>) {\n        Self::record(\"replace\", &attrs);\n    }\n\n    pub fn remove(attrs: HashMap<String, Value>) {\n        Self::record(\"remove\", &attrs);\n    }\n\n    fn record(action: &str, attrs: &HashMap<String, Value>) {\n        let stream = attrs.get(\"stream\").and_then(|v| v.as_str()).unwrap_or(\"\").to_string();\n        let target = attrs.get(\"target\").and_then(|v| v.as_str()).unwrap_or(\"\").to_string();\n        let html = attrs.get(\"html\").and_then(|v| v.as_str()).unwrap_or(\"\").to_string();\n        // Rendered attribute text the broadcast lowering composed. Read\n        // from the bag so an app that writes `attributes:` is not\n        // silently stripped of it.\n        let attributes =\n            attrs.get(\"attributes\").and_then(|v| v.as_str()).unwrap_or(\"\").to_string();\n        LOG.with(|c| {\n            c.borrow_mut().push((\n                action.to_string(),\n                stream.clone(),\n                target.clone(),\n                html.clone(),\n            ))\n        });\n        // Forward to the live Action Cable fan-out. The model callbacks\n        // (`broadcasts_to` lowering) hand us the already-rendered partial\n        // as `html`; compose the `<turbo-stream>` wrapper and fan it out to\n        // every subscriber of `stream`. Without this the production path is\n        // a no-op (only the in-memory LOG is populated) and a subscribed\n        // `<turbo-cable-stream-source>` never sees the create/destroy\n        // broadcast — the e2e action_cable spec. The cable server fans out\n        // via per-subscriber mpsc channels, so this is safe to call from\n        // any axum worker thread.\n        let fragment = crate::cable::turbo_stream_html_with(action, &target, &html, &attributes);\n        crate::cable::CABLE.broadcast(&stream, &fragment);\n    }\n}\n"},{"path":"src/cable.rs","content":"//! Action Cable server + Turbo Streams broadcaster.\n//!\n//! Mirrors the TS runtime's `CableServer` + `/cable` handler.\n//! Ported from railcar's proven implementation (pings + the\n//! `actioncable-v1-json` subprotocol are both known-good there).\n//!\n//! Two halves:\n//!   1. Broadcasting — models call `broadcast_prepend_to` /\n//!      `broadcast_replace_to` / `broadcast_remove_to`, which render\n//!      the appropriate `<turbo-stream>` element and push it to\n//!      every subscriber of the given channel. A partial-renderer\n//!      registry (`register_partial`) lets the runtime reach back\n//!      into the generated `views::*` functions without this file\n//!      having to know model-specific types.\n//!   2. WebSocket — `cable_handler` upgrades incoming requests,\n//!      sends a welcome frame, pings every 3s, and on `subscribe`\n//!      commands decodes Turbo's signed-stream-name blob to recover\n//!      the channel name, then registers the socket's outbound mpsc\n//!      sender with the global `CABLE` registry.\n//!\n//! Generated code (Phase B of the cable work) calls these via\n//! `crate::cable::...` from `impl Broadcaster for <Model>` blocks\n//! produced by the emitter's `broadcasts_to` translation.\n//!\n//! The `actioncable-v1-json` subprotocol spec:\n//!   https://github.com/rails/rails/blob/main/actioncable/lib/action_cable/server/worker.rb\n//! Frames used here: `welcome`, `ping`, `confirm_subscription`,\n//! `message`. Rejections + `unsubscribe` commands aren't needed\n//! for the current broadcast paths.\n\nuse std::collections::HashMap;\nuse std::sync::{LazyLock, RwLock};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nuse axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};\nuse axum::response::IntoResponse;\nuse base64::Engine;\nuse futures_util::{SinkExt, StreamExt};\nuse serde_json::{json, Value};\nuse tokio::sync::mpsc;\n\n// ── Partial-renderer registry ──────────────────────────────────\n//\n// Models register a closure that renders an instance identified by\n// id into its Turbo Stream partial HTML. Kept as a runtime lookup\n// (rather than parameterising Broadcaster on the model type) so\n// broadcasts called on associations — e.g., `comment.article`'s\n// replace broadcast — can find the parent's partial without the\n// child model needing to know the parent's view module.\n\npub type RenderPartialFn = Box<dyn Fn(i64) -> String + Send + Sync>;\n\nstatic PARTIAL_RENDERERS: LazyLock<RwLock<HashMap<String, RenderPartialFn>>> =\n    LazyLock::new(|| RwLock::new(HashMap::new()));\n\n/// Register a partial renderer for `type_name` (the model class\n/// name, e.g. `\"Article\"`). The closure receives a record id and\n/// returns the rendered partial HTML, or empty string on miss.\npub fn register_partial(type_name: &str, f: impl Fn(i64) -> String + Send + Sync + 'static) {\n    PARTIAL_RENDERERS\n        .write()\n        .expect(\"cable partial renderers poisoned\")\n        .insert(type_name.to_string(), Box::new(f));\n}\n\n/// Look up and invoke a registered partial renderer. Returns a\n/// placeholder div when no renderer is registered — tests can\n/// assert on the fallback rather than panicking.\npub fn render_partial(type_name: &str, id: i64) -> String {\n    let table = PARTIAL_RENDERERS\n        .read()\n        .expect(\"cable partial renderers poisoned\");\n    match table.get(type_name) {\n        Some(f) => f(id),\n        None => format!(\"<div>{} #{}</div>\", type_name, id),\n    }\n}\n\n// ── Turbo Streams rendering ────────────────────────────────────\n\n/// Render a single `<turbo-stream>` element. Empty content collapses\n/// to a self-closing template (used by `remove` actions).\npub fn turbo_stream_html(action: &str, target: &str, content: &str) -> String {\n    turbo_stream_html_with(action, target, content, \"\")\n}\n\n/// `turbo_stream_html` plus the EXTRA attribute text the element carries\n/// — ` maintain_scroll=\"true\"`, rendered by the broadcast lowering, where\n/// the literal hash the app wrote can be read. It carries its own leading\n/// space and rides BEFORE `action`/`target`, which is where turbo-rails'\n/// `tag.turbo_stream(template, **attributes, action:, target:)` puts it.\n/// A separate function rather than a defaulted parameter because Rust has\n/// none; the three-argument spelling above is what every call site uses.\npub fn turbo_stream_html_with(\n    action: &str,\n    target: &str,\n    content: &str,\n    attributes: &str,\n) -> String {\n    if content.is_empty() {\n        format!(\n            r#\"<turbo-stream{} action=\"{}\" target=\"{}\"></turbo-stream>\"#,\n            attributes, action, target\n        )\n    } else {\n        format!(\n            r#\"<turbo-stream{} action=\"{}\" target=\"{}\"><template>{}</template></turbo-stream>\"#,\n            attributes, action, target, content,\n        )\n    }\n}\n\n/// Rails convention: `<singular>_<id>`. Naive depluralise — strips\n/// a trailing `s` if present. Matches railcar's `dom_id_for`.\nfn dom_id_for(table_name: &str, id: i64) -> String {\n    let singular = table_name.strip_suffix('s').unwrap_or(table_name);\n    format!(\"{}_{}\", singular, id)\n}\n\n// ── Broadcast helpers ──────────────────────────────────────────\n\n/// Replace the target element with the record's partial. Defaults\n/// `target` to `<singular>_<id>` when caller passes an empty\n/// string (matches Rails' `broadcast_replace_to` with no explicit\n/// target).\npub fn broadcast_replace_to(\n    table_name: &str,\n    id: i64,\n    type_name: &str,\n    channel: &str,\n    target: &str,\n) {\n    let target = if target.is_empty() {\n        dom_id_for(table_name, id)\n    } else {\n        target.to_string()\n    };\n    let html = render_partial(type_name, id);\n    let stream = turbo_stream_html(\"replace\", &target, &html);\n    CABLE.broadcast(channel, &stream);\n}\n\n/// Prepend the record's partial into the target container.\n/// Defaults `target` to the table name (the scaffold's `<ul\n/// id=\"articles\">` convention).\npub fn broadcast_prepend_to(\n    table_name: &str,\n    id: i64,\n    type_name: &str,\n    channel: &str,\n    target: &str,\n) {\n    let target = if target.is_empty() {\n        table_name.to_string()\n    } else {\n        target.to_string()\n    };\n    let html = render_partial(type_name, id);\n    let stream = turbo_stream_html(\"prepend\", &target, &html);\n    CABLE.broadcast(channel, &stream);\n}\n\n/// Append the record's partial into the target container. Same\n/// default-target rule as prepend.\npub fn broadcast_append_to(\n    table_name: &str,\n    id: i64,\n    type_name: &str,\n    channel: &str,\n    target: &str,\n) {\n    let target = if target.is_empty() {\n        table_name.to_string()\n    } else {\n        target.to_string()\n    };\n    let html = render_partial(type_name, id);\n    let stream = turbo_stream_html(\"append\", &target, &html);\n    CABLE.broadcast(channel, &stream);\n}\n\n/// Remove the target element. Target defaults to `<singular>_<id>`\n/// so `broadcast_remove_to(channel)` on a record deletes its own\n/// DOM node.\npub fn broadcast_remove_to(table_name: &str, id: i64, channel: &str, target: &str) {\n    let target = if target.is_empty() {\n        dom_id_for(table_name, id)\n    } else {\n        target.to_string()\n    };\n    let stream = turbo_stream_html(\"remove\", &target, \"\");\n    CABLE.broadcast(channel, &stream);\n}\n\n// ── CableServer ────────────────────────────────────────────────\n\nstruct Subscriber {\n    /// Outbound channel to this socket's send task. Cloned into the\n    /// registry; the socket task owns the receiver half.\n    tx: mpsc::UnboundedSender<String>,\n    /// The raw identifier string the client sent on subscribe. We\n    /// echo it back in every broadcast so Turbo can route the\n    /// message to the right `<turbo-cable-stream-source>` element.\n    identifier: String,\n}\n\npub struct CableServer {\n    channels: RwLock<HashMap<String, Vec<Subscriber>>>,\n}\n\n/// Process-wide registry. One per server; fine as a static because\n/// the server runs one app per process (same assumption as\n/// `server::LAYOUT_FN`).\npub static CABLE: LazyLock<CableServer> = LazyLock::new(|| CableServer {\n    channels: RwLock::new(HashMap::new()),\n});\n\nimpl CableServer {\n    fn subscribe(&self, channel: &str, tx: mpsc::UnboundedSender<String>, identifier: &str) {\n        self.channels\n            .write()\n            .expect(\"cable channels poisoned\")\n            .entry(channel.to_string())\n            .or_default()\n            .push(Subscriber {\n                tx,\n                identifier: identifier.to_string(),\n            });\n    }\n\n    /// Drop any subscribers whose `tx` matches the given pointer.\n    /// Called on socket close. We compare by pointer rather than\n    /// PartialEq because `UnboundedSender` doesn't implement it;\n    /// each subscriber holds a distinct sender, so pointer identity\n    /// is sufficient and avoids threading a subscriber id through\n    /// the WebSocket task.\n    fn unsubscribe(&self, tx_ptr: usize) {\n        let mut channels = self.channels.write().expect(\"cable channels poisoned\");\n        for subs in channels.values_mut() {\n            subs.retain(|s| &s.tx as *const _ as usize != tx_ptr);\n        }\n        channels.retain(|_, subs| !subs.is_empty());\n    }\n\n    /// Push `html` as a Turbo Stream `message` frame to every\n    /// subscriber on `channel`. Dropped senders are ignored — the\n    /// subscriber will be cleaned up on the close-driven\n    /// `unsubscribe` path.\n    pub fn broadcast(&self, channel: &str, html: &str) {\n        let channels = self.channels.read().expect(\"cable channels poisoned\");\n        if let Some(subs) = channels.get(channel) {\n            for sub in subs {\n                let frame = json!({\n                    \"type\": \"message\",\n                    \"identifier\": sub.identifier,\n                    \"message\": html,\n                })\n                .to_string();\n                let _ = sub.tx.send(frame);\n            }\n        }\n    }\n}\n\n// ── WebSocket handler ──────────────────────────────────────────\n\n/// Axum handler for `GET /cable`. Negotiates the\n/// `actioncable-v1-json` subprotocol (Turbo's client requires the\n/// echo) and hands off to the per-socket task.\npub async fn cable_handler(ws: WebSocketUpgrade) -> impl IntoResponse {\n    ws.protocols([\"actioncable-v1-json\"])\n        .on_upgrade(handle_socket)\n}\n\nasync fn handle_socket(socket: WebSocket) {\n    let (mut sender, mut receiver) = socket.split();\n\n    // Welcome frame — the Action Cable client waits for this before\n    // it sends its first `subscribe`.\n    if sender\n        .send(Message::Text(\n            json!({\"type\": \"welcome\"}).to_string().into(),\n        ))\n        .await\n        .is_err()\n    {\n        return;\n    }\n\n    // Single outbound channel merges broadcasts + pings onto the\n    // shared sender half. Cloning the tx into the ping task and\n    // the registry lets each source push independently without\n    // locking the socket writer.\n    let (tx, mut rx) = mpsc::unbounded_channel::<String>();\n    let tx_ptr = &tx as *const _ as usize;\n\n    let ping_tx = tx.clone();\n    let ping_task = tokio::spawn(async move {\n        let mut interval = tokio::time::interval(std::time::Duration::from_secs(3));\n        // First tick fires immediately; skip it so we don't ping\n        // before the welcome + confirm_subscription round-trip.\n        interval.tick().await;\n        loop {\n            interval.tick().await;\n            let ts = SystemTime::now()\n                .duration_since(UNIX_EPOCH)\n                .map(|d| d.as_secs() as i64)\n                .unwrap_or(0);\n            let frame = json!({\"type\": \"ping\", \"message\": ts}).to_string();\n            if ping_tx.send(frame).is_err() {\n                break;\n            }\n        }\n    });\n\n    let send_task = tokio::spawn(async move {\n        while let Some(frame) = rx.recv().await {\n            if sender.send(Message::Text(frame.into())).await.is_err() {\n                break;\n            }\n        }\n    });\n\n    while let Some(Ok(msg)) = receiver.next().await {\n        let Message::Text(text) = msg else { continue };\n        let Ok(payload) = serde_json::from_str::<Value>(&text) else {\n            continue;\n        };\n        if payload.get(\"command\").and_then(Value::as_str) != Some(\"subscribe\") {\n            continue;\n        }\n        let Some(identifier) = payload.get(\"identifier\").and_then(Value::as_str) else {\n            continue;\n        };\n        let Some(channel) = decode_channel(identifier) else {\n            continue;\n        };\n        CABLE.subscribe(&channel, tx.clone(), identifier);\n        let confirm = json!({\n            \"type\": \"confirm_subscription\",\n            \"identifier\": identifier,\n        })\n        .to_string();\n        let _ = tx.send(confirm);\n    }\n\n    ping_task.abort();\n    send_task.abort();\n    CABLE.unsubscribe(tx_ptr);\n}\n\n/// Recover the channel name from Turbo's `signed_stream_name`.\n/// The identifier is a JSON blob like\n/// `{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"<base64>--<digest>\"}`;\n/// the base64 segment holds a JSON-encoded channel name (e.g.\n/// `\"articles\"`). If either decode fails we fall back to the raw\n/// identifier so tests can subscribe by literal channel string.\nfn decode_channel(identifier: &str) -> Option<String> {\n    let id_json = serde_json::from_str::<Value>(identifier).ok()?;\n    let signed = id_json\n        .get(\"signed_stream_name\")\n        .and_then(Value::as_str)?;\n    let base64_part = signed.split(\"--\").next().unwrap_or(\"\");\n    let decoded_bytes = base64::engine::general_purpose::STANDARD\n        .decode(base64_part)\n        .ok()?;\n    let decoded = std::str::from_utf8(&decoded_bytes).ok()?;\n    serde_json::from_str::<String>(decoded).ok()\n}\n\n// ── Broadcaster trait ──────────────────────────────────────────\n\n/// Implemented on models with `broadcasts_to` declarations. The\n/// emitter's `broadcasts_to` translation (Phase B) generates these\n/// implementations; the runtime calls them from the generated\n/// `save()` / `destroy()` methods at the end of a successful\n/// persist.\n///\n/// Kept separate from `Model` so that models without any broadcast\n/// hooks don't need a stub impl — the emitter only emits this for\n/// models that declare `broadcasts_to`, and the save/destroy\n/// codegen conditionally calls into it.\npub trait Broadcaster {\n    fn after_save(&self);\n    fn after_delete(&self);\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn turbo_stream_html_wraps_content_in_template() {\n        let got = turbo_stream_html(\"replace\", \"article_1\", \"<div>hi</div>\");\n        assert_eq!(\n            got,\n            r#\"<turbo-stream action=\"replace\" target=\"article_1\"><template><div>hi</div></template></turbo-stream>\"#\n        );\n    }\n\n    #[test]\n    fn turbo_stream_html_self_closes_when_empty() {\n        let got = turbo_stream_html(\"remove\", \"article_1\", \"\");\n        assert_eq!(\n            got,\n            r#\"<turbo-stream action=\"remove\" target=\"article_1\"></turbo-stream>\"#\n        );\n    }\n\n    #[test]\n    fn dom_id_for_strips_trailing_s() {\n        assert_eq!(dom_id_for(\"articles\", 7), \"article_7\");\n        assert_eq!(dom_id_for(\"comment\", 3), \"comment_3\");\n    }\n\n    #[test]\n    fn render_partial_falls_back_when_unregistered() {\n        // Use a distinct type name so parallel tests don't collide.\n        let got = render_partial(\"UnregisteredNoise\", 99);\n        assert_eq!(got, \"<div>UnregisteredNoise #99</div>\");\n    }\n\n    #[test]\n    fn decode_channel_recovers_plain_base64_name() {\n        // Construct a signed stream name for `\"articles\"`.\n        let inner = serde_json::to_string(\"articles\").unwrap();\n        let b64 =\n            base64::engine::general_purpose::STANDARD.encode(inner.as_bytes());\n        let signed = format!(\"{}--unsigned\", b64);\n        let identifier = serde_json::json!({\n            \"channel\": \"Turbo::StreamsChannel\",\n            \"signed_stream_name\": signed,\n        })\n        .to_string();\n        assert_eq!(decode_channel(&identifier).as_deref(), Some(\"articles\"));\n    }\n\n    #[test]\n    fn decode_channel_returns_none_on_bad_input() {\n        assert!(decode_channel(\"not json\").is_none());\n        assert!(decode_channel(r#\"{\"no\":\"signed\"}\"#).is_none());\n    }\n}\n"},{"path":"src/controllers/application_controller.rs","content":"#[allow(unused_imports)]\nuse crate::action_controller_base::{self, Base};\n#[allow(unused_imports)]\nuse crate::flash::Flash;\n#[allow(unused_imports)]\nuse crate::session::Session;\n#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\n#[allow(unused_imports)]\nuse crate::errors_ext::{raise, NotImplementedError, RecordNotFound, RecordInvalid};\n#[derive(Clone, Default)]\npub struct ApplicationController {\n}\n\nimpl ApplicationController {\n}\n\nimpl ApplicationController {\n    pub fn render(&self, content: String) {\n        crate::http::response_set_body(content);\n    }\n    pub fn render_with(&self, content: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status_code = status.as_deref().map(crate::http::status_name_to_code_pub);\n        crate::http::response_set_body_with(content, content_type, status_code);\n    }\n    pub fn request_format(&self) -> String { crate::http::request_format_get() }\n    pub fn redirect_to(&self, url: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        // Carry `notice:` / `alert:` to the next request via the\n        // flash cookie (the per-action wrapper sweeps FLASH_OUT into\n        // Set-Cookie). Recorded in a thread-local rather than on\n        // `self.flash` because not every controller carries a flash\n        // field, but any controller can redirect with a notice.\n        if let Some(n) = opts.get(\"notice\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"notice\", n); }\n        if let Some(a) = opts.get(\"alert\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"alert\", a); }\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).unwrap_or(\"see_other\");\n        crate::http::response_set_redirect(url, crate::http::status_name_to_code_pub(status));\n    }\n    pub fn head(&self, status: &str, opts: std::collections::HashMap<String, serde_json::Value>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        crate::http::response_set_head(status, content_type);\n    }\n}\n"},{"path":"src/controllers/articles_controller.rs","content":"#[allow(unused_imports)]\nuse crate::action_controller_base::{self, Base};\n#[allow(unused_imports)]\nuse crate::flash::Flash;\n#[allow(unused_imports)]\nuse crate::session::Session;\n#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\n#[allow(unused_imports)]\nuse crate::errors_ext::{raise, NotImplementedError, RecordNotFound, RecordInvalid};\n#[derive(Clone, Default)]\npub struct ArticlesController {\n    pub articles: Vec<Article>,\n    pub article: Article,\n    pub flash: Flash,\n    pub params: std::collections::HashMap<String, serde_json::Value>,\n}\n\nimpl ArticlesController {\n    pub fn process_action(&mut self, action_name: &str) {\n        match action_name {\n                \"index\" => { self.index() },\n                \"show\" => { self.show() },\n                \"new\" => { self.new_action() },\n                \"edit\" => { self.edit() },\n                \"create\" => { self.create() },\n                \"update\" => { self.update() },\n                \"destroy\" => { self.destroy() }\n                _ => (),\n            }\n    }\n\n    pub fn index(&mut self) {\n        let stmt = Db::prepare(\"SELECT id, body, created_at, title, updated_at FROM articles ORDER BY created_at DESC\");\n        let mut results = vec![];\n        while Db::step_pred(stmt) {\n            results.push(Article::from_stmt(stmt));\n        };\n        Db::finalize(stmt);\n        let __comments_ids = results.clone().into_iter().map(|a| { a.id() }).collect::<Vec<_>>();\n        let __comments_stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments WHERE article_id IN (\", Db::escape_int_list(__comments_ids)), \")\")));\n        let mut __comments_loaded = vec![];\n        while Db::step_pred(__comments_stmt) {\n            __comments_loaded.push(Comment::from_stmt(__comments_stmt));\n        };\n        Db::finalize(__comments_stmt);\n        results.iter_mut().for_each(|a| {\n            let mut __comments_group = vec![];\n            __comments_loaded.clone().iter_mut().for_each(|r| { if r.article_id() == a.id() { __comments_group.push(r.clone()) }; });\n            a._preload_comments(__comments_group.clone());;\n        });\n        self.articles = results.clone();\n        if self.request_format() == \"json\" { self.render_with(Articles::index_json(self.articles.clone()), std::collections::HashMap::from([(\"content_type\", \"application/json\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.render(Articles::index(self.articles.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\"))) };\n    }\n\n    pub fn show(&mut self) {\n        self.article = Article::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).to_string().parse::<i64>().unwrap_or(0));\n        if self.request_format() == \"json\" { self.render_with(Articles::show_json(self.article.clone()), std::collections::HashMap::from([(\"content_type\", \"application/json\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.render(Articles::show(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\"))) };\n    }\n\n    pub fn new_action(&mut self) {\n        self.article = Article::new(std::collections::HashMap::new());\n        self.render(Articles::_new(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\")));\n    }\n\n    pub fn edit(&mut self) {\n        self.article = Article::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).to_string().parse::<i64>().unwrap_or(0));\n        self.render(Articles::edit(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\")));\n    }\n\n    pub fn create(&mut self) {\n        self.article = Article::from_params(self.article_params());\n        if self.article.save() { if self.request_format() == \"json\" { self.render_with(Articles::show_json(self.article.clone()), std::collections::HashMap::from([(\"status\", \"created\".to_string()), (\"location\", RouteHelpers::article_path(self.article.id())), (\"content_type\", (\"application/json\").to_string())]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"notice\", \"Article was successfully created.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } } else { self.render_with(Articles::_new(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\")), std::collections::HashMap::from([(\"status\", \"unprocessable_content\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) };\n    }\n\n    pub fn update(&mut self) {\n        self.article = Article::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).to_string().parse::<i64>().unwrap_or(0));\n        if self.article.update_from_article_params(self.article_params()) { if self.request_format() == \"json\" { self.render_with(Articles::show_json(self.article.clone()), std::collections::HashMap::from([(\"status\", \"ok\".to_string()), (\"location\", RouteHelpers::article_path(self.article.id())), (\"content_type\", (\"application/json\").to_string())]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"notice\", \"Article was successfully updated.\"), (\"status\", \"see_other\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } } else { self.render_with(Articles::edit(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\")), std::collections::HashMap::from([(\"status\", \"unprocessable_content\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) };\n    }\n\n    pub fn destroy(&mut self) {\n        self.article = Article::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).to_string().parse::<i64>().unwrap_or(0));\n        self.article.destroy();\n        if self.request_format() == \"json\" { self.head(\"no_content\", std::collections::HashMap::from([(\"content_type\", \"application/json\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.redirect_to(RouteHelpers::articles_path(), std::collections::HashMap::from([(\"notice\", \"Article was successfully destroyed.\"), (\"status\", \"see_other\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) };\n    }\n\n    pub fn article_params(&self) -> ArticleParams {\n        ArticleParams::from_raw(self.params.clone())\n    }\n}\n\nimpl ArticlesController {\n    pub fn render(&self, content: String) {\n        crate::http::response_set_body(content);\n    }\n    pub fn render_with(&self, content: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status_code = status.as_deref().map(crate::http::status_name_to_code_pub);\n        crate::http::response_set_body_with(content, content_type, status_code);\n    }\n    pub fn request_format(&self) -> String { crate::http::request_format_get() }\n    pub fn redirect_to(&self, url: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        // Carry `notice:` / `alert:` to the next request via the\n        // flash cookie (the per-action wrapper sweeps FLASH_OUT into\n        // Set-Cookie). Recorded in a thread-local rather than on\n        // `self.flash` because not every controller carries a flash\n        // field, but any controller can redirect with a notice.\n        if let Some(n) = opts.get(\"notice\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"notice\", n); }\n        if let Some(a) = opts.get(\"alert\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"alert\", a); }\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).unwrap_or(\"see_other\");\n        crate::http::response_set_redirect(url, crate::http::status_name_to_code_pub(status));\n    }\n    pub fn head(&self, status: &str, opts: std::collections::HashMap<String, serde_json::Value>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        crate::http::response_set_head(status, content_type);\n    }\n}\n\n// ── rust2 wedge 2c.2: axum handler wrappers ──\n// Per-action free fns axum's Router can dispatch into. Build the\n// controller via Default, call the action, and translate the\n// thread-local response state into an `axum::response::Response`.\npub async fn _axum_index(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut c = ArticlesController::default();\n    c.flash = crate::http::flash_from_request(&headers);\n    c.index();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_new(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut c = ArticlesController::default();\n    c.flash = crate::http::flash_from_request(&headers);\n    c.new_action();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_create(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Form(form): axum::extract::Form<std::collections::HashMap<String, String>>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params = crate::http::params_from_form(form);\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.create();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_show(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Path(id_raw): axum::extract::Path<String>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.show();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_edit(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Path(id_raw): axum::extract::Path<String>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.edit();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_update(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Path(id_raw): axum::extract::Path<String>, axum::extract::Form(form): axum::extract::Form<std::collections::HashMap<String, String>>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params = crate::http::params_from_form(form);\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.update();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_destroy(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Path(id_raw): axum::extract::Path<String>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.destroy();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\n"},{"path":"src/controllers/comments_controller.rs","content":"#[allow(unused_imports)]\nuse crate::action_controller_base::{self, Base};\n#[allow(unused_imports)]\nuse crate::flash::Flash;\n#[allow(unused_imports)]\nuse crate::session::Session;\n#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\n#[allow(unused_imports)]\nuse crate::errors_ext::{raise, NotImplementedError, RecordNotFound, RecordInvalid};\n#[derive(Clone, Default)]\npub struct CommentsController {\n    pub article: Article,\n    pub comment: Comment,\n    pub params: std::collections::HashMap<String, serde_json::Value>,\n}\n\nimpl CommentsController {\n    pub fn process_action(&mut self, action_name: &str) {\n        match action_name {\n                \"create\" => { self.create() },\n                \"destroy\" => { self.destroy() }\n                _ => (),\n            }\n    }\n\n    pub fn create(&mut self) {\n        self.article = Article::find(self.params.get(\"article_id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).to_string().parse::<i64>().unwrap_or(0));\n        self.comment = Comment::from_params(self.comment_params());\n        self.comment.set_article_id(self.article.id());\n        if self.comment.save() { self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"notice\", \"Comment was successfully created.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"alert\", \"Could not create comment.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) };\n    }\n\n    pub fn destroy(&mut self) {\n        self.article = Article::find(self.params.get(\"article_id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).to_string().parse::<i64>().unwrap_or(0));\n        self.comment = Comment::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).to_string().parse::<i64>().unwrap_or(0));\n        if self.comment.article_id() != self.article.id() { self.head(\"not_found\", std::collections::HashMap::new());\n        return; } else {  };\n        self.comment.destroy();\n        self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"notice\", \"Comment was successfully deleted.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn comment_params(&self) -> CommentParams {\n        CommentParams::from_raw(self.params.clone())\n    }\n}\n\nimpl CommentsController {\n    pub fn render(&self, content: String) {\n        crate::http::response_set_body(content);\n    }\n    pub fn render_with(&self, content: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status_code = status.as_deref().map(crate::http::status_name_to_code_pub);\n        crate::http::response_set_body_with(content, content_type, status_code);\n    }\n    pub fn request_format(&self) -> String { crate::http::request_format_get() }\n    pub fn redirect_to(&self, url: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        // Carry `notice:` / `alert:` to the next request via the\n        // flash cookie (the per-action wrapper sweeps FLASH_OUT into\n        // Set-Cookie). Recorded in a thread-local rather than on\n        // `self.flash` because not every controller carries a flash\n        // field, but any controller can redirect with a notice.\n        if let Some(n) = opts.get(\"notice\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"notice\", n); }\n        if let Some(a) = opts.get(\"alert\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"alert\", a); }\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).unwrap_or(\"see_other\");\n        crate::http::response_set_redirect(url, crate::http::status_name_to_code_pub(status));\n    }\n    pub fn head(&self, status: &str, opts: std::collections::HashMap<String, serde_json::Value>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        crate::http::response_set_head(status, content_type);\n    }\n}\n\n// ── rust2 wedge 2c.2: axum handler wrappers ──\n// Per-action free fns axum's Router can dispatch into. Build the\n// controller via Default, call the action, and translate the\n// thread-local response state into an `axum::response::Response`.\npub async fn _axum_create(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, axum::extract::Path(article_id_raw): axum::extract::Path<String>, axum::extract::Form(form): axum::extract::Form<std::collections::HashMap<String, String>>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params = crate::http::params_from_form(form);\n    let article_id: i64 = article_id_raw.strip_suffix(\".json\").unwrap_or(&article_id_raw).parse().unwrap_or(0);\n    params.insert(\"article_id\".to_string(), serde_json::Value::from(article_id));\n    let mut c = CommentsController { params, ..Default::default() };\n    c.create();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_destroy(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, axum::extract::Path((article_id_raw, id_raw)): axum::extract::Path<(String, String)>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n    let article_id: i64 = article_id_raw.strip_suffix(\".json\").unwrap_or(&article_id_raw).parse().unwrap_or(0);\n    params.insert(\"article_id\".to_string(), serde_json::Value::from(article_id));\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = CommentsController { params, ..Default::default() };\n    c.destroy();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\n"},{"path":"src/controllers/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod application_controller;\npub mod articles_controller;\npub mod comments_controller;\npub use application_controller::ApplicationController;\npub use articles_controller::ArticlesController;\npub use comments_controller::CommentsController;\n"},{"path":"src/db.rs","content":"//! Roundhouse Rust DB runtime.\n//!\n//! Hand-written helpers the Rust emitter copies verbatim into each\n//! generated project as `src/db.rs`. Owns the per-test SQLite\n//! connection and hides rusqlite borrowing from the generated code\n//! — save/destroy/count/find all go through `with_conn`.\n//!\n//! Two entry points:\n//!   - `setup_test_db(schema)` — thread-local `:memory:` connection\n//!     for tests. Each test re-installs a fresh DB so prior-test\n//!     state doesn't bleed across.\n//!   - `open_production_db(path, schema)` — file-backed connections\n//!     installed into a process-wide pool (`OnceLock<Vec<Mutex<\n//!     Connection>>>`). Used by `main.rs` on server startup.\n//!     Pool size = `DATABASE_POOL_SIZE` env var, defaulting to\n//!     `std::thread::available_parallelism()`. SQLite is opened in\n//!     WAL mode so N readers actually proceed in parallel.\n//!     `with_conn` reaches either slot — test thread-local first,\n//!     then the production pool (try_lock each entry; fall back to\n//!     blocking-lock on slot 0).\n\nuse std::cell::{Cell, RefCell};\nuse std::collections::HashMap;\nuse std::fs;\nuse std::path::Path;\nuse std::sync::{Mutex, OnceLock};\n\nuse rusqlite::types::Value;\nuse rusqlite::Connection;\n\nthread_local! {\n    /// The connection the current thread's test (or request handler)\n    /// uses. `None` until `setup_test_db` initializes it.\n    static CONN: RefCell<Option<Connection>> = const { RefCell::new(None) };\n\n    /// Test-only SQL capture log. `Some(vec)` while a `capture_sql`\n    /// window is open on this thread; every `prepare`/`exec` records\n    /// its SQL into it. Thread-local — like `CONN` above — so it's\n    /// isolated across the parallel `#[tokio::test]` workers and\n    /// scoped to the in-process request a test drives (axum-test's\n    /// mock transport polls the handler inline on the test thread, so\n    /// the queries land in this thread's log). The query counter is\n    /// the only instrument that sees the `includes(:assoc)` N+1 that\n    /// `compare` is structurally blind to (roundhouse#40, #27).\n    static SQL_CAPTURE: RefCell<Option<Vec<String>>> = const { RefCell::new(None) };\n}\n\n/// Process-wide sqlite connection pool for the production server.\n/// `axum` handlers run on a multi-thread tokio runtime so per-thread\n/// thread-locals don't work; each slot in this Vec is an independent\n/// `Connection` guarded by its own `Mutex`, and `with_conn` picks\n/// whichever slot it can `try_lock` first. SQLite is opened in WAL\n/// mode (see `open_production_db`), which is what makes N readers\n/// actually proceed in parallel. Pool size defaults to\n/// `std::thread::available_parallelism()`; override with\n/// `DATABASE_POOL_SIZE`.\nstatic PROD_POOL: OnceLock<Vec<Mutex<Connection>>> = OnceLock::new();\n\n/// rusqlite per-connection prepared-statement cache capacity\n/// (roundhouse#12). Each `Connection` keeps an LRU of compiled statements\n/// keyed by SQL; `prepare_cached` reuses them, skipping the re-parse the\n/// blog's fixed query set otherwise paid on every request. Inlined\n/// literals make id-bearing queries key per-id, so the LRU also bounds\n/// memory — evicted statements are finalized by rusqlite, no leak.\n/// Placeholder binding (the planned follow-on) makes the key the static\n/// query shape. Default rusqlite capacity is 16; raise it to comfortably\n/// hold the blog's working set.\nconst STMT_CACHE_CAP: usize = 128;\n\n/// Initialize a fresh `:memory:` SQLite database on the current\n/// thread, run the supplied schema DDL, and install the connection\n/// so later `with_conn` calls can reach it. Replaces any connection\n/// left over from a previous test that ran on the same thread.\n///\n/// `schema_sql` is the generated `crate::schema_sql::CREATE_TABLES`\n/// string — passed explicitly so this file can stay target-agnostic\n/// and compile standalone in the Roundhouse repo's runtime tree.\npub fn setup_test_db(schema_sql: &str) {\n    let conn = Connection::open_in_memory().expect(\"open :memory: sqlite\");\n    conn.set_prepared_statement_cache_capacity(STMT_CACHE_CAP);\n    conn.execute_batch(schema_sql).expect(\"run schema SQL\");\n    CONN.with(|c| *c.borrow_mut() = Some(conn));\n}\n\n/// Borrow the current thread's test connection, falling back to\n/// the process-wide production connection. Panics if neither is\n/// installed — callers are generated code that runs inside either\n/// a test (whose setup already called `setup_test_db`) or a live\n/// request (whose `main.rs` already called `open_production_db`).\npub fn with_conn<R, F: FnOnce(&Connection) -> R>(f: F) -> R {\n    // Check the test-connection slot first — lets a production-\n    // configured binary still run unit tests against a per-thread\n    // in-memory DB. `CONN.with` runs its closure synchronously so\n    // we can carry the closure-ownership handoff explicitly: if\n    // the test slot has a connection, run `f` there and return\n    // Ok(result); otherwise return Err(f) and run `f` against the\n    // production mutex below.\n    let result: Result<R, F> = CONN.with(|c| {\n        let borrowed = c.borrow();\n        match borrowed.as_ref() {\n            Some(conn) => Ok(f(conn)),\n            None => Err(f),\n        }\n    });\n    match result {\n        Ok(out) => out,\n        Err(f) => {\n            let pool = PROD_POOL.get().expect(\n                \"db not initialized; call setup_test_db or open_production_db first\",\n            );\n            for slot in pool {\n                if let Ok(guard) = slot.try_lock() {\n                    return f(&guard);\n                }\n            }\n            let guard = pool[0].lock().expect(\"prod DB mutex poisoned\");\n            f(&guard)\n        }\n    }\n}\n\n/// Open a file-backed sqlite database for the production server,\n/// apply the schema DDL idempotently, and install it as the\n/// process-wide connection. Creates intermediate directories if\n/// needed — `better-sqlite3` creates the file but not its parent\n/// dir, and rusqlite mirrors that behavior; the TS runtime hit\n/// the same gotcha during smoke test, so we preempt it here.\n///\n/// `schema_sql` is the generated `schema_sql::CREATE_TABLES`\n/// string. The emitter produces `CREATE TABLE IF NOT EXISTS`\n/// directly, so re-opening an existing DB no-ops over the\n/// already-present tables.\npub fn open_production_db(path: &str, schema_sql: &str) {\n    let pool_size = std::env::var(\"DATABASE_POOL_SIZE\")\n        .ok()\n        .and_then(|s| s.parse::<usize>().ok())\n        .filter(|n| *n > 0)\n        .unwrap_or_else(|| {\n            std::thread::available_parallelism()\n                .map(|n| n.get())\n                .unwrap_or(4)\n        });\n    open_production_pool(path, schema_sql, pool_size);\n}\n\n/// Pool builder shared by `open_production_db` and the unit tests.\n/// Kept separate so tests can pick an explicit pool size without\n/// reaching for `std::env::set_var` (which is `unsafe` under\n/// Rust edition 2024).\npub fn open_production_pool(path: &str, schema_sql: &str, pool_size: usize) {\n    // Turn off SQLite's global memory-status accounting before the library\n    // initializes. With it on (the bundled-build default), every internal\n    // malloc/free takes the process-wide `mem0` mutex, which serializes all\n    // pool connections under load — profiled at ~65% of thread-time blocked\n    // on that lock at c=64 (roundhouse#32). sqlite3_config is only effective\n    // pre-initialization; if something already opened a connection it returns\n    // SQLITE_MISUSE and accounting simply stays on (correct, just slower), so\n    // the result is deliberately ignored.\n    unsafe {\n        rusqlite::ffi::sqlite3_config(rusqlite::ffi::SQLITE_CONFIG_MEMSTATUS, 0i32);\n    }\n    if path != \":memory:\" {\n        if let Some(parent) = Path::new(path).parent() {\n            if !parent.as_os_str().is_empty() && !parent.exists() {\n                fs::create_dir_all(parent).expect(\"mkdir db parent\");\n            }\n        }\n    }\n    let mut conns: Vec<Mutex<Connection>> = Vec::with_capacity(pool_size);\n    for _ in 0..pool_size {\n        let conn = Connection::open(path).expect(\"open sqlite db\");\n        conn.set_prepared_statement_cache_capacity(STMT_CACHE_CAP);\n        conn.pragma_update(None, \"journal_mode\", \"WAL\")\n            .expect(\"enable WAL\");\n        conn.pragma_update(None, \"foreign_keys\", \"ON\")\n            .expect(\"enable foreign keys\");\n        // CREATE TABLE IF NOT EXISTS is idempotent on file-backed DBs;\n        // applying per-conn is what lets a `:memory:` pool work, since\n        // each `:memory:` is an independent database.\n        conn.execute_batch(schema_sql).expect(\"apply schema\");\n        conns.push(Mutex::new(conn));\n    }\n    PROD_POOL\n        .set(conns)\n        .map_err(|_| \"PROD_POOL already initialized\")\n        .expect(\"set PROD_POOL\");\n}\n\n// ── Low-level prepare/step/column API ───────────────────────────────\n//\n// Per-statement state lives in a thread-local `STATEMENTS` table; the\n// opaque `i64` stmt id indexes into it. Rows materialize on `prepare`\n// — rusqlite's `Statement`/`Rows` borrow chain from `Connection` is\n// awkward to thread through a `RefCell<HashMap<i64, _>>`, so we eat\n// the up-front allocation in exchange for a self-contained per-stmt\n// entry. Matches `runtime/crystal/db.cr`'s `Roundhouse::Db` API\n// shape so the lowered model bodies (`Db.prepare(sql)`, `Db.step?\n// (stmt)`, etc., from `src/lower/model_to_library/adapter_emit.rs`)\n// emit the same calls under both targets.\n\n/// A materialized prepared-statement entry: all rows fetched up front\n/// + a cursor position + the most recently-stepped row snapshot.\nstruct StmtEntry {\n    rows: Vec<Vec<Value>>,\n    pos: usize,\n    current: Option<Vec<Value>>,\n}\n\nthread_local! {\n    static STATEMENTS: RefCell<HashMap<i64, StmtEntry>> =\n        RefCell::new(HashMap::new());\n    static NEXT_STMT_ID: Cell<i64> = const { Cell::new(0) };\n    static LAST_INSERT_ROWID: Cell<i64> = const { Cell::new(0) };\n}\n\n/// `Db` namespace — the lowerer (`src/lower/model_to_library/\n/// adapter_emit.rs` + `src/lower/arel/visitor.rs`) emits\n/// `Db.prepare(sql)` / `Db.step?(stmt)` / `Db.column_int(stmt, i)`\n/// against the synthesized per-model adapter methods. Mirrors the\n/// Crystal target's `Roundhouse::Db` module member-for-member.\npub struct Db;\n\nimpl Db {\n    /// Run a one-shot DDL/INSERT/UPDATE/DELETE. Captures\n    /// `last_insert_rowid` so the subsequent accessor returns the\n    /// freshly-inserted id (the typical `Db.exec(insert_sql);\n    /// id = Db.last_insert_rowid` shape in lowered persistence).\n    pub fn exec(sql: &str) {\n        Self::record_sql(sql);\n        with_conn(|conn| {\n            conn.execute_batch(sql).expect(\"Db::exec\");\n            LAST_INSERT_ROWID.with(|c| c.set(conn.last_insert_rowid()));\n        });\n    }\n\n    /// Begin recording every SQL string issued through `prepare` /\n    /// `exec` on this thread, until `capture_sql_take`. Test-only\n    /// instrument mirroring spinel's `Db.capture_sql` — the query\n    /// counter that catches the `includes(:assoc)` N+1 (roundhouse#40,\n    /// #27). See the `SQL_CAPTURE` thread-local for why thread-local.\n    pub fn capture_sql_start() {\n        SQL_CAPTURE.with(|c| *c.borrow_mut() = Some(Vec::new()));\n    }\n\n    /// Stop recording and return the SQL captured since\n    /// `capture_sql_start` (empty if capture was never started).\n    pub fn capture_sql_take() -> Vec<String> {\n        SQL_CAPTURE.with(|c| c.borrow_mut().take().unwrap_or_default())\n    }\n\n    fn record_sql(sql: &str) {\n        SQL_CAPTURE.with(|c| {\n            if let Some(log) = c.borrow_mut().as_mut() {\n                log.push(sql.to_string());\n            }\n        });\n    }\n\n    /// Prepare a SELECT, materialize every row, return the opaque\n    /// stmt id. Subsequent `step` / `column_*` / `finalize` calls take\n    /// the id by value. `prepare_cached` reuses the connection's compiled\n    /// statement (roundhouse#12) — the parse is skipped on a cache hit;\n    /// rusqlite resets the cached statement on checkout and returns it to\n    /// the LRU when the `CachedStatement` drops at the end of this closure.\n    pub fn prepare(sql: &str) -> i64 {\n        Self::record_sql(sql);\n        let rows: Vec<Vec<Value>> = with_conn(|conn| {\n            let mut stmt = conn.prepare_cached(sql).expect(\"Db::prepare\");\n            let n_cols = stmt.column_count();\n            let mut out: Vec<Vec<Value>> = Vec::new();\n            let mut rows = stmt.query([]).expect(\"Db::prepare query\");\n            while let Some(row) = rows.next().expect(\"Db::prepare step\") {\n                let mut col_vec = Vec::with_capacity(n_cols);\n                for i in 0..n_cols {\n                    let v: Value = row.get(i).expect(\"Db::prepare col\");\n                    col_vec.push(v);\n                }\n                out.push(col_vec);\n            }\n            out\n        });\n        let id = NEXT_STMT_ID.with(|c| {\n            let n = c.get() + 1;\n            c.set(n);\n            n\n        });\n        STATEMENTS.with(|s| {\n            s.borrow_mut().insert(\n                id,\n                StmtEntry { rows, pos: 0, current: None },\n            );\n        });\n        id\n    }\n\n    /// Advance the cursor. Snapshots the current row into the entry\n    /// and returns true; clears the snapshot + returns false when\n    /// exhausted. Idempotent on unknown stmt ids (returns false).\n    pub fn step_pred(stmt_id: i64) -> bool {\n        STATEMENTS.with(|s| {\n            let mut map = s.borrow_mut();\n            let Some(entry) = map.get_mut(&stmt_id) else { return false };\n            if entry.pos < entry.rows.len() {\n                entry.current = Some(entry.rows[entry.pos].clone());\n                entry.pos += 1;\n                true\n            } else {\n                entry.current = None;\n                false\n            }\n        })\n    }\n\n    /// Read an integer column from the row most recently stepped.\n    /// NULL coerces to 0 (matches Crystal/TS shims); non-Int variants\n    /// best-effort coerce.\n    pub fn column_float(stmt_id: i64, i: i64) -> f64 {\n        STATEMENTS.with(|s| {\n            let map = s.borrow();\n            let Some(entry) = map.get(&stmt_id) else { return 0.0 };\n            let Some(row) = entry.current.as_ref() else { return 0.0 };\n            match row.get(i as usize) {\n                Some(Value::Real(v)) => *v,\n                Some(Value::Integer(v)) => *v as f64,\n                Some(Value::Text(t)) => t.parse().unwrap_or(0.0),\n                _ => 0.0,\n            }\n        })\n    }\n\n    pub fn column_int(stmt_id: i64, i: i64) -> i64 {\n        STATEMENTS.with(|s| {\n            let map = s.borrow();\n            let Some(entry) = map.get(&stmt_id) else { return 0 };\n            let Some(row) = entry.current.as_ref() else { return 0 };\n            match row.get(i as usize) {\n                Some(Value::Integer(v)) => *v,\n                Some(Value::Real(v)) => *v as i64,\n                Some(Value::Text(t)) => t.parse().unwrap_or(0),\n                _ => 0,\n            }\n        })\n    }\n\n    /// Read a text column. NULL → \"\" (matches Crystal/TS); numeric\n    /// variants stringify.\n    pub fn column_text(stmt_id: i64, i: i64) -> String {\n        STATEMENTS.with(|s| {\n            let map = s.borrow();\n            let Some(entry) = map.get(&stmt_id) else { return String::new() };\n            let Some(row) = entry.current.as_ref() else { return String::new() };\n            match row.get(i as usize) {\n                Some(Value::Text(t)) => t.clone(),\n                Some(Value::Integer(v)) => v.to_string(),\n                Some(Value::Real(v)) => v.to_string(),\n                Some(Value::Blob(b)) => String::from_utf8_lossy(b).into_owned(),\n                _ => String::new(),\n            }\n        })\n    }\n\n    /// Read a boolean column. SQLite stores booleans as 0/1 integers\n    /// — widen to bool. Nulls coerce to false.\n    pub fn column_bool(stmt_id: i64, i: i64) -> bool {\n        Self::column_int(stmt_id, i) != 0\n    }\n\n    /// Nullable-column reads. A column the schema declares nullable\n    /// holds NULL until something sets it, and NULL is not the type's\n    /// zero: \"\" in a nullable UNIQUE column collides row-to-row, and 0\n    /// in a nullable fk makes `where(fk: nil)` match nothing. The\n    /// lowerer emits these for those columns; the readers above stay\n    /// as they are for NOT NULL columns.\n    fn column_is_null(stmt_id: i64, i: i64) -> bool {\n        STATEMENTS.with(|s| {\n            let map = s.borrow();\n            let Some(entry) = map.get(&stmt_id) else { return true };\n            let Some(row) = entry.current.as_ref() else { return true };\n            matches!(row.get(i as usize), None | Some(Value::Null))\n        })\n    }\n\n    pub fn column_int_opt(stmt_id: i64, i: i64) -> Option<i64> {\n        if Self::column_is_null(stmt_id, i) {\n            None\n        } else {\n            Some(Self::column_int(stmt_id, i))\n        }\n    }\n\n    pub fn column_float_opt(stmt_id: i64, i: i64) -> Option<f64> {\n        if Self::column_is_null(stmt_id, i) {\n            None\n        } else {\n            Some(Self::column_float(stmt_id, i))\n        }\n    }\n\n    pub fn column_text_opt(stmt_id: i64, i: i64) -> Option<String> {\n        if Self::column_is_null(stmt_id, i) {\n            None\n        } else {\n            Some(Self::column_text(stmt_id, i))\n        }\n    }\n\n    pub fn column_bool_opt(stmt_id: i64, i: i64) -> Option<bool> {\n        Self::column_int_opt(stmt_id, i).map(|v| v != 0)\n    }\n\n    /// Drop the stmt-table entry. Idempotent on unknown ids.\n    pub fn finalize(stmt_id: i64) {\n        STATEMENTS.with(|s| {\n            s.borrow_mut().remove(&stmt_id);\n        });\n    }\n\n    /// Last-row-id from the most recent `exec`. SQLite-specific.\n    pub fn last_insert_rowid() -> i64 {\n        LAST_INSERT_ROWID.with(|c| c.get())\n    }\n\n    /// SQL-quote a string literal. Single quotes doubled per SQLite's\n    /// escape rule; no other byte transforms.\n    pub fn escape_string(s: &str) -> String {\n        let mut out = String::with_capacity(s.len() + 2);\n        out.push('\\'');\n        for ch in s.chars() {\n            if ch == '\\'' {\n                out.push_str(\"''\");\n            } else {\n                out.push(ch);\n            }\n        }\n        out.push('\\'');\n        out\n    }\n\n    /// Render an integer for SQL inlining.\n    pub fn escape_int(n: i64) -> String {\n        n.to_string()\n    }\n\n    /// Render an integer list for `IN (...)` eager-load batches (issue\n    /// #27). An empty list yields \"NULL\" so `IN (NULL)` is valid SQL\n    /// matching no rows — an empty `IN ()` is a syntax error.\n    /// Nullable-column writes: `None` renders the SQL keyword NULL\n    /// rather than `''` / `0`, so a nullable column round-trips as\n    /// None.\n    pub fn escape_string_opt(s: Option<&str>) -> String {\n        match s {\n            Some(v) => Self::escape_string(v),\n            None => \"NULL\".to_string(),\n        }\n    }\n\n    pub fn escape_int_opt(n: Option<i64>) -> String {\n        match n {\n            Some(v) => Self::escape_int(v),\n            None => \"NULL\".to_string(),\n        }\n    }\n\n    pub fn escape_float_opt(f: Option<f64>) -> String {\n        match f {\n            Some(v) => v.to_string(),\n            None => \"NULL\".to_string(),\n        }\n    }\n\n    pub fn escape_bool_opt(b: Option<bool>) -> String {\n        match b {\n            Some(v) => Self::escape_bool(v),\n            None => \"NULL\".to_string(),\n        }\n    }\n\n    pub fn escape_int_list(ids: Vec<i64>) -> String {\n        if ids.is_empty() {\n            return \"NULL\".to_string();\n        }\n        ids.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(\", \")\n    }\n\n    /// SQLite stores booleans as 0/1 integers.\n    pub fn escape_bool(b: bool) -> String {\n        (if b { \"1\" } else { \"0\" }).to_string()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    const TINY_SCHEMA: &str = r#\"\nCREATE TABLE widgets (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  name TEXT\n);\n\"#;\n\n    #[test]\n    fn setup_installs_connection_with_schema() {\n        setup_test_db(TINY_SCHEMA);\n        let row_count = with_conn(|c| {\n            c.execute(\"INSERT INTO widgets (name) VALUES ('a'), ('b')\", [])\n                .expect(\"insert\")\n        });\n        assert_eq!(row_count, 2);\n        let count: i64 = with_conn(|c| {\n            c.query_row(\"SELECT COUNT(*) FROM widgets\", [], |r| r.get(0))\n                .expect(\"count\")\n        });\n        assert_eq!(count, 2);\n    }\n\n    #[test]\n    fn production_pool_serves_parallel_readers() {\n        // Force a 4-slot pool against per-conn `:memory:` databases.\n        // Each slot is independent, so we use a query that doesn't\n        // depend on shared state — purpose is to confirm `with_conn`\n        // can hand out slots concurrently without deadlocking, and\n        // that `try_lock` picks an idle slot under contention.\n        open_production_pool(\":memory:\", TINY_SCHEMA, 4);\n\n        let handles: Vec<_> = (0..16)\n            .map(|i| {\n                std::thread::spawn(move || {\n                    // Read a literal so each slot's empty :memory: is fine.\n                    let n: i64 = with_conn(|c| {\n                        c.query_row(\"SELECT ?1 + 1\", [i as i64], |r| r.get(0))\n                            .expect(\"query\")\n                    });\n                    assert_eq!(n, i as i64 + 1);\n                })\n            })\n            .collect();\n        for h in handles {\n            h.join().expect(\"worker join\");\n        }\n    }\n\n    #[test]\n    fn setup_replaces_previous_connection() {\n        setup_test_db(TINY_SCHEMA);\n        with_conn(|c| {\n            c.execute(\"INSERT INTO widgets (name) VALUES ('stale')\", [])\n                .expect(\"insert\")\n        });\n        setup_test_db(TINY_SCHEMA);\n        let count: i64 = with_conn(|c| {\n            c.query_row(\"SELECT COUNT(*) FROM widgets\", [], |r| r.get(0))\n                .expect(\"count\")\n        });\n        assert_eq!(count, 0, \"new connection should start empty\");\n    }\n}\n"},{"path":"src/errors_ext.rs","content":"//! Per-target error/raise primitives the transpiled framework runtime\n//! reaches via bare-token emit.\n//!\n//! `raise(KIND, payload)` is the Ruby-shape `raise Klass, \"...\"`\n//! emitted by the transpile pipeline. Rust has no `raise` keyword and\n//! the trio of error classes (`NotImplementedError`, `RecordNotFound`,\n//! `RecordInvalid`) doesn't transpile cleanly yet — Display + Error\n//! synthesis for `class < StandardError` is a separate emit feature.\n//!\n//! Phase 3 stub: a single `FrameworkError` enum, three module-level\n//! consts the transpile's bare tokens map to (via the `imports` field\n//! in `RUST_RUNTIME`), and a `raise` function generic over the payload\n//! type. The function returns `!` so call sites in non-Unit-returning\n//! methods compile (`!` coerces to any type).\n//!\n//! Payload is intentionally `T` (no Debug/Display bound) because the\n//! emitted code passes structs (`self`, `Base` instances) that don't\n//! derive Debug yet. Lost message content is acceptable for the\n//! contract-marker raise calls (table_name, instantiate, etc.) — the\n//! panic still surfaces the kind, which is enough to diagnose a\n//! missing subclass override.\n\n#[derive(Debug, Clone, Copy)]\npub enum FrameworkError {\n    NotImplemented,\n    RecordNotFound,\n    RecordInvalid,\n}\n\n#[allow(non_upper_case_globals)]\npub const NotImplementedError: FrameworkError = FrameworkError::NotImplemented;\n#[allow(non_upper_case_globals)]\npub const RecordNotFound: FrameworkError = FrameworkError::RecordNotFound;\n#[allow(non_upper_case_globals)]\npub const RecordInvalid: FrameworkError = FrameworkError::RecordInvalid;\n\n/// Ruby-shape `raise Klass, payload`. Panics with the framework\n/// error kind; payload is accepted but discarded.\n///\n/// Returns `!` so the caller can use it as the body of any-typed\n/// method (`fn table_name() -> String { raise(...) }` compiles).\npub fn raise<T>(kind: FrameworkError, _payload: T) -> ! {\n    panic!(\"FrameworkError::{:?}\", kind);\n}\n\n/// Placeholder for Ruby's `self.name` (class name) inside emitted\n/// class methods. The transpile lowers bare `name` calls to a free\n/// function reference; until the emit-side rewrites these to\n/// per-class string literals or `std::any::type_name::<Self>()`,\n/// this stub gives the references something to resolve to.\npub fn name() -> &'static str {\n    \"Base\"\n}\n\n// Thread-local validation-error buffer the AR shim's `save` path\n// reads through. Every emitted model's `validate` body pushes\n// user-visible messages here — rust2 emit rewrites the lowered\n// `self.errors() << msg` shape to `validation_errors_push(msg)` so\n// the messages survive the `errors() returns owned Vec<String>` shim\n// that would otherwise drop them. `save` clears the buffer before\n// running validate and reads `validation_errors_is_empty()` to\n// decide whether to persist.\n//\n// Per-thread keeps tests independent (cargo test runs tests on\n// separate threads); a global Mutex would cross-contaminate.\nthread_local! {\n    static VALIDATION_ERRORS: std::cell::RefCell<Vec<String>> =\n        std::cell::RefCell::new(Vec::new());\n}\n\npub fn validation_errors_clear() {\n    VALIDATION_ERRORS.with(|c| c.borrow_mut().clear());\n}\n\npub fn validation_errors_push(msg: String) {\n    VALIDATION_ERRORS.with(|c| c.borrow_mut().push(msg));\n}\n\npub fn validation_errors_is_empty() -> bool {\n    VALIDATION_ERRORS.with(|c| c.borrow().is_empty())\n}\n\npub fn validation_errors_snapshot() -> Vec<String> {\n    VALIDATION_ERRORS.with(|c| c.borrow().clone())\n}\n"},{"path":"src/fixtures/articles.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct ArticlesFixtures;\n\nimpl ArticlesFixtures {\n    pub fn one() -> Article {\n        Article::find(1_i64)\n    }\n\n    pub fn two() -> Article {\n        Article::find(2_i64)\n    }\n\n    pub fn _fixtures_load_bang() {\n        let mut instance = Article::new(std::collections::HashMap::new());\n        instance.set_id(1_i64);\n        instance.set_title(Some(\"Getting Started with Rails\".to_string()));\n        instance.set_body(Some(\"Rails is a web application framework running on the Ruby programming language.\".to_string()));\n        instance.save_after_validation();\n        instance = Article::new(std::collections::HashMap::new());\n        instance.set_id(2_i64);\n        instance.set_title(Some(\"Understanding MVC Architecture\".to_string()));\n        instance.set_body(Some(\"MVC stands for Model-View-Controller. Models handle data and business logic.\".to_string()));\n        instance.save_after_validation();\n    }\n}\n\n// Wedge 2c.3 bare-fn compat shims — delegate to the impl.\npub fn one() -> Article { ArticlesFixtures::one() }\npub fn two() -> Article { ArticlesFixtures::two() }\n"},{"path":"src/fixtures/comments.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct CommentsFixtures;\n\nimpl CommentsFixtures {\n    pub fn one() -> Comment {\n        Comment::find(1_i64)\n    }\n\n    pub fn two() -> Comment {\n        Comment::find(2_i64)\n    }\n\n    pub fn _fixtures_load_bang() {\n        let mut instance = Comment::new(std::collections::HashMap::new());\n        instance.set_id(1_i64);\n        instance.set_article_id(1_i64);\n        instance.set_commenter(Some(\"Alice\".to_string()));\n        instance.set_body(Some(\"Great introduction! Rails really does make development faster.\".to_string()));\n        instance.save_after_validation();\n        instance = Comment::new(std::collections::HashMap::new());\n        instance.set_id(2_i64);\n        instance.set_article_id(2_i64);\n        instance.set_commenter(Some(\"Bob\".to_string()));\n        instance.set_body(Some(\"This pattern really helps keep code organized!\".to_string()));\n        instance.save_after_validation();\n    }\n}\n\n// Wedge 2c.3 bare-fn compat shims — delegate to the impl.\npub fn one() -> Comment { CommentsFixtures::one() }\npub fn two() -> Comment { CommentsFixtures::two() }\n"},{"path":"src/fixtures/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod articles;\npub mod comments;\npub use articles::ArticlesFixtures;\npub use comments::CommentsFixtures;\n\n/// Per-test entry point. Brings up a fresh in-memory SQLite,\n/// runs the schema DDL, and loads every fixture in declaration\n/// order. Tests call this as their first line; repeat calls on\n/// the same thread reset to a clean slate.\npub fn setup() {\n    crate::db::setup_test_db(crate::schema_sql::CREATE_TABLES);\n    ArticlesFixtures::_fixtures_load_bang();\n    CommentsFixtures::_fixtures_load_bang();\n}\n"},{"path":"src/flash.rs","content":"//! ActionDispatch::Flash — per-app flash store with typed\n//! `notice`/`alert` fields plus HWIA-shape shim methods.\n//!\n//! Hand-written for rust2 Phase 3 (sibling of\n//! `runtime/ruby/action_dispatch/flash.rb` and\n//! `runtime/typescript/`'s transpiled `flash.ts`). The transpile\n//! pipeline produces broken Rust for this file's HWIA shim methods\n//! (Index trait, self-indexing); hand-writing avoids fighting those\n//! emit bugs. The struct surface matches the typed-targets contract\n//! (`is_flash_name` in `view_to_library/extra_params.rs` declares the\n//! closed `notice`/`alert` field set).\n\nuse std::collections::HashMap;\n\n#[derive(Debug, Default, Clone)]\npub struct Flash {\n    pub notice: Option<String>,\n    pub alert: Option<String>,\n}\n\nimpl Flash {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn from_persisted(other: Option<&HashMap<String, String>>) -> Self {\n        let mut flash = Self::default();\n        if let Some(map) = other {\n            if let Some(v) = map.get(\"notice\") {\n                flash.notice = Some(v.clone());\n            }\n            if let Some(v) = map.get(\"alert\") {\n                flash.alert = Some(v.clone());\n            }\n        }\n        flash\n    }\n\n    pub fn get(&self, key: &str) -> Option<String> {\n        match key {\n            \"notice\" => self.notice.clone(),\n            \"alert\" => self.alert.clone(),\n            _ => None,\n        }\n    }\n\n    pub fn set(&mut self, key: &str, value: Option<String>) {\n        match key {\n            \"notice\" => self.notice = value,\n            \"alert\" => self.alert = value,\n            _ => {}\n        }\n    }\n\n    pub fn fetch(&self, key: &str, default: Option<String>) -> Option<String> {\n        self.get(key).or(default)\n    }\n\n    pub fn key(&self, key: &str) -> bool {\n        self.get(key).is_some()\n    }\n\n    pub fn has_key(&self, key: &str) -> bool {\n        self.key(key)\n    }\n\n    pub fn include(&self, key: &str) -> bool {\n        self.key(key)\n    }\n\n    pub fn delete(&mut self, key: &str) -> Option<String> {\n        match key {\n            \"notice\" => self.notice.take(),\n            \"alert\" => self.alert.take(),\n            _ => None,\n        }\n    }\n\n    pub fn len(&self) -> usize {\n        let mut n = 0;\n        if self.notice.is_some() {\n            n += 1;\n        }\n        if self.alert.is_some() {\n            n += 1;\n        }\n        n\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.notice.is_none() && self.alert.is_none()\n    }\n\n    pub fn keys(&self) -> Vec<&'static str> {\n        let mut out = Vec::new();\n        if self.notice.is_some() {\n            out.push(\"notice\");\n        }\n        if self.alert.is_some() {\n            out.push(\"alert\");\n        }\n        out\n    }\n\n    pub fn to_h(&self) -> HashMap<String, String> {\n        let mut out = HashMap::new();\n        if let Some(v) = &self.notice {\n            out.insert(\"notice\".to_string(), v.clone());\n        }\n        if let Some(v) = &self.alert {\n            out.insert(\"alert\".to_string(), v.clone());\n        }\n        out\n    }\n}\n"},{"path":"src/hash_ext.rs","content":"//! HashMap helpers bridging Ruby's `Hash#merge` semantics.\n//!\n//! Ruby `hash.merge(other)` returns a new hash with `other`'s entries\n//! layered on top. The transpiled framework runtime emits this on\n//! HashMap-typed receivers with mixed K/V types — typically a literal\n//! built from `(&str, &str)` or `(&str, String)` pairs merged with a\n//! parameter-typed `HashMap<String, serde_json::Value>`. Generic Rust\n//! merge traits can't bridge that K/V variance.\n//!\n//! `merge_attrs` is the pragmatic landing zone: it accepts any pair-\n//! iterator on both sides whose K is `Into<String>` and V is\n//! `Into<serde_json::Value>`, and produces a unified\n//! `HashMap<String, serde_json::Value>`. That matches the\n//! transpiled-runtime usage where the merged map is consumed by\n//! `render_attrs`, `r#where`, etc. — call sites that don't need to\n//! preserve the literal's narrower K/V types.\n\nuse serde_json::Value;\nuse std::collections::HashMap;\n\npub fn merge_attrs<I1, K1, V1, I2, K2, V2>(base: I1, other: I2) -> HashMap<String, Value>\nwhere\n    I1: IntoIterator<Item = (K1, V1)>,\n    K1: Into<String>,\n    V1: Into<Value>,\n    I2: IntoIterator<Item = (K2, V2)>,\n    K2: Into<String>,\n    V2: Into<Value>,\n{\n    let mut out: HashMap<String, Value> = HashMap::new();\n    for (k, v) in base {\n        out.insert(k.into(), v.into());\n    }\n    for (k, v) in other {\n        out.insert(k.into(), v.into());\n    }\n    out\n}\n"},{"path":"src/http.rs","content":"//! Roundhouse Rust HTTP runtime.\n//!\n//! Hand-written, shipped alongside generated code (copied in by the\n//! Rust emitter as `src/http.rs`). Provides the controller-facing\n//! types + helpers the emitter assumes exist: a `Params` wrapper over\n//! Rails-style bracketed form parameters, a `Redirect` convenience, a\n//! small `ViewCtx` carrying flash + request context to views, and\n//! re-exports of axum's `Html` / `Response` / `IntoResponse` so\n//! emitted action signatures can reference them through a single\n//! `crate::http::*` path.\n//!\n//! Axum is the HTTP framework (chosen to match railcar's precedent +\n//! the surrounding Rust ecosystem's gravity). Actions return `impl\n//! IntoResponse`; form bodies extract via `axum::extract::Form`\n//! wrapping a per-controller `#[derive(Deserialize)]` struct.\n\nuse std::collections::HashMap;\n\npub use axum::response::{Html, IntoResponse, Redirect, Response};\n\n/// Wrapper over the flat HashMap form body that axum's `Form`\n/// extractor produces. Rails posts nested keys like `article[title]=\n/// Foo&article[body]=Bar`; this type provides the `.expect(scope,\n/// &[keys])` accessor used by emitted strong-params helpers and the\n/// `[key]` lookup used by `params[:id]` style access.\n#[derive(Debug, Default, Clone)]\npub struct Params {\n    inner: HashMap<String, String>,\n}\n\nimpl Params {\n    pub fn new(inner: HashMap<String, String>) -> Self {\n        Self { inner }\n    }\n\n    /// Rails `params[:id]` / `params[\"id\"]` — return the raw string\n    /// value for a top-level key. Missing keys return an empty\n    /// string (matches Rails' `params[:missing]` returning nil when\n    /// later coerced; for Phase 4d's ID parsing, use `.int(key)`).\n    pub fn get(&self, key: &str) -> &str {\n        self.inner.get(key).map(|s| s.as_str()).unwrap_or(\"\")\n    }\n\n    /// Parse a param as an `i64`. Used in place of the Ruby\n    /// `params[:id]` which is string-typed but always gets coerced\n    /// to an integer for DB lookup. Returns 0 on missing/unparsable.\n    pub fn int(&self, key: &str) -> i64 {\n        self.inner.get(key).and_then(|s| s.parse().ok()).unwrap_or(0)\n    }\n\n    /// Strong-params extractor: pull every `scope[field]` key out of\n    /// the flat form body and return a new `HashMap<String, String>`\n    /// keyed on `field`. Emitted strong-params helpers use this to\n    /// populate their typed struct's fields.\n    ///\n    /// `params.expect(article: [:title, :body])` in Rails lowers to\n    /// `params.expect(\"article\", &[\"title\", \"body\"])` in emitted\n    /// Rust, and the returned map is consumed by the model's\n    /// from-params constructor.\n    pub fn expect(&self, scope: &str, keys: &[&str]) -> HashMap<String, String> {\n        let prefix = format!(\"{scope}[\");\n        let mut out = HashMap::new();\n        for key in keys {\n            let full = format!(\"{prefix}{key}]\");\n            if let Some(v) = self.inner.get(&full) {\n                out.insert((*key).to_string(), v.clone());\n            }\n        }\n        out\n    }\n}\n\nimpl From<HashMap<String, String>> for Params {\n    fn from(inner: HashMap<String, String>) -> Self {\n        Self::new(inner)\n    }\n}\n\n/// Convenience: emit `crate::http::redirect(&path)` from a path\n/// helper's result. Wraps axum's `Redirect::to` with the 303 See\n/// Other status that Rails uses for create/update/destroy redirects.\npub fn redirect(path: &str) -> Redirect {\n    Redirect::to(path)\n}\n\n/// Convenience: emit `crate::http::html(body)` to wrap a view's\n/// String output as an HTML response. Same as `Html(body)` but one\n/// import shorter at call sites.\npub fn html(body: String) -> Html<String> {\n    Html(body)\n}\n\n/// Error response with HTTP 422 (unprocessable entity) — Rails'\n/// convention for validation failures on create/update. Emitters\n/// wrap a view render in this on the `else` branch of `@model.save`.\npub fn unprocessable(body: String) -> (axum::http::StatusCode, Html<String>) {\n    (axum::http::StatusCode::UNPROCESSABLE_ENTITY, Html(body))\n}\n\n/// Context threaded through view functions. Phase 4d minimum: flash\n/// notice (read in every view via `notice.present?`). Later: current\n/// user, csrf token, request path, locale, etc.\n#[derive(Debug, Default, Clone)]\npub struct ViewCtx {\n    pub notice: Option<String>,\n}\n\nimpl ViewCtx {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn with_notice(notice: impl Into<String>) -> Self {\n        Self { notice: Some(notice.into()) }\n    }\n}\n\n// ── rust2 controller-action response state ──────────────────────\n//\n// Rails controllers thread response data through implicit state —\n// `render`, `redirect_to`, `head`, and `response.headers[…] = …`\n// each accumulate into the controller's response object, which the\n// framework serializes to the HTTP body after the action returns.\n// Rust2's emit shape carries the controller as `impl X { pub fn\n// show(&mut self) }` — `&mut self` methods returning `()`. That\n// signature isn't compatible with axum's free-fn-extractor-then-\n// IntoResponse contract.\n//\n// Bridge: emit per-action axum wrapper free fns that clear this\n// thread-local, build the controller, call the action, then\n// translate the accumulated `ControllerResponse` into an\n// `axum::response::Response`. The AC::Base shim's `render` /\n// `render_with` / `redirect_to` / `head` methods (today no-ops\n// emitted at `src/emit/rust2.rs:~782`) become thin writers to\n// this state.\n//\n// Per-thread because axum dispatches each request on a tokio task\n// that's pinned to one thread for the duration of an action body\n// (controller bodies are sync `&mut self` methods — no `.await`\n// inside, so thread affinity holds). A future migration to async\n// action bodies would need a per-task storage shape (extension\n// types, task_local!, etc.).\n\n#[derive(Debug, Clone)]\npub struct ControllerResponse {\n    pub status: u16,\n    pub body: String,\n    pub content_type: String,\n    /// Set when `redirect_to` fires; the wrapper emits a 3xx with\n    /// this as the `Location` header instead of an HTML body.\n    pub location: Option<String>,\n}\n\nimpl Default for ControllerResponse {\n    fn default() -> Self {\n        Self {\n            status: 200,\n            body: String::new(),\n            content_type: \"text/html; charset=utf-8\".to_string(),\n            location: None,\n        }\n    }\n}\n\nthread_local! {\n    static RESPONSE: std::cell::RefCell<ControllerResponse> =\n        std::cell::RefCell::new(ControllerResponse::default());\n    static REQUEST_FORMAT: std::cell::RefCell<String> =\n        std::cell::RefCell::new(String::from(\"html\"));\n    // Flash the CURRENT action set (`redirect_to … notice:` /\n    // `flash[:x] = …`) — carried to the NEXT request via the rh_flash\n    // cookie. Only what the action set lands here; the incoming flash\n    // (loaded into the controller's `flash` field for display) is never\n    // copied in, so it naturally shows exactly once. Set synchronously\n    // inside the per-action wrapper (no `.await` between the controller\n    // body and `flash_out_take`), so thread affinity holds — same\n    // discipline as `RESPONSE`.\n    static FLASH_OUT: std::cell::RefCell<std::collections::HashMap<String, String>> =\n        std::cell::RefCell::new(std::collections::HashMap::new());\n}\n\n/// Request extension carrying the inferred format (\"html\"/\"json\").\n/// Set by the `request_format_middleware` in `server.rs` after it\n/// strips a `.json` suffix off the URI; read by the per-action axum\n/// wrappers (extracted via `axum::extract::Extension<RequestFormatExt>`)\n/// and threaded into the thread-local before the controller body runs.\n#[derive(Clone, Debug)]\npub struct RequestFormatExt(pub String);\n\n/// Stash the inferred format on the per-task thread-local. The axum\n/// wrapper calls this synchronously immediately before the controller\n/// action body — `AC::Base#request_format` (emitted as a shim method\n/// on each controller) reads it back via `request_format_get`. No\n/// `.await` between set and read, so thread affinity holds.\npub fn request_format_set(format: String) {\n    REQUEST_FORMAT.with(|r| *r.borrow_mut() = format);\n}\n\n/// Read the current request's format. Called by the controller-shim\n/// `request_format()` method; defaults to `\"html\"` if no middleware\n/// has populated it (e.g. unit tests instantiating a controller\n/// directly).\npub fn request_format_get() -> String {\n    REQUEST_FORMAT.with(|r| r.borrow().clone())\n}\n\n/// Tag every request with its inferred format (\"html\" or \"json\") as\n/// an extension before it reaches the per-action handler. The\n/// emitted router attaches this as a `.layer()` so both `axum::serve`\n/// (production) and `axum_test::TestServer` (controller tests) share\n/// one wiring path.\n///\n/// Why an Extension and not a URI rewrite: in axum 0.8 `Router::layer`\n/// wraps each route's handler — route matching + `Path<...>`\n/// extraction happens *before* the layer runs, so URI rewrites here\n/// are too late to affect routing. The router emit registers explicit\n/// `.json`-suffixed entries for parameterless paths\n/// (`src/emit/rust2.rs::render_axum_router_body`); parameterized\n/// paths capture the `.json` tail as part of the segment (e.g.\n/// `id=\"1.json\"`), and the action wrapper strips the suffix before\n/// parsing the id as `i64`. This layer just surfaces the inferred\n/// format so the `if self.request_format() == \"json\"` branch dispatches\n/// the JSON jbuilder view.\npub async fn request_format_middleware(\n    mut req: axum::extract::Request,\n    next: axum::middleware::Next,\n) -> axum::response::Response {\n    let format = if req.uri().path().ends_with(\".json\") {\n        \"json\"\n    } else {\n        \"html\"\n    };\n    req.extensions_mut().insert(RequestFormatExt(format.to_string()));\n    next.run(req).await\n}\n\n/// Reset the thread-local to defaults. Called at the top of each\n/// axum wrapper so a prior action's state doesn't leak into the\n/// current request.\npub fn response_clear() {\n    RESPONSE.with(|r| *r.borrow_mut() = ControllerResponse::default());\n    // Reset the carried-flash accumulator alongside the response so a\n    // prior request's `redirect_to … notice:` can't leak into this one.\n    FLASH_OUT.with(|f| f.borrow_mut().clear());\n}\n\n// ── flash: cookie-backed, per-session storage adapter ───────────\n//\n// The rust2 server is a storage adapter for the \"show exactly once\"\n// flash lifecycle: the action that sets a flash (`redirect_to …\n// notice:`) records it in the FLASH_OUT thread-local, which the\n// per-action wrapper writes to the `rh_flash` cookie; the follow-on\n// request reloads it into the controller's `flash` field for display\n// (via `flash_from_request`) and sets no new flash, so the cookie is\n// cleared and the notice shows once. Per-browser by construction —\n// parallel sessions never share a flash slot.\n\n/// Cookie name carrying flash between requests.\nconst FLASH_COOKIE: &str = \"rh_flash\";\n\n/// Record a flash the current action set (notice/alert only — the\n/// closed key set the lowerer recognizes). Called by the controller\n/// `redirect_to` shim (and any `flash[:x] = …` lowering). The map is\n/// swept into the response cookie by `apply_flash_cookie`.\npub fn flash_out_set(key: &str, value: &str) {\n    if key == \"notice\" || key == \"alert\" {\n        FLASH_OUT.with(|f| {\n            f.borrow_mut().insert(key.to_string(), value.to_string());\n        });\n    }\n}\n\n/// Take (and reset) the flash the action set this request. Called by\n/// the per-action wrapper after the controller body returns.\npub fn flash_out_take() -> std::collections::HashMap<String, String> {\n    FLASH_OUT.with(|f| std::mem::take(&mut *f.borrow_mut()))\n}\n\n/// Build a `Flash` from the incoming `rh_flash` cookie so views can\n/// render `notice` / `alert`. Absent or unparseable cookie → empty\n/// flash (the first request in a session carries none).\npub fn flash_from_request(headers: &axum::http::HeaderMap) -> crate::flash::Flash {\n    let map = read_flash_cookie(headers);\n    crate::flash::Flash::from_persisted(Some(&map))\n}\n\n/// Write the carried-forward flash onto the response as a `Set-Cookie`\n/// header. An empty map clears the cookie (Max-Age=0) so a notice shown\n/// once doesn't stick. Survives the `layout_wrap` middleware (which\n/// preserves response headers) and redirect pass-through.\npub fn apply_flash_cookie(\n    resp: &mut axum::response::Response,\n    persisted: &std::collections::HashMap<String, String>,\n) {\n    let cookie = if persisted.is_empty() {\n        format!(\"{FLASH_COOKIE}=; Path=/; Max-Age=0; HttpOnly\")\n    } else {\n        // Deterministic key order; values percent-encoded so the\n        // urlencoded `key=value&…` structure (and cookie-octet rules)\n        // survive arbitrary notice text.\n        let mut parts: Vec<String> = Vec::new();\n        for k in [\"notice\", \"alert\"] {\n            if let Some(v) = persisted.get(k) {\n                parts.push(format!(\"{k}={}\", pct_encode(v)));\n            }\n        }\n        format!(\"{FLASH_COOKIE}={}; Path=/; HttpOnly\", parts.join(\"&\"))\n    };\n    if let Ok(hv) = axum::http::HeaderValue::from_str(&cookie) {\n        resp.headers_mut()\n            .append(axum::http::header::SET_COOKIE, hv);\n    }\n}\n\n/// Parse the `rh_flash` cookie out of the request headers into a\n/// String-keyed map (the shape `Flash::from_persisted` reloads from).\nfn read_flash_cookie(\n    headers: &axum::http::HeaderMap,\n) -> std::collections::HashMap<String, String> {\n    let mut out = std::collections::HashMap::new();\n    let raw = match headers\n        .get(axum::http::header::COOKIE)\n        .and_then(|v| v.to_str().ok())\n    {\n        Some(s) => s,\n        None => return out,\n    };\n    // Cookie header is `name=value; name2=value2; …`. Find our jar.\n    for pair in raw.split(';') {\n        let pair = pair.trim();\n        if let Some(val) = pair.strip_prefix(\"rh_flash=\") {\n            for kv in val.split('&') {\n                if let Some((k, v)) = kv.split_once('=') {\n                    if k == \"notice\" || k == \"alert\" {\n                        let decoded = pct_decode(v);\n                        if !decoded.is_empty() {\n                            out.insert(k.to_string(), decoded);\n                        }\n                    }\n                }\n            }\n        }\n    }\n    out\n}\n\n/// Minimal percent-encoder over the unreserved set — keeps the cookie\n/// value within RFC 6265 cookie-octets and our `&`/`=` delimiters\n/// unambiguous (both are escaped when they appear in a value).\nfn pct_encode(s: &str) -> String {\n    let mut out = String::with_capacity(s.len());\n    for b in s.bytes() {\n        match b {\n            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {\n                out.push(b as char)\n            }\n            _ => out.push_str(&format!(\"%{b:02X}\")),\n        }\n    }\n    out\n}\n\n/// Inverse of `pct_encode`. Unknown/malformed `%` sequences pass\n/// through literally rather than erroring.\nfn pct_decode(s: &str) -> String {\n    let bytes = s.as_bytes();\n    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());\n    let mut i = 0;\n    while i < bytes.len() {\n        if bytes[i] == b'%' && i + 3 <= bytes.len() {\n            if let Ok(byte) = u8::from_str_radix(&s[i + 1..i + 3], 16) {\n                out.push(byte);\n                i += 3;\n                continue;\n            }\n        }\n        out.push(bytes[i]);\n        i += 1;\n    }\n    String::from_utf8_lossy(&out).into_owned()\n}\n\n/// `render(content)` — stash the body string. Defaults already\n/// have 200/text-html, so a bare render is fully wired.\npub fn response_set_body(body: String) {\n    RESPONSE.with(|r| r.borrow_mut().body = body);\n}\n\n/// `render_with(content, opts)` — body + content_type, optionally\n/// status. Honors common `opts` keys (`content_type`, `status`).\n/// Unknown keys ignored; the AC::Base shim's call site already\n/// strips the Ruby-only knobs.\npub fn response_set_body_with(body: String, content_type: Option<String>, status: Option<u16>) {\n    RESPONSE.with(|r| {\n        let mut resp = r.borrow_mut();\n        resp.body = body;\n        if let Some(ct) = content_type {\n            resp.content_type = ct;\n        }\n        if let Some(st) = status {\n            resp.status = st;\n        }\n    });\n}\n\n/// `redirect_to(path, opts)` — 303 See Other by default; the\n/// `status: :see_other` opt matches Rails' default convention for\n/// post-mutation redirects (avoids form re-submit on back/refresh).\npub fn response_set_redirect(location: String, status: u16) {\n    RESPONSE.with(|r| {\n        let mut resp = r.borrow_mut();\n        resp.status = status;\n        resp.location = Some(location);\n        resp.body = String::new();\n    });\n}\n\n/// `head(name, opts)` — Rails-style status symbol → numeric code.\n/// Body stays empty. Symbol names mirror `Rack::Utils::SYMBOL_TO_STATUS_CODE`.\npub fn response_set_head(status_name: &str, content_type: Option<String>) {\n    let code = status_name_to_code(status_name);\n    RESPONSE.with(|r| {\n        let mut resp = r.borrow_mut();\n        resp.status = code;\n        resp.body = String::new();\n        if let Some(ct) = content_type {\n            resp.content_type = ct;\n        }\n    });\n}\n\n/// Snapshot + reset — used by the per-action axum wrapper to read\n/// out the state immediately after the action returns. Returns\n/// owned value so the borrow on the thread-local is short.\npub fn response_take() -> ControllerResponse {\n    RESPONSE.with(|r| std::mem::take(&mut *r.borrow_mut()))\n}\n\n/// Translate a thread-local response into an `axum::response::Response`.\n/// Redirect-shaped state produces a 3xx with `Location`; otherwise\n/// emits the body with the recorded content-type + status.\npub fn response_into_axum(resp: ControllerResponse) -> axum::response::Response {\n    use axum::http::StatusCode;\n    use axum::response::IntoResponse;\n    let status = StatusCode::from_u16(resp.status).unwrap_or(StatusCode::OK);\n    if let Some(location) = resp.location {\n        let mut response = (status, ()).into_response();\n        if let Ok(hv) = axum::http::HeaderValue::from_str(&location) {\n            response.headers_mut().insert(axum::http::header::LOCATION, hv);\n        }\n        return response;\n    }\n    let body = resp.body;\n    let content_type = resp.content_type;\n    let mut response = (status, body).into_response();\n    if let Ok(hv) = axum::http::HeaderValue::from_str(&content_type) {\n        response\n            .headers_mut()\n            .insert(axum::http::header::CONTENT_TYPE, hv);\n    }\n    response\n}\n\n/// Public alias for `status_name_to_code` — exposed for the AC::Base\n/// shim emitted in `src/emit/rust2.rs`, which reaches it from\n/// outside the crate-private `http` module. Same semantics; just\n/// a re-export that survives module privacy.\npub fn status_name_to_code_pub(name: &str) -> u16 {\n    status_name_to_code(name)\n}\n\n/// Ruby `Object#to_s` analog. Rails' `inner_v.to_s` in\n/// `ActionView::ViewHelpers#render_attrs` ships through any\n/// Hash[String, untyped]; on strict-typed targets the `untyped`\n/// alias resolves to `serde_json::Value`, whose `Display` /\n/// `to_string()` emits a JSON serialization (so\n/// `Value::String(\"reload\").to_string()` becomes `\"\\\"reload\\\"\"`,\n/// not `reload`). Ruby's `String#to_s` is identity — bare string.\n///\n/// `RubyToS` bridges: implementations cover the three recv types\n/// rust2 emit lowers `untyped`-receiver `.to_s` Sends to (`str` /\n/// `String` / `serde_json::Value`). Rust resolves the impl at\n/// compile time via auto-deref, so the rust2 dispatch can emit\n/// `(recv).ruby_to_s()` uniformly without distinguishing closure\n/// params (genuinely `&String` at runtime, body-typer marks\n/// `Untyped`) from value-typed locals (genuinely `&Value`).\n///\n/// Used at every call site in `runtime/ruby/action_view/view_helpers.rb`\n/// that produces an attribute / data-attribute / link tag value;\n/// the lowered IR's `.to_s` Sends on Untyped recvs route through\n/// this trait by the recv-Ty-aware bridge in\n/// `src/emit/rust2/expr/send/dispatch.rs`.\npub trait RubyToS {\n    fn ruby_to_s(&self) -> String;\n}\n\nimpl RubyToS for str {\n    fn ruby_to_s(&self) -> String {\n        self.to_string()\n    }\n}\n\nimpl RubyToS for String {\n    fn ruby_to_s(&self) -> String {\n        self.clone()\n    }\n}\n\nimpl RubyToS for serde_json::Value {\n    fn ruby_to_s(&self) -> String {\n        match self {\n            serde_json::Value::String(s) => s.clone(),\n            serde_json::Value::Null => String::new(),\n            other => other.to_string(),\n        }\n    }\n}\n\n/// Translate a flat axum-`Form<HashMap<String, String>>` body into\n/// the nested params shape Rails controllers expect. Form names of\n/// the shape `article[title]=Foo` land at `params[\"article\"]\n/// [\"title\"] = \"Foo\"`; top-level names pass through unchanged.\n///\n/// One level of bracket-nesting only — scaffold blog forms don't\n/// reach deeper. The lowered `<Resource>Params::from_raw` factory\n/// always looks up a single nested scope (`params.get(resource)`)\n/// then individual fields under it, so the single-level shape\n/// covers every emitted call site today. Deep nesting\n/// (`comment[article_attributes][title]`) becomes a follow-on if\n/// `accepts_nested_attributes_for` lands.\npub fn params_from_form(\n    form: HashMap<String, String>,\n) -> HashMap<String, serde_json::Value> {\n    let mut out: HashMap<String, serde_json::Value> = HashMap::new();\n    for (k, v) in form {\n        if let (Some(open), Some(close)) = (k.find('['), k.rfind(']')) {\n            if close > open {\n                let scope = &k[..open];\n                let inner = &k[open + 1..close];\n                let entry = out\n                    .entry(scope.to_string())\n                    .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));\n                if let serde_json::Value::Object(map) = entry {\n                    map.insert(inner.to_string(), serde_json::Value::from(v));\n                    continue;\n                }\n            }\n        }\n        out.insert(k, serde_json::Value::from(v));\n    }\n    out\n}\n\n/// Rails status-symbol → HTTP code. Subset matching the names the\n/// scaffold emit reaches (`:ok`, `:no_content`, `:not_found`,\n/// `:unprocessable_entity`, `:see_other`). Unknown names fall back\n/// to 200 OK — the controller path that emits an unknown symbol is\n/// generally a bug the caller will see via behavior, not a route\n/// the framework should silently 500 on.\nfn status_name_to_code(name: &str) -> u16 {\n    match name {\n        \"ok\" => 200,\n        \"created\" => 201,\n        \"accepted\" => 202,\n        \"no_content\" => 204,\n        \"moved_permanently\" => 301,\n        \"found\" => 302,\n        \"see_other\" => 303,\n        \"not_modified\" => 304,\n        \"temporary_redirect\" => 307,\n        \"permanent_redirect\" => 308,\n        \"bad_request\" => 400,\n        \"unauthorized\" => 401,\n        \"forbidden\" => 403,\n        \"not_found\" => 404,\n        \"unprocessable_entity\" | \"unprocessable_content\" => 422,\n        \"internal_server_error\" => 500,\n        _ => 200,\n    }\n}\n"},{"path":"src/importmap.rs","content":"pub struct Importmap;\n\nimpl Importmap {\n    pub fn pins() -> Vec<serde_json::Value> {\n        vec![serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"application\"), (\"path\", \"/assets/application.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"@hotwired/turbo-rails\"), (\"path\", \"/assets/turbo.min.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"@hotwired/stimulus\"), (\"path\", \"/assets/stimulus.min.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"@hotwired/stimulus-loading\"), (\"path\", \"/assets/stimulus-loading.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"controllers/application\"), (\"path\", \"/assets/controllers/application.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"controllers/hello_controller\"), (\"path\", \"/assets/controllers/hello_controller.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"controllers\"), (\"path\", \"/assets/controllers/index.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect())]\n    }\n\n    pub fn entry() -> String {\n        (\"application\").to_string()\n    }\n}\n"},{"path":"src/inflector.rs","content":"// Generated from runtime/ruby/inflector.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\npub struct Inflector;\n\nimpl Inflector {\n    pub fn pluralize(count: i64, word: &str) -> String {\n        if count == 1_i64 { format!(\"1 {}\", word.clone()) } else { format!(\"{} {}s\", count, word.clone()) }\n    }\n}\n"},{"path":"src/json_builder.rs","content":"// Generated from runtime/ruby/json_builder.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\nuse crate::http::RubyToS;\n\nstatic ESCAPES: std::sync::LazyLock<std::collections::HashMap<&'static str, &'static str>> = std::sync::LazyLock::new(|| std::collections::HashMap::from([(\"\\\\\", \"\\\\\\\\\"), (\"\\\"\", \"\\\\\\\"\"), (\"\\n\", \"\\\\n\"), (\"\\r\", \"\\\\r\"), (\"\\t\", \"\\\\t\"), (\"\\u{8}\", \"\\\\b\"), (\"\\u{c}\", \"\\\\f\")]));\nstatic ESCAPE_PATTERN: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| regex::Regex::new(\"[\\\\\\\\\\\"\\\\n\\\\r\\\\t\\\\x08\\\\f]\").unwrap());\n\npub struct JsonBuilder;\n\nimpl JsonBuilder {\n    pub fn encode_string(s: &str) -> String {\n        ESCAPE_PATTERN.replace_all(&s, |__caps: &regex::Captures| -> String { (*ESCAPES.get(&__caps[0]).unwrap_or(&\"\")).to_string() }).into_owned()\n    }\n\n    pub fn encode_value(v: serde_json::Value) -> String {\n        if v.clone().is_null() { return (\"null\").to_string() };\n        if v.clone() == true { return (\"true\").to_string() };\n        if v.clone() == false { return (\"false\").to_string() };\n        if v.clone().is_i64() { return v.as_i64().unwrap().to_string() };\n        if v.clone().is_f64() { return v.as_f64().unwrap().to_string() };\n        if v.clone().is_string() { return format!(\"\\\"{}\\\"\", Self::encode_string(&(v.as_str().unwrap()))) };\n        format!(\"\\\"{}\\\"\", Self::encode_string(&(v.ruby_to_s())))\n    }\n\n    pub fn encode_datetime<T: crate::rh_datetime::EncodeDatetime>(v: T) -> String {\n        v.rh_encode_datetime()\n    }\n}\n"},{"path":"src/lib.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod action_controller_base;\npub mod active_record_adapter;\npub mod active_record_base;\npub mod adapter_interface;\npub mod broadcasts;\npub mod cable;\npub mod controllers;\npub mod db;\npub mod errors_ext;\n#[cfg(test)]\npub mod fixtures;\npub mod flash;\npub mod hash_ext;\npub mod http;\npub mod importmap;\npub mod inflector;\npub mod json_builder;\npub mod models;\npub mod param_value;\npub mod params;\npub mod rh_datetime;\npub mod route_helpers;\npub mod router;\npub mod schema_sql;\npub mod server;\npub mod session;\n#[cfg(test)]\npub mod test_support;\n#[cfg(test)]\npub mod tests;\npub mod view_helpers;\npub mod views;\n"},{"path":"src/main.rs","content":"// Generated by Roundhouse (rust2).\n\nuse app::{router, schema_sql, server};\n\n#[tokio::main]\nasync fn main() {\n    let db_path = std::env::var(\"DATABASE_PATH\").ok();\n    let port = std::env::var(\"PORT\").ok().and_then(|s| s.parse().ok());\n    server::start(\n        router::router(),\n        server::StartOptions {\n            db_path,\n            port,\n            schema_sql: schema_sql::CREATE_TABLES,\n            layout: Some(app::views::layouts::render_layout),\n        },\n    )\n    .await;\n}\n"},{"path":"src/models/application_record.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct ApplicationRecord;\n\nimpl ApplicationRecord {\n    pub fn abstract_pred() -> bool {\n        true\n    }\n}\n"},{"path":"src/models/article.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct Article {\n    pub id: i64,\n    pub body: Option<String>,\n    pub created_at_raw: String,\n    pub title: Option<String>,\n    pub updated_at_raw: String,\n    pub comments_cache: Vec<Comment>,\n    pub comments_loaded: bool,\n}\n\nimpl Article {\n    pub fn id(&self) -> i64 {\n        self.id\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn id_pred(&self) -> bool {\n        !(false) && self.id != 0_i64\n    }\n\n    pub fn body(&self) -> Option<String> {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: Option<String>) {\n        self.body = value\n    }\n\n    pub fn body_pred(&self) -> bool {\n        !(self.body.is_none()) && ((self.body).clone().unwrap()) != \"\"\n    }\n\n    pub fn body_previously_changed_pred(&self) -> bool {\n        !(self.saved_changes()[\"body\"].is_null())\n    }\n\n    pub fn saved_change_to_body_pred(&self) -> bool {\n        !(self.saved_changes()[\"body\"].is_null())\n    }\n\n    pub fn body_previously_was(&self) -> serde_json::Value {\n        self.attribute_previously_was(\"body\")\n    }\n\n    pub fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {\n        crate::rh_datetime::parse_db_time(&self.created_at_raw)\n    }\n\n    pub fn created_at_raw(&self) -> String {\n        self.created_at_raw.clone()\n    }\n\n    pub fn set_created_at(&mut self, value: chrono::DateTime<chrono::Utc>) {\n        self.created_at_raw = crate::rh_datetime::format_db_time(value);\n    }\n\n    pub fn set_created_at_raw(&mut self, value: &str) {\n        self.created_at_raw = (value).to_string()\n    }\n\n    pub fn created_at_pred(&self) -> bool {\n        !(false) && self.created_at_raw != \"\"\n    }\n\n    pub fn created_at_previously_changed_pred(&self) -> bool {\n        !(self.saved_changes()[\"created_at\"].is_null())\n    }\n\n    pub fn saved_change_to_created_at_pred(&self) -> bool {\n        !(self.saved_changes()[\"created_at\"].is_null())\n    }\n\n    pub fn created_at_previously_was(&self) -> serde_json::Value {\n        self.attribute_previously_was(\"created_at\")\n    }\n\n    pub fn title(&self) -> Option<String> {\n        self.title.clone()\n    }\n\n    pub fn set_title(&mut self, value: Option<String>) {\n        self.title = value\n    }\n\n    pub fn title_pred(&self) -> bool {\n        !(self.title.is_none()) && ((self.title).clone().unwrap()) != \"\"\n    }\n\n    pub fn title_previously_changed_pred(&self) -> bool {\n        !(self.saved_changes()[\"title\"].is_null())\n    }\n\n    pub fn saved_change_to_title_pred(&self) -> bool {\n        !(self.saved_changes()[\"title\"].is_null())\n    }\n\n    pub fn title_previously_was(&self) -> serde_json::Value {\n        self.attribute_previously_was(\"title\")\n    }\n\n    pub fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {\n        crate::rh_datetime::parse_db_time(&self.updated_at_raw)\n    }\n\n    pub fn updated_at_raw(&self) -> String {\n        self.updated_at_raw.clone()\n    }\n\n    pub fn set_updated_at(&mut self, value: chrono::DateTime<chrono::Utc>) {\n        self.updated_at_raw = crate::rh_datetime::format_db_time(value);\n    }\n\n    pub fn set_updated_at_raw(&mut self, value: &str) {\n        self.updated_at_raw = (value).to_string()\n    }\n\n    pub fn updated_at_pred(&self) -> bool {\n        !(false) && self.updated_at_raw != \"\"\n    }\n\n    pub fn updated_at_previously_changed_pred(&self) -> bool {\n        !(self.saved_changes()[\"updated_at\"].is_null())\n    }\n\n    pub fn saved_change_to_updated_at_pred(&self) -> bool {\n        !(self.saved_changes()[\"updated_at\"].is_null())\n    }\n\n    pub fn updated_at_previously_was(&self) -> serde_json::Value {\n        self.attribute_previously_was(\"updated_at\")\n    }\n\n    pub fn table_name() -> String {\n        (\"articles\").to_string()\n    }\n\n    pub fn schema_columns() -> Vec<String> {\n        vec![\"id\".to_string(), \"body\".to_string(), \"created_at\".to_string(), \"title\".to_string(), \"updated_at\".to_string()]\n    }\n\n    pub fn schema_time_columns() -> Vec<String> {\n        vec![\"created_at\".to_string(), \"updated_at\".to_string()]\n    }\n\n    pub fn instantiate(row: std::collections::HashMap<String, serde_json::Value>) -> Article {\n        let mut instance = Article::from_row(ArticleRow::from_raw(row.clone()));\n        instance.mark_persisted_bang();\n        instance.clone()\n    }\n\n    pub fn from_row(row: ArticleRow) -> Article {\n        let mut instance = Article::new(std::collections::HashMap::new());\n        instance.set_id(row.id());\n        instance.set_body(row.body());\n        instance.set_created_at_raw(&(row.created_at()));\n        instance.set_title(row.title());\n        instance.set_updated_at_raw(&(row.updated_at()));\n        instance._note_hydrated();\n        instance.clone()\n    }\n\n    pub fn from_stmt(stmt: i64) -> Article {\n        let mut instance = Article::new(std::collections::HashMap::new());\n        instance.set_id(Db::column_int(stmt, 0_i64));\n        instance.set_body(Db::column_text_opt(stmt, 1_i64));\n        instance.set_created_at_raw(&(Db::column_text(stmt, 2_i64)));\n        instance.set_title(Db::column_text_opt(stmt, 3_i64));\n        instance.set_updated_at_raw(&(Db::column_text(stmt, 4_i64)));\n        instance.mark_persisted_bang();\n        instance._note_hydrated();\n        instance.clone()\n    }\n\n    pub fn new(attrs: std::collections::HashMap<String, serde_json::Value>) -> Self {\n        /* TODO rust2: ExprNode::Discriminant(22) */;\n        let id = (attrs.clone().get(\"id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap();\n        let body = (attrs.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string());\n        let created_at_raw = (attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let title = (attrs.clone().get(\"title\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string());\n        let updated_at_raw = (attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let mut comments_cache: Vec<Comment> = vec![];\n        let mut comments_loaded: bool = false;\n        Self { id, body, created_at_raw, title, updated_at_raw, comments_cache, comments_loaded }\n    }\n\n    pub fn attributes(&self) -> std::collections::HashMap<String, serde_json::Value> {\n        std::collections::HashMap::from([(\"body\".to_string(), serde_json::Value::from(self.body.clone())), (\"created_at\".to_string(), serde_json::Value::from(self.created_at_raw.clone())), (\"title\".to_string(), serde_json::Value::from(self.title.clone())), (\"updated_at\".to_string(), serde_json::Value::from(self.updated_at_raw.clone()))])\n    }\n\n    pub fn get_index(&self, name: &str) -> serde_json::Value {\n        match name {\n                \"id\" => { serde_json::Value::from(self.id) },\n                \"body\" => { serde_json::Value::from(self.body.clone()) },\n                \"created_at\" => { serde_json::Value::from(self.created_at_raw.clone()) },\n                \"title\" => { serde_json::Value::from(self.title.clone()) },\n                \"updated_at\" => { serde_json::Value::from(self.updated_at_raw.clone()) }\n                _ => serde_json::Value::Null,\n            }\n    }\n\n    pub fn set_index(&mut self, name: &str, value: serde_json::Value) {\n        match name {\n                \"id\" => { self.id = (value.clone()).as_i64().unwrap() },\n                \"body\" => { self.body = (value.clone()).as_str().map(|s| s.to_string()) },\n                \"created_at\" => { self.created_at_raw = (value.clone()).as_str().unwrap().to_string().to_string() },\n                \"title\" => { self.title = (value.clone()).as_str().map(|s| s.to_string()) },\n                \"updated_at\" => { self.updated_at_raw = (value.clone()).as_str().unwrap().to_string().to_string() }\n                _ => (),\n            }\n    }\n\n    pub fn update(&mut self, attrs: std::collections::HashMap<String, serde_json::Value>) -> bool {\n        if attrs.contains_key(\"body\") { self.set_body((attrs.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string())) };\n        if attrs.contains_key(\"created_at\") { self.set_created_at_raw((attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap()) };\n        if !({ let _ = (attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap().to_string(); false }) { self.set_created_at_raw(&(crate::rh_datetime::format_db_time((attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().and_then(crate::rh_datetime::parse_db_time).unwrap()))) };\n        if attrs.contains_key(\"title\") { self.set_title((attrs.clone().get(\"title\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string())) };\n        if attrs.contains_key(\"updated_at\") { self.set_updated_at_raw((attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap()) };\n        if !({ let _ = (attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap().to_string(); false }) { self.set_updated_at_raw(&(crate::rh_datetime::format_db_time((attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().and_then(crate::rh_datetime::parse_db_time).unwrap()))) };\n        self.save()\n    }\n\n    pub fn update_bang(&mut self, attrs: std::collections::HashMap<String, serde_json::Value>) -> Article {\n        if attrs.contains_key(\"body\") { self.set_body((attrs.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string())) };\n        if attrs.contains_key(\"created_at\") { self.set_created_at_raw((attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap()) };\n        if !({ let _ = (attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap().to_string(); false }) { self.set_created_at_raw(&(crate::rh_datetime::format_db_time((attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().and_then(crate::rh_datetime::parse_db_time).unwrap()))) };\n        if attrs.contains_key(\"title\") { self.set_title((attrs.clone().get(\"title\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string())) };\n        if attrs.contains_key(\"updated_at\") { self.set_updated_at_raw((attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap()) };\n        if !({ let _ = (attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap().to_string(); false }) { self.set_updated_at_raw(&(crate::rh_datetime::format_db_time((attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().and_then(crate::rh_datetime::parse_db_time).unwrap()))) };\n        self.save_bang();\n        self.clone()\n    }\n\n    pub fn fill_timestamps(&mut self, creating: bool) {\n        let now = crate::rh_datetime::db_now();\n        self.updated_at_raw = now.clone().to_string();\n        if creating { self.created_at_raw = now.clone().to_string() };\n    }\n\n    pub fn _adapter_find_by_id(id: i64) -> Option<Article> {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, body, created_at, title, updated_at FROM articles WHERE id = \", Db::escape_int(id)), \" LIMIT 1\")));\n        let mut result: Option<Article> = None;\n        if Db::step_pred(stmt) { result = Some(Article::from_stmt(stmt)) };\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_all() -> Vec<Article> {\n        let stmt = Db::prepare(\"SELECT id, body, created_at, title, updated_at FROM articles\");\n        let mut results = vec![];\n        while Db::step_pred(stmt) {\n            results.push(Article::from_stmt(stmt))\n        };\n        Db::finalize(stmt);\n        results.clone()\n    }\n\n    pub fn _adapter_last() -> Option<Article> {\n        let stmt = Db::prepare(\"SELECT id, body, created_at, title, updated_at FROM articles ORDER BY id DESC LIMIT 1\");\n        let mut result: Option<Article> = None;\n        if Db::step_pred(stmt) { result = Some(Article::from_stmt(stmt)) };\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_insert(&self) -> i64 {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"INSERT INTO articles (body, created_at, title, updated_at) VALUES (\", Db::escape_string_opt(self.body.as_deref())), \", \"), Db::escape_string(&(self.created_at_raw))), \", \"), Db::escape_string_opt(self.title.as_deref())), \", \"), Db::escape_string(&(self.updated_at_raw))), \")\")));\n        Db::last_insert_rowid()\n    }\n\n    pub fn _adapter_update(&self) {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"UPDATE articles SET body = \", Db::escape_string_opt(self.body.as_deref())), \", created_at = \"), Db::escape_string(&(self.created_at_raw))), \", title = \"), Db::escape_string_opt(self.title.as_deref())), \", updated_at = \"), Db::escape_string(&(self.updated_at_raw))), \" WHERE id = \"), Db::escape_int(self.id))));\n    }\n\n    pub fn _adapter_delete(&self) {\n        Db::exec(&(format!(\"{}{}\", \"DELETE FROM articles WHERE id = \", Db::escape_int(self.id))));\n    }\n\n    pub fn _adapter_count() -> i64 {\n        let stmt = Db::prepare(\"SELECT COUNT(*) FROM articles\");\n        Db::step_pred(stmt);\n        let result = Db::column_int(stmt, 0_i64);\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_exists_by_id_pred(id: i64) -> bool {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT 1 FROM articles WHERE id = \", Db::escape_int(id)), \" LIMIT 1\")));\n        let result = Db::step_pred(stmt);\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_truncate() {\n        Db::exec(\"DELETE FROM articles\");\n        Db::exec(\"DELETE FROM sqlite_sequence WHERE name = 'articles'\");\n    }\n\n    pub fn delete_all() {\n        Db::exec(\"DELETE FROM articles\");\n    }\n\n    pub fn _adapter_reload(&mut self) -> Article {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, body, created_at, title, updated_at FROM articles WHERE id = \", Db::escape_int(self.id)), \" LIMIT 1\")));\n        if Db::step_pred(stmt) { { self.id = Db::column_int(stmt, 0_i64);\n        self.body = Db::column_text_opt(stmt, 1_i64);\n        self.created_at_raw = Db::column_text(stmt, 2_i64).to_string();\n        self.title = Db::column_text_opt(stmt, 3_i64);\n        self.updated_at_raw = Db::column_text(stmt, 4_i64).to_string();\n        self.mark_persisted_bang() } };\n        Db::finalize(stmt);\n        self.clone()\n    }\n\n    pub fn from_params(p: ArticleParams) -> Article {\n        let mut instance = Article::new(std::collections::HashMap::new());\n        if p.title_provided() { instance.set_title(Some(p.title())) };\n        if p.body_provided() { instance.set_body(Some(p.body())) };\n        instance.clone()\n    }\n\n    pub fn update_from_article_params(&mut self, p: ArticleParams) -> bool {\n        if !(!(p.title_provided())) { self.set_title(Some(p.title())) };\n        if !(!(p.body_provided())) { self.set_body(Some(p.body())) };\n        self.save()\n    }\n\n    pub fn update_from_article_params_bang(&mut self, p: ArticleParams) -> Article {\n        if !(!(p.title_provided())) { self.set_title(Some(p.title())) };\n        if !(!(p.body_provided())) { self.set_body(Some(p.body())) };\n        self.save_bang();\n        self.clone()\n    }\n\n    pub fn validate(&self) {\n        if self.title.is_none() || ((self.title).clone().unwrap()).is_empty() { crate::errors_ext::validation_errors_push((\"Title can't be blank\").to_string()) };\n        if self.body.is_none() || ((self.body).clone().unwrap()).is_empty() { crate::errors_ext::validation_errors_push((\"Body can't be blank\").to_string()) };\n        if !(self.body.is_none()) { { let mut len = ((self.body).clone().unwrap()).len() as i64;\n        if len < 10_i64 { crate::errors_ext::validation_errors_push((\"Body is too short (minimum is 10 characters)\").to_string()) }; } };\n    }\n\n    pub fn comments(&self) -> Vec<Comment> {\n        if self.comments_loaded { return self.comments_cache.clone() };\n        let stmt = Db::prepare(&(format!(\"{}{}\", \"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments WHERE article_id = \", Db::escape_int(self.id))));\n        let mut results = vec![];\n        while Db::step_pred(stmt) {\n            results.push(Comment::from_stmt(stmt))\n        };\n        Db::finalize(stmt);\n        results.clone()\n    }\n\n    pub fn _preload_comments(&mut self, list: Vec<Comment>) {\n        self.comments_cache = list;\n        self.comments_loaded = true;\n    }\n\n    pub fn comments_loaded_pred(&self) -> bool {\n        self.comments_loaded\n    }\n\n    pub fn comments_target(&self) -> Vec<Comment> {\n        self.comments_cache.clone()\n    }\n\n    pub fn comment_ids(&self) -> Vec<i64> {\n        self.comments().into_iter().map(|rh_id_record| { rh_id_record.id() }).collect::<Vec<_>>()\n    }\n\n    pub fn before_destroy(&self) {\n        self.comments().iter_mut().for_each(|c| { c.destroy(); });\n    }\n\n    pub fn dom_prefix(&self) -> String {\n        (\"article\").to_string()\n    }\n\n    pub fn to_param(&self) -> String {\n        self.id.to_string()\n    }\n\n    pub fn dom_record_key(&self) -> String {\n        self.id.to_string()\n    }\n\n    pub fn cache_key(&self) -> String {\n        if self.persisted_pred() { format!(\"articles/{}\", self.id) } else { (\"articles/new\").to_string() }\n    }\n\n    pub fn cache_key_with_version(&self) -> String {\n        format!(\"{}-{}\", self.cache_key(), self.updated_at_raw)\n    }\n\n    pub fn after_create_commit(&self) {\n        Broadcasts::prepend(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", (\"articles\").to_string()), (\"html\", ViewHelpers::broadcast_render(&(ViewHelpers::begin_broadcast_render()), &(Articles::article(self.clone(), None, None))))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn after_update_commit(&self) {\n        Broadcasts::replace(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", format!(\"{}_{}\", self.dom_prefix(), self.dom_record_key())), (\"html\", ViewHelpers::broadcast_render(&(ViewHelpers::begin_broadcast_render()), &(Articles::article(self.clone(), None, None))))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn after_destroy_commit(&self) {\n        Broadcasts::remove(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", format!(\"{}_{}\", self.dom_prefix(), self.dom_record_key()))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n}\n\nimpl Article {\npub fn mark_persisted_bang(&mut self) { }\npub fn errors(&self) -> Vec<String> { crate::errors_ext::validation_errors_snapshot() }\npub fn save(&mut self) -> bool {\ncrate::errors_ext::validation_errors_clear();\nself.validate();\nif !crate::errors_ext::validation_errors_is_empty() { return false; }\nself.save_after_validation()\n}\npub fn save_after_validation(&mut self) -> bool {\nif self.id == 0 { self.id = self._adapter_insert(); self.after_create_commit(); }\nelse if Self::_adapter_exists_by_id_pred(self.id) { self._adapter_update(); self.after_update_commit(); }\nelse { let _ = self._adapter_insert(); self.after_create_commit(); }\ntrue\n}\npub fn save_bang(&mut self) -> Self {\nif !(self.save()) { crate::errors_ext::raise(crate::errors_ext::RecordInvalid, self.clone()) };\nself.clone()\n}\npub fn destroy(&mut self) { self.before_destroy(); self._adapter_delete(); self.after_destroy_commit(); }\npub fn exists_pred(id: i64) -> bool { Self::_adapter_exists_by_id_pred(id) }\npub fn persisted_pred(&self) -> bool { self.id != 0 }\npub fn find(id: i64) -> Self { Self::_adapter_find_by_id(id).expect(\"record not found\") }\npub fn count() -> i64 { Self::_adapter_count() }\npub fn all() -> Vec<Article> { Self::_adapter_all() }\npub fn last() -> Option<Article> { Self::_adapter_last() }\npub fn reload(&mut self) { let _ = self._adapter_reload(); }\npub fn saved_changes(&self) -> serde_json::Value { serde_json::Value::Object(serde_json::Map::new()) }\npub fn id_previously_changed_pred(&self) -> bool { false }\npub fn attribute_previously_was(&self, name: &str) -> serde_json::Value { let _ = name; serde_json::Value::Null }\npub fn _note_hydrated(&self) {}\npub fn create(attrs: std::collections::HashMap<String, serde_json::Value>) -> Article { let mut m = Self::new(attrs); m.save(); m }\n}\n"},{"path":"src/models/article_params.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct ArticleParams {\n    pub title_provided: bool,\n    pub body_provided: bool,\n    pub title: String,\n    pub body: String,\n}\n\nimpl ArticleParams {\n    pub fn new() -> Self {\n        let mut title_provided: bool = false;\n        let mut body_provided: bool = false;\n        let mut title: String = (\"\").to_string();\n        let mut body: String = (\"\").to_string();\n        Self { title_provided, body_provided, title, body }\n    }\n\n    pub fn title(&self) -> String {\n        self.title.clone()\n    }\n\n    pub fn set_title(&mut self, value: &str) {\n        self.title = (value).to_string()\n    }\n\n    pub fn title_provided(&self) -> bool {\n        self.title_provided\n    }\n\n    pub fn set_title_provided(&mut self, value: bool) {\n        self.title_provided = value\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: &str) {\n        self.body = (value).to_string()\n    }\n\n    pub fn body_provided(&self) -> bool {\n        self.body_provided\n    }\n\n    pub fn set_body_provided(&mut self, value: bool) {\n        self.body_provided = value\n    }\n\n    pub fn from_raw(params: std::collections::HashMap<String, ParamValue>) -> ArticleParams {\n        let sub = Params::sub(params, \"article\");\n        let mut instance = ArticleParams::new();\n        instance.set_title_provided(Params::provided(sub.clone(), \"title\"));\n        instance.set_title(&(Params::str(sub.clone(), \"title\", \"\")));\n        instance.set_body_provided(Params::provided(sub.clone(), \"body\"));\n        instance.set_body(&(Params::str(sub.clone(), \"body\", \"\")));\n        instance.clone()\n    }\n\n    pub fn get_index(&self, key: &str) -> serde_json::Value {\n        match key {\n                \"title\" => { serde_json::Value::from(self.title.clone()) },\n                \"body\" => { serde_json::Value::from(self.body.clone()) }\n                _ => serde_json::Value::Null,\n            }\n    }\n\n    pub fn to_h(&self) -> std::collections::HashMap<String, String> {\n        std::collections::HashMap::from([(\"title\".to_string(), self.title.clone()), (\"body\".to_string(), self.body.clone())])\n    }\n}\n"},{"path":"src/models/article_row.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct ArticleRow {\n    pub id: i64,\n    pub body: Option<String>,\n    pub created_at: String,\n    pub title: Option<String>,\n    pub updated_at: String,\n}\n\nimpl ArticleRow {\n    pub fn id(&self) -> i64 {\n        self.id\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn body(&self) -> Option<String> {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: Option<String>) {\n        self.body = value\n    }\n\n    pub fn created_at(&self) -> String {\n        self.created_at.clone()\n    }\n\n    pub fn set_created_at(&mut self, value: &str) {\n        self.created_at = (value).to_string()\n    }\n\n    pub fn title(&self) -> Option<String> {\n        self.title.clone()\n    }\n\n    pub fn set_title(&mut self, value: Option<String>) {\n        self.title = value\n    }\n\n    pub fn updated_at(&self) -> String {\n        self.updated_at.clone()\n    }\n\n    pub fn set_updated_at(&mut self, value: &str) {\n        self.updated_at = (value).to_string()\n    }\n\n    pub fn new() -> Self {\n        let mut id: i64 = 0_i64;\n        let mut body: Option<String> = None;\n        let mut created_at: String = (\"\").to_string();\n        let mut title: Option<String> = None;\n        let mut updated_at: String = (\"\").to_string();\n        Self { id, body, created_at, title, updated_at }\n    }\n\n    pub fn from_raw(row: std::collections::HashMap<String, serde_json::Value>) -> ArticleRow {\n        let mut instance = ArticleRow::new();\n        instance.set_id((row.clone().get(\"id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap());\n        instance.set_body((row.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string()));\n        instance.set_created_at((row.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap());\n        instance.set_title((row.clone().get(\"title\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string()));\n        instance.set_updated_at((row.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap());\n        instance.clone()\n    }\n}\n"},{"path":"src/models/comment.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct Comment {\n    pub id: i64,\n    pub article_id: i64,\n    pub body: Option<String>,\n    pub commenter: Option<String>,\n    pub created_at_raw: String,\n    pub updated_at_raw: String,\n}\n\nimpl Comment {\n    pub fn id(&self) -> i64 {\n        self.id\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn id_pred(&self) -> bool {\n        !(false) && self.id != 0_i64\n    }\n\n    pub fn article_id(&self) -> i64 {\n        self.article_id\n    }\n\n    pub fn set_article_id(&mut self, value: i64) {\n        self.article_id = value\n    }\n\n    pub fn article_id_pred(&self) -> bool {\n        !(false) && self.article_id != 0_i64\n    }\n\n    pub fn article_id_previously_changed_pred(&self) -> bool {\n        !(self.saved_changes()[\"article_id\"].is_null())\n    }\n\n    pub fn saved_change_to_article_id_pred(&self) -> bool {\n        !(self.saved_changes()[\"article_id\"].is_null())\n    }\n\n    pub fn article_id_previously_was(&self) -> serde_json::Value {\n        self.attribute_previously_was(\"article_id\")\n    }\n\n    pub fn body(&self) -> Option<String> {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: Option<String>) {\n        self.body = value\n    }\n\n    pub fn body_pred(&self) -> bool {\n        !(self.body.is_none()) && ((self.body).clone().unwrap()) != \"\"\n    }\n\n    pub fn body_previously_changed_pred(&self) -> bool {\n        !(self.saved_changes()[\"body\"].is_null())\n    }\n\n    pub fn saved_change_to_body_pred(&self) -> bool {\n        !(self.saved_changes()[\"body\"].is_null())\n    }\n\n    pub fn body_previously_was(&self) -> serde_json::Value {\n        self.attribute_previously_was(\"body\")\n    }\n\n    pub fn commenter(&self) -> Option<String> {\n        self.commenter.clone()\n    }\n\n    pub fn set_commenter(&mut self, value: Option<String>) {\n        self.commenter = value\n    }\n\n    pub fn commenter_pred(&self) -> bool {\n        !(self.commenter.is_none()) && ((self.commenter).clone().unwrap()) != \"\"\n    }\n\n    pub fn commenter_previously_changed_pred(&self) -> bool {\n        !(self.saved_changes()[\"commenter\"].is_null())\n    }\n\n    pub fn saved_change_to_commenter_pred(&self) -> bool {\n        !(self.saved_changes()[\"commenter\"].is_null())\n    }\n\n    pub fn commenter_previously_was(&self) -> serde_json::Value {\n        self.attribute_previously_was(\"commenter\")\n    }\n\n    pub fn created_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {\n        crate::rh_datetime::parse_db_time(&self.created_at_raw)\n    }\n\n    pub fn created_at_raw(&self) -> String {\n        self.created_at_raw.clone()\n    }\n\n    pub fn set_created_at(&mut self, value: chrono::DateTime<chrono::Utc>) {\n        self.created_at_raw = crate::rh_datetime::format_db_time(value);\n    }\n\n    pub fn set_created_at_raw(&mut self, value: &str) {\n        self.created_at_raw = (value).to_string()\n    }\n\n    pub fn created_at_pred(&self) -> bool {\n        !(false) && self.created_at_raw != \"\"\n    }\n\n    pub fn created_at_previously_changed_pred(&self) -> bool {\n        !(self.saved_changes()[\"created_at\"].is_null())\n    }\n\n    pub fn saved_change_to_created_at_pred(&self) -> bool {\n        !(self.saved_changes()[\"created_at\"].is_null())\n    }\n\n    pub fn created_at_previously_was(&self) -> serde_json::Value {\n        self.attribute_previously_was(\"created_at\")\n    }\n\n    pub fn updated_at(&self) -> Option<chrono::DateTime<chrono::Utc>> {\n        crate::rh_datetime::parse_db_time(&self.updated_at_raw)\n    }\n\n    pub fn updated_at_raw(&self) -> String {\n        self.updated_at_raw.clone()\n    }\n\n    pub fn set_updated_at(&mut self, value: chrono::DateTime<chrono::Utc>) {\n        self.updated_at_raw = crate::rh_datetime::format_db_time(value);\n    }\n\n    pub fn set_updated_at_raw(&mut self, value: &str) {\n        self.updated_at_raw = (value).to_string()\n    }\n\n    pub fn updated_at_pred(&self) -> bool {\n        !(false) && self.updated_at_raw != \"\"\n    }\n\n    pub fn updated_at_previously_changed_pred(&self) -> bool {\n        !(self.saved_changes()[\"updated_at\"].is_null())\n    }\n\n    pub fn saved_change_to_updated_at_pred(&self) -> bool {\n        !(self.saved_changes()[\"updated_at\"].is_null())\n    }\n\n    pub fn updated_at_previously_was(&self) -> serde_json::Value {\n        self.attribute_previously_was(\"updated_at\")\n    }\n\n    pub fn table_name() -> String {\n        (\"comments\").to_string()\n    }\n\n    pub fn schema_columns() -> Vec<String> {\n        vec![\"id\".to_string(), \"article_id\".to_string(), \"body\".to_string(), \"commenter\".to_string(), \"created_at\".to_string(), \"updated_at\".to_string()]\n    }\n\n    pub fn schema_time_columns() -> Vec<String> {\n        vec![\"created_at\".to_string(), \"updated_at\".to_string()]\n    }\n\n    pub fn instantiate(row: std::collections::HashMap<String, serde_json::Value>) -> Comment {\n        let mut instance = Comment::from_row(CommentRow::from_raw(row.clone()));\n        instance.mark_persisted_bang();\n        instance.clone()\n    }\n\n    pub fn from_row(row: CommentRow) -> Comment {\n        let mut instance = Comment::new(std::collections::HashMap::new());\n        instance.set_id(row.id());\n        instance.set_article_id(row.article_id());\n        instance.set_body(row.body());\n        instance.set_commenter(row.commenter());\n        instance.set_created_at_raw(&(row.created_at()));\n        instance.set_updated_at_raw(&(row.updated_at()));\n        instance._note_hydrated();\n        instance.clone()\n    }\n\n    pub fn from_stmt(stmt: i64) -> Comment {\n        let mut instance = Comment::new(std::collections::HashMap::new());\n        instance.set_id(Db::column_int(stmt, 0_i64));\n        instance.set_article_id(Db::column_int(stmt, 1_i64));\n        instance.set_body(Db::column_text_opt(stmt, 2_i64));\n        instance.set_commenter(Db::column_text_opt(stmt, 3_i64));\n        instance.set_created_at_raw(&(Db::column_text(stmt, 4_i64)));\n        instance.set_updated_at_raw(&(Db::column_text(stmt, 5_i64)));\n        instance.mark_persisted_bang();\n        instance._note_hydrated();\n        instance.clone()\n    }\n\n    pub fn new(attrs: std::collections::HashMap<String, serde_json::Value>) -> Self {\n        /* TODO rust2: ExprNode::Discriminant(22) */;\n        let id = (attrs.clone().get(\"id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap();\n        let article_id = (attrs.clone().get(\"article_id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap();\n        let body = (attrs.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string());\n        let commenter = (attrs.clone().get(\"commenter\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string());\n        let created_at_raw = (attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let updated_at_raw = (attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        Self { id, article_id, body, commenter, created_at_raw, updated_at_raw }\n    }\n\n    pub fn attributes(&self) -> std::collections::HashMap<String, serde_json::Value> {\n        std::collections::HashMap::from([(\"article_id\".to_string(), serde_json::Value::from(self.article_id)), (\"body\".to_string(), serde_json::Value::from(self.body.clone())), (\"commenter\".to_string(), serde_json::Value::from(self.commenter.clone())), (\"created_at\".to_string(), serde_json::Value::from(self.created_at_raw.clone())), (\"updated_at\".to_string(), serde_json::Value::from(self.updated_at_raw.clone()))])\n    }\n\n    pub fn get_index(&self, name: &str) -> serde_json::Value {\n        match name {\n                \"id\" => { serde_json::Value::from(self.id) },\n                \"article_id\" => { serde_json::Value::from(self.article_id) },\n                \"body\" => { serde_json::Value::from(self.body.clone()) },\n                \"commenter\" => { serde_json::Value::from(self.commenter.clone()) },\n                \"created_at\" => { serde_json::Value::from(self.created_at_raw.clone()) },\n                \"updated_at\" => { serde_json::Value::from(self.updated_at_raw.clone()) }\n                _ => serde_json::Value::Null,\n            }\n    }\n\n    pub fn set_index(&mut self, name: &str, value: serde_json::Value) {\n        match name {\n                \"id\" => { self.id = (value.clone()).as_i64().unwrap() },\n                \"article_id\" => { self.article_id = (value.clone()).as_i64().unwrap() },\n                \"body\" => { self.body = (value.clone()).as_str().map(|s| s.to_string()) },\n                \"commenter\" => { self.commenter = (value.clone()).as_str().map(|s| s.to_string()) },\n                \"created_at\" => { self.created_at_raw = (value.clone()).as_str().unwrap().to_string().to_string() },\n                \"updated_at\" => { self.updated_at_raw = (value.clone()).as_str().unwrap().to_string().to_string() }\n                _ => (),\n            }\n    }\n\n    pub fn update(&mut self, attrs: std::collections::HashMap<String, serde_json::Value>) -> bool {\n        if attrs.contains_key(\"article_id\") { self.set_article_id((attrs.clone().get(\"article_id\").cloned().unwrap_or(serde_json::Value::Null)).as_i64().unwrap()) };\n        if attrs.contains_key(\"body\") { self.set_body((attrs.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string())) };\n        if attrs.contains_key(\"commenter\") { self.set_commenter((attrs.clone().get(\"commenter\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string())) };\n        if attrs.contains_key(\"created_at\") { self.set_created_at_raw((attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap()) };\n        if !({ let _ = (attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap().to_string(); false }) { self.set_created_at_raw(&(crate::rh_datetime::format_db_time((attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().and_then(crate::rh_datetime::parse_db_time).unwrap()))) };\n        if attrs.contains_key(\"updated_at\") { self.set_updated_at_raw((attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap()) };\n        if !({ let _ = (attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap().to_string(); false }) { self.set_updated_at_raw(&(crate::rh_datetime::format_db_time((attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().and_then(crate::rh_datetime::parse_db_time).unwrap()))) };\n        self.save()\n    }\n\n    pub fn update_bang(&mut self, attrs: std::collections::HashMap<String, serde_json::Value>) -> Comment {\n        if attrs.contains_key(\"article_id\") { self.set_article_id((attrs.clone().get(\"article_id\").cloned().unwrap_or(serde_json::Value::Null)).as_i64().unwrap()) };\n        if attrs.contains_key(\"body\") { self.set_body((attrs.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string())) };\n        if attrs.contains_key(\"commenter\") { self.set_commenter((attrs.clone().get(\"commenter\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string())) };\n        if attrs.contains_key(\"created_at\") { self.set_created_at_raw((attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap()) };\n        if !({ let _ = (attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap().to_string(); false }) { self.set_created_at_raw(&(crate::rh_datetime::format_db_time((attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().and_then(crate::rh_datetime::parse_db_time).unwrap()))) };\n        if attrs.contains_key(\"updated_at\") { self.set_updated_at_raw((attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap()) };\n        if !({ let _ = (attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap().to_string(); false }) { self.set_updated_at_raw(&(crate::rh_datetime::format_db_time((attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().and_then(crate::rh_datetime::parse_db_time).unwrap()))) };\n        self.save_bang();\n        self.clone()\n    }\n\n    pub fn fill_timestamps(&mut self, creating: bool) {\n        let now = crate::rh_datetime::db_now();\n        self.updated_at_raw = now.clone().to_string();\n        if creating { self.created_at_raw = now.clone().to_string() };\n    }\n\n    pub fn _adapter_find_by_id(id: i64) -> Option<Comment> {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments WHERE id = \", Db::escape_int(id)), \" LIMIT 1\")));\n        let mut result: Option<Comment> = None;\n        if Db::step_pred(stmt) { result = Some(Comment::from_stmt(stmt)) };\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_all() -> Vec<Comment> {\n        let stmt = Db::prepare(\"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments\");\n        let mut results = vec![];\n        while Db::step_pred(stmt) {\n            results.push(Comment::from_stmt(stmt))\n        };\n        Db::finalize(stmt);\n        results.clone()\n    }\n\n    pub fn _adapter_last() -> Option<Comment> {\n        let stmt = Db::prepare(\"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments ORDER BY id DESC LIMIT 1\");\n        let mut result: Option<Comment> = None;\n        if Db::step_pred(stmt) { result = Some(Comment::from_stmt(stmt)) };\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_insert(&self) -> i64 {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"INSERT INTO comments (article_id, body, commenter, created_at, updated_at) VALUES (\", Db::escape_int(self.article_id)), \", \"), Db::escape_string_opt(self.body.as_deref())), \", \"), Db::escape_string_opt(self.commenter.as_deref())), \", \"), Db::escape_string(&(self.created_at_raw))), \", \"), Db::escape_string(&(self.updated_at_raw))), \")\")));\n        Db::last_insert_rowid()\n    }\n\n    pub fn _adapter_update(&self) {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"UPDATE comments SET article_id = \", Db::escape_int(self.article_id)), \", body = \"), Db::escape_string_opt(self.body.as_deref())), \", commenter = \"), Db::escape_string_opt(self.commenter.as_deref())), \", created_at = \"), Db::escape_string(&(self.created_at_raw))), \", updated_at = \"), Db::escape_string(&(self.updated_at_raw))), \" WHERE id = \"), Db::escape_int(self.id))));\n    }\n\n    pub fn _adapter_delete(&self) {\n        Db::exec(&(format!(\"{}{}\", \"DELETE FROM comments WHERE id = \", Db::escape_int(self.id))));\n    }\n\n    pub fn _adapter_count() -> i64 {\n        let stmt = Db::prepare(\"SELECT COUNT(*) FROM comments\");\n        Db::step_pred(stmt);\n        let result = Db::column_int(stmt, 0_i64);\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_exists_by_id_pred(id: i64) -> bool {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT 1 FROM comments WHERE id = \", Db::escape_int(id)), \" LIMIT 1\")));\n        let result = Db::step_pred(stmt);\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_truncate() {\n        Db::exec(\"DELETE FROM comments\");\n        Db::exec(\"DELETE FROM sqlite_sequence WHERE name = 'comments'\");\n    }\n\n    pub fn delete_all() {\n        Db::exec(\"DELETE FROM comments\");\n    }\n\n    pub fn _adapter_reload(&mut self) -> Comment {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments WHERE id = \", Db::escape_int(self.id)), \" LIMIT 1\")));\n        if Db::step_pred(stmt) { { self.id = Db::column_int(stmt, 0_i64);\n        self.article_id = Db::column_int(stmt, 1_i64);\n        self.body = Db::column_text_opt(stmt, 2_i64);\n        self.commenter = Db::column_text_opt(stmt, 3_i64);\n        self.created_at_raw = Db::column_text(stmt, 4_i64).to_string();\n        self.updated_at_raw = Db::column_text(stmt, 5_i64).to_string();\n        self.mark_persisted_bang() } };\n        Db::finalize(stmt);\n        self.clone()\n    }\n\n    pub fn from_params(p: CommentParams) -> Comment {\n        let mut instance = Comment::new(std::collections::HashMap::new());\n        if p.commenter_provided() { instance.set_commenter(Some(p.commenter())) };\n        if p.body_provided() { instance.set_body(Some(p.body())) };\n        instance.clone()\n    }\n\n    pub fn update_from_comment_params(&mut self, p: CommentParams) -> bool {\n        if !(!(p.commenter_provided())) { self.set_commenter(Some(p.commenter())) };\n        if !(!(p.body_provided())) { self.set_body(Some(p.body())) };\n        self.save()\n    }\n\n    pub fn update_from_comment_params_bang(&mut self, p: CommentParams) -> Comment {\n        if !(!(p.commenter_provided())) { self.set_commenter(Some(p.commenter())) };\n        if !(!(p.body_provided())) { self.set_body(Some(p.body())) };\n        self.save_bang();\n        self.clone()\n    }\n\n    pub fn validate(&self) {\n        if self.commenter.is_none() || ((self.commenter).clone().unwrap()).is_empty() { crate::errors_ext::validation_errors_push((\"Commenter can't be blank\").to_string()) };\n        if self.body.is_none() || ((self.body).clone().unwrap()).is_empty() { crate::errors_ext::validation_errors_push((\"Body can't be blank\").to_string()) };\n        if false || self.article_id == 0_i64 || !(Article::exists_pred(self.article_id)) { crate::errors_ext::validation_errors_push((\"Article must exist\").to_string()) };\n    }\n\n    pub fn article(&self) -> Option<Article> {\n        if !(self.article_id == 0_i64) { { let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, body, created_at, title, updated_at FROM articles WHERE id = \", Db::escape_int(self.article_id)), \" LIMIT 1\")));\n        let mut result: Option<Article> = None;\n        if Db::step_pred(stmt) { result = Some(Article::from_stmt(stmt)) };\n        Db::finalize(stmt);\n        result } } else { None }\n    }\n\n    pub fn set_article(&mut self, value: Option<Article>) {\n        if value.is_none() { self.article_id = 0_i64 } else { self.article_id = value.clone().unwrap().id() }\n    }\n\n    pub fn dom_prefix(&self) -> String {\n        (\"comment\").to_string()\n    }\n\n    pub fn to_param(&self) -> String {\n        self.id.to_string()\n    }\n\n    pub fn dom_record_key(&self) -> String {\n        self.id.to_string()\n    }\n\n    pub fn cache_key(&self) -> String {\n        if self.persisted_pred() { format!(\"comments/{}\", self.id) } else { (\"comments/new\").to_string() }\n    }\n\n    pub fn cache_key_with_version(&self) -> String {\n        format!(\"{}-{}\", self.cache_key(), self.updated_at_raw)\n    }\n\n    pub fn after_create_commit(&self) {\n        Broadcasts::append(std::collections::HashMap::from([(\"stream\", format!(\"article_{}_comments\", self.article_id)), (\"target\", (\"comments\").to_string()), (\"html\", ViewHelpers::broadcast_render(&(ViewHelpers::begin_broadcast_render()), &(Comments::comment(self.clone(), None, None))))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n        let Some(parent) = self.article() else { return };\n        Broadcasts::replace(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", format!(\"{}_{}\", parent.dom_prefix(), parent.dom_record_key())), (\"html\", ViewHelpers::broadcast_render(&(ViewHelpers::begin_broadcast_render()), &(Articles::article(parent.clone().clone(), None, None))))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn after_update_commit(&self) {\n        Broadcasts::replace(std::collections::HashMap::from([(\"stream\", format!(\"article_{}_comments\", self.article_id)), (\"target\", format!(\"{}_{}\", self.dom_prefix(), self.dom_record_key())), (\"html\", ViewHelpers::broadcast_render(&(ViewHelpers::begin_broadcast_render()), &(Comments::comment(self.clone(), None, None))))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn after_destroy_commit(&self) {\n        Broadcasts::remove(std::collections::HashMap::from([(\"stream\", format!(\"article_{}_comments\", self.article_id)), (\"target\", format!(\"{}_{}\", self.dom_prefix(), self.dom_record_key()))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n        let Some(parent) = self.article() else { return };\n        Broadcasts::replace(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", format!(\"{}_{}\", parent.dom_prefix(), parent.dom_record_key())), (\"html\", ViewHelpers::broadcast_render(&(ViewHelpers::begin_broadcast_render()), &(Articles::article(parent.clone().clone(), None, None))))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n}\n\nimpl Comment {\npub fn mark_persisted_bang(&mut self) { }\npub fn errors(&self) -> Vec<String> { crate::errors_ext::validation_errors_snapshot() }\npub fn save(&mut self) -> bool {\ncrate::errors_ext::validation_errors_clear();\nself.validate();\nif !crate::errors_ext::validation_errors_is_empty() { return false; }\nself.save_after_validation()\n}\npub fn save_after_validation(&mut self) -> bool {\nif self.id == 0 { self.id = self._adapter_insert(); self.after_create_commit(); }\nelse if Self::_adapter_exists_by_id_pred(self.id) { self._adapter_update(); self.after_update_commit(); }\nelse { let _ = self._adapter_insert(); self.after_create_commit(); }\ntrue\n}\npub fn save_bang(&mut self) -> Self {\nif !(self.save()) { crate::errors_ext::raise(crate::errors_ext::RecordInvalid, self.clone()) };\nself.clone()\n}\npub fn destroy(&mut self) { self._adapter_delete(); self.after_destroy_commit(); }\npub fn exists_pred(id: i64) -> bool { Self::_adapter_exists_by_id_pred(id) }\npub fn persisted_pred(&self) -> bool { self.id != 0 }\npub fn find(id: i64) -> Self { Self::_adapter_find_by_id(id).expect(\"record not found\") }\npub fn count() -> i64 { Self::_adapter_count() }\npub fn all() -> Vec<Comment> { Self::_adapter_all() }\npub fn last() -> Option<Comment> { Self::_adapter_last() }\npub fn reload(&mut self) { let _ = self._adapter_reload(); }\npub fn saved_changes(&self) -> serde_json::Value { serde_json::Value::Object(serde_json::Map::new()) }\npub fn id_previously_changed_pred(&self) -> bool { false }\npub fn attribute_previously_was(&self, name: &str) -> serde_json::Value { let _ = name; serde_json::Value::Null }\npub fn _note_hydrated(&self) {}\npub fn create(attrs: std::collections::HashMap<String, serde_json::Value>) -> Comment { let mut m = Self::new(attrs); m.save(); m }\n}\n"},{"path":"src/models/comment_params.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct CommentParams {\n    pub commenter_provided: bool,\n    pub body_provided: bool,\n    pub commenter: String,\n    pub body: String,\n}\n\nimpl CommentParams {\n    pub fn new() -> Self {\n        let mut commenter_provided: bool = false;\n        let mut body_provided: bool = false;\n        let mut commenter: String = (\"\").to_string();\n        let mut body: String = (\"\").to_string();\n        Self { commenter_provided, body_provided, commenter, body }\n    }\n\n    pub fn commenter(&self) -> String {\n        self.commenter.clone()\n    }\n\n    pub fn set_commenter(&mut self, value: &str) {\n        self.commenter = (value).to_string()\n    }\n\n    pub fn commenter_provided(&self) -> bool {\n        self.commenter_provided\n    }\n\n    pub fn set_commenter_provided(&mut self, value: bool) {\n        self.commenter_provided = value\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: &str) {\n        self.body = (value).to_string()\n    }\n\n    pub fn body_provided(&self) -> bool {\n        self.body_provided\n    }\n\n    pub fn set_body_provided(&mut self, value: bool) {\n        self.body_provided = value\n    }\n\n    pub fn from_raw(params: std::collections::HashMap<String, ParamValue>) -> CommentParams {\n        let sub = Params::sub(params, \"comment\");\n        let mut instance = CommentParams::new();\n        instance.set_commenter_provided(Params::provided(sub.clone(), \"commenter\"));\n        instance.set_commenter(&(Params::str(sub.clone(), \"commenter\", \"\")));\n        instance.set_body_provided(Params::provided(sub.clone(), \"body\"));\n        instance.set_body(&(Params::str(sub.clone(), \"body\", \"\")));\n        instance.clone()\n    }\n\n    pub fn get_index(&self, key: &str) -> serde_json::Value {\n        match key {\n                \"commenter\" => { serde_json::Value::from(self.commenter.clone()) },\n                \"body\" => { serde_json::Value::from(self.body.clone()) }\n                _ => serde_json::Value::Null,\n            }\n    }\n\n    pub fn to_h(&self) -> std::collections::HashMap<String, String> {\n        std::collections::HashMap::from([(\"commenter\".to_string(), self.commenter.clone()), (\"body\".to_string(), self.body.clone())])\n    }\n}\n"},{"path":"src/models/comment_row.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct CommentRow {\n    pub id: i64,\n    pub article_id: i64,\n    pub body: Option<String>,\n    pub commenter: Option<String>,\n    pub created_at: String,\n    pub updated_at: String,\n}\n\nimpl CommentRow {\n    pub fn id(&self) -> i64 {\n        self.id\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn article_id(&self) -> i64 {\n        self.article_id\n    }\n\n    pub fn set_article_id(&mut self, value: i64) {\n        self.article_id = value\n    }\n\n    pub fn body(&self) -> Option<String> {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: Option<String>) {\n        self.body = value\n    }\n\n    pub fn commenter(&self) -> Option<String> {\n        self.commenter.clone()\n    }\n\n    pub fn set_commenter(&mut self, value: Option<String>) {\n        self.commenter = value\n    }\n\n    pub fn created_at(&self) -> String {\n        self.created_at.clone()\n    }\n\n    pub fn set_created_at(&mut self, value: &str) {\n        self.created_at = (value).to_string()\n    }\n\n    pub fn updated_at(&self) -> String {\n        self.updated_at.clone()\n    }\n\n    pub fn set_updated_at(&mut self, value: &str) {\n        self.updated_at = (value).to_string()\n    }\n\n    pub fn new() -> Self {\n        let mut id: i64 = 0_i64;\n        let mut article_id: i64 = 0_i64;\n        let mut body: Option<String> = None;\n        let mut commenter: Option<String> = None;\n        let mut created_at: String = (\"\").to_string();\n        let mut updated_at: String = (\"\").to_string();\n        Self { id, article_id, body, commenter, created_at, updated_at }\n    }\n\n    pub fn from_raw(row: std::collections::HashMap<String, serde_json::Value>) -> CommentRow {\n        let mut instance = CommentRow::new();\n        instance.set_id((row.clone().get(\"id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap());\n        instance.set_article_id((row.clone().get(\"article_id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap());\n        instance.set_body((row.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string()));\n        instance.set_commenter((row.clone().get(\"commenter\").cloned().unwrap_or(serde_json::Value::Null)).as_str().map(|s| s.to_string()));\n        instance.set_created_at((row.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap());\n        instance.set_updated_at((row.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::Null)).as_str().unwrap());\n        instance.clone()\n    }\n}\n"},{"path":"src/models/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod application_record;\npub mod article;\npub mod article_params;\npub mod article_row;\npub mod comment;\npub mod comment_params;\npub mod comment_row;\npub use application_record::ApplicationRecord;\npub use article::Article;\npub use article_params::ArticleParams;\npub use article_row::ArticleRow;\npub use comment::Comment;\npub use comment_params::CommentParams;\npub use comment_row::CommentRow;\n"},{"path":"src/param_value.rs","content":"//! Recursive Rails-params value type — the rust analog of\n//! `runtime/typescript/param_value.ts` and `runtime/crystal/param_value.cr`.\n//!\n//! Rails request params shape as a recursive tree of String,\n//! Vec<ParamValue>, or HashMap<String, ParamValue>. The TS sibling\n//! declares it as a union of (string | string[] | { [k: string]:\n//! ParamValue }); Crystal as `alias ParamValue = String | Hash(String,\n//! ParamValue) | Array(ParamValue)`.\n//!\n//! In rust2 Phase 3 this is a re-export alias to `serde_json::Value`\n//! — same recursive shape, already familiar to every emit path that\n//! lowers `untyped`. Concrete enum can replace this later if the\n//! typed-value discipline gets tighter (Ty::Untyped reform).\n\npub type ParamValue = serde_json::Value;\n"},{"path":"src/params.rs","content":"// Narrowing accessors over the recursive request-params tree.\n//\n// `Roundhouse::ParamValue` was a per-target TYPE WITH NO OPERATIONS, so\n// every consumer narrowed at its own call site with `is_a?`, and each\n// emitter recognized that as an IDIOM (a type test in `If`-condition\n// position whose branches ARE the narrowed value) rather than modeling\n// the test. Move the same fact to any other position and it falls off a\n// cliff — measured, three spellings each broke a different target.\n//\n// So the operations live beside the type, hand-written per target the\n// way `Db` is: these bodies inspect this target's representation, which\n// is what makes them a primitive rather than framework logic.\n//\n// Semantics fixed by a Rails 8.1 oracle: `permit` keeps a blank \"\" and\n// `Parameters#compact` drops only an explicit nil. So blank counts as\n// provided; absent, JSON null, and non-scalar do not.\n\nuse crate::param_value::ParamValue;\nuse std::collections::HashMap;\n\npub struct Params;\n\nimpl Params {\n    pub fn sub(params: HashMap<String, ParamValue>, key: &str) -> HashMap<String, ParamValue> {\n        match params.get(key) {\n            Some(serde_json::Value::Object(map)) => {\n                map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()\n            }\n            _ => HashMap::new(),\n        }\n    }\n\n    pub fn str(sub: HashMap<String, ParamValue>, key: &str, fallback: &str) -> String {\n        match sub.get(key) {\n            Some(serde_json::Value::String(s)) => s.clone(),\n            _ => fallback.to_string(),\n        }\n    }\n\n    pub fn provided(sub: HashMap<String, ParamValue>, key: &str) -> bool {\n        matches!(sub.get(key), Some(serde_json::Value::String(_)))\n    }\n}\n"},{"path":"src/rh_datetime.rs","content":"// Roundhouse Rust datetime runtime — the native-`chrono::DateTime<Utc>`\n// seam for temporal (Date/DateTime/Time) columns.\n//\n// Storage stays portable ISO-8601 TEXT: a temporal column hydrates into\n// a `String` ivar (`column_text`), exactly like every other target. The\n// model's synthesized reader parses that text into a native\n// `chrono::DateTime<Utc>` via `parse_db_time` (see the\n// `ActiveSupport.parse_db_time` mapping in\n// `src/emit/rust2/expr/send/mod.rs`). JSON serialization then formats a\n// `DateTime<Utc>` back to Rails' canonical `...Z` millisecond form.\n//\n// Rust has no ad-hoc overloading, so `JsonBuilder::encode_datetime` (the\n// call site in emitted JSON views) is generic over the `EncodeDatetime`\n// trait, implemented for BOTH `Option<String>` (the stored-text form,\n// preserving the shared runtime's micro→milli reformat) and\n// `Option<chrono::DateTime<Utc>>` (the native reader form). This keeps\n// every existing call site compiling without change.\n\nuse chrono::{DateTime, NaiveDateTime, Utc};\n\n/// Parse a stored ISO-8601 value into a native UTC `DateTime`. Nil-safe:\n/// an empty stored value (SQL NULL hydrates as `\"\"`, never a Rust\n/// `Option::None` at the ivar) → `None`. Handles the two forms\n/// roundhouse ever stores:\n///\n///   * DB-dump / seed form — `\"2026-05-15 21:14:56.300213\"` (space\n///     separator, zone-less, microsecond precision, implicitly UTC).\n///   * RFC3339 form — `\"2026-05-15T21:14:56Z\"` (what `fill_timestamps`\n///     writes, and API-supplied values).\n///\n/// A value that parses under neither returns `None` rather than\n/// panicking — a malformed stored timestamp shouldn't take down a read\n/// path.\npub fn parse_db_time(s: &str) -> Option<DateTime<Utc>> {\n    if s.is_empty() {\n        return None;\n    }\n    // DB-dump / seed form: space separator, zone-less UTC. The `%.f`\n    // fraction is optional, so this covers both `...:56.300213` and a\n    // bare `...:56`; the explicit no-fraction format below is a belt-\n    // and-suspenders fallback for chrono versions that treat `%.f` as\n    // requiring the dot.\n    if let Ok(ndt) = NaiveDateTime::parse_from_str(s, \"%Y-%m-%d %H:%M:%S%.f\") {\n        return Some(ndt.and_utc());\n    }\n    if let Ok(ndt) = NaiveDateTime::parse_from_str(s, \"%Y-%m-%d %H:%M:%S\") {\n        return Some(ndt.and_utc());\n    }\n    // RFC3339 form (T separator, explicit offset / Z).\n    if let Ok(dt) = DateTime::parse_from_rfc3339(s) {\n        return Some(dt.with_timezone(&Utc));\n    }\n    None\n}\n\n/// Write-side sibling of `parse_db_time` — the `ActiveSupport.db_now`\n/// intrinsic (see the mapping in `src/emit/rust2/expr/send/mod.rs`).\n/// Returns the current UTC time in Rails' exact sqlite storage form:\n///\n///   \"YYYY-MM-DD HH:MM:SS.ffffff\"\n///\n/// — space separator, zero-padded 6-digit fractional seconds\n/// (microseconds), no zone marker. E.g. `\"2026-07-02 21:33:40.675251\"`.\n/// `fill_timestamps` stamps with this so a column's TEXT values stay\n/// homogeneous — and lexicographically ordered — when a\n/// roundhouse-emitted app shares a database with a real Rails app.\n/// chrono's `%.6f` renders the fraction *including* the leading dot.\npub fn db_now() -> String {\n    Utc::now().format(\"%Y-%m-%d %H:%M:%S%.6f\").to_string()\n}\n\n/// Write-side normalize sibling of `db_now` — the\n/// `ActiveSupport.format_db_time` intrinsic behind the synthesized\n/// public `<col>=` temporal writer. Formats a `DateTime<Utc>` to the\n/// same storage text `db_now` produces. Non-optional in and out, like\n/// `db_now`: a NOT NULL column's writer calls it directly\n/// (`String` = `String`), and a nullable column's writer maps it over\n/// its `Option` (`value.map(format_db_time)` → `Option<String>`) —\n/// the emitter picks the form from the argument's stamped optionality.\npub fn format_db_time(t: DateTime<Utc>) -> String {\n    t.format(\"%Y-%m-%d %H:%M:%S%.6f\").to_string()\n}\n\n/// Trait backing the generic `JsonBuilder::encode_datetime`. Formats a\n/// temporal value to Rails' canonical JSON shape (UTC, millisecond\n/// precision, `Z` suffix, quoted) or the literal `null`.\npub trait EncodeDatetime {\n    fn rh_encode_datetime(self) -> String;\n}\n\n/// Native reader form: a temporal column's getter yields\n/// `Option<DateTime<Utc>>`. Format to `\"2026-05-15T21:14:56.300Z\"`.\nimpl EncodeDatetime for Option<DateTime<Utc>> {\n    fn rh_encode_datetime(self) -> String {\n        match self {\n            None => \"null\".to_string(),\n            Some(dt) => format!(\"\\\"{}\\\"\", dt.to_utc().format(\"%Y-%m-%dT%H:%M:%S%.3fZ\")),\n        }\n    }\n}\n\n/// Stored-text form: pre-formatted ISO-8601 text (e.g. a value passed\n/// straight through from the DB or a hand-built JSON node). Mirrors the\n/// shared `runtime/ruby/json_builder.rb` `encode_datetime` logic —\n/// slice out date + `HH:MM:SS`, pad/truncate the fraction to exactly 3\n/// digits, and re-emit as `\"<date>T<time>.<ms>Z\"`. A too-short value is\n/// escaped and quoted verbatim.\nimpl EncodeDatetime for Option<String> {\n    fn rh_encode_datetime(self) -> String {\n        let Some(s) = self else {\n            return \"null\".to_string();\n        };\n        if s.len() < 19 {\n            return format!(\"\\\"{}\\\"\", crate::json_builder::JsonBuilder::encode_string(&s));\n        }\n        let date = &s[0..10];\n        let time = &s[11..19];\n        let mut ms = \"000\".to_string();\n        if s.len() > 20 && &s[19..20] == \".\" {\n            let frac = &s[20..];\n            let padded = format!(\"{frac}000\");\n            ms = padded[0..3].to_string();\n        }\n        format!(\"\\\"{date}T{time}.{ms}Z\\\"\")\n    }\n}\n"},{"path":"src/route_helpers.rs","content":"pub struct RouteHelpers;\n\nimpl RouteHelpers {\n    pub fn root_path() -> String {\n        (\"/\").to_string()\n    }\n\n    pub fn articles_path() -> String {\n        (\"/articles\").to_string()\n    }\n\n    pub fn new_article_path() -> String {\n        (\"/articles/new\").to_string()\n    }\n\n    pub fn article_path(id: i64) -> String {\n        format!(\"/articles/{}\", id)\n    }\n\n    pub fn edit_article_path(id: i64) -> String {\n        format!(\"/articles/{}/edit\", id)\n    }\n\n    pub fn article_comments_path(article_id: i64) -> String {\n        format!(\"/articles/{}/comments\", article_id)\n    }\n\n    pub fn article_comment_path(article_id: i64, id: i64) -> String {\n        format!(\"/articles/{}/comments/{}\", article_id, id)\n    }\n\n    pub fn article_json_path(id: i64) -> String {\n        format!(\"{}{}\", format!(\"/articles/{}\", id), \".json\")\n    }\n}\n\n// Wedge 2c.3 bare-fn compat shims — delegate to `impl RouteHelpers`.\npub fn root_path() -> String { RouteHelpers::root_path() }\npub fn articles_path() -> String { RouteHelpers::articles_path() }\npub fn new_article_path() -> String { RouteHelpers::new_article_path() }\npub fn article_path(id: i64) -> String { RouteHelpers::article_path(id) }\npub fn edit_article_path(id: i64) -> String { RouteHelpers::edit_article_path(id) }\npub fn article_comments_path(article_id: i64) -> String { RouteHelpers::article_comments_path(article_id) }\npub fn article_comment_path(article_id: i64, id: i64) -> String { RouteHelpers::article_comment_path(article_id, id) }\npub fn article_json_path(id: i64) -> String { RouteHelpers::article_json_path(id) }\n"},{"path":"src/router.rs","content":"// Generated from runtime/ruby/action_dispatch/router.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\n#[derive(Clone, Default)]\npub struct Route {\n    pub verb: String,\n    pub pattern: String,\n    pub controller: String,\n    pub action: String,\n    pub req_format: Option<String>,\n    pub int_params: String,\n    pub pattern_parts: Vec<String>,\n}\n\nimpl Route {\n    pub fn verb(&self) -> String {\n        self.verb.clone()\n    }\n\n    pub fn pattern(&self) -> String {\n        self.pattern.clone()\n    }\n\n    pub fn controller(&self) -> String {\n        self.controller.clone()\n    }\n\n    pub fn action(&self) -> String {\n        self.action.clone()\n    }\n\n    pub fn req_format(&self) -> Option<String> {\n        self.req_format.clone()\n    }\n\n    pub fn int_params(&self) -> String {\n        self.int_params.clone()\n    }\n\n    pub fn pattern_parts(&self) -> Vec<String> {\n        self.pattern_parts.clone()\n    }\n\n    pub fn new(verb: &str, pattern: &str, controller: &str, action: &str, req_format: Option<String>, int_params: &str) -> Self {\n        let mut verb: String = (verb).to_string();\n        let mut pattern: String = (pattern.clone()).to_string();\n        let mut controller: String = (controller).to_string();\n        let mut action: String = (action).to_string();\n        let mut req_format: Option<String> = req_format;\n        let mut int_params: String = (int_params).to_string();\n        let mut pattern_parts: Vec<String> = pattern.clone().split(\"/\").map(|s| s.to_string()).collect::<Vec<String>>();\n        Self { verb, pattern, controller, action, req_format, int_params, pattern_parts }\n    }\n}\n\n#[derive(Clone, Default)]\npub struct MatchResult {\n    pub controller: String,\n    pub action: String,\n    pub path_params: std::collections::HashMap<String, String>,\n    pub req_format: Option<String>,\n}\n\nimpl MatchResult {\n    pub fn controller(&self) -> String {\n        self.controller.clone()\n    }\n\n    pub fn action(&self) -> String {\n        self.action.clone()\n    }\n\n    pub fn path_params(&self) -> std::collections::HashMap<String, String> {\n        self.path_params.clone()\n    }\n\n    pub fn req_format(&self) -> Option<String> {\n        self.req_format.clone()\n    }\n\n    pub fn new(controller: &str, action: &str, path_params: std::collections::HashMap<String, String>, req_format: Option<String>) -> Self {\n        let mut controller: String = (controller).to_string();\n        let mut action: String = (action).to_string();\n        let mut path_params: std::collections::HashMap<String, String> = path_params;\n        let mut req_format: Option<String> = req_format;\n        Self { controller, action, path_params, req_format }\n    }\n}\n\npub struct Router;\n\nimpl Router {\n    pub fn r#match(method: &str, path: &str, table: Vec<Route>) -> Option<MatchResult> {\n        let mut dotted = path.clone().split(\".\").map(|s| s.to_string()).collect::<Vec<String>>();\n        if (dotted.len() as i64) >= 2_i64 { { let mut ext = dotted.clone()[((dotted.len() as i64) - 1_i64) as usize].clone();\n        if !(ext.contains(\"/\")) && Self::format_extension(&(ext.clone())) { { let mut stripped = Self::match_path(&(method.clone()), &((&path.clone()[(0_i64) as usize..((0_i64) + ((path.len() as i64) - ext.len() as i64 - 1_i64)) as usize]).to_string().to_string()), table.clone().clone(), &(ext.clone()));\n        if !(stripped.is_none()) { return Some(stripped.clone().unwrap()) } } } } };\n        Self::match_path(&(method.clone()), &(path.clone()), table.clone().clone(), \"\")\n    }\n\n    pub fn format_extension(ext: &str) -> bool {\n        let mut known = \"html text js css ics csv vcf vtt png jpeg jpg gif bmp tiff svg webp mpeg mp3 mp4 ogg webm otf ttf woff woff2 pdf zip gzip xml rss atom yaml json turbo_stream\".split(\" \").map(|s| s.to_string()).collect::<Vec<String>>();\n        let mut i = 0_i64;\n        while i < known.len() as i64 {\n            if known.clone()[(i) as usize].clone() == ext { return true };\n            /* TODO rust2: ExprNode::Discriminant(16) */\n        };\n        false\n    }\n\n    pub fn match_path(method: &str, path: &str, table: Vec<Route>, format: &str) -> Option<MatchResult> {\n        let method_upcase = method.to_string().to_uppercase();\n        let path_parts = path.split(\"/\").map(|s| s.to_string()).collect::<Vec<String>>();\n        let mut i = 0_i64;\n        while i < table.len() as i64 {\n            let mut route = table.clone()[(i) as usize].clone();\n            if route.verb().to_string() == method_upcase { { let mut params = Self::match_parts(route.pattern_parts(), path_parts.clone(), &(route.int_params()), &(format));\n            if !(params.is_none()) { return Some(MatchResult::new(&(route.controller()), &(route.action()), params.clone().unwrap(), route.req_format())) } } };\n            /* TODO rust2: ExprNode::Discriminant(16) */\n        };\n        None\n    }\n\n    pub fn match_pattern(pattern: &str, path: &str, int_params: &str) -> Option<std::collections::HashMap<String, String>> {\n        Self::match_parts(pattern.split(\"/\").map(|s| s.to_string()).collect::<Vec<String>>(), path.split(\"/\").map(|s| s.to_string()).collect::<Vec<String>>(), &(int_params), \"\")\n    }\n\n    pub fn match_parts(pattern_parts: Vec<String>, path_parts: Vec<String>, int_params: &str, format: &str) -> Option<std::collections::HashMap<String, String>> {\n        if (pattern_parts.len() as i64) != path_parts.len() as i64 { return None };\n        let mut params: std::collections::HashMap<String, String> = std::collections::HashMap::new();\n        if !(format.is_empty()) { { params.insert((\"format\").to_string(), (format.clone()).to_string()); } };\n        let mut i = 0_i64;\n        while i < pattern_parts.len() as i64 {\n            let mut pp = pattern_parts.clone()[(i) as usize].clone();\n            let mut ap = path_parts.clone()[(i) as usize].clone();\n            if pp.starts_with(\":\") { let name = &pp.clone()[(1_i64) as usize..];\n            let seg = ap.to_string();\n            if Self::int_constrained(&(int_params), &(name.clone())) && !(Self::digits_only(&(seg))) { return None };\n            { params.insert((name.clone()).to_string(), (ap.clone()).to_string()); } } else { if pp.clone() != ap.clone() { return None } };\n            /* TODO rust2: ExprNode::Discriminant(16) */\n        };\n        Some(params.clone())\n    }\n\n    pub fn int_constrained(int_params: &str, name: &str) -> bool {\n        if int_params.is_empty() { return false };\n        let mut parts = int_params.clone().split(\" \").map(|s| s.to_string()).collect::<Vec<String>>();\n        let mut i = 0_i64;\n        while i < parts.len() as i64 {\n            if parts.clone()[(i) as usize].clone() == name { return true };\n            /* TODO rust2: ExprNode::Discriminant(16) */\n        };\n        false\n    }\n\n    pub fn digits_only(s: &str) -> bool {\n        if s.is_empty() { return false };\n        let mut i = 0_i64;\n        while i < s.len() as i64 {\n            let mut c = (&s.clone()[(i) as usize..((i) + (1_i64)) as usize]).to_string().to_string();\n            if c.clone() != \"0\" && c.clone() != \"1\" && c.clone() != \"2\" && c.clone() != \"3\" && c.clone() != \"4\" && c.clone() != \"5\" && c.clone() != \"6\" && c.clone() != \"7\" && c.clone() != \"8\" && c.clone() != \"9\" { return false };\n            /* TODO rust2: ExprNode::Discriminant(16) */\n        };\n        true\n    }\n}\n\n// rust2 wedge 2c.2: concrete axum router.\n#[allow(dead_code)]\npub fn router() -> axum::Router {\n    axum::Router::new()\n        .route(\"/\", axum::routing::get(crate::controllers::articles_controller::_axum_index))\n        .route(\"/.json\", axum::routing::get(crate::controllers::articles_controller::_axum_index))\n        .route(\"/articles\", axum::routing::get(crate::controllers::articles_controller::_axum_index).post(crate::controllers::articles_controller::_axum_create))\n        .route(\"/articles.json\", axum::routing::get(crate::controllers::articles_controller::_axum_index).post(crate::controllers::articles_controller::_axum_create))\n        .route(\"/articles/new\", axum::routing::get(crate::controllers::articles_controller::_axum_new))\n        .route(\"/articles/new.json\", axum::routing::get(crate::controllers::articles_controller::_axum_new))\n        .route(\"/articles/{article_id}/comments\", axum::routing::post(crate::controllers::comments_controller::_axum_create))\n        .route(\"/articles/{article_id}/comments/{id}\", axum::routing::delete(crate::controllers::comments_controller::_axum_destroy))\n        .route(\"/articles/{id}\", axum::routing::get(crate::controllers::articles_controller::_axum_show).patch(crate::controllers::articles_controller::_axum_update).put(crate::controllers::articles_controller::_axum_update).delete(crate::controllers::articles_controller::_axum_destroy))\n        .route(\"/articles/{id}/edit\", axum::routing::get(crate::controllers::articles_controller::_axum_edit))\n        .layer(axum::middleware::from_fn(crate::http::request_format_middleware))\n}\n"},{"path":"src/schema_sql.rs","content":"// Generated by Roundhouse (rust2).\n\npub const CREATE_TABLES: &str = r#\"\nCREATE TABLE IF NOT EXISTS articles (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  body TEXT,\n  created_at TEXT NOT NULL,\n  title TEXT,\n  updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS comments (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  article_id INTEGER NOT NULL,\n  body TEXT,\n  commenter TEXT,\n  created_at TEXT NOT NULL,\n  updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS index_comments_on_article_id ON comments (article_id);\n\"#;\n"},{"path":"src/server.rs","content":"//! Roundhouse Rust server runtime.\n//!\n//! Hand-written, shipped alongside generated code (copied in by the\n//! Rust emitter as `src/server.rs`). The emitted `main.rs` calls\n//! `start(router, opts)` to open the production DB, apply\n//! schema, install middleware, and run axum.\n//!\n//! Middleware stack (outer → inner):\n//!   - `layout_wrap` — wraps HTML responses in the full document\n//!     shell (Tailwind + importmap + Action Cable meta). Mirrors\n//!     the TS runtime's `renderLayout`.\n//!   - `method_override` — Rails forms POST `_method=patch|\n//!     put|delete`; we read the form body, rewrite the request\n//!     method, and re-inject the body so downstream `axum::Form`\n//!     extractors still work.\n//!\n//! `start` also mounts `GET /cable` onto the axum router, handing\n//! the upgrade off to `crate::cable::cable_handler`. The route is\n//! always registered — apps that don't use Turbo Streams simply\n//! never receive a client connection, and the handler is cheap\n//! (one OnceLock hashmap check on subscribe).\n\nuse std::net::SocketAddr;\n\nuse axum::{\n    body::Body,\n    extract::Request,\n    http::{header, HeaderValue, Method, StatusCode},\n    middleware::{self, Next},\n    response::Response,\n    routing::get,\n    Router,\n};\nuse tower_http::services::ServeDir;\n\nuse crate::cable;\nuse crate::db;\nuse crate::view_helpers;\n\npub struct StartOptions<'a> {\n    /// File path for the sqlite DB. Defaults to\n    /// `./storage/development.sqlite3`.\n    pub db_path: Option<String>,\n    /// Listener port. Defaults to 3000 or `PORT` env var.\n    pub port: Option<u16>,\n    /// Schema SQL to apply on startup — typically\n    /// `crate::schema_sql::CREATE_TABLES`.\n    pub schema_sql: &'a str,\n    /// Layout renderer — the emitted `render_layouts_application`\n    /// (or equivalent). Called after each non-redirect response\n    /// with the inner view body already stashed via\n    /// `view_helpers::set_yield`. When `None`, the layout-wrap\n    /// middleware falls back to the minimal synthesized shell\n    /// below. Applies to apps that don't emit a layouts/\n    /// application ERB template (e.g. tiny-blog).\n    pub layout: Option<fn() -> String>,\n}\n\n/// Process-wide layout renderer, set by `start`. Read by the\n/// `layout_wrap` middleware. Axum middleware fns can't capture\n/// runtime state cleanly without boxing + extensions, so we use\n/// a static slot — the server runs one app per process.\nstatic LAYOUT_FN: std::sync::OnceLock<fn() -> String> = std::sync::OnceLock::new();\n\n/// Start the server. Opens DB, applies schema, layers middleware\n/// on top of the caller-supplied router, and runs axum until the\n/// process exits.\npub async fn start(router: Router, opts: StartOptions<'_>) {\n    let db_path = opts\n        .db_path\n        .unwrap_or_else(|| \"storage/development.sqlite3\".to_string());\n    let port: u16 = opts.port.unwrap_or_else(|| {\n        std::env::var(\"PORT\")\n            .ok()\n            .and_then(|s| s.parse().ok())\n            .unwrap_or(3000)\n    });\n\n    if let Some(layout) = opts.layout {\n        let _ = LAYOUT_FN.set(layout);\n    }\n\n    db::open_production_db(&db_path, opts.schema_sql);\n\n    // Static assets: serve `static/assets/<name>` for `/assets/*`\n    // requests via tower-http's ServeDir. Mirrors Rails' Propshaft URL\n    // shape — the importmap pins and `stylesheet_link_tag(\"tailwind\")`\n    // both point at /assets/<name>. `bin/rh transpile rust` writes the\n    // actual files (Tailwind compile output, turbo.min.js copy) into\n    // `static/assets/`. ServeDir returns 404 on miss without consuming\n    // the request, so other routes still resolve normally.\n    let app = router\n        .nest_service(\"/assets\", ServeDir::new(\"static/assets\"))\n        .route(\"/cable\", get(cable::cable_handler))\n        .layer(middleware::from_fn(layout_wrap));\n    // `method_override` must run BEFORE path+method routing. Applied as a\n    // `Router::layer` it would wrap the *matched* per-route service — but\n    // axum decides a POST to a delete-only path is a 405 during routing,\n    // so the layer (which does run) is already bound to the 405 service\n    // and rewriting the verb there can't re-route (the e2e turbo/cable\n    // DELETE cleanup 405s). Wrapping the whole router as the fallback of\n    // an outer, route-less Router puts the override ahead of all real\n    // routing, so `next.run()` re-enters routing with the corrected verb.\n    let app = Router::new()\n        .fallback_service(app)\n        .layer(middleware::from_fn(method_override));\n\n    let addr: SocketAddr = ([127, 0, 0, 1], port).into();\n    let listener = tokio::net::TcpListener::bind(addr)\n        .await\n        .expect(\"bind listener\");\n    println!(\"Roundhouse server listening on http://localhost:{}\", port);\n    axum::serve(listener, app).await.expect(\"axum serve\");\n}\n\n// ── method override middleware ─────────────────────────────────\n\n/// Rails scaffold forms submit as POST with a hidden `_method`\n/// field when the real verb is PATCH / PUT / DELETE (browsers\n/// don't natively support those in form elements). We consume the\n/// form body, check for `_method`, rewrite the request method, and\n/// re-inject the buffered body so the downstream `Form` extractor\n/// still reads the params.\nasync fn method_override(req: Request, next: Next) -> Response {\n    if req.method() != Method::POST {\n        return next.run(req).await;\n    }\n    let content_type = req\n        .headers()\n        .get(header::CONTENT_TYPE)\n        .and_then(|v| v.to_str().ok())\n        .unwrap_or(\"\")\n        .to_string();\n    if !content_type.starts_with(\"application/x-www-form-urlencoded\") {\n        return next.run(req).await;\n    }\n\n    let (mut parts, body) = req.into_parts();\n    let bytes = match axum::body::to_bytes(body, 16 * 1024 * 1024).await {\n        Ok(b) => b,\n        Err(_) => {\n            return Response::builder()\n                .status(StatusCode::BAD_REQUEST)\n                .body(Body::from(\"body too large\"))\n                .unwrap();\n        }\n    };\n\n    // Scan for `_method=<verb>` in the urlencoded body. We only\n    // look for the first hit — scaffold forms emit a single\n    // _method field; more than that would be a bug upstream.\n    let body_str = std::str::from_utf8(&bytes).unwrap_or(\"\");\n    let mut override_verb: Option<Method> = None;\n    for pair in body_str.split('&') {\n        let (k, v) = match pair.split_once('=') {\n            Some(kv) => kv,\n            None => continue,\n        };\n        if k == \"_method\" {\n            let upper = v.to_ascii_uppercase();\n            override_verb = match upper.as_str() {\n                \"PATCH\" => Some(Method::PATCH),\n                \"PUT\" => Some(Method::PUT),\n                \"DELETE\" => Some(Method::DELETE),\n                _ => None,\n            };\n            break;\n        }\n    }\n    if let Some(m) = override_verb {\n        parts.method = m;\n    }\n\n    let new_req = Request::from_parts(parts, Body::from(bytes));\n    next.run(new_req).await\n}\n\n// ── layout wrap middleware ─────────────────────────────────────\n\n/// Wrap HTML-typed response bodies in the document shell. Only\n/// touches 2xx + 422 responses with `text/html` content type;\n/// redirects pass through untouched, as do non-HTML responses (the\n/// WebSocket upgrade, any JSON endpoints).\nasync fn layout_wrap(req: Request, next: Next) -> Response {\n    // Wipe any stale yield/slot state before the handler runs.\n    // Axum's multi-thread runtime means each worker thread has\n    // its own thread-local; reset covers the current worker.\n    view_helpers::reset_render_state();\n\n    let res = next.run(req).await;\n\n    let status = res.status();\n    let is_html = res\n        .headers()\n        .get(header::CONTENT_TYPE)\n        .and_then(|v| v.to_str().ok())\n        .map(|ct| ct.starts_with(\"text/html\"))\n        .unwrap_or(false);\n\n    // Pass through non-HTML + redirects.\n    if !is_html {\n        return res;\n    }\n    if status.is_redirection() {\n        return res;\n    }\n\n    let (mut parts, body) = res.into_parts();\n    let bytes = match axum::body::to_bytes(body, 16 * 1024 * 1024).await {\n        Ok(b) => b,\n        Err(_) => {\n            return Response::builder()\n                .status(StatusCode::INTERNAL_SERVER_ERROR)\n                .body(Body::from(\"response body too large\"))\n                .unwrap();\n        }\n    };\n    let inner = std::str::from_utf8(&bytes)\n        .map(str::to_string)\n        .unwrap_or_default();\n\n    // If the app has an emitted layout (`opts.layout` was set in\n    // `start`), stash the inner body for `<%= yield %>` and invoke\n    // the layout. Otherwise fall back to the minimal synthesized\n    // shell so apps without an ERB layout still render.\n    let wrapped = if let Some(layout) = LAYOUT_FN.get() {\n        view_helpers::set_yield(&inner);\n        layout()\n    } else {\n        render_layout(&inner)\n    };\n\n    parts.headers.remove(header::CONTENT_LENGTH);\n    parts.headers.insert(\n        header::CONTENT_TYPE,\n        HeaderValue::from_static(\"text/html; charset=utf-8\"),\n    );\n    Response::from_parts(parts, Body::from(wrapped))\n}\n\n/// The document shell. Asset paths point at `/assets/tailwind.css`\n/// + `/assets/turbo.min.js`, served by `tower-http`'s `ServeDir`\n/// mounted on `/assets` (see `start`). `bin/rh transpile rust` is\n/// expected to have populated `static/assets/` with the Tailwind\n/// compile output + a copy of turbo.min.js; without that step the\n/// page is unstyled but functional. Plain `@hotwired/turbo` (not\n/// `@hotwired/turbo-rails`) avoids the latter's transitive\n/// `@rails/actioncable/src` lookup, which would 404 in the browser\n/// — our cable handler at `/cable` matches turbo's default URL.\n/// Inline data-URI favicon suppresses the no-icon 404 on each\n/// page load. Used only as a fallback when no emitter layout is\n/// supplied via `StartOptions::layout`; the emitted Layouts\n/// module overrides this for apps that have `app/views/layouts/\n/// application.{erb,rb}`.\nfn render_layout(body: &str) -> String {\n    format!(\n        r##\"<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Roundhouse App</title>\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    <link rel=\"icon\" href=\"data:,\">\n    <link rel=\"stylesheet\" href=\"/assets/tailwind.css\">\n    <script type=\"importmap\">\n    {{\n      \"imports\": {{\n        \"@hotwired/turbo\": \"/assets/turbo.min.js\"\n      }}\n    }}\n    </script>\n    <script type=\"module\">import \"@hotwired/turbo\";</script>\n  </head>\n  <body>\n    <main class=\"container mx-auto mt-8 px-5 flex flex-col\">\n      {}\n    </main>\n  </body>\n</html>\n\"##,\n        body,\n    )\n}\n"},{"path":"src/session.rs","content":"//! ActionDispatch::Session — per-app session store. Empty by default\n//! (real-blog uses no session keys); HWIA-shape shim methods route\n//! through an internal HashMap so apps that introduce session keys\n//! can grow the surface without a runtime rewrite.\n//!\n//! Hand-written for rust2 Phase 3 (sibling of\n//! `runtime/ruby/action_dispatch/session.rb`). The transpile pipeline\n//! produces broken Rust for this file's shim methods; hand-writing\n//! avoids fighting those emit bugs.\n\nuse std::collections::HashMap;\n\n#[derive(Debug, Default, Clone)]\npub struct Session {\n    data: HashMap<String, String>,\n}\n\nimpl Session {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn from_persisted(other: Option<&HashMap<String, String>>) -> Self {\n        let mut session = Self::default();\n        if let Some(map) = other {\n            for (k, v) in map {\n                session.data.insert(k.clone(), v.clone());\n            }\n        }\n        session\n    }\n\n    pub fn get(&self, key: &str) -> Option<String> {\n        self.data.get(key).cloned()\n    }\n\n    pub fn set(&mut self, key: &str, value: String) {\n        self.data.insert(key.to_string(), value);\n    }\n\n    pub fn fetch(&self, key: &str, default: Option<String>) -> Option<String> {\n        self.get(key).or(default)\n    }\n\n    pub fn key(&self, key: &str) -> bool {\n        self.data.contains_key(key)\n    }\n\n    pub fn has_key(&self, key: &str) -> bool {\n        self.key(key)\n    }\n\n    pub fn include(&self, key: &str) -> bool {\n        self.key(key)\n    }\n\n    pub fn delete(&mut self, key: &str) -> Option<String> {\n        self.data.remove(key)\n    }\n\n    pub fn len(&self) -> usize {\n        self.data.len()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.data.is_empty()\n    }\n\n    pub fn to_h(&self) -> HashMap<String, String> {\n        self.data.clone()\n    }\n}\n"},{"path":"src/test_support.rs","content":"//! Roundhouse Rust test-support runtime.\n//!\n//! Hand-written, shipped alongside generated code (copied in by the\n//! Rust emitter as `src/test_support.rs`). Emitted controller tests\n//! use this surface through the `TestResponseExt` trait, so the\n//! assertion call sites stay stable while the implementation can\n//! evolve.\n//!\n//! Phase 4d ships substring-match implementations of `assert_select`\n//! — matches railcar's choice, zero extra deps, good-enough for the\n//! scaffold blog's HTML assertions. A later upgrade to a real CSS\n//! selector engine (the `scraper` crate, `html5ever`, or similar)\n//! only needs to touch this file — emitted tests call the trait\n//! methods and are insulated from the rendering strategy.\n\nuse axum_test::TestResponse;\n\npub trait TestResponseExt {\n    /// `assert_response :success` — status 200 OK.\n    fn assert_ok(&self);\n\n    /// `assert_response :unprocessable_entity` — status 422.\n    fn assert_unprocessable(&self);\n\n    /// `assert_response <status>` for any concrete status.\n    fn assert_status(&self, code: u16);\n\n    /// `assert_redirected_to <path>`. Checks the response is a 3xx\n    /// redirect and the `Location` header matches the expected path.\n    /// Phase 4d substring-matches the path in Location to forgive\n    /// absolute-vs-relative URL differences; a stricter check can\n    /// swap in later without touching emitted tests.\n    fn assert_redirected_to(&self, path: &str);\n\n    /// `assert_select <selector>` — response body contains an\n    /// element matching the (very) loose selector form. Substring\n    /// match on the opening tag or `id=` / `class=` attribute\n    /// fragment. Covers the scaffold blog's shapes:\n    ///   \"h1\"             → contains \"<h1\"\n    ///   \"#articles\"      → contains `id=\"articles\"`\n    ///   \".p-4\"           → contains `class=\"... p-4 ...\"` (as substring)\n    ///   \"form\"           → contains \"<form\"\n    fn assert_select(&self, selector: &str);\n\n    /// `assert_select <selector>, <text>` — the `selector` check\n    /// above *and* the response body contains `text`. Phase 4d\n    /// doesn't verify the text lives inside the selector match\n    /// (would require structural parsing); a later scraper-backed\n    /// impl can tighten this.\n    fn assert_select_text(&self, selector: &str, text: &str);\n\n    /// `assert_select <selector>, minimum: N` — response body\n    /// contains at least `n` occurrences of the selector fragment.\n    /// Again substring-counted in Phase 4d.\n    fn assert_select_min(&self, selector: &str, n: usize);\n}\n\nimpl TestResponseExt for TestResponse {\n    fn assert_ok(&self) {\n        assert_eq!(self.status_code(), 200, \"expected 200 OK\");\n    }\n\n    fn assert_unprocessable(&self) {\n        assert_eq!(self.status_code(), 422, \"expected 422 Unprocessable Entity\");\n    }\n\n    fn assert_status(&self, code: u16) {\n        assert_eq!(self.status_code().as_u16(), code, \"expected status {code}\");\n    }\n\n    fn assert_redirected_to(&self, path: &str) {\n        assert!(\n            self.status_code().is_redirection(),\n            \"expected redirection, got {}\",\n            self.status_code(),\n        );\n        let location = self\n            .headers()\n            .get(axum::http::header::LOCATION)\n            .and_then(|v| v.to_str().ok())\n            .unwrap_or(\"\");\n        assert!(\n            location.contains(path),\n            \"expected Location to contain {path:?}, got {location:?}\",\n        );\n    }\n\n    fn assert_select(&self, selector: &str) {\n        let doc = dom_parse(&self.text());\n        assert!(\n            !dom_select(&doc, selector).is_empty(),\n            \"expected body to match selector {selector:?}\",\n        );\n    }\n\n    fn assert_select_text(&self, selector: &str, text: &str) {\n        let doc = dom_parse(&self.text());\n        let nodes = dom_select(&doc, selector);\n        assert!(\n            !nodes.is_empty(),\n            \"expected body to match selector {selector:?}\",\n        );\n        assert!(\n            nodes.iter().any(|n| dom_text(n).contains(text)),\n            \"expected text {text:?} under selector {selector:?}\",\n        );\n    }\n\n    fn assert_select_min(&self, selector: &str, n: usize) {\n        let doc = dom_parse(&self.text());\n        let count = dom_select(&doc, selector).len();\n        assert!(\n            count >= n,\n            \"expected at least {n} matches for selector {selector:?}, got {count}\",\n        );\n    }\n}\n\n// ── Dom primitive surface (the assert_select substrate) ────────────\n//\n// The HTML-query contract `assert_select` lowers to, shared in shape\n// with the Ruby/TS/Python/Elixir twins (cross-target contract in\n// runtime/spinel/test/test_helper.rbs). Stub: the substring matcher\n// dressed as a Dom — `dom_select` fabricates one synthetic node (the\n// whole document) per fragment occurrence and `dom_text` returns it\n// verbatim, so presence / `minimum:` / text checks degrade to exactly\n// the pre-contract behavior. The upgrade path is to swap these three\n// functions for a `scraper`/`html5ever`-backed engine — real nodes,\n// real CSS selectors — touching only this file; the `TestResponseExt`\n// call sites and every other target stay put.\n\n/// Parsed-document handle. Stub: the document *is* its html string.\ntype DomDoc = String;\n/// Matched-node handle. Stub: the html the node was found in.\ntype DomNode = String;\n\n/// Parse an HTML document.\nfn dom_parse(html: &str) -> DomDoc {\n    html.to_string()\n}\n\n/// Nodes matching `selector` within `root` (a document or node). Stub:\n/// one synthetic node (the root's html) per substring-fragment\n/// occurrence, so nested selects re-scan the whole string.\nfn dom_select(root: &str, selector: &str) -> Vec<DomNode> {\n    let fragment = selector_fragment(selector);\n    root.match_indices(&fragment).map(|_| root.to_string()).collect()\n}\n\n/// Concatenated descendant text of a node. Stub: the node's html\n/// verbatim (so a content check degrades to a body-substring check).\nfn dom_text(node: &DomNode) -> &str {\n    node\n}\n\n/// Map a loose selector to a substring fragment that probably appears\n/// in matching HTML. The stub's selector rule (replaced by a real CSS\n/// engine on upgrade): handles `#id`, `.class`, `tag`, and the first\n/// element of compound selectors like `\"#comments .p-4\"` (splits on\n/// whitespace, picks the first chunk).\nfn selector_fragment(selector: &str) -> String {\n    let first = selector.split_whitespace().next().unwrap_or(\"\");\n    if let Some(id) = first.strip_prefix('#') {\n        format!(\"id=\\\"{id}\\\"\")\n    } else if let Some(class) = first.strip_prefix('.') {\n        format!(\"{class}\\\"\")\n    } else {\n        format!(\"<{first}\")\n    }\n}\n"},{"path":"src/tests/article.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n\n#[allow(unused_imports)]\nuse crate::fixtures::*;\n#[allow(unused_imports)]\nuse crate::inflector::Inflector;\n#[allow(unused_imports)]\nuse crate::json_builder::JsonBuilder;\n#[allow(unused_imports)]\nuse crate::router::{MatchResult, Route, Router};\n// No ViewHelpers row: this block is only ever concatenated after\n// MODEL_IMPORTS, which imports `view_helpers::{self, ViewHelpers}` —\n// a second `use` of the same NAME is E0252, not an unused-import.\n#[allow(unused_imports)]\nuse crate::flash::Flash;\n#[allow(unused_imports)]\nuse crate::session::Session;\n// Generated by Roundhouse (rust2).\n\n#[test]\npub fn test_creates_an_article_with_valid_attributes() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    if { let _ = article.id(); false } { panic!(\"{}\", \"refute_nil failed\") };\n    if \"Getting Started with Rails\" != article.title().clone().unwrap_or_default() { panic!(\"{}\", \"assert_equal failed\") };\n}\n\n#[test]\npub fn test_validates_title_presence() {\n        crate::fixtures::setup();\n    let mut article = Article::new(std::collections::HashMap::from([(\"title\", \"\"), (\"body\", \"Valid body content here.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if article.save() { panic!(\"{}\", \"refute failed\") };\n}\n\n#[test]\npub fn test_validates_body_minimum_length() {\n        crate::fixtures::setup();\n    let mut article = Article::new(std::collections::HashMap::from([(\"title\", \"Valid Title\"), (\"body\", \"Short\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if article.save() { panic!(\"{}\", \"refute failed\") };\n}\n\n#[test]\npub fn test_destroys_comments_when_article_is_destroyed() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    let __diff_before = Comment::count();\n    article.destroy();\n    let mut __diff_after = Comment::count();\n    if __diff_after - __diff_before != -1_i64 { panic!(\"{}\", \"Comment.count didn't change by -1\") };\n}\n"},{"path":"src/tests/articles_controller_test.rs","content":"// Generated by Roundhouse.\n\n#[allow(unused_imports)]\nuse crate::fixtures;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::route_helpers;\n#[allow(unused_imports)]\nuse crate::test_support::TestResponseExt;\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_get_index() {\n    // \"should get index\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let resp = server.get(&route_helpers::articles_path()).await;\n    resp.assert_ok();\n    resp.assert_select_text(\"h1\", &\"Articles\");\n    resp.assert_select(\"#articles\");\n    resp.assert_select_min(\"h2\", 1_i64 as usize);\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_get_new() {\n    // \"should get new\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let resp = server.get(&route_helpers::new_article_path()).await;\n    resp.assert_ok();\n    resp.assert_select(\"form\");\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_create_article() {\n    // \"should create article\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let _before = Article::count();\n    let resp = server.post(&route_helpers::articles_path()).form(&std::collections::HashMap::<String, String>::from([(\"article[body]\".to_string(), \"A sufficiently long body for validation.\".to_string()), (\"article[title]\".to_string(), \"New Title\".to_string())])).await;\n    let _after = Article::count();\n    assert_eq!(_after - _before, 1);\n    resp.assert_redirected_to(&route_helpers::article_path(Article::last().unwrap().id));\n    assert_eq!(\"New Title\", Article::last().unwrap().title.clone().unwrap_or_default());\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_not_create_article_with_invalid_params() {\n    // \"should not create article with invalid params\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let _before = Article::count();\n    let resp = server.post(&route_helpers::articles_path()).form(&std::collections::HashMap::<String, String>::from([(\"article[title]\".to_string(), \"\".to_string()), (\"article[body]\".to_string(), \"\".to_string())])).await;\n    let _after = Article::count();\n    assert_eq!(_after - _before, 0);\n    resp.assert_unprocessable();\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_show_article() {\n    // \"should show article\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let resp = server.get(&route_helpers::article_path(article.id)).await;\n    resp.assert_ok();\n    resp.assert_select_text(\"h1\", &article.title.clone().unwrap_or_default());\n    resp.assert_select_text(\"h2\", &\"Comments\");\n    resp.assert_select_min(\"#comments .p-4\", 1_i64 as usize);\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_get_edit() {\n    // \"should get edit\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let resp = server.get(&route_helpers::edit_article_path(article.id)).await;\n    resp.assert_ok();\n    resp.assert_select(\"form\");\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_update_article() {\n    // \"should update article\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let resp = server.patch(&route_helpers::article_path(article.id)).form(&std::collections::HashMap::<String, String>::from([(\"article[body]\".to_string(), article.body.clone().unwrap_or_default().to_string()), (\"article[title]\".to_string(), \"Updated Title\".to_string())])).await;\n    resp.assert_redirected_to(&route_helpers::article_path(article.id));\n    article.reload();\n    assert_eq!(\"Updated Title\", article.title.clone().unwrap_or_default());\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_not_update_article_with_invalid_params() {\n    // \"should not update article with invalid params\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let resp = server.patch(&route_helpers::article_path(article.id)).form(&std::collections::HashMap::<String, String>::from([(\"article[title]\".to_string(), \"\".to_string()), (\"article[body]\".to_string(), \"\".to_string())])).await;\n    resp.assert_unprocessable();\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_destroy_article() {\n    // \"should destroy article\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let _before = Article::count();\n    let resp = server.delete(&route_helpers::article_path(article.id)).await;\n    let _after = Article::count();\n    assert_eq!(_after - _before, -1);\n    resp.assert_redirected_to(&route_helpers::articles_path());\n}\n"},{"path":"src/tests/comment.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// The broadcast-render bracket pair around synthesized broadcast\n// payloads (`ViewHelpers::broadcast_render(\n// &ViewHelpers::begin_broadcast_render(), …)` in the `after_*_commit`\n// expansions).\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n\n#[allow(unused_imports)]\nuse crate::fixtures::*;\n#[allow(unused_imports)]\nuse crate::inflector::Inflector;\n#[allow(unused_imports)]\nuse crate::json_builder::JsonBuilder;\n#[allow(unused_imports)]\nuse crate::router::{MatchResult, Route, Router};\n// No ViewHelpers row: this block is only ever concatenated after\n// MODEL_IMPORTS, which imports `view_helpers::{self, ViewHelpers}` —\n// a second `use` of the same NAME is E0252, not an unused-import.\n#[allow(unused_imports)]\nuse crate::flash::Flash;\n#[allow(unused_imports)]\nuse crate::session::Session;\n// Generated by Roundhouse (rust2).\n\n#[test]\npub fn test_creates_a_comment_on_an_article() {\n        crate::fixtures::setup();\n    let mut comment = CommentsFixtures::one();\n    if { let _ = comment.id(); false } { panic!(\"{}\", \"refute_nil failed\") };\n    if ArticlesFixtures::one().id() != comment.article_id() { panic!(\"{}\", \"assert_equal failed\") };\n}\n\n#[test]\npub fn test_belongs_to_article_association() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    let mut comment = Comment::create(std::collections::HashMap::from([((\"article_id\").to_string(), serde_json::Value::from(article.id())), ((\"commenter\").to_string(), serde_json::Value::from((\"Commenter\").to_string())), ((\"body\").to_string(), serde_json::Value::from((\"Comment body text.\").to_string()))]));\n    if article.id() != comment.article_id() { panic!(\"{}\", \"assert_equal failed\") };\n}\n\n#[test]\npub fn test_requires_commenter() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    let mut comment = Comment::new(std::collections::HashMap::from([((\"article_id\").to_string(), serde_json::Value::from(article.id())), ((\"body\").to_string(), serde_json::Value::from((\"Comment without commenter\").to_string()))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if comment.save() { panic!(\"{}\", \"refute failed\") };\n}\n\n#[test]\npub fn test_requires_body() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    let mut comment = Comment::new(std::collections::HashMap::from([((\"article_id\").to_string(), serde_json::Value::from(article.id())), ((\"commenter\").to_string(), serde_json::Value::from((\"Someone\").to_string()))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if comment.save() { panic!(\"{}\", \"refute failed\") };\n}\n\n#[test]\npub fn test_requires_valid_article() {\n        crate::fixtures::setup();\n    let mut comment = Comment::new(std::collections::HashMap::from([((\"commenter\").to_string(), serde_json::Value::from(\"Test\")), ((\"body\").to_string(), serde_json::Value::from(\"A test comment.\")), ((\"article_id\").to_string(), serde_json::Value::from(999999_i64))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if comment.save() { panic!(\"{}\", \"refute failed\") };\n}\n"},{"path":"src/tests/comments_controller_test.rs","content":"// Generated by Roundhouse.\n\n#[allow(unused_imports)]\nuse crate::fixtures;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::route_helpers;\n#[allow(unused_imports)]\nuse crate::test_support::TestResponseExt;\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_create_comment() {\n    // \"should create comment\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let _before = Comment::count();\n    let resp = server.post(&route_helpers::article_comments_path(article.id)).form(&std::collections::HashMap::<String, String>::from([(\"comment[commenter]\".to_string(), \"Test\".to_string()), (\"comment[body]\".to_string(), \"A test comment.\".to_string())])).await;\n    let _after = Comment::count();\n    assert_eq!(_after - _before, 1);\n    resp.assert_redirected_to(&route_helpers::article_path(article.id));\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_not_create_comment_with_invalid_params() {\n    // \"should not create comment with invalid params\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let _before = Comment::count();\n    let resp = server.post(&route_helpers::article_comments_path(article.id)).form(&std::collections::HashMap::<String, String>::from([(\"comment[commenter]\".to_string(), \"\".to_string()), (\"comment[body]\".to_string(), \"\".to_string())])).await;\n    let _after = Comment::count();\n    assert_eq!(_after - _before, 0);\n    resp.assert_redirected_to(&route_helpers::article_path(article.id));\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_destroy_comment() {\n    // \"should destroy comment\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let mut comment = fixtures::comments::one();\n    let _before = Comment::count();\n    let resp = server.delete(&route_helpers::article_comment_path(article.id, comment.id)).await;\n    let _after = Comment::count();\n    assert_eq!(_after - _before, -1);\n    resp.assert_redirected_to(&route_helpers::article_path(article.id));\n}\n"},{"path":"src/tests/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod article;\npub mod articles_controller_test;\npub mod comment;\npub mod comments_controller_test;\n"},{"path":"src/view_helpers.rs","content":"// Generated from runtime/ruby/action_view/view_helpers.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\nuse crate::active_record_base::Base;\nuse crate::hash_ext::merge_attrs;\nuse crate::http::RubyToS;\n\nstatic HTML_ESCAPES: std::sync::LazyLock<std::collections::HashMap<&'static str, &'static str>> = std::sync::LazyLock::new(|| std::collections::HashMap::from([(\"&\", \"&amp;\"), (\"<\", \"&lt;\"), (\">\", \"&gt;\"), (\"\\\"\", \"&quot;\"), (\"'\", \"&#39;\")]));\nstatic HTML_ESCAPE_PATTERN: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| regex::Regex::new(\"[&<>\\\"']\").unwrap());\nstatic URL_ESCAPES: std::sync::LazyLock<std::collections::HashMap<&'static str, &'static str>> = std::sync::LazyLock::new(|| std::collections::HashMap::from([(\" \", \"+\"), (\"!\", \"%21\"), (\"\\\"\", \"%22\"), (\"#\", \"%23\"), (\"$\", \"%24\"), (\"%\", \"%25\"), (\"&\", \"%26\"), (\"'\", \"%27\"), (\"(\", \"%28\"), (\")\", \"%29\"), (\"*\", \"%2A\"), (\"+\", \"%2B\"), (\",\", \"%2C\"), (\"/\", \"%2F\"), (\":\", \"%3A\"), (\";\", \"%3B\"), (\"<\", \"%3C\"), (\"=\", \"%3D\"), (\">\", \"%3E\"), (\"?\", \"%3F\"), (\"@\", \"%40\"), (\"[\", \"%5B\"), (\"\\\\\", \"%5C\"), (\"]\", \"%5D\"), (\"^\", \"%5E\"), (\"`\", \"%60\"), (\"{\", \"%7B\"), (\"|\", \"%7C\"), (\"}\", \"%7D\")]));\nstatic URL_ESCAPE_PATTERN: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| regex::Regex::new(\"[ !\\\"\\\\#$%&'()*+,/:;<=>?@\\\\[\\\\\\\\\\\\]^`{|}]\").unwrap());\nstatic URI_ESCAPES: std::sync::LazyLock<std::collections::HashMap<&'static str, &'static str>> = std::sync::LazyLock::new(|| std::collections::HashMap::from([(\" \", \"%20\"), (\"!\", \"%21\"), (\"\\\"\", \"%22\"), (\"#\", \"%23\"), (\"$\", \"%24\"), (\"%\", \"%25\"), (\"&\", \"%26\"), (\"'\", \"%27\"), (\"(\", \"%28\"), (\")\", \"%29\"), (\"*\", \"%2A\"), (\"+\", \"%2B\"), (\",\", \"%2C\"), (\"/\", \"%2F\"), (\":\", \"%3A\"), (\";\", \"%3B\"), (\"<\", \"%3C\"), (\"=\", \"%3D\"), (\">\", \"%3E\"), (\"?\", \"%3F\"), (\"@\", \"%40\"), (\"[\", \"%5B\"), (\"\\\\\", \"%5C\"), (\"]\", \"%5D\"), (\"^\", \"%5E\"), (\"`\", \"%60\"), (\"{\", \"%7B\"), (\"|\", \"%7C\"), (\"}\", \"%7D\")]));\nstatic MAILTO_ESCAPE_PATTERN: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| regex::Regex::new(\"[ !\\\"\\\\#$%&'()*+,/:;<=>?\\\\[\\\\\\\\\\\\]^`{|}]\").unwrap());\n\npub struct ViewHelpers;\n\nthread_local! {\n    static SLOTS: std::cell::RefCell<Option<std::collections::HashMap<String, String>>> = const { std::cell::RefCell::new(None) };\n    static BROADCAST_RENDERING: std::cell::RefCell<Option<bool>> = const { std::cell::RefCell::new(None) };\n}\n\nimpl ViewHelpers {\n    pub fn reset_slots_bang() {\n        SLOTS.with(|__s| *__s.borrow_mut() = Some(std::collections::HashMap::new()));\n        BROADCAST_RENDERING.with(|__s| *__s.borrow_mut() = Some(false));\n    }\n\n    pub fn content_for_set(slot: &str, value: &str) {\n        let mut prior = Self::get_slot(&(slot.clone()));\n        { SLOTS.with(|__s| __s.borrow_mut().get_or_insert_with(std::collections::HashMap::new).insert((slot.clone()).to_string(), (format!(\"{}{}\", prior, value)).to_string())); };\n    }\n\n    pub fn content_for_get(slot: &str) -> Option<String> {\n        SLOTS.with(|__s| __s.borrow().as_ref().and_then(|__m| __m.get(slot).cloned()))\n    }\n\n    pub fn content_for_pred(slot: &str) -> bool {\n        !(Self::get_slot(&(slot)).is_empty())\n    }\n\n    pub fn get_slot(slot: &str) -> String {\n        SLOTS.with(|__s| __s.borrow().as_ref().and_then(|__m| __m.get(slot).cloned())).unwrap_or((\"\").to_string())\n    }\n\n    pub fn get_yield() -> String {\n        SLOTS.with(|__s| __s.borrow().as_ref().and_then(|__m| __m.get(\"__body__\").cloned())).unwrap_or((\"\").to_string())\n    }\n\n    pub fn set_yield(content: &str) {\n        { SLOTS.with(|__s| __s.borrow_mut().get_or_insert_with(std::collections::HashMap::new).insert((\"__body__\").to_string(), (content).to_string())); };\n    }\n\n    pub fn html_escape(s: &str) -> String {\n        HTML_ESCAPE_PATTERN.replace_all(&s, |__caps: &regex::Captures| -> String { (*HTML_ESCAPES.get(&__caps[0]).unwrap_or(&\"\")).to_string() }).into_owned()\n    }\n\n    pub fn h(value: serde_json::Value) -> String {\n        Self::html_escape(&(value.ruby_to_s()))\n    }\n\n    pub fn url_encode(s: &str) -> String {\n        URL_ESCAPE_PATTERN.replace_all(&s, |__caps: &regex::Captures| -> String { (*URL_ESCAPES.get(&__caps[0]).unwrap_or(&\"\")).to_string() }).into_owned()\n    }\n\n    pub fn url_encode_component(s: &str) -> String {\n        URL_ESCAPE_PATTERN.replace_all(&s, |__caps: &regex::Captures| -> String { (*URI_ESCAPES.get(&__caps[0]).unwrap_or(&\"\")).to_string() }).into_owned()\n    }\n\n    pub fn url_encode_mailto_address(s: &str) -> String {\n        MAILTO_ESCAPE_PATTERN.replace_all(&s, |__caps: &regex::Captures| -> String { (*URI_ESCAPES.get(&__caps[0]).unwrap_or(&\"\")).to_string() }).into_owned()\n    }\n\n    pub fn truncate(s: &str, length: i64, omission: &str) -> String {\n        if (s.len() as i64) <= length { return (s.clone()).to_string() };\n        let mut cutoff = length - omission.len() as i64;\n        if cutoff < 0_i64 { cutoff = 0_i64 };\n        format!(\"{}{}\", (&s.clone()[(0_i64) as usize..((0_i64) + (cutoff)) as usize]).to_string(), omission.clone())\n    }\n\n    pub fn dom_id(record: Base, suffix: Option<String>) -> String {\n        if !(record.persisted_pred()) { if suffix.is_none() { format!(\"new_{}\", record.dom_prefix()) } else { format!(\"{}_{}\", suffix.clone().unwrap(), record.dom_prefix()) } } else { if suffix.is_none() { format!(\"{}_{}\", record.dom_prefix(), record.dom_record_key()) } else { format!(\"{}_{}_{}\", suffix.clone().unwrap(), record.dom_prefix(), record.dom_record_key()) } }\n    }\n\n    pub fn link_to(text: &str, href: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let attrs = Self::render_attrs(merge_attrs(std::collections::HashMap::from([(\"href\", href)]), opts.clone()));\n        format!(\"<a{}>{}</a>\", attrs, Self::html_escape(&(text)))\n    }\n\n    pub fn mail_query_append(query: &str, header: &str, value: &str) -> String {\n        let separator = if query.is_empty() { \"?\" } else { \"&\" };\n        format!(\"{}{}{}={}\", query.clone(), separator, header, Self::url_encode_component(&(value)))\n    }\n\n    pub fn mail_to(email: &str, name: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let mut cc = opts.clone().get(\"cc\").cloned();\n        let mut bcc = opts.clone().get(\"bcc\").cloned();\n        let mut body = opts.clone().get(\"body\").cloned();\n        let mut subject = opts.clone().get(\"subject\").cloned();\n        let mut reply_to = opts.clone().get(\"reply_to\").cloned();\n        let mut query = (\"\").to_string();\n        if !(cc.is_none()) { query = Self::mail_query_append(&(query.clone()), \"cc\", &(cc.clone().unwrap().ruby_to_s())) };\n        if !(bcc.is_none()) { query = Self::mail_query_append(&(query.clone()), \"bcc\", &(bcc.clone().unwrap().ruby_to_s())) };\n        if !(body.is_none()) { query = Self::mail_query_append(&(query.clone()), \"body\", &(body.clone().unwrap().ruby_to_s())) };\n        if !(subject.is_none()) { query = Self::mail_query_append(&(query.clone()), \"subject\", &(subject.clone().unwrap().ruby_to_s())) };\n        if !(reply_to.is_none()) { query = Self::mail_query_append(&(query.clone()), \"reply-to\", &(reply_to.clone().unwrap().ruby_to_s())) };\n        let mut html_opts = opts.clone().clone().clone();\n        html_opts.remove(\"cc\");\n        html_opts.remove(\"bcc\");\n        html_opts.remove(\"body\");\n        html_opts.remove(\"subject\");\n        html_opts.remove(\"reply_to\");\n        let href = format!(\"mailto:{}{}\", Self::url_encode_mailto_address(&(email.clone())), query.clone());\n        let attrs = Self::render_attrs(merge_attrs(std::collections::HashMap::from([(\"href\", href)]), html_opts.clone()));\n        let label = if name.is_empty() { email.clone() } else { name.clone() };\n        format!(\"<a{}>{}</a>\", attrs, Self::html_escape(&(label)))\n    }\n\n    pub fn link_to_raw(text: &str, href: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let attrs = Self::render_attrs(merge_attrs(std::collections::HashMap::from([(\"href\", href)]), opts.clone()));\n        format!(\"<a{}>{}</a>\", attrs, text)\n    }\n\n    pub fn raw(value: serde_json::Value) -> String {\n        value.ruby_to_s()\n    }\n\n    pub fn safe_join(parts: Vec<String>, separator: &str) -> String {\n        parts.join(separator)\n    }\n\n    pub fn button_to(text: &str, href: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let mut method = opts.clone().get(\"method\").cloned();\n        let form_class = opts.clone().get(\"form_class\").cloned();\n        let mut inner_opts = opts.clone().clone().clone();\n        inner_opts.remove(\"method\");\n        inner_opts.remove(\"form_class\");\n        let mut form_attrs = std::collections::HashMap::from([(\"action\", (href).to_string()), (\"method\", (\"post\").to_string())]).clone();\n        { form_attrs.insert(\"class\", form_class.unwrap_or(serde_json::Value::from(\"button_to\")).to_string()); };\n        let button_attrs = Self::render_attrs(merge_attrs(std::collections::HashMap::from([(\"type\", \"submit\")]), inner_opts.clone()));\n        let method_input = if !(method.is_none()) && method.clone().unwrap().ruby_to_s() != \"post\" { format!(\"<input type=\\\"hidden\\\" name=\\\"_method\\\" value=\\\"{}\\\">\", (method.clone().unwrap()).ruby_to_s()) } else { (\"\").to_string() };\n        let auth_token_input = Self::csrf_token_hidden_input();\n        format!(\"<form{}>{}<button{}>{}</button>{}</form>\", Self::render_attrs(form_attrs.clone().into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()), method_input, button_attrs, Self::html_escape(&(text)), auth_token_input)\n    }\n\n    pub fn csrf_meta_tags() -> String {\n        format!(\"<meta name=\\\"csrf-param\\\" content=\\\"authenticity_token\\\" />\\n<meta name=\\\"csrf-token\\\" content=\\\"{}\\\" />\", Self::form_authenticity_token())\n    }\n\n    pub fn form_authenticity_token() -> String {\n        (\"\").to_string()\n    }\n\n    pub fn csp_meta_tag() -> String {\n        (\"\").to_string()\n    }\n\n    pub fn stylesheet_link_tag(name: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let href = format!(\"/assets/{}.css\", name);\n        let attrs = Self::render_attrs(merge_attrs(std::collections::HashMap::from([(\"rel\", (\"stylesheet\").to_string()), (\"href\", href)]), opts.clone()));\n        format!(\"<link{}>\", attrs)\n    }\n\n    pub fn javascript_include_tag(source: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let mut name = if source.contains(\".\") { (source.clone()).to_string() } else { format!(\"{}.js\", source.clone()) };\n        let src = if name.starts_with(\"/\") || name.contains(\"://\") { (name.clone()).to_string() } else { format!(\"/assets/{}\", name.clone()) };\n        let attrs = Self::render_attrs(merge_attrs(std::collections::HashMap::from([(\"src\", src)]), opts.clone()));\n        format!(\"<script{}></script>\", attrs)\n    }\n\n    pub fn image_path(source: &str, skip_pipeline: bool) -> String {\n        if skip_pipeline { return (source.clone()).to_string() };\n        if source.starts_with(\"/\") { return (source.clone()).to_string() };\n        if source.contains(\"://\") { return (source.clone()).to_string() };\n        format!(\"/assets/{}\", source.clone())\n    }\n\n    pub fn asset_path(source: &str, skip_pipeline: bool) -> String {\n        if skip_pipeline { return (source.clone()).to_string() };\n        if source.starts_with(\"/\") { return (source.clone()).to_string() };\n        if source.contains(\"://\") { return (source.clone()).to_string() };\n        format!(\"/assets/{}\", source.clone())\n    }\n\n    pub fn url_for(target: &str) -> String {\n        (target).to_string()\n    }\n\n    pub fn image_url(source: &str, skip_pipeline: bool) -> String {\n        if skip_pipeline { return (source.clone()).to_string() };\n        if source.starts_with(\"/\") { return (source.clone()).to_string() };\n        if source.contains(\"://\") { return (source.clone()).to_string() };\n        format!(\"/assets/{}\", source.clone())\n    }\n\n    pub fn path_to_javascript(source: &str, skip_pipeline: bool) -> String {\n        if skip_pipeline { return (source.clone()).to_string() };\n        if source.starts_with(\"/\") { return (source.clone()).to_string() };\n        if source.contains(\"://\") { return (source.clone()).to_string() };\n        let name = if source.contains(\".\") { (source.clone()).to_string() } else { format!(\"{}.js\", source.clone()) };\n        format!(\"/assets/{}\", name)\n    }\n\n    pub fn javascript_path(source: &str, skip_pipeline: bool) -> String {\n        if skip_pipeline { return (source.clone()).to_string() };\n        if source.starts_with(\"/\") { return (source.clone()).to_string() };\n        if source.contains(\"://\") { return (source.clone()).to_string() };\n        let name = if source.contains(\".\") { (source.clone()).to_string() } else { format!(\"{}.js\", source.clone()) };\n        format!(\"/assets/{}\", name)\n    }\n\n    pub fn image_tag(source: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let mut attrs: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n        let mut size = None;\n        { let _m = opts.clone(); let mut _items: Vec<_> = _m.iter().collect(); _items.sort_by(|a, b| a.0.cmp(b.0)); _items.into_iter().for_each(|(k, v)| { if k.clone() == \"size\" { size = Some(v.clone()) } else { { attrs.insert((k.clone()).to_string(), serde_json::Value::from(v.clone())); } }; }); };\n        { attrs.insert((\"src\").to_string(), serde_json::Value::from(Self::image_path(&(source), false))); };\n        if !(size.is_none()) { { let mut parts = size.clone().unwrap().ruby_to_s().split(\"x\").map(|s| s.to_string()).collect::<Vec<String>>();\n        { attrs.insert((\"width\").to_string(), serde_json::Value::from(parts.clone()[(0_i64) as usize].clone())); };\n        { attrs.insert((\"height\").to_string(), serde_json::Value::from(if (parts.len() as i64) > 1_i64 { parts.clone()[(1_i64) as usize].clone() } else { parts.clone()[(0_i64) as usize].clone() })); } } };\n        format!(\"<img{}>\", Self::render_attrs(attrs.clone().into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()))\n    }\n\n    pub fn content_tag(name: &str, content: Option<serde_json::Value>, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let n = name.to_string();\n        format!(\"<{}{}>{}</{}>\", n.clone(), Self::render_attrs(opts.clone()), Self::html_escape(&(content.clone().unwrap().ruby_to_s())), n.clone())\n    }\n\n    pub fn javascript_importmap_tags(pins: Option<Vec<serde_json::Value>>, entry: &str) -> String {\n        if pins.is_none() || pins.clone().unwrap().is_empty() { { let mut json = (\"{\\n  \\\"imports\\\": {\\n    \\\"@hotwired/turbo\\\": \\\"/assets/turbo.min.js\\\"\\n  }\\n}\").to_string();\n        return format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"<script type=\\\"importmap\\\" data-turbo-track=\\\"reload\\\">\", json.clone()), \"</script>\"), \"\\n\"), \"<link rel=\\\"modulepreload\\\" href=\\\"/assets/turbo.min.js\\\">\"), \"\\n\"), \"<script type=\\\"module\\\">import \\\"@hotwired/turbo\\\"</script>\") } };\n        let import_lines = pins.clone().iter().flatten().map(|p| { format!(\"    \\\"{}\\\": \\\"{}\\\"\", (p.clone()[\"name\"]).ruby_to_s(), (p.clone()[\"path\"]).ruby_to_s()) }).collect::<Vec<_>>().join(\",\\n\");\n        let mut json = format!(\"{}{}\", format!(\"{}{}\", \"{\\n  \\\"imports\\\": {\\n\", import_lines), \"\\n  }\\n}\");\n        let mut parts = vec![];\n        parts.push((format!(\"{}{}\", format!(\"{}{}\", \"<script type=\\\"importmap\\\" data-turbo-track=\\\"reload\\\">\", json.clone()), \"</script>\")).to_string());\n        pins.clone().iter().flatten().for_each(|p| { parts.push((format!(\"<link rel=\\\"modulepreload\\\" href=\\\"{}\\\">\", (p.clone()[\"path\"]).ruby_to_s())).to_string()); });\n        parts.push((format!(\"<script type=\\\"module\\\">import \\\"{}\\\"</script>\", entry)).to_string());\n        parts.join(\"\\n\")\n    }\n\n    pub fn turbo_stream_from(stream: serde_json::Value, channel: &str) -> String {\n        let encoded = { use base64::Engine; base64::engine::general_purpose::STANDARD.encode(serde_json::to_string(&stream).unwrap()) };\n        format!(\"<turbo-cable-stream-source channel=\\\"{}\\\" signed-stream-name=\\\"{}--unsigned\\\"></turbo-cable-stream-source>\", channel, encoded)\n    }\n\n    pub fn csrf_token_hidden_input() -> String {\n        if BROADCAST_RENDERING.with(|__s| __s.borrow().clone()).unwrap_or_default() == true { return (\"\").to_string() };\n        format!(\"<input type=\\\"hidden\\\" name=\\\"authenticity_token\\\" value=\\\"{}\\\">\", Self::form_authenticity_token())\n    }\n\n    pub fn cache_scope() -> String {\n        if BROADCAST_RENDERING.with(|__s| __s.borrow().clone()).unwrap_or_default() == true { return (\"broadcast/\").to_string() };\n        (\"\").to_string()\n    }\n\n    pub fn begin_broadcast_render() -> String {\n        BROADCAST_RENDERING.with(|__s| *__s.borrow_mut() = Some(true));\n        (\"\").to_string()\n    }\n\n    pub fn broadcast_render(_armed: &str, html: &str) -> String {\n        BROADCAST_RENDERING.with(|__s| *__s.borrow_mut() = Some(false));\n        (html).to_string()\n    }\n\n    pub fn method_override_input(method: serde_json::Value) -> String {\n        let mut method_str = method.ruby_to_s();\n        if method_str.clone() == \"get\" || method_str.clone() == \"post\" { (\"\").to_string() } else { format!(\"<input type=\\\"hidden\\\" name=\\\"_method\\\" value=\\\"{}\\\">\", method_str.clone()) }\n    }\n\n    pub fn form_with(opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let mut attrs = opts.clone().clone().clone();\n        attrs.remove(\"method\");\n        attrs.remove(\"url\");\n        attrs.remove(\"model\");\n        attrs.remove(\"scope\");\n        attrs.remove(\"format\");\n        attrs.remove(\"builder\");\n        let mut url = opts.clone().get(\"url\").cloned();\n        let action = if url.is_none() { (\"\").to_string() } else { format!(\" action=\\\"{}\\\"\", Self::html_escape(&(url.clone().unwrap().ruby_to_s()))) };\n        format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"<form{}{} accept-charset=\\\"UTF-8\\\" method=\\\"post\\\">\", Self::render_attrs(attrs.clone().into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()), action), Self::method_override_input(opts.clone().get(\"method\").cloned().unwrap_or(serde_json::Value::from(\"post\")))), Self::csrf_token_hidden_input()), \"</form>\")\n    }\n\n    pub fn optional_value_attr(value: serde_json::Value) -> String {\n        if value.clone().is_null() || value.ruby_to_s().is_empty() { (\"\").to_string() } else { format!(\" value=\\\"{}\\\"\", Self::html_escape(&(value.ruby_to_s()))) }\n    }\n\n    pub fn escape_or_empty(value: serde_json::Value) -> String {\n        if value.clone().is_null() { (\"\").to_string() } else { Self::html_escape(&(value.ruby_to_s())) }\n    }\n\n    pub fn render_attrs(attrs: std::collections::HashMap<String, serde_json::Value>) -> String {\n        if attrs.is_empty() { return (\"\").to_string() };\n        let mut pairs = vec![];\n        { let _m = attrs.clone(); let mut _items: Vec<_> = _m.iter().collect(); _items.sort_by(|a, b| a.0.cmp(b.0)); _items.into_iter().for_each(|(k, v)| {\n            let name = k.to_string();\n            if !(v.clone().is_null()) { if v.clone().is_object() { v.clone().as_object().unwrap().iter().for_each(|(inner_k, inner_v)| {\n                let inner_name = inner_k.ruby_to_s().replace(\"_\", \"-\");\n                if !(inner_v.clone().is_null()) { pairs.push((format!(\" {}-{}=\\\"{}\\\"\", name.clone(), inner_name, Self::html_escape(&(inner_v.ruby_to_s())))).to_string()) }\n            }) } else { if Self::boolean_attr_pred(&(name.clone())) { if !(v.ruby_to_s() == \"false\") { pairs.push((format!(\" {}=\\\"{}\\\"\", name.clone(), name.clone())).to_string()) } } else { pairs.push((format!(\" {}=\\\"{}\\\"\", name.clone(), Self::html_escape(&(v.ruby_to_s())))).to_string()) } } }\n        }); };\n        pairs.join(\"\")\n    }\n\n    pub fn boolean_attr_pred(name: &str) -> bool {\n        name.clone() == \"allowfullscreen\" || name.clone() == \"allowpaymentrequest\" || name.clone() == \"async\" || name.clone() == \"autofocus\" || name.clone() == \"autoplay\" || name.clone() == \"checked\" || name.clone() == \"compact\" || name.clone() == \"controls\" || name.clone() == \"declare\" || name.clone() == \"default\" || name.clone() == \"defaultchecked\" || name.clone() == \"defaultmuted\" || name.clone() == \"defaultselected\" || name.clone() == \"defer\" || name.clone() == \"disabled\" || name.clone() == \"enabled\" || name.clone() == \"formnovalidate\" || name.clone() == \"hidden\" || name.clone() == \"indeterminate\" || name.clone() == \"inert\" || name.clone() == \"ismap\" || name.clone() == \"itemscope\" || name.clone() == \"loop\" || name.clone() == \"multiple\" || name.clone() == \"muted\" || name.clone() == \"nohref\" || name.clone() == \"nomodule\" || name.clone() == \"noresize\" || name.clone() == \"noshade\" || name.clone() == \"novalidate\" || name.clone() == \"nowrap\" || name.clone() == \"open\" || name.clone() == \"pauseonexit\" || name.clone() == \"playsinline\" || name.clone() == \"pubdate\" || name.clone() == \"readonly\" || name.clone() == \"required\" || name.clone() == \"reversed\" || name.clone() == \"scoped\" || name.clone() == \"seamless\" || name.clone() == \"selected\" || name.clone() == \"sortable\" || name.clone() == \"truespeed\" || name.clone() == \"typemustmatch\" || name.clone() == \"visible\"\n    }\n}\n\n// rust2 compat: bare-fn wrappers consumed by server.rs.\npub fn reset_render_state() { ViewHelpers::reset_slots_bang() }\npub fn set_yield(content: &str) { ViewHelpers::set_yield(content) }\npub fn get_yield() -> String { ViewHelpers::get_yield() }\n"},{"path":"src/views/articles.rs","content":"#[allow(unused_imports)]\nuse std::fmt::Write as _;\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::inflector::{self, Inflector};\n#[allow(unused_imports)]\nuse crate::importmap::{self, Importmap};\n#[allow(unused_imports)]\nuse crate::json_builder::{self, JsonBuilder};\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct Articles;\n\nimpl Articles {\n    pub fn article(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(\"<div id=\\\"\");\n        io.push_str(&format!(\"article_{}\", article.clone().id()));\n        io.push_str(\"\\\" class=\\\"flex flex-col sm:flex-row justify-between items-center pb-5 sm:pb-0\\\">\\n  <div class=\\\"p-4 border rounded mb-4 flex-grow\\\">\\n    <h2 class=\\\"text-xl font-bold\\\">\\n      \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"text-blue-600 hover:underline\", ViewHelpers::html_escape(&(article.title().map(|v| v.to_string()).unwrap_or_default()))).ok(); };\n        io.push_str(\"\\n      <span id=\\\"\");\n        io.push_str(&format!(\"comments_count_article_{}\", article.clone().id()));\n        io.push_str(\"\\\" class=\\\"text-gray-500 text-sm font-normal ml-2\\\">\\n        (\");\n        io.push_str(&Inflector::pluralize(article.comments().len() as i64, \"comment\"));\n        io.push_str(\")\\n      </span>\\n    </h2>\\n    <p class=\\\"text-gray-700 mt-2\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(ViewHelpers::truncate(&(article.body().map(|v| v.to_string()).unwrap_or_default()), 100_i64, \"...\"))));\n        io.push_str(\"</p>\\n  </div>\\n  <div class=\\\"w-full sm:w-auto flex flex-col sm:flex-row space-x-2 space-y-2\\\">\\n    \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Show\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::edit_article_path(article.id()))), \"w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Edit\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<form action=\\\"{}\\\" method=\\\"post\\\" class=\\\"{}\\\">{}<button type=\\\"submit\\\" class=\\\"{}\\\" data-turbo-confirm=\\\"{}\\\">{}</button>{}</form>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"button_to\", ViewHelpers::method_override_input(serde_json::Value::from(\"delete\")), \"w-full sm:w-auto rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer\", \"Are you sure?\", \"Destroy\", ViewHelpers::csrf_token_hidden_input()).ok(); };\n        io.push_str(\"\\n  </div>\\n</div>\\n\");\n        io\n    }\n\n    pub fn form(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        let mut form_method = if article.persisted_pred() { \"patch\" } else { \"post\" };\n        { write!(io, \"<form action=\\\"{}\\\" accept-charset=\\\"UTF-8\\\" method=\\\"post\\\" class=\\\"{}\\\">\", ViewHelpers::html_escape(&(if article.persisted_pred() { RouteHelpers::article_path(article.id()) } else { RouteHelpers::articles_path() })), \"contents\").ok(); };\n        io.push_str(&ViewHelpers::method_override_input(serde_json::Value::from(form_method.clone())));\n        io.push_str(&ViewHelpers::csrf_token_hidden_input());\n        io.push_str(\"\\n\");\n        if !(article.errors().is_empty()) { { io.push_str(\"    <div id=\\\"error_explanation\\\" class=\\\"bg-red-50 text-red-500 px-3 py-2 font-medium rounded-md mt-3\\\">\\n      <h2>\");\n        io.push_str(&Inflector::pluralize(article.errors().len() as i64, \"error\"));\n        io.push_str(\" prohibited this article from being saved:</h2>\\n\\n      <ul class=\\\"list-disc ml-6\\\">\\n\");\n        article.errors().iter_mut().for_each(|error| {\n            io.push_str(\"          <li>\");\n            io.push_str(&ViewHelpers::html_escape(&(error.to_string())));\n            io.push_str(\"</li>\\n\");\n        });\n        io.push_str(\"      </ul>\\n    </div>\\n\") } };\n        io.push_str(\"\\n  <div class=\\\"my-5\\\">\\n    \");\n        { write!(io, \"<label for=\\\"article_title\\\">Title</label>\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<input type=\\\"text\\\" name=\\\"article[title]\\\" id=\\\"article_title\\\"{} class=\\\"{}\\\">\", ViewHelpers::optional_value_attr(article.clone().get_index(\"title\")), \"block shadow-sm rounded-md border px-3 py-2 mt-2 w-full border-gray-400 focus:outline-blue-600\").ok(); };\n        io.push_str(\"\\n  </div>\\n\\n  <div class=\\\"my-5\\\">\\n    \");\n        { write!(io, \"<label for=\\\"article_body\\\">Body</label>\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<textarea name=\\\"article[body]\\\" id=\\\"article_body\\\" rows=\\\"{}\\\" class=\\\"{}\\\">\\n{}</textarea>\", \"4\", \"block shadow-sm rounded-md border px-3 py-2 mt-2 w-full border-gray-400 focus:outline-blue-600\", ViewHelpers::escape_or_empty(article.clone().get_index(\"body\"))).ok(); };\n        io.push_str(\"\\n  </div>\\n\\n  <div class=\\\"inline\\\">\\n    \");\n        { write!(io, \"<input type=\\\"submit\\\" name=\\\"commit\\\" value=\\\"{}\\\" data-disable-with=\\\"{}\\\" class=\\\"{}\\\">\", if form_method.clone() == \"patch\" { \"Update Article\" } else { \"Create Article\" }, if form_method.clone() == \"patch\" { \"Update Article\" } else { \"Create Article\" }, \"w-full sm:w-auto rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer\").ok(); };\n        io.push_str(\"\\n  </div>\\n\");\n        io.push_str(\"</form>\");\n        io\n    }\n\n    pub fn edit(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        ViewHelpers::content_for_set(\"title\", \"Editing article\");\n        io.push_str(\"\\n<div class=\\\"md:w-2/3 w-full\\\">\\n  <h1 class=\\\"font-bold text-4xl\\\">Editing article</h1>\\n\\n  \");\n        io.push_str(&Articles::form(article.clone().clone(), None, None));\n        io.push_str(\"\\n\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Show this article\").ok(); };\n        io.push_str(\"\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::articles_path())), \"w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Back to articles\").ok(); };\n        io.push_str(\"\\n</div>\\n\");\n        io\n    }\n\n    pub fn index(articles: Vec<Article>, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(&ViewHelpers::turbo_stream_from(serde_json::Value::from(\"articles\"), \"Turbo::StreamsChannel\"));\n        io.push_str(\"\\n\\n\");\n        ViewHelpers::content_for_set(\"title\", \"Articles\");\n        io.push_str(\"\\n\");\n        ViewHelpers::content_for_set(\"head\", \"<meta name=\\\"turbo-cache-control\\\" content=\\\"no-cache\\\">\");\n        io.push_str(\"\\n<div class=\\\"w-full\\\">\\n\");\n        if !(notice.is_none()) && !(notice.clone().unwrap().is_empty()) { { io.push_str(\"    <p class=\\\"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block\\\" id=\\\"notice\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(notice.clone().unwrap().to_string())));\n        io.push_str(\"</p>\\n\") } };\n        io.push_str(\"\\n  <div class=\\\"flex justify-between items-center\\\">\\n    <h1 class=\\\"font-bold text-4xl\\\">Articles</h1>\\n    \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::new_article_path())), \"rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium\", \"New article\").ok(); };\n        io.push_str(\"\\n  </div>\\n\\n  <div id=\\\"articles\\\" class=\\\"min-w-full divide-y divide-gray-200 space-y-5\\\">\\n\");\n        if !(articles.is_empty()) { io.push_str(\"      \");\n        articles.clone().iter_mut().for_each(|a| { io.push_str(&Articles::article(a.clone(), None, None)); });\n        io.push_str(\"\\n\") } else { io.push_str(\"      <p class=\\\"text-center my-10\\\">No articles found.</p>\\n\") };\n        io.push_str(\"  </div>\\n</div>\\n\");\n        io\n    }\n\n    pub fn _new(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        ViewHelpers::content_for_set(\"title\", \"New article\");\n        io.push_str(\"\\n<div class=\\\"md:w-2/3 w-full\\\">\\n  <h1 class=\\\"font-bold text-4xl\\\">New article</h1>\\n\\n  \");\n        io.push_str(&Articles::form(article.clone(), None, None));\n        io.push_str(\"\\n\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::articles_path())), \"w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Back to articles\").ok(); };\n        io.push_str(\"\\n</div>\\n\");\n        io\n    }\n\n    pub fn show(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        ViewHelpers::content_for_set(\"title\", \"Showing article\");\n        io.push_str(\"\\n\");\n        let mut cf_head = String::new();\n        cf_head.push_str(\"  \");\n        ViewHelpers::content_for_set(\"head\", \"<meta name=\\\"turbo-cache-control\\\" content=\\\"no-preview\\\">\");\n        cf_head.push_str(\"\\n  <meta name=\\\"article-id\\\" content=\\\"\");\n        cf_head.push_str(&ViewHelpers::html_escape(&(article.id().to_string())));\n        cf_head.push_str(\"\\\">\\n\");\n        ViewHelpers::content_for_set(\"head\", &(cf_head));\n        io.push_str(\"\\n<div class=\\\"md:w-2/3 w-full\\\">\\n\");\n        if !(notice.is_none()) && !(notice.clone().unwrap().is_empty()) { { io.push_str(\"    <p class=\\\"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block\\\" id=\\\"notice\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(notice.clone().unwrap().to_string())));\n        io.push_str(\"</p>\\n\") } };\n        io.push_str(\"\\n  <h1 class=\\\"font-bold text-4xl\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(article.title().map(|v| v.to_string()).unwrap_or_default())));\n        io.push_str(\"</h1>\\n\\n  <div class=\\\"my-4\\\">\\n    <p class=\\\"text-gray-700\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(article.body().map(|v| v.to_string()).unwrap_or_default())));\n        io.push_str(\"</p>\\n  </div>\\n\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::edit_article_path(article.id()))), \"w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Edit this article\").ok(); };\n        io.push_str(\"\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::articles_path())), \"w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Back to articles\").ok(); };\n        io.push_str(\"\\n  \");\n        { write!(io, \"<form action=\\\"{}\\\" method=\\\"post\\\" class=\\\"{}\\\">{}<button type=\\\"submit\\\" class=\\\"{}\\\" data-turbo-confirm=\\\"{}\\\">{}</button>{}</form>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"sm:inline-block mt-2 sm:mt-0 sm:ml-2\", ViewHelpers::method_override_input(serde_json::Value::from(\"delete\")), \"w-full rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer\", \"Are you sure?\", \"Destroy this article\", ViewHelpers::csrf_token_hidden_input()).ok(); };\n        io.push_str(\"\\n</div>\\n\\n<hr class=\\\"my-8\\\">\\n\\n<h2 class=\\\"text-xl font-bold mb-4\\\">Comments</h2>\\n\\n\");\n        io.push_str(&ViewHelpers::turbo_stream_from(serde_json::Value::from(format!(\"article_{}_comments\", article.id())), \"Turbo::StreamsChannel\"));\n        io.push_str(\"\\n\\n<div id=\\\"comments\\\" class=\\\"space-y-4 mb-8\\\">\\n  \");\n        article.comments().iter_mut().for_each(|c| { io.push_str(&Comments::comment(c.clone(), None, None)); });\n        io.push_str(\"\\n</div>\\n\\n<h3 class=\\\"text-lg font-semibold mb-2\\\">Add a Comment</h3>\\n\\n\");\n        let mut form_record = Comment::new(std::collections::HashMap::new());\n        let form_method = \"post\";\n        { write!(io, \"<form action=\\\"{}\\\" accept-charset=\\\"UTF-8\\\" method=\\\"post\\\" class=\\\"{}\\\">\", ViewHelpers::html_escape(&(RouteHelpers::article_comments_path(article.id()))), \"space-y-4\").ok(); };\n        io.push_str(&ViewHelpers::method_override_input(serde_json::Value::from(form_method)));\n        io.push_str(&ViewHelpers::csrf_token_hidden_input());\n        io.push_str(\"\\n  <div>\\n    \");\n        { write!(io, \"<label for=\\\"comment_commenter\\\" class=\\\"{}\\\">Commenter</label>\", \"block font-medium\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<input type=\\\"text\\\" name=\\\"comment[commenter]\\\" id=\\\"comment_commenter\\\"{} class=\\\"{}\\\">\", ViewHelpers::optional_value_attr(form_record.clone().get_index(\"commenter\")), \"block w-full border rounded p-2\").ok(); };\n        io.push_str(\"\\n  </div>\\n  <div>\\n    \");\n        { write!(io, \"<label for=\\\"comment_body\\\" class=\\\"{}\\\">Body</label>\", \"block font-medium\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<textarea name=\\\"comment[body]\\\" id=\\\"comment_body\\\" rows=\\\"{}\\\" class=\\\"{}\\\">\\n{}</textarea>\", \"3\", \"block w-full border rounded p-2\", ViewHelpers::escape_or_empty(form_record.clone().get_index(\"body\"))).ok(); };\n        io.push_str(\"\\n  </div>\\n  \");\n        { write!(io, \"<input type=\\\"submit\\\" name=\\\"commit\\\" value=\\\"{}\\\" data-disable-with=\\\"{}\\\" class=\\\"{}\\\">\", \"Add Comment\", \"Add Comment\", \"bg-blue-600 text-white px-4 py-2 rounded\").ok(); };\n        io.push_str(\"\\n\");\n        io.push_str(\"</form>\");\n        io\n    }\n\n    pub fn article_json(article: Article) -> String {\n        let mut io = String::new();\n        io.push_str(\"{\");\n        io.push_str(\"\\\"id\\\":\");\n        io.push_str(&JsonBuilder::encode_value(serde_json::Value::from(article.id())));\n        io.push_str(\",\");\n        io.push_str(\"\\\"title\\\":\");\n        io.push_str(&JsonBuilder::encode_value(serde_json::Value::from(article.title())));\n        io.push_str(\",\");\n        io.push_str(\"\\\"body\\\":\");\n        io.push_str(&JsonBuilder::encode_value(serde_json::Value::from(article.body())));\n        io.push_str(\",\");\n        io.push_str(\"\\\"created_at\\\":\");\n        io.push_str(&JsonBuilder::encode_datetime(Some(article.created_at_raw())).to_string());\n        io.push_str(\",\");\n        io.push_str(\"\\\"updated_at\\\":\");\n        io.push_str(&JsonBuilder::encode_datetime(Some(article.updated_at_raw())).to_string());\n        io.push_str(\",\");\n        io.push_str(\"\\\"url\\\":\");\n        io.push_str(&JsonBuilder::encode_value(serde_json::Value::from(RouteHelpers::article_json_path(article.id()))));\n        io.push_str(\"}\");\n        io\n    }\n\n    pub fn index_json(articles: Vec<Article>) -> String {\n        let mut io = String::new();\n        io.push_str(\"[\");\n        io.push_str(&articles.into_iter().map(|article| { Articles::article_json(article.clone()) }).collect::<Vec<_>>().join(\",\"));\n        io.push_str(\"]\");\n        io\n    }\n\n    pub fn show_json(article: Article) -> String {\n        let mut io = String::new();\n        io.push_str(&Articles::article_json(article.clone()));\n        io\n    }\n}\n"},{"path":"src/views/comments.rs","content":"#[allow(unused_imports)]\nuse std::fmt::Write as _;\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::inflector::{self, Inflector};\n#[allow(unused_imports)]\nuse crate::importmap::{self, Importmap};\n#[allow(unused_imports)]\nuse crate::json_builder::{self, JsonBuilder};\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct Comments;\n\nimpl Comments {\n    pub fn comment(comment: Comment, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(\"<div id=\\\"\");\n        io.push_str(&format!(\"comment_{}\", comment.clone().id()));\n        io.push_str(\"\\\" class=\\\"p-4 bg-gray-50 rounded\\\">\\n  <p class=\\\"font-semibold\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(comment.commenter().map(|v| v.to_string()).unwrap_or_default())));\n        io.push_str(\"</p>\\n  <p class=\\\"text-gray-700\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(comment.body().map(|v| v.to_string()).unwrap_or_default())));\n        io.push_str(\"</p>\\n  \");\n        { write!(io, \"<form action=\\\"{}\\\" method=\\\"post\\\" class=\\\"{}\\\">{}<button type=\\\"submit\\\" class=\\\"{}\\\" data-turbo-confirm=\\\"{}\\\">{}</button>{}</form>\", ViewHelpers::html_escape(&(RouteHelpers::article_comment_path(comment.article_id(), comment.id()))), \"button_to\", ViewHelpers::method_override_input(serde_json::Value::from(\"delete\")), \"text-red-600 text-sm mt-2\", \"Are you sure?\", \"Delete\", ViewHelpers::csrf_token_hidden_input()).ok(); };\n        io.push_str(\"\\n</div>\\n\");\n        io\n    }\n}\n"},{"path":"src/views/layouts.rs","content":"#[allow(unused_imports)]\nuse std::fmt::Write as _;\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::inflector::{self, Inflector};\n#[allow(unused_imports)]\nuse crate::importmap::{self, Importmap};\n#[allow(unused_imports)]\nuse crate::json_builder::{self, JsonBuilder};\n#[allow(unused_imports)]\nuse crate::params::{self, Params};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct Layouts;\n\nimpl Layouts {\n    pub fn application(body: &str, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(\"<!DOCTYPE html>\\n<html>\\n  <head>\\n    <title>\");\n        io.push_str(&ViewHelpers::html_escape(&(ViewHelpers::content_for_get(\"title\").unwrap_or(\"Real Blog\".to_string()).to_string())));\n        io.push_str(\"</title>\\n    <meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\">\\n    <meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\\n    <meta name=\\\"application-name\\\" content=\\\"Real Blog\\\">\\n    <meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\\n    \");\n        io.push_str(&ViewHelpers::csrf_meta_tags());\n        io.push_str(\"\\n    \");\n        io.push_str(&ViewHelpers::csp_meta_tag());\n        io.push_str(\"\\n\\n    \");\n        io.push_str(&ViewHelpers::get_slot(\"head\").to_string());\n        io.push_str(\"\\n\\n\\n    <link rel=\\\"icon\\\" href=\\\"/icon.png\\\" type=\\\"image/png\\\">\\n    <link rel=\\\"icon\\\" href=\\\"/icon.svg\\\" type=\\\"image/svg+xml\\\">\\n    <link rel=\\\"apple-touch-icon\\\" href=\\\"/icon.png\\\">\\n\\n    \");\n        io.push_str(&format!(\"{}{}\", format!(\"{}{}\", ViewHelpers::stylesheet_link_tag(\"application\", std::collections::HashMap::from([(\"data-turbo-track\", \"reload\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()), \"\\n\"), ViewHelpers::stylesheet_link_tag(\"tailwind\", std::collections::HashMap::from([(\"data-turbo-track\", \"reload\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>())));\n        io.push_str(\"\\n    \");\n        io.push_str(&ViewHelpers::javascript_importmap_tags(Some(Importmap::pins()), &(Importmap::entry())));\n        io.push_str(\"\\n  </head>\\n\\n  <body>\\n    <main class=\\\"container mx-auto mt-28 px-5 flex flex-col\\\">\\n      \");\n        io.push_str(&body);\n        io.push_str(\"\\n    </main>\\n  </body>\\n</html>\\n\");\n        io\n    }\n\n    pub fn mailer(body: &str, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(\"<!DOCTYPE html>\\n<html>\\n  <head>\\n    <meta http-equiv=\\\"Content-Type\\\" content=\\\"text/html; charset=utf-8\\\">\\n    <style>\\n      /* Email styles need to be inline */\\n    </style>\\n  </head>\\n\\n  <body>\\n    \");\n        io.push_str(&body);\n        io.push_str(\"\\n  </body>\\n</html>\\n\");\n        io\n    }\n}\n\n// Wedge 2c.4 layout bridge — server runtime slot is\n// `fn() -> String` (no args), so wrap the 3-arg\n// template signature with a thread-local body read.\npub fn render_layout() -> String {\n    let body = crate::view_helpers::ViewHelpers::get_yield();\n    Layouts::application(&body, None, None)\n}\n"},{"path":"src/views/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod articles;\npub mod comments;\npub mod layouts;\npub use articles::Articles;\npub use comments::Comments;\npub use layouts::Layouts;\n"},{"path":"static/assets/application.js","content":"// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails\nimport \"@hotwired/turbo-rails\"\nimport \"controllers\"\n"},{"path":"static/assets/controllers/application.js","content":"import { Application } from \"@hotwired/stimulus\"\n\nconst application = Application.start()\n\n// Configure Stimulus development experience\napplication.debug = false\nwindow.Stimulus   = application\n\nexport { application }\n"},{"path":"static/assets/controllers/hello_controller.js","content":"import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  connect() {\n    this.element.textContent = \"Hello World!\"\n  }\n}\n"},{"path":"static/assets/controllers/index.js","content":"// Import and register all your controllers from the importmap via controllers/**/*_controller\nimport { application } from \"controllers/application\"\nimport { eagerLoadControllersFrom } from \"@hotwired/stimulus-loading\"\neagerLoadControllersFrom(\"controllers\", application)\n"},{"path":"static/assets/stimulus-loading.js","content":"// FIXME: es-module-shim won't shim the dynamic import without this explicit import\nimport \"@hotwired/stimulus\"\n\nconst controllerAttribute = \"data-controller\"\n\n// Eager load all controllers registered beneath the `under` path in the import map to the passed application instance.\nexport function eagerLoadControllersFrom(under, application) {\n  const paths = Object.keys(parseImportmapJson()).filter(path => path.match(new RegExp(`^${under}/.*_controller$`)))\n  paths.forEach(path => registerControllerFromPath(path, under, application))\n}\n\nfunction parseImportmapJson() {\n  return JSON.parse(document.querySelector(\"script[type=importmap]\").text).imports\n}\n\nfunction registerControllerFromPath(path, under, application) {\n  const name = path\n    .replace(new RegExp(`^${under}/`), \"\")\n    .replace(\"_controller\", \"\")\n    .replace(/\\//g, \"--\")\n    .replace(/_/g, \"-\")\n\n  if (canRegisterController(name, application)) {\n    import(path)\n      .then(module => registerController(name, module, application))\n      .catch(error => console.error(`Failed to register controller: ${name} (${path})`, error))\n  }\n}\n\n\n// Lazy load controllers registered beneath the `under` path in the import map to the passed application instance.\nexport function lazyLoadControllersFrom(under, application, element = document) {\n  lazyLoadExistingControllers(under, application, element)\n  lazyLoadNewControllers(under, application, element)\n}\n\nfunction lazyLoadExistingControllers(under, application, element) {\n  queryControllerNamesWithin(element).forEach(controllerName => loadController(controllerName, under, application))\n}\n\nfunction lazyLoadNewControllers(under, application, element) {\n  new MutationObserver((mutationsList) => {\n    for (const { attributeName, target, type } of mutationsList) {\n      switch (type) {\n        case \"attributes\": {\n          if (attributeName == controllerAttribute && target.getAttribute(controllerAttribute)) {\n            extractControllerNamesFrom(target).forEach(controllerName => loadController(controllerName, under, application))\n          }\n        }\n\n        case \"childList\": {\n          lazyLoadExistingControllers(under, application, target)\n        }\n      }\n    }\n  }).observe(element, { attributeFilter: [controllerAttribute], subtree: true, childList: true })\n}\n\nfunction queryControllerNamesWithin(element) {\n  return Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).map(extractControllerNamesFrom).flat()\n}\n\nfunction extractControllerNamesFrom(element) {\n  return element.getAttribute(controllerAttribute).split(/\\s+/).filter(content => content.length)\n}\n\nfunction loadController(name, under, application) {\n  if (canRegisterController(name, application)) {\n    import(controllerFilename(name, under))\n      .then(module => registerController(name, module, application))\n      .catch(error => console.error(`Failed to autoload controller: ${name}`, error))\n  }\n}\n\nfunction controllerFilename(name, under) {\n  return `${under}/${name.replace(/--/g, \"/\").replace(/-/g, \"_\")}_controller`\n}\n\nfunction registerController(name, module, application) {\n  if (canRegisterController(name, application)) {\n    application.register(name, module.default)\n  }\n}\n\nfunction canRegisterController(name, application){\n  return !application.router.modulesByIdentifier.has(name)\n}\n"},{"path":"static/assets/stimulus.min.js","content":"//= link ./stimulus-autoloader.js\n//= link ./stimulus-importmap-autoloader.js\n//= link ./stimulus-loading.js\nclass e{constructor(e,t,s){this.eventTarget=e,this.eventName=t,this.eventOptions=s,this.unorderedBindings=new Set}connect(){this.eventTarget.addEventListener(this.eventName,this,this.eventOptions)}disconnect(){this.eventTarget.removeEventListener(this.eventName,this,this.eventOptions)}bindingConnected(e){this.unorderedBindings.add(e)}bindingDisconnected(e){this.unorderedBindings.delete(e)}handleEvent(e){const t=function(e){if(\"immediatePropagationStopped\"in e)return e;{const{stopImmediatePropagation:t}=e;return Object.assign(e,{immediatePropagationStopped:!1,stopImmediatePropagation(){this.immediatePropagationStopped=!0,t.call(this)}})}}(e);for(const e of this.bindings){if(t.immediatePropagationStopped)break;e.handleEvent(t)}}hasBindings(){return this.unorderedBindings.size>0}get bindings(){return Array.from(this.unorderedBindings).sort(((e,t)=>{const s=e.index,r=t.index;return s<r?-1:s>r?1:0}))}}class t{constructor(e){this.application=e,this.eventListenerMaps=new Map,this.started=!1}start(){this.started||(this.started=!0,this.eventListeners.forEach((e=>e.connect())))}stop(){this.started&&(this.started=!1,this.eventListeners.forEach((e=>e.disconnect())))}get eventListeners(){return Array.from(this.eventListenerMaps.values()).reduce(((e,t)=>e.concat(Array.from(t.values()))),[])}bindingConnected(e){this.fetchEventListenerForBinding(e).bindingConnected(e)}bindingDisconnected(e,t=!1){this.fetchEventListenerForBinding(e).bindingDisconnected(e),t&&this.clearEventListenersForBinding(e)}handleError(e,t,s={}){this.application.handleError(e,`Error ${t}`,s)}clearEventListenersForBinding(e){const t=this.fetchEventListenerForBinding(e);t.hasBindings()||(t.disconnect(),this.removeMappedEventListenerFor(e))}removeMappedEventListenerFor(e){const{eventTarget:t,eventName:s,eventOptions:r}=e,n=this.fetchEventListenerMapForEventTarget(t),i=this.cacheKey(s,r);n.delete(i),0==n.size&&this.eventListenerMaps.delete(t)}fetchEventListenerForBinding(e){const{eventTarget:t,eventName:s,eventOptions:r}=e;return this.fetchEventListener(t,s,r)}fetchEventListener(e,t,s){const r=this.fetchEventListenerMapForEventTarget(e),n=this.cacheKey(t,s);let i=r.get(n);return i||(i=this.createEventListener(e,t,s),r.set(n,i)),i}createEventListener(t,s,r){const n=new e(t,s,r);return this.started&&n.connect(),n}fetchEventListenerMapForEventTarget(e){let t=this.eventListenerMaps.get(e);return t||(t=new Map,this.eventListenerMaps.set(e,t)),t}cacheKey(e,t){const s=[e];return Object.keys(t).sort().forEach((e=>{s.push(`${t[e]?\"\":\"!\"}${e}`)})),s.join(\":\")}}const s={stop:({event:e,value:t})=>(t&&e.stopPropagation(),!0),prevent:({event:e,value:t})=>(t&&e.preventDefault(),!0),self:({event:e,value:t,element:s})=>!t||s===e.target},r=/^(?:(?:([^.]+?)\\+)?(.+?)(?:\\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/;function n(e){return\"window\"==e?window:\"document\"==e?document:void 0}function i(e){return e.replace(/(?:[_-])([a-z0-9])/g,((e,t)=>t.toUpperCase()))}function o(e){return i(e.replace(/--/g,\"-\").replace(/__/g,\"_\"))}function a(e){return e.charAt(0).toUpperCase()+e.slice(1)}function c(e){return e.replace(/([A-Z])/g,((e,t)=>`-${t.toLowerCase()}`))}function l(e){return null!=e}function h(e,t){return Object.prototype.hasOwnProperty.call(e,t)}const u=[\"meta\",\"ctrl\",\"alt\",\"shift\"];const d={a:()=>\"click\",button:()=>\"click\",form:()=>\"submit\",details:()=>\"toggle\",input:e=>\"submit\"==e.getAttribute(\"type\")?\"click\":\"input\",select:()=>\"change\",textarea:()=>\"input\"};function m(e){throw new Error(e)}function g(e){try{return JSON.parse(e)}catch(t){return e}}class p{constructor(e,t){this.context=e,this.action=t}get index(){return this.action.index}get eventTarget(){return this.action.eventTarget}get eventOptions(){return this.action.eventOptions}get identifier(){return this.context.identifier}handleEvent(e){const t=this.prepareActionEvent(e);this.willBeInvokedByEvent(e)&&this.applyEventModifiers(t)&&this.invokeWithEvent(t)}get eventName(){return this.action.eventName}get method(){const e=this.controller[this.methodName];if(\"function\"==typeof e)return e;throw new Error(`Action \"${this.action}\" references undefined method \"${this.methodName}\"`)}applyEventModifiers(e){const{element:t}=this.action,{actionDescriptorFilters:s}=this.context.application,{controller:r}=this.context;let n=!0;for(const[i,o]of Object.entries(this.eventOptions))if(i in s){const a=s[i];n=n&&a({name:i,value:o,event:e,element:t,controller:r})}return n}prepareActionEvent(e){return Object.assign(e,{params:this.action.params})}invokeWithEvent(e){const{target:t,currentTarget:s}=e;try{this.method.call(this.controller,e),this.context.logDebugActivity(this.methodName,{event:e,target:t,currentTarget:s,action:this.methodName})}catch(t){const{identifier:s,controller:r,element:n,index:i}=this,o={identifier:s,controller:r,element:n,index:i,event:e};this.context.handleError(t,`invoking action \"${this.action}\"`,o)}}willBeInvokedByEvent(e){const t=e.target;return!(e instanceof KeyboardEvent&&this.action.shouldIgnoreKeyboardEvent(e))&&(!(e instanceof MouseEvent&&this.action.shouldIgnoreMouseEvent(e))&&(this.element===t||(t instanceof Element&&this.element.contains(t)?this.scope.containsElement(t):this.scope.containsElement(this.action.element))))}get controller(){return this.context.controller}get methodName(){return this.action.methodName}get element(){return this.scope.element}get scope(){return this.context.scope}}class f{constructor(e,t){this.mutationObserverInit={attributes:!0,childList:!0,subtree:!0},this.element=e,this.started=!1,this.delegate=t,this.elements=new Set,this.mutationObserver=new MutationObserver((e=>this.processMutations(e)))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,this.mutationObserverInit),this.refresh())}pause(e){this.started&&(this.mutationObserver.disconnect(),this.started=!1),e(),this.started||(this.mutationObserver.observe(this.element,this.mutationObserverInit),this.started=!0)}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started){const e=new Set(this.matchElementsInTree());for(const t of Array.from(this.elements))e.has(t)||this.removeElement(t);for(const t of Array.from(e))this.addElement(t)}}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){\"attributes\"==e.type?this.processAttributeChange(e.target,e.attributeName):\"childList\"==e.type&&(this.processRemovedNodes(e.removedNodes),this.processAddedNodes(e.addedNodes))}processAttributeChange(e,t){this.elements.has(e)?this.delegate.elementAttributeChanged&&this.matchElement(e)?this.delegate.elementAttributeChanged(e,t):this.removeElement(e):this.matchElement(e)&&this.addElement(e)}processRemovedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.processTree(e,this.removeElement)}}processAddedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.elementIsActive(e)&&this.processTree(e,this.addElement)}}matchElement(e){return this.delegate.matchElement(e)}matchElementsInTree(e=this.element){return this.delegate.matchElementsInTree(e)}processTree(e,t){for(const s of this.matchElementsInTree(e))t.call(this,s)}elementFromNode(e){if(e.nodeType==Node.ELEMENT_NODE)return e}elementIsActive(e){return e.isConnected==this.element.isConnected&&this.element.contains(e)}addElement(e){this.elements.has(e)||this.elementIsActive(e)&&(this.elements.add(e),this.delegate.elementMatched&&this.delegate.elementMatched(e))}removeElement(e){this.elements.has(e)&&(this.elements.delete(e),this.delegate.elementUnmatched&&this.delegate.elementUnmatched(e))}}class b{constructor(e,t,s){this.attributeName=t,this.delegate=s,this.elementObserver=new f(e,this)}get element(){return this.elementObserver.element}get selector(){return`[${this.attributeName}]`}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get started(){return this.elementObserver.started}matchElement(e){return e.hasAttribute(this.attributeName)}matchElementsInTree(e){const t=this.matchElement(e)?[e]:[],s=Array.from(e.querySelectorAll(this.selector));return t.concat(s)}elementMatched(e){this.delegate.elementMatchedAttribute&&this.delegate.elementMatchedAttribute(e,this.attributeName)}elementUnmatched(e){this.delegate.elementUnmatchedAttribute&&this.delegate.elementUnmatchedAttribute(e,this.attributeName)}elementAttributeChanged(e,t){this.delegate.elementAttributeValueChanged&&this.attributeName==t&&this.delegate.elementAttributeValueChanged(e,t)}}function v(e,t,s){O(e,t).add(s)}function y(e,t,s){O(e,t).delete(s),A(e,t)}function O(e,t){let s=e.get(t);return s||(s=new Set,e.set(t,s)),s}function A(e,t){const s=e.get(t);null!=s&&0==s.size&&e.delete(t)}class E{constructor(){this.valuesByKey=new Map}get keys(){return Array.from(this.valuesByKey.keys())}get values(){return Array.from(this.valuesByKey.values()).reduce(((e,t)=>e.concat(Array.from(t))),[])}get size(){return Array.from(this.valuesByKey.values()).reduce(((e,t)=>e+t.size),0)}add(e,t){v(this.valuesByKey,e,t)}delete(e,t){y(this.valuesByKey,e,t)}has(e,t){const s=this.valuesByKey.get(e);return null!=s&&s.has(t)}hasKey(e){return this.valuesByKey.has(e)}hasValue(e){return Array.from(this.valuesByKey.values()).some((t=>t.has(e)))}getValuesForKey(e){const t=this.valuesByKey.get(e);return t?Array.from(t):[]}getKeysForValue(e){return Array.from(this.valuesByKey).filter((([t,s])=>s.has(e))).map((([e,t])=>e))}}class w extends E{constructor(){super(),this.keysByValue=new Map}get values(){return Array.from(this.keysByValue.keys())}add(e,t){super.add(e,t),v(this.keysByValue,t,e)}delete(e,t){super.delete(e,t),y(this.keysByValue,t,e)}hasValue(e){return this.keysByValue.has(e)}getKeysForValue(e){const t=this.keysByValue.get(e);return t?Array.from(t):[]}}class M{constructor(e,t,s,r){this._selector=t,this.details=r,this.elementObserver=new f(e,this),this.delegate=s,this.matchesByElement=new E}get started(){return this.elementObserver.started}get selector(){return this._selector}set selector(e){this._selector=e,this.refresh()}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get element(){return this.elementObserver.element}matchElement(e){const{selector:t}=this;if(t){const s=e.matches(t);return this.delegate.selectorMatchElement?s&&this.delegate.selectorMatchElement(e,this.details):s}return!1}matchElementsInTree(e){const{selector:t}=this;if(t){const s=this.matchElement(e)?[e]:[],r=Array.from(e.querySelectorAll(t)).filter((e=>this.matchElement(e)));return s.concat(r)}return[]}elementMatched(e){const{selector:t}=this;t&&this.selectorMatched(e,t)}elementUnmatched(e){const t=this.matchesByElement.getKeysForValue(e);for(const s of t)this.selectorUnmatched(e,s)}elementAttributeChanged(e,t){const{selector:s}=this;if(s){const t=this.matchElement(e),r=this.matchesByElement.has(s,e);t&&!r?this.selectorMatched(e,s):!t&&r&&this.selectorUnmatched(e,s)}}selectorMatched(e,t){this.delegate.selectorMatched(e,t,this.details),this.matchesByElement.add(t,e)}selectorUnmatched(e,t){this.delegate.selectorUnmatched(e,t,this.details),this.matchesByElement.delete(t,e)}}class k{constructor(e,t){this.element=e,this.delegate=t,this.started=!1,this.stringMap=new Map,this.mutationObserver=new MutationObserver((e=>this.processMutations(e)))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,{attributes:!0,attributeOldValue:!0}),this.refresh())}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started)for(const e of this.knownAttributeNames)this.refreshAttribute(e,null)}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){const t=e.attributeName;t&&this.refreshAttribute(t,e.oldValue)}refreshAttribute(e,t){const s=this.delegate.getStringMapKeyForAttribute(e);if(null!=s){this.stringMap.has(e)||this.stringMapKeyAdded(s,e);const r=this.element.getAttribute(e);if(this.stringMap.get(e)!=r&&this.stringMapValueChanged(r,s,t),null==r){const t=this.stringMap.get(e);this.stringMap.delete(e),t&&this.stringMapKeyRemoved(s,e,t)}else this.stringMap.set(e,r)}}stringMapKeyAdded(e,t){this.delegate.stringMapKeyAdded&&this.delegate.stringMapKeyAdded(e,t)}stringMapValueChanged(e,t,s){this.delegate.stringMapValueChanged&&this.delegate.stringMapValueChanged(e,t,s)}stringMapKeyRemoved(e,t,s){this.delegate.stringMapKeyRemoved&&this.delegate.stringMapKeyRemoved(e,t,s)}get knownAttributeNames(){return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)))}get currentAttributeNames(){return Array.from(this.element.attributes).map((e=>e.name))}get recordedAttributeNames(){return Array.from(this.stringMap.keys())}}class N{constructor(e,t,s){this.attributeObserver=new b(e,t,this),this.delegate=s,this.tokensByElement=new E}get started(){return this.attributeObserver.started}start(){this.attributeObserver.start()}pause(e){this.attributeObserver.pause(e)}stop(){this.attributeObserver.stop()}refresh(){this.attributeObserver.refresh()}get element(){return this.attributeObserver.element}get attributeName(){return this.attributeObserver.attributeName}elementMatchedAttribute(e){this.tokensMatched(this.readTokensForElement(e))}elementAttributeValueChanged(e){const[t,s]=this.refreshTokensForElement(e);this.tokensUnmatched(t),this.tokensMatched(s)}elementUnmatchedAttribute(e){this.tokensUnmatched(this.tokensByElement.getValuesForKey(e))}tokensMatched(e){e.forEach((e=>this.tokenMatched(e)))}tokensUnmatched(e){e.forEach((e=>this.tokenUnmatched(e)))}tokenMatched(e){this.delegate.tokenMatched(e),this.tokensByElement.add(e.element,e)}tokenUnmatched(e){this.delegate.tokenUnmatched(e),this.tokensByElement.delete(e.element,e)}refreshTokensForElement(e){const t=this.tokensByElement.getValuesForKey(e),s=this.readTokensForElement(e),r=function(e,t){const s=Math.max(e.length,t.length);return Array.from({length:s},((s,r)=>[e[r],t[r]]))}(t,s).findIndex((([e,t])=>{return r=t,!((s=e)&&r&&s.index==r.index&&s.content==r.content);var s,r}));return-1==r?[[],[]]:[t.slice(r),s.slice(r)]}readTokensForElement(e){const t=this.attributeName;return function(e,t,s){return e.trim().split(/\\s+/).filter((e=>e.length)).map(((e,r)=>({element:t,attributeName:s,content:e,index:r})))}(e.getAttribute(t)||\"\",e,t)}}class F{constructor(e,t,s){this.tokenListObserver=new N(e,t,this),this.delegate=s,this.parseResultsByToken=new WeakMap,this.valuesByTokenByElement=new WeakMap}get started(){return this.tokenListObserver.started}start(){this.tokenListObserver.start()}stop(){this.tokenListObserver.stop()}refresh(){this.tokenListObserver.refresh()}get element(){return this.tokenListObserver.element}get attributeName(){return this.tokenListObserver.attributeName}tokenMatched(e){const{element:t}=e,{value:s}=this.fetchParseResultForToken(e);s&&(this.fetchValuesByTokenForElement(t).set(e,s),this.delegate.elementMatchedValue(t,s))}tokenUnmatched(e){const{element:t}=e,{value:s}=this.fetchParseResultForToken(e);s&&(this.fetchValuesByTokenForElement(t).delete(e),this.delegate.elementUnmatchedValue(t,s))}fetchParseResultForToken(e){let t=this.parseResultsByToken.get(e);return t||(t=this.parseToken(e),this.parseResultsByToken.set(e,t)),t}fetchValuesByTokenForElement(e){let t=this.valuesByTokenByElement.get(e);return t||(t=new Map,this.valuesByTokenByElement.set(e,t)),t}parseToken(e){try{return{value:this.delegate.parseValueForToken(e)}}catch(e){return{error:e}}}}class B{constructor(e,t){this.context=e,this.delegate=t,this.bindingsByAction=new Map}start(){this.valueListObserver||(this.valueListObserver=new F(this.element,this.actionAttribute,this),this.valueListObserver.start())}stop(){this.valueListObserver&&(this.valueListObserver.stop(),delete this.valueListObserver,this.disconnectAllActions())}get element(){return this.context.element}get identifier(){return this.context.identifier}get actionAttribute(){return this.schema.actionAttribute}get schema(){return this.context.schema}get bindings(){return Array.from(this.bindingsByAction.values())}connectAction(e){const t=new p(this.context,e);this.bindingsByAction.set(e,t),this.delegate.bindingConnected(t)}disconnectAction(e){const t=this.bindingsByAction.get(e);t&&(this.bindingsByAction.delete(e),this.delegate.bindingDisconnected(t))}disconnectAllActions(){this.bindings.forEach((e=>this.delegate.bindingDisconnected(e,!0))),this.bindingsByAction.clear()}parseValueForToken(e){const t=class{constructor(e,t,s,r){this.element=e,this.index=t,this.eventTarget=s.eventTarget||e,this.eventName=s.eventName||function(e){const t=e.tagName.toLowerCase();if(t in d)return d[t](e)}(e)||m(\"missing event name\"),this.eventOptions=s.eventOptions||{},this.identifier=s.identifier||m(\"missing identifier\"),this.methodName=s.methodName||m(\"missing method name\"),this.keyFilter=s.keyFilter||\"\",this.schema=r}static forToken(e,t){return new this(e.element,e.index,function(e){const t=e.trim().match(r)||[];let s=t[2],i=t[3];return i&&![\"keydown\",\"keyup\",\"keypress\"].includes(s)&&(s+=`.${i}`,i=\"\"),{eventTarget:n(t[4]),eventName:s,eventOptions:t[7]?(o=t[7],o.split(\":\").reduce(((e,t)=>Object.assign(e,{[t.replace(/^!/,\"\")]:!/^!/.test(t)})),{})):{},identifier:t[5],methodName:t[6],keyFilter:t[1]||i};var o}(e.content),t)}toString(){const e=this.keyFilter?`.${this.keyFilter}`:\"\",t=this.eventTargetName?`@${this.eventTargetName}`:\"\";return`${this.eventName}${e}${t}->${this.identifier}#${this.methodName}`}shouldIgnoreKeyboardEvent(e){if(!this.keyFilter)return!1;const t=this.keyFilter.split(\"+\");if(this.keyFilterDissatisfied(e,t))return!0;const s=t.filter((e=>!u.includes(e)))[0];return!!s&&(h(this.keyMappings,s)||m(`contains unknown key filter: ${this.keyFilter}`),this.keyMappings[s].toLowerCase()!==e.key.toLowerCase())}shouldIgnoreMouseEvent(e){if(!this.keyFilter)return!1;const t=[this.keyFilter];return!!this.keyFilterDissatisfied(e,t)}get params(){const e={},t=new RegExp(`^data-${this.identifier}-(.+)-param$`,\"i\");for(const{name:s,value:r}of Array.from(this.element.attributes)){const n=s.match(t),o=n&&n[1];o&&(e[i(o)]=g(r))}return e}get eventTargetName(){return(e=this.eventTarget)==window?\"window\":e==document?\"document\":void 0;var e}get keyMappings(){return this.schema.keyMappings}keyFilterDissatisfied(e,t){const[s,r,n,i]=u.map((e=>t.includes(e)));return e.metaKey!==s||e.ctrlKey!==r||e.altKey!==n||e.shiftKey!==i}}.forToken(e,this.schema);if(t.identifier==this.identifier)return t}elementMatchedValue(e,t){this.connectAction(t)}elementUnmatchedValue(e,t){this.disconnectAction(t)}}class C{constructor(e,t){this.context=e,this.receiver=t,this.stringMapObserver=new k(this.element,this),this.valueDescriptorMap=this.controller.valueDescriptorMap}start(){this.stringMapObserver.start(),this.invokeChangedCallbacksForDefaultValues()}stop(){this.stringMapObserver.stop()}get element(){return this.context.element}get controller(){return this.context.controller}getStringMapKeyForAttribute(e){if(e in this.valueDescriptorMap)return this.valueDescriptorMap[e].name}stringMapKeyAdded(e,t){const s=this.valueDescriptorMap[t];this.hasValue(e)||this.invokeChangedCallback(e,s.writer(this.receiver[e]),s.writer(s.defaultValue))}stringMapValueChanged(e,t,s){const r=this.valueDescriptorNameMap[t];null!==e&&(null===s&&(s=r.writer(r.defaultValue)),this.invokeChangedCallback(t,e,s))}stringMapKeyRemoved(e,t,s){const r=this.valueDescriptorNameMap[e];this.hasValue(e)?this.invokeChangedCallback(e,r.writer(this.receiver[e]),s):this.invokeChangedCallback(e,r.writer(r.defaultValue),s)}invokeChangedCallbacksForDefaultValues(){for(const{key:e,name:t,defaultValue:s,writer:r}of this.valueDescriptors)null==s||this.controller.data.has(e)||this.invokeChangedCallback(t,r(s),void 0)}invokeChangedCallback(e,t,s){const r=`${e}Changed`,n=this.receiver[r];if(\"function\"==typeof n){const r=this.valueDescriptorNameMap[e];try{const e=r.reader(t);let i=s;s&&(i=r.reader(s)),n.call(this.receiver,e,i)}catch(e){throw e instanceof TypeError&&(e.message=`Stimulus Value \"${this.context.identifier}.${r.name}\" - ${e.message}`),e}}}get valueDescriptors(){const{valueDescriptorMap:e}=this;return Object.keys(e).map((t=>e[t]))}get valueDescriptorNameMap(){const e={};return Object.keys(this.valueDescriptorMap).forEach((t=>{const s=this.valueDescriptorMap[t];e[s.name]=s})),e}hasValue(e){const t=`has${a(this.valueDescriptorNameMap[e].name)}`;return this.receiver[t]}}class ${constructor(e,t){this.context=e,this.delegate=t,this.targetsByName=new E}start(){this.tokenListObserver||(this.tokenListObserver=new N(this.element,this.attributeName,this),this.tokenListObserver.start())}stop(){this.tokenListObserver&&(this.disconnectAllTargets(),this.tokenListObserver.stop(),delete this.tokenListObserver)}tokenMatched({element:e,content:t}){this.scope.containsElement(e)&&this.connectTarget(e,t)}tokenUnmatched({element:e,content:t}){this.disconnectTarget(e,t)}connectTarget(e,t){var s;this.targetsByName.has(t,e)||(this.targetsByName.add(t,e),null===(s=this.tokenListObserver)||void 0===s||s.pause((()=>this.delegate.targetConnected(e,t))))}disconnectTarget(e,t){var s;this.targetsByName.has(t,e)&&(this.targetsByName.delete(t,e),null===(s=this.tokenListObserver)||void 0===s||s.pause((()=>this.delegate.targetDisconnected(e,t))))}disconnectAllTargets(){for(const e of this.targetsByName.keys)for(const t of this.targetsByName.getValuesForKey(e))this.disconnectTarget(t,e)}get attributeName(){return`data-${this.context.identifier}-target`}get element(){return this.context.element}get scope(){return this.context.scope}}function T(e,t){const s=x(e);return Array.from(s.reduce(((e,s)=>(function(e,t){const s=e[t];return Array.isArray(s)?s:[]}(s,t).forEach((t=>e.add(t))),e)),new Set))}function S(e,t){return x(e).reduce(((e,s)=>(e.push(...function(e,t){const s=e[t];return s?Object.keys(s).map((e=>[e,s[e]])):[]}(s,t)),e)),[])}function x(e){const t=[];for(;e;)t.push(e),e=Object.getPrototypeOf(e);return t.reverse()}class D{constructor(e,t){this.started=!1,this.context=e,this.delegate=t,this.outletsByName=new E,this.outletElementsByName=new E,this.selectorObserverMap=new Map,this.attributeObserverMap=new Map}start(){this.started||(this.outletDefinitions.forEach((e=>{this.setupSelectorObserverForOutlet(e),this.setupAttributeObserverForOutlet(e)})),this.started=!0,this.dependentContexts.forEach((e=>e.refresh())))}refresh(){this.selectorObserverMap.forEach((e=>e.refresh())),this.attributeObserverMap.forEach((e=>e.refresh()))}stop(){this.started&&(this.started=!1,this.disconnectAllOutlets(),this.stopSelectorObservers(),this.stopAttributeObservers())}stopSelectorObservers(){this.selectorObserverMap.size>0&&(this.selectorObserverMap.forEach((e=>e.stop())),this.selectorObserverMap.clear())}stopAttributeObservers(){this.attributeObserverMap.size>0&&(this.attributeObserverMap.forEach((e=>e.stop())),this.attributeObserverMap.clear())}selectorMatched(e,t,{outletName:s}){const r=this.getOutlet(e,s);r&&this.connectOutlet(r,e,s)}selectorUnmatched(e,t,{outletName:s}){const r=this.getOutletFromMap(e,s);r&&this.disconnectOutlet(r,e,s)}selectorMatchElement(e,{outletName:t}){const s=this.selector(t),r=this.hasOutlet(e,t),n=e.matches(`[${this.schema.controllerAttribute}~=${t}]`);return!!s&&(r&&n&&e.matches(s))}elementMatchedAttribute(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}elementAttributeValueChanged(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}elementUnmatchedAttribute(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}connectOutlet(e,t,s){var r;this.outletElementsByName.has(s,t)||(this.outletsByName.add(s,e),this.outletElementsByName.add(s,t),null===(r=this.selectorObserverMap.get(s))||void 0===r||r.pause((()=>this.delegate.outletConnected(e,t,s))))}disconnectOutlet(e,t,s){var r;this.outletElementsByName.has(s,t)&&(this.outletsByName.delete(s,e),this.outletElementsByName.delete(s,t),null===(r=this.selectorObserverMap.get(s))||void 0===r||r.pause((()=>this.delegate.outletDisconnected(e,t,s))))}disconnectAllOutlets(){for(const e of this.outletElementsByName.keys)for(const t of this.outletElementsByName.getValuesForKey(e))for(const s of this.outletsByName.getValuesForKey(e))this.disconnectOutlet(s,t,e)}updateSelectorObserverForOutlet(e){const t=this.selectorObserverMap.get(e);t&&(t.selector=this.selector(e))}setupSelectorObserverForOutlet(e){const t=this.selector(e),s=new M(document.body,t,this,{outletName:e});this.selectorObserverMap.set(e,s),s.start()}setupAttributeObserverForOutlet(e){const t=this.attributeNameForOutletName(e),s=new b(this.scope.element,t,this);this.attributeObserverMap.set(e,s),s.start()}selector(e){return this.scope.outlets.getSelectorForOutletName(e)}attributeNameForOutletName(e){return this.scope.schema.outletAttributeForScope(this.identifier,e)}getOutletNameFromOutletAttributeName(e){return this.outletDefinitions.find((t=>this.attributeNameForOutletName(t)===e))}get outletDependencies(){const e=new E;return this.router.modules.forEach((t=>{T(t.definition.controllerConstructor,\"outlets\").forEach((s=>e.add(s,t.identifier)))})),e}get outletDefinitions(){return this.outletDependencies.getKeysForValue(this.identifier)}get dependentControllerIdentifiers(){return this.outletDependencies.getValuesForKey(this.identifier)}get dependentContexts(){const e=this.dependentControllerIdentifiers;return this.router.contexts.filter((t=>e.includes(t.identifier)))}hasOutlet(e,t){return!!this.getOutlet(e,t)||!!this.getOutletFromMap(e,t)}getOutlet(e,t){return this.application.getControllerForElementAndIdentifier(e,t)}getOutletFromMap(e,t){return this.outletsByName.getValuesForKey(t).find((t=>t.element===e))}get scope(){return this.context.scope}get schema(){return this.context.schema}get identifier(){return this.context.identifier}get application(){return this.context.application}get router(){return this.application.router}}class L{constructor(e,t){this.logDebugActivity=(e,t={})=>{const{identifier:s,controller:r,element:n}=this;t=Object.assign({identifier:s,controller:r,element:n},t),this.application.logDebugActivity(this.identifier,e,t)},this.module=e,this.scope=t,this.controller=new e.controllerConstructor(this),this.bindingObserver=new B(this,this.dispatcher),this.valueObserver=new C(this,this.controller),this.targetObserver=new $(this,this),this.outletObserver=new D(this,this);try{this.controller.initialize(),this.logDebugActivity(\"initialize\")}catch(e){this.handleError(e,\"initializing controller\")}}connect(){this.bindingObserver.start(),this.valueObserver.start(),this.targetObserver.start(),this.outletObserver.start();try{this.controller.connect(),this.logDebugActivity(\"connect\")}catch(e){this.handleError(e,\"connecting controller\")}}refresh(){this.outletObserver.refresh()}disconnect(){try{this.controller.disconnect(),this.logDebugActivity(\"disconnect\")}catch(e){this.handleError(e,\"disconnecting controller\")}this.outletObserver.stop(),this.targetObserver.stop(),this.valueObserver.stop(),this.bindingObserver.stop()}get application(){return this.module.application}get identifier(){return this.module.identifier}get schema(){return this.application.schema}get dispatcher(){return this.application.dispatcher}get element(){return this.scope.element}get parentElement(){return this.element.parentElement}handleError(e,t,s={}){const{identifier:r,controller:n,element:i}=this;s=Object.assign({identifier:r,controller:n,element:i},s),this.application.handleError(e,`Error ${t}`,s)}targetConnected(e,t){this.invokeControllerMethod(`${t}TargetConnected`,e)}targetDisconnected(e,t){this.invokeControllerMethod(`${t}TargetDisconnected`,e)}outletConnected(e,t,s){this.invokeControllerMethod(`${o(s)}OutletConnected`,e,t)}outletDisconnected(e,t,s){this.invokeControllerMethod(`${o(s)}OutletDisconnected`,e,t)}invokeControllerMethod(e,...t){const s=this.controller;\"function\"==typeof s[e]&&s[e](...t)}}function V(e){return function(e,t){const s=I(e),r=function(e,t){return K(t).reduce(((s,r)=>{const n=function(e,t,s){const r=Object.getOwnPropertyDescriptor(e,s);if(!r||!(\"value\"in r)){const e=Object.getOwnPropertyDescriptor(t,s).value;return r&&(e.get=r.get||e.get,e.set=r.set||e.set),e}}(e,t,r);return n&&Object.assign(s,{[r]:n}),s}),{})}(e.prototype,t);return Object.defineProperties(s.prototype,r),s}(e,function(e){return T(e,\"blessings\").reduce(((t,s)=>{const r=s(e);for(const e in r){const s=t[e]||{};t[e]=Object.assign(s,r[e])}return t}),{})}(e))}const K=\"function\"==typeof Object.getOwnPropertySymbols?e=>[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)]:Object.getOwnPropertyNames,I=(()=>{function e(e){function t(){return Reflect.construct(e,arguments,new.target)}return t.prototype=Object.create(e.prototype,{constructor:{value:t}}),Reflect.setPrototypeOf(t,e),t}try{return function(){const t=e((function(){this.a.call(this)}));t.prototype.a=function(){},new t}(),e}catch(e){return e=>class extends e{}}})();class j{constructor(e,t){this.application=e,this.definition=function(e){return{identifier:e.identifier,controllerConstructor:V(e.controllerConstructor)}}(t),this.contextsByScope=new WeakMap,this.connectedContexts=new Set}get identifier(){return this.definition.identifier}get controllerConstructor(){return this.definition.controllerConstructor}get contexts(){return Array.from(this.connectedContexts)}connectContextForScope(e){const t=this.fetchContextForScope(e);this.connectedContexts.add(t),t.connect()}disconnectContextForScope(e){const t=this.contextsByScope.get(e);t&&(this.connectedContexts.delete(t),t.disconnect())}fetchContextForScope(e){let t=this.contextsByScope.get(e);return t||(t=new L(this,e),this.contextsByScope.set(e,t)),t}}class U{constructor(e){this.scope=e}has(e){return this.data.has(this.getDataKey(e))}get(e){return this.getAll(e)[0]}getAll(e){const t=this.data.get(this.getDataKey(e))||\"\";return t.match(/[^\\s]+/g)||[]}getAttributeName(e){return this.data.getAttributeNameForKey(this.getDataKey(e))}getDataKey(e){return`${e}-class`}get data(){return this.scope.data}}class P{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get(e){const t=this.getAttributeNameForKey(e);return this.element.getAttribute(t)}set(e,t){const s=this.getAttributeNameForKey(e);return this.element.setAttribute(s,t),this.get(e)}has(e){const t=this.getAttributeNameForKey(e);return this.element.hasAttribute(t)}delete(e){if(this.has(e)){const t=this.getAttributeNameForKey(e);return this.element.removeAttribute(t),!0}return!1}getAttributeNameForKey(e){return`data-${this.identifier}-${c(e)}`}}class R{constructor(e){this.warnedKeysByObject=new WeakMap,this.logger=e}warn(e,t,s){let r=this.warnedKeysByObject.get(e);r||(r=new Set,this.warnedKeysByObject.set(e,r)),r.has(t)||(r.add(t),this.logger.warn(s,e))}}function z(e,t){return`[${e}~=\"${t}\"]`}class _{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce(((e,t)=>e||this.findTarget(t)||this.findLegacyTarget(t)),void 0)}findAll(...e){return e.reduce(((e,t)=>[...e,...this.findAllTargets(t),...this.findAllLegacyTargets(t)]),[])}findTarget(e){const t=this.getSelectorForTargetName(e);return this.scope.findElement(t)}findAllTargets(e){const t=this.getSelectorForTargetName(e);return this.scope.findAllElements(t)}getSelectorForTargetName(e){return z(this.schema.targetAttributeForScope(this.identifier),e)}findLegacyTarget(e){const t=this.getLegacySelectorForTargetName(e);return this.deprecate(this.scope.findElement(t),e)}findAllLegacyTargets(e){const t=this.getLegacySelectorForTargetName(e);return this.scope.findAllElements(t).map((t=>this.deprecate(t,e)))}getLegacySelectorForTargetName(e){const t=`${this.identifier}.${e}`;return z(this.schema.targetAttribute,t)}deprecate(e,t){if(e){const{identifier:s}=this,r=this.schema.targetAttribute,n=this.schema.targetAttributeForScope(s);this.guide.warn(e,`target:${t}`,`Please replace ${r}=\"${s}.${t}\" with ${n}=\"${t}\". The ${r} attribute is deprecated and will be removed in a future version of Stimulus.`)}return e}get guide(){return this.scope.guide}}class q{constructor(e,t){this.scope=e,this.controllerElement=t}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce(((e,t)=>e||this.findOutlet(t)),void 0)}findAll(...e){return e.reduce(((e,t)=>[...e,...this.findAllOutlets(t)]),[])}getSelectorForOutletName(e){const t=this.schema.outletAttributeForScope(this.identifier,e);return this.controllerElement.getAttribute(t)}findOutlet(e){const t=this.getSelectorForOutletName(e);if(t)return this.findElement(t,e)}findAllOutlets(e){const t=this.getSelectorForOutletName(e);return t?this.findAllElements(t,e):[]}findElement(e,t){return this.scope.queryElements(e).filter((s=>this.matchesElement(s,e,t)))[0]}findAllElements(e,t){return this.scope.queryElements(e).filter((s=>this.matchesElement(s,e,t)))}matchesElement(e,t,s){const r=e.getAttribute(this.scope.schema.controllerAttribute)||\"\";return e.matches(t)&&r.split(\" \").includes(s)}}class W{constructor(e,t,s,r){this.targets=new _(this),this.classes=new U(this),this.data=new P(this),this.containsElement=e=>e.closest(this.controllerSelector)===this.element,this.schema=e,this.element=t,this.identifier=s,this.guide=new R(r),this.outlets=new q(this.documentScope,t)}findElement(e){return this.element.matches(e)?this.element:this.queryElements(e).find(this.containsElement)}findAllElements(e){return[...this.element.matches(e)?[this.element]:[],...this.queryElements(e).filter(this.containsElement)]}queryElements(e){return Array.from(this.element.querySelectorAll(e))}get controllerSelector(){return z(this.schema.controllerAttribute,this.identifier)}get isDocumentScope(){return this.element===document.documentElement}get documentScope(){return this.isDocumentScope?this:new W(this.schema,document.documentElement,this.identifier,this.guide.logger)}}class J{constructor(e,t,s){this.element=e,this.schema=t,this.delegate=s,this.valueListObserver=new F(this.element,this.controllerAttribute,this),this.scopesByIdentifierByElement=new WeakMap,this.scopeReferenceCounts=new WeakMap}start(){this.valueListObserver.start()}stop(){this.valueListObserver.stop()}get controllerAttribute(){return this.schema.controllerAttribute}parseValueForToken(e){const{element:t,content:s}=e;return this.parseValueForElementAndIdentifier(t,s)}parseValueForElementAndIdentifier(e,t){const s=this.fetchScopesByIdentifierForElement(e);let r=s.get(t);return r||(r=this.delegate.createScopeForElementAndIdentifier(e,t),s.set(t,r)),r}elementMatchedValue(e,t){const s=(this.scopeReferenceCounts.get(t)||0)+1;this.scopeReferenceCounts.set(t,s),1==s&&this.delegate.scopeConnected(t)}elementUnmatchedValue(e,t){const s=this.scopeReferenceCounts.get(t);s&&(this.scopeReferenceCounts.set(t,s-1),1==s&&this.delegate.scopeDisconnected(t))}fetchScopesByIdentifierForElement(e){let t=this.scopesByIdentifierByElement.get(e);return t||(t=new Map,this.scopesByIdentifierByElement.set(e,t)),t}}class H{constructor(e){this.application=e,this.scopeObserver=new J(this.element,this.schema,this),this.scopesByIdentifier=new E,this.modulesByIdentifier=new Map}get element(){return this.application.element}get schema(){return this.application.schema}get logger(){return this.application.logger}get controllerAttribute(){return this.schema.controllerAttribute}get modules(){return Array.from(this.modulesByIdentifier.values())}get contexts(){return this.modules.reduce(((e,t)=>e.concat(t.contexts)),[])}start(){this.scopeObserver.start()}stop(){this.scopeObserver.stop()}loadDefinition(e){this.unloadIdentifier(e.identifier);const t=new j(this.application,e);this.connectModule(t);const s=e.controllerConstructor.afterLoad;s&&s.call(e.controllerConstructor,e.identifier,this.application)}unloadIdentifier(e){const t=this.modulesByIdentifier.get(e);t&&this.disconnectModule(t)}getContextForElementAndIdentifier(e,t){const s=this.modulesByIdentifier.get(t);if(s)return s.contexts.find((t=>t.element==e))}proposeToConnectScopeForElementAndIdentifier(e,t){const s=this.scopeObserver.parseValueForElementAndIdentifier(e,t);s?this.scopeObserver.elementMatchedValue(s.element,s):console.error(`Couldn't find or create scope for identifier: \"${t}\" and element:`,e)}handleError(e,t,s){this.application.handleError(e,t,s)}createScopeForElementAndIdentifier(e,t){return new W(this.schema,e,t,this.logger)}scopeConnected(e){this.scopesByIdentifier.add(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.connectContextForScope(e)}scopeDisconnected(e){this.scopesByIdentifier.delete(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.disconnectContextForScope(e)}connectModule(e){this.modulesByIdentifier.set(e.identifier,e);this.scopesByIdentifier.getValuesForKey(e.identifier).forEach((t=>e.connectContextForScope(t)))}disconnectModule(e){this.modulesByIdentifier.delete(e.identifier);this.scopesByIdentifier.getValuesForKey(e.identifier).forEach((t=>e.disconnectContextForScope(t)))}}const Z={controllerAttribute:\"data-controller\",actionAttribute:\"data-action\",targetAttribute:\"data-target\",targetAttributeForScope:e=>`data-${e}-target`,outletAttributeForScope:(e,t)=>`data-${e}-${t}-outlet`,keyMappings:Object.assign(Object.assign({enter:\"Enter\",tab:\"Tab\",esc:\"Escape\",space:\" \",up:\"ArrowUp\",down:\"ArrowDown\",left:\"ArrowLeft\",right:\"ArrowRight\",home:\"Home\",end:\"End\",page_up:\"PageUp\",page_down:\"PageDown\"},G(\"abcdefghijklmnopqrstuvwxyz\".split(\"\").map((e=>[e,e])))),G(\"0123456789\".split(\"\").map((e=>[e,e]))))};function G(e){return e.reduce(((e,[t,s])=>Object.assign(Object.assign({},e),{[t]:s})),{})}class Q{constructor(e=document.documentElement,r=Z){this.logger=console,this.debug=!1,this.logDebugActivity=(e,t,s={})=>{this.debug&&this.logFormattedMessage(e,t,s)},this.element=e,this.schema=r,this.dispatcher=new t(this),this.router=new H(this),this.actionDescriptorFilters=Object.assign({},s)}static start(e,t){const s=new this(e,t);return s.start(),s}async start(){await new Promise((e=>{\"loading\"==document.readyState?document.addEventListener(\"DOMContentLoaded\",(()=>e())):e()})),this.logDebugActivity(\"application\",\"starting\"),this.dispatcher.start(),this.router.start(),this.logDebugActivity(\"application\",\"start\")}stop(){this.logDebugActivity(\"application\",\"stopping\"),this.dispatcher.stop(),this.router.stop(),this.logDebugActivity(\"application\",\"stop\")}register(e,t){this.load({identifier:e,controllerConstructor:t})}registerActionOption(e,t){this.actionDescriptorFilters[e]=t}load(e,...t){(Array.isArray(e)?e:[e,...t]).forEach((e=>{e.controllerConstructor.shouldLoad&&this.router.loadDefinition(e)}))}unload(e,...t){(Array.isArray(e)?e:[e,...t]).forEach((e=>this.router.unloadIdentifier(e)))}get controllers(){return this.router.contexts.map((e=>e.controller))}getControllerForElementAndIdentifier(e,t){const s=this.router.getContextForElementAndIdentifier(e,t);return s?s.controller:null}handleError(e,t,s){var r;this.logger.error(\"%s\\n\\n%o\\n\\n%o\",t,e,s),null===(r=window.onerror)||void 0===r||r.call(window,t,\"\",0,0,e)}logFormattedMessage(e,t,s={}){s=Object.assign({application:this},s),this.logger.groupCollapsed(`${e} #${t}`),this.logger.log(\"details:\",Object.assign({},s)),this.logger.groupEnd()}}function X(e,t,s){return e.application.getControllerForElementAndIdentifier(t,s)}function Y(e,t,s){let r=X(e,t,s);return r||(e.application.router.proposeToConnectScopeForElementAndIdentifier(t,s),r=X(e,t,s),r||void 0)}function ee([e,t],s){return function(e){const{token:t,typeDefinition:s}=e,r=`${c(t)}-value`,n=function(e){const{controller:t,token:s,typeDefinition:r}=e,n=function(e){const{controller:t,token:s,typeObject:r}=e,n=l(r.type),i=l(r.default),o=n&&i,a=n&&!i,c=!n&&i,h=te(r.type),u=se(e.typeObject.default);if(a)return h;if(c)return u;if(h!==u){throw new Error(`The specified default value for the Stimulus Value \"${t?`${t}.${s}`:s}\" must match the defined type \"${h}\". The provided default value of \"${r.default}\" is of type \"${u}\".`)}if(o)return h}({controller:t,token:s,typeObject:r}),i=se(r),o=te(r),a=n||i||o;if(a)return a;throw new Error(`Unknown value type \"${t?`${t}.${r}`:s}\" for \"${s}\" value`)}(e);return{type:n,key:r,name:i(r),get defaultValue(){return function(e){const t=te(e);if(t)return re[t];const s=h(e,\"default\"),r=h(e,\"type\"),n=e;if(s)return n.default;if(r){const{type:e}=n,t=te(e);if(t)return re[t]}return e}(s)},get hasCustomDefaultValue(){return void 0!==se(s)},reader:ne[n],writer:ie[n]||ie.default}}({controller:s,token:e,typeDefinition:t})}function te(e){switch(e){case Array:return\"array\";case Boolean:return\"boolean\";case Number:return\"number\";case Object:return\"object\";case String:return\"string\"}}function se(e){switch(typeof e){case\"boolean\":return\"boolean\";case\"number\":return\"number\";case\"string\":return\"string\"}return Array.isArray(e)?\"array\":\"[object Object]\"===Object.prototype.toString.call(e)?\"object\":void 0}const re={get array(){return[]},boolean:!1,number:0,get object(){return{}},string:\"\"},ne={array(e){const t=JSON.parse(e);if(!Array.isArray(t))throw new TypeError(`expected value of type \"array\" but instead got value \"${e}\" of type \"${se(t)}\"`);return t},boolean:e=>!(\"0\"==e||\"false\"==String(e).toLowerCase()),number:e=>Number(e.replace(/_/g,\"\")),object(e){const t=JSON.parse(e);if(null===t||\"object\"!=typeof t||Array.isArray(t))throw new TypeError(`expected value of type \"object\" but instead got value \"${e}\" of type \"${se(t)}\"`);return t},string:e=>e},ie={default:function(e){return`${e}`},array:oe,object:oe};function oe(e){return JSON.stringify(e)}class ae{constructor(e){this.context=e}static get shouldLoad(){return!0}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:s={},prefix:r=this.identifier,bubbles:n=!0,cancelable:i=!0}={}){const o=new CustomEvent(r?`${r}:${e}`:e,{detail:s,bubbles:n,cancelable:i});return t.dispatchEvent(o),o}}ae.blessings=[function(e){return T(e,\"classes\").reduce(((e,t)=>{return Object.assign(e,{[`${s=t}Class`]:{get(){const{classes:e}=this;if(e.has(s))return e.get(s);{const t=e.getAttributeName(s);throw new Error(`Missing attribute \"${t}\"`)}}},[`${s}Classes`]:{get(){return this.classes.getAll(s)}},[`has${a(s)}Class`]:{get(){return this.classes.has(s)}}});var s}),{})},function(e){return T(e,\"targets\").reduce(((e,t)=>{return Object.assign(e,{[`${s=t}Target`]:{get(){const e=this.targets.find(s);if(e)return e;throw new Error(`Missing target element \"${s}\" for \"${this.identifier}\" controller`)}},[`${s}Targets`]:{get(){return this.targets.findAll(s)}},[`has${a(s)}Target`]:{get(){return this.targets.has(s)}}});var s}),{})},function(e){const t=S(e,\"values\"),s={valueDescriptorMap:{get(){return t.reduce(((e,t)=>{const s=ee(t,this.identifier),r=this.data.getAttributeNameForKey(s.key);return Object.assign(e,{[r]:s})}),{})}}};return t.reduce(((e,t)=>Object.assign(e,function(e,t){const s=ee(e,t),{key:r,name:n,reader:i,writer:o}=s;return{[n]:{get(){const e=this.data.get(r);return null!==e?i(e):s.defaultValue},set(e){void 0===e?this.data.delete(r):this.data.set(r,o(e))}},[`has${a(n)}`]:{get(){return this.data.has(r)||s.hasCustomDefaultValue}}}}(t))),s)},function(e){return T(e,\"outlets\").reduce(((e,t)=>Object.assign(e,function(e){const t=o(e);return{[`${t}Outlet`]:{get(){const t=this.outlets.find(e),s=this.outlets.getSelectorForOutletName(e);if(t){const s=Y(this,t,e);if(s)return s;throw new Error(`The provided outlet element is missing an outlet controller \"${e}\" instance for host controller \"${this.identifier}\"`)}throw new Error(`Missing outlet element \"${e}\" for host controller \"${this.identifier}\". Stimulus couldn't find a matching outlet element using selector \"${s}\".`)}},[`${t}Outlets`]:{get(){const t=this.outlets.findAll(e);return t.length>0?t.map((t=>{const s=Y(this,t,e);if(s)return s;console.warn(`The provided outlet element is missing an outlet controller \"${e}\" instance for host controller \"${this.identifier}\"`,t)})).filter((e=>e)):[]}},[`${t}OutletElement`]:{get(){const t=this.outlets.find(e),s=this.outlets.getSelectorForOutletName(e);if(t)return t;throw new Error(`Missing outlet element \"${e}\" for host controller \"${this.identifier}\". Stimulus couldn't find a matching outlet element using selector \"${s}\".`)}},[`${t}OutletElements`]:{get(){return this.outlets.findAll(e)}},[`has${a(t)}Outlet`]:{get(){return this.outlets.has(e)}}}}(t))),{})}],ae.targets=[],ae.outlets=[],ae.values={};export{Q as Application,b as AttributeObserver,L as Context,ae as Controller,f as ElementObserver,w as IndexedMultimap,E as Multimap,M as SelectorObserver,k as StringMapObserver,N as TokenListObserver,F as ValueListObserver,v as add,Z as defaultSchema,y as del,O as fetch,A as prune};\n//# sourceMappingURL=stimulus.min.js.map\n"},{"path":"static/assets/tailwind.css","content":"/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */\n@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-green-50:oklch(98.2% .018 155.826);--color-green-500:oklch(72.3% .219 149.579);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-700:oklch(37.3% .034 259.733);--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.fixed{position:fixed}.static{position:static}.sticky{position:sticky}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.my-5{margin-block:calc(var(--spacing) * 5)}.my-8{margin-block:calc(var(--spacing) * 8)}.my-10{margin-block:calc(var(--spacing) * 10)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-28{margin-top:calc(var(--spacing) * 28)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-6{margin-left:calc(var(--spacing) * 6)}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.w-full{width:100%}.min-w-full{min-width:100%}.flex-grow{flex-grow:1}.cursor-pointer{cursor:pointer}.list-disc{list-style-type:disc}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.rounded{border-radius:.25rem}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-gray-400{border-color:var(--color-gray-400)}.border-red-400{border-color:var(--color-red-400)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-600{background-color:var(--color-red-600)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\\.5{padding-block:calc(var(--spacing) * 2.5)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-blue-600{color:var(--color-blue-600)}.text-gray-500{color:var(--color-gray-500)}.text-gray-700{color:var(--color-gray-700)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}@media (hover:hover){.hover\\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\\:underline:hover{text-decoration-line:underline}}.focus\\:outline-blue-600:focus{outline-color:var(--color-blue-600)}.focus\\:outline-red-600:focus{outline-color:var(--color-red-600)}@media (min-width:40rem){.sm\\:mt-0{margin-top:calc(var(--spacing) * 0)}.sm\\:ml-2{margin-left:calc(var(--spacing) * 2)}.sm\\:inline-block{display:inline-block}.sm\\:w-auto{width:auto}.sm\\:flex-row{flex-direction:row}.sm\\:pb-0{padding-bottom:calc(var(--spacing) * 0)}}@media (min-width:48rem){.md\\:w-2\\/3{width:66.6667%}}}@property --tw-space-y-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-border-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:\"*\";inherits:false}@property --tw-brightness{syntax:\"*\";inherits:false}@property --tw-contrast{syntax:\"*\";inherits:false}@property --tw-grayscale{syntax:\"*\";inherits:false}@property --tw-hue-rotate{syntax:\"*\";inherits:false}@property --tw-invert{syntax:\"*\";inherits:false}@property --tw-opacity{syntax:\"*\";inherits:false}@property --tw-saturate{syntax:\"*\";inherits:false}@property --tw-sepia{syntax:\"*\";inherits:false}@property --tw-drop-shadow{syntax:\"*\";inherits:false}@property --tw-drop-shadow-color{syntax:\"*\";inherits:false}@property --tw-drop-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:\"*\";inherits:false}"},{"path":"static/assets/turbo.min.js","content":"/*!\nTurbo 8.0.23\nCopyright © 2026 37signals LLC\n */\nconst e={eager:\"eager\",lazy:\"lazy\"};class t extends HTMLElement{static delegateConstructor=void 0;loaded=Promise.resolve();static get observedAttributes(){return[\"disabled\",\"loading\",\"src\"]}constructor(){super(),this.delegate=new t.delegateConstructor(this)}connectedCallback(){this.delegate.connect()}disconnectedCallback(){this.delegate.disconnect()}reload(){return this.delegate.sourceURLReloaded()}attributeChangedCallback(e){\"loading\"==e?this.delegate.loadingStyleChanged():\"src\"==e?this.delegate.sourceURLChanged():\"disabled\"==e&&this.delegate.disabledChanged()}get src(){return this.getAttribute(\"src\")}set src(e){e?this.setAttribute(\"src\",e):this.removeAttribute(\"src\")}get refresh(){return this.getAttribute(\"refresh\")}set refresh(e){e?this.setAttribute(\"refresh\",e):this.removeAttribute(\"refresh\")}get shouldReloadWithMorph(){return this.src&&\"morph\"===this.refresh}get loading(){return function(t){if(\"lazy\"===t.toLowerCase())return e.lazy;return e.eager}(this.getAttribute(\"loading\")||\"\")}set loading(e){e?this.setAttribute(\"loading\",e):this.removeAttribute(\"loading\")}get disabled(){return this.hasAttribute(\"disabled\")}set disabled(e){e?this.setAttribute(\"disabled\",\"\"):this.removeAttribute(\"disabled\")}get autoscroll(){return this.hasAttribute(\"autoscroll\")}set autoscroll(e){e?this.setAttribute(\"autoscroll\",\"\"):this.removeAttribute(\"autoscroll\")}get complete(){return!this.delegate.isLoading}get isActive(){return this.ownerDocument===document&&!this.isPreview}get isPreview(){return this.ownerDocument?.documentElement?.hasAttribute(\"data-turbo-preview\")}}const s={enabled:!0,progressBarDelay:500,unvisitableExtensions:new Set([\".7z\",\".aac\",\".apk\",\".avi\",\".bmp\",\".bz2\",\".css\",\".csv\",\".deb\",\".dmg\",\".doc\",\".docx\",\".exe\",\".gif\",\".gz\",\".heic\",\".heif\",\".ico\",\".iso\",\".jpeg\",\".jpg\",\".js\",\".json\",\".m4a\",\".mkv\",\".mov\",\".mp3\",\".mp4\",\".mpeg\",\".mpg\",\".msi\",\".ogg\",\".ogv\",\".pdf\",\".pkg\",\".png\",\".ppt\",\".pptx\",\".rar\",\".rtf\",\".svg\",\".tar\",\".tif\",\".tiff\",\".txt\",\".wav\",\".webm\",\".webp\",\".wma\",\".wmv\",\".xls\",\".xlsx\",\".xml\",\".zip\"])};function r(e){if(\"false\"==e.getAttribute(\"data-turbo-eval\"))return e;{const t=document.createElement(\"script\"),s=w();return s&&(t.nonce=s),t.textContent=e.textContent,t.async=!1,function(e,t){for(const{name:s,value:r}of t.attributes)e.setAttribute(s,r)}(t,e),t}}function i(e,{target:t,cancelable:s,detail:r}={}){const i=new CustomEvent(e,{cancelable:s,bubbles:!0,composed:!0,detail:r});return t&&t.isConnected?t.dispatchEvent(i):document.documentElement.dispatchEvent(i),i}function n(e){e.preventDefault(),e.stopImmediatePropagation()}function o(){return\"hidden\"===document.visibilityState?c():a()}function a(){return new Promise((e=>requestAnimationFrame((()=>e()))))}function c(){return new Promise((e=>setTimeout((()=>e()),0)))}function l(e=\"\"){return(new DOMParser).parseFromString(e,\"text/html\")}function h(e,...t){const s=function(e,t){return e.reduce(((e,s,r)=>e+s+(null==t[r]?\"\":t[r])),\"\")}(e,t).replace(/^\\n/,\"\").split(\"\\n\"),r=s[0].match(/^\\s+/),i=r?r[0].length:0;return s.map((e=>e.slice(i))).join(\"\\n\")}function d(){return Array.from({length:36}).map(((e,t)=>8==t||13==t||18==t||23==t?\"-\":14==t?\"4\":19==t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(16*Math.random()).toString(16))).join(\"\")}function u(e,...t){for(const s of t.map((t=>t?.getAttribute(e))))if(\"string\"==typeof s)return s;return null}function m(...e){for(const t of e)\"turbo-frame\"==t.localName&&t.setAttribute(\"busy\",\"\"),t.setAttribute(\"aria-busy\",\"true\")}function p(...e){for(const t of e)\"turbo-frame\"==t.localName&&t.removeAttribute(\"busy\"),t.removeAttribute(\"aria-busy\")}function f(e,t=2e3){return new Promise((s=>{const r=()=>{e.removeEventListener(\"error\",r),e.removeEventListener(\"load\",r),s()};e.addEventListener(\"load\",r,{once:!0}),e.addEventListener(\"error\",r,{once:!0}),setTimeout(s,t)}))}function g(e){switch(e){case\"replace\":return history.replaceState;case\"advance\":case\"restore\":return history.pushState}}function b(...e){const t=u(\"data-turbo-action\",...e);return function(e){return\"advance\"==e||\"replace\"==e||\"restore\"==e}(t)?t:null}function v(e){return document.querySelector(`meta[name=\"${e}\"]`)}function S(e){const t=v(e);return t&&t.content}function w(){const e=v(\"csp-nonce\");if(e){const{nonce:t,content:s}=e;return\"\"==t?s:t}}function y(e,t){if(e instanceof Element)return e.closest(t)||y(e.assignedSlot||e.getRootNode()?.host,t)}function E(e){return!!e&&null==e.closest(\"[inert], :disabled, [hidden], details:not([open]), dialog:not([open])\")&&\"function\"==typeof e.focus}function A(e){return Array.from(e.querySelectorAll(\"[autofocus]\")).find(E)}function R(e){if(\"_blank\"===e)return!1;if(e){for(const t of document.getElementsByName(e))if(t instanceof HTMLIFrameElement)return!1;return!0}return!0}function T(e){const t=y(e,\"a[href], a[xlink\\\\:href]\");if(!t)return null;if(t.href.startsWith(\"#\"))return null;if(t.hasAttribute(\"download\"))return null;const s=t.getAttribute(\"target\");return s&&\"_self\"!==s?null:t}const L={\"aria-disabled\":{beforeSubmit:e=>{e.setAttribute(\"aria-disabled\",\"true\"),e.addEventListener(\"click\",n)},afterSubmit:e=>{e.removeAttribute(\"aria-disabled\"),e.removeEventListener(\"click\",n)}},disabled:{beforeSubmit:e=>e.disabled=!0,afterSubmit:e=>e.disabled=!1}};const C=new class{#e=null;constructor(e){Object.assign(this,e)}get submitter(){return this.#e}set submitter(e){this.#e=L[e]||e}}({mode:\"on\",submitter:\"disabled\"}),P={drive:s,forms:C};function k(e){return new URL(e.toString(),document.baseURI)}function M(e){let t;return e.hash?e.hash.slice(1):(t=e.href.match(/#(.*)$/))?t[1]:void 0}function F(e,t){return k(t?.getAttribute(\"formaction\")||e.getAttribute(\"action\")||e.action)}function I(e){return(function(e){return function(e){return e.pathname.split(\"/\").slice(1)}(e).slice(-1)[0]}(e).match(/\\.[^.]*$/)||[])[0]||\"\"}function q(e,t){return function(e,t){const s=O(t.origin+t.pathname);return O(e.href)===s||e.href.startsWith(s)}(e,t)&&!P.drive.unvisitableExtensions.has(I(e))}function H(e){return k(e.getAttribute(\"href\")||\"\")}function B(e){return function(e){const t=M(e);return null!=t?e.href.slice(0,-(t.length+1)):e.href}(e)}function N(e,t){return k(e).href==k(t).href}function O(e){return e.endsWith(\"/\")?e:e+\"/\"}class D{constructor(e){this.response=e}get succeeded(){return this.response.ok}get failed(){return!this.succeeded}get clientError(){return this.statusCode>=400&&this.statusCode<=499}get serverError(){return this.statusCode>=500&&this.statusCode<=599}get redirected(){return this.response.redirected}get location(){return k(this.response.url)}get isHTML(){return this.contentType&&this.contentType.match(/^(?:text\\/([^\\s;,]+\\b)?html|application\\/xhtml\\+xml)\\b/)}get statusCode(){return this.response.status}get contentType(){return this.header(\"Content-Type\")}get responseText(){return this.response.clone().text()}get responseHTML(){return this.isHTML?this.response.clone().text():Promise.resolve(void 0)}header(e){return this.response.headers.get(e)}}class x extends Set{constructor(e){super(),this.maxSize=e}add(e){if(this.size>=this.maxSize){const e=this.values().next().value;this.delete(e)}super.add(e)}}const V=new x(20);function W(e,t={}){const s=new Headers(t.headers||{}),r=d();return V.add(r),s.append(\"X-Turbo-Request-Id\",r),window.fetch(e,{...t,headers:s})}function U(e){switch(e.toLowerCase()){case\"get\":return _.get;case\"post\":return _.post;case\"put\":return _.put;case\"patch\":return _.patch;case\"delete\":return _.delete}}const _={get:\"get\",post:\"post\",put:\"put\",patch:\"patch\",delete:\"delete\"};function $(e){switch(e.toLowerCase()){case j.multipart:return j.multipart;case j.plain:return j.plain;default:return j.urlEncoded}}const j={urlEncoded:\"application/x-www-form-urlencoded\",multipart:\"multipart/form-data\",plain:\"text/plain\"};class z{abortController=new AbortController;#t=e=>{};constructor(e,t,s,r=new URLSearchParams,i=null,n=j.urlEncoded){const[o,a]=K(k(s),t,r,n);this.delegate=e,this.url=o,this.target=i,this.fetchOptions={credentials:\"same-origin\",redirect:\"follow\",method:t.toUpperCase(),headers:{...this.defaultHeaders},body:a,signal:this.abortSignal,referrer:this.delegate.referrer?.href},this.enctype=n}get method(){return this.fetchOptions.method}set method(e){const t=this.isSafe?this.url.searchParams:this.fetchOptions.body||new FormData,s=U(e)||_.get;this.url.search=\"\";const[r,i]=K(this.url,s,t,this.enctype);this.url=r,this.fetchOptions.body=i,this.fetchOptions.method=s.toUpperCase()}get headers(){return this.fetchOptions.headers}set headers(e){this.fetchOptions.headers=e}get body(){return this.isSafe?this.url.searchParams:this.fetchOptions.body}set body(e){this.fetchOptions.body=e}get location(){return this.url}get params(){return this.url.searchParams}get entries(){return this.body?Array.from(this.body.entries()):[]}cancel(){this.abortController.abort()}async perform(){const{fetchOptions:e}=this;this.delegate.prepareRequest(this);const t=await this.#s(e);try{this.delegate.requestStarted(this),t.detail.fetchRequest?this.response=t.detail.fetchRequest.response:this.response=W(this.url.href,e);const s=await this.response;return await this.receive(s)}catch(e){if(\"AbortError\"!==e.name)throw this.#r(e)&&this.delegate.requestErrored(this,e),e}finally{this.delegate.requestFinished(this)}}async receive(e){const t=new D(e);return i(\"turbo:before-fetch-response\",{cancelable:!0,detail:{fetchResponse:t},target:this.target}).defaultPrevented?this.delegate.requestPreventedHandlingResponse(this,t):t.succeeded?this.delegate.requestSucceededWithResponse(this,t):this.delegate.requestFailedWithResponse(this,t),t}get defaultHeaders(){return{Accept:\"text/html, application/xhtml+xml\"}}get isSafe(){return G(this.method)}get abortSignal(){return this.abortController.signal}acceptResponseType(e){this.headers.Accept=[e,this.headers.Accept].join(\", \")}async#s(e){const t=new Promise((e=>this.#t=e)),s=i(\"turbo:before-fetch-request\",{cancelable:!0,detail:{fetchOptions:e,url:this.url,resume:this.#t},target:this.target});return this.url=s.detail.url,s.defaultPrevented&&await t,s}#r(e){return!i(\"turbo:fetch-request-error\",{target:this.target,cancelable:!0,detail:{request:this,error:e}}).defaultPrevented}}function G(e){return U(e)==_.get}function K(e,t,s,r){const i=Array.from(s).length>0?new URLSearchParams(J(s)):e.searchParams;return G(t)?[X(e,i),null]:r==j.urlEncoded?[e,i]:[e,s]}function J(e){const t=[];for(const[s,r]of e)r instanceof File||t.push([s,r]);return t}function X(e,t){const s=new URLSearchParams(J(t));return e.search=s.toString(),e}class Y{started=!1;constructor(e,t){this.delegate=e,this.element=t,this.intersectionObserver=new IntersectionObserver(this.intersect)}start(){this.started||(this.started=!0,this.intersectionObserver.observe(this.element))}stop(){this.started&&(this.started=!1,this.intersectionObserver.unobserve(this.element))}intersect=e=>{const t=e.slice(-1)[0];t?.isIntersecting&&this.delegate.elementAppearedInViewport(this.element)}}class Q{static contentType=\"text/vnd.turbo-stream.html\";static wrap(e){return\"string\"==typeof e?new this(function(e){const t=document.createElement(\"template\");return t.innerHTML=e,t.content}(e)):e}constructor(e){this.fragment=function(e){for(const t of e.querySelectorAll(\"turbo-stream\")){const e=document.importNode(t,!0);for(const t of e.templateElement.content.querySelectorAll(\"script\"))t.replaceWith(r(t));t.replaceWith(e)}return e}(e)}}const Z=e=>e;class ee{keys=[];entries={};#i;constructor(e,t=Z){this.size=e,this.#i=t}has(e){return this.#i(e)in this.entries}get(e){if(this.has(e)){const t=this.read(e);return this.touch(e),t}}put(e,t){return this.write(e,t),this.touch(e),t}clear(){for(const e of Object.keys(this.entries))this.evict(e)}read(e){return this.entries[this.#i(e)]}write(e,t){this.entries[this.#i(e)]=t}touch(e){e=this.#i(e);const t=this.keys.indexOf(e);t>-1&&this.keys.splice(t,1),this.keys.unshift(e),this.trim()}trim(){for(const e of this.keys.splice(this.size))this.evict(e)}evict(e){delete this.entries[e]}}const te=1e4,se=new class extends ee{#n=null;#o={};constructor(e=1,t=100){super(e,B),this.prefetchDelay=t}putLater(e,t,s){this.#n=setTimeout((()=>{t.perform(),this.put(e,t,s),this.#n=null}),this.prefetchDelay)}put(e,t,s=te){super.put(e,t),this.#o[B(e)]=new Date((new Date).getTime()+s)}clear(){super.clear(),this.#n&&clearTimeout(this.#n)}evict(e){super.evict(e),delete this.#o[e]}has(e){if(super.has(e)){const t=this.#o[B(e)];return t&&t>Date.now()}return!1}},re={initialized:\"initialized\",requesting:\"requesting\",waiting:\"waiting\",receiving:\"receiving\",stopping:\"stopping\",stopped:\"stopped\"};class ie{state=re.initialized;static confirmMethod(e){return Promise.resolve(confirm(e))}constructor(e,t,s,r=!1){const i=function(e,t){const s=t?.getAttribute(\"formmethod\")||e.getAttribute(\"method\")||\"\";return U(s.toLowerCase())||_.get}(t,s),n=function(e,t){const s=k(e);G(t)&&(s.search=\"\");return s}(function(e,t){const s=\"string\"==typeof e.action?e.action:null;return t?.hasAttribute(\"formaction\")?t.getAttribute(\"formaction\")||\"\":e.getAttribute(\"action\")||s||\"\"}(t,s),i),o=function(e,t){const s=new FormData(e),r=t?.getAttribute(\"name\"),i=t?.getAttribute(\"value\");r&&s.append(r,i||\"\");return s}(t,s),a=function(e,t){return $(t?.getAttribute(\"formenctype\")||e.enctype)}(t,s);this.delegate=e,this.formElement=t,this.submitter=s,this.fetchRequest=new z(this,i,n,o,t,a),this.mustRedirect=r}get method(){return this.fetchRequest.method}set method(e){this.fetchRequest.method=e}get action(){return this.fetchRequest.url.toString()}set action(e){this.fetchRequest.url=k(e)}get body(){return this.fetchRequest.body}get enctype(){return this.fetchRequest.enctype}get isSafe(){return this.fetchRequest.isSafe}get location(){return this.fetchRequest.url}async start(){const{initialized:e,requesting:t}=re,s=u(\"data-turbo-confirm\",this.submitter,this.formElement);if(\"string\"==typeof s){const e=\"function\"==typeof P.forms.confirm?P.forms.confirm:ie.confirmMethod;if(!await e(s,this.formElement,this.submitter))return}if(this.state==e)return this.state=t,this.fetchRequest.perform()}stop(){const{stopping:e,stopped:t}=re;if(this.state!=e&&this.state!=t)return this.state=e,this.fetchRequest.cancel(),!0}prepareRequest(e){if(!e.isSafe){const t=function(e){if(null!=e){const t=(document.cookie?document.cookie.split(\"; \"):[]).find((t=>t.startsWith(e)));if(t){const e=t.split(\"=\").slice(1).join(\"=\");return e?decodeURIComponent(e):void 0}}}(S(\"csrf-param\"))||S(\"csrf-token\");t&&(e.headers[\"X-CSRF-Token\"]=t)}this.requestAcceptsTurboStreamResponse(e)&&e.acceptResponseType(Q.contentType)}requestStarted(e){this.state=re.waiting,this.submitter&&P.forms.submitter.beforeSubmit(this.submitter),this.setSubmitsWith(),m(this.formElement),i(\"turbo:submit-start\",{target:this.formElement,detail:{formSubmission:this}}),this.delegate.formSubmissionStarted(this)}requestPreventedHandlingResponse(e,t){se.clear(),this.result={success:t.succeeded,fetchResponse:t}}requestSucceededWithResponse(e,t){if(t.clientError||t.serverError)this.delegate.formSubmissionFailedWithResponse(this,t);else if(se.clear(),this.requestMustRedirect(e)&&function(e){return 200==e.statusCode&&!e.redirected}(t)){const e=new Error(\"Form responses must redirect to another location\");this.delegate.formSubmissionErrored(this,e)}else this.state=re.receiving,this.result={success:!0,fetchResponse:t},this.delegate.formSubmissionSucceededWithResponse(this,t)}requestFailedWithResponse(e,t){this.result={success:!1,fetchResponse:t},this.delegate.formSubmissionFailedWithResponse(this,t)}requestErrored(e,t){this.result={success:!1,error:t},this.delegate.formSubmissionErrored(this,t)}requestFinished(e){this.state=re.stopped,this.submitter&&P.forms.submitter.afterSubmit(this.submitter),this.resetSubmitterText(),p(this.formElement),i(\"turbo:submit-end\",{target:this.formElement,detail:{formSubmission:this,...this.result}}),this.delegate.formSubmissionFinished(this)}setSubmitsWith(){if(this.submitter&&this.submitsWith)if(this.submitter.matches(\"button\"))this.originalSubmitText=this.submitter.innerHTML,this.submitter.innerHTML=this.submitsWith;else if(this.submitter.matches(\"input\")){const e=this.submitter;this.originalSubmitText=e.value,e.value=this.submitsWith}}resetSubmitterText(){if(this.submitter&&this.originalSubmitText)if(this.submitter.matches(\"button\"))this.submitter.innerHTML=this.originalSubmitText;else if(this.submitter.matches(\"input\")){this.submitter.value=this.originalSubmitText}}requestMustRedirect(e){return!e.isSafe&&this.mustRedirect}requestAcceptsTurboStreamResponse(e){return!e.isSafe||function(e,...t){return t.some((t=>t&&t.hasAttribute(e)))}(\"data-turbo-stream\",this.submitter,this.formElement)}get submitsWith(){return this.submitter?.getAttribute(\"data-turbo-submits-with\")}}class ne{constructor(e){this.element=e}get activeElement(){return this.element.ownerDocument.activeElement}get children(){return[...this.element.children]}hasAnchor(e){return null!=this.getElementForAnchor(e)}getElementForAnchor(e){return e?this.element.querySelector(`[id='${e}'], a[name='${e}']`):null}get isConnected(){return this.element.isConnected}get firstAutofocusableElement(){return A(this.element)}get permanentElements(){return ae(this.element)}getPermanentElementById(e){return oe(this.element,e)}getPermanentElementMapForSnapshot(e){const t={};for(const s of this.permanentElements){const{id:r}=s,i=e.getPermanentElementById(r);i&&(t[r]=[s,i])}return t}}function oe(e,t){return e.querySelector(`#${t}[data-turbo-permanent]`)}function ae(e){return e.querySelectorAll(\"[id][data-turbo-permanent]\")}class ce{started=!1;constructor(e,t){this.delegate=e,this.eventTarget=t}start(){this.started||(this.eventTarget.addEventListener(\"submit\",this.submitCaptured,!0),this.started=!0)}stop(){this.started&&(this.eventTarget.removeEventListener(\"submit\",this.submitCaptured,!0),this.started=!1)}submitCaptured=()=>{this.eventTarget.removeEventListener(\"submit\",this.submitBubbled,!1),this.eventTarget.addEventListener(\"submit\",this.submitBubbled,!1)};submitBubbled=e=>{if(!e.defaultPrevented){const t=e.target instanceof HTMLFormElement?e.target:void 0,s=e.submitter||void 0;t&&function(e,t){const s=t?.getAttribute(\"formmethod\")||e.getAttribute(\"method\");return\"dialog\"!=s}(t,s)&&function(e,t){const s=t?.getAttribute(\"formtarget\")||e.getAttribute(\"target\");return R(s)}(t,s)&&this.delegate.willSubmitForm(t,s)&&(e.preventDefault(),e.stopImmediatePropagation(),this.delegate.formSubmitted(t,s))}}}class le{#a=e=>{};#c=e=>{};constructor(e,t){this.delegate=e,this.element=t}scrollToAnchor(e){const t=this.snapshot.getElementForAnchor(e);t?(this.focusElement(t),this.scrollToElement(t)):this.scrollToPosition({x:0,y:0})}scrollToAnchorFromLocation(e){this.scrollToAnchor(M(e))}scrollToElement(e){e.scrollIntoView()}focusElement(e){e instanceof HTMLElement&&(e.hasAttribute(\"tabindex\")?e.focus():(e.setAttribute(\"tabindex\",\"-1\"),e.focus(),e.removeAttribute(\"tabindex\")))}scrollToPosition({x:e,y:t}){this.scrollRoot.scrollTo(e,t)}scrollToTop(){this.scrollToPosition({x:0,y:0})}get scrollRoot(){return window}async render(e){const{isPreview:t,shouldRender:s,willRender:r,newSnapshot:i}=e,n=r;if(s)try{this.renderPromise=new Promise((e=>this.#a=e)),this.renderer=e,await this.prepareToRenderSnapshot(e);const s=new Promise((e=>this.#c=e)),r={resume:this.#c,render:this.renderer.renderElement,renderMethod:this.renderer.renderMethod};this.delegate.allowsImmediateRender(i,r)||await s,await this.renderSnapshot(e),this.delegate.viewRenderedSnapshot(i,t,this.renderer.renderMethod),this.delegate.preloadOnLoadLinksForView(this.element),this.finishRenderingSnapshot(e)}finally{delete this.renderer,this.#a(void 0),delete this.renderPromise}else n&&this.invalidate(e.reloadReason)}invalidate(e){this.delegate.viewInvalidated(e)}async prepareToRenderSnapshot(e){this.markAsPreview(e.isPreview),await e.prepareToRender()}markAsPreview(e){e?this.element.setAttribute(\"data-turbo-preview\",\"\"):this.element.removeAttribute(\"data-turbo-preview\")}markVisitDirection(e){this.element.setAttribute(\"data-turbo-visit-direction\",e)}unmarkVisitDirection(){this.element.removeAttribute(\"data-turbo-visit-direction\")}async renderSnapshot(e){await e.render()}finishRenderingSnapshot(e){e.finishRendering()}}class he extends le{missing(){this.element.innerHTML='<strong class=\"turbo-frame-error\">Content missing</strong>'}get snapshot(){return new ne(this.element)}}class de{constructor(e,t){this.delegate=e,this.element=t}start(){this.element.addEventListener(\"click\",this.clickBubbled),document.addEventListener(\"turbo:click\",this.linkClicked),document.addEventListener(\"turbo:before-visit\",this.willVisit)}stop(){this.element.removeEventListener(\"click\",this.clickBubbled),document.removeEventListener(\"turbo:click\",this.linkClicked),document.removeEventListener(\"turbo:before-visit\",this.willVisit)}clickBubbled=e=>{this.clickEventIsSignificant(e)?this.clickEvent=e:delete this.clickEvent};linkClicked=e=>{this.clickEvent&&this.clickEventIsSignificant(e)&&this.delegate.shouldInterceptLinkClick(e.target,e.detail.url,e.detail.originalEvent)&&(this.clickEvent.preventDefault(),e.preventDefault(),this.delegate.linkClickIntercepted(e.target,e.detail.url,e.detail.originalEvent)),delete this.clickEvent};willVisit=e=>{delete this.clickEvent};clickEventIsSignificant(e){const t=e.composed?e.target?.parentElement:e.target,s=T(t)||t;return s instanceof Element&&s.closest(\"turbo-frame, html\")==this.element}}class ue{started=!1;constructor(e,t){this.delegate=e,this.eventTarget=t}start(){this.started||(this.eventTarget.addEventListener(\"click\",this.clickCaptured,!0),this.started=!0)}stop(){this.started&&(this.eventTarget.removeEventListener(\"click\",this.clickCaptured,!0),this.started=!1)}clickCaptured=()=>{this.eventTarget.removeEventListener(\"click\",this.clickBubbled,!1),this.eventTarget.addEventListener(\"click\",this.clickBubbled,!1)};clickBubbled=e=>{if(e instanceof MouseEvent&&this.clickEventIsSignificant(e)){const t=T(e.composedPath&&e.composedPath()[0]||e.target);if(t&&R(t.target)){const s=H(t);this.delegate.willFollowLinkToLocation(t,s,e)&&(e.preventDefault(),this.delegate.followedLinkToLocation(t,s))}}};clickEventIsSignificant(e){return!(e.target&&e.target.isContentEditable||e.defaultPrevented||e.which>1||e.altKey||e.ctrlKey||e.metaKey||e.shiftKey)}}class me{constructor(e,t){this.delegate=e,this.linkInterceptor=new ue(this,t)}start(){this.linkInterceptor.start()}stop(){this.linkInterceptor.stop()}canPrefetchRequestToLocation(e,t){return!1}prefetchAndCacheRequestToLocation(e,t){}willFollowLinkToLocation(e,t,s){return this.delegate.willSubmitFormLinkToLocation(e,t,s)&&(e.hasAttribute(\"data-turbo-method\")||e.hasAttribute(\"data-turbo-stream\"))}followedLinkToLocation(e,t){const s=document.createElement(\"form\");for(const[e,r]of t.searchParams)s.append(Object.assign(document.createElement(\"input\"),{type:\"hidden\",name:e,value:r}));const r=Object.assign(t,{search:\"\"});s.setAttribute(\"data-turbo\",\"true\"),s.setAttribute(\"action\",r.href),s.setAttribute(\"hidden\",\"\");const i=e.getAttribute(\"data-turbo-method\");i&&s.setAttribute(\"method\",i);const n=e.getAttribute(\"data-turbo-frame\");n&&s.setAttribute(\"data-turbo-frame\",n);const o=b(e);o&&s.setAttribute(\"data-turbo-action\",o);const a=e.getAttribute(\"data-turbo-confirm\");a&&s.setAttribute(\"data-turbo-confirm\",a);e.hasAttribute(\"data-turbo-stream\")&&s.setAttribute(\"data-turbo-stream\",\"\"),this.delegate.submittedFormLinkToLocation(e,t,s),document.body.appendChild(s),s.addEventListener(\"turbo:submit-end\",(()=>s.remove()),{once:!0}),requestAnimationFrame((()=>s.requestSubmit()))}}class pe{static async preservingPermanentElements(e,t,s){const r=new this(e,t);r.enter(),await s(),r.leave()}constructor(e,t){this.delegate=e,this.permanentElementMap=t}enter(){for(const e in this.permanentElementMap){const[t,s]=this.permanentElementMap[e];this.delegate.enteringBardo(t,s),this.replaceNewPermanentElementWithPlaceholder(s)}}leave(){for(const e in this.permanentElementMap){const[t]=this.permanentElementMap[e];this.replaceCurrentPermanentElementWithClone(t),this.replacePlaceholderWithPermanentElement(t),this.delegate.leavingBardo(t)}}replaceNewPermanentElementWithPlaceholder(e){const t=function(e){const t=document.createElement(\"meta\");return t.setAttribute(\"name\",\"turbo-permanent-placeholder\"),t.setAttribute(\"content\",e.id),t}(e);e.replaceWith(t)}replaceCurrentPermanentElementWithClone(e){const t=e.cloneNode(!0);e.replaceWith(t)}replacePlaceholderWithPermanentElement(e){const t=this.getPlaceholderById(e.id);t?.replaceWith(e)}getPlaceholderById(e){return this.placeholders.find((t=>t.content==e))}get placeholders(){return[...document.querySelectorAll(\"meta[name=turbo-permanent-placeholder][content]\")]}}class fe{#l=null;static renderElement(e,t){}constructor(e,t,s,r=!0){this.currentSnapshot=e,this.newSnapshot=t,this.isPreview=s,this.willRender=r,this.renderElement=this.constructor.renderElement,this.promise=new Promise(((e,t)=>this.resolvingFunctions={resolve:e,reject:t}))}get shouldRender(){return!0}get shouldAutofocus(){return!0}get reloadReason(){}prepareToRender(){}render(){}finishRendering(){this.resolvingFunctions&&(this.resolvingFunctions.resolve(),delete this.resolvingFunctions)}async preservingPermanentElements(e){await pe.preservingPermanentElements(this,this.permanentElementMap,e)}focusFirstAutofocusableElement(){if(this.shouldAutofocus){const e=this.connectedSnapshot.firstAutofocusableElement;e&&e.focus()}}enteringBardo(e){this.#l||e.contains(this.currentSnapshot.activeElement)&&(this.#l=this.currentSnapshot.activeElement)}leavingBardo(e){e.contains(this.#l)&&this.#l instanceof HTMLElement&&(this.#l.focus(),this.#l=null)}get connectedSnapshot(){return this.newSnapshot.isConnected?this.newSnapshot:this.currentSnapshot}get currentElement(){return this.currentSnapshot.element}get newElement(){return this.newSnapshot.element}get permanentElementMap(){return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot)}get renderMethod(){return\"replace\"}}class ge extends fe{static renderElement(e,t){const s=document.createRange();s.selectNodeContents(e),s.deleteContents();const r=t,i=r.ownerDocument?.createRange();i&&(i.selectNodeContents(r),e.appendChild(i.extractContents()))}constructor(e,t,s,r,i,n=!0){super(t,s,r,i,n),this.delegate=e}get shouldRender(){return!0}async render(){await o(),this.preservingPermanentElements((()=>{this.loadFrameElement()})),this.scrollFrameIntoView(),await o(),this.focusFirstAutofocusableElement(),await o(),this.activateScriptElements()}loadFrameElement(){this.delegate.willRenderFrame(this.currentElement,this.newElement),this.renderElement(this.currentElement,this.newElement)}scrollFrameIntoView(){if(this.currentElement.autoscroll||this.newElement.autoscroll){const s=this.currentElement.firstElementChild,r=(e=this.currentElement.getAttribute(\"data-autoscroll-block\"),t=\"end\",\"end\"==e||\"start\"==e||\"center\"==e||\"nearest\"==e?e:t),i=function(e,t){return\"auto\"==e||\"smooth\"==e?e:t}(this.currentElement.getAttribute(\"data-autoscroll-behavior\"),\"auto\");if(s)return s.scrollIntoView({block:r,behavior:i}),!0}var e,t;return!1}activateScriptElements(){for(const e of this.newScriptElements){const t=r(e);e.replaceWith(t)}}get newScriptElements(){return this.currentElement.querySelectorAll(\"script\")}}var be=function(){const e=()=>{},t={morphStyle:\"outerHTML\",callbacks:{beforeNodeAdded:e,afterNodeAdded:e,beforeNodeMorphed:e,afterNodeMorphed:e,beforeNodeRemoved:e,afterNodeRemoved:e,beforeAttributeUpdated:e},head:{style:\"merge\",shouldPreserve:e=>\"true\"===e.getAttribute(\"im-preserve\"),shouldReAppend:e=>\"true\"===e.getAttribute(\"im-re-append\"),shouldRemove:e,afterHeadMorphed:e},restoreFocus:!0};const s=function(){function e(e,t,s,i){if(!1===i.callbacks.beforeNodeAdded(t))return null;if(i.idMap.has(t)){const n=document.createElement(t.tagName);return e.insertBefore(n,s),r(n,t,i),i.callbacks.afterNodeAdded(n),n}{const r=document.importNode(t,!0);return e.insertBefore(r,s),i.callbacks.afterNodeAdded(r),r}}const t=function(){function e(e,t,s){let r=e.idMap.get(t),i=e.idMap.get(s);if(!i||!r)return!1;for(const e of r)if(i.has(e))return!0;return!1}function t(e,t){const s=e,r=t;return s.nodeType===r.nodeType&&s.tagName===r.tagName&&(!s.getAttribute?.(\"id\")||s.getAttribute?.(\"id\")===r.getAttribute?.(\"id\"))}return function(s,r,i,n){let o=null,a=r.nextSibling,c=0,l=i;for(;l&&l!=n;){if(t(l,r)){if(e(s,l,r))return l;null===o&&(s.idMap.has(l)||(o=l))}if(null===o&&a&&t(l,a)&&(c++,a=a.nextSibling,c>=2&&(o=void 0)),s.activeElementAndParents.includes(l))break;l=l.nextSibling}return o||null}}();function s(e,t){if(e.idMap.has(t))o(e.pantry,t,null);else{if(!1===e.callbacks.beforeNodeRemoved(t))return;t.parentNode?.removeChild(t),e.callbacks.afterNodeRemoved(t)}}function i(e,t,r){let i=t;for(;i&&i!==r;){let t=i;i=i.nextSibling,s(e,t)}return i}function n(e,t,s,r){const i=r.target.getAttribute?.(\"id\")===t&&r.target||r.target.querySelector(`[id=\"${t}\"]`)||r.pantry.querySelector(`[id=\"${t}\"]`);return function(e,t){const s=e.getAttribute(\"id\");for(;e=e.parentNode;){let r=t.idMap.get(e);r&&(r.delete(s),r.size||t.idMap.delete(e))}}(i,r),o(e,i,s),i}function o(e,t,s){if(e.moveBefore)try{e.moveBefore(t,s)}catch(r){e.insertBefore(t,s)}else e.insertBefore(t,s)}return function(o,a,c,l=null,h=null){a instanceof HTMLTemplateElement&&c instanceof HTMLTemplateElement&&(a=a.content,c=c.content),l||=a.firstChild;for(const s of c.childNodes){if(l&&l!=h){const e=t(o,s,l,h);if(e){e!==l&&i(o,l,e),r(e,s,o),l=e.nextSibling;continue}}if(s instanceof Element){const e=s.getAttribute(\"id\");if(o.persistentIds.has(e)){const t=n(a,e,l,o);r(t,s,o),l=t.nextSibling;continue}}const c=e(a,s,l,o);c&&(l=c.nextSibling)}for(;l&&l!=h;){const e=l;l=l.nextSibling,s(o,e)}}}(),r=function(){function e(e,s,r,i){const n=s[r];if(n!==e[r]){const o=t(r,e,\"update\",i);o||(e[r]=s[r]),n?o||e.setAttribute(r,\"\"):t(r,e,\"remove\",i)||e.removeAttribute(r)}}function t(e,t,s,r){return!(\"value\"!==e||!r.ignoreActiveValue||t!==document.activeElement)||!1===r.callbacks.beforeAttributeUpdated(e,t,s)}function r(e,t){return!!t.ignoreActiveValue&&e===document.activeElement&&e!==document.body}return function(n,o,a){return a.ignoreActive&&n===document.activeElement?null:(!1===a.callbacks.beforeNodeMorphed(n,o)||(n instanceof HTMLHeadElement&&a.head.ignore||(n instanceof HTMLHeadElement&&\"morph\"!==a.head.style?i(n,o,a):(!function(s,i,n){let o=i.nodeType;if(1===o){const o=s,a=i,c=o.attributes,l=a.attributes;for(const e of l)t(e.name,o,\"update\",n)||o.getAttribute(e.name)!==e.value&&o.setAttribute(e.name,e.value);for(let e=c.length-1;0<=e;e--){const s=c[e];if(s&&!a.hasAttribute(s.name)){if(t(s.name,o,\"remove\",n))continue;o.removeAttribute(s.name)}}r(o,n)||function(s,r,i){if(s instanceof HTMLInputElement&&r instanceof HTMLInputElement&&\"file\"!==r.type){let n=r.value,o=s.value;e(s,r,\"checked\",i),e(s,r,\"disabled\",i),r.hasAttribute(\"value\")?o!==n&&(t(\"value\",s,\"update\",i)||(s.setAttribute(\"value\",n),s.value=n)):t(\"value\",s,\"remove\",i)||(s.value=\"\",s.removeAttribute(\"value\"))}else if(s instanceof HTMLOptionElement&&r instanceof HTMLOptionElement)e(s,r,\"selected\",i);else if(s instanceof HTMLTextAreaElement&&r instanceof HTMLTextAreaElement){let e=r.value,n=s.value;if(t(\"value\",s,\"update\",i))return;e!==n&&(s.value=e),s.firstChild&&s.firstChild.nodeValue!==e&&(s.firstChild.nodeValue=e)}}(o,a,n)}8!==o&&3!==o||s.nodeValue!==i.nodeValue&&(s.nodeValue=i.nodeValue)}(n,o,a),r(n,a)||s(a,n,o))),a.callbacks.afterNodeMorphed(n,o)),n)}}();function i(e,t,s){let r=[],i=[],n=[],o=[],a=new Map;for(const e of t.children)a.set(e.outerHTML,e);for(const t of e.children){let e=a.has(t.outerHTML),r=s.head.shouldReAppend(t),c=s.head.shouldPreserve(t);e||c?r?i.push(t):(a.delete(t.outerHTML),n.push(t)):\"append\"===s.head.style?r&&(i.push(t),o.push(t)):!1!==s.head.shouldRemove(t)&&i.push(t)}o.push(...a.values());let c=[];for(const t of o){let i=document.createRange().createContextualFragment(t.outerHTML).firstChild;if(!1!==s.callbacks.beforeNodeAdded(i)){if(\"href\"in i&&i.href||\"src\"in i&&i.src){let e,t=new Promise((function(t){e=t}));i.addEventListener(\"load\",(function(){e()})),c.push(t)}e.appendChild(i),s.callbacks.afterNodeAdded(i),r.push(i)}}for(const t of i)!1!==s.callbacks.beforeNodeRemoved(t)&&(e.removeChild(t),s.callbacks.afterNodeRemoved(t));return s.head.afterHeadMorphed(e,{added:r,kept:n,removed:i}),c}const n=function(){function e(){const e=document.createElement(\"div\");return e.hidden=!0,document.body.insertAdjacentElement(\"afterend\",e),e}function s(e){let t=[],s=document.activeElement;if(\"BODY\"!==s?.tagName&&e.contains(s))for(;s&&(t.push(s),s!==e);)s=s.parentElement;return t}function r(e){let t=Array.from(e.querySelectorAll(\"[id]\"));return e.getAttribute?.(\"id\")&&t.push(e),t}function i(e,t,s,r){for(const i of r){const r=i.getAttribute(\"id\");if(t.has(r)){let t=i;for(;t;){let i=e.get(t);if(null==i&&(i=new Set,e.set(t,i)),i.add(r),t===s)break;t=t.parentElement}}}}return function(n,o,a){const{persistentIds:c,idMap:l}=function(e,t){const s=r(e),n=r(t),o=function(e,t){let s=new Set,r=new Map;for(const{id:t,tagName:i}of e)r.has(t)?s.add(t):r.set(t,i);let i=new Set;for(const{id:e,tagName:n}of t)i.has(e)?s.add(e):r.get(e)===n&&i.add(e);for(const e of s)i.delete(e);return i}(s,n);let a=new Map;i(a,o,e,s);const c=t.__idiomorphRoot||t;return i(a,o,c,n),{persistentIds:o,idMap:a}}(n,o),h=function(e){let s=Object.assign({},t);return Object.assign(s,e),s.callbacks=Object.assign({},t.callbacks,e.callbacks),s.head=Object.assign({},t.head,e.head),s}(a),d=h.morphStyle||\"outerHTML\";if(![\"innerHTML\",\"outerHTML\"].includes(d))throw`Do not understand how to morph style ${d}`;return{target:n,newContent:o,config:h,morphStyle:d,ignoreActive:h.ignoreActive,ignoreActiveValue:h.ignoreActiveValue,restoreFocus:h.restoreFocus,idMap:l,persistentIds:c,pantry:e(),activeElementAndParents:s(n),callbacks:h.callbacks,head:h.head}}}(),{normalizeElement:o,normalizeParent:a}=function(){const e=new WeakSet;class t{constructor(e){this.originalNode=e,this.realParentNode=e.parentNode,this.previousSibling=e.previousSibling,this.nextSibling=e.nextSibling}get childNodes(){const e=[];let t=this.previousSibling?this.previousSibling.nextSibling:this.realParentNode.firstChild;for(;t&&t!=this.nextSibling;)e.push(t),t=t.nextSibling;return e}querySelectorAll(e){return this.childNodes.reduce(((t,s)=>{if(s instanceof Element){s.matches(e)&&t.push(s);const r=s.querySelectorAll(e);for(let e=0;e<r.length;e++)t.push(r[e])}return t}),[])}insertBefore(e,t){return this.realParentNode.insertBefore(e,t)}moveBefore(e,t){return this.realParentNode.moveBefore(e,t)}get __idiomorphRoot(){return this.originalNode}}return{normalizeElement:function(e){return e instanceof Document?e.documentElement:e},normalizeParent:function s(r){if(null==r)return document.createElement(\"div\");if(\"string\"==typeof r)return s(function(t){let s=new DOMParser,r=t.replace(/<svg(\\s[^>]*>|>)([\\s\\S]*?)<\\/svg>/gim,\"\");if(r.match(/<\\/html>/)||r.match(/<\\/head>/)||r.match(/<\\/body>/)){let i=s.parseFromString(t,\"text/html\");if(r.match(/<\\/html>/))return e.add(i),i;{let t=i.firstChild;return t&&e.add(t),t}}{let r=s.parseFromString(\"<body><template>\"+t+\"</template></body>\",\"text/html\").body.querySelector(\"template\").content;return e.add(r),r}}(r));if(e.has(r))return r;if(r instanceof Node){if(r.parentNode)return new t(r);{const e=document.createElement(\"div\");return e.append(r),e}}{const e=document.createElement(\"div\");for(const t of[...r])e.append(t);return e}}}}();return{morph:function(e,t,r={}){e=o(e);const c=a(t),l=n(e,c,r),h=function(e,t){if(!e.config.restoreFocus)return t();let s=document.activeElement;if(!(s instanceof HTMLInputElement||s instanceof HTMLTextAreaElement))return t();const{id:r,selectionStart:i,selectionEnd:n}=s,o=t();r&&r!==document.activeElement?.getAttribute(\"id\")&&(s=e.target.querySelector(`[id=\"${r}\"]`),s?.focus());s&&!s.selectionEnd&&n&&s.setSelectionRange(i,n);return o}(l,(()=>function(e,t,s,r){if(e.head.block){const n=t.querySelector(\"head\"),o=s.querySelector(\"head\");if(n&&o){const t=i(n,o,e);return Promise.all(t).then((()=>{const t=Object.assign(e,{head:{block:!1,ignore:!0}});return r(t)}))}}return r(e)}(l,e,c,(t=>\"innerHTML\"===t.morphStyle?(s(t,e,c),Array.from(e.childNodes)):function(e,t,r){const i=a(t);return s(e,i,r,t,t.nextSibling),Array.from(i.childNodes)}(t,e,c)))));return l.pantry.remove(),h},defaults:t}}();function ve(e,t,{callbacks:s,...r}={}){be.morph(e,t,{...r,callbacks:new Ee(s)})}function Se(e,t,s={}){ve(e,t.childNodes,{...s,morphStyle:\"innerHTML\"})}function we(e,s){return e instanceof t&&e.shouldReloadWithMorph&&(!s||function(e,t){return t instanceof Element&&\"TURBO-FRAME\"===t.nodeName&&e.id===t.id&&(!t.getAttribute(\"src\")||N(e.src,t.getAttribute(\"src\")))}(e,s))&&!e.closest(\"[data-turbo-permanent]\")}function ye(e){return e.parentElement.closest(\"turbo-frame[src][refresh=morph]\")}class Ee{#h;constructor({beforeNodeMorphed:e}={}){this.#h=e||(()=>!0)}beforeNodeAdded=e=>!(e.id&&e.hasAttribute(\"data-turbo-permanent\")&&document.getElementById(e.id));beforeNodeMorphed=(e,t)=>{if(e instanceof Element){if(!e.hasAttribute(\"data-turbo-permanent\")&&this.#h(e,t)){return!i(\"turbo:before-morph-element\",{cancelable:!0,target:e,detail:{currentElement:e,newElement:t}}).defaultPrevented}return!1}};beforeAttributeUpdated=(e,t,s)=>!i(\"turbo:before-morph-attribute\",{cancelable:!0,target:t,detail:{attributeName:e,mutationType:s}}).defaultPrevented;beforeNodeRemoved=e=>this.beforeNodeMorphed(e);afterNodeMorphed=(e,t)=>{e instanceof Element&&i(\"turbo:morph-element\",{target:e,detail:{currentElement:e,newElement:t}})}}class Ae extends ge{static renderElement(e,t){i(\"turbo:before-frame-morph\",{target:e,detail:{currentElement:e,newElement:t}}),Se(e,t,{callbacks:{beforeNodeMorphed:(t,s)=>!we(t,s)||ye(t)!==e||(t.reload(),!1)}})}async preservingPermanentElements(e){return await e()}}class Re{static animationDuration=300;static get defaultCSS(){return h`\n      .turbo-progress-bar {\n        position: fixed;\n        display: block;\n        top: 0;\n        left: 0;\n        height: 3px;\n        background: #0076ff;\n        z-index: 2147483647;\n        transition:\n          width ${Re.animationDuration}ms ease-out,\n          opacity ${Re.animationDuration/2}ms ${Re.animationDuration/2}ms ease-in;\n        transform: translate3d(0, 0, 0);\n      }\n    `}hiding=!1;value=0;visible=!1;constructor(){this.stylesheetElement=this.createStylesheetElement(),this.progressElement=this.createProgressElement(),this.installStylesheetElement(),this.setValue(0)}show(){this.visible||(this.visible=!0,this.installProgressElement(),this.startTrickling())}hide(){this.visible&&!this.hiding&&(this.hiding=!0,this.fadeProgressElement((()=>{this.uninstallProgressElement(),this.stopTrickling(),this.visible=!1,this.hiding=!1})))}setValue(e){this.value=e,this.refresh()}installStylesheetElement(){document.head.insertBefore(this.stylesheetElement,document.head.firstChild)}installProgressElement(){this.progressElement.style.width=\"0\",this.progressElement.style.opacity=\"1\",document.documentElement.insertBefore(this.progressElement,document.body),this.refresh()}fadeProgressElement(e){this.progressElement.style.opacity=\"0\",setTimeout(e,1.5*Re.animationDuration)}uninstallProgressElement(){this.progressElement.parentNode&&document.documentElement.removeChild(this.progressElement)}startTrickling(){this.trickleInterval||(this.trickleInterval=window.setInterval(this.trickle,Re.animationDuration))}stopTrickling(){window.clearInterval(this.trickleInterval),delete this.trickleInterval}trickle=()=>{this.setValue(this.value+Math.random()/100)};refresh(){requestAnimationFrame((()=>{this.progressElement.style.width=10+90*this.value+\"%\"}))}createStylesheetElement(){const e=document.createElement(\"style\");e.type=\"text/css\",e.textContent=Re.defaultCSS;const t=w();return t&&(e.nonce=t),e}createProgressElement(){const e=document.createElement(\"div\");return e.className=\"turbo-progress-bar\",e}}class Te extends ne{detailsByOuterHTML=this.children.filter((e=>!function(e){const t=e.localName;return\"noscript\"==t}(e))).map((e=>function(e){e.hasAttribute(\"nonce\")&&e.setAttribute(\"nonce\",\"\");return e}(e))).reduce(((e,t)=>{const{outerHTML:s}=t,r=s in e?e[s]:{type:Le(t),tracked:Ce(t),elements:[]};return{...e,[s]:{...r,elements:[...r.elements,t]}}}),{});get trackedElementSignature(){return Object.keys(this.detailsByOuterHTML).filter((e=>this.detailsByOuterHTML[e].tracked)).join(\"\")}getScriptElementsNotInSnapshot(e){return this.getElementsMatchingTypeNotInSnapshot(\"script\",e)}getStylesheetElementsNotInSnapshot(e){return this.getElementsMatchingTypeNotInSnapshot(\"stylesheet\",e)}getElementsMatchingTypeNotInSnapshot(e,t){return Object.keys(this.detailsByOuterHTML).filter((e=>!(e in t.detailsByOuterHTML))).map((e=>this.detailsByOuterHTML[e])).filter((({type:t})=>t==e)).map((({elements:[e]})=>e))}get provisionalElements(){return Object.keys(this.detailsByOuterHTML).reduce(((e,t)=>{const{type:s,tracked:r,elements:i}=this.detailsByOuterHTML[t];return null!=s||r?i.length>1?[...e,...i.slice(1)]:e:[...e,...i]}),[])}getMetaValue(e){const t=this.findMetaElementByName(e);return t?t.getAttribute(\"content\"):null}findMetaElementByName(e){return Object.keys(this.detailsByOuterHTML).reduce(((t,s)=>{const{elements:[r]}=this.detailsByOuterHTML[s];return function(e,t){const s=e.localName;return\"meta\"==s&&e.getAttribute(\"name\")==t}(r,e)?r:t}),0)}}function Le(e){return function(e){const t=e.localName;return\"script\"==t}(e)?\"script\":function(e){const t=e.localName;return\"style\"==t||\"link\"==t&&\"stylesheet\"==e.getAttribute(\"rel\")}(e)?\"stylesheet\":void 0}function Ce(e){return\"reload\"==e.getAttribute(\"data-turbo-track\")}class Pe extends ne{static fromHTMLString(e=\"\"){return this.fromDocument(l(e))}static fromElement(e){return this.fromDocument(e.ownerDocument)}static fromDocument({documentElement:e,body:t,head:s}){return new this(e,t,new Te(s))}constructor(e,t,s){super(t),this.documentElement=e,this.headSnapshot=s}clone(){const e=this.element.cloneNode(!0),t=this.element.querySelectorAll(\"select\"),s=e.querySelectorAll(\"select\");for(const[e,r]of t.entries()){const t=s[e];for(const e of t.selectedOptions)e.selected=!1;for(const e of r.selectedOptions)t.options[e.index].selected=!0}for(const t of e.querySelectorAll('input[type=\"password\"]'))t.value=\"\";for(const t of e.querySelectorAll(\"noscript\"))t.remove();return new Pe(this.documentElement,e,this.headSnapshot)}get lang(){return this.documentElement.getAttribute(\"lang\")}get dir(){return this.documentElement.getAttribute(\"dir\")}get headElement(){return this.headSnapshot.element}get rootLocation(){return k(this.getSetting(\"root\")??\"/\")}get cacheControlValue(){return this.getSetting(\"cache-control\")}get isPreviewable(){return\"no-preview\"!=this.cacheControlValue}get isCacheable(){return\"no-cache\"!=this.cacheControlValue}get isVisitable(){return\"reload\"!=this.getSetting(\"visit-control\")}get prefersViewTransitions(){return(\"true\"===this.getSetting(\"view-transition\")||\"same-origin\"===this.headSnapshot.getMetaValue(\"view-transition\"))&&!window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches}get refreshMethod(){return this.getSetting(\"refresh-method\")}get refreshScroll(){return this.getSetting(\"refresh-scroll\")}getSetting(e){return this.headSnapshot.getMetaValue(`turbo-${e}`)}}class ke{#d=!1;#u=Promise.resolve();renderChange(e,t){return e&&this.viewTransitionsAvailable&&!this.#d?(this.#d=!0,this.#u=this.#u.then((async()=>{await document.startViewTransition(t).finished}))):this.#u=this.#u.then(t),this.#u}get viewTransitionsAvailable(){return document.startViewTransition}}const Me={action:\"advance\",historyChanged:!1,visitCachedSnapshot:()=>{},willRender:!0,updateHistory:!0,shouldCacheSnapshot:!0,acceptsStreamResponse:!1,refresh:{}},Fe=\"visitStart\",Ie=\"requestStart\",qe=\"requestEnd\",He=\"visitEnd\",Be=\"initialized\",Ne=\"started\",Oe=\"canceled\",De=\"failed\",xe=\"completed\",Ve=0,We=-1,Ue=-2,_e={advance:\"forward\",restore:\"back\",replace:\"none\"};class $e{identifier=d();timingMetrics={};followedRedirect=!1;historyChanged=!1;scrolled=!1;shouldCacheSnapshot=!0;acceptsStreamResponse=!1;snapshotCached=!1;state=Be;viewTransitioner=new ke;constructor(e,t,s,r={}){this.delegate=e,this.location=t,this.restorationIdentifier=s||d();const{action:i,historyChanged:n,referrer:o,snapshot:a,snapshotHTML:c,response:l,visitCachedSnapshot:h,willRender:u,updateHistory:m,shouldCacheSnapshot:p,acceptsStreamResponse:f,direction:g,refresh:b}={...Me,...r};this.action=i,this.historyChanged=n,this.referrer=o,this.snapshot=a,this.snapshotHTML=c,this.response=l,this.isPageRefresh=this.view.isPageRefresh(this),this.visitCachedSnapshot=h,this.willRender=u,this.updateHistory=m,this.scrolled=!u,this.shouldCacheSnapshot=p,this.acceptsStreamResponse=f,this.direction=g||_e[i],this.refresh=b}get adapter(){return this.delegate.adapter}get view(){return this.delegate.view}get history(){return this.delegate.history}get restorationData(){return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)}start(){this.state==Be&&(this.recordTimingMetric(Fe),this.state=Ne,this.adapter.visitStarted(this),this.delegate.visitStarted(this))}cancel(){this.state==Ne&&(this.request&&this.request.cancel(),this.cancelRender(),this.state=Oe)}complete(){this.state==Ne&&(this.recordTimingMetric(He),this.adapter.visitCompleted(this),this.state=xe,this.followRedirect(),this.followedRedirect||this.delegate.visitCompleted(this))}fail(){this.state==Ne&&(this.state=De,this.adapter.visitFailed(this),this.delegate.visitCompleted(this))}changeHistory(){if(!this.historyChanged&&this.updateHistory){const e=g(this.location.href===this.referrer?.href?\"replace\":this.action);this.history.update(e,this.location,this.restorationIdentifier),this.historyChanged=!0}}issueRequest(){this.hasPreloadedResponse()?this.simulateRequest():this.shouldIssueRequest()&&!this.request&&(this.request=new z(this,_.get,this.location),this.request.perform())}simulateRequest(){this.response&&(this.startRequest(),this.recordResponse(),this.finishRequest())}startRequest(){this.recordTimingMetric(Ie),this.adapter.visitRequestStarted(this)}recordResponse(e=this.response){if(this.response=e,e){const{statusCode:t}=e;je(t)?this.adapter.visitRequestCompleted(this):this.adapter.visitRequestFailedWithStatusCode(this,t)}}finishRequest(){this.recordTimingMetric(qe),this.adapter.visitRequestFinished(this)}loadResponse(){if(this.response){const{statusCode:e,responseHTML:t}=this.response;this.render((async()=>{if(this.shouldCacheSnapshot&&this.cacheSnapshot(),this.view.renderPromise&&await this.view.renderPromise,je(e)&&null!=t){const e=Pe.fromHTMLString(t);await this.renderPageSnapshot(e,!1),this.adapter.visitRendered(this),this.complete()}else await this.view.renderError(Pe.fromHTMLString(t),this),this.adapter.visitRendered(this),this.fail()}))}}getCachedSnapshot(){const e=this.view.getCachedSnapshotForLocation(this.location)||this.getPreloadedSnapshot();if(e&&(!M(this.location)||e.hasAnchor(M(this.location)))&&(\"restore\"==this.action||e.isPreviewable))return e}getPreloadedSnapshot(){if(this.snapshotHTML)return Pe.fromHTMLString(this.snapshotHTML)}hasCachedSnapshot(){return null!=this.getCachedSnapshot()}loadCachedSnapshot(){const e=this.getCachedSnapshot();if(e){const t=this.shouldIssueRequest();this.render((async()=>{this.cacheSnapshot(),this.isPageRefresh?this.adapter.visitRendered(this):(this.view.renderPromise&&await this.view.renderPromise,await this.renderPageSnapshot(e,t),this.adapter.visitRendered(this),t||this.complete())}))}}followRedirect(){this.redirectedToLocation&&!this.followedRedirect&&this.response?.redirected&&(this.adapter.visitProposedToLocation(this.redirectedToLocation,{action:\"replace\",response:this.response,shouldCacheSnapshot:!1,willRender:!1}),this.followedRedirect=!0)}prepareRequest(e){this.acceptsStreamResponse&&e.acceptResponseType(Q.contentType)}requestStarted(){this.startRequest()}requestPreventedHandlingResponse(e,t){}async requestSucceededWithResponse(e,t){const s=await t.responseHTML,{redirected:r,statusCode:i}=t;null==s?this.recordResponse({statusCode:Ue,redirected:r}):(this.redirectedToLocation=t.redirected?t.location:void 0,this.recordResponse({statusCode:i,responseHTML:s,redirected:r}))}async requestFailedWithResponse(e,t){const s=await t.responseHTML,{redirected:r,statusCode:i}=t;null==s?this.recordResponse({statusCode:Ue,redirected:r}):this.recordResponse({statusCode:i,responseHTML:s,redirected:r})}requestErrored(e,t){this.recordResponse({statusCode:Ve,redirected:!1})}requestFinished(){this.finishRequest()}performScroll(){this.scrolled||this.view.forceReloaded||this.view.shouldPreserveScrollPosition(this)||(\"restore\"==this.action?this.scrollToRestoredPosition()||this.scrollToAnchor()||this.view.scrollToTop():this.scrollToAnchor()||this.view.scrollToTop(),this.scrolled=!0)}scrollToRestoredPosition(){const{scrollPosition:e}=this.restorationData;if(e)return this.view.scrollToPosition(e),!0}scrollToAnchor(){const e=M(this.location);if(null!=e)return this.view.scrollToAnchor(e),!0}recordTimingMetric(e){this.timingMetrics[e]=(new Date).getTime()}getTimingMetrics(){return{...this.timingMetrics}}hasPreloadedResponse(){return\"object\"==typeof this.response}shouldIssueRequest(){return\"restore\"==this.action?!this.hasCachedSnapshot():this.willRender}cacheSnapshot(){this.snapshotCached||(this.view.cacheSnapshot(this.snapshot).then((e=>e&&this.visitCachedSnapshot(e))),this.snapshotCached=!0)}async render(e){this.cancelRender(),await new Promise((e=>{this.frame=\"hidden\"===document.visibilityState?setTimeout((()=>e()),0):requestAnimationFrame((()=>e()))})),await e(),delete this.frame}async renderPageSnapshot(e,t){await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(e),(async()=>{await this.view.renderPage(e,t,this.willRender,this),this.performScroll()}))}cancelRender(){this.frame&&(cancelAnimationFrame(this.frame),delete this.frame)}}function je(e){return e>=200&&e<300}class ze{progressBar=new Re;constructor(e){this.session=e}visitProposedToLocation(e,t){q(e,this.navigator.rootLocation)?this.navigator.startVisit(e,t?.restorationIdentifier||d(),t):window.location.href=e.toString()}visitStarted(e){this.location=e.location,this.redirectedToLocation=null,e.loadCachedSnapshot(),e.issueRequest()}visitRequestStarted(e){this.progressBar.setValue(0),e.hasCachedSnapshot()||\"restore\"!=e.action?this.showVisitProgressBarAfterDelay():this.showProgressBar()}visitRequestCompleted(e){e.loadResponse(),e.response.redirected&&(this.redirectedToLocation=e.redirectedToLocation)}visitRequestFailedWithStatusCode(e,t){switch(t){case Ve:case We:case Ue:return this.reload({reason:\"request_failed\",context:{statusCode:t}});default:return e.loadResponse()}}visitRequestFinished(e){}visitCompleted(e){this.progressBar.setValue(1),this.hideVisitProgressBar()}pageInvalidated(e){this.reload(e)}visitFailed(e){this.progressBar.setValue(1),this.hideVisitProgressBar()}visitRendered(e){}linkPrefetchingIsEnabledForLocation(e){return!0}formSubmissionStarted(e){this.progressBar.setValue(0),this.showFormProgressBarAfterDelay()}formSubmissionFinished(e){this.progressBar.setValue(1),this.hideFormProgressBar()}showVisitProgressBarAfterDelay(){this.visitProgressBarTimeout=window.setTimeout(this.showProgressBar,this.session.progressBarDelay)}hideVisitProgressBar(){this.progressBar.hide(),null!=this.visitProgressBarTimeout&&(window.clearTimeout(this.visitProgressBarTimeout),delete this.visitProgressBarTimeout)}showFormProgressBarAfterDelay(){null==this.formProgressBarTimeout&&(this.formProgressBarTimeout=window.setTimeout(this.showProgressBar,this.session.progressBarDelay))}hideFormProgressBar(){this.progressBar.hide(),null!=this.formProgressBarTimeout&&(window.clearTimeout(this.formProgressBarTimeout),delete this.formProgressBarTimeout)}showProgressBar=()=>{this.progressBar.show()};reload(e){i(\"turbo:reload\",{detail:e}),window.location.href=(this.redirectedToLocation||this.location)?.toString()||window.location.href}get navigator(){return this.session.navigator}}class Ge{selector=\"[data-turbo-temporary]\";started=!1;start(){this.started||(this.started=!0,addEventListener(\"turbo:before-cache\",this.removeTemporaryElements,!1))}stop(){this.started&&(this.started=!1,removeEventListener(\"turbo:before-cache\",this.removeTemporaryElements,!1))}removeTemporaryElements=e=>{for(const e of this.temporaryElements)e.remove()};get temporaryElements(){return[...document.querySelectorAll(this.selector)]}}class Ke{constructor(e,t){this.session=e,this.element=t,this.linkInterceptor=new de(this,t),this.formSubmitObserver=new ce(this,t)}start(){this.linkInterceptor.start(),this.formSubmitObserver.start()}stop(){this.linkInterceptor.stop(),this.formSubmitObserver.stop()}shouldInterceptLinkClick(e,t,s){return this.#m(e)}linkClickIntercepted(e,t,s){const r=this.#p(e);r&&r.delegate.linkClickIntercepted(e,t,s)}willSubmitForm(e,t){return null==e.closest(\"turbo-frame\")&&this.#f(e,t)&&this.#m(e,t)}formSubmitted(e,t){const s=this.#p(e,t);s&&s.delegate.formSubmitted(e,t)}#f(e,t){const s=F(e,t),r=this.element.ownerDocument.querySelector('meta[name=\"turbo-root\"]'),i=k(r?.content??\"/\");return this.#m(e,t)&&q(s,i)}#m(e,t){if(e instanceof HTMLFormElement?this.session.submissionIsNavigatable(e,t):this.session.elementIsNavigatable(e)){const s=this.#p(e,t);return!!s&&s!=e.closest(\"turbo-frame\")}return!1}#p(e,s){const r=s?.getAttribute(\"data-turbo-frame\")||e.getAttribute(\"data-turbo-frame\");if(r&&\"_top\"!=r){const e=this.element.querySelector(`#${r}:not([disabled])`);if(e instanceof t)return e}}}class Je{location;restorationIdentifier=d();restorationData={};started=!1;currentIndex=0;constructor(e){this.delegate=e}start(){this.started||(addEventListener(\"popstate\",this.onPopState,!1),this.currentIndex=history.state?.turbo?.restorationIndex||0,this.started=!0,this.replace(new URL(window.location.href)))}stop(){this.started&&(removeEventListener(\"popstate\",this.onPopState,!1),this.started=!1)}push(e,t){this.update(history.pushState,e,t)}replace(e,t){this.update(history.replaceState,e,t)}update(e,t,s=d()){e===history.pushState&&++this.currentIndex;const r={turbo:{restorationIdentifier:s,restorationIndex:this.currentIndex}};e.call(history,r,\"\",t.href),this.location=t,this.restorationIdentifier=s}getRestorationDataForIdentifier(e){return this.restorationData[e]||{}}updateRestorationData(e){const{restorationIdentifier:t}=this,s=this.restorationData[t];this.restorationData[t]={...s,...e}}assumeControlOfScrollRestoration(){this.previousScrollRestoration||(this.previousScrollRestoration=history.scrollRestoration??\"auto\",history.scrollRestoration=\"manual\")}relinquishControlOfScrollRestoration(){this.previousScrollRestoration&&(history.scrollRestoration=this.previousScrollRestoration,delete this.previousScrollRestoration)}onPopState=e=>{const{turbo:t}=e.state||{};if(this.location=new URL(window.location.href),t){const{restorationIdentifier:e,restorationIndex:s}=t;this.restorationIdentifier=e;const r=s>this.currentIndex?\"forward\":\"back\";this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location,e,r),this.currentIndex=s}else this.currentIndex++,this.delegate.historyPoppedWithEmptyState(this.location)}}class Xe{started=!1;#g=null;constructor(e,t){this.delegate=e,this.eventTarget=t}start(){this.started||(\"loading\"===this.eventTarget.readyState?this.eventTarget.addEventListener(\"DOMContentLoaded\",this.#b,{once:!0}):this.#b())}stop(){this.started&&(this.eventTarget.removeEventListener(\"mouseenter\",this.#v,{capture:!0,passive:!0}),this.eventTarget.removeEventListener(\"mouseleave\",this.#S,{capture:!0,passive:!0}),this.eventTarget.removeEventListener(\"turbo:before-fetch-request\",this.#w,!0),this.started=!1)}#b=()=>{this.eventTarget.addEventListener(\"mouseenter\",this.#v,{capture:!0,passive:!0}),this.eventTarget.addEventListener(\"mouseleave\",this.#S,{capture:!0,passive:!0}),this.eventTarget.addEventListener(\"turbo:before-fetch-request\",this.#w,!0),this.started=!0};#v=e=>{if(\"false\"===S(\"turbo-prefetch\"))return;const t=e.target;if(t.matches&&t.matches(\"a[href]:not([target^=_]):not([download])\")&&this.#y(t)){const e=t,s=H(e);if(this.delegate.canPrefetchRequestToLocation(e,s)){this.#g=e;const r=new z(this,_.get,s,new URLSearchParams,t);r.fetchOptions.priority=\"low\",se.putLater(s,r,this.#E)}}};#S=e=>{e.target===this.#g&&this.#A()};#A=()=>{se.clear(),this.#g=null};#w=e=>{if(\"FORM\"!==e.target.tagName&&\"GET\"===e.detail.fetchOptions.method){const t=se.get(e.detail.url);t&&(e.detail.fetchRequest=t),se.clear()}};prepareRequest(e){const t=e.target;e.headers[\"X-Sec-Purpose\"]=\"prefetch\";const s=t.closest(\"turbo-frame\"),r=t.getAttribute(\"data-turbo-frame\")||s?.getAttribute(\"target\")||s?.id;r&&\"_top\"!==r&&(e.headers[\"Turbo-Frame\"]=r)}requestSucceededWithResponse(){}requestStarted(e){}requestErrored(e){}requestFinished(e){}requestPreventedHandlingResponse(e,t){}requestFailedWithResponse(e,t){}get#E(){return Number(S(\"turbo-prefetch-cache-time\"))||te}#y(e){return!!e.getAttribute(\"href\")&&(!Ye(e)&&(!Qe(e)&&(!Ze(e)&&(!et(e)&&!st(e)))))}}const Ye=e=>e.origin!==document.location.origin||![\"http:\",\"https:\"].includes(e.protocol)||e.hasAttribute(\"target\"),Qe=e=>e.pathname+e.search===document.location.pathname+document.location.search||e.href.startsWith(\"#\"),Ze=e=>{if(\"false\"===e.getAttribute(\"data-turbo-prefetch\"))return!0;if(\"false\"===e.getAttribute(\"data-turbo\"))return!0;const t=y(e,\"[data-turbo-prefetch]\");return!(!t||\"false\"!==t.getAttribute(\"data-turbo-prefetch\"))},et=e=>{const t=e.getAttribute(\"data-turbo-method\");return!(!t||\"get\"===t.toLowerCase())||(!!tt(e)||(!!e.hasAttribute(\"data-turbo-confirm\")||!!e.hasAttribute(\"data-turbo-stream\")))},tt=e=>e.hasAttribute(\"data-remote\")||e.hasAttribute(\"data-behavior\")||e.hasAttribute(\"data-confirm\")||e.hasAttribute(\"data-method\"),st=e=>i(\"turbo:before-prefetch\",{target:e,cancelable:!0}).defaultPrevented;class rt{constructor(e){this.delegate=e}proposeVisit(e,t={}){this.delegate.allowsVisitingLocationWithAction(e,t.action)&&this.delegate.visitProposedToLocation(e,t)}startVisit(e,t,s={}){this.stop(),this.currentVisit=new $e(this,k(e),t,{referrer:this.location,...s}),this.currentVisit.start()}submitForm(e,t){this.stop(),this.formSubmission=new ie(this,e,t,!0),this.formSubmission.start()}stop(){this.formSubmission&&(this.formSubmission.stop(),delete this.formSubmission),this.currentVisit&&(this.currentVisit.cancel(),delete this.currentVisit)}get adapter(){return this.delegate.adapter}get view(){return this.delegate.view}get rootLocation(){return this.view.snapshot.rootLocation}get history(){return this.delegate.history}formSubmissionStarted(e){\"function\"==typeof this.adapter.formSubmissionStarted&&this.adapter.formSubmissionStarted(e)}async formSubmissionSucceededWithResponse(e,t){if(e==this.formSubmission){const s=await t.responseHTML;if(s){const r=e.isSafe;r||this.view.clearSnapshotCache();const{statusCode:i,redirected:n}=t,o={action:this.#R(e,t),shouldCacheSnapshot:r,response:{statusCode:i,responseHTML:s,redirected:n}};this.proposeVisit(t.location,o)}}}async formSubmissionFailedWithResponse(e,t){const s=await t.responseHTML;if(s){const e=Pe.fromHTMLString(s);t.serverError?await this.view.renderError(e,this.currentVisit):await this.view.renderPage(e,!1,!0,this.currentVisit),\"preserve\"!==e.refreshScroll&&this.view.scrollToTop(),this.view.clearSnapshotCache()}}formSubmissionErrored(e,t){console.error(t)}formSubmissionFinished(e){\"function\"==typeof this.adapter.formSubmissionFinished&&this.adapter.formSubmissionFinished(e)}linkPrefetchingIsEnabledForLocation(e){return\"function\"!=typeof this.adapter.linkPrefetchingIsEnabledForLocation||this.adapter.linkPrefetchingIsEnabledForLocation(e)}visitStarted(e){this.delegate.visitStarted(e)}visitCompleted(e){this.delegate.visitCompleted(e),delete this.currentVisit}locationWithActionIsSamePage(e,t){return!1}get location(){return this.history.location}get restorationIdentifier(){return this.history.restorationIdentifier}#R(e,t){const{submitter:s,formElement:r}=e;return b(s,r)||this.#T(t)}#T(e){return e.redirected&&e.location.href===this.location?.href?\"replace\":\"advance\"}}const it=0,nt=1,ot=2,at=3;class ct{stage=it;started=!1;constructor(e){this.delegate=e}start(){this.started||(this.stage==it&&(this.stage=nt),document.addEventListener(\"readystatechange\",this.interpretReadyState,!1),addEventListener(\"pagehide\",this.pageWillUnload,!1),this.started=!0)}stop(){this.started&&(document.removeEventListener(\"readystatechange\",this.interpretReadyState,!1),removeEventListener(\"pagehide\",this.pageWillUnload,!1),this.started=!1)}interpretReadyState=()=>{const{readyState:e}=this;\"interactive\"==e?this.pageIsInteractive():\"complete\"==e&&this.pageIsComplete()};pageIsInteractive(){this.stage==nt&&(this.stage=ot,this.delegate.pageBecameInteractive())}pageIsComplete(){this.pageIsInteractive(),this.stage==ot&&(this.stage=at,this.delegate.pageLoaded())}pageWillUnload=()=>{this.delegate.pageWillUnload()};get readyState(){return document.readyState}}class lt{started=!1;constructor(e){this.delegate=e}start(){this.started||(addEventListener(\"scroll\",this.onScroll,!1),this.onScroll(),this.started=!0)}stop(){this.started&&(removeEventListener(\"scroll\",this.onScroll,!1),this.started=!1)}onScroll=()=>{this.updatePosition({x:window.pageXOffset,y:window.pageYOffset})};updatePosition(e){this.delegate.scrollPositionChanged(e)}}class ht{render({fragment:e}){pe.preservingPermanentElements(this,function(e){const t=ae(document.documentElement),s={};for(const r of t){const{id:t}=r;for(const i of e.querySelectorAll(\"turbo-stream\")){const e=oe(i.templateElement.content,t);e&&(s[t]=[r,e])}}return s}(e),(()=>{!async function(e,t){const s=`turbo-stream-autofocus-${d()}`,r=e.querySelectorAll(\"turbo-stream\"),i=function(e){for(const t of e){const e=A(t.templateElement.content);if(e)return e}return null}(r);let n=null;i&&(n=i.id?i.id:s,i.id=n);t(),await o();if((null==document.activeElement||document.activeElement==document.body)&&n){const e=document.getElementById(n);E(e)&&e.focus(),e&&e.id==s&&e.removeAttribute(\"id\")}}(e,(()=>{!async function(e){const[t,s]=await async function(e,t){const s=t();return e(),await a(),[s,t()]}(e,(()=>document.activeElement)),r=t&&t.id;if(r){const e=document.getElementById(r);E(e)&&e!=s&&e.focus()}}((()=>{document.documentElement.appendChild(e)}))}))}))}enteringBardo(e,t){t.replaceWith(e.cloneNode(!0))}leavingBardo(){}}class dt{sources=new Set;#L=!1;constructor(e){this.delegate=e}start(){this.#L||(this.#L=!0,addEventListener(\"turbo:before-fetch-response\",this.inspectFetchResponse,!1))}stop(){this.#L&&(this.#L=!1,removeEventListener(\"turbo:before-fetch-response\",this.inspectFetchResponse,!1))}connectStreamSource(e){this.streamSourceIsConnected(e)||(this.sources.add(e),e.addEventListener(\"message\",this.receiveMessageEvent,!1))}disconnectStreamSource(e){this.streamSourceIsConnected(e)&&(this.sources.delete(e),e.removeEventListener(\"message\",this.receiveMessageEvent,!1))}streamSourceIsConnected(e){return this.sources.has(e)}inspectFetchResponse=e=>{const t=function(e){const t=e.detail?.fetchResponse;if(t instanceof D)return t}(e);t&&function(e){const t=e.contentType??\"\";return t.startsWith(Q.contentType)}(t)&&(e.preventDefault(),this.receiveMessageResponse(t))};receiveMessageEvent=e=>{this.#L&&\"string\"==typeof e.data&&this.receiveMessageHTML(e.data)};async receiveMessageResponse(e){const t=await e.responseHTML;t&&this.receiveMessageHTML(t)}receiveMessageHTML(e){this.delegate.receivedMessageFromStream(Q.wrap(e))}}class ut extends fe{static renderElement(e,t){const{documentElement:s,body:r}=document;s.replaceChild(t,r)}async render(){this.replaceHeadAndBody(),this.activateScriptElements()}replaceHeadAndBody(){const{documentElement:e,head:t}=document;e.replaceChild(this.newHead,t),this.renderElement(this.currentElement,this.newElement)}activateScriptElements(){for(const e of this.scriptElements){const t=e.parentNode;if(t){const s=r(e);t.replaceChild(s,e)}}}get newHead(){return this.newSnapshot.headSnapshot.element}get scriptElements(){return document.documentElement.querySelectorAll(\"script\")}}class mt extends fe{static renderElement(e,t){document.body&&t instanceof HTMLBodyElement?document.body.replaceWith(t):document.documentElement.appendChild(t)}get shouldRender(){return this.newSnapshot.isVisitable&&this.trackedElementsAreIdentical}get reloadReason(){return this.newSnapshot.isVisitable?this.trackedElementsAreIdentical?void 0:{reason:\"tracked_element_mismatch\"}:{reason:\"turbo_visit_control_is_reload\"}}async prepareToRender(){this.#C(),await this.mergeHead()}async render(){this.willRender&&await this.replaceBody()}finishRendering(){super.finishRendering(),this.isPreview||this.focusFirstAutofocusableElement()}get currentHeadSnapshot(){return this.currentSnapshot.headSnapshot}get newHeadSnapshot(){return this.newSnapshot.headSnapshot}get newElement(){return this.newSnapshot.element}#C(){const{documentElement:e}=this.currentSnapshot,{dir:t,lang:s}=this.newSnapshot;s?e.setAttribute(\"lang\",s):e.removeAttribute(\"lang\"),t?e.setAttribute(\"dir\",t):e.removeAttribute(\"dir\")}async mergeHead(){const e=this.mergeProvisionalElements(),t=this.copyNewHeadStylesheetElements();this.copyNewHeadScriptElements(),await e,await t,this.willRender&&this.removeUnusedDynamicStylesheetElements()}async replaceBody(){await this.preservingPermanentElements((async()=>{this.activateNewBody(),await this.assignNewBody()}))}get trackedElementsAreIdentical(){return this.currentHeadSnapshot.trackedElementSignature==this.newHeadSnapshot.trackedElementSignature}async copyNewHeadStylesheetElements(){const e=[];for(const t of this.newHeadStylesheetElements)e.push(f(t)),document.head.appendChild(t);await Promise.all(e)}copyNewHeadScriptElements(){for(const e of this.newHeadScriptElements)document.head.appendChild(r(e))}removeUnusedDynamicStylesheetElements(){for(const e of this.unusedDynamicStylesheetElements)document.head.removeChild(e)}async mergeProvisionalElements(){const e=[...this.newHeadProvisionalElements];for(const t of this.currentHeadProvisionalElements)this.isCurrentElementInElementList(t,e)||document.head.removeChild(t);for(const t of e)document.head.appendChild(t)}isCurrentElementInElementList(e,t){for(const[s,r]of t.entries()){if(\"TITLE\"==e.tagName){if(\"TITLE\"!=r.tagName)continue;if(e.innerHTML==r.innerHTML)return t.splice(s,1),!0}if(r.isEqualNode(e))return t.splice(s,1),!0}return!1}removeCurrentHeadProvisionalElements(){for(const e of this.currentHeadProvisionalElements)document.head.removeChild(e)}copyNewHeadProvisionalElements(){for(const e of this.newHeadProvisionalElements)document.head.appendChild(e)}activateNewBody(){document.adoptNode(this.newElement),this.removeNoscriptElements(),this.activateNewBodyScriptElements()}removeNoscriptElements(){for(const e of this.newElement.querySelectorAll(\"noscript\"))e.remove()}activateNewBodyScriptElements(){for(const e of this.newBodyScriptElements){const t=r(e);e.replaceWith(t)}}async assignNewBody(){await this.renderElement(this.currentElement,this.newElement)}get unusedDynamicStylesheetElements(){return this.oldHeadStylesheetElements.filter((e=>\"dynamic\"===e.getAttribute(\"data-turbo-track\")))}get oldHeadStylesheetElements(){return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)}get newHeadStylesheetElements(){return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)}get newHeadScriptElements(){return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot)}get currentHeadProvisionalElements(){return this.currentHeadSnapshot.provisionalElements}get newHeadProvisionalElements(){return this.newHeadSnapshot.provisionalElements}get newBodyScriptElements(){return this.newElement.querySelectorAll(\"script\")}}class pt extends mt{static renderElement(e,t){ve(e,t,{callbacks:{beforeNodeMorphed:(e,t)=>!(we(e,t)&&!ye(e))||(e.reload(),!1)}}),i(\"turbo:morph\",{detail:{currentElement:e,newElement:t}})}async preservingPermanentElements(e){return await e()}get renderMethod(){return\"morph\"}get shouldAutofocus(){return!1}}class ft extends ee{constructor(e){super(e,B)}get snapshots(){return this.entries}}class gt extends le{snapshotCache=new ft(10);lastRenderedLocation=new URL(location.href);forceReloaded=!1;shouldTransitionTo(e){return this.snapshot.prefersViewTransitions&&e.prefersViewTransitions}renderPage(e,t=!1,s=!0,r){const i=new(this.isPageRefresh(r)&&\"morph\"===(r?.refresh?.method||this.snapshot.refreshMethod)?pt:mt)(this.snapshot,e,t,s);return i.shouldRender?r?.changeHistory():this.forceReloaded=!0,this.render(i)}renderError(e,t){t?.changeHistory();const s=new ut(this.snapshot,e,!1);return this.render(s)}clearSnapshotCache(){this.snapshotCache.clear()}async cacheSnapshot(e=this.snapshot){if(e.isCacheable){this.delegate.viewWillCacheSnapshot();const{lastRenderedLocation:t}=this;await c();const s=e.clone();return this.snapshotCache.put(t,s),s}}getCachedSnapshotForLocation(e){return this.snapshotCache.get(e)}isPageRefresh(e){return!e||this.lastRenderedLocation.pathname===e.location.pathname&&\"replace\"===e.action}shouldPreserveScrollPosition(e){return this.isPageRefresh(e)&&\"preserve\"===(e?.refresh?.scroll||this.snapshot.refreshScroll)}get snapshot(){return Pe.fromElement(this.element)}}class bt{selector=\"a[data-turbo-preload]\";constructor(e,t){this.delegate=e,this.snapshotCache=t}start(){\"loading\"===document.readyState?document.addEventListener(\"DOMContentLoaded\",this.#P):this.preloadOnLoadLinksForView(document.body)}stop(){document.removeEventListener(\"DOMContentLoaded\",this.#P)}preloadOnLoadLinksForView(e){for(const t of e.querySelectorAll(this.selector))this.delegate.shouldPreloadLink(t)&&this.preloadURL(t)}async preloadURL(e){const t=new URL(e.href);if(this.snapshotCache.has(t))return;const s=new z(this,_.get,t,new URLSearchParams,e);await s.perform()}prepareRequest(e){e.headers[\"X-Sec-Purpose\"]=\"prefetch\"}async requestSucceededWithResponse(e,t){try{const s=await t.responseHTML,r=Pe.fromHTMLString(s);this.snapshotCache.put(e.url,r)}catch(e){}}requestStarted(e){}requestErrored(e){}requestFinished(e){}requestPreventedHandlingResponse(e,t){}requestFailedWithResponse(e,t){}#P=()=>{this.preloadOnLoadLinksForView(document.body)}}class vt{constructor(e){this.session=e}clear(){this.session.clearCache()}resetCacheControl(){this.#k(\"\")}exemptPageFromCache(){this.#k(\"no-cache\")}exemptPageFromPreview(){this.#k(\"no-preview\")}#k(e){!function(e,t){let s=v(e);s||(s=document.createElement(\"meta\"),s.setAttribute(\"name\",e),document.head.appendChild(s)),s.setAttribute(\"content\",t)}(\"turbo-cache-control\",e)}}function St(e){Object.defineProperties(e,wt)}const wt={absoluteURL:{get(){return this.toString()}}},yt=new class{navigator=new rt(this);history=new Je(this);view=new gt(this,document.documentElement);adapter=new ze(this);pageObserver=new ct(this);cacheObserver=new Ge;linkPrefetchObserver=new Xe(this,document);linkClickObserver=new ue(this,window);formSubmitObserver=new ce(this,document);scrollObserver=new lt(this);streamObserver=new dt(this);formLinkClickObserver=new me(this,document.documentElement);frameRedirector=new Ke(this,document.documentElement);streamMessageRenderer=new ht;cache=new vt(this);enabled=!0;started=!1;#M=150;constructor(e){this.recentRequests=e,this.preloader=new bt(this,this.view.snapshotCache),this.debouncedRefresh=this.refresh,this.pageRefreshDebouncePeriod=this.pageRefreshDebouncePeriod}start(){this.started||(this.pageObserver.start(),this.cacheObserver.start(),this.linkPrefetchObserver.start(),this.formLinkClickObserver.start(),this.linkClickObserver.start(),this.formSubmitObserver.start(),this.scrollObserver.start(),this.streamObserver.start(),this.frameRedirector.start(),this.history.start(),this.preloader.start(),this.started=!0,this.enabled=!0)}disable(){this.enabled=!1}stop(){this.started&&(this.pageObserver.stop(),this.cacheObserver.stop(),this.linkPrefetchObserver.stop(),this.formLinkClickObserver.stop(),this.linkClickObserver.stop(),this.formSubmitObserver.stop(),this.scrollObserver.stop(),this.streamObserver.stop(),this.frameRedirector.stop(),this.history.stop(),this.preloader.stop(),this.started=!1)}registerAdapter(e){this.adapter=e}visit(e,s={}){const r=s.frame?document.getElementById(s.frame):null;if(r instanceof t){const t=s.action||b(r);r.delegate.proposeVisitIfNavigatedWithAction(r,t),r.src=e.toString()}else this.navigator.proposeVisit(k(e),s)}refresh(e,t={}){t=\"string\"==typeof t?{requestId:t}:t;const{method:s,requestId:r,scroll:i}=t,n=r&&this.recentRequests.has(r),o=e===document.baseURI;n||this.navigator.currentVisit||!o||this.visit(e,{action:\"replace\",shouldCacheSnapshot:!1,refresh:{method:s,scroll:i}})}connectStreamSource(e){this.streamObserver.connectStreamSource(e)}disconnectStreamSource(e){this.streamObserver.disconnectStreamSource(e)}renderStreamMessage(e){this.streamMessageRenderer.render(Q.wrap(e))}clearCache(){this.view.clearSnapshotCache()}setProgressBarDelay(e){console.warn(\"Please replace `session.setProgressBarDelay(delay)` with `session.progressBarDelay = delay`. The function is deprecated and will be removed in a future version of Turbo.`\"),this.progressBarDelay=e}set progressBarDelay(e){P.drive.progressBarDelay=e}get progressBarDelay(){return P.drive.progressBarDelay}set drive(e){P.drive.enabled=e}get drive(){return P.drive.enabled}set formMode(e){P.forms.mode=e}get formMode(){return P.forms.mode}get location(){return this.history.location}get restorationIdentifier(){return this.history.restorationIdentifier}get pageRefreshDebouncePeriod(){return this.#M}set pageRefreshDebouncePeriod(e){this.refresh=function(e,t){let s=null;return(...r)=>{clearTimeout(s),s=setTimeout((()=>e.apply(this,r)),t)}}(this.debouncedRefresh.bind(this),e),this.#M=e}shouldPreloadLink(e){const s=e.hasAttribute(\"data-turbo-method\"),r=e.hasAttribute(\"data-turbo-stream\"),i=e.getAttribute(\"data-turbo-frame\"),n=\"_top\"==i?null:document.getElementById(i)||y(e,\"turbo-frame:not([disabled])\");if(s||r||n instanceof t)return!1;{const t=new URL(e.href);return this.elementIsNavigatable(e)&&q(t,this.snapshot.rootLocation)}}historyPoppedToLocationWithRestorationIdentifierAndDirection(e,t,s){this.enabled?this.navigator.startVisit(e,t,{action:\"restore\",historyChanged:!0,direction:s}):this.adapter.pageInvalidated({reason:\"turbo_disabled\"})}historyPoppedWithEmptyState(e){this.history.replace(e),this.view.lastRenderedLocation=e,this.view.cacheSnapshot()}scrollPositionChanged(e){this.history.updateRestorationData({scrollPosition:e})}willSubmitFormLinkToLocation(e,t){return this.elementIsNavigatable(e)&&q(t,this.snapshot.rootLocation)}submittedFormLinkToLocation(){}canPrefetchRequestToLocation(e,t){return this.elementIsNavigatable(e)&&q(t,this.snapshot.rootLocation)&&this.navigator.linkPrefetchingIsEnabledForLocation(t)}willFollowLinkToLocation(e,t,s){return this.elementIsNavigatable(e)&&q(t,this.snapshot.rootLocation)&&this.applicationAllowsFollowingLinkToLocation(e,t,s)}followedLinkToLocation(e,t){const s=this.getActionForLink(e),r=e.hasAttribute(\"data-turbo-stream\");this.visit(t.href,{action:s,acceptsStreamResponse:r})}allowsVisitingLocationWithAction(e,t){return this.applicationAllowsVisitingLocation(e)}visitProposedToLocation(e,t){St(e),this.adapter.visitProposedToLocation(e,t)}visitStarted(e){e.acceptsStreamResponse||(m(document.documentElement),this.view.markVisitDirection(e.direction)),St(e.location),this.notifyApplicationAfterVisitingLocation(e.location,e.action)}visitCompleted(e){this.view.unmarkVisitDirection(),p(document.documentElement),this.notifyApplicationAfterPageLoad(e.getTimingMetrics())}willSubmitForm(e,t){const s=F(e,t);return this.submissionIsNavigatable(e,t)&&q(k(s),this.snapshot.rootLocation)}formSubmitted(e,t){this.navigator.submitForm(e,t)}pageBecameInteractive(){this.view.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()}pageLoaded(){this.history.assumeControlOfScrollRestoration()}pageWillUnload(){this.history.relinquishControlOfScrollRestoration()}receivedMessageFromStream(e){this.renderStreamMessage(e)}viewWillCacheSnapshot(){this.notifyApplicationBeforeCachingSnapshot()}allowsImmediateRender({element:e},t){const s=this.notifyApplicationBeforeRender(e,t),{defaultPrevented:r,detail:{render:i}}=s;return this.view.renderer&&i&&(this.view.renderer.renderElement=i),!r}viewRenderedSnapshot(e,t,s){this.view.lastRenderedLocation=this.history.location,this.notifyApplicationAfterRender(s)}preloadOnLoadLinksForView(e){this.preloader.preloadOnLoadLinksForView(e)}viewInvalidated(e){this.adapter.pageInvalidated(e)}frameLoaded(e){this.notifyApplicationAfterFrameLoad(e)}frameRendered(e,t){this.notifyApplicationAfterFrameRender(e,t)}applicationAllowsFollowingLinkToLocation(e,t,s){return!this.notifyApplicationAfterClickingLinkToLocation(e,t,s).defaultPrevented}applicationAllowsVisitingLocation(e){return!this.notifyApplicationBeforeVisitingLocation(e).defaultPrevented}notifyApplicationAfterClickingLinkToLocation(e,t,s){return i(\"turbo:click\",{target:e,detail:{url:t.href,originalEvent:s},cancelable:!0})}notifyApplicationBeforeVisitingLocation(e){return i(\"turbo:before-visit\",{detail:{url:e.href},cancelable:!0})}notifyApplicationAfterVisitingLocation(e,t){return i(\"turbo:visit\",{detail:{url:e.href,action:t}})}notifyApplicationBeforeCachingSnapshot(){return i(\"turbo:before-cache\")}notifyApplicationBeforeRender(e,t){return i(\"turbo:before-render\",{detail:{newBody:e,...t},cancelable:!0})}notifyApplicationAfterRender(e){return i(\"turbo:render\",{detail:{renderMethod:e}})}notifyApplicationAfterPageLoad(e={}){return i(\"turbo:load\",{detail:{url:this.location.href,timing:e}})}notifyApplicationAfterFrameLoad(e){return i(\"turbo:frame-load\",{target:e})}notifyApplicationAfterFrameRender(e,t){return i(\"turbo:frame-render\",{detail:{fetchResponse:e},target:t,cancelable:!0})}submissionIsNavigatable(e,t){if(\"off\"==P.forms.mode)return!1;{const s=!t||this.elementIsNavigatable(t);return\"optin\"==P.forms.mode?s&&null!=e.closest('[data-turbo=\"true\"]'):s&&this.elementIsNavigatable(e)}}elementIsNavigatable(e){const t=y(e,\"[data-turbo]\"),s=y(e,\"turbo-frame\");return P.drive.enabled||s?!t||\"false\"!=t.getAttribute(\"data-turbo\"):!!t&&\"true\"==t.getAttribute(\"data-turbo\")}getActionForLink(e){return b(e)||\"advance\"}get snapshot(){return this.view.snapshot}}(V),{cache:Et,navigator:At}=yt;function Rt(){yt.start()}function Tt(e){yt.registerAdapter(e)}function Lt(e,t){yt.visit(e,t)}function Ct(e){yt.connectStreamSource(e)}function Pt(e){yt.disconnectStreamSource(e)}function kt(e){yt.renderStreamMessage(e)}function Mt(e){console.warn(\"Please replace `Turbo.setProgressBarDelay(delay)` with `Turbo.config.drive.progressBarDelay = delay`. The top-level function is deprecated and will be removed in a future version of Turbo.`\"),P.drive.progressBarDelay=e}function Ft(e){console.warn(\"Please replace `Turbo.setConfirmMethod(confirmMethod)` with `Turbo.config.forms.confirm = confirmMethod`. The top-level function is deprecated and will be removed in a future version of Turbo.`\"),P.forms.confirm=e}function It(e){console.warn(\"Please replace `Turbo.setFormMode(mode)` with `Turbo.config.forms.mode = mode`. The top-level function is deprecated and will be removed in a future version of Turbo.`\"),P.forms.mode=e}function qt(e,t){pt.renderElement(e,t)}function Ht(e,t){Ae.renderElement(e,t)}var Bt=Object.freeze({__proto__:null,PageRenderer:mt,PageSnapshot:Pe,FrameRenderer:ge,fetch:W,config:P,session:yt,cache:Et,navigator:At,start:Rt,registerAdapter:Tt,visit:Lt,connectStreamSource:Ct,disconnectStreamSource:Pt,renderStreamMessage:kt,setProgressBarDelay:Mt,setConfirmMethod:Ft,setFormMode:It,morphBodyElements:qt,morphTurboFrameElements:Ht,morphChildren:Se,morphElements:ve});class Nt extends Error{}function Ot(e,s){if(e){const r=e.getAttribute(\"src\");if(null!=r&&null!=s&&N(r,s))throw new Error(`Matching <turbo-frame id=\"${e.id}\"> element has a source URL which references itself`);if(e.ownerDocument!==document&&(e=document.importNode(e,!0)),e instanceof t)return e.connectedCallback(),e.disconnectedCallback(),e}}const Dt={after(){this.removeDuplicateTargetSiblings(),this.targetElements.forEach((e=>e.parentElement?.insertBefore(this.templateContent,e.nextSibling)))},append(){this.removeDuplicateTargetChildren(),this.targetElements.forEach((e=>e.append(this.templateContent)))},before(){this.removeDuplicateTargetSiblings(),this.targetElements.forEach((e=>e.parentElement?.insertBefore(this.templateContent,e)))},prepend(){this.removeDuplicateTargetChildren(),this.targetElements.forEach((e=>e.prepend(this.templateContent)))},remove(){this.targetElements.forEach((e=>e.remove()))},replace(){const e=this.getAttribute(\"method\");this.targetElements.forEach((t=>{\"morph\"===e?ve(t,this.templateContent):t.replaceWith(this.templateContent)}))},update(){const e=this.getAttribute(\"method\");this.targetElements.forEach((t=>{\"morph\"===e?Se(t,this.templateContent):(t.innerHTML=\"\",t.append(this.templateContent))}))},refresh(){const e=this.getAttribute(\"method\"),t=this.requestId,s=this.getAttribute(\"scroll\");yt.refresh(this.baseURI,{method:e,requestId:t,scroll:s})}};class xt extends HTMLElement{static async renderElement(e){await e.performAction()}async connectedCallback(){try{await this.render()}catch(e){console.error(e)}finally{this.disconnect()}}async render(){return this.renderPromise??=(async()=>{const e=this.beforeRenderEvent;this.dispatchEvent(e)&&(await o(),await e.detail.render(this))})()}disconnect(){try{this.remove()}catch{}}removeDuplicateTargetChildren(){this.duplicateChildren.forEach((e=>e.remove()))}get duplicateChildren(){const e=this.targetElements.flatMap((e=>[...e.children])).filter((e=>!!e.getAttribute(\"id\"))),t=[...this.templateContent?.children||[]].filter((e=>!!e.getAttribute(\"id\"))).map((e=>e.getAttribute(\"id\")));return e.filter((e=>t.includes(e.getAttribute(\"id\"))))}removeDuplicateTargetSiblings(){this.duplicateSiblings.forEach((e=>e.remove()))}get duplicateSiblings(){const e=this.targetElements.flatMap((e=>[...e.parentElement.children])).filter((e=>!!e.id)),t=[...this.templateContent?.children||[]].filter((e=>!!e.id)).map((e=>e.id));return e.filter((e=>t.includes(e.id)))}get performAction(){if(this.action){const e=Dt[this.action];if(e)return e;this.#F(\"unknown action\")}this.#F(\"action attribute is missing\")}get targetElements(){return this.target?this.targetElementsById:this.targets?this.targetElementsByQuery:void this.#F(\"target or targets attribute is missing\")}get templateContent(){return this.templateElement.content.cloneNode(!0)}get templateElement(){if(null===this.firstElementChild){const e=this.ownerDocument.createElement(\"template\");return this.appendChild(e),e}if(this.firstElementChild instanceof HTMLTemplateElement)return this.firstElementChild;this.#F(\"first child element must be a <template> element\")}get action(){return this.getAttribute(\"action\")}get target(){return this.getAttribute(\"target\")}get targets(){return this.getAttribute(\"targets\")}get requestId(){return this.getAttribute(\"request-id\")}#F(e){throw new Error(`${this.description}: ${e}`)}get description(){return(this.outerHTML.match(/<[^>]+>/)??[])[0]??\"<turbo-stream>\"}get beforeRenderEvent(){return new CustomEvent(\"turbo:before-stream-render\",{bubbles:!0,cancelable:!0,detail:{newStream:this,render:xt.renderElement}})}get targetElementsById(){const e=this.ownerDocument?.getElementById(this.target);return null!==e?[e]:[]}get targetElementsByQuery(){const e=this.ownerDocument?.querySelectorAll(this.targets);return 0!==e.length?Array.prototype.slice.call(e):[]}}class Vt extends HTMLElement{streamSource=null;connectedCallback(){this.streamSource=this.src.match(/^ws{1,2}:/)?new WebSocket(this.src):new EventSource(this.src),Ct(this.streamSource)}disconnectedCallback(){this.streamSource&&(this.streamSource.close(),Pt(this.streamSource))}get src(){return this.getAttribute(\"src\")||\"\"}}t.delegateConstructor=class{fetchResponseLoaded=e=>Promise.resolve();#I=null;#q=()=>{};#H=!1;#B=!1;#N=new Set;#O=!1;action=null;constructor(e){this.element=e,this.view=new he(this,this.element),this.appearanceObserver=new Y(this,this.element),this.formLinkClickObserver=new me(this,this.element),this.linkInterceptor=new de(this,this.element),this.restorationIdentifier=d(),this.formSubmitObserver=new ce(this,this.element)}connect(){this.#H||(this.#H=!0,this.loadingStyle==e.lazy?this.appearanceObserver.start():this.#D(),this.formLinkClickObserver.start(),this.linkInterceptor.start(),this.formSubmitObserver.start())}disconnect(){this.#H&&(this.#H=!1,this.appearanceObserver.stop(),this.formLinkClickObserver.stop(),this.linkInterceptor.stop(),this.formSubmitObserver.stop(),this.element.hasAttribute(\"recurse\")||this.#I?.cancel())}disabledChanged(){this.disabled?this.#I?.cancel():this.loadingStyle==e.eager&&this.#D()}sourceURLChanged(){this.#x(\"src\")||(this.sourceURL||this.#I?.cancel(),this.element.isConnected&&(this.complete=!1),(this.loadingStyle==e.eager||this.#B)&&this.#D())}sourceURLReloaded(){const{refresh:e,src:t}=this.element;return this.#O=t&&\"morph\"===e,this.element.removeAttribute(\"complete\"),this.element.src=null,this.element.src=t,this.element.loaded}loadingStyleChanged(){this.loadingStyle==e.lazy?this.appearanceObserver.start():(this.appearanceObserver.stop(),this.#D())}async#D(){this.enabled&&this.isActive&&!this.complete&&this.sourceURL&&(this.element.loaded=this.#V(k(this.sourceURL)),this.appearanceObserver.stop(),await this.element.loaded,this.#B=!0)}async loadResponse(e){(e.redirected||e.succeeded&&e.isHTML)&&(this.sourceURL=e.response.url);try{const t=await e.responseHTML;if(t){const s=l(t);Pe.fromDocument(s).isVisitable?await this.#W(e,s):await this.#U(e)}}finally{this.#O=!1,this.fetchResponseLoaded=()=>Promise.resolve()}}elementAppearedInViewport(e){this.proposeVisitIfNavigatedWithAction(e,b(e)),this.#D()}willSubmitFormLinkToLocation(e){return this.#_(e)}submittedFormLinkToLocation(e,t,s){const r=this.#p(e);r&&s.setAttribute(\"data-turbo-frame\",r.id)}shouldInterceptLinkClick(e,t,s){return this.#_(e)}linkClickIntercepted(e,t){this.#$(e,t)}willSubmitForm(e,t){return e.closest(\"turbo-frame\")==this.element&&this.#_(e,t)}formSubmitted(e,t){this.formSubmission&&this.formSubmission.stop(),this.formSubmission=new ie(this,e,t);const{fetchRequest:s}=this.formSubmission,r=this.#p(e,t);this.prepareRequest(s,r),this.formSubmission.start()}prepareRequest(e,t=this){e.headers[\"Turbo-Frame\"]=t.id,this.currentNavigationElement?.hasAttribute(\"data-turbo-stream\")&&e.acceptResponseType(Q.contentType)}requestStarted(e){m(this.element)}requestPreventedHandlingResponse(e,t){this.#q()}async requestSucceededWithResponse(e,t){await this.loadResponse(t),this.#q()}async requestFailedWithResponse(e,t){await this.loadResponse(t),this.#q()}requestErrored(e,t){console.error(t),this.#q()}requestFinished(e){p(this.element)}formSubmissionStarted({formElement:e}){m(e,this.#p(e))}formSubmissionSucceededWithResponse(e,t){const s=this.#p(e.formElement,e.submitter);s.delegate.proposeVisitIfNavigatedWithAction(s,b(e.submitter,e.formElement,s)),s.delegate.loadResponse(t),e.isSafe||yt.clearCache()}formSubmissionFailedWithResponse(e,t){this.element.delegate.loadResponse(t),yt.clearCache()}formSubmissionErrored(e,t){console.error(t)}formSubmissionFinished({formElement:e}){p(e,this.#p(e))}allowsImmediateRender({element:e},t){const s=i(\"turbo:before-frame-render\",{target:this.element,detail:{newFrame:e,...t},cancelable:!0}),{defaultPrevented:r,detail:{render:n}}=s;return this.view.renderer&&n&&(this.view.renderer.renderElement=n),!r}viewRenderedSnapshot(e,t,s){}preloadOnLoadLinksForView(e){yt.preloadOnLoadLinksForView(e)}viewInvalidated(){}willRenderFrame(e,t){this.previousFrameElement=e.cloneNode(!0)}visitCachedSnapshot=({element:e})=>{const t=e.querySelector(\"#\"+this.element.id);t&&this.previousFrameElement&&t.replaceChildren(...this.previousFrameElement.children),delete this.previousFrameElement};async#W(e,t){const s=await this.extractForeignFrameElement(t.body),r=this.#O?Ae:ge;if(s){const t=new ne(s),i=new r(this,this.view.snapshot,t,!1,!1);this.view.renderPromise&&await this.view.renderPromise,this.changeHistory(),await this.view.render(i),this.complete=!0,yt.frameRendered(e,this.element),yt.frameLoaded(this.element),await this.fetchResponseLoaded(e)}else this.#j(e)&&this.#z(e)}async#V(e){const t=new z(this,_.get,e,new URLSearchParams,this.element);return this.#I?.cancel(),this.#I=t,new Promise((e=>{this.#q=()=>{this.#q=()=>{},this.#I=null,e()},t.perform()}))}#$(e,t,s){const r=this.#p(e,s);r.delegate.proposeVisitIfNavigatedWithAction(r,b(s,e,r)),this.#G(e,(()=>{r.src=t}))}proposeVisitIfNavigatedWithAction(e,t=null){if(this.action=t,this.action){const t=Pe.fromElement(e).clone(),{visitCachedSnapshot:s}=e.delegate;e.delegate.fetchResponseLoaded=async r=>{if(e.src){const{statusCode:i,redirected:n}=r,o={response:{statusCode:i,redirected:n,responseHTML:await r.responseHTML},visitCachedSnapshot:s,willRender:!1,updateHistory:!1,restorationIdentifier:this.restorationIdentifier,snapshot:t};this.action&&(o.action=this.action),yt.visit(e.src,o)}}}}changeHistory(){if(this.action){const e=g(this.action);yt.history.update(e,k(this.element.src||\"\"),this.restorationIdentifier)}}async#U(e){console.warn(`The response (${e.statusCode}) from <turbo-frame id=\"${this.element.id}\"> is performing a full page visit due to turbo-visit-control.`),await this.#K(e.response)}#j(e){this.element.setAttribute(\"complete\",\"\");const t=e.response;return!i(\"turbo:frame-missing\",{target:this.element,detail:{response:t,visit:async(e,t)=>{e instanceof Response?this.#K(e):yt.visit(e,t)}},cancelable:!0}).defaultPrevented}#z(e){this.view.missing(),this.#J(e)}#J(e){const t=`The response (${e.statusCode}) did not contain the expected <turbo-frame id=\"${this.element.id}\"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;throw new Nt(t)}async#K(e){const t=new D(e),s=await t.responseHTML,{location:r,redirected:i,statusCode:n}=t;return yt.visit(r,{response:{redirected:i,statusCode:n,responseHTML:s}})}#p(e,s){const r=u(\"data-turbo-frame\",s,e)||this.element.getAttribute(\"target\"),i=this.#X(r);return i instanceof t?i:this.element}async extractForeignFrameElement(e){let s;const r=CSS.escape(this.id);try{if(s=Ot(e.querySelector(`turbo-frame#${r}`),this.sourceURL),s)return s;if(s=Ot(e.querySelector(`turbo-frame[src][recurse~=${r}]`),this.sourceURL),s)return await s.loaded,await this.extractForeignFrameElement(s)}catch(e){return console.error(e),new t}return null}#Y(e,t){return q(k(F(e,t)),this.rootLocation)}#_(e,t){const s=u(\"data-turbo-frame\",t,e)||this.element.getAttribute(\"target\");if(e instanceof HTMLFormElement&&!this.#Y(e,t))return!1;if(!this.enabled||\"_top\"==s)return!1;if(s){const e=this.#X(s);if(e)return!e.disabled;if(\"_parent\"==s)return!1}return!!yt.elementIsNavigatable(e)&&!(t&&!yt.elementIsNavigatable(t))}get id(){return this.element.id}get disabled(){return this.element.disabled}get enabled(){return!this.disabled}get sourceURL(){if(this.element.src)return this.element.src}set sourceURL(e){this.#Q(\"src\",(()=>{this.element.src=e??null}))}get loadingStyle(){return this.element.loading}get isLoading(){return void 0!==this.formSubmission||void 0!==this.#q()}get complete(){return this.element.hasAttribute(\"complete\")}set complete(e){e?this.element.setAttribute(\"complete\",\"\"):this.element.removeAttribute(\"complete\")}get isActive(){return this.element.isActive&&this.#H}get rootLocation(){const e=this.element.ownerDocument.querySelector('meta[name=\"turbo-root\"]');return k(e?.content??\"/\")}#x(e){return this.#N.has(e)}#Q(e,t){this.#N.add(e),t(),this.#N.delete(e)}#G(e,t){this.currentNavigationElement=e,t(),delete this.currentNavigationElement}#X(e){if(null!=e){const s=\"_parent\"===e?this.element.parentElement.closest(\"turbo-frame\"):document.getElementById(e);if(s instanceof t)return s}}},void 0===customElements.get(\"turbo-frame\")&&customElements.define(\"turbo-frame\",t),void 0===customElements.get(\"turbo-stream\")&&customElements.define(\"turbo-stream\",xt),void 0===customElements.get(\"turbo-stream-source\")&&customElements.define(\"turbo-stream-source\",Vt),(()=>{const e=document.currentScript;if(!e)return;if(e.hasAttribute(\"data-turbo-suppress-warning\"))return;let t=e.parentElement;for(;t;){if(t==document.body)return console.warn(h`\n        You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!\n\n        Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.\n\n        For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements\n\n        ——\n        Suppress this warning by adding a \"data-turbo-suppress-warning\" attribute to: %s\n      `,e.outerHTML);t=t.parentElement}})(),window.Turbo={...Bt,StreamActions:Dt},Rt();var Wt=Object.freeze({__proto__:null,FetchEnctype:j,FetchMethod:_,FetchRequest:z,FetchResponse:D,FrameElement:t,FrameLoadingStyle:e,FrameRenderer:ge,PageRenderer:mt,PageSnapshot:Pe,StreamActions:Dt,StreamElement:xt,StreamSourceElement:Vt,cache:Et,config:P,connectStreamSource:Ct,disconnectStreamSource:Pt,fetch:W,fetchEnctypeFromString:$,fetchMethodFromString:U,isSafe:G,morphBodyElements:qt,morphChildren:Se,morphElements:ve,morphTurboFrameElements:Ht,navigator:At,registerAdapter:Tt,renderStreamMessage:kt,session:yt,setConfirmMethod:Ft,setFormMode:It,setProgressBarDelay:Mt,start:Rt,visit:Lt});let Ut;async function _t(){return Ut||$t(jt().then($t))}function $t(e){return Ut=e}async function jt(){const{createConsumer:e}=await Promise.resolve().then((function(){return fs}));return e()}async function zt(e,t){const{subscriptions:s}=await _t();return s.create(e,t)}var Gt=Object.freeze({__proto__:null,getConsumer:_t,setConsumer:$t,createConsumer:jt,subscribeTo:zt});function Kt(e){return e&&\"object\"==typeof e?e instanceof Date||e instanceof RegExp?e:Array.isArray(e)?e.map(Kt):Object.keys(e).reduce((function(t,s){return t[s[0].toLowerCase()+s.slice(1).replace(/([A-Z]+)/g,(function(e,t){return\"_\"+t.toLowerCase()}))]=Kt(e[s]),t}),{}):e}class Jt extends HTMLElement{static observedAttributes=[\"channel\",\"signed-stream-name\"];async connectedCallback(){Ct(this),this.subscription=await zt(this.channel,{received:this.dispatchMessageEvent.bind(this),connected:this.subscriptionConnected.bind(this),disconnected:this.subscriptionDisconnected.bind(this)})}disconnectedCallback(){Pt(this),this.subscription&&this.subscription.unsubscribe(),this.subscriptionDisconnected()}attributeChangedCallback(){this.subscription&&(this.disconnectedCallback(),this.connectedCallback())}dispatchMessageEvent(e){const t=new MessageEvent(\"message\",{data:e});return this.dispatchEvent(t)}subscriptionConnected(){this.setAttribute(\"connected\",\"\")}subscriptionDisconnected(){this.removeAttribute(\"connected\")}get channel(){return{channel:this.getAttribute(\"channel\"),signed_stream_name:this.getAttribute(\"signed-stream-name\"),...Kt({...this.dataset})}}}void 0===customElements.get(\"turbo-cable-stream-source\")&&customElements.define(\"turbo-cable-stream-source\",Jt),window.Turbo=Wt,addEventListener(\"turbo:before-fetch-request\",(function(e){if(e.target instanceof HTMLFormElement){const{target:t,detail:{fetchOptions:s}}=e;t.addEventListener(\"turbo:submit-start\",(({detail:{formSubmission:{submitter:e}}})=>{const r=function(e){return e instanceof FormData||e instanceof URLSearchParams}(s.body)?s.body:new URLSearchParams,i=function(e,t,s){const r=function(e){return e instanceof HTMLButtonElement||e instanceof HTMLInputElement?\"_method\"===e.name?e.value:e.hasAttribute(\"formmethod\")?e.formMethod:null:null}(e),i=t.get(\"_method\"),n=s.getAttribute(\"method\")||\"get\";return\"string\"==typeof r?r:\"string\"==typeof i?i:n}(e,r,t);/get/i.test(i)||(/post/i.test(i)?r.delete(\"_method\"):r.set(\"_method\",i),s.method=\"post\")}),{once:!0})}}));var Xt={logger:\"undefined\"!=typeof console?console:void 0,WebSocket:\"undefined\"!=typeof WebSocket?WebSocket:void 0},Yt={log(...e){this.enabled&&(e.push(Date.now()),Xt.logger.log(\"[ActionCable]\",...e))}};const Qt=()=>(new Date).getTime(),Zt=e=>(Qt()-e)/1e3;class es{constructor(e){this.visibilityDidChange=this.visibilityDidChange.bind(this),this.connection=e,this.reconnectAttempts=0}start(){this.isRunning()||(this.startedAt=Qt(),delete this.stoppedAt,this.startPolling(),addEventListener(\"visibilitychange\",this.visibilityDidChange),Yt.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`))}stop(){this.isRunning()&&(this.stoppedAt=Qt(),this.stopPolling(),removeEventListener(\"visibilitychange\",this.visibilityDidChange),Yt.log(\"ConnectionMonitor stopped\"))}isRunning(){return this.startedAt&&!this.stoppedAt}recordMessage(){this.pingedAt=Qt()}recordConnect(){this.reconnectAttempts=0,delete this.disconnectedAt,Yt.log(\"ConnectionMonitor recorded connect\")}recordDisconnect(){this.disconnectedAt=Qt(),Yt.log(\"ConnectionMonitor recorded disconnect\")}startPolling(){this.stopPolling(),this.poll()}stopPolling(){clearTimeout(this.pollTimeout)}poll(){this.pollTimeout=setTimeout((()=>{this.reconnectIfStale(),this.poll()}),this.getPollInterval())}getPollInterval(){const{staleThreshold:e,reconnectionBackoffRate:t}=this.constructor;return 1e3*e*Math.pow(1+t,Math.min(this.reconnectAttempts,10))*(1+(0===this.reconnectAttempts?1:t)*Math.random())}reconnectIfStale(){this.connectionIsStale()&&(Yt.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${Zt(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`),this.reconnectAttempts++,this.disconnectedRecently()?Yt.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${Zt(this.disconnectedAt)} s`):(Yt.log(\"ConnectionMonitor reopening\"),this.connection.reopen()))}get refreshedAt(){return this.pingedAt?this.pingedAt:this.startedAt}connectionIsStale(){return Zt(this.refreshedAt)>this.constructor.staleThreshold}disconnectedRecently(){return this.disconnectedAt&&Zt(this.disconnectedAt)<this.constructor.staleThreshold}visibilityDidChange(){\"visible\"===document.visibilityState&&setTimeout((()=>{!this.connectionIsStale()&&this.connection.isOpen()||(Yt.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`),this.connection.reopen())}),200)}}es.staleThreshold=6,es.reconnectionBackoffRate=.15;var ts=es,ss={message_types:{welcome:\"welcome\",disconnect:\"disconnect\",ping:\"ping\",confirmation:\"confirm_subscription\",rejection:\"reject_subscription\"},disconnect_reasons:{unauthorized:\"unauthorized\",invalid_request:\"invalid_request\",server_restart:\"server_restart\",remote:\"remote\"},default_mount_path:\"/cable\",protocols:[\"actioncable-v1-json\",\"actioncable-unsupported\"]};const{message_types:rs,protocols:is}=ss,ns=is.slice(0,is.length-1),os=[].indexOf;class as{constructor(e){this.open=this.open.bind(this),this.consumer=e,this.subscriptions=this.consumer.subscriptions,this.monitor=new ts(this),this.disconnected=!0}send(e){return!!this.isOpen()&&(this.webSocket.send(JSON.stringify(e)),!0)}open(){if(this.isActive())return Yt.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`),!1;{const e=[...is,...this.consumer.subprotocols||[]];return Yt.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${e}`),this.webSocket&&this.uninstallEventHandlers(),this.webSocket=new Xt.WebSocket(this.consumer.url,e),this.installEventHandlers(),this.monitor.start(),!0}}close({allowReconnect:e}={allowReconnect:!0}){if(e||this.monitor.stop(),this.isOpen())return this.webSocket.close()}reopen(){if(Yt.log(`Reopening WebSocket, current state is ${this.getState()}`),!this.isActive())return this.open();try{return this.close()}catch(e){Yt.log(\"Failed to reopen WebSocket\",e)}finally{Yt.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`),setTimeout(this.open,this.constructor.reopenDelay)}}getProtocol(){if(this.webSocket)return this.webSocket.protocol}isOpen(){return this.isState(\"open\")}isActive(){return this.isState(\"open\",\"connecting\")}triedToReconnect(){return this.monitor.reconnectAttempts>0}isProtocolSupported(){return os.call(ns,this.getProtocol())>=0}isState(...e){return os.call(e,this.getState())>=0}getState(){if(this.webSocket)for(let e in Xt.WebSocket)if(Xt.WebSocket[e]===this.webSocket.readyState)return e.toLowerCase();return null}installEventHandlers(){for(let e in this.events){const t=this.events[e].bind(this);this.webSocket[`on${e}`]=t}}uninstallEventHandlers(){for(let e in this.events)this.webSocket[`on${e}`]=function(){}}}as.reopenDelay=500,as.prototype.events={message(e){if(!this.isProtocolSupported())return;const{identifier:t,message:s,reason:r,reconnect:i,type:n}=JSON.parse(e.data);switch(this.monitor.recordMessage(),n){case rs.welcome:return this.triedToReconnect()&&(this.reconnectAttempted=!0),this.monitor.recordConnect(),this.subscriptions.reload();case rs.disconnect:return Yt.log(`Disconnecting. Reason: ${r}`),this.close({allowReconnect:i});case rs.ping:return null;case rs.confirmation:return this.subscriptions.confirmSubscription(t),this.reconnectAttempted?(this.reconnectAttempted=!1,this.subscriptions.notify(t,\"connected\",{reconnected:!0})):this.subscriptions.notify(t,\"connected\",{reconnected:!1});case rs.rejection:return this.subscriptions.reject(t);default:return this.subscriptions.notify(t,\"received\",s)}},open(){if(Yt.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`),this.disconnected=!1,!this.isProtocolSupported())return Yt.log(\"Protocol is unsupported. Stopping monitor and disconnecting.\"),this.close({allowReconnect:!1})},close(e){if(Yt.log(\"WebSocket onclose event\"),!this.disconnected)return this.disconnected=!0,this.monitor.recordDisconnect(),this.subscriptions.notifyAll(\"disconnected\",{willAttemptReconnect:this.monitor.isRunning()})},error(){Yt.log(\"WebSocket onerror event\")}};var cs=as;class ls{constructor(e,t={},s){this.consumer=e,this.identifier=JSON.stringify(t),function(e,t){if(null!=t)for(let s in t){const r=t[s];e[s]=r}}(this,s)}perform(e,t={}){return t.action=e,this.send(t)}send(e){return this.consumer.send({command:\"message\",identifier:this.identifier,data:JSON.stringify(e)})}unsubscribe(){return this.consumer.subscriptions.remove(this)}}var hs=class{constructor(e){this.subscriptions=e,this.pendingSubscriptions=[]}guarantee(e){-1==this.pendingSubscriptions.indexOf(e)?(Yt.log(`SubscriptionGuarantor guaranteeing ${e.identifier}`),this.pendingSubscriptions.push(e)):Yt.log(`SubscriptionGuarantor already guaranteeing ${e.identifier}`),this.startGuaranteeing()}forget(e){Yt.log(`SubscriptionGuarantor forgetting ${e.identifier}`),this.pendingSubscriptions=this.pendingSubscriptions.filter((t=>t!==e))}startGuaranteeing(){this.stopGuaranteeing(),this.retrySubscribing()}stopGuaranteeing(){clearTimeout(this.retryTimeout)}retrySubscribing(){this.retryTimeout=setTimeout((()=>{this.subscriptions&&\"function\"==typeof this.subscriptions.subscribe&&this.pendingSubscriptions.map((e=>{Yt.log(`SubscriptionGuarantor resubscribing ${e.identifier}`),this.subscriptions.subscribe(e)}))}),500)}};class ds{constructor(e){this.consumer=e,this.guarantor=new hs(this),this.subscriptions=[]}create(e,t){const s=\"object\"==typeof e?e:{channel:e},r=new ls(this.consumer,s,t);return this.add(r)}add(e){return this.subscriptions.push(e),this.consumer.ensureActiveConnection(),this.notify(e,\"initialized\"),this.subscribe(e),e}remove(e){return this.forget(e),this.findAll(e.identifier).length||this.sendCommand(e,\"unsubscribe\"),e}reject(e){return this.findAll(e).map((e=>(this.forget(e),this.notify(e,\"rejected\"),e)))}forget(e){return this.guarantor.forget(e),this.subscriptions=this.subscriptions.filter((t=>t!==e)),e}findAll(e){return this.subscriptions.filter((t=>t.identifier===e))}reload(){return this.subscriptions.map((e=>this.subscribe(e)))}notifyAll(e,...t){return this.subscriptions.map((s=>this.notify(s,e,...t)))}notify(e,t,...s){let r;return r=\"string\"==typeof e?this.findAll(e):[e],r.map((e=>\"function\"==typeof e[t]?e[t](...s):void 0))}subscribe(e){this.sendCommand(e,\"subscribe\")&&this.guarantor.guarantee(e)}confirmSubscription(e){Yt.log(`Subscription confirmed ${e}`),this.findAll(e).map((e=>this.guarantor.forget(e)))}sendCommand(e,t){const{identifier:s}=e;return this.consumer.send({command:t,identifier:s})}}class us{constructor(e){this._url=e,this.subscriptions=new ds(this),this.connection=new cs(this),this.subprotocols=[]}get url(){return ms(this._url)}send(e){return this.connection.send(e)}connect(){return this.connection.open()}disconnect(){return this.connection.close({allowReconnect:!1})}ensureActiveConnection(){if(!this.connection.isActive())return this.connection.open()}addSubProtocol(e){this.subprotocols=[...this.subprotocols,e]}}function ms(e){if(\"function\"==typeof e&&(e=e()),e&&!/^wss?:/i.test(e)){const t=document.createElement(\"a\");return t.href=e,t.href=t.href,t.protocol=t.protocol.replace(\"http\",\"ws\"),t.href}return e}function ps(e){const t=document.head.querySelector(`meta[name='action-cable-${e}']`);if(t)return t.getAttribute(\"content\")}var fs=Object.freeze({__proto__:null,Connection:cs,ConnectionMonitor:ts,Consumer:us,INTERNAL:ss,Subscription:ls,Subscriptions:ds,SubscriptionGuarantor:hs,adapters:Xt,createWebSocketURL:ms,logger:Yt,createConsumer:function(e=ps(\"url\")||ss.default_mount_path){return new us(e)},getConfig:ps});export{Wt as Turbo,Gt as cable};\n//# sourceMappingURL=turbo.min.js.map\n"},{"path":"storage/.keep","content":""}]}