require_relative "config" module Backlog # Resolves a board name to its Planka board record, caching the resolved # id as `board_id=` in the repo's `.cc-os/config` (issue #22). # # The tracker's board *name* stays the source of truth; the id is only a # cache. The cached id is honored only when the config's tracker is # exactly `planka:`, and a cached `boards.get(id)` hit counts # only if the returned board's name still matches — a 404 or a rename # falls back to the projects+boards name lookup and rewrites the cache. # Any cache failure (unreadable config, unwritable config, stale id) # degrades to the name lookup; it never fails the command. # # Filesystem access is injected: `read_config` returns the raw # `.cc-os/config` contents (or nil), `write_config` persists updated # contents. Cache writes go through Config.set, the comment-preserving # writer — never Config.merge. class BoardResolver # @param client [Planka::Client] an already-authenticated client # @param read_config [#call] -> String, nil — raw .cc-os/config contents # @param write_config [#call] (String) -> void — persist updated contents def initialize(client:, read_config:, write_config:) @client = client @read_config = read_config @write_config = write_config end # A resolver with no config access: always name-lookup, never caches. def self.null(client:) new(client: client, read_config: -> {}, write_config: ->(_) {}) end # @param name [String] the board name (source of truth) # @return [Planka::Types::Board] # @raise [RuntimeError] if no board with that name exists def board_for(name) cached = cached_board(name) return cached if cached board = lookup_by_name!(name) cache_id(name, board.id) board end private def cached_board(name) config = safe_config return nil unless config && config.tracker == "planka:#{name}" id = config.board_id return nil unless id board = @client.boards.get(id).board board && board.name == name ? board : nil rescue StandardError nil end def lookup_by_name!(name) @client.projects.list.each do |project| board = @client.boards.list(project.id).find { |b| b.name == name } return board if board end raise "no board named #{name.inspect} found (run board-ensure first)" end def cache_id(name, id) contents = @read_config.call config = Config.new(contents) return unless config.tracker == "planka:#{name}" return if config.board_id == id.to_s @write_config.call(Config.set(contents, "board_id", id)) rescue StandardError nil # cache write failure never fails the command end def safe_config Config.new(@read_config.call) rescue StandardError nil end end end