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",
"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."
}

View File

@ -71,7 +71,7 @@ when "board-ensure"
project = parse_project_flag(rest)
client = build_client
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(', ')}; " \
"labels created: #{result.labels_created.join(', ')}"
when "activate"
@ -79,7 +79,7 @@ when "activate"
fail_soft("activate requires a board name") unless name
client = build_client
board = Backlog::BoardEnsurer.new(client: client).activate(name)
puts "activated: #{board['name']}"
puts "activated: #{board.name}"
when "archive"
name = rest.shift
fail_soft("archive requires a board name") unless name
@ -87,7 +87,7 @@ when "archive"
client = build_client
board = Backlog::BoardEnsurer.new(client: client).archive(name)
puts "archived: #{board['name']}"
puts "archived: #{board.name}"
when "resolve"
input = JSON.parse($stdin.read)
resolver = Backlog::Resolver.new(
@ -102,7 +102,7 @@ when "card-add"
description = parse_flag(rest, "--description")
client = build_client
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"
board = require_flag(rest, "--board", "cards")
client = build_client
@ -130,7 +130,7 @@ when "card-comment"
text = require_flag(rest, "--text", "card-comment")
client = build_client
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"
puts <<~USAGE
usage: os-backlog <command> [options]

View File

@ -54,30 +54,30 @@ module Backlog
# archived board by that name exists.
#
# @param name [String] the active (non-prefixed) name to activate
# @return [Hash] the updated board
# @return [Types::Board] the updated board
def activate(name)
board = find_board_by_name(BoardSpec.archived_name(name))
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
# Rename an active board to its archived form. Only ever called on
# explicit human request — never invoked by +ensure+.
#
# @param name [String] the active (non-prefixed) name to archive
# @return [Hash] the updated board
# @return [Types::Board] the updated board
def archive(name)
board = find_board_by_name(name)
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
private
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
project = @client.projects.create(name: project_name, type: "shared")
@ -90,41 +90,41 @@ module Backlog
def fix_visibility(project)
return unless @human_user_id
@client.projects.add_manager(project["id"], @human_user_id)
@client.projects.update(project["id"], ownerProjectManagerId: nil)
@client.projects.add_manager(project.id, @human_user_id)
@client.projects.update(project.id, ownerProjectManagerId: nil)
end
def find_or_create_board(project, name)
board = find_board_by_name(name, project: project)
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]
end
def find_board_by_name(name, project: nil)
projects = project ? [project] : @client.projects.list
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
end
nil
end
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 = []
BoardSpec::LISTS.each_with_index do |list_name, index|
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
end
created
end
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 = []
BoardSpec::LABEL_NAMES.each do |label_name|
next if existing.include?(label_name)
@ -139,7 +139,7 @@ module Backlog
candidates = BoardSpec::LABEL_COLOR_CANDIDATES.fetch(label_name)
last_error = nil
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
last_error = e
next

View File

@ -16,83 +16,100 @@ module Backlog
# 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).
#
# @return [Hash] the created card
# @return [Planka::Types::Card] the created card
def add(board_name:, title:, description: nil)
board = find_board!(board_name)
backlog = find_list!(board, "Backlog")
attrs = { name: title, type: "project", position: next_position(board, backlog) }
attrs[:description] = description if description
@client.cards.create(backlog["id"], attrs)
@client.cards.create(backlog.id, attrs)
end
# Read-only snapshot of a whole board: every list in BoardSpec order
# (plus any extras), each with its cards and their label 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:)
board = find_board!(board_name)
included = @client.boards.get(board["id"])["included"]
labels_by_id = (included["labels"] || []).to_h { |l| [l["id"], l["name"]] }
card_labels = (included["cardLabels"] || []).group_by { |cl| cl["cardId"] }
cards_by_list = (included["cards"] || []).group_by { |c| c["listId"] }
detail = @client.boards.get(board.id)
labels_by_id = detail.labels.to_h { |l| [l.id, l.name] }
card_labels = detail.card_labels.group_by(&:card_id)
cards_by_list = detail.cards.group_by(&:list_id)
lists = (included["lists"] || [])
.select { |l| l["name"] } # skip built-in archive/trash lists (name: null)
.sort_by { |l| l["position"] || Float::INFINITY }
lists = detail.lists
.select(&:name) # skip built-in archive/trash lists (name: nil)
.sort_by { |l| l.position || Float::INFINITY }
.map do |list|
cards = (cards_by_list[list["id"]] || []).map do |card|
label_names = (card_labels[card["id"]] || []).filter_map { |cl| labels_by_id[cl["labelId"]] }
card.merge("labels" => label_names)
cards = (cards_by_list[list.id] || []).map do |card|
label_names = (card_labels[card.id] || []).filter_map { |cl| labels_by_id[cl.label_id] }
card_hash(card, label_names)
end
{ "name" => list["name"], "cards" => cards }
{ "name" => list.name, "cards" => cards }
end
{ "board" => board["name"], "labels" => labels_by_id.values, "lists" => lists }
{ "board" => board.name, "labels" => labels_by_id.values, "lists" => lists }
end
# Attach a board label (by name) to a card. The only card mutation the
# 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:)
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
@client.labels.attach_to_card(card_id, record["id"])
@client.labels.attach_to_card(card_id, record.id)
end
# Add a comment to a card. The only card mutation the audit agent is
# allowed.
#
# @return [Hash] the created comment
# @return [Planka::Types::Comment] the created comment
def comment(card_id:, text:)
@client.comments.create(card_id, text: text)
end
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)
@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
end
raise "no board named #{name.inspect} found (run board-ensure first)"
end
def find_list!(board, 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
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
list
end
def next_position(board, list)
included = @client.boards.get(board["id"])["included"]
positions = (included["cards"] || [])
.select { |c| c["listId"] == list["id"] }
.filter_map { |c| c["position"] }
detail = @client.boards.get(board.id)
positions = detail.cards
.select { |c| c.list_id == list.id }
.filter_map(&:position)
(positions.max || 0) + 65_536
end
end

View File

@ -10,13 +10,12 @@ class BoardEnsurerTest < Minitest::Test
result = ensurer.ensure("llf-schema", project_name: "Dev")
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::LABEL_NAMES, result.labels_created
project = client.projects_store.first
assert_equal "Dev", project.name
assert_equal "shared", project.type
end
def test_rerun_is_a_noop
@ -52,8 +51,8 @@ class BoardEnsurerTest < Minitest::Test
ensurer.ensure("llf-schema", project_name: "Dev")
project = client.projects_store.first
assert_includes project.managers, "human-1"
assert_nil project.ownerProjectManagerId
assert_includes client.managers_for(project.id), "human-1"
assert_nil project.owner_project_manager_id
end
def test_semi_label_is_part_of_enforced_set
@ -73,16 +72,17 @@ class BoardEnsurerTest < Minitest::Test
refute_equal first_choice, p0.color
end
def test_activate_renames_archived_board_back
client = FakePlankaClient.new
ensurer = Backlog::BoardEnsurer.new(client: client, human_user_id: "human-1")
ensurer.ensure("llf-schema", project_name: "Dev")
board = client.boards_store.first
board.name = "archived--llf-schema"
client.boards.update(board.id, name: "archived--llf-schema")
result = ensurer.activate("llf-schema")
assert_equal "llf-schema", result["name"]
assert_equal "llf-schema", result.name
end
def test_archive_renames_active_board
@ -92,7 +92,7 @@ class BoardEnsurerTest < Minitest::Test
result = ensurer.archive("llf-schema")
assert_equal "archived--llf-schema", result["name"]
assert_equal "archived--llf-schema", result.name
end
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")
backlog = @client.lists_store.find { |l| l.name == "Backlog" }
assert_equal backlog.id, card["listId"]
assert_equal "Fix the flaky import", card["name"]
assert_equal "project", card["type"]
assert_equal backlog.id, card.list_id
assert_equal "Fix the flaky import", card.name
assert_equal "project", card.type
end
def test_add_positions_new_card_after_existing_backlog_cards
first = @cards.add(board_name: "llf-schema", title: "one")
second = @cards.add(board_name: "llf-schema", title: "two")
assert_operator second["position"], :>, first["position"]
assert_operator second.position, :>, first.position
end
def test_add_with_description
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
def test_add_raises_for_missing_board
@ -52,7 +52,7 @@ class CardsTest < Minitest::Test
def test_snapshot_includes_card_label_names
card = @cards.add(board_name: "llf-schema", title: "labeled task")
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")
@ -67,7 +67,7 @@ class CardsTest < Minitest::Test
def test_attach_label_by_name
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")
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")
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
assert_match(/no label/, error.message)
end
@ -86,10 +86,10 @@ class CardsTest < Minitest::Test
def test_comment_adds_text_to_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 "audit: in Doing 9 days", comment["text"]
assert_equal card.id, comment.card_id
assert_equal "audit: in Doing 9 days", comment.text
assert_equal 1, @client.comments.store.size
end
end

View File

@ -1,26 +1,15 @@
require "minitest/autorun"
require "planka_api"
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
# A hand-rolled fake of the subset of Planka::Client's resource surface
# BoardEnsurer touches: projects, boards, lists, labels. In-memory only,
# no network. Mirrors the gem's real return shapes (string-keyed hashes).
# BoardEnsurer/Cards touches: projects, boards, lists, labels, cards,
# 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
Project = Struct.new(:id, :name, :type, :ownerProjectManagerId, :managers, keyword_init: true)
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
attr_reader :projects, :boards, :lists, :labels, :cards, :comments
def initialize(reject_colors: [])
@next_id = 0
@ -30,6 +19,7 @@ module BacklogTestHelpers
@labels_store = []
@cards_store = []
@card_labels_store = []
@comments_store = []
@projects = ProjectsResource.new(self)
@boards = BoardsResource.new(self)
@ -39,105 +29,115 @@ module BacklogTestHelpers
@comments = CommentsResource.new(self)
end
attr_reader :comments
def next_id = (@next_id += 1).to_s
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)
@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
class ProjectsResource
def initialize(client) = @client = client
def list = @client.projects_store.map { |p| stringify(p) }
def list = @client.projects_store.dup
def create(attrs)
project = FakePlankaClient::Project.new(
id: @client.next_id, name: attrs[:name], type: attrs[:type],
ownerProjectManagerId: @client.next_id, managers: []
project = Planka::Types::Project.new(
id: @client.next_id, created_at: nil, updated_at: nil, name: attrs[:name],
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
stringify(project)
project
end
def update(id, attrs)
project = @client.projects_store.find { |p| p.id == id }
project.ownerProjectManagerId = attrs[:ownerProjectManagerId] if attrs.key?(:ownerProjectManagerId)
stringify(project)
idx = @client.projects_store.index(project)
updated = project
updated = updated.with(owner_project_manager_id: attrs[:ownerProjectManagerId]) if attrs.key?(:ownerProjectManagerId)
@client.projects_store[idx] = updated
updated
end
def add_manager(project_id, user_id)
project = @client.projects_store.find { |p| p.id == project_id }
project.managers << user_id
{ "id" => @client.next_id, "projectId" => project_id, "userId" => user_id }
@client.managers_for(project_id) << user_id
Planka::Types::ProjectManager.new(id: @client.next_id, created_at: nil, updated_at: nil,
project_id: project_id, user_id: user_id)
end
private
def stringify(p) = { "id" => p.id, "name" => p.name, "type" => p.type }
end
class BoardsResource
def initialize(client) = @client = client
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
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
stringify(board)
board
end
def update(id, attrs)
board = @client.boards_store.find { |b| b.id == id }
board.name = attrs[:name] if attrs.key?(:name)
stringify(board)
idx = @client.boards_store.index(board)
updated = board
updated = updated.with(name: attrs[:name]) if attrs.key?(:name)
@client.boards_store[idx] = updated
updated
end
def get(id)
board = @client.boards_store.find { |b| b.id == id }
{
"item" => stringify(board),
"included" => {
"lists" => @client.lists.list(id),
"labels" => @client.labels.list(id),
"cards" => @client.cards.all_for_board(id),
"cardLabels" => @client.card_labels_store.dup
}
}
Planka::Types::BoardDetail.new(
board: board,
lists: @client.lists.list(id),
labels: @client.labels.list(id),
cards: @client.cards.all_for_board(id),
card_labels: @client.card_labels_store.dup,
board_memberships: []
)
end
private
def stringify(b) = { "id" => b.id, "projectId" => b.project_id, "name" => b.name }
end
class ListsResource
def initialize(client) = @client = client
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
def create(board_id, attrs)
raise ArgumentError, "list create requires type" unless attrs[:type]
list = FakePlankaClient::List.new(id: @client.next_id, board_id: board_id, name: attrs[:name],
type: attrs[:type], position: attrs[:position])
list = Planka::Types::List.new(
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
stringify(list)
end
private
def stringify(l)
{ "id" => l.id, "boardId" => l.board_id, "name" => l.name, "type" => l.type, "position" => l.position }
list
end
end
@ -148,24 +148,19 @@ module BacklogTestHelpers
raise ArgumentError, "card create requires type" unless attrs[:type]
list = @client.lists_store.find { |l| l.id == list_id }
card = FakePlankaClient::Card.new(
id: @client.next_id, list_id: list_id, board_id: list.board_id,
name: attrs[:name], type: attrs[:type], position: attrs[:position],
description: attrs[:description]
card = Planka::Types::Card.new(
id: @client.next_id, created_at: nil, updated_at: nil, type: attrs[:type],
position: attrs[:position], name: attrs[:name], 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
stringify(card)
card
end
def all_for_board(board_id)
@client.cards_store.select { |c| c.board_id == board_id }.map { |c| stringify(c) }
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 }
@client.cards_store.select { |c| c.board_id == board_id }
end
end
@ -176,38 +171,41 @@ module BacklogTestHelpers
end
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
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
stringify(label)
label
end
def attach_to_card(card_id, label_id)
@client.attach_label(card_id, label_id)
@client.card_labels_store.last
end
private
def stringify(l) = { "id" => l.id, "boardId" => l.board_id, "name" => l.name, "color" => l.color }
end
class CommentsResource
def initialize(client)
@client = client
@store = []
end
attr_reader :store
def store = @client.comments_store
def create(card_id, attrs)
comment = { "id" => @client.next_id, "cardId" => card_id, "text" => attrs[:text] }
@store << comment
comment = Planka::Types::Comment.new(
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
end
end