os-backlog: migrate to planka-api 0.2.0 typed returns

0.2.0 returns Planka::Types value objects instead of parsed-JSON
hashes. Replace all hash access with typed accessors, rebuild
Cards#snapshot output as explicit string-keyed hashes (JSON output
stays byte-compatible, so skill/agent contracts are unchanged), and
rewrite test fakes on real Planka::Types records. 34 runs green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GjXD4ZDsucFZXNgVVrnZB
This commit is contained in:
jared 2026-07-10 16:51:23 -04:00
parent 91c6cbebd7
commit 8df0cb7e1e
7 changed files with 168 additions and 153 deletions

View File

@ -1,5 +1,5 @@
{ {
"name": "os-backlog", "name": "os-backlog",
"version": "0.1.0", "version": "0.2.0",
"description": "Uniform Planka backlog boards across projects: idempotent board-ensure (lists, labels, archive convention, Planka 2.1.1 quirk handling) and a deterministic repo-to-board routing resolver." "description": "Uniform Planka backlog boards across projects: idempotent board-ensure (lists, labels, archive convention, Planka 2.1.1 quirk handling) and a deterministic repo-to-board routing resolver."
} }

View File

@ -71,7 +71,7 @@ when "board-ensure"
project = parse_project_flag(rest) project = parse_project_flag(rest)
client = build_client client = build_client
result = Backlog::BoardEnsurer.new(client: client).ensure(name, project_name: project) result = Backlog::BoardEnsurer.new(client: client).ensure(name, project_name: project)
puts "board #{result.board['name']} (#{result.created ? 'created' : 'existing'}) " \ puts "board #{result.board.name} (#{result.created ? 'created' : 'existing'}) " \
"in project #{project}; lists created: #{result.lists_created.join(', ')}; " \ "in project #{project}; lists created: #{result.lists_created.join(', ')}; " \
"labels created: #{result.labels_created.join(', ')}" "labels created: #{result.labels_created.join(', ')}"
when "activate" when "activate"
@ -79,7 +79,7 @@ when "activate"
fail_soft("activate requires a board name") unless name fail_soft("activate requires a board name") unless name
client = build_client client = build_client
board = Backlog::BoardEnsurer.new(client: client).activate(name) board = Backlog::BoardEnsurer.new(client: client).activate(name)
puts "activated: #{board['name']}" puts "activated: #{board.name}"
when "archive" when "archive"
name = rest.shift name = rest.shift
fail_soft("archive requires a board name") unless name fail_soft("archive requires a board name") unless name
@ -87,7 +87,7 @@ when "archive"
client = build_client client = build_client
board = Backlog::BoardEnsurer.new(client: client).archive(name) board = Backlog::BoardEnsurer.new(client: client).archive(name)
puts "archived: #{board['name']}" puts "archived: #{board.name}"
when "resolve" when "resolve"
input = JSON.parse($stdin.read) input = JSON.parse($stdin.read)
resolver = Backlog::Resolver.new( resolver = Backlog::Resolver.new(
@ -102,7 +102,7 @@ when "card-add"
description = parse_flag(rest, "--description") description = parse_flag(rest, "--description")
client = build_client client = build_client
card = Backlog::Cards.new(client: client).add(board_name: board, title: title, description: description) card = Backlog::Cards.new(client: client).add(board_name: board, title: title, description: description)
puts "created card ##{card['id']} at Backlog on #{board}: #{card['name']}" puts "created card ##{card.id} at Backlog on #{board}: #{card.name}"
when "cards" when "cards"
board = require_flag(rest, "--board", "cards") board = require_flag(rest, "--board", "cards")
client = build_client client = build_client
@ -130,7 +130,7 @@ when "card-comment"
text = require_flag(rest, "--text", "card-comment") text = require_flag(rest, "--text", "card-comment")
client = build_client client = build_client
comment = Backlog::Cards.new(client: client).comment(card_id: card_id, text: text) comment = Backlog::Cards.new(client: client).comment(card_id: card_id, text: text)
puts "commented on card ##{card_id} (comment ##{comment['id']})" puts "commented on card ##{card_id} (comment ##{comment.id})"
when nil, "-h", "--help" when nil, "-h", "--help"
puts <<~USAGE puts <<~USAGE
usage: os-backlog <command> [options] usage: os-backlog <command> [options]

View File

@ -54,30 +54,30 @@ module Backlog
# archived board by that name exists. # archived board by that name exists.
# #
# @param name [String] the active (non-prefixed) name to activate # @param name [String] the active (non-prefixed) name to activate
# @return [Hash] the updated board # @return [Types::Board] the updated board
def activate(name) def activate(name)
board = find_board_by_name(BoardSpec.archived_name(name)) board = find_board_by_name(BoardSpec.archived_name(name))
raise "no archived board named #{BoardSpec.archived_name(name).inspect} found" unless board raise "no archived board named #{BoardSpec.archived_name(name).inspect} found" unless board
@client.boards.update(board["id"], name: name) @client.boards.update(board.id, name: name)
end end
# Rename an active board to its archived form. Only ever called on # Rename an active board to its archived form. Only ever called on
# explicit human request — never invoked by +ensure+. # explicit human request — never invoked by +ensure+.
# #
# @param name [String] the active (non-prefixed) name to archive # @param name [String] the active (non-prefixed) name to archive
# @return [Hash] the updated board # @return [Types::Board] the updated board
def archive(name) def archive(name)
board = find_board_by_name(name) board = find_board_by_name(name)
raise "no active board named #{name.inspect} found" unless board raise "no active board named #{name.inspect} found" unless board
@client.boards.update(board["id"], name: BoardSpec.archived_name(name)) @client.boards.update(board.id, name: BoardSpec.archived_name(name))
end end
private private
def find_or_create_project(project_name) def find_or_create_project(project_name)
existing = @client.projects.list.find { |p| p["name"] == project_name } existing = @client.projects.list.find { |p| p.name == project_name }
return existing if existing return existing if existing
project = @client.projects.create(name: project_name, type: "shared") project = @client.projects.create(name: project_name, type: "shared")
@ -90,41 +90,41 @@ module Backlog
def fix_visibility(project) def fix_visibility(project)
return unless @human_user_id return unless @human_user_id
@client.projects.add_manager(project["id"], @human_user_id) @client.projects.add_manager(project.id, @human_user_id)
@client.projects.update(project["id"], ownerProjectManagerId: nil) @client.projects.update(project.id, ownerProjectManagerId: nil)
end end
def find_or_create_board(project, name) def find_or_create_board(project, name)
board = find_board_by_name(name, project: project) board = find_board_by_name(name, project: project)
return [board, false] if board return [board, false] if board
created = @client.boards.create(project["id"], name: name, position: 65_536) created = @client.boards.create(project.id, name: name, position: 65_536)
[created, true] [created, true]
end end
def find_board_by_name(name, project: nil) def find_board_by_name(name, project: nil)
projects = project ? [project] : @client.projects.list projects = project ? [project] : @client.projects.list
projects.each do |p| projects.each do |p|
board = @client.boards.list(p["id"]).find { |b| b["name"] == name } board = @client.boards.list(p.id).find { |b| b.name == name }
return board if board return board if board
end end
nil nil
end end
def ensure_lists(board) def ensure_lists(board)
existing = @client.lists.list(board["id"]).filter_map { |l| l["name"] } existing = @client.lists.list(board.id).filter_map(&:name)
created = [] created = []
BoardSpec::LISTS.each_with_index do |list_name, index| BoardSpec::LISTS.each_with_index do |list_name, index|
next if existing.include?(list_name) next if existing.include?(list_name)
@client.lists.create(board["id"], name: list_name, type: "active", position: (index + 1) * 65_536) @client.lists.create(board.id, name: list_name, type: "active", position: (index + 1) * 65_536)
created << list_name created << list_name
end end
created created
end end
def ensure_labels(board) def ensure_labels(board)
existing = @client.labels.list(board["id"]).filter_map { |l| l["name"] } existing = @client.labels.list(board.id).filter_map(&:name)
created = [] created = []
BoardSpec::LABEL_NAMES.each do |label_name| BoardSpec::LABEL_NAMES.each do |label_name|
next if existing.include?(label_name) next if existing.include?(label_name)
@ -139,7 +139,7 @@ module Backlog
candidates = BoardSpec::LABEL_COLOR_CANDIDATES.fetch(label_name) candidates = BoardSpec::LABEL_COLOR_CANDIDATES.fetch(label_name)
last_error = nil last_error = nil
candidates.each do |color| candidates.each do |color|
return @client.labels.create(board["id"], name: label_name, color: color, position: 65_536) return @client.labels.create(board.id, name: label_name, color: color, position: 65_536)
rescue Planka::ValidationError => e rescue Planka::ValidationError => e
last_error = e last_error = e
next next

View File

@ -16,83 +16,100 @@ module Backlog
# Create a card at the board's Backlog list. Raises if the board or # Create a card at the board's Backlog list. Raises if the board or
# its Backlog list doesn't exist (callers run board-ensure first). # its Backlog list doesn't exist (callers run board-ensure first).
# #
# @return [Hash] the created card # @return [Planka::Types::Card] the created card
def add(board_name:, title:, description: nil) def add(board_name:, title:, description: nil)
board = find_board!(board_name) board = find_board!(board_name)
backlog = find_list!(board, "Backlog") backlog = find_list!(board, "Backlog")
attrs = { name: title, type: "project", position: next_position(board, backlog) } attrs = { name: title, type: "project", position: next_position(board, backlog) }
attrs[:description] = description if description attrs[:description] = description if description
@client.cards.create(backlog["id"], attrs) @client.cards.create(backlog.id, attrs)
end end
# Read-only snapshot of a whole board: every list in BoardSpec order # Read-only snapshot of a whole board: every list in BoardSpec order
# (plus any extras), each with its cards and their label names. # (plus any extras), each with its cards and their label names.
# #
# @return [Hash] { "board" => name, "labels" => [names], # @return [Hash] { "board" => name, "labels" => [names],
# "lists" => [{ "name", "cards" => [{card fields + "labels"}] }] } # "lists" => [{ "name", "cards" => [{"id","name","labels",...card fields}] }] }
def snapshot(board_name:) def snapshot(board_name:)
board = find_board!(board_name) board = find_board!(board_name)
included = @client.boards.get(board["id"])["included"] detail = @client.boards.get(board.id)
labels_by_id = (included["labels"] || []).to_h { |l| [l["id"], l["name"]] } labels_by_id = detail.labels.to_h { |l| [l.id, l.name] }
card_labels = (included["cardLabels"] || []).group_by { |cl| cl["cardId"] } card_labels = detail.card_labels.group_by(&:card_id)
cards_by_list = (included["cards"] || []).group_by { |c| c["listId"] } cards_by_list = detail.cards.group_by(&:list_id)
lists = (included["lists"] || []) lists = detail.lists
.select { |l| l["name"] } # skip built-in archive/trash lists (name: null) .select(&:name) # skip built-in archive/trash lists (name: nil)
.sort_by { |l| l["position"] || Float::INFINITY } .sort_by { |l| l.position || Float::INFINITY }
.map do |list| .map do |list|
cards = (cards_by_list[list["id"]] || []).map do |card| cards = (cards_by_list[list.id] || []).map do |card|
label_names = (card_labels[card["id"]] || []).filter_map { |cl| labels_by_id[cl["labelId"]] } label_names = (card_labels[card.id] || []).filter_map { |cl| labels_by_id[cl.label_id] }
card.merge("labels" => label_names) card_hash(card, label_names)
end end
{ "name" => list["name"], "cards" => cards } { "name" => list.name, "cards" => cards }
end end
{ "board" => board["name"], "labels" => labels_by_id.values, "lists" => lists } { "board" => board.name, "labels" => labels_by_id.values, "lists" => lists }
end end
# Attach a board label (by name) to a card. The only card mutation the # Attach a board label (by name) to a card. The only card mutation the
# triage agent is allowed — label changes, never column moves. # triage agent is allowed — label changes, never column moves.
# #
# @return [Hash] the card-label join record # @return [Planka::Types::CardLabel] the card-label join record
def attach_label(board_name:, card_id:, label:) def attach_label(board_name:, card_id:, label:)
board = find_board!(board_name) board = find_board!(board_name)
record = @client.labels.list(board["id"]).find { |l| l["name"] == label } record = @client.labels.list(board.id).find { |l| l.name == label }
raise "board #{board_name.inspect} has no label #{label.inspect} (run board-ensure)" unless record raise "board #{board_name.inspect} has no label #{label.inspect} (run board-ensure)" unless record
@client.labels.attach_to_card(card_id, record["id"]) @client.labels.attach_to_card(card_id, record.id)
end end
# Add a comment to a card. The only card mutation the audit agent is # Add a comment to a card. The only card mutation the audit agent is
# allowed. # allowed.
# #
# @return [Hash] the created comment # @return [Planka::Types::Comment] the created comment
def comment(card_id:, text:) def comment(card_id:, text:)
@client.comments.create(card_id, text: text) @client.comments.create(card_id, text: text)
end end
private private
# Build the plain-hash card representation the CLI/JSON output emits,
# keeping the output shape byte-compatible with the 0.1.x snapshot
# (string keys, "labels" merged in) even though Types::Card is an
# immutable Data object we can no longer +merge+.
def card_hash(card, label_names)
{
"id" => card.id,
"name" => card.name,
"description" => card.description,
"position" => card.position,
"listId" => card.list_id,
"boardId" => card.board_id,
"dueDate" => card.due_date,
"labels" => label_names
}
end
def find_board!(name) def find_board!(name)
@client.projects.list.each do |project| @client.projects.list.each do |project|
board = @client.boards.list(project["id"]).find { |b| b["name"] == name } board = @client.boards.list(project.id).find { |b| b.name == name }
return board if board return board if board
end end
raise "no board named #{name.inspect} found (run board-ensure first)" raise "no board named #{name.inspect} found (run board-ensure first)"
end end
def find_list!(board, list_name) def find_list!(board, list_name)
list = @client.lists.list(board["id"]).find { |l| l["name"] == list_name } list = @client.lists.list(board.id).find { |l| l.name == list_name }
raise "board #{board['name'].inspect} has no #{list_name.inspect} list (run board-ensure)" unless list raise "board #{board.name.inspect} has no #{list_name.inspect} list (run board-ensure)" unless list
list list
end end
def next_position(board, list) def next_position(board, list)
included = @client.boards.get(board["id"])["included"] detail = @client.boards.get(board.id)
positions = (included["cards"] || []) positions = detail.cards
.select { |c| c["listId"] == list["id"] } .select { |c| c.list_id == list.id }
.filter_map { |c| c["position"] } .filter_map(&:position)
(positions.max || 0) + 65_536 (positions.max || 0) + 65_536
end end
end end

View File

@ -10,13 +10,12 @@ class BoardEnsurerTest < Minitest::Test
result = ensurer.ensure("llf-schema", project_name: "Dev") result = ensurer.ensure("llf-schema", project_name: "Dev")
assert result.created assert result.created
assert_equal "llf-schema", result.board["name"] assert_equal "llf-schema", result.board.name
assert_equal Backlog::BoardSpec::LISTS, result.lists_created assert_equal Backlog::BoardSpec::LISTS, result.lists_created
assert_equal Backlog::BoardSpec::LABEL_NAMES, result.labels_created assert_equal Backlog::BoardSpec::LABEL_NAMES, result.labels_created
project = client.projects_store.first project = client.projects_store.first
assert_equal "Dev", project.name assert_equal "Dev", project.name
assert_equal "shared", project.type
end end
def test_rerun_is_a_noop def test_rerun_is_a_noop
@ -52,8 +51,8 @@ class BoardEnsurerTest < Minitest::Test
ensurer.ensure("llf-schema", project_name: "Dev") ensurer.ensure("llf-schema", project_name: "Dev")
project = client.projects_store.first project = client.projects_store.first
assert_includes project.managers, "human-1" assert_includes client.managers_for(project.id), "human-1"
assert_nil project.ownerProjectManagerId assert_nil project.owner_project_manager_id
end end
def test_semi_label_is_part_of_enforced_set def test_semi_label_is_part_of_enforced_set
@ -73,16 +72,17 @@ class BoardEnsurerTest < Minitest::Test
refute_equal first_choice, p0.color refute_equal first_choice, p0.color
end end
def test_activate_renames_archived_board_back def test_activate_renames_archived_board_back
client = FakePlankaClient.new client = FakePlankaClient.new
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1") ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
ensurer.ensure("llf-schema", project_name: "Dev") ensurer.ensure("llf-schema", project_name: "Dev")
board = client.boards_store.first board = client.boards_store.first
board.name = "archived--llf-schema" client.boards.update(board.id, name: "archived--llf-schema")
result = ensurer.activate("llf-schema") result = ensurer.activate("llf-schema")
assert_equal "llf-schema", result["name"] assert_equal "llf-schema", result.name
end end
def test_archive_renames_active_board def test_archive_renames_active_board
@ -92,7 +92,7 @@ class BoardEnsurerTest < Minitest::Test
result = ensurer.archive("llf-schema") result = ensurer.archive("llf-schema")
assert_equal "archived--llf-schema", result["name"] assert_equal "archived--llf-schema", result.name
end end
def test_ensure_never_touches_archived_boards def test_ensure_never_touches_archived_boards

View File

@ -14,22 +14,22 @@ class CardsTest < Minitest::Test
card = @cards.add(board_name: "llf-schema", title: "Fix the flaky import") card = @cards.add(board_name: "llf-schema", title: "Fix the flaky import")
backlog = @client.lists_store.find { |l| l.name == "Backlog" } backlog = @client.lists_store.find { |l| l.name == "Backlog" }
assert_equal backlog.id, card["listId"] assert_equal backlog.id, card.list_id
assert_equal "Fix the flaky import", card["name"] assert_equal "Fix the flaky import", card.name
assert_equal "project", card["type"] assert_equal "project", card.type
end end
def test_add_positions_new_card_after_existing_backlog_cards def test_add_positions_new_card_after_existing_backlog_cards
first = @cards.add(board_name: "llf-schema", title: "one") first = @cards.add(board_name: "llf-schema", title: "one")
second = @cards.add(board_name: "llf-schema", title: "two") second = @cards.add(board_name: "llf-schema", title: "two")
assert_operator second["position"], :>, first["position"] assert_operator second.position, :>, first.position
end end
def test_add_with_description def test_add_with_description
card = @cards.add(board_name: "llf-schema", title: "t", description: "details here") card = @cards.add(board_name: "llf-schema", title: "t", description: "details here")
assert_equal "details here", card["description"] assert_equal "details here", card.description
end end
def test_add_raises_for_missing_board def test_add_raises_for_missing_board
@ -52,7 +52,7 @@ class CardsTest < Minitest::Test
def test_snapshot_includes_card_label_names def test_snapshot_includes_card_label_names
card = @cards.add(board_name: "llf-schema", title: "labeled task") card = @cards.add(board_name: "llf-schema", title: "labeled task")
p1 = @client.labels_store.find { |l| l.name == "P1" } p1 = @client.labels_store.find { |l| l.name == "P1" }
@client.attach_label(card["id"], p1.id) @client.attach_label(card.id, p1.id)
snapshot = @cards.snapshot(board_name: "llf-schema") snapshot = @cards.snapshot(board_name: "llf-schema")
@ -67,7 +67,7 @@ class CardsTest < Minitest::Test
def test_attach_label_by_name def test_attach_label_by_name
card = @cards.add(board_name: "llf-schema", title: "raw card") card = @cards.add(board_name: "llf-schema", title: "raw card")
@cards.attach_label(board_name: "llf-schema", card_id: card["id"], label: "afk-ready") @cards.attach_label(board_name: "llf-schema", card_id: card.id, label: "afk-ready")
snapshot = @cards.snapshot(board_name: "llf-schema") snapshot = @cards.snapshot(board_name: "llf-schema")
backlog = snapshot["lists"].find { |l| l["name"] == "Backlog" } backlog = snapshot["lists"].find { |l| l["name"] == "Backlog" }
@ -78,7 +78,7 @@ class CardsTest < Minitest::Test
card = @cards.add(board_name: "llf-schema", title: "raw card") card = @cards.add(board_name: "llf-schema", title: "raw card")
error = assert_raises(RuntimeError) do error = assert_raises(RuntimeError) do
@cards.attach_label(board_name: "llf-schema", card_id: card["id"], label: "nonexistent") @cards.attach_label(board_name: "llf-schema", card_id: card.id, label: "nonexistent")
end end
assert_match(/no label/, error.message) assert_match(/no label/, error.message)
end end
@ -86,10 +86,10 @@ class CardsTest < Minitest::Test
def test_comment_adds_text_to_card def test_comment_adds_text_to_card
card = @cards.add(board_name: "llf-schema", title: "stale card") card = @cards.add(board_name: "llf-schema", title: "stale card")
comment = @cards.comment(card_id: card["id"], text: "audit: in Doing 9 days") comment = @cards.comment(card_id: card.id, text: "audit: in Doing 9 days")
assert_equal card["id"], comment["cardId"] assert_equal card.id, comment.card_id
assert_equal "audit: in Doing 9 days", comment["text"] assert_equal "audit: in Doing 9 days", comment.text
assert_equal 1, @client.comments.store.size assert_equal 1, @client.comments.store.size
end end
end end

View File

@ -1,26 +1,15 @@
require "minitest/autorun" require "minitest/autorun"
require "planka_api"
require_relative "../lib/backlog" require_relative "../lib/backlog"
# Minimal stand-ins for the planka-api gem's error hierarchy, so tests never
# require (or need) the real gem installed — only the shape our code
# rescues against.
module Planka
class Error < StandardError; end
class ValidationError < Error; end
end
module BacklogTestHelpers module BacklogTestHelpers
# A hand-rolled fake of the subset of Planka::Client's resource surface # A hand-rolled fake of the subset of Planka::Client's resource surface
# BoardEnsurer touches: projects, boards, lists, labels. In-memory only, # BoardEnsurer/Cards touches: projects, boards, lists, labels, cards,
# no network. Mirrors the gem's real return shapes (string-keyed hashes). # comments. In-memory only, no network. Returns the real planka-api 0.2.0
# typed records (Planka::Types::*) so these tests exercise the same
# accessor shapes production code does.
class FakePlankaClient class FakePlankaClient
Project = Struct.new(:id, :name, :type, :ownerProjectManagerId, :managers, keyword_init: true) attr_reader :projects, :boards, :lists, :labels, :cards, :comments
Board = Struct.new(:id, :project_id, :name, keyword_init: true)
List = Struct.new(:id, :board_id, :name, :type, :position, keyword_init: true)
Label = Struct.new(:id, :board_id, :name, :color, keyword_init: true)
Card = Struct.new(:id, :list_id, :board_id, :name, :type, :position, :description, keyword_init: true)
attr_reader :projects, :boards, :lists, :labels, :cards
def initialize(reject_colors: []) def initialize(reject_colors: [])
@next_id = 0 @next_id = 0
@ -30,6 +19,7 @@ module BacklogTestHelpers
@labels_store = [] @labels_store = []
@cards_store = [] @cards_store = []
@card_labels_store = [] @card_labels_store = []
@comments_store = []
@projects = ProjectsResource.new(self) @projects = ProjectsResource.new(self)
@boards = BoardsResource.new(self) @boards = BoardsResource.new(self)
@ -39,105 +29,115 @@ module BacklogTestHelpers
@comments = CommentsResource.new(self) @comments = CommentsResource.new(self)
end end
attr_reader :comments
def next_id = (@next_id += 1).to_s def next_id = (@next_id += 1).to_s
attr_reader :projects_store, :boards_store, :lists_store, :labels_store, attr_reader :projects_store, :boards_store, :lists_store, :labels_store,
:cards_store, :card_labels_store :cards_store, :card_labels_store, :comments_store
# Project managers aren't part of Types::Project (they're their own
# join record, Types::ProjectManager) — tracked here so tests can
# assert on the visibility-fix quirk (BoardEnsurer#fix_visibility).
def managers_for(project_id)
@managers_store ||= {}
@managers_store[project_id] ||= []
end
def attach_label(card_id, label_id) def attach_label(card_id, label_id)
@card_labels_store << { "id" => next_id, "cardId" => card_id, "labelId" => label_id } record = Planka::Types::CardLabel.new(
id: next_id, created_at: nil, updated_at: nil, card_id: card_id, label_id: label_id
)
@card_labels_store << record
record
end end
class ProjectsResource class ProjectsResource
def initialize(client) = @client = client def initialize(client) = @client = client
def list = @client.projects_store.map { |p| stringify(p) } def list = @client.projects_store.dup
def create(attrs) def create(attrs)
project = FakePlankaClient::Project.new( project = Planka::Types::Project.new(
id: @client.next_id, name: attrs[:name], type: attrs[:type], id: @client.next_id, created_at: nil, updated_at: nil, name: attrs[:name],
ownerProjectManagerId: @client.next_id, managers: [] description: nil, background_type: nil, background_gradient: nil, is_hidden: nil,
owner_project_manager_id: @client.next_id, background_image_id: nil, is_favorite: nil
) )
@client.projects_store << project @client.projects_store << project
stringify(project) project
end end
def update(id, attrs) def update(id, attrs)
project = @client.projects_store.find { |p| p.id == id } project = @client.projects_store.find { |p| p.id == id }
project.ownerProjectManagerId = attrs[:ownerProjectManagerId] if attrs.key?(:ownerProjectManagerId) idx = @client.projects_store.index(project)
stringify(project) updated = project
updated = updated.with(owner_project_manager_id: attrs[:ownerProjectManagerId]) if attrs.key?(:ownerProjectManagerId)
@client.projects_store[idx] = updated
updated
end end
def add_manager(project_id, user_id) def add_manager(project_id, user_id)
project = @client.projects_store.find { |p| p.id == project_id } @client.managers_for(project_id) << user_id
project.managers << user_id Planka::Types::ProjectManager.new(id: @client.next_id, created_at: nil, updated_at: nil,
{ "id" => @client.next_id, "projectId" => project_id, "userId" => user_id } project_id: project_id, user_id: user_id)
end end
private
def stringify(p) = { "id" => p.id, "name" => p.name, "type" => p.type }
end end
class BoardsResource class BoardsResource
def initialize(client) = @client = client def initialize(client) = @client = client
def list(project_id) def list(project_id)
@client.boards_store.select { |b| b.project_id == project_id }.map { |b| stringify(b) } @client.boards_store.select { |b| b.project_id == project_id }
end end
def create(project_id, attrs) def create(project_id, attrs)
board = FakePlankaClient::Board.new(id: @client.next_id, project_id: project_id, name: attrs[:name]) board = Planka::Types::Board.new(
id: @client.next_id, created_at: nil, updated_at: nil, position: attrs[:position],
name: attrs[:name], default_view: nil, default_card_type: nil,
limit_card_types_to_default_one: nil, always_display_card_creator: nil,
display_card_ages: nil, expand_task_lists_by_default: nil,
project_id: project_id, is_subscribed: nil
)
@client.boards_store << board @client.boards_store << board
stringify(board) board
end end
def update(id, attrs) def update(id, attrs)
board = @client.boards_store.find { |b| b.id == id } board = @client.boards_store.find { |b| b.id == id }
board.name = attrs[:name] if attrs.key?(:name) idx = @client.boards_store.index(board)
stringify(board) updated = board
updated = updated.with(name: attrs[:name]) if attrs.key?(:name)
@client.boards_store[idx] = updated
updated
end end
def get(id) def get(id)
board = @client.boards_store.find { |b| b.id == id } board = @client.boards_store.find { |b| b.id == id }
{ Planka::Types::BoardDetail.new(
"item" => stringify(board), board: board,
"included" => { lists: @client.lists.list(id),
"lists" => @client.lists.list(id), labels: @client.labels.list(id),
"labels" => @client.labels.list(id), cards: @client.cards.all_for_board(id),
"cards" => @client.cards.all_for_board(id), card_labels: @client.card_labels_store.dup,
"cardLabels" => @client.card_labels_store.dup board_memberships: []
} )
}
end end
private
def stringify(b) = { "id" => b.id, "projectId" => b.project_id, "name" => b.name }
end end
class ListsResource class ListsResource
def initialize(client) = @client = client def initialize(client) = @client = client
def list(board_id) def list(board_id)
@client.lists_store.select { |l| l.board_id == board_id }.map { |l| stringify(l) } @client.lists_store.select { |l| l.board_id == board_id }
end end
def create(board_id, attrs) def create(board_id, attrs)
raise ArgumentError, "list create requires type" unless attrs[:type] raise ArgumentError, "list create requires type" unless attrs[:type]
list = FakePlankaClient::List.new(id: @client.next_id, board_id: board_id, name: attrs[:name], list = Planka::Types::List.new(
type: attrs[:type], position: attrs[:position]) id: @client.next_id, created_at: nil, updated_at: nil, type: attrs[:type],
position: attrs[:position], name: attrs[:name], color: nil, board_id: board_id
)
@client.lists_store << list @client.lists_store << list
stringify(list) list
end
private
def stringify(l)
{ "id" => l.id, "boardId" => l.board_id, "name" => l.name, "type" => l.type, "position" => l.position }
end end
end end
@ -148,24 +148,19 @@ module BacklogTestHelpers
raise ArgumentError, "card create requires type" unless attrs[:type] raise ArgumentError, "card create requires type" unless attrs[:type]
list = @client.lists_store.find { |l| l.id == list_id } list = @client.lists_store.find { |l| l.id == list_id }
card = FakePlankaClient::Card.new( card = Planka::Types::Card.new(
id: @client.next_id, list_id: list_id, board_id: list.board_id, id: @client.next_id, created_at: nil, updated_at: nil, type: attrs[:type],
name: attrs[:name], type: attrs[:type], position: attrs[:position], position: attrs[:position], name: attrs[:name], description: attrs[:description],
description: attrs[:description] due_date: nil, is_due_completed: nil, stopwatch: nil, comments_total: nil,
is_closed: nil, list_changed_at: nil, board_id: list.board_id, list_id: list_id,
creator_user_id: nil, prev_list_id: nil, cover_attachment_id: nil, is_subscribed: nil
) )
@client.cards_store << card @client.cards_store << card
stringify(card) card
end end
def all_for_board(board_id) def all_for_board(board_id)
@client.cards_store.select { |c| c.board_id == board_id }.map { |c| stringify(c) } @client.cards_store.select { |c| c.board_id == board_id }
end
private
def stringify(c)
{ "id" => c.id, "listId" => c.list_id, "boardId" => c.board_id, "name" => c.name,
"type" => c.type, "position" => c.position, "description" => c.description }
end end
end end
@ -176,38 +171,41 @@ module BacklogTestHelpers
end end
def list(board_id) def list(board_id)
@client.labels_store.select { |l| l.board_id == board_id }.map { |l| stringify(l) } @client.labels_store.select { |l| l.board_id == board_id }
end end
def create(board_id, attrs) def create(board_id, attrs)
raise Planka::ValidationError, "color #{attrs[:color]} rejected" if @reject_colors.include?(attrs[:color]) if @reject_colors.include?(attrs[:color])
raise Planka::ValidationError.new(status: 422,
body: { "message" => "color #{attrs[:color]} rejected" })
end
label = FakePlankaClient::Label.new(id: @client.next_id, board_id: board_id, name: attrs[:name], color: attrs[:color]) label = Planka::Types::Label.new(
id: @client.next_id, created_at: nil, updated_at: nil, position: attrs[:position],
name: attrs[:name], color: attrs[:color], board_id: board_id
)
@client.labels_store << label @client.labels_store << label
stringify(label) label
end end
def attach_to_card(card_id, label_id) def attach_to_card(card_id, label_id)
@client.attach_label(card_id, label_id) @client.attach_label(card_id, label_id)
@client.card_labels_store.last
end end
private
def stringify(l) = { "id" => l.id, "boardId" => l.board_id, "name" => l.name, "color" => l.color }
end end
class CommentsResource class CommentsResource
def initialize(client) def initialize(client)
@client = client @client = client
@store = []
end end
attr_reader :store def store = @client.comments_store
def create(card_id, attrs) def create(card_id, attrs)
comment = { "id" => @client.next_id, "cardId" => card_id, "text" => attrs[:text] } comment = Planka::Types::Comment.new(
@store << comment id: @client.next_id, created_at: nil, updated_at: nil, text: attrs[:text],
card_id: card_id, user_id: nil
)
@client.comments_store << comment
comment comment
end end
end end