feat: add tags workflow
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AppErrorVariant } from "@/error";
|
||||
import { parseProjectConfig } from "./parseProjectConfig";
|
||||
|
||||
describe("parseProjectConfig", () => {
|
||||
it("parses tags list", () => {
|
||||
const result = parseProjectConfig("tags:\n - bug\n - frontend\n");
|
||||
expect(result.isOk()).toBe(true);
|
||||
if (result.isErr()) return;
|
||||
expect(result.value.tags).toEqual(["bug", "frontend"]);
|
||||
});
|
||||
|
||||
it("accepts empty / missing tags", () => {
|
||||
expect(parseProjectConfig("")._unsafeUnwrap().tags).toEqual([]);
|
||||
expect(parseProjectConfig("tags: []\n")._unsafeUnwrap().tags).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it("dedupes and trims tags", () => {
|
||||
const result = parseProjectConfig(
|
||||
"tags:\n - bug\n - bug \n - frontend\n",
|
||||
);
|
||||
expect(result._unsafeUnwrap().tags).toEqual(["bug", "frontend"]);
|
||||
});
|
||||
|
||||
it("rejects invalid YAML", () => {
|
||||
const result = parseProjectConfig("tags: [\n");
|
||||
expect(result.isErr()).toBe(true);
|
||||
if (result.isOk()) return;
|
||||
expect(result.error.variant).toBe(AppErrorVariant.INVALID_PROJECT_CONFIG);
|
||||
});
|
||||
|
||||
it("rejects non-string tags", () => {
|
||||
const result = parseProjectConfig("tags:\n - 1\n");
|
||||
expect(result.isErr()).toBe(true);
|
||||
if (result.isOk()) return;
|
||||
expect(result.error.variant).toBe(AppErrorVariant.INVALID_PROJECT_CONFIG);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as yaml from "js-yaml";
|
||||
import { err, ok, Result } from "neverthrow";
|
||||
import { appError, AppError, AppErrorVariant } from "@/error";
|
||||
import {
|
||||
EMPTY_PROJECT_CONFIG,
|
||||
normalizeTag,
|
||||
type ProjectConfig,
|
||||
} from "@/model/projectConfig";
|
||||
|
||||
/**
|
||||
* Parse task-folder config.yml content.
|
||||
* Accepts missing/empty tags → [].
|
||||
*/
|
||||
export function parseProjectConfig(
|
||||
content: string,
|
||||
): Result<ProjectConfig, AppError> {
|
||||
if (content.trim() === "") {
|
||||
return ok({ ...EMPTY_PROJECT_CONFIG });
|
||||
}
|
||||
|
||||
let data: unknown;
|
||||
try {
|
||||
data = yaml.load(content);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : "invalid YAML";
|
||||
return err(
|
||||
appError(AppErrorVariant.INVALID_PROJECT_CONFIG, { detail }),
|
||||
);
|
||||
}
|
||||
|
||||
if (data === null || data === undefined) {
|
||||
return ok({ ...EMPTY_PROJECT_CONFIG });
|
||||
}
|
||||
|
||||
if (typeof data !== "object" || Array.isArray(data)) {
|
||||
return err(
|
||||
appError(AppErrorVariant.INVALID_PROJECT_CONFIG, {
|
||||
detail: "root must be a mapping",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const record = data as Record<string, unknown>;
|
||||
if (record.tags === undefined) {
|
||||
return ok({ tags: [] });
|
||||
}
|
||||
|
||||
if (!Array.isArray(record.tags)) {
|
||||
return err(
|
||||
appError(AppErrorVariant.INVALID_PROJECT_CONFIG, {
|
||||
detail: "tags must be an array of strings",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const tags: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of record.tags) {
|
||||
if (typeof item !== "string") {
|
||||
return err(
|
||||
appError(AppErrorVariant.INVALID_PROJECT_CONFIG, {
|
||||
detail: "tags must be an array of strings",
|
||||
}),
|
||||
);
|
||||
}
|
||||
const tag = normalizeTag(item);
|
||||
if (!tag || seen.has(tag)) continue;
|
||||
seen.add(tag);
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
return ok({ tags });
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseProjectConfig } from "./parseProjectConfig";
|
||||
import { serializeProjectConfig } from "./serializeProjectConfig";
|
||||
|
||||
describe("serializeProjectConfig", () => {
|
||||
it("round-trips tags", () => {
|
||||
const yaml = serializeProjectConfig({ tags: ["bug", "docs"] });
|
||||
expect(yaml).toContain("bug");
|
||||
expect(yaml).toContain("docs");
|
||||
const parsed = parseProjectConfig(yaml);
|
||||
expect(parsed._unsafeUnwrap().tags).toEqual(["bug", "docs"]);
|
||||
});
|
||||
|
||||
it("serializes empty tags", () => {
|
||||
const yaml = serializeProjectConfig({ tags: [] });
|
||||
const parsed = parseProjectConfig(yaml);
|
||||
expect(parsed._unsafeUnwrap().tags).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as yaml from "js-yaml";
|
||||
import type { ProjectConfig } from "@/model/projectConfig";
|
||||
|
||||
export function serializeProjectConfig(config: ProjectConfig): string {
|
||||
return yaml
|
||||
.dump(
|
||||
{ tags: config.tags },
|
||||
{
|
||||
lineWidth: -1,
|
||||
forceQuotes: false,
|
||||
},
|
||||
)
|
||||
.trimEnd()
|
||||
.concat("\n");
|
||||
}
|
||||
Reference in New Issue
Block a user