{"language":"crystal","files":[{"path":"shard.yml","content":"name: roundhouse-app\nversion: 0.1.0\ncrystal: \">= 1.6.0\"\n\ndependencies:\n  sqlite3:\n    github: crystal-lang/crystal-sqlite3\n"},{"path":"spec/article_spec.cr","content":"require \"../src/test_helper\"\nrequire \"../src/app\"\n\nclass ArticleTest < RoundhouseTest\n  def test_creates_an_article_with_valid_attributes : Nil\n    article = ArticlesFixtures.one\n    raise \"refute_nil failed\" if article.id.not_nil!.nil?\n    raise \"assert_equal failed\" if \"Getting Started with Rails\" != article.title.not_nil!\n  end\n\n  def test_validates_title_presence : Nil\n    article = begin\n      __inst = Article.new({:title => \"\", :body => \"Valid body content here.\"})\n      __inst.title = \"\"\n      __inst.body = \"Valid body content here.\"\n      __inst\n    end\n    raise \"refute failed\" if article.save.not_nil!\n  end\n\n  def test_validates_body_minimum_length : Nil\n    article = begin\n      __inst = Article.new({:title => \"Valid Title\", :body => \"Short\"})\n      __inst.title = \"Valid Title\"\n      __inst.body = \"Short\"\n      __inst\n    end\n    raise \"refute failed\" if article.save.not_nil!\n  end\n\n  def test_destroys_comments_when_article_is_destroyed : Nil\n    article = ArticlesFixtures.one\n    __diff_before = Comment.count.not_nil!\n    article.destroy\n    __diff_after = Comment.count.not_nil!\n    raise \"Comment.count didn't change by -1\" if __diff_after - __diff_before != -1_i64\n  end\nend\n"},{"path":"spec/articles_controller_spec.cr","content":"require \"../src/test_helper\"\nrequire \"../src/app\"\n\nclass ArticlesControllerTest < RoundhouseTest\n  @article : Article?\n\n  def test_should_get_index : Nil\n    @article = ArticlesFixtures.one\n    get(RouteHelpers.articles_path.not_nil!)\n    assert_response(:success)\n    assert_select(\"h1\", \"Articles\")\n    assert_select(\"#articles\") do assert_select(\"h2\", minimum: 1_i64) end\n  end\n\n  def test_should_get_new : Nil\n    @article = ArticlesFixtures.one\n    get(RouteHelpers.new_article_path.not_nil!)\n    assert_response(:success)\n    assert_select(\"form\")\n  end\n\n  def test_should_create_article : Nil\n    @article = ArticlesFixtures.one\n    __diff_before = Article.count.not_nil!\n    post(RouteHelpers.articles_path.not_nil!, params: {article: {body: \"A sufficiently long body for validation.\", title: \"New Title\"}})\n    __diff_after = Article.count.not_nil!\n    raise \"Article.count didn't change by 1\" if __diff_after - __diff_before != 1_i64\n\n    assert_redirected_to(RouteHelpers.article_path(Article.last.not_nil!.id))\n    raise \"assert_equal failed\" if \"New Title\" != Article.last.not_nil!.title\n  end\n\n  def test_should_not_create_article_with_invalid_params : Nil\n    @article = ArticlesFixtures.one\n    __diff_before = Article.count.not_nil!\n    post(RouteHelpers.articles_path.not_nil!, params: {article: {title: \"\", body: \"\"}})\n    __diff_after = Article.count.not_nil!\n    raise \"Article.count didn't change by 0\" if __diff_after - __diff_before != 0_i64\n\n    assert_response(:unprocessable_entity)\n  end\n\n  def test_should_show_article : Nil\n    @article = ArticlesFixtures.one\n    get(RouteHelpers.article_path(@article.not_nil!.id.not_nil!))\n    assert_response(:success)\n    assert_select(\"h1\", @article.not_nil!.title.not_nil!)\n    assert_select(\"h2\", \"Comments\")\n    assert_select(\"#comments .p-4\", minimum: 1_i64)\n  end\n\n  def test_should_get_edit : Nil\n    @article = ArticlesFixtures.one\n    get(RouteHelpers.edit_article_path(@article.not_nil!.id.not_nil!))\n    assert_response(:success)\n    assert_select(\"form\")\n  end\n\n  def test_should_update_article : Nil\n    @article = ArticlesFixtures.one\n    patch(RouteHelpers.article_path(@article.not_nil!.id.not_nil!), params: {article: {body: @article.not_nil!.body.not_nil!, title: \"Updated Title\"}})\n    assert_redirected_to(RouteHelpers.article_path(@article.not_nil!.id.not_nil!))\n    @article.not_nil!.reload\n    raise \"assert_equal failed\" if \"Updated Title\" != @article.not_nil!.title.not_nil!\n  end\n\n  def test_should_not_update_article_with_invalid_params : Nil\n    @article = ArticlesFixtures.one\n    patch(RouteHelpers.article_path(@article.not_nil!.id.not_nil!), params: {article: {title: \"\", body: \"\"}})\n    assert_response(:unprocessable_entity)\n  end\n\n  def test_should_destroy_article : Nil\n    @article = ArticlesFixtures.one\n    __diff_before = Article.count.not_nil!\n    delete(RouteHelpers.article_path(@article.not_nil!.id.not_nil!))\n    __diff_after = Article.count.not_nil!\n    raise \"Article.count didn't change by -1\" if __diff_after - __diff_before != -1_i64\n\n    assert_redirected_to(RouteHelpers.articles_path.not_nil!)\n  end\nend\n"},{"path":"spec/comment_spec.cr","content":"require \"../src/test_helper\"\nrequire \"../src/app\"\n\nclass CommentTest < RoundhouseTest\n  def test_creates_a_comment_on_an_article : Nil\n    comment = CommentsFixtures.one\n    raise \"refute_nil failed\" if comment.id.not_nil!.nil?\n    raise \"assert_equal failed\" if ArticlesFixtures.one.id.not_nil! != comment.article_id.not_nil!\n  end\n\n  def test_belongs_to_article_association : Nil\n    article = ArticlesFixtures.one\n    comment = begin\n      __inst = Comment.new({:article_id => article.id.not_nil!, :commenter => \"Commenter\", :body => \"Comment body text.\"})\n      __inst.article_id = article.id.not_nil!\n      __inst.commenter = \"Commenter\"\n      __inst.body = \"Comment body text.\"\n      __inst.save\n      __inst\n    end\n    raise \"assert_equal failed\" if article.id.not_nil! != comment.article_id\n  end\n\n  def test_requires_commenter : Nil\n    article = ArticlesFixtures.one\n    comment = begin\n      __inst = Comment.new({:article_id => article.id.not_nil!, :body => \"Comment without commenter\"})\n      __inst.article_id = article.id.not_nil!\n      __inst.body = \"Comment without commenter\"\n      __inst\n    end\n    raise \"refute failed\" if comment.save.not_nil!\n  end\n\n  def test_requires_body : Nil\n    article = ArticlesFixtures.one\n    comment = begin\n      __inst = Comment.new({:article_id => article.id.not_nil!, :commenter => \"Someone\"})\n      __inst.article_id = article.id.not_nil!\n      __inst.commenter = \"Someone\"\n      __inst\n    end\n    raise \"refute failed\" if comment.save.not_nil!\n  end\n\n  def test_requires_valid_article : Nil\n    comment = begin\n      __inst = Comment.new({:commenter => \"Test\", :body => \"A test comment.\", :article_id => 999999_i64})\n      __inst.commenter = \"Test\"\n      __inst.body = \"A test comment.\"\n      __inst.article_id = 999999_i64\n      __inst\n    end\n    raise \"refute failed\" if comment.save.not_nil!\n  end\nend\n"},{"path":"spec/comments_controller_spec.cr","content":"require \"../src/test_helper\"\nrequire \"../src/app\"\n\nclass CommentsControllerTest < RoundhouseTest\n  @article : Article?\n  @comment : Comment?\n\n  def test_should_create_comment : Nil\n    @article = ArticlesFixtures.one\n    @comment = CommentsFixtures.one\n    __diff_before = Comment.count.not_nil!\n    post(RouteHelpers.article_comments_path(@article.not_nil!.id.not_nil!), params: {comment: {commenter: \"Test\", body: \"A test comment.\"}})\n    __diff_after = Comment.count.not_nil!\n    raise \"Comment.count didn't change by 1\" if __diff_after - __diff_before != 1_i64\n    assert_redirected_to(RouteHelpers.article_path(@article.not_nil!.id.not_nil!))\n  end\n\n  def test_should_not_create_comment_with_invalid_params : Nil\n    @article = ArticlesFixtures.one\n    @comment = CommentsFixtures.one\n    __diff_before = Comment.count.not_nil!\n    post(RouteHelpers.article_comments_path(@article.not_nil!.id.not_nil!), params: {comment: {commenter: \"\", body: \"\"}})\n    __diff_after = Comment.count.not_nil!\n    raise \"Comment.count didn't change by 0\" if __diff_after - __diff_before != 0_i64\n    assert_redirected_to(RouteHelpers.article_path(@article.not_nil!.id.not_nil!))\n  end\n\n  def test_should_destroy_comment : Nil\n    @article = ArticlesFixtures.one\n    @comment = CommentsFixtures.one\n    __diff_before = Comment.count.not_nil!\n    delete(RouteHelpers.article_comment_path(@article.not_nil!.id.not_nil!, @comment.not_nil!.id.not_nil!))\n    __diff_after = Comment.count.not_nil!\n    raise \"Comment.count didn't change by -1\" if __diff_after - __diff_before != -1_i64\n    assert_redirected_to(RouteHelpers.article_path(@article.not_nil!.id.not_nil!))\n  end\nend\n"},{"path":"src/action_controller_base.cr","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\nSTATUS_CODES = {ok: 200_i64, created: 201_i64, accepted: 202_i64, no_content: 204_i64, moved_permanently: 301_i64, found: 302_i64, see_other: 303_i64, not_modified: 304_i64, bad_request: 400_i64, unauthorized: 401_i64, forbidden: 403_i64, not_found: 404_i64, unprocessable_entity: 422_i64, unprocessable_content: 422_i64, internal_server_error: 500_i64}\n\nmodule ActionController\n  class Base\n    property params : Hash(String, Roundhouse::ParamValue)\n    property session : ActionDispatch::Session\n    property flash : ActionDispatch::Flash\n    property request_method : String?\n    property request_path : String?\n    property request_format : Symbol\n    property status : Int64\n    property body : String\n    property location : String?\n    property content_type : String\n\n    def initialize : Nil\n      @params = {} of String => Roundhouse::ParamValue\n      @session = ::ActionDispatch::Session.new\n      @flash = ::ActionDispatch::Flash.new\n      @status = 200_i64\n      @body = \"\"\n      @location = nil\n      @request_format = :html\n      @content_type = \"text/html; charset=utf-8\"\n    end\n\n    def process_action(_action_name : Symbol) : Nil\n      raise NotImplementedError.new(\"process_action must be overridden by subclass\")\n    end\n\n    def render(body : String, status : Symbol = :ok, content_type : String? = nil, location : String? = nil) : Nil\n      @body = body\n      @status = resolve_status(status)\n      if content_type.nil?\n        nil\n      else\n        @content_type = content_type\n      end\n      if location.nil?\n        nil\n      else\n        @location = location\n      end\n      nil\n    end\n\n    def redirect_to(path : String, notice : String? = nil, alert : String? = nil, status : Symbol = :found) : Nil\n      @location = path\n      @status = resolve_status(status)\n      if notice.nil?\n        nil\n      else\n        @flash[:notice] = notice\n      end\n      if alert.nil?\n        nil\n      else\n        @flash[:alert] = alert\n      end\n      nil\n    end\n\n    def head(status : Symbol, content_type : String? = nil) : Nil\n      @status = resolve_status(status)\n      @body = \"\"\n      if content_type.nil?\n        nil\n      else\n        @content_type = content_type\n      end\n      nil\n    end\n\n    def resolve_status(s : Symbol) : Int64\n      STATUS_CODES.fetch(s, 200_i64)\n    end\n  end\nend\n"},{"path":"src/active_record_base.cr","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\nmodule ActiveRecord\n  class Base\n    property id : Int64\n    @errors : Array(String)\n    @persisted : Bool\n    @destroyed : Bool\n\n    def errors : Array(String)\n      @errors\n    end\n\n    def initialize(_attrs = {} of String => String)\n      @id = 0_i64\n      @errors = [] of String\n      @persisted = false\n      @destroyed = false\n    end\n\n    def self.table_name : String\n      raise NotImplementedError.new(\"#{name}.table_name must be overridden\")\n    end\n\n    def self.schema_columns : Array(Symbol)\n      raise NotImplementedError.new(\"#{name}.schema_columns must be overridden\")\n    end\n\n    def self.instantiate(_row)\n      raise NotImplementedError.new(\"#{name}.instantiate must be overridden\")\n    end\n\n    def self._adapter_find_by_id(id : Int64)\n      row = ::ActiveRecord.adapter.find(table_name.not_nil!, id)\n      return if row.nil?\n      instantiate(row)\n    end\n\n    def self._adapter_all\n      ::ActiveRecord.adapter.all(table_name.not_nil!).map { |row| instantiate(row) }\n    end\n\n    def _adapter_insert : Int64\n    end\n\n    def _adapter_update : Nil\n    end\n\n    def _adapter_delete : Nil\n    end\n\n    def self._adapter_count : Int64\n      ::ActiveRecord.adapter.count(table_name.not_nil!)\n    end\n\n    def self._adapter_exists_by_id?(id : Int64) : Bool\n      ::ActiveRecord.adapter.exists?(table_name.not_nil!, id)\n    end\n\n    def self._adapter_truncate : Nil\n      ::ActiveRecord.adapter.truncate(table_name.not_nil!)\n    end\n\n    def _adapter_reload\n    end\n\n    def attributes\n      {} of String => String\n    end\n\n    def [](_name)\n      raise NotImplementedError.new(\"[] must be overridden by subclass\")\n    end\n\n    def []=(_name, _value)\n      raise NotImplementedError.new(\"[]= must be overridden by subclass\")\n    end\n\n    def assign_from_row(_row)\n    end\n\n    def dom_prefix : String\n      raise NotImplementedError.new(\"dom_prefix must be overridden by subclass\")\n    end\n\n    def persisted? : Bool\n      @persisted\n    end\n\n    def new_record? : Bool\n      !(@persisted)\n    end\n\n    def destroyed? : Bool\n      @destroyed\n    end\n\n    def mark_persisted! : Nil\n      @persisted = true\n      @destroyed = false\n    end\n\n    def self.all\n      _adapter_all\n    end\n\n    def self.find(id : Int64)\n      result = _adapter_find_by_id(id)\n      raise RecordNotFound.new(\"Couldn't find #{name} with id=#{id}\") if result.nil?\n      result\n    end\n\n    def self.find_by(conditions)\n      rows = ::ActiveRecord.adapter.where(table_name.not_nil!, conditions.to_h)\n      return if rows.size == 0_i64\n      instantiate(rows[0_i64])\n    end\n\n    def self.where(conditions)\n      ::ActiveRecord.adapter.where(table_name.not_nil!, conditions.to_h).map { |row| instantiate(row) }\n    end\n\n    def self.count : Int64\n      _adapter_count.not_nil!\n    end\n\n    def self.exists?(id : Int64) : Bool\n      _adapter_exists_by_id?(id)\n    end\n\n    def self.destroy_all\n      records = all\n      records.each { |r| r.destroy }\n      records\n    end\n\n    def self.create(attrs = {} of String => String)\n      instance = new(attrs)\n      instance.save.not_nil!\n      instance\n    end\n\n    def self.create!(attrs = {} of String => String)\n      instance = new(attrs)\n      if instance.save.not_nil!\n        nil\n      else\n        raise RecordInvalid.new(instance)\n      end\n      instance\n    end\n\n    def self.last\n      records = all\n      if records.empty?\n        nil\n      else\n        records[-1_i64]\n      end\n    end\n\n    def save : Bool\n      before_validation\n      ok = valid?.not_nil!\n      after_validation\n      if ok\n        nil\n      else\n        return false\n      end\n\n      before_save\n      if new_record?.not_nil!\n        before_create\n        fill_timestamps(true)\n        @id = _adapter_insert.not_nil!\n        @persisted = true\n        after_create\n        after_create_commit\n      else\n        before_update\n        fill_timestamps(false)\n        _adapter_update\n        after_update\n        after_update_commit\n      end\n      after_save\n      after_save_commit\n      after_commit\n      true\n    end\n\n    def save!\n      if save.not_nil!\n        nil\n      else\n        raise RecordInvalid.new(self)\n      end\n      self\n    end\n\n    def destroy\n      if persisted?.not_nil!\n        nil\n      else\n        return self\n      end\n      before_destroy\n      _adapter_delete\n      @persisted = false\n      @destroyed = true\n      after_destroy\n      after_destroy_commit\n      after_commit\n      self\n    end\n\n    def reload\n      _adapter_reload\n      self\n    end\n\n    def before_validation : Nil\n    end\n\n    def after_validation : Nil\n    end\n\n    def before_save : Nil\n    end\n\n    def after_save : Nil\n    end\n\n    def before_create : Nil\n    end\n\n    def after_create : Nil\n    end\n\n    def before_update : Nil\n    end\n\n    def after_update : Nil\n    end\n\n    def before_destroy : Nil\n    end\n\n    def after_destroy : Nil\n    end\n\n    def after_commit : Nil\n    end\n\n    def after_create_commit : Nil\n    end\n\n    def after_update_commit : Nil\n    end\n\n    def after_destroy_commit : Nil\n    end\n\n    def after_save_commit : Nil\n    end\n\n    def after_touch : Nil\n    end\n\n    def validate : Nil\n    end\n\n    def fill_timestamps(creating : Bool) : Nil\n      cols = self.class.schema_columns\n      now = Time.utc.to_rfc3339.not_nil!\n      self[:updated_at] = now if cols.includes?(:updated_at)\n      self[:created_at] = now if creating && cols.includes?(:created_at) && self[:created_at].nil?\n    end\n\n    def valid? : Bool\n      @errors = [] of String\n      validate\n      @errors.not_nil!.empty?\n    end\n  end\nend\n\nmodule ActiveRecord\n  property adapter : ActiveRecord::AdapterInterface?\nend\n"},{"path":"src/app.cr","content":"# Generated by Roundhouse — Crystal entry point.\n# Requires the framework runtime in dependency order\n# (Crystal processes `include` at parse time, so modules\n# included by classes must be loaded first), then app code\n# alphabetically.\n\nrequire \"./param_value\"\nrequire \"./inflector\"\nrequire \"./active_record_base\"\nrequire \"./errors\"\nrequire \"./router\"\nrequire \"./action_controller_base\"\nrequire \"./view_helpers\"\nrequire \"./broadcasts\"\nrequire \"./cable\"\nrequire \"./controllers/application_controller\"\nrequire \"./controllers/articles_controller\"\nrequire \"./controllers/comments_controller\"\nrequire \"./db\"\nrequire \"./fixtures/articles\"\nrequire \"./fixtures/comments\"\nrequire \"./flash\"\nrequire \"./http\"\nrequire \"./importmap\"\nrequire \"./json_builder\"\nrequire \"./models/application_record\"\nrequire \"./models/article\"\nrequire \"./models/article_params\"\nrequire \"./models/article_row\"\nrequire \"./models/comment\"\nrequire \"./models/comment_params\"\nrequire \"./models/comment_row\"\nrequire \"./route_helpers\"\nrequire \"./routes\"\nrequire \"./schema\"\nrequire \"./seeds\"\nrequire \"./server\"\nrequire \"./session\"\nrequire \"./test_helper\"\nrequire \"./test_setup\"\nrequire \"./test_support\"\nrequire \"./views/articles/_article\"\nrequire \"./views/articles/_article_json\"\nrequire \"./views/articles/_form\"\nrequire \"./views/articles/edit\"\nrequire \"./views/articles/index\"\nrequire \"./views/articles/index_json\"\nrequire \"./views/articles/new\"\nrequire \"./views/articles/show\"\nrequire \"./views/articles/show_json\"\nrequire \"./views/comments/_comment\"\nrequire \"./views/layouts/application\"\nrequire \"./views/layouts/mailer\"\n"},{"path":"src/broadcasts.cr","content":"# Broadcasts — turbo-stream emit bridge.\n#\n# The model lowerer's `broadcasts_to` expansion produces calls like\n# `Broadcasts.prepend(stream: \"x\", target: \"y\", html: \"...\")` from\n# inside model callback methods (`after_create`, `after_update`,\n# etc.). This shim adapts those calls to the cable broadcaster.\n#\n# State is held in a module-level Array so tests can assert on what\n# was emitted; production also forwards each entry through the\n# installed broadcaster (typically a WebSocket fan-out via Cable).\n\nmodule Broadcasts\n  alias BroadcasterFn = Proc(String, String, Nil)\n\n  @@broadcaster : BroadcasterFn? = nil\n  @@log = [] of NamedTuple(action: String, stream: String, target: String, html: String)\n\n  # Production server installs a broadcaster that pumps fragments\n  # over the cable; tests / CLI runs leave it unset and calls\n  # become silent no-ops (the in-memory log is still populated so\n  # tests can inspect emit ordering).\n  def self.install_broadcaster(fn : BroadcasterFn?) : Nil\n    @@broadcaster = fn\n  end\n\n  def self.reset_log! : Nil\n    @@log.clear\n  end\n\n  def self.log\n    @@log.dup\n  end\n\n  def self.append(*, stream : String, target : String, html : String) : Nil\n    record(\"append\", stream, target, html)\n  end\n\n  def self.prepend(*, stream : String, target : String, html : String) : Nil\n    record(\"prepend\", stream, target, html)\n  end\n\n  def self.replace(*, stream : String, target : String, html : String) : Nil\n    record(\"replace\", stream, target, html)\n  end\n\n  def self.remove(*, stream : String, target : String) : Nil\n    record(\"remove\", stream, target, \"\")\n  end\n\n  private def self.record(action : String, stream : String, target : String, html : String) : Nil\n    @@log << {action: action, stream: stream, target: target, html: html}\n    if fn = @@broadcaster\n      fn.call(stream, render_fragment(action, target, html))\n    end\n    nil\n  end\n\n  # Compose the `<turbo-stream>` fragment. Pure; doesn't touch the\n  # log — used by tests and transport layers that need to ship the\n  # fragment over the wire.\n  def self.render_fragment(action : String, target : String, html : String = \"\") : String\n    if action == \"remove\"\n      %(<turbo-stream action=\"remove\" target=\"#{target}\"></turbo-stream>)\n    else\n      %(<turbo-stream action=\"#{action}\" target=\"#{target}\"><template>#{html}</template></turbo-stream>)\n    end\n  end\nend\n"},{"path":"src/cable.cr","content":"# Roundhouse Crystal cable runtime.\n#\n# Action Cable WebSocket + Turbo Streams broadcaster. Mirrors\n# runtime/rust/cable.rs + runtime/python/cable.py — same wire\n# format (actioncable-v1-json), same partial-renderer registry,\n# same per-channel subscriber map.\n#\n# Uses Crystal's stdlib `HTTP::WebSocket`; no extra shard.\n\nrequire \"base64\"\nrequire \"http/web_socket\"\nrequire \"json\"\n\nmodule Roundhouse\n  module Cable\n    # ── Partial-renderer registry ───────────────────────────────\n\n    @@partial_renderers : Hash(String, Proc(Int64, String)) = {} of String => Proc(Int64, String)\n\n    def self.register_partial(type_name : String, fn : Proc(Int64, String)) : Nil\n      @@partial_renderers[type_name] = fn\n    end\n\n    def self.render_partial(type_name : String, id : Int64) : String\n      fn = @@partial_renderers[type_name]?\n      return fn.call(id) if fn\n      \"<div>#{type_name} ##{id}</div>\"\n    end\n\n    # ── Turbo Streams rendering ─────────────────────────────────\n\n    def self.turbo_stream_html(action : String, target : String, content : String) : String\n      if content.empty?\n        %(<turbo-stream action=\"#{action}\" target=\"#{target}\"></turbo-stream>)\n      else\n        %(<turbo-stream action=\"#{action}\" target=\"#{target}\"><template>#{content}</template></turbo-stream>)\n      end\n    end\n\n    private def self.dom_id_for(table : String, id : Int64) : String\n      singular = table.ends_with?('s') ? table[0, table.size - 1] : table\n      \"#{singular}_#{id}\"\n    end\n\n    # ── Broadcast helpers ───────────────────────────────────────\n\n    def self.broadcast_replace_to(table : String, id : Int64, type_name : String, channel : String, target : String) : Nil\n      t = target.empty? ? dom_id_for(table, id) : target\n      html = render_partial(type_name, id)\n      dispatch(channel, turbo_stream_html(\"replace\", t, html))\n    end\n\n    def self.broadcast_prepend_to(table : String, id : Int64, type_name : String, channel : String, target : String) : Nil\n      t = target.empty? ? table : target\n      html = render_partial(type_name, id)\n      dispatch(channel, turbo_stream_html(\"prepend\", t, html))\n    end\n\n    def self.broadcast_append_to(table : String, id : Int64, type_name : String, channel : String, target : String) : Nil\n      t = target.empty? ? table : target\n      html = render_partial(type_name, id)\n      dispatch(channel, turbo_stream_html(\"append\", t, html))\n    end\n\n    def self.broadcast_remove_to(table : String, id : Int64, channel : String, target : String) : Nil\n      t = target.empty? ? dom_id_for(table, id) : target\n      dispatch(channel, turbo_stream_html(\"remove\", t, \"\"))\n    end\n\n    # ── Subscriber registry + dispatch ──────────────────────────\n\n    # channel name → list of {ws, identifier} pairs. The identifier\n    # is the raw subscribe-frame JSON echoed back on every broadcast\n    # so Turbo routes the frame to the correct stream-source.\n    @@subscribers : Hash(String, Array(Tuple(HTTP::WebSocket, String))) = {} of String => Array(Tuple(HTTP::WebSocket, String))\n    @@subscribers_mutex = Mutex.new\n\n    private def self.dispatch(channel : String, html : String) : Nil\n      subs = @@subscribers_mutex.synchronize { (@@subscribers[channel]? || ([] of Tuple(HTTP::WebSocket, String))).dup }\n      subs.each do |(ws, identifier)|\n        msg = {\"type\" => \"message\", \"identifier\" => identifier, \"message\" => html}.to_json\n        begin\n          ws.send(msg)\n        rescue\n          # socket may have closed between our snapshot and now —\n          # cleanup happens on the handler side.\n        end\n      end\n    end\n\n    # ── WebSocket handler ───────────────────────────────────────\n\n    # Route /cable — upgrades the connection, runs the\n    # actioncable-v1-json flow. Called from server.cr.\n    def self.handle(context : HTTP::Server::Context) : Nil\n      # Negotiate the subprotocol — Turbo's client requires it.\n      wanted = context.request.headers[\"Sec-WebSocket-Protocol\"]? || \"\"\n      if !wanted.includes?(\"actioncable-v1-json\")\n        context.response.status_code = 400\n        context.response.print \"unsupported subprotocol\"\n        return\n      end\n      context.response.headers[\"Sec-WebSocket-Protocol\"] = \"actioncable-v1-json\"\n\n      ws_handler = HTTP::WebSocketHandler.new do |ws, _ctx|\n        run_socket(ws)\n      end\n      ws_handler.call(context)\n    end\n\n    private def self.run_socket(ws : HTTP::WebSocket) : Nil\n      sub_entries = [] of Tuple(String, Tuple(HTTP::WebSocket, String))\n\n      ws.send({\"type\" => \"welcome\"}.to_json)\n\n      # Ping every 3 seconds on a background fiber.\n      ping_fiber = spawn do\n        loop do\n          sleep 3.seconds\n          break if ws.closed?\n          begin\n            ws.send({\"type\" => \"ping\", \"message\" => Time.utc.to_unix}.to_json)\n          rescue\n            break\n          end\n        end\n      end\n\n      ws.on_message do |msg|\n        next unless payload = JSON.parse(msg).as_h?\n        next unless payload[\"command\"]? == \"subscribe\"\n        identifier = payload[\"identifier\"]?.try(&.as_s)\n        next unless identifier\n        channel = decode_channel(identifier)\n        next unless channel\n        entry = {ws, identifier}\n        @@subscribers_mutex.synchronize do\n          (@@subscribers[channel] ||= [] of Tuple(HTTP::WebSocket, String)) << entry\n        end\n        sub_entries << {channel, entry}\n        ws.send({\"type\" => \"confirm_subscription\", \"identifier\" => identifier}.to_json)\n      rescue\n        # malformed JSON etc — silently drop\n      end\n\n      ws.on_close do |_code, _reason|\n        @@subscribers_mutex.synchronize do\n          sub_entries.each do |(channel, entry)|\n            if list = @@subscribers[channel]?\n              list.delete(entry)\n              @@subscribers.delete(channel) if list.empty?\n            end\n          end\n        end\n      end\n    end\n\n    # Recover the channel name from Turbo's signed_stream_name.\n    # Identifier is a JSON object with\n    #   {\"channel\":\"Turbo::StreamsChannel\",\n    #    \"signed_stream_name\":\"<base64>--<digest>\"}\n    # The base64 prefix decodes to a JSON-encoded channel name.\n    private def self.decode_channel(identifier : String) : String?\n      id_data = JSON.parse(identifier).as_h?\n      return nil unless id_data\n      signed = id_data[\"signed_stream_name\"]?.try(&.as_s)\n      return nil unless signed\n      b64 = signed.split(\"--\", 2).first\n      decoded = Base64.decode_string(b64)\n      JSON.parse(decoded).as_s\n    rescue\n      nil\n    end\n\n    # Stub kept for compatibility with existing server.cr wiring.\n    def self.broadcast(channel : String, body : String) : Nil\n      dispatch(channel, body)\n    end\n  end\nend\n"},{"path":"src/controllers/application_controller.cr","content":"class ApplicationController < ActionController::Base\nend\n"},{"path":"src/controllers/articles_controller.cr","content":"class ArticlesController < ApplicationController\n  @articles : Array(Article)?\n  @article : Article?\n\n  def process_action(action_name : Symbol) : Nil\n    case action_name\n    when :index\n      index\n    when :show\n      show\n    when :new\n      new_action\n    when :edit\n      edit\n    when :create\n      create\n    when :update\n      update\n    when :destroy\n      destroy\n    end\n  end\n\n  def index : Nil\n    stmt = Roundhouse::Db.prepare(\"SELECT id, body, created_at, title, updated_at FROM articles\" + \" ORDER BY created_at DESC\")\n    results = [] of Article\n    while Roundhouse::Db.step?(stmt)\n      results << Article.from_stmt(stmt)\n    end\n    Roundhouse::Db.finalize(stmt)\n    __comments_ids = results.map { |a| a.id.not_nil! }\n    __comments_stmt = Roundhouse::Db.prepare(\"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments WHERE article_id IN (\" + Roundhouse::Db.escape_int_list(__comments_ids) + \")\")\n    __comments_loaded = [] of Comment\n    while Roundhouse::Db.step?(__comments_stmt)\n      __comments_loaded << Comment.from_stmt(__comments_stmt)\n    end\n    Roundhouse::Db.finalize(__comments_stmt)\n    results.each { |a| __comments_group = [] of Comment\n    __comments_loaded.each { |r| __comments_group << r if r.article_id.not_nil! == a.id.not_nil! }\n    a._preload_comments(__comments_group) }\n    @articles = results\n    if request_format.not_nil! == :json\n      render(Views::Articles.index_json(@articles.not_nil!), content_type: \"application/json\")\n    else\n      render(Views::Articles.index(@articles.not_nil!, @flash.not_nil![:notice], @flash.not_nil![:alert]))\n    end\n  end\n\n  def show : Nil\n    @article = Article.find(@params.not_nil!.fetch(\"id\", \"0\").to_s.to_i)\n    if request_format.not_nil! == :json\n      render(Views::Articles.show_json(@article.not_nil!), content_type: \"application/json\")\n    else\n      render(Views::Articles.show(@article.not_nil!, @flash.not_nil![:notice], @flash.not_nil![:alert]))\n    end\n  end\n\n  def new_action : Nil\n    @article = Article.new\n    render(Views::Articles.new(@article.not_nil!, @flash.not_nil![:notice], @flash.not_nil![:alert]))\n  end\n\n  def edit : Nil\n    @article = Article.find(@params.not_nil!.fetch(\"id\", \"0\").to_s.to_i)\n    render(Views::Articles.edit(@article.not_nil!, @flash.not_nil![:notice], @flash.not_nil![:alert]))\n  end\n\n  def create : Nil\n    @article = Article.from_params(article_params)\n    if @article.not_nil!.save.not_nil!\n      if request_format.not_nil! == :json\n        render(Views::Articles.show_json(@article.not_nil!), status: :created, location: RouteHelpers.article_path(@article.not_nil!.id.not_nil!), content_type: \"application/json\")\n      else\n        redirect_to(RouteHelpers.article_path(@article.not_nil!.id.not_nil!), notice: \"Article was successfully created.\")\n      end\n    else\n      render(Views::Articles.new(@article.not_nil!, @flash.not_nil![:notice], @flash.not_nil![:alert]), status: :unprocessable_content)\n    end\n  end\n\n  def update : Nil\n    @article = Article.find(@params.not_nil!.fetch(\"id\", \"0\").to_s.to_i)\n    if @article.not_nil!.update(article_params)\n      if request_format.not_nil! == :json\n        render(Views::Articles.show_json(@article.not_nil!), status: :ok, location: RouteHelpers.article_path(@article.not_nil!.id.not_nil!), content_type: \"application/json\")\n      else\n        redirect_to(RouteHelpers.article_path(@article.not_nil!.id.not_nil!), notice: \"Article was successfully updated.\", status: :see_other)\n      end\n    else\n      render(Views::Articles.edit(@article.not_nil!, @flash.not_nil![:notice], @flash.not_nil![:alert]), status: :unprocessable_content)\n    end\n  end\n\n  def destroy : Nil\n    @article = Article.find(@params.not_nil!.fetch(\"id\", \"0\").to_s.to_i)\n    @article.not_nil!.destroy\n    if request_format.not_nil! == :json\n      head(:no_content, content_type: \"application/json\")\n    else\n      redirect_to(RouteHelpers.articles_path.not_nil!, notice: \"Article was successfully destroyed.\", status: :see_other)\n    end\n  end\n\n  def article_params : ArticleParams\n    ArticleParams.from_raw(@params.not_nil!)\n  end\nend\n"},{"path":"src/controllers/comments_controller.cr","content":"class CommentsController < ApplicationController\n  @article : Article?\n  @comment : Comment?\n\n  def process_action(action_name : Symbol) : Nil\n    case action_name\n    when :create\n      create\n    when :destroy\n      destroy\n    end\n  end\n\n  def create : Nil\n    @article = Article.find(@params.not_nil!.fetch(\"article_id\", \"0\").to_s.to_i)\n    @comment = Comment.from_params(comment_params)\n    @comment.not_nil!.article_id = @article.not_nil!.id.not_nil!\n    if @comment.not_nil!.save.not_nil!\n      redirect_to(RouteHelpers.article_path(@article.not_nil!.id.not_nil!), notice: \"Comment was successfully created.\")\n    else\n      redirect_to(RouteHelpers.article_path(@article.not_nil!.id.not_nil!), alert: \"Could not create comment.\")\n    end\n  end\n\n  def destroy : Nil\n    @article = Article.find(@params.not_nil!.fetch(\"article_id\", \"0\").to_s.to_i)\n    @comment = Comment.find(@params.not_nil!.fetch(\"id\", \"0\").to_s.to_i)\n    if @comment.not_nil!.article_id.not_nil! != @article.not_nil!.id.not_nil!\n      head(:not_found)\n      return\n    end\n    @comment.not_nil!.destroy\n    redirect_to(RouteHelpers.article_path(@article.not_nil!.id.not_nil!), notice: \"Comment was successfully deleted.\")\n  end\n\n  def comment_params : CommentParams\n    CommentParams.from_raw(@params.not_nil!)\n  end\nend\n"},{"path":"src/db.cr","content":"# Roundhouse Crystal DB runtime — sqlite primitive layer plus the\n# `ActiveRecord.adapter` plug-in.\n#\n# Three responsibilities:\n#   1. `Roundhouse::Db` — owns the sqlite3 connection. `open_production_db`\n#      is called from `Roundhouse::Server.start`; `setup_test_db` resets\n#      the connection between specs.\n#   2. `Roundhouse::ActiveRecordAdapter` — abstract base pinning the 9-\n#      method contract `runtime/ruby/active_record/base.rb` calls\n#      (`all`, `find`, `where`, `count`, `exists?`, `insert`, `update`,\n#      `delete`, `truncate`). Polymorphic slot so production sqlite,\n#      test in-memory, and future libsql/D1 implementations all plug\n#      into the same `ActiveRecord.adapter` setter.\n#   3. `Roundhouse::SqliteAdapter` — concrete sqlite implementation.\n#      Server boot assigns an instance to `ActiveRecord.adapter`.\n#\n# The `module ActiveRecord ... end` extension at the bottom adds the\n# `.adapter` getter/setter that the Ruby source's\n# `class << self; attr_accessor :adapter; end` would have produced —\n# the runtime_loader transpile pipeline doesn't yet expose\n# module-level attr_accessors on the metaclass, so we declare them\n# here to keep `ActiveRecord.adapter = X` and `ActiveRecord.adapter.X`\n# resolvable.\n\nrequire \"sqlite3\"\n\nmodule Roundhouse\n  module Db\n    @@db : DB::Database? = nil\n\n    # Per-prepared-statement state: the open ResultSet plus the most\n    # recently materialized row. step? advances the cursor and snapshots\n    # the row into `current`; column_int/column_text then index into\n    # the snapshot. crystal-db's ResultSet is sequential-read-only\n    # (`rs.read` consumes one column), so materializing to an array\n    # is the way to keep `column_*(stmt, i)` random-access.\n    class StmtEntry\n      getter result_set : DB::ResultSet\n      property current : Array(DB::Any)?\n\n      def initialize(@result_set : DB::ResultSet)\n        @current = nil\n      end\n    end\n\n    @@statements = {} of Int64 => StmtEntry\n    @@next_id : Int64 = 0_i64\n    @@last_insert_rowid : Int64 = 0_i64\n    @@changes : Int64 = 0_i64\n\n    def self.setup_test_db(schema_sql : String) : Nil\n      reset_statements\n      if old = @@db\n        old.close\n      end\n      db = DB.open(\"sqlite3::memory:\")\n      schema_sql.split(\";\\n\").each do |chunk|\n        stmt = chunk.strip\n        next if stmt.empty?\n        db.exec(stmt)\n      end\n      @@db = db\n    end\n\n    def self.conn : DB::Database\n      @@db.not_nil!\n    end\n\n    def self.open_production_db(path : String, schema_sql : String) : Nil\n      reset_statements\n      if old = @@db\n        old.close\n      end\n      dir = File.dirname(path)\n      Dir.mkdir_p(dir) unless Dir.exists?(dir)\n      db = DB.open(\"sqlite3://#{path}\")\n      count = db.query_one(\n        \"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\",\n        as: Int64,\n      )\n      if count == 0\n        schema_sql.split(\";\\n\").each do |chunk|\n          stmt = chunk.strip\n          next if stmt.empty?\n          db.exec(stmt)\n        end\n      end\n      @@db = db\n    end\n\n    # ── Low-level prepare/step/column API ────────────────────────\n    #\n    # Mirrors `runtime/spinel/db.rb` and `runtime/typescript/db.ts`\n    # verbatim. Model adapter methods (`_adapter_find`, `_adapter_save`,\n    # etc.) emitted by `src/lower/model_to_library/adapter_emit.rs`\n    # compose inlined SQL via `escape_int`/`escape_string` and dispatch\n    # against this surface. Per-statement state lives in the\n    # `@@statements` table; opaque Int64 stmt ids index into it.\n\n    # Run any one-shot DDL/INSERT/UPDATE/DELETE. Captures the\n    # last_insert_rowid + changes so subsequent calls to those\n    # accessors return the most recent values (the same shape as the\n    # TS shim: `Db.exec(insert_sql)` followed by `Db.last_insert_rowid`).\n    def self.exec(sql : String) : Nil\n      result = conn.exec(sql)\n      @@last_insert_rowid = result.last_insert_id\n      @@changes = result.rows_affected\n    end\n\n    # Prepare a SELECT, returning an opaque integer handle. Subsequent\n    # `step?` / `column_int` / `column_text` / `finalize` calls take it\n    # by reference. Per-process stmt-id sequence; reset across\n    # `setup_test_db` / `open_production_db` so test runs start from 1.\n    def self.prepare(sql : String) : Int64\n      rs = conn.query(sql)\n      @@next_id += 1\n      @@statements[@@next_id] = StmtEntry.new(rs)\n      @@next_id\n    end\n\n    # Advance the cursor on a prepared statement. Returns true and\n    # snapshots the current row into the stmt entry on success; false\n    # (with the snapshot cleared) when the result set is exhausted.\n    def self.step?(stmt_id : Int64) : Bool\n      entry = @@statements[stmt_id]\n      if entry.result_set.move_next\n        col_count = entry.result_set.column_count\n        row = Array(DB::Any).new(col_count) do\n          entry.result_set.read.as(DB::Any)\n        end\n        entry.current = row\n        true\n      else\n        entry.current = nil\n        false\n      end\n    end\n\n    # Read an integer column at zero-based index from the row most\n    # recently snapshotted by `step?`. NULL coerces to 0 (matches the\n    # TS shim and `runtime/spinel/db.rb`); non-Int variants of `DB::Any`\n    # coerce via `to_i64`.\n    def self.column_int(stmt_id : Int64, i : Int64) : Int64\n      entry = @@statements[stmt_id]\n      row = entry.current.not_nil!\n      v = row[i]\n      case v\n      when Nil     then 0_i64\n      when Int64   then v\n      when Int32   then v.to_i64\n      when Float64 then v.to_i64\n      when Float32 then v.to_i64\n      when Bool    then v ? 1_i64 : 0_i64\n      when String  then v.to_i64? || 0_i64\n      else              0_i64\n      end\n    end\n\n    # Read a text column at zero-based index. NULL coerces to \"\"\n    # (matches the TS shim — lowered code compares strings, never\n    # against nil). Bytes/numeric variants stringify via `to_s`.\n    def self.column_text(stmt_id : Int64, i : Int64) : String\n      entry = @@statements[stmt_id]\n      row = entry.current.not_nil!\n      v = row[i]\n      case v\n      when Nil    then \"\"\n      when String then v\n      else             v.to_s\n      end\n    end\n\n    # Release the underlying ResultSet and drop the stmt-table entry.\n    # Idempotent — finalize on an unknown stmt id is a no-op (mirrors\n    # the TS shim).\n    def self.finalize(stmt_id : Int64) : Nil\n      entry = @@statements[stmt_id]?\n      return if entry.nil?\n      entry.result_set.close\n      @@statements.delete(stmt_id)\n    end\n\n    def self.last_insert_rowid : Int64\n      @@last_insert_rowid\n    end\n\n    def self.changes : Int64\n      @@changes\n    end\n\n    # SQL-quote a string value. Single-quotes are doubled per sqlite's\n    # string-literal escape rule; no other byte transforms (the lowered\n    # adapter emit never inlines binary blobs).\n    def self.escape_string(s : String) : String\n      \"'\" + s.gsub(\"'\", \"''\") + \"'\"\n    end\n\n    # Render an Integer for SQL inlining. Matches the TS shim's\n    # truncate-to-int semantics; the Crystal type system already\n    # constrains the input to Int, so there's no parse-or-zero\n    # fallback.\n    def self.escape_int(n : Int) : String\n      n.to_s\n    end\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. Generic over\n    # the element type so it accepts Array(Int32)/Array(Int64) alike.\n    def self.escape_int_list(ids : Array(T)) : String forall T\n      return \"NULL\" if ids.empty?\n\n      ids.map(&.to_s).join(\", \")\n    end\n\n    # SQLite stores booleans as 0/1 integers (no native bool type) —\n    # mirrors the Ruby/spinel sibling shims.\n    def self.escape_bool(b : Bool) : String\n      b ? \"1\" : \"0\"\n    end\n\n    # Read a boolean column. SQLite returns 0/1 (integer), widen to Bool.\n    # Nulls coerce to false.\n    def self.column_bool(stmt : Int64, idx : Int) : Bool\n      column_int(stmt, idx) != 0\n    end\n\n    # Drain in-flight ResultSets before swapping the underlying\n    # connection. Without this, `setup_test_db` between specs would\n    # leak ResultSets bound to a closed connection.\n    private def self.reset_statements : Nil\n      @@statements.each_value do |entry|\n        entry.result_set.close rescue nil\n      end\n      @@statements.clear\n      @@next_id = 0_i64\n      @@last_insert_rowid = 0_i64\n      @@changes = 0_i64\n    end\n  end\n\n  # Abstract adapter contract — the 9 methods `ActiveRecord::Base`\n  # (transpiled from runtime/ruby/active_record/base.rb) calls\n  # against `ActiveRecord.adapter`. Every concrete adapter (sqlite\n  # production, in-memory framework-test, future libsql/D1) inherits\n  # and implements these. Returns are intentionally untyped here —\n  # row shape (`Hash(String, DB::Any)` for sqlite, `Hash(String, _)`\n  # for the in-memory adapter) varies by implementation, but per-\n  # call-site Crystal inference threads the actual concrete type\n  # through to `instantiate(row)`.\n  #\n  # Test-helper methods (`create_table`, `drop_table`, `reset_all!`,\n  # `schema`) are NOT in the abstract — they're called directly on\n  # test-only adapter mocks (when present), never via the\n  # `ActiveRecord.adapter` slot.\n  abstract class ActiveRecordAdapter\n    abstract def all(table_name : String)\n    abstract def find(table_name : String, id)\n    abstract def where(table_name : String, conditions : Hash(Symbol, _))\n    abstract def count(table_name : String) : Int64\n    abstract def exists?(table_name : String, id) : Bool\n    abstract def insert(table_name : String, attributes : Hash(Symbol, _)) : Int64\n    abstract def update(table_name : String, id, attributes : Hash(Symbol, _)) : Nil\n    abstract def delete(table_name : String, id) : Nil\n    abstract def truncate(table_name : String) : Nil\n  end\n\n  # Concrete sqlite-backed adapter. Method names + arities match the\n  # Ruby surface; row results come back as `Hash(String, DB::Any)`\n  # matching Crystal's crystal-db return shape.\n  class SqliteAdapter < ActiveRecordAdapter\n    private def conn\n      Roundhouse::Db.conn\n    end\n\n    def all(table_name : String)\n      rows = [] of Hash(String, DB::Any)\n      conn.query(\"SELECT * FROM #{table_name}\") do |rs|\n        rs.column_count.times { rs.column_name(0) } # warm up metadata\n        names = (0...rs.column_count).map { |i| rs.column_name(i) }\n        rs.each do\n          h = {} of String => DB::Any\n          names.each_with_index { |n, i| h[n] = rs.read }\n          rows << h\n        end\n      end\n      rows\n    end\n\n    def find(table_name : String, id)\n      row = nil\n      conn.query(\"SELECT * FROM #{table_name} WHERE id = ? LIMIT 1\", id) do |rs|\n        names = (0...rs.column_count).map { |i| rs.column_name(i) }\n        rs.each do\n          h = {} of String => DB::Any\n          names.each_with_index { |n, i| h[n] = rs.read }\n          row = h\n        end\n      end\n      row\n    end\n\n    def where(table_name : String, conditions : Hash(Symbol, _))\n      keys = conditions.keys\n      rows = [] of Hash(String, DB::Any)\n      return rows if keys.empty?\n      where_clause = keys.map { |k| \"#{k} = ?\" }.join(\" AND \")\n      args = keys.map { |k| conditions[k].as(DB::Any) }\n      conn.query(\"SELECT * FROM #{table_name} WHERE #{where_clause}\", args: args) do |rs|\n        names = (0...rs.column_count).map { |i| rs.column_name(i) }\n        rs.each do\n          h = {} of String => DB::Any\n          names.each_with_index { |n, i| h[n] = rs.read }\n          rows << h\n        end\n      end\n      rows\n    end\n\n    def count(table_name : String) : Int64\n      conn.query_one(\"SELECT COUNT(*) FROM #{table_name}\", as: Int64)\n    end\n\n    def exists?(table_name : String, id) : Bool\n      n = conn.query_one(\n        \"SELECT COUNT(*) FROM #{table_name} WHERE id = ?\",\n        id,\n        as: Int64,\n      )\n      n > 0\n    end\n\n    def insert(table_name : String, attributes : Hash(Symbol, _)) : Int64\n      keys = attributes.keys\n      cols = keys.map(&.to_s).join(\", \")\n      placeholders = ([\"?\"] * keys.size).join(\", \")\n      args = keys.map { |k| attributes[k].as(DB::Any) }\n      conn.exec(\"INSERT INTO #{table_name} (#{cols}) VALUES (#{placeholders})\", args: args)\n      conn.query_one(\"SELECT last_insert_rowid()\", as: Int64)\n    end\n\n    def update(table_name : String, id, attributes : Hash(Symbol, _)) : Nil\n      keys = attributes.keys\n      return if keys.empty?\n      sets = keys.map { |k| \"#{k} = ?\" }.join(\", \")\n      args = keys.map { |k| attributes[k].as(DB::Any) } + [id.as(DB::Any)]\n      conn.exec(\"UPDATE #{table_name} SET #{sets} WHERE id = ?\", args: args)\n    end\n\n    def delete(table_name : String, id) : Nil\n      conn.exec(\"DELETE FROM #{table_name} WHERE id = ?\", id)\n    end\n\n    def truncate(table_name : String) : Nil\n      conn.exec(\"DELETE FROM #{table_name}\")\n    end\n  end\nend\n\n# Module-level attr_accessor analog. The Ruby source declares\n# `class << self; attr_accessor :adapter; end` inside `module\n# ActiveRecord`; the transpiler doesn't yet emit module-metaclass\n# accessors. Re-opening the module here adds the missing surface.\n#\n# Slot is typed as the abstract base so any adapter implementation\n# (production sqlite, framework-test in-memory, future libsql/D1)\n# can plug in via `ActiveRecord.adapter = <impl>`.\nmodule ActiveRecord\n  # Phantom class the analyzer registers for the adapter slot type;\n  # `runtime/ruby/active_record/base.rbs` declares the slot as\n  # `() -> ActiveRecord::AdapterInterface`. Each target maps the name\n  # onto its concrete adapter type — Crystal points it at the abstract\n  # base in `Roundhouse::ActiveRecordAdapter` so the transpiled\n  # `property adapter : ActiveRecord::AdapterInterface?` resolves.\n  alias AdapterInterface = Roundhouse::ActiveRecordAdapter\n\n  @@adapter : Roundhouse::ActiveRecordAdapter? = nil\n\n  def self.adapter : Roundhouse::ActiveRecordAdapter\n    @@adapter.not_nil!\n  end\n\n  def self.adapter=(value : Roundhouse::ActiveRecordAdapter) : Roundhouse::ActiveRecordAdapter\n    @@adapter = value\n  end\nend\n"},{"path":"src/errors.cr","content":"# Generated from runtime/ruby/active_record/errors.rb at app emit time.\n# Do not edit by hand — edit the source `.rb` and re-run emit.\n\nmodule ActiveRecord\n  class RecordNotFound < Exception\n    def initialize(message : String = \"ActiveRecord::RecordNotFound\") : Nil\n      super(message)\n    end\n  end\nend\n\nmodule ActiveRecord\n  class RecordInvalid < Exception\n    property record : ActiveRecord::Base\n\n    def initialize(record : ActiveRecord::Base) : Nil\n      @record = record\n      super(\"Validation failed: #{record.errors.join(\", \")}\")\n    end\n  end\nend\n"},{"path":"src/fixtures/articles.cr","content":"module ArticlesFixtures\n  def self.one : Article\n    Article.find(1_i64)\n  end\n\n  def self.two : Article\n    Article.find(2_i64)\n  end\n\n  def self._fixtures_load! : Nil\n    instance = Article.new\n    instance.id = 1_i64\n    instance.title = \"Getting Started with Rails\"\n    instance.body = \"Rails is a web application framework running on the Ruby programming language.\"\n    instance.save.not_nil!\n    instance = Article.new\n    instance.id = 2_i64\n    instance.title = \"Understanding MVC Architecture\"\n    instance.body = \"MVC stands for Model-View-Controller. Models handle data and business logic.\"\n    instance.save.not_nil!\n  end\nend\n"},{"path":"src/fixtures/comments.cr","content":"module CommentsFixtures\n  def self.one : Comment\n    Comment.find(1_i64)\n  end\n\n  def self.two : Comment\n    Comment.find(2_i64)\n  end\n\n  def self._fixtures_load! : Nil\n    instance = Comment.new\n    instance.id = 1_i64\n    instance.article_id = 1_i64\n    instance.commenter = \"Alice\"\n    instance.body = \"Great introduction! Rails really does make development faster.\"\n    instance.save.not_nil!\n    instance = Comment.new\n    instance.id = 2_i64\n    instance.article_id = 2_i64\n    instance.commenter = \"Bob\"\n    instance.body = \"This pattern really helps keep code organized!\"\n    instance.save.not_nil!\n  end\nend\n"},{"path":"src/flash.cr","content":"# Generated from runtime/ruby/action_dispatch/flash.rb at app emit time.\n# Do not edit by hand — edit the source `.rb` and re-run emit.\n\nmodule ActionDispatch\n  class Flash\n    property notice : String?\n    property alert : String?\n\n    def initialize(other : Hash(String, String)? = nil) : Nil\n      @notice = nil\n      @alert = nil\n      return if other.nil?\n      v = other[\"notice\"]\n      @notice = v if !(v.nil?)\n      v = other[\"alert\"]\n      @alert = v if !(v.nil?)\n    end\n\n    def [](key)\n      k = key.to_s\n      return @notice if k == \"notice\"\n      return @alert if k == \"alert\"\n      nil\n    end\n\n    def []=(key, value)\n      k = key.to_s\n      if k == \"notice\"\n        @notice = value\n      else\n        @alert = value if k == \"alert\"\n      end\n      value\n    end\n\n    def fetch(key, default = nil)\n      v = self[key]\n      return v if !(v.nil?)\n      default\n    end\n\n    def key?(key)\n      v = self[key]\n      !(v.nil?)\n    end\n\n    def has_key?(key)\n      key?(key)\n    end\n\n    def include?(key)\n      key?(key)\n    end\n\n    def delete(key)\n      k = key.to_s\n      if k == \"notice\"\n        v = @notice\n        @notice = nil\n        return v\n      end\n      if k == \"alert\"\n        v = @alert\n        @alert = nil\n        return v\n      end\n      nil\n    end\n\n    def length\n      n = 0_i64\n      n += 1_i64 if !(@notice.nil?)\n      n += 1_i64 if !(@alert.nil?)\n      n\n    end\n\n    def size\n      n = 0_i64\n      n += 1_i64 if !(@notice.nil?)\n      n += 1_i64 if !(@alert.nil?)\n      n\n    end\n\n    def empty? : Bool\n      @notice.nil? && @alert.nil?\n    end\n\n    def keys : Array(String)\n      result = [] of String\n      result.push(\"notice\") if !(@notice.nil?)\n      result.push(\"alert\") if !(@alert.nil?)\n      result\n    end\n\n    def values : Array(String)\n      result = [] of String\n      result.push(@notice) if !(@notice.nil?)\n      result.push(@alert) if !(@alert.nil?)\n      result\n    end\n\n    def each\n      yield \"notice\", @notice if !(@notice.nil?)\n      yield \"alert\", @alert if !(@alert.nil?)\n      self\n    end\n\n    def to_h : Hash(String, String)\n      result = {} of String => String\n      result[\"notice\"] = @notice if !(@notice.nil?)\n      result[\"alert\"] = @alert if !(@alert.nil?)\n      result\n    end\n\n    def merge(other : Hash(String, String))\n      result = ::ActionDispatch::Flash.new\n      result.notice = @notice\n      result.alert = @alert\n      other.each do |k, v| result[k] = v end\n      result\n    end\n  end\nend\n"},{"path":"src/http.cr","content":"# Roundhouse Crystal HTTP runtime.\n#\n# Hand-written, shipped alongside generated code (copied in by the\n# Crystal emitter as `src/http.cr`). Provides the Roundhouse::Http\n# surface that emitted controller actions call into: ActionResponse\n# (typed return value), ActionContext (params + request shape),\n# Router (in-memory route-match table), plus legacy stubs for the\n# Phase 4c controller shape (render/redirect_to/head/respond_to)\n# that the preview emitters still reference.\n#\n# Mirrors `runtime/rust/http.rs` / `runtime/typescript/juntos.ts` in\n# shape and posture: pure in-process dispatch via `Router.match`\n# means tests call controller actions directly — no HTTP server,\n# no sockets, no event-loop glue. A real HTTP transport slots in\n# later by adding a `HTTP::Handler` on top of the same match table.\n\nmodule Roundhouse\n  module Http\n    # What every controller action returns. Fields are optional so\n    # actions pick the shape they need:\n    #   - `body`: the HTML string the view rendered (for GET actions)\n    #   - `status`: HTTP status code (default 200; 422 for\n    #     unprocessable, 303 for redirects)\n    #   - `location`: redirect target URL; test assertions on\n    #     `assert_redirected_to` check this field.\n    class ActionResponse\n      property body : String\n      property status : Int32\n      property location : String\n\n      def initialize(@body : String = \"\", @status : Int32 = 200, @location : String = \"\")\n      end\n    end\n\n    # Context passed to every action. `params` merges path params +\n    # form body. Values are always strings — controllers coerce to\n    # integers via `.to_i64` at the boundary.\n    class ActionContext\n      getter params : Hash(String, String)\n\n      def initialize(@params : Hash(String, String) = {} of String => String)\n      end\n    end\n\n    # One entry in the router's match table. The handler is a proc\n    # taking ActionContext and returning ActionResponse.\n    alias Handler = ActionContext -> ActionResponse\n\n    record Route, method : String, path : String, handler : Handler\n\n    # In-memory router + dispatch. Controllers register routes at\n    # require time (the emitted `src/routes.cr` runs Router.root /\n    # Router.resources at top level); tests dispatch through\n    # `Router.match` without a live HTTP server.\n    class Router\n      @@routes : Array(Route) = [] of Route\n\n      def self.reset : Nil\n        @@routes.clear\n      end\n\n      def self.add(method : String, path : String, handler : Handler) : Nil\n        @@routes << Route.new(method, path, handler)\n      end\n\n      # Match a request path against the registered routes; return\n      # the handler + extracted path params, or nil. Path params\n      # come from `:id`-style segments in the route pattern.\n      def self.match(method : String, path : String) : {Handler, Hash(String, String)}?\n        @@routes.each do |route|\n          next unless route.method == method\n          if extracted = try_match(route.path, path)\n            return {route.handler, extracted}\n          end\n        end\n        nil\n      end\n\n      private def self.try_match(pattern : String, path : String) : Hash(String, String)?\n        pat_parts = pattern.split('/').reject(&.empty?)\n        path_parts = path.split('/').reject(&.empty?)\n        return nil unless pat_parts.size == path_parts.size\n        params = {} of String => String\n        pat_parts.zip(path_parts).each do |pat, seg|\n          if pat.starts_with?(':')\n            params[pat[1..]] = seg\n          elsif pat != seg\n            return nil\n          end\n        end\n        params\n      end\n    end\n\n    # Legacy Phase-4c stubs kept for the compile-only pass of\n    # controllers still using respond_to/render; pass-2 template\n    # actions don't call these.\n    class Response\n      def initialize\n      end\n    end\n\n    class Params\n      def expect(*args, **kwargs) : Params\n        self\n      end\n\n      def [](key) : Int64\n        0_i64\n      end\n    end\n\n    def self.params : Params\n      Params.new\n    end\n\n    def self.render(*args, **kwargs) : Response\n      Response.new\n    end\n\n    def self.redirect_to(*args, **kwargs) : Response\n      Response.new\n    end\n\n    def self.head(*args, **kwargs) : Response\n      Response.new\n    end\n\n    def self.respond_to(&) : Response\n      fr = FormatRouter.new\n      yield fr\n      Response.new\n    end\n\n    class FormatRouter\n      def html(&) : Response\n        yield\n        Response.new\n      end\n\n      def json(&) : Response\n        yield\n        Response.new\n      end\n    end\n  end\nend\n"},{"path":"src/importmap.cr","content":"module Importmap\n  def self.pins : Array(NamedTuple(name: String, path: String))\n    [{name: \"application\", path: \"/assets/application.js\"}, {name: \"@hotwired/turbo-rails\", path: \"/assets/turbo.min.js\"}, {name: \"@hotwired/stimulus\", path: \"/assets/stimulus.min.js\"}, {name: \"@hotwired/stimulus-loading\", path: \"/assets/stimulus-loading.js\"}, {name: \"controllers/application\", path: \"/assets/controllers/application.js\"}, {name: \"controllers/hello_controller\", path: \"/assets/controllers/hello_controller.js\"}, {name: \"controllers\", path: \"/assets/controllers/index.js\"}]\n  end\n\n  def self.entry : String\n    \"application\"\n  end\nend\n"},{"path":"src/inflector.cr","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\nmodule Inflector\n  def self.pluralize(count : Int64, word : String) : String\n    if count == 1_i64\n      \"1 #{word}\"\n    else\n      \"#{count} #{word}s\"\n    end\n  end\nend\n"},{"path":"src/json_builder.cr","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\nESCAPES = { \"\\\\\" => \"\\\\\\\\\", \"\\\"\" => \"\\\\\\\"\", \"\\n\" => \"\\\\n\", \"\\r\" => \"\\\\r\", \"\\t\" => \"\\\\t\", \"\\u{8}\" => \"\\\\b\", \"\\u{c}\" => \"\\\\f\" }\nESCAPE_PATTERN = /[\\\\\"\\n\\r\\t\\x08\\f]/\n\nmodule JsonBuilder\n  def self.encode_string(s : String) : String\n    s.gsub(ESCAPE_PATTERN, ESCAPES)\n  end\n\n  def self.encode_value(v)\n    return \"null\" if v.nil?\n    return \"true\" if v == true\n    return \"false\" if v == false\n    return v.to_s if v.is_a?(Int)\n    return v.to_s if v.is_a?(Float)\n    return \"\\\"#{encode_string(v)}\\\"\" if v.is_a?(String)\n    \"\\\"#{encode_string(v.to_s)}\\\"\"\n  end\n\n  def self.encode_datetime(s : String?) : String\n    return \"null\" if s.nil?\n    str = s.to_s\n    return \"\\\"#{encode_string(str)}\\\"\" if str.size < 19_i64\n    date = str[0_i64, 10_i64]\n    time = str[11_i64, 8_i64]\n    ms = \"000\"\n    if str.size > 20_i64 && str[19_i64, 1_i64] == \".\"\n      frac = str[20_i64..]\n      padded = \"#{frac}000\"\n      ms = padded[0_i64, 3_i64]\n    end\n    \"\\\"#{date}T#{time}.#{ms}Z\\\"\"\n  end\nend\n"},{"path":"src/main.cr","content":"# Generated by Roundhouse — Crystal binary entry point.\nrequire \"./app\"\n\nRoundhouse::Server.start(\n  schema_sql: Schema.statements.join(\";\\n\"),\n  routes: Routes.table,\n  root_route: Routes.root,\n  layout: ->(body : String) { Views::Layouts.application(body) },\n  controllers: {\n    :application => ApplicationController,\n    :articles => ArticlesController,\n    :comments => CommentsController,\n  } of Symbol => ActionController::Base.class,\n)\n"},{"path":"src/models/application_record.cr","content":"class ApplicationRecord < ActiveRecord::Base\n  def self.abstract? : Bool\n    true\n  end\nend\n"},{"path":"src/models/article.cr","content":"class Article < ApplicationRecord\n  property body : String?\n  property created_at : String?\n  property title : String?\n  property updated_at : String?\n  @comments_cache : Array(Comment) = [] of Comment\n  @comments_loaded : Bool = false\n\n  def self.table_name : String\n    \"articles\"\n  end\n\n  def self.schema_columns : Array(Symbol)\n    [:id, :body, :created_at, :title, :updated_at]\n  end\n\n  def self.instantiate(row)\n    instance = Article.from_row(ArticleRow.from_raw(row))\n    instance.mark_persisted!\n    instance\n  end\n\n  def self.from_row(row : ArticleRow)\n    instance = Article.new\n    instance.id = (row.id.not_nil!).as?(Int64).not_nil!\n    instance.body = (row.body.not_nil!).as?(String).not_nil!\n    instance.created_at = (row.created_at.not_nil!).as?(String).not_nil!\n    instance.title = (row.title.not_nil!).as?(String).not_nil!\n    instance.updated_at = (row.updated_at.not_nil!).as?(String).not_nil!\n    instance\n  end\n\n  def self.from_stmt(stmt : Int64)\n    instance = Article.new\n    instance.id = Roundhouse::Db.column_int(stmt, 0_i64)\n    instance.body = Roundhouse::Db.column_text(stmt, 1_i64)\n    instance.created_at = Roundhouse::Db.column_text(stmt, 2_i64)\n    instance.title = Roundhouse::Db.column_text(stmt, 3_i64)\n    instance.updated_at = Roundhouse::Db.column_text(stmt, 4_i64)\n    instance.mark_persisted!\n    instance\n  end\n\n  def assign_from_row(row)\n    self.id = row[\"id\"]\n    self.body = row[\"body\"]\n    self.created_at = row[\"created_at\"]\n    self.title = row[\"title\"]\n    self.updated_at = row[\"updated_at\"]\n  end\n\n  def attributes\n    {body: @body.not_nil!, created_at: @created_at.not_nil!, title: @title.not_nil!, updated_at: @updated_at.not_nil!}\n  end\n\n  def [](name)\n    case name\n    when :id\n      @id\n    when :body\n      @body\n    when :created_at\n      @created_at\n    when :title\n      @title\n    when :updated_at\n      @updated_at\n    end\n  end\n\n  def []=(name : Symbol, value : Int64 | String) : Int64 | String | Nil\n    case name\n    when :id\n      @id = (value).as?(Int64).not_nil!\n    when :body\n      @body = (value).as?(String).not_nil!\n    when :created_at\n      @created_at = (value).as?(String).not_nil!\n    when :title\n      @title = (value).as?(String).not_nil!\n    when :updated_at\n      @updated_at = (value).as?(String).not_nil!\n    end\n  end\n\n  def update(p : ArticleParams) : Bool\n    if p.title.not_nil!.nil?\n      nil\n    else\n      self.title = p.title.not_nil!\n    end\n    if p.body.not_nil!.nil?\n      nil\n    else\n      self.body = p.body.not_nil!\n    end\n    save.not_nil!\n  end\n\n  def self._adapter_find_by_id(id : Int64)\n    stmt = Roundhouse::Db.prepare(\"SELECT id, body, created_at, title, updated_at FROM articles\" + \" WHERE \" + \"id = \" + Roundhouse::Db.escape_int(id) + \" LIMIT 1\")\n    result = nil\n    if Roundhouse::Db.step?(stmt)\n      result = Article.from_stmt(stmt)\n    end\n    Roundhouse::Db.finalize(stmt)\n    result\n  end\n\n  def self._adapter_all\n    stmt = Roundhouse::Db.prepare(\"SELECT id, body, created_at, title, updated_at FROM articles\")\n    results = [] of Article\n    while Roundhouse::Db.step?(stmt)\n      results << Article.from_stmt(stmt)\n    end\n    Roundhouse::Db.finalize(stmt)\n    results\n  end\n\n  def _adapter_insert : Int64\n    Roundhouse::Db.exec(\"INSERT INTO articles (body, created_at, title, updated_at) VALUES (\" + Roundhouse::Db.escape_string(@body.not_nil!) + \", \" + Roundhouse::Db.escape_string(@created_at.not_nil!) + \", \" + Roundhouse::Db.escape_string(@title.not_nil!) + \", \" + Roundhouse::Db.escape_string(@updated_at.not_nil!) + \")\")\n    Roundhouse::Db.last_insert_rowid.not_nil!\n  end\n\n  def _adapter_update : Nil\n    Roundhouse::Db.exec(\"UPDATE articles SET \" + \"body = \" + Roundhouse::Db.escape_string(@body.not_nil!) + \", created_at = \" + Roundhouse::Db.escape_string(@created_at.not_nil!) + \", title = \" + Roundhouse::Db.escape_string(@title.not_nil!) + \", updated_at = \" + Roundhouse::Db.escape_string(@updated_at.not_nil!) + \" WHERE \" + \"id = \" + Roundhouse::Db.escape_int(@id.not_nil!))\n  end\n\n  def _adapter_delete : Nil\n    Roundhouse::Db.exec(\"DELETE FROM articles\" + \" WHERE \" + \"id = \" + Roundhouse::Db.escape_int(@id.not_nil!))\n  end\n\n  def self._adapter_count : Int64\n    stmt = Roundhouse::Db.prepare(\"SELECT COUNT(*) FROM articles\")\n    Roundhouse::Db.step?(stmt)\n    result = Roundhouse::Db.column_int(stmt, 0_i64)\n    Roundhouse::Db.finalize(stmt)\n    result\n  end\n\n  def self._adapter_exists_by_id?(id : Int64) : Bool\n    stmt = Roundhouse::Db.prepare(\"SELECT 1 FROM articles\" + \" WHERE \" + \"id = \" + Roundhouse::Db.escape_int(id) + \" LIMIT 1\")\n    result = Roundhouse::Db.step?(stmt)\n    Roundhouse::Db.finalize(stmt)\n    result\n  end\n\n  def self._adapter_truncate : Nil\n    Roundhouse::Db.exec(\"DELETE FROM articles\")\n    Roundhouse::Db.exec(\"DELETE FROM sqlite_sequence WHERE name = 'articles'\")\n  end\n\n  def _adapter_reload\n    stmt = Roundhouse::Db.prepare(\"SELECT id, body, created_at, title, updated_at FROM articles WHERE id = \" + Roundhouse::Db.escape_int(@id.not_nil!) + \" LIMIT 1\")\n    if Roundhouse::Db.step?(stmt)\n      @id = Roundhouse::Db.column_int(stmt, 0_i64)\n      @body = Roundhouse::Db.column_text(stmt, 1_i64)\n      @created_at = Roundhouse::Db.column_text(stmt, 2_i64)\n      @title = Roundhouse::Db.column_text(stmt, 3_i64)\n      @updated_at = Roundhouse::Db.column_text(stmt, 4_i64)\n      mark_persisted!\n    end\n    Roundhouse::Db.finalize(stmt)\n    self\n  end\n\n  def self.from_params(p : ArticleParams)\n    instance = Article.new\n    instance.title = p.title.not_nil!\n    instance.body = p.body.not_nil!\n    instance\n  end\n\n  def validate : Nil\n    errors << \"title can't be blank\" if @title.nil? || @title.not_nil!.empty?\n    errors << \"body can't be blank\" if @body.nil? || @body.not_nil!.empty?\n    if !(@body.nil?)\n      len = @body.not_nil!.size\n      errors << \"body is too short (minimum is 10)\" if len < 10_i64\n    end\n  end\n\n  def comments : Array(Comment)\n    return @comments_cache.not_nil! if @comments_loaded.not_nil!\n    stmt = Roundhouse::Db.prepare(\"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments\" + \" WHERE \" + \"article_id = \" + Roundhouse::Db.escape_int(@id.not_nil!))\n    results = [] of Comment\n    while Roundhouse::Db.step?(stmt)\n      results << Comment.from_stmt(stmt)\n    end\n    Roundhouse::Db.finalize(stmt)\n    results\n  end\n\n  def _preload_comments(list : Array(Comment)) : Nil\n    @comments_cache = list\n    @comments_loaded = true\n  end\n\n  def before_destroy : Nil\n    comments.each { |c| c.destroy }\n  end\n\n  def dom_prefix : String\n    \"article\"\n  end\n\n  def after_create_commit : Nil\n    Broadcasts.prepend(stream: \"articles\", target: \"articles\", html: Views::Articles.article(self))\n  end\n\n  def after_update_commit : Nil\n    Broadcasts.replace(stream: \"articles\", target: \"article_#{@id.not_nil!}\", html: Views::Articles.article(self))\n  end\n\n  def after_destroy_commit : Nil\n    Broadcasts.remove(stream: \"articles\", target: \"article_#{@id.not_nil!}\")\n  end\nend\n"},{"path":"src/models/article_params.cr","content":"class ArticleParams\n  property title : String\n  property body : String\n\n  def initialize : Nil\n    @title = \"\"\n    @body = \"\"\n  end\n\n  def self.from_raw(params : Hash(String, Roundhouse::ParamValue))\n    raw_sub = params.fetch \"article\", {} of String => Roundhouse::ParamValue\n    sub = if (raw_sub.is_a?(Hash) || raw_sub.is_a?(NamedTuple))\n      (raw_sub).as?(Hash(String, Roundhouse::ParamValue)).not_nil!\n    else\n      {} of String => Roundhouse::ParamValue\n    end\n    instance = ArticleParams.new\n    raw_title = sub.fetch \"title\", \"\"\n    instance.title = if raw_title.is_a?(String)\n      raw_title\n    else\n      \"\"\n    end\n    raw_body = sub.fetch \"body\", \"\"\n    instance.body = if raw_body.is_a?(String)\n      raw_body\n    else\n      \"\"\n    end\n    instance\n  end\n\n  def to_h : Hash(String, String)\n    { \"title\" => @title.not_nil!, \"body\" => @body.not_nil! }\n  end\nend\n"},{"path":"src/models/article_row.cr","content":"class ArticleRow\n  property id : Int64\n  property body : String\n  property created_at : String\n  property title : String\n  property updated_at : String\n\n  def initialize : Nil\n    @id = 0_i64\n    @body = \"\"\n    @created_at = \"\"\n    @title = \"\"\n    @updated_at = \"\"\n  end\n\n  def self.from_raw(row)\n    instance = ArticleRow.new\n    instance.id = (row[\"id\"]? || 0_i64).as?(Int64).not_nil!\n    instance.body = (row[\"body\"]).as?(String).not_nil!\n    instance.created_at = (row[\"created_at\"]).as?(String).not_nil!\n    instance.title = (row[\"title\"]).as?(String).not_nil!\n    instance.updated_at = (row[\"updated_at\"]).as?(String).not_nil!\n    instance\n  end\nend\n"},{"path":"src/models/comment.cr","content":"class Comment < ApplicationRecord\n  property article_id : Int64?\n  property body : String?\n  property commenter : String?\n  property created_at : String?\n  property updated_at : String?\n\n  def self.table_name : String\n    \"comments\"\n  end\n\n  def self.schema_columns : Array(Symbol)\n    [:id, :article_id, :body, :commenter, :created_at, :updated_at]\n  end\n\n  def self.instantiate(row)\n    instance = Comment.from_row(CommentRow.from_raw(row))\n    instance.mark_persisted!\n    instance\n  end\n\n  def self.from_row(row : CommentRow)\n    instance = Comment.new\n    instance.id = (row.id.not_nil!).as?(Int64).not_nil!\n    instance.article_id = (row.article_id.not_nil!).as?(Int64).not_nil!\n    instance.body = (row.body.not_nil!).as?(String).not_nil!\n    instance.commenter = (row.commenter.not_nil!).as?(String).not_nil!\n    instance.created_at = (row.created_at.not_nil!).as?(String).not_nil!\n    instance.updated_at = (row.updated_at.not_nil!).as?(String).not_nil!\n    instance\n  end\n\n  def self.from_stmt(stmt : Int64)\n    instance = Comment.new\n    instance.id = Roundhouse::Db.column_int(stmt, 0_i64)\n    instance.article_id = Roundhouse::Db.column_int(stmt, 1_i64)\n    instance.body = Roundhouse::Db.column_text(stmt, 2_i64)\n    instance.commenter = Roundhouse::Db.column_text(stmt, 3_i64)\n    instance.created_at = Roundhouse::Db.column_text(stmt, 4_i64)\n    instance.updated_at = Roundhouse::Db.column_text(stmt, 5_i64)\n    instance.mark_persisted!\n    instance\n  end\n\n  def assign_from_row(row)\n    self.id = row[\"id\"]\n    self.article_id = row[\"article_id\"]\n    self.body = row[\"body\"]\n    self.commenter = row[\"commenter\"]\n    self.created_at = row[\"created_at\"]\n    self.updated_at = row[\"updated_at\"]\n  end\n\n  def attributes\n    {article_id: @article_id.not_nil!, body: @body.not_nil!, commenter: @commenter.not_nil!, created_at: @created_at.not_nil!, updated_at: @updated_at.not_nil!}\n  end\n\n  def [](name)\n    case name\n    when :id\n      @id\n    when :article_id\n      @article_id\n    when :body\n      @body\n    when :commenter\n      @commenter\n    when :created_at\n      @created_at\n    when :updated_at\n      @updated_at\n    end\n  end\n\n  def []=(name : Symbol, value : Int64 | String) : Int64 | String | Nil\n    case name\n    when :id\n      @id = (value).as?(Int64).not_nil!\n    when :article_id\n      @article_id = (value).as?(Int64).not_nil!\n    when :body\n      @body = (value).as?(String).not_nil!\n    when :commenter\n      @commenter = (value).as?(String).not_nil!\n    when :created_at\n      @created_at = (value).as?(String).not_nil!\n    when :updated_at\n      @updated_at = (value).as?(String).not_nil!\n    end\n  end\n\n  def update(p : CommentParams) : Bool\n    if p.commenter.not_nil!.nil?\n      nil\n    else\n      self.commenter = p.commenter.not_nil!\n    end\n    if p.body.not_nil!.nil?\n      nil\n    else\n      self.body = p.body.not_nil!\n    end\n    save.not_nil!\n  end\n\n  def self._adapter_find_by_id(id : Int64)\n    stmt = Roundhouse::Db.prepare(\"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments\" + \" WHERE \" + \"id = \" + Roundhouse::Db.escape_int(id) + \" LIMIT 1\")\n    result = nil\n    if Roundhouse::Db.step?(stmt)\n      result = Comment.from_stmt(stmt)\n    end\n    Roundhouse::Db.finalize(stmt)\n    result\n  end\n\n  def self._adapter_all\n    stmt = Roundhouse::Db.prepare(\"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments\")\n    results = [] of Comment\n    while Roundhouse::Db.step?(stmt)\n      results << Comment.from_stmt(stmt)\n    end\n    Roundhouse::Db.finalize(stmt)\n    results\n  end\n\n  def _adapter_insert : Int64\n    Roundhouse::Db.exec(\"INSERT INTO comments (article_id, body, commenter, created_at, updated_at) VALUES (\" + Roundhouse::Db.escape_int(@article_id.not_nil!) + \", \" + Roundhouse::Db.escape_string(@body.not_nil!) + \", \" + Roundhouse::Db.escape_string(@commenter.not_nil!) + \", \" + Roundhouse::Db.escape_string(@created_at.not_nil!) + \", \" + Roundhouse::Db.escape_string(@updated_at.not_nil!) + \")\")\n    Roundhouse::Db.last_insert_rowid.not_nil!\n  end\n\n  def _adapter_update : Nil\n    Roundhouse::Db.exec(\"UPDATE comments SET \" + \"article_id = \" + Roundhouse::Db.escape_int(@article_id.not_nil!) + \", body = \" + Roundhouse::Db.escape_string(@body.not_nil!) + \", commenter = \" + Roundhouse::Db.escape_string(@commenter.not_nil!) + \", created_at = \" + Roundhouse::Db.escape_string(@created_at.not_nil!) + \", updated_at = \" + Roundhouse::Db.escape_string(@updated_at.not_nil!) + \" WHERE \" + \"id = \" + Roundhouse::Db.escape_int(@id.not_nil!))\n  end\n\n  def _adapter_delete : Nil\n    Roundhouse::Db.exec(\"DELETE FROM comments\" + \" WHERE \" + \"id = \" + Roundhouse::Db.escape_int(@id.not_nil!))\n  end\n\n  def self._adapter_count : Int64\n    stmt = Roundhouse::Db.prepare(\"SELECT COUNT(*) FROM comments\")\n    Roundhouse::Db.step?(stmt)\n    result = Roundhouse::Db.column_int(stmt, 0_i64)\n    Roundhouse::Db.finalize(stmt)\n    result\n  end\n\n  def self._adapter_exists_by_id?(id : Int64) : Bool\n    stmt = Roundhouse::Db.prepare(\"SELECT 1 FROM comments\" + \" WHERE \" + \"id = \" + Roundhouse::Db.escape_int(id) + \" LIMIT 1\")\n    result = Roundhouse::Db.step?(stmt)\n    Roundhouse::Db.finalize(stmt)\n    result\n  end\n\n  def self._adapter_truncate : Nil\n    Roundhouse::Db.exec(\"DELETE FROM comments\")\n    Roundhouse::Db.exec(\"DELETE FROM sqlite_sequence WHERE name = 'comments'\")\n  end\n\n  def _adapter_reload\n    stmt = Roundhouse::Db.prepare(\"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments WHERE id = \" + Roundhouse::Db.escape_int(@id.not_nil!) + \" LIMIT 1\")\n    if Roundhouse::Db.step?(stmt)\n      @id = Roundhouse::Db.column_int(stmt, 0_i64)\n      @article_id = Roundhouse::Db.column_int(stmt, 1_i64)\n      @body = Roundhouse::Db.column_text(stmt, 2_i64)\n      @commenter = Roundhouse::Db.column_text(stmt, 3_i64)\n      @created_at = Roundhouse::Db.column_text(stmt, 4_i64)\n      @updated_at = Roundhouse::Db.column_text(stmt, 5_i64)\n      mark_persisted!\n    end\n    Roundhouse::Db.finalize(stmt)\n    self\n  end\n\n  def self.from_params(p : CommentParams)\n    instance = Comment.new\n    instance.commenter = p.commenter.not_nil!\n    instance.body = p.body.not_nil!\n    instance\n  end\n\n  def validate : Nil\n    errors << \"commenter can't be blank\" if @commenter.nil? || @commenter.not_nil!.empty?\n    errors << \"body can't be blank\" if @body.nil? || @body.not_nil!.empty?\n    errors << \"article must exist\" if @article_id.nil? || @article_id.not_nil! == 0_i64 || !(Article.exists?(@article_id.not_nil!))\n  end\n\n  def article : Article?\n    if @article_id.not_nil! == 0_i64\n      nil\n    else\n      stmt = Roundhouse::Db.prepare(\"SELECT id, body, created_at, title, updated_at FROM articles\" + \" WHERE \" + \"id = \" + Roundhouse::Db.escape_int(@article_id.not_nil!) + \" LIMIT 1\")\n      result = nil\n      if Roundhouse::Db.step?(stmt)\n        result = Article.from_stmt(stmt)\n      end\n      Roundhouse::Db.finalize(stmt)\n      result\n    end\n  end\n\n  def dom_prefix : String\n    \"comment\"\n  end\n\n  def after_create_commit : Nil\n    Broadcasts.append(stream: \"article_#{@article_id.not_nil!}_comments\", target: \"comments\", html: Views::Comments.comment(self))\n    parent = article\n    return if parent.nil?\n    Broadcasts.replace(stream: \"articles\", target: \"article_#{parent.id.not_nil!}\", html: Views::Articles.article(parent))\n  end\n\n  def after_update_commit : Nil\n    Broadcasts.replace(stream: \"article_#{@article_id.not_nil!}_comments\", target: \"comment_#{@id.not_nil!}\", html: Views::Comments.comment(self))\n  end\n\n  def after_destroy_commit : Nil\n    Broadcasts.remove(stream: \"article_#{@article_id.not_nil!}_comments\", target: \"comment_#{@id.not_nil!}\")\n    parent = article\n    return if parent.nil?\n    Broadcasts.replace(stream: \"articles\", target: \"article_#{parent.id.not_nil!}\", html: Views::Articles.article(parent))\n  end\nend\n"},{"path":"src/models/comment_params.cr","content":"class CommentParams\n  property commenter : String\n  property body : String\n\n  def initialize : Nil\n    @commenter = \"\"\n    @body = \"\"\n  end\n\n  def self.from_raw(params : Hash(String, Roundhouse::ParamValue))\n    raw_sub = params.fetch \"comment\", {} of String => Roundhouse::ParamValue\n    sub = if (raw_sub.is_a?(Hash) || raw_sub.is_a?(NamedTuple))\n      (raw_sub).as?(Hash(String, Roundhouse::ParamValue)).not_nil!\n    else\n      {} of String => Roundhouse::ParamValue\n    end\n    instance = CommentParams.new\n    raw_commenter = sub.fetch \"commenter\", \"\"\n    instance.commenter = if raw_commenter.is_a?(String)\n      raw_commenter\n    else\n      \"\"\n    end\n    raw_body = sub.fetch \"body\", \"\"\n    instance.body = if raw_body.is_a?(String)\n      raw_body\n    else\n      \"\"\n    end\n    instance\n  end\n\n  def to_h : Hash(String, String)\n    { \"commenter\" => @commenter.not_nil!, \"body\" => @body.not_nil! }\n  end\nend\n"},{"path":"src/models/comment_row.cr","content":"class CommentRow\n  property id : Int64\n  property article_id : Int64\n  property body : String\n  property commenter : String\n  property created_at : String\n  property updated_at : String\n\n  def initialize : Nil\n    @id = 0_i64\n    @article_id = 0_i64\n    @body = \"\"\n    @commenter = \"\"\n    @created_at = \"\"\n    @updated_at = \"\"\n  end\n\n  def self.from_raw(row)\n    instance = CommentRow.new\n    instance.id = (row[\"id\"]? || 0_i64).as?(Int64).not_nil!\n    instance.article_id = (row[\"article_id\"]? || 0_i64).as?(Int64).not_nil!\n    instance.body = (row[\"body\"]).as?(String).not_nil!\n    instance.commenter = (row[\"commenter\"]).as?(String).not_nil!\n    instance.created_at = (row[\"created_at\"]).as?(String).not_nil!\n    instance.updated_at = (row[\"updated_at\"]).as?(String).not_nil!\n    instance\n  end\nend\n"},{"path":"src/param_value.cr","content":"# Recursive type alias for request parameters.\n#\n# Form bodies and URL params arrive as a tree of String leaves, Hashes\n# keyed by String, and Arrays. Rails' Rack parser walks\n# `comment[author][name]=x` and `tags[]=a&tags[]=b` shapes into the\n# same recursive structure; the Roundhouse runtime mirrors that.\n#\n# `Roundhouse::ParamValue` is the cross-target type contract: each\n# target's runtime defines its own recursive realization (Crystal\n# alias here, TS `type ParamValue = …`, Ruby/Spinel dynamic). The\n# lowerer emits target-agnostic `is_a?(Hash)` / `is_a?(String)`\n# narrowing around accesses; each emit translates `is_a?` to its\n# idiomatic narrowing predicate.\n#\n# Crystal's `alias` admits self-reference through a generic\n# constructor (`Hash`/`Array` here) — the same pattern stdlib's\n# `JSON::Any` uses internally.\n\nmodule Roundhouse\n  alias ParamValue = String | Hash(String, ParamValue) | Array(ParamValue)\nend\n"},{"path":"src/route_helpers.cr","content":"module RouteHelpers\n  def self.root_path : String\n    \"/\"\n  end\n\n  def self.articles_path : String\n    \"/articles\"\n  end\n\n  def self.new_article_path : String\n    \"/articles/new\"\n  end\n\n  def self.article_path(id : Int64) : String\n    \"/articles/#{id}\"\n  end\n\n  def self.edit_article_path(id : Int64) : String\n    \"/articles/#{id}/edit\"\n  end\n\n  def self.article_comments_path(article_id : Int64) : String\n    \"/articles/#{article_id}/comments\"\n  end\n\n  def self.article_comment_path(article_id : Int64, id : Int64) : String\n    \"/articles/#{article_id}/comments/#{id}\"\n  end\nend\n"},{"path":"src/router.cr","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\nmodule ActionDispatch\n  module Router\n    class Route\n      property verb : String\n      property pattern : String\n      property controller : Symbol\n      property action : Symbol\n\n      def initialize(verb : String, pattern : String, controller : Symbol, action : Symbol) : Nil\n        @verb = verb\n        @pattern = pattern\n        @controller = controller\n        @action = action\n      end\n    end\n  end\nend\n\nmodule ActionDispatch\n  module Router\n    class MatchResult\n      property controller : Symbol\n      property action : Symbol\n      property path_params : Hash(String, String)\n\n      def initialize(controller : Symbol, action : Symbol, path_params : Hash(String, String)) : Nil\n        @controller = controller\n        @action = action\n        @path_params = path_params\n      end\n    end\n  end\nend\n\nmodule ActionDispatch\n  module Router\n    def self.match(method : String, path : String, table : Array(ActionDispatch::Router::Route)) : ActionDispatch::Router::MatchResult?\n      method_upcase = method.to_s.upcase\n      i = 0_i64\n      while i < table.size\n        route = table[i]\n        if route.not_nil!.verb.to_s == method_upcase\n          params = match_pattern(route.not_nil!.pattern.to_s, path)\n          if params.nil?\n            nil\n          else\n            return ::ActionDispatch::Router::MatchResult.new(route.not_nil!.controller, route.not_nil!.action, params)\n          end\n        end\n        i += 1_i64\n      end\n      nil\n    end\n\n    def self.match_pattern(pattern : String, path : String) : Hash(String, String)?\n      pattern_parts = pattern.split(\"/\")\n      path_parts = path.split(\"/\")\n      return if pattern_parts.size != path_parts.size\n      params = {} of String => String\n      i = 0_i64\n      while i < pattern_parts.size\n        pp = pattern_parts[i]\n        ap = path_parts[i]\n        if pp.starts_with?(\":\")\n          params[pp[1_i64..]] = ap\n        else\n          return if pp != ap\n        end\n        i += 1_i64\n      end\n      params\n    end\n  end\nend\n"},{"path":"src/routes.cr","content":"module Routes\n  def self.table : Array(ActionDispatch::Router::Route)\n    [::ActionDispatch::Router::Route.new(\"GET\", \"/articles\", :articles, :index), ::ActionDispatch::Router::Route.new(\"GET\", \"/articles/new\", :articles, :new), ::ActionDispatch::Router::Route.new(\"POST\", \"/articles\", :articles, :create), ::ActionDispatch::Router::Route.new(\"GET\", \"/articles/:id\", :articles, :show), ::ActionDispatch::Router::Route.new(\"GET\", \"/articles/:id/edit\", :articles, :edit), ::ActionDispatch::Router::Route.new(\"PATCH\", \"/articles/:id\", :articles, :update), ::ActionDispatch::Router::Route.new(\"DELETE\", \"/articles/:id\", :articles, :destroy), ::ActionDispatch::Router::Route.new(\"POST\", \"/articles/:article_id/comments\", :comments, :create), ::ActionDispatch::Router::Route.new(\"DELETE\", \"/articles/:article_id/comments/:id\", :comments, :destroy)]\n  end\n\n  def self.root : ActionDispatch::Router::Route\n    ::ActionDispatch::Router::Route.new(\"GET\", \"/\", :articles, :index)\n  end\nend\n"},{"path":"src/schema.cr","content":"module Schema\n  def self.statements : Array(String)\n    [\"CREATE 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)\", \"CREATE 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)\", \"CREATE INDEX IF NOT EXISTS index_comments_on_article_id ON comments (article_id)\"]\n  end\nend\n"},{"path":"src/seeds.cr","content":"module Seeds\n  def self.run : Nil\n    if Article.count.not_nil! > 0_i64\n      nil\n    else\n      article1 = Article.create!(title: \"Getting Started with Rails\", body: \"Rails is a web application framework running on the Ruby programming language. It makes building web apps faster and easier with conventions over configuration.\")\n      Comment.create!(article_id: article1.id, commenter: \"Alice\", body: \"Great introduction! Rails really does make development faster.\")\n      Comment.create!(article_id: article1.id, commenter: \"Bob\", body: \"I love how Rails handles database migrations automatically.\")\n      \n      article2 = Article.create!(title: \"Understanding MVC Architecture\", body: \"MVC stands for Model-View-Controller. Models handle data and business logic, Views display information to users, and Controllers coordinate between them.\")\n      Comment.create!(article_id: article2.id, commenter: \"Carol\", body: \"This pattern really helps keep code organized!\")\n      \n      article3 = Article.create!(title: \"Ruby2JS: Rails Everywhere\", body: \"Ruby2JS transpiles Ruby to JavaScript, enabling Rails applications to run in browsers, on Node.js, and at the edge. Same code, different runtimes.\")\n      \n      puts \"Created #{Article.count.not_nil!} articles and #{Comment.count.not_nil!} comments\"\n    end\n  end\nend\n"},{"path":"src/server.cr","content":"# Roundhouse Crystal server runtime — primitive HTTP listener that\n# dispatches through the transpiled framework runtime.\n#\n# Pipeline mirrors runtime/typescript/server.ts:\n#   1. Parse HTTP request → method, path, body params\n#   2. ActionDispatch::Router.match(method, path, routes_table) →\n#      {controller: Symbol, action: Symbol, path_params: Hash}\n#   3. Look up the controller class in @@controllers, instantiate\n#   4. Set @params (ActionController::Parameters), @session, @flash\n#   5. Invoke controller.process_action(action)\n#   6. Format @body, @status, @location into the HTTP response\n#\n# Controllers extend ActionController::Base (transpiled from\n# runtime/ruby/action_controller/base.rb) and inherit render /\n# redirect_to / head etc. The Roundhouse:: namespace here is reserved\n# for primitive concerns (HTTP, sqlite, websocket); framework concerns\n# live under ActionView/ActionController/ActionDispatch/ActiveRecord\n# from the transpiled runtime.\n\nrequire \"http/server\"\nrequire \"mime\"\nrequire \"uri\"\nrequire \"./db\"\nrequire \"./cable\"\n\nmodule Roundhouse\n  module Server\n    @@layout : Proc(String, String)? = nil\n    # Per-route record shape: `{method:, pattern:, controller:, action:}`.\n    # Matches the RBS record-row type that `ActionDispatch::Router.match`\n    # declares for its `table` parameter\n    # (`Array[{ method: String, pattern: String, controller: Symbol,\n    # action: Symbol }]`). Crystal renders that record as a NamedTuple,\n    # and the lowerer emits route rows via the matching shorthand\n    # literal (`{method: \"GET\", ...}`).\n    alias RouteRow = ActionDispatch::Router::Route\n    @@routes : Array(RouteRow) = [] of RouteRow\n    @@controllers : Hash(Symbol, ActionController::Base.class) = {} of Symbol => ActionController::Base.class\n    @@session : ActionDispatch::Session = ActionDispatch::Session.new\n    @@flash : ActionDispatch::Flash = ActionDispatch::Flash.new\n\n    def self.start(\n      schema_sql : String,\n      routes,\n      controllers : Hash(Symbol, ActionController::Base.class),\n      root_route = nil,\n      layout : Proc(String, String)? = nil,\n      db_path : String? = nil,\n      port : Int32? = nil,\n    ) : Nil\n      resolved_path = db_path || \"db/development.sqlite3\"\n      resolved_port = port || (ENV[\"PORT\"]?.try(&.to_i) || 3000)\n\n      Roundhouse::Db.open_production_db(resolved_path, schema_sql)\n      ActiveRecord.adapter = Roundhouse::SqliteAdapter.new\n\n      @@routes = if root_route\n                   [root_route] + routes\n                 else\n                   routes\n                 end\n      @@controllers = controllers\n      @@layout = layout\n\n      server = HTTP::Server.new do |context|\n        dispatch(context)\n      end\n      address = server.bind_tcp(\"127.0.0.1\", resolved_port)\n      puts \"Roundhouse Crystal server listening on http://#{address}\"\n      server.listen\n    end\n\n    def self.dispatch(context : HTTP::Server::Context) : Nil\n      ActionView::ViewHelpers.reset_slots!\n      method = context.request.method.upcase\n      path = context.request.path\n\n      if path == \"/cable\"\n        Roundhouse::Cable.handle(context)\n        return\n      end\n\n      # Static assets: serve `static/<path>` for any `/assets/*` request.\n      # Mirrors Rails' Propshaft URL shape — the importmap pins and\n      # `stylesheet_link_tag(\"tailwind\")` both point at /assets/<name>.\n      # `bin/rh transpile crystal` writes the actual files into\n      # `static/assets/` (Tailwind compile output, turbo.min.js copy).\n      # Returns 404 fall-through if the file isn't present, so route\n      # matching never sees an /assets/ path that's served from disk.\n      if path.starts_with?(\"/assets/\")\n        file = File.join(\"static\", path)\n        if File.file?(file)\n          context.response.headers[\"Content-Type\"] =\n            MIME.from_filename?(file) || \"application/octet-stream\"\n          File.open(file) { |io| IO.copy(io, context.response.output) }\n          return\n        end\n      end\n\n      # Per-request format inference. Strip a `.json` suffix from the\n      # request path before route matching so `/articles/1.json` and\n      # `/articles/1` share one route entry, and remember the format\n      # so the controller's respond_to-flattened branch can pick the\n      # right view + Content-Type. Mirrors `runtime/typescript/server.\n      # ts:149-161` and the Ruby scaffold's `main.rb:82-93` — every\n      # target needs this glue at its server entry point since route\n      # patterns are format-agnostic.\n      request_format = :html\n      route_path = path\n      if route_path.ends_with?(\".json\")\n        request_format = :json\n        route_path = route_path[0, route_path.size - 5]\n      end\n\n      body_params = read_form_body(context.request)\n      # `_method=delete|patch|put` from Rails' hidden form field is\n      # always a top-level (non-nested) key, so it survives bracket-\n      # parsing untouched.\n      if method == \"POST\"\n        raw_method = body_params[\"_method\"]?\n        if raw_method.is_a?(String)\n          upper = raw_method.upcase\n          if upper == \"PATCH\" || upper == \"PUT\" || upper == \"DELETE\"\n            method = upper\n          end\n        end\n      end\n\n      matched = ActionDispatch::Router.match(method, route_path, @@routes)\n      if matched.nil?\n        context.response.status_code = 404\n        context.response.content_type = \"text/plain\"\n        context.response.print \"Not Found: #{method} #{path}\"\n        return\n      end\n\n      # `matched` is now a typed `ActionDispatch::Router::MatchResult`\n      # (was a `Hash[Symbol, untyped]` requiring explicit `.as(T)` per\n      # field). Per-field types are baked into the class definition;\n      # no narrowing or casts needed.\n      ctrl_sym = matched.controller\n      action = matched.action\n      path_params = matched.path_params\n      ctrl_class = @@controllers[ctrl_sym]?\n      if ctrl_class.nil?\n        context.response.status_code = 500\n        context.response.content_type = \"text/plain\"\n        context.response.print \"No controller registered: #{ctrl_sym}\"\n        return\n      end\n\n      # Build merged params: path + query + body. Path captures and\n      # query-string entries are always String leaves; form-body keys\n      # may be bracket-nested (`comment[commenter]`) and surface as\n      # `Hash(String, ParamValue)` sub-trees. The slot's typed value\n      # union `Roundhouse::ParamValue = String | Hash(...) | Array(...)`\n      # accepts either shape; the lowered `<Resource>Params.from_raw`\n      # emit narrows via `is_a?(Hash)` / `is_a?(String)` at access.\n      merged = {} of String => Roundhouse::ParamValue\n      path_params.each { |k, v| merged[k] = v }\n      context.request.query_params.each { |k, v| merged[k] = v }\n      body_params.each { |k, v| merged[k] = v }\n\n      ctrl = ctrl_class.new\n      ctrl.params = merged\n      ctrl.session = @@session\n      ctrl.flash = @@flash\n      ctrl.request_method = method\n      ctrl.request_path = path\n      ctrl.request_format = request_format\n\n      begin\n        ctrl.process_action(action)\n      rescue err : Exception\n        STDERR.puts \"handler error: #{err.message}\"\n        STDERR.puts err.backtrace.join(\"\\n\")\n        context.response.status_code = 500\n        context.response.content_type = \"text/plain\"\n        context.response.print \"Server error: #{err.message}\"\n        return\n      end\n\n      # Carry flash forward exactly once: post-redirect, the next\n      # request reads the flash, the request after that sees fresh.\n      flash_for_response = ctrl.flash || ActionDispatch::Flash.new\n      @@flash = ActionDispatch::Flash.new\n\n      status = ctrl.status || 200i64\n      body = ctrl.body || \"\"\n      location = ctrl.location\n\n      if !location.nil? && !location.empty?\n        context.response.status_code = status.to_i\n        context.response.headers[\"Location\"] = location\n        @@flash = flash_for_response\n        return\n      end\n\n      # JSON responses skip the html layout wrap and ship the\n      # controller body verbatim with the controller-supplied\n      # Content-Type. Mirrors `runtime/typescript/server.ts:236-248`\n      # and the Ruby scaffold's `main.rb:157-167` branch — the\n      # controller's `respond_to`-flattened body picks the JSON view\n      # + content_type; the server just honors it.\n      if request_format == :json\n        context.response.status_code = status.to_i\n        context.response.content_type =\n          (ctrl.content_type.empty? ? \"application/json; charset=utf-8\" : ctrl.content_type)\n        context.response.print body\n        return\n      end\n\n      # Layout wrapping: when a layout proc is configured, pass body\n      # to it (mirrors TS's `layout?: (body) => string` shape).\n      response_body = if (l = @@layout)\n                       ActionView::ViewHelpers.set_yield(body)\n                       l.call(body)\n                     else\n                       body\n                     end\n\n      context.response.status_code = status.to_i\n      context.response.content_type = \"text/html; charset=utf-8\"\n      context.response.print response_body\n    end\n\n    # Parse a `application/x-www-form-urlencoded` body into a nested\n    # `Hash(String, Roundhouse::ParamValue)`. Rails-shape bracket keys\n    # are unwrapped:\n    #\n    #   `comment[commenter]=Sam` → `{\"comment\" => {\"commenter\" => \"Sam\"}}`\n    #   `tags[]=a&tags[]=b`       → `{\"tags\" => [\"a\", \"b\"]}`\n    #   `_method=delete`          → `{\"_method\" => \"delete\"}`\n    #\n    # Bare (no-bracket) keys land as String leaves. Mirrors the\n    # TS server's `parseFormData` + `setNestedParam` (runtime/\n    # typescript/server.ts) and Spinel's `assign_form_pair`\n    # (runtime/spinel/cgi_io.rb).\n    def self.read_form_body(request : HTTP::Request) : Hash(String, Roundhouse::ParamValue)\n      result = {} of String => Roundhouse::ParamValue\n      content_type = request.headers[\"Content-Type\"]? || \"\"\n      return result unless content_type.starts_with?(\"application/x-www-form-urlencoded\")\n      body_io = request.body\n      return result if body_io.nil?\n      raw = body_io.gets_to_end\n      return result if raw.empty?\n      URI::Params.parse(raw) do |k, v|\n        set_nested_param(result, k, v)\n      end\n      result\n    end\n\n\n    # Insert `key=val` into the nested params map, handling Rails'\n    # bracket syntax. Recognized shapes:\n    #\n    #   `parent[child]=v` → `out[parent] = { child => v }`\n    #   `parent[]=v`      → `out[parent] = [..., v]`\n    #\n    # Deeper nesting (`a[b][c]`) is unsupported today — the real-blog\n    # fixture only exercises one level. Future work can extend the\n    # recursion if an app needs it; the ParamValue type admits it.\n    private def self.set_nested_param(\n      into : Hash(String, Roundhouse::ParamValue),\n      key : String,\n      val : String,\n    ) : Nil\n      open_bracket = key.index('[')\n      if open_bracket.nil?\n        into[key] = val\n        return\n      end\n      close_bracket = key.index(']', open_bracket + 1)\n      return if close_bracket.nil?\n      parent = key[0, open_bracket]\n      inner = key[(open_bracket + 1)...close_bracket]\n      if inner.empty?\n        # `tags[]=v` — array append.\n        existing = into[parent]?\n        bucket = if existing.is_a?(Array)\n                   existing\n                 else\n                   [] of Roundhouse::ParamValue\n                 end\n        bucket << val.as(Roundhouse::ParamValue)\n        into[parent] = bucket\n      else\n        # `parent[child]=v` — nested hash.\n        existing = into[parent]?\n        bucket = if existing.is_a?(Hash)\n                   existing\n                 else\n                   {} of String => Roundhouse::ParamValue\n                 end\n        bucket[inner] = val\n        into[parent] = bucket\n      end\n    end\n  end\nend\n"},{"path":"src/session.cr","content":"# Generated from runtime/ruby/action_dispatch/session.rb at app emit time.\n# Do not edit by hand — edit the source `.rb` and re-run emit.\n\nmodule ActionDispatch\n  class Session\n    @data : Hash(String, String?)\n\n    def initialize(other = nil)\n      @data = {} of String => String??\n      return if other.nil?\n      keys = other.keys\n      i = 0_i64\n      while i < keys.size\n        k = keys[i]\n        v = other[k]\n        @data.not_nil![k.to_s] = v\n        i += 1_i64\n      end\n    end\n\n    def [](key)\n      k = key.to_s\n      return @data[k] if @data.has_key?(k)\n      nil\n    end\n\n    def []=(key, value)\n      @data[key.to_s] = value\n      value\n    end\n\n    def fetch(key, default = nil)\n      k = key.to_s\n      return @data[k] if @data.has_key?(k)\n      default\n    end\n\n    def key?(key)\n      @data.has_key?(key.to_s)\n    end\n\n    def has_key?(key)\n      @data.has_key?(key.to_s)\n    end\n\n    def include?(key)\n      @data.has_key?(key.to_s)\n    end\n\n    def delete(key)\n      @data.delete(key.to_s)\n    end\n\n    def length\n      @data.size\n    end\n\n    def size\n      @data.size\n    end\n\n    def empty? : Bool\n      @data.empty?\n    end\n\n    def keys : Array(String)\n      @data.keys\n    end\n\n    def values\n      @data.values\n    end\n\n    def each\n      keys = @data.keys\n      i = 0_i64\n      while i < keys.size\n        k = keys[i]\n        v = @data[k]\n        yield k, v\n        i += 1_i64\n      end\n      self\n    end\n\n    def to_h\n      @data\n    end\n\n    def merge(other)\n      result = ::ActionDispatch::Session.new(to_h)\n      other.each do |k, v| result[k] = v end\n      result\n    end\n  end\nend\n"},{"path":"src/test_helper.cr","content":"# Test base class for emitted Crystal specs. Provides the per-test\n# isolation harness (`RoundhouseTest.discover`), the\n# ActionDispatch::IntegrationTest surface (get/post/etc., assert_\n# response/redirected_to/select), and the few assertion methods that\n# the inline-assertion lowerer (`src/lower/test_module_to_library/\n# inline_assertions.rs`) deliberately leaves unrewritten — those\n# whose semantics differ enough across targets that uniform inline\n# emit isn't safe.\n#\n# **What's emitted inline as `raise unless …`** (NOT defined here):\n#   assert / assert_not / refute, assert_equal / refute_equal,\n#   assert_nil / refute_nil, assert_empty / refute_empty,\n#   assert_includes / refute_includes, assert_kind_of,\n#   assert_instance_of, assert_predicate / refute_predicate,\n#   assert_raises, assert_difference / assert_no_difference.\n#\n# **Kept here** (cross-target friction at the lowering level):\n#   - `assert_match`: Crystal's `Regex#matches?` requires `String`\n#     (not nilable); the inline lowering's `pat.match?(val)` shape\n#     hits Crystal's strict null-check. Nilable-handling stays in\n#     the helper.\n#   - `assert_operator`: Class-subclass `<` checks are a Ruby/Crystal\n#     idiom with no TS analog; lowering would have to translate per-\n#     target. Kept here in symmetric form across targets.\n#\n# Discovery: each emitted test class invokes `RoundhouseTest.discover`\n# at the bottom of its file. The macro walks the class's instance\n# methods at compile time, generating one `it \"<name>\"` Spec block\n# per `test_*` method. Each `it` instantiates a fresh test object and\n# calls the matching method, mirroring Minitest's per-test isolation.\n\nrequire \"spec\"\n\nabstract class RoundhouseTest\n  # Accepts `String?` so callers can pass nilable values directly\n  # (e.g. `err.message` from a Crystal Exception returns `String?`);\n  # nil fails the assertion the same as a non-matching string.\n  def assert_match(pattern, value : String?, msg : String? = nil) : Nil\n    if value.nil?\n      fail(msg || \"expected non-nil string to match #{pattern.inspect}\")\n    end\n    re = pattern.is_a?(Regex) ? pattern.as(Regex) : Regex.new(pattern.to_s)\n    fail(msg || \"expected #{value.inspect} to match #{re.inspect}\") unless re.matches?(value)\n  end\n\n  # Ruby's `assert_operator a, :op, b` — eval `a.op(b)` and assert truthy.\n  # Symbol-shaped op names (':<', ':>') and the bare form both accepted.\n  # Class-subclass `<` (e.g. `assert_operator A, :<, B` for \"A is a\n  # subclass of B\") works natively in Crystal — class `<` is the\n  # subclass relation.\n  def assert_operator(left, op, right, msg : String? = nil) : Nil\n    op_str = op.to_s.lstrip(':')\n    result = case op_str\n             when \"<\"  then left < right\n             when \">\"  then left > right\n             when \"<=\" then left <= right\n             when \">=\" then left >= right\n             when \"==\" then left == right\n             when \"!=\" then left != right\n             else\n               fail(msg || \"assert_operator: unsupported op #{op}\")\n             end\n    fail(msg || \"expected #{left.inspect} #{op_str} #{right.inspect}\") unless result\n  end\n\n  def flunk(msg : String? = nil) : Nil\n    fail(msg || \"flunked\")\n  end\n\n  def skip(msg : String? = nil) : Nil\n    raise Spec::SpecSkip.new(msg || \"skipped\", file: __FILE__, line: __LINE__)\n  end\n\n  # ── ActionDispatch::IntegrationTest surface ──────────────────────\n  #\n  # In-process dispatch via `Routes.table` + the registered controller\n  # registry. Mirrors `runtime/typescript/minitest.ts:290-371` and\n  # spinel's `dispatch_request` (`runtime/spinel/test/test_helper.rb:\n  # 219-248`). Tests stash status / body / location on the test\n  # instance for subsequent `assert_response` / `assert_select` /\n  # `assert_redirected_to` checks.\n  @__body : String = \"\"\n  @__status : Int64 = 0_i64\n  @__location : String = \"\"\n  @__session : ::ActionDispatch::Session = ::ActionDispatch::Session.new\n  @__flash : ::ActionDispatch::Flash = ::ActionDispatch::Flash.new\n\n  def get(path : String, params = nil) : Nil\n    _ = params\n    dispatch(\"GET\", path, {} of String => Roundhouse::ParamValue)\n  end\n\n  def post(path : String, params = nil) : Nil\n    dispatch(\"POST\", path, normalize_params(params))\n  end\n\n  def put(path : String, params = nil) : Nil\n    dispatch(\"PUT\", path, normalize_params(params))\n  end\n\n  def patch(path : String, params = nil) : Nil\n    dispatch(\"PATCH\", path, normalize_params(params))\n  end\n\n  def delete(path : String, params = nil) : Nil\n    _ = params\n    dispatch(\"DELETE\", path, {} of String => Roundhouse::ParamValue)\n  end\n\n  def head(path : String, params = nil) : Nil\n    _ = params\n    dispatch(\"HEAD\", path, {} of String => Roundhouse::ParamValue)\n  end\n\n  # Test fixtures pass NamedTuple-shape params\n  # (`params: {article: {title: \"…\"}}`); the wire-level request body\n  # is `Hash(String, ParamValue)`. Recursively stringify keys and\n  # narrow Symbol leaves to their String form, matching what the\n  # production form-body parser would produce. Untyped param admits\n  # NamedTuple, Hash, or nil from each call site.\n  private def normalize_params(params) : Hash(String, Roundhouse::ParamValue)\n    out = {} of String => Roundhouse::ParamValue\n    case params\n    when Hash\n      params.each { |k, v| out[k.to_s] = nested_param_value(v) }\n    when NamedTuple\n      params.each { |k, v| out[k.to_s] = nested_param_value(v) }\n    end\n    out\n  end\n\n  private def nested_param_value(v) : Roundhouse::ParamValue\n    case v\n    when String\n      v\n    when Symbol\n      v.to_s\n    when Hash\n      inner = {} of String => Roundhouse::ParamValue\n      v.each { |kk, vv| inner[kk.to_s] = nested_param_value(vv) }\n      inner\n    when Array\n      v.map { |elem| nested_param_value(elem) }.as(Roundhouse::ParamValue)\n    when NamedTuple\n      inner = {} of String => Roundhouse::ParamValue\n      v.each { |kk, vv| inner[kk.to_s] = nested_param_value(vv) }\n      inner\n    else\n      v.to_s\n    end\n  end\n\n  private def dispatch(\n    method : String,\n    path : String,\n    body : Hash(String, Roundhouse::ParamValue),\n  ) : Nil\n    ::ActionView::ViewHelpers.reset_slots!\n    matched = ::ActionDispatch::Router.match(method, path, RoundhouseTest.routes)\n    if matched.nil?\n      fail(\"no route for #{method} #{path}\")\n    end\n    matched = matched.not_nil!\n    ctrl_class = RoundhouseTest.controllers[matched.controller]?\n    if ctrl_class.nil?\n      fail(\"no controller registered for #{matched.controller}\")\n    end\n    merged = {} of String => Roundhouse::ParamValue\n    matched.path_params.each { |k, v| merged[k] = v.as(Roundhouse::ParamValue) }\n    body.each { |k, v| merged[k] = v }\n    ctrl = ctrl_class.not_nil!.new\n    ctrl.params = merged\n    ctrl.session = @__session\n    ctrl.flash = @__flash\n    ctrl.request_method = method\n    ctrl.request_path = path\n    ctrl.process_action(matched.action)\n    @__body = ctrl.body || \"\"\n    @__status = ctrl.status || 200_i64\n    @__location = ctrl.location || \"\"\n    @__flash = ctrl.flash\n  end\n\n  # ── HTTP response assertions ─────────────────────────────────────\n\n  STATUS_SYMBOLS = {\n    success:              200..299,\n    redirect:             300..399,\n    missing:              404,\n    not_found:            404,\n    error:                500..599,\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    bad_request:          400,\n    unauthorized:         401,\n    forbidden:            403,\n    unprocessable_entity: 422,\n    # Rails 8.1.x scaffold renamed `:unprocessable_entity` →\n    # `:unprocessable_content` mid-version (HTTP 422 description\n    # churn). Alias both so emit follows whichever the fixture's\n    # scaffold currently produces.\n    unprocessable_content: 422,\n    internal_server_error: 500,\n  }\n\n  def assert_response(expected, msg : String? = nil) : Nil\n    actual = @__status.to_i32\n    matched = case expected\n              when Int\n                expected.to_i32 == actual\n              when Symbol\n                rng = STATUS_SYMBOLS[expected]?\n                case rng\n                when Range then rng.includes?(actual)\n                when Int   then rng.to_i32 == actual\n                else false\n                end\n              else\n                false\n              end\n    return if matched\n    body_preview = @__body[0, 200]? || @__body\n    fail(msg || \"expected response #{expected.inspect}, got status=#{actual} body=#{body_preview.inspect}\")\n  end\n\n  def assert_redirected_to(expected_path : String, msg : String? = nil) : Nil\n    if @__status < 300 || @__status >= 400\n      fail(msg || \"expected a redirect, got status=#{@__status} location=#{@__location.inspect}\")\n    end\n    return if @__location.includes?(expected_path)\n    fail(msg || \"expected Location to contain #{expected_path.inspect}, got #{@__location.inspect}\")\n  end\n\n  # `assert_select` substring-matches on the opening tag or\n  # id=\"x\" / class=\"x\"-style fragment derived from the selector.\n  # Rough but effective for the scaffold-blog HTML shapes —\n  # bodies like `\"#articles\"`, `\".p-4\"`, `\"h1\"`. Block form\n  # additionally yields so nested `assert_select`s further narrow\n  # within the matched section; we don't shrink the body here, so\n  # nested checks still see the full response body — same loose\n  # semantic as the TS shim.\n  def assert_select(selector : String, content : String? = nil, msg : String? = nil) : Nil\n    fragment = selector_fragment(selector)\n    unless @__body.includes?(fragment)\n      fail(msg || \"expected body to match selector #{selector.inspect} (looked for #{fragment.inspect})\")\n      return\n    end\n    if !content.nil? && !@__body.includes?(content)\n      fail(msg || \"expected body to contain #{content.inspect} matching selector #{selector.inspect}\")\n    end\n  end\n\n  # Kwarg form — `assert_select(\"h2\", minimum: 1, maximum: 5)` — Rails\n  # passes `minimum:` / `maximum:` / `count:` for cardinality checks.\n  # The substring-match shim treats these as best-effort no-ops; the\n  # selector-presence check below is sufficient for the scaffold-blog\n  # shapes.\n  def assert_select(selector : String, **opts) : Nil\n    _ = opts\n    assert_select(selector)\n  end\n\n  def assert_select(selector : String, **opts, &block) : Nil\n    _ = opts\n    assert_select(selector)\n    yield\n  end\n\n  def assert_select(selector : String, &block) : Nil\n    assert_select(selector)\n    yield\n  end\n\n  private def selector_fragment(selector : String) : String\n    first = selector.split(/\\s+/).first\n    case first\n    when .starts_with?(\"#\") then %(id=\"#{first[1..]}\")\n    when .starts_with?(\".\") then %(#{first[1..]}\")\n    else                         \"<#{first}\"\n    end\n  end\n\n  # ── per-test registry + reset hooks ──────────────────────────────\n  #\n  # Routes table, controller registry, fixture loaders, and the\n  # schema reset SQL live as class state on `RoundhouseTest`. The\n  # emitted `src/test_setup.cr` registers them at process-init time;\n  # the per-test `before_each` (installed by the `inherited` macro\n  # below) resets the in-memory DB and re-runs each fixture loader\n  # so every spec starts from a clean state.\n\n  alias RouteRow = ::ActionDispatch::Router::Route\n\n  @@routes : Array(RouteRow) = [] of RouteRow\n  @@controllers : Hash(Symbol, ::ActionController::Base.class) =\n    {} of Symbol => ::ActionController::Base.class\n  @@fixture_loaders : Array(-> Nil) = [] of -> Nil\n  @@schema_sql : String = \"\"\n\n  def self.routes : Array(RouteRow)\n    @@routes\n  end\n\n  def self.routes=(value : Array(RouteRow)) : Array(RouteRow)\n    @@routes = value\n  end\n\n  def self.controllers : Hash(Symbol, ::ActionController::Base.class)\n    @@controllers\n  end\n\n  def self.controllers=(value : Hash(Symbol, ::ActionController::Base.class)) : Hash(Symbol, ::ActionController::Base.class)\n    @@controllers = value\n  end\n\n  def self.fixture_loaders : Array(-> Nil)\n    @@fixture_loaders\n  end\n\n  def self.fixture_loaders=(value : Array(-> Nil)) : Array(-> Nil)\n    @@fixture_loaders = value\n  end\n\n  def self.schema_sql : String\n    @@schema_sql\n  end\n\n  def self.schema_sql=(value : String) : String\n    @@schema_sql = value\n  end\n\n  # Reset in-memory DB to a fresh schema and reload every registered\n  # fixture set. Called from the `before_each` block in the discover\n  # macro so each spec sees the canonical starting state.\n  #\n  # No-ops cleanly when the app carries no schema / no fixtures —\n  # framework-test harnesses (router_test, view_helpers_test, etc.)\n  # exercise the runtime layer directly without a Rails-shape app\n  # underneath, so their `src/test_setup.cr` skips the schema and\n  # fixture registrations.\n  def self.reset_and_load_fixtures : Nil\n    return if @@schema_sql.empty?\n    Roundhouse::Db.setup_test_db(@@schema_sql)\n    ::ActiveRecord.adapter = Roundhouse::SqliteAdapter.new\n    @@fixture_loaders.each(&.call)\n  end\n\n  # Bridge the assertion failure into Spec's expectation channel —\n  # Spec catches `Spec::AssertionFailed` and reports it as a failed `it`.\n  private def fail(msg : String) : Nil\n    raise Spec::AssertionFailed.new(msg, file: __FILE__, line: __LINE__)\n  end\n\n  # ── discovery macro ──────────────────────────────────────────────\n  #\n  # Generate `describe <Klass> do … it \"test_X\" do <Klass>.new.test_X; end … end`\n  # at the bottom of the test file. Walks the class's own instance\n  # methods at compile time; each `test_*` method becomes one spec.\n  # Crystal's `spec` autorun fires when `require \"spec\"` is loaded and\n  # the program reaches main, so the test_helper itself doesn't need\n  # an explicit runner.\n  #\n  # `before_each` wraps every `it` with the DB-reset + fixture-reload\n  # so specs start from the canonical state.\n  macro inherited\n    macro finished\n      describe \\{{ @type }} do\n        before_each do\n          RoundhouseTest.reset_and_load_fixtures\n        end\n        \\{% for m in @type.methods %}\n          \\{% if m.name.starts_with?(\"test_\") %}\n            it \\{{ m.name.stringify }} do\n              \\{{ @type }}.new.\\{{ m.name.id }}\n            end\n          \\{% end %}\n        \\{% end %}\n      end\n    end\n  end\nend\n"},{"path":"src/test_setup.cr","content":"# Generated by Roundhouse — Crystal test setup.\n#\n# Registers schema, routes, controllers, and fixture loaders\n# with `RoundhouseTest` class state at program load. Specs\n# pull this in via `app.cr`'s alphabetical sweep; main.cr's\n# production entrypoint doesn't reference this state.\n#\n# Each registration is gated on the corresponding artifact\n# existing. Framework-only test harnesses (router_test,\n# view_helpers_test, etc.) carry test_modules but no\n# schema/routes/controllers — emitting those references\n# unconditionally would trip `undefined constant Schema`.\n\nRoundhouseTest.schema_sql = Schema.statements.join(\";\\n\")\nRoundhouseTest.routes = Routes.table\nRoundhouseTest.controllers = {\n  :application => ApplicationController,\n  :articles => ArticlesController,\n  :comments => CommentsController,\n} of Symbol => ::ActionController::Base.class\nRoundhouseTest.fixture_loaders = [\n  -> { ArticlesFixtures._fixtures_load!; nil },\n  -> { CommentsFixtures._fixtures_load!; nil },\n] of -> Nil\n"},{"path":"src/test_support.cr","content":"# Roundhouse Crystal test-support runtime.\n#\n# Hand-written, shipped alongside generated code (copied in by the\n# Crystal emitter as `src/test_support.cr`). Controller specs call\n# into `TestClient` for HTTP dispatch (pure in-process — no real\n# server) and the returned `TestResponse` for Rails-compatible\n# assertions (`assert_ok`, `assert_redirected_to`, `assert_select`).\n#\n# Mirrors `runtime/typescript/test_support.ts` and\n# `runtime/rust/test_support.rs` in intent, shape, and assertion\n# semantics — substring-match on the response body, loose-but-\n# reliable for the scaffold blog's HTML. A later phase can swap in\n# a real HTML parser (Crystal's XML::Node) by touching only this\n# file; emitted spec bodies are insulated via method contracts.\n\nrequire \"./http\"\n\nmodule Roundhouse\n  module TestSupport\n    # Pure-Crystal test client — dispatches through Router.match,\n    # calls the resolved handler, wraps the response. No real HTTP,\n    # no event-loop glue. Fast + leak-free across specs.\n    class TestClient\n      def get(path : String) : TestResponse\n        dispatch(\"GET\", path, {} of String => String)\n      end\n\n      def post(path : String, body : Hash(String, String) = {} of String => String) : TestResponse\n        dispatch(\"POST\", path, body)\n      end\n\n      def patch(path : String, body : Hash(String, String) = {} of String => String) : TestResponse\n        dispatch(\"PATCH\", path, body)\n      end\n\n      def delete(path : String) : TestResponse\n        dispatch(\"DELETE\", path, {} of String => String)\n      end\n\n      private def dispatch(method : String, path : String, body : Hash(String, String)) : TestResponse\n        result = Roundhouse::Http::Router.match(method, path)\n        raise \"no route for #{method} #{path}\" if result.nil?\n        handler, path_params = result\n        merged = path_params.merge(body)\n        response = handler.call(Roundhouse::Http::ActionContext.new(merged))\n        TestResponse.new(response)\n      end\n    end\n\n    # Wrapper around `ActionResponse` exposing assertion helpers.\n    # Method names mirror Rails' Minitest HTTP assertions; bodies\n    # substring-match for `assert_select`-style queries.\n    class TestResponse\n      getter body : String\n      getter status : Int32\n      getter location : String\n\n      def initialize(raw : Roundhouse::Http::ActionResponse)\n        @body = raw.body\n        @status = raw.status\n        @location = raw.location\n      end\n\n      # `assert_response :success` — status 200 OK.\n      def assert_ok : Nil\n        raise \"expected 200 OK, got #{@status}\" unless @status == 200\n      end\n\n      # `assert_response :unprocessable_entity` — status 422.\n      def assert_unprocessable : Nil\n        raise \"expected 422 Unprocessable Entity, got #{@status}\" unless @status == 422\n      end\n\n      # `assert_response <code>`.\n      def assert_status(code : Int32) : Nil\n        raise \"expected status #{code}, got #{@status}\" unless @status == code\n      end\n\n      # `assert_redirected_to <path>` — status is 3xx and Location\n      # substring-matches the expected path. Loose to tolerate\n      # absolute-vs-relative URL differences.\n      def assert_redirected_to(path : String) : Nil\n        raise \"expected a redirection, got #{@status}\" unless @status >= 300 && @status < 400\n        unless @location.includes?(path)\n          raise \"expected Location to contain #{path.inspect}, got #{@location.inspect}\"\n        end\n      end\n\n      # `assert_select <selector>` — body contains a match for the\n      # selector. Substring-matches on the opening tag or\n      # `id=`/`class=` fragment. Covers the scaffold blog shapes:\n      #   \"h1\"        → contains \"<h1\"\n      #   \"#articles\" → contains `id=\"articles\"`\n      #   \".p-4\"      → contains `p-4\"`\n      #   \"form\"      → contains \"<form\"\n      def assert_select(selector : String) : Nil\n        fragment = TestSupport.selector_fragment(selector)\n        unless @body.includes?(fragment)\n          raise \"expected body to match selector #{selector.inspect} (looked for #{fragment.inspect})\"\n        end\n      end\n\n      # `assert_select <selector>, <text>` — selector check + body\n      # also contains the text.\n      def assert_select_text(selector : String, text : String) : Nil\n        assert_select(selector)\n        unless @body.includes?(text)\n          raise \"expected body to contain text #{text.inspect} under selector #{selector.inspect}\"\n        end\n      end\n\n      # `assert_select <selector>, minimum: N` — at least `n`\n      # occurrences of the selector fragment.\n      def assert_select_min(selector : String, n : Int32) : Nil\n        fragment = TestSupport.selector_fragment(selector)\n        count = 0\n        from = 0\n        while (i = @body.index(fragment, from))\n          count += 1\n          from = i + fragment.size\n        end\n        if count < n\n          raise \"expected at least #{n} matches for selector #{selector.inspect}, got #{count}\"\n        end\n      end\n    end\n\n    # Loose selector → substring fragment. Same rules as the Rust\n    # and TS twins.\n    def self.selector_fragment(selector : String) : String\n      first = selector.split.first? || \"\"\n      case first[0]?\n      when '#'\n        %(id=\"#{first[1..]}\")\n      when '.'\n        %(#{first[1..]}\")\n      else\n        \"<#{first}\"\n      end\n    end\n  end\nend\n"},{"path":"src/view_helpers.cr","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\nHTML_ESCAPES = { \"&\" => \"&amp;\", \"<\" => \"&lt;\", \">\" => \"&gt;\", \"\\\"\" => \"&quot;\", \"'\" => \"&#39;\" }\nHTML_ESCAPE_PATTERN = /[&<>\"']/\n\nmodule ActionView\n  module ViewHelpers\n    @@slots : Hash(Symbol, String) = {} of Symbol => String\n\n    def self.reset_slots! : Nil\n      @@slots = {} of Symbol => String\n    end\n\n    def self.content_for_set(slot : Symbol, value : String) : Nil\n      @@slots[slot] = value\n      nil\n    end\n\n    def self.content_for_get(slot : Symbol) : String?\n      @@slots[slot]?\n    end\n\n    def self.get_slot(slot : Symbol) : String\n      @@slots[slot]? || \"\"\n    end\n\n    def self.get_yield : String\n      @@slots[:__body__]? || \"\"\n    end\n\n    def self.set_yield(content : String) : Nil\n      @@slots[:__body__] = content\n      nil\n    end\n\n    def self.html_escape(s : String) : String\n      s.gsub(HTML_ESCAPE_PATTERN, HTML_ESCAPES)\n    end\n\n    def self.truncate(s : String, length : Int64 = 30_i64, omission : String = \"...\") : String\n      return s if s.size <= length\n      cutoff = length - omission.size\n      cutoff = 0_i64 if cutoff < 0_i64\n      \"#{s[0_i64, cutoff]}#{omission}\"\n    end\n\n    def self.dom_id(record : ActiveRecord::Base, suffix : Symbol? = nil) : String\n      if suffix.nil?\n        \"#{record.dom_prefix}_#{record.id}\"\n      else\n        \"#{suffix}_#{record.dom_prefix}_#{record.id}\"\n      end\n    end\n\n    def self.link_to(text, href, opts = {} of String => String)\n      attrs = render_attrs({:href => href}.merge(opts.to_h))\n      \"<a#{attrs}>#{html_escape(text)}</a>\"\n    end\n\n    def self.button_to(text, href, opts = {} of String => String)\n      method = opts[:method]?\n      form_class = opts[:form_class]?\n      inner_opts = opts.to_h.dup\n      inner_opts.delete(:method)\n      inner_opts.delete(:form_class)\n      form_attrs = {action: href, method: \"post\"}.to_h\n      form_attrs[:class] = (form_class || \"button_to\").to_s\n      button_attrs = render_attrs({:type => \"submit\"}.merge(inner_opts))\n      method_input = if !(method.nil?) && method.to_s != \"post\"\n        \"<input type=\\\"hidden\\\" name=\\\"_method\\\" value=\\\"#{method}\\\">\"\n      else\n        \"\"\n      end\n      auth_token_input = \"<input type=\\\"hidden\\\" name=\\\"authenticity_token\\\" value=\\\"\\\">\"\n      \"<form#{render_attrs(form_attrs)}>#{method_input}<button#{button_attrs}>#{html_escape(text)}</button>#{auth_token_input}</form>\"\n    end\n\n    def self.csrf_meta_tags : String\n      \"<meta name=\\\"csrf-param\\\" content=\\\"authenticity_token\\\" />\\n<meta name=\\\"csrf-token\\\" content=\\\"\\\" />\"\n    end\n\n    def self.csp_meta_tag : String\n      \"\"\n    end\n\n    def self.stylesheet_link_tag(name, opts = {} of String => String)\n      href = \"/assets/#{name}.css\"\n      attrs = render_attrs({:rel => \"stylesheet\", :href => href}.merge(opts.to_h))\n      \"<link#{attrs}>\"\n    end\n\n    def self.javascript_importmap_tags(pins : Array(NamedTuple(name: String, path: String))? = nil, entry : String = \"application\") : String\n      if pins.nil? || pins.empty?\n        json = \"{\\n  \\\"imports\\\": {\\n    \\\"@hotwired/turbo\\\": \\\"/assets/turbo.min.js\\\"\\n  }\\n}\"\n        return \"<script type=\\\"importmap\\\" data-turbo-track=\\\"reload\\\">\" + json + \"</script>\" + \"\\n\" + \"<link rel=\\\"modulepreload\\\" href=\\\"/assets/turbo.min.js\\\">\" + \"\\n\" + \"<script type=\\\"module\\\">import \\\"@hotwired/turbo\\\"</script>\"\n      end\n      import_lines = pins.map { |p| \"    \\\"#{p[:name]}\\\": \\\"#{p[:path]}\\\"\" }.join(\",\\n\")\n      json = \"{\\n  \\\"imports\\\": {\\n\" + import_lines + \"\\n  }\\n}\"\n      parts = [] of String\n      parts << \"<script type=\\\"importmap\\\" data-turbo-track=\\\"reload\\\">\" + json + \"</script>\"\n      pins.each do |p| parts << \"<link rel=\\\"modulepreload\\\" href=\\\"#{p[:path]}\\\">\" end\n      parts << \"<script type=\\\"module\\\">import \\\"#{entry}\\\"</script>\"\n      parts.join(\"\\n\")\n    end\n\n    def self.turbo_stream_from(stream)\n      encoded = Base64.strict_encode(stream.to_json)\n      \"<turbo-cable-stream-source channel=\\\"Turbo::StreamsChannel\\\" signed-stream-name=\\\"#{encoded}--unsigned\\\"></turbo-cable-stream-source>\"\n    end\n\n    def self.csrf_token_hidden_input : String\n      \"<input type=\\\"hidden\\\" name=\\\"authenticity_token\\\" value=\\\"\\\">\"\n    end\n\n    def self.method_override_input(method : Symbol) : String\n      method_str = method.to_s\n      if method_str == \"get\" || method_str == \"post\"\n        \"\"\n      else\n        \"<input type=\\\"hidden\\\" name=\\\"_method\\\" value=\\\"#{method_str}\\\">\"\n      end\n    end\n\n    def self.optional_value_attr(value)\n      if value.nil? || value.to_s.empty?\n        \"\"\n      else\n        \" value=\\\"#{html_escape(value.to_s)}\\\"\"\n      end\n    end\n\n    def self.escape_or_empty(value)\n      if value.nil?\n        \"\"\n      else\n        html_escape(value.to_s)\n      end\n    end\n\n    def self.render_attrs(attrs)\n      return \"\" if attrs.empty?\n      pairs = [] of String\n      attrs.each do |k, v|\n        next if v.nil?\n        name = k.to_s\n        if (v.is_a?(Hash) || v.is_a?(NamedTuple))\n          v.each do |inner_k, inner_v|\n            next if inner_v.nil?\n            inner_name = inner_k.to_s.tr(\"_\", \"-\")\n            pairs << \" #{name}-#{inner_name}=\\\"#{html_escape(inner_v.to_s)}\\\"\"\n          end\n        else\n          pairs << \" #{name}=\\\"#{html_escape(v.to_s)}\\\"\"\n        end\n      end\n      pairs.join\n    end\n  end\nend\n"},{"path":"src/views/articles/_article.cr","content":"module Views\n  module Articles\n    def self.article(article : Article, notice : String? = nil, alert : String? = nil) : String\n      io = String::Builder.new\n      io << \"<div id=\\\"#{::ActionView::ViewHelpers.dom_id(article)}\\\" 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      <a href=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.article_path(article.id.not_nil!))}\\\" class=\\\"#{\"text-blue-600 hover:underline\"}\\\">#{::ActionView::ViewHelpers.html_escape(article.title.not_nil!)}</a>\\n      <span id=\\\"#{::ActionView::ViewHelpers.dom_id(article, :comments_count)}\\\" class=\\\"text-gray-500 text-sm font-normal ml-2\\\">\\n        (#{Inflector.pluralize(article.comments.size, \"comment\")})\\n      </span>\\n    </h2>\\n    <p class=\\\"text-gray-700 mt-2\\\">#{::ActionView::ViewHelpers.html_escape(::ActionView::ViewHelpers.truncate(article.body.not_nil!, length: 100_i64))}</p>\\n  </div>\\n  <div class=\\\"w-full sm:w-auto flex flex-col sm:flex-row space-x-2 space-y-2\\\">\\n    <a href=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.article_path(article.id.not_nil!))}\\\" class=\\\"#{\"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\"}</a>\\n    <a href=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.edit_article_path(article.id.not_nil!))}\\\" class=\\\"#{\"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\"}</a>\\n    <form action=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.article_path(article.id.not_nil!))}\\\" method=\\\"post\\\" class=\\\"#{\"button_to\"}\\\">#{::ActionView::ViewHelpers.method_override_input(:delete)}<button type=\\\"submit\\\" class=\\\"#{\"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\"}\\\" data-turbo-confirm=\\\"#{\"Are you sure?\"}\\\">#{\"Destroy\"}</button>#{::ActionView::ViewHelpers.csrf_token_hidden_input.not_nil!}</form>\\n  </div>\\n</div>\\n\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/articles/_article_json.cr","content":"module Views\n  module Articles\n    def self.article_json(article : Article) : String\n      io = String::Builder.new\n      io << \"{\"\n      io << \"\\\"id\\\":\"\n      io << JsonBuilder.encode_value(article.id.not_nil!)\n      io << \",\"\n      io << \"\\\"title\\\":\"\n      io << JsonBuilder.encode_value(article.title.not_nil!)\n      io << \",\"\n      io << \"\\\"body\\\":\"\n      io << JsonBuilder.encode_value(article.body.not_nil!)\n      io << \",\"\n      io << \"\\\"created_at\\\":\"\n      io << JsonBuilder.encode_datetime(article.created_at.not_nil!)\n      io << \",\"\n      io << \"\\\"updated_at\\\":\"\n      io << JsonBuilder.encode_datetime(article.updated_at.not_nil!)\n      io << \",\"\n      io << \"\\\"url\\\":\"\n      io << JsonBuilder.encode_value(RouteHelpers.article_path(article.id.not_nil!) + \".json\")\n      io << \"}\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/articles/_form.cr","content":"module Views\n  module Articles\n    def self.form(article : Article, notice : String? = nil, alert : String? = nil) : String\n      io = String::Builder.new\n      form_method = if article.persisted?.not_nil!\n        :patch\n      else\n        :post\n      end\n      io << \"<form action=\\\"#{::ActionView::ViewHelpers.html_escape(if article.persisted?.not_nil!\n        RouteHelpers.article_path(article.id.not_nil!)\n      else\n        RouteHelpers.articles_path.not_nil!\n      end)}\\\" accept-charset=\\\"UTF-8\\\" method=\\\"post\\\" class=\\\"#{\"contents\"}\\\">#{::ActionView::ViewHelpers.method_override_input(form_method)}#{::ActionView::ViewHelpers.csrf_token_hidden_input.not_nil!}\\n\"\n      if ! article.errors.empty?\n        io << \"    <div id=\\\"error_explanation\\\" class=\\\"bg-red-50 text-red-500 px-3 py-2 font-medium rounded-md mt-3\\\">\\n      <h2>#{Inflector.pluralize(article.errors.size, \"error\")} prohibited this article from being saved:</h2>\\n\\n      <ul class=\\\"list-disc ml-6\\\">\\n        \"\n        article.errors.each do |error| io << \"\\n          <li>#{::ActionView::ViewHelpers.html_escape(error)}</li>\\n        \" end\n        io << \"\\n      </ul>\\n    </div>\\n\"\n      end\n      io << \"\\n  <div class=\\\"my-5\\\">\\n    <label for=\\\"article_title\\\">Title</label>\\n    <input type=\\\"text\\\" name=\\\"article[title]\\\" id=\\\"article_title\\\"#{::ActionView::ViewHelpers.optional_value_attr(article[:title])} class=\\\"#{\"block shadow-sm rounded-md border px-3 py-2 mt-2 w-full border-gray-400 focus:outline-blue-600\"}\\\">\\n  </div>\\n\\n  <div class=\\\"my-5\\\">\\n    <label for=\\\"article_body\\\">Body</label>\\n    <textarea name=\\\"article[body]\\\" id=\\\"article_body\\\" rows=\\\"#{::ActionView::ViewHelpers.html_escape(4_i64.to_s)}\\\" class=\\\"#{\"block shadow-sm rounded-md border px-3 py-2 mt-2 w-full border-gray-400 focus:outline-blue-600\"}\\\">#{::ActionView::ViewHelpers.escape_or_empty(article[:body])}</textarea>\\n  </div>\\n\\n  <div class=\\\"inline\\\">\\n    <input type=\\\"submit\\\" name=\\\"commit\\\" value=\\\"#{::ActionView::ViewHelpers.html_escape(if form_method == :patch\n        \"Update Article\"\n      else\n        \"Create Article\"\n      end)}\\\" data-disable-with=\\\"#{::ActionView::ViewHelpers.html_escape(if form_method == :patch\n        \"Update Article\"\n      else\n        \"Create Article\"\n      end)}\\\" class=\\\"#{\"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\"}\\\">\\n  </div>\\n</form>\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/articles/edit.cr","content":"module Views\n  module Articles\n    def self.edit(article : Article, notice : String? = nil, alert : String? = nil) : String\n      io = String::Builder.new\n      ::ActionView::ViewHelpers.content_for_set(:title, \"Editing article\")\n      io << \"\\n<div class=\\\"md:w-2/3 w-full\\\">\\n  <h1 class=\\\"font-bold text-4xl\\\">Editing article</h1>\\n\\n  #{Views::Articles.form(article)}\\n\\n  <a href=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.article_path(article.id.not_nil!))}\\\" class=\\\"#{\"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\"}</a>\\n  <a href=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.articles_path.not_nil!)}\\\" class=\\\"#{\"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\"}</a>\\n</div>\\n\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/articles/index.cr","content":"module Views\n  module Articles\n    def self.index(articles : Array(Article), notice : String? = nil, alert : String? = nil) : String\n      io = String::Builder.new\n      io << \"#{::ActionView::ViewHelpers.turbo_stream_from(\"articles\")}\\n\\n\"\n      ::ActionView::ViewHelpers.content_for_set(:title, \"Articles\")\n      io << \"\\n<div class=\\\"w-full\\\">\\n\"\n      if ! notice.nil? && ! notice.empty?\n        io << \"    <p class=\\\"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block\\\" id=\\\"notice\\\">#{::ActionView::ViewHelpers.html_escape(notice)}</p>\\n\"\n      end\n      io << \"\\n  <div class=\\\"flex justify-between items-center\\\">\\n    <h1 class=\\\"font-bold text-4xl\\\">Articles</h1>\\n    <a href=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.new_article_path.not_nil!)}\\\" class=\\\"#{\"rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium\"}\\\">#{\"New article\"}</a>\\n  </div>\\n\\n  <div id=\\\"articles\\\" class=\\\"min-w-full divide-y divide-gray-200 space-y-5\\\">\\n\"\n      if ! articles.empty?\n        io << \"      \"\n        articles.each { |a| io << Views::Articles.article(a) }\n        io << \"\\n\"\n      else\n        io << \"      <p class=\\\"text-center my-10\\\">No articles found.</p>\\n\"\n      end\n      io << \"  </div>\\n</div>\\n\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/articles/index_json.cr","content":"module Views\n  module Articles\n    def self.index_json(articles : Array(Article)) : String\n      io = String::Builder.new\n      io << \"[\"\n      io << articles.map { |article| Views::Articles.article_json(article) }.join(\",\")\n      io << \"]\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/articles/new.cr","content":"module Views\n  module Articles\n    def self.new(article : Article, notice : String? = nil, alert : String? = nil) : String\n      io = String::Builder.new\n      ::ActionView::ViewHelpers.content_for_set(:title, \"New article\")\n      io << \"\\n<div class=\\\"md:w-2/3 w-full\\\">\\n  <h1 class=\\\"font-bold text-4xl\\\">New article</h1>\\n\\n  #{Views::Articles.form(article)}\\n\\n  <a href=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.articles_path.not_nil!)}\\\" class=\\\"#{\"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\"}</a>\\n</div>\\n\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/articles/show.cr","content":"module Views\n  module Articles\n    def self.show(article : Article, notice : String? = nil, alert : String? = nil) : String\n      io = String::Builder.new\n      ::ActionView::ViewHelpers.content_for_set(:title, \"Showing article\")\n      io << \"\\n<div class=\\\"md:w-2/3 w-full\\\">\\n\"\n      if ! notice.nil? && ! notice.empty?\n        io << \"    <p class=\\\"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block\\\" id=\\\"notice\\\">#{::ActionView::ViewHelpers.html_escape(notice)}</p>\\n\"\n      end\n      io << \"\\n  <h1 class=\\\"font-bold text-4xl\\\">#{::ActionView::ViewHelpers.html_escape(article.title.not_nil!)}</h1>\\n\\n  <div class=\\\"my-4\\\">\\n    <p class=\\\"text-gray-700\\\">#{::ActionView::ViewHelpers.html_escape(article.body.not_nil!)}</p>\\n  </div>\\n\\n  <a href=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.edit_article_path(article.id.not_nil!))}\\\" class=\\\"#{\"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\"}</a>\\n  <a href=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.articles_path.not_nil!)}\\\" class=\\\"#{\"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\"}</a>\\n  <form action=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.article_path(article.id.not_nil!))}\\\" method=\\\"post\\\" class=\\\"#{\"sm:inline-block mt-2 sm:mt-0 sm:ml-2\"}\\\">#{::ActionView::ViewHelpers.method_override_input(:delete)}<button type=\\\"submit\\\" class=\\\"#{\"w-full rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer\"}\\\" data-turbo-confirm=\\\"#{\"Are you sure?\"}\\\">#{\"Destroy this article\"}</button>#{::ActionView::ViewHelpers.csrf_token_hidden_input.not_nil!}</form>\\n</div>\\n\\n<hr class=\\\"my-8\\\">\\n\\n<h2 class=\\\"text-xl font-bold mb-4\\\">Comments</h2>\\n\\n#{::ActionView::ViewHelpers.turbo_stream_from(\"article_#{article.id.not_nil!}_comments\")}\\n\\n<div id=\\\"comments\\\" class=\\\"space-y-4 mb-8\\\">\\n  \"\n      article.comments.each { |c| io << Views::Comments.comment(c) }\n      io << \"\\n</div>\\n\\n<h3 class=\\\"text-lg font-semibold mb-2\\\">Add a Comment</h3>\\n\\n\"\n      form_record = Comment.new\n      form_method = :post\n      io << \"<form action=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.article_comments_path(article.id.not_nil!))}\\\" accept-charset=\\\"UTF-8\\\" method=\\\"post\\\" class=\\\"#{\"space-y-4\"}\\\">#{::ActionView::ViewHelpers.method_override_input(form_method)}#{::ActionView::ViewHelpers.csrf_token_hidden_input.not_nil!}\\n  <div>\\n    <label for=\\\"comment_commenter\\\" class=\\\"#{\"block font-medium\"}\\\">Commenter</label>\\n    <input type=\\\"text\\\" name=\\\"comment[commenter]\\\" id=\\\"comment_commenter\\\"#{::ActionView::ViewHelpers.optional_value_attr(form_record[:commenter])} class=\\\"#{\"block w-full border rounded p-2\"}\\\">\\n  </div>\\n  <div>\\n    <label for=\\\"comment_body\\\" class=\\\"#{\"block font-medium\"}\\\">Body</label>\\n    <textarea name=\\\"comment[body]\\\" id=\\\"comment_body\\\" rows=\\\"#{::ActionView::ViewHelpers.html_escape(3_i64.to_s)}\\\" class=\\\"#{\"block w-full border rounded p-2\"}\\\">#{::ActionView::ViewHelpers.escape_or_empty(form_record[:body])}</textarea>\\n  </div>\\n  <input type=\\\"submit\\\" name=\\\"commit\\\" value=\\\"#{\"Add Comment\"}\\\" data-disable-with=\\\"#{\"Add Comment\"}\\\" class=\\\"#{\"bg-blue-600 text-white px-4 py-2 rounded\"}\\\">\\n</form>\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/articles/show_json.cr","content":"module Views\n  module Articles\n    def self.show_json(article : Article) : String\n      io = String::Builder.new\n      io << Views::Articles.article_json(article)\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/comments/_comment.cr","content":"module Views\n  module Comments\n    def self.comment(comment : Comment, notice : String? = nil, alert : String? = nil) : String\n      io = String::Builder.new\n      io << \"<div id=\\\"#{::ActionView::ViewHelpers.dom_id(comment)}\\\" class=\\\"p-4 bg-gray-50 rounded\\\">\\n  <p class=\\\"font-semibold\\\">#{::ActionView::ViewHelpers.html_escape(comment.commenter.not_nil!)}</p>\\n  <p class=\\\"text-gray-700\\\">#{::ActionView::ViewHelpers.html_escape(comment.body.not_nil!)}</p>\\n  <form action=\\\"#{::ActionView::ViewHelpers.html_escape(RouteHelpers.article_comment_path(comment.article_id.not_nil!, comment.id.not_nil!))}\\\" method=\\\"post\\\" class=\\\"#{\"button_to\"}\\\">#{::ActionView::ViewHelpers.method_override_input(:delete)}<button type=\\\"submit\\\" class=\\\"#{\"text-red-600 text-sm mt-2\"}\\\" data-turbo-confirm=\\\"#{\"Are you sure?\"}\\\">#{\"Delete\"}</button>#{::ActionView::ViewHelpers.csrf_token_hidden_input.not_nil!}</form>\\n</div>\\n\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/layouts/application.cr","content":"module Views\n  module Layouts\n    def self.application(body : String, notice : String? = nil, alert : String? = nil) : String\n      io = String::Builder.new\n      io << \"<!DOCTYPE html>\\n<html>\\n  <head>\\n    <title>#{::ActionView::ViewHelpers.html_escape(::ActionView::ViewHelpers.content_for_get(:title) || \"Real Blog\")}</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    #{::ActionView::ViewHelpers.csrf_meta_tags.not_nil!}\\n    #{::ActionView::ViewHelpers.csp_meta_tag.not_nil!}\\n\\n    #{::ActionView::ViewHelpers.get_slot(:head)}\\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    #{::ActionView::ViewHelpers.stylesheet_link_tag(\"application\", { :\"data-turbo-track\" => \"reload\" }) + \"\\n\" + ::ActionView::ViewHelpers.stylesheet_link_tag(\"tailwind\", { :\"data-turbo-track\" => \"reload\" })}\\n    #{::ActionView::ViewHelpers.javascript_importmap_tags(Importmap.pins, Importmap.entry.not_nil!)}\\n  </head>\\n\\n  <body>\\n    <main class=\\\"container mx-auto mt-28 px-5 flex flex-col\\\">\\n      #{body}\\n    </main>\\n  </body>\\n</html>\\n\"\n      io.to_s\n    end\n  end\nend\n"},{"path":"src/views/layouts/mailer.cr","content":"module Views\n  module Layouts\n    def self.mailer(body : String, notice : String? = nil, alert : String? = nil) : String\n      io = String::Builder.new\n      io << \"<!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    #{body}\\n  </body>\\n</html>\\n\"\n      io.to_s\n    end\n  end\nend\n"}]}