{"record":{"id":"9ce1feedf90ed60b","repo":"coding-horror/basic-computer-games","slug":"invalid-suit","errorCode":null,"errorMessage":"invalid suit","messagePattern":"invalid suit","errorType":"validation","errorClass":"CardError","httpStatus":null,"severity":"error","filePath":"94_War/ruby/war.rb","lineNumber":17,"sourceCode":"#!/usr/bin/env ruby\n# reinterpreted from BASIC by stephan.com\nclass War\n  class Card\n    class CardError < StandardError; end\n\n    SUITS = %i[spades hearts clubs diamonds].freeze\n    PIPS = %i[ace deuce trey four five six seven eight nine ten jack king queen].freeze\n    CARDS = SUITS.product(PIPS).freeze\n    VALUES = PIPS.zip(1..13).to_h.freeze\n\n    attr_reader :value\n\n    def initialize(suit, pip)\n      @suit = suit\n      @pip = pip\n      raise CardError, 'invalid suit' unless SUITS.include? @suit\n      raise CardError, 'invalid pip' unless PIPS.include? @pip\n\n      @value = VALUES[pip]\n    end\n\n    def <=>(other)\n      @value <=> other.value\n    end\n\n    def >(other)\n      @value > other.value\n    end\n\n    def <(other)\n      @value < other.value\n    end\n\n    def to_s","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/coding-horror/basic-computer-games/blob/5301155192d91d74d337899cecc59dbda59c4c17/94_War/ruby/war.rb#L1-L35","documentation":"CardError is a custom Ruby exception (subclass of StandardError) defined inside War::Card. It is raised in Card#initialize when the suit argument is not one of :spades, :hearts, :clubs, or :diamonds (checked against the frozen SUITS constant). In the current codebase this guard is effectively dead code: the only call site is Card.shuffle (war.rb:40), which destructures pairs from CARDS = SUITS.product(PIPS), guaranteeing every suit is a valid SUITS member. CardError is never rescued anywhere, so if a bad suit were ever passed the program would crash with an unhandled exception traceback.","triggerScenarios":"A future refactor or external caller passing Card.new(:joker, :ace), Card.new('spades', :ace) (string instead of symbol), or any suit not in the SUITS array. Deserializing saved game state from JSON/YAML where suits are strings. Parsing card names from user input or the original BASIC data format without converting to valid symbols.","commonSituations":"Adding custom deck support (jokers, extra suits). Serializing/deserializing card state across a network boundary. Internationalizing suit names. Writing test fixtures with typos in suit symbols. Refactoring PIPS/SUITS constants without updating all references.","solutions":["Validate the suit against SUITS before calling Card.new: raise CardError early with a clear message at the data boundary.","Convert string input to symbol with .to_sym and check membership: SUITS.include?(suit.to_sym) before construction.","Rescue War::Card::CardError at the call site to degrade gracefully instead of crashing.","Always use Card.shuffle for standard decks, which provably never triggers the guard.","Add a factory method Card.from_h or Card.from_json that validates and converts input before calling new."],"exampleFix":"# before\ncard = Card.new('spades', :ace)  # string, not symbol -> raises CardError\n\n# after — validate and convert at the boundary\nsuit = suit.to_sym if suit.is_a?(String)\nraise CardError, \"invalid suit: #{suit}\" unless SUITS.include?(suit)\ncard = Card.new(suit, pip)\n\n# or rescue at call site\nbegin\n  card = Card.new(suit_input, pip_input)\nrescue War::Card::CardError => e\n  warn \"Skipping invalid card: #{e.message}\"\n  next\nend","handlingStrategy":"validation","validationCode":"# Validate suit symbol before constructing a Card\nsuit = suit.to_sym if suit.is_a?(String)\nunless War::Card::SUITS.include?(suit)\n  warn \"Ignoring invalid suit: #{suit.inspect}\"\n  next  # or raise, or use a default\nend\ncard = War::Card.new(suit, pip)","typeGuard":"# Type guard: narrow any input to a valid suit symbol or reject\nmodule War\n  class Card\n    def self.valid_suit?(s)\n      SUITS.include?(s.to_sym)\n    end\n\n    def self.from_input(suit_str, pip_str)\n      suit = suit_str.to_sym\n      pip = pip_str.to_sym\n      return nil unless SUITS.include?(suit) && PIPS.include?(pip)\n      new(suit, pip)\n    end\n  end\nend","tryCatchPattern":"# Rescue CardError at the call site for graceful degradation\nbegin\n  card = War::Card.new(parsed_suit, parsed_pip)\nrescue War::Card::CardError => e\n  warn \"Invalid card data (#{e.message}), skipping.\"\n  next\nend","preventionTips":["Always use Card.shuffle for standard 52-card decks — it provably never passes an invalid suit.","When accepting suits from external sources (JSON, user input, network), call .to_sym and check SUITS.include? before Card.new.","Define a factory method (Card.from_hash, Card.from_json) that validates at the deserialization boundary.","Add CardError to a rescue clause at the top-level entry point so uncaught bad data logs instead of crashing.","Keep SUITS and PIPS frozen constants — they are the single source of truth for valid values.","Write a unit test that feeds every invalid suit type (string, nil, integer, wrong symbol) and asserts CardError is raised."],"tags":["ruby","validation","custom-exception","enum","card-game","dead-code","symbol-vs-string"],"backgroundTag":null,"analyzedSha":"5301155192d91d74d337899cecc59dbda59c4c17","analyzedAt":"2026-08-13T19:29:00.979Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}