diff --git a/src/components/Controls/ExportRulesYAML.tsx b/src/components/Controls/ExportRulesYAML.tsx index 6ef1fe9..cc12190 100644 --- a/src/components/Controls/ExportRulesYAML.tsx +++ b/src/components/Controls/ExportRulesYAML.tsx @@ -5,6 +5,7 @@ import { saveAs } from "file-saver"; import { ColumnOption } from "csv-stringify/browser/esm/sync"; import { MenuItem } from "@mui/material"; import JSZip from "jszip"; +import { ruleFileName } from "../../utils/ruleFileName"; const columns: ColumnOption[] = [ { @@ -21,9 +22,6 @@ const columns: ColumnOption[] = [ }, ]; -const cast = (value) => - Array.isArray(value) ? value.join(",") : JSON.stringify(value); - export default function ExportRulesYAML({ onClose }) { const { dataService, orderBy, order, searchText, setAlertState } = useContext( AppContext @@ -70,9 +68,10 @@ export default function ExportRulesYAML({ onClose }) { const zip = new JSZip(); for (const rule of rules) { zip.file( - `${cast( - rule["json.Authorities.Standards.References.Rule Identifier.Id"] - )}.${rule["id"]}.yml`, + ruleFileName( + rule["json.Authorities.Standards.References.Rule Identifier.Id"], + rule["id"] + ), rule["content"] ); } diff --git a/src/utils/ruleFileName.test.ts b/src/utils/ruleFileName.test.ts new file mode 100644 index 0000000..c5b5d99 --- /dev/null +++ b/src/utils/ruleFileName.test.ts @@ -0,0 +1,26 @@ +import { MISSING_RULE_ID, ruleFileName } from "./ruleFileName"; + +describe("ruleFileName", () => { + const id = "3f2a9c1e-7b4d-4c1a-9e2f-0a1b2c3d4e5f"; + + it("uses the rule identifier as the file name prefix", () => { + expect(ruleFileName(["CORE-000001"], id)).toBe(`CORE-000001.${id}.yml`); + }); + + it("joins multiple rule identifiers with a comma", () => { + expect(ruleFileName(["CORE-000001", "CORE-000002"], id)).toBe( + `CORE-000001,CORE-000002.${id}.yml` + ); + }); + + it.each([ + ["an empty array", []], + ["an array of empty values", [""]], + ["undefined", undefined], + ])("uses a placeholder when the rule identifier is %s", (_, ruleId) => { + const fileName = ruleFileName(ruleId, id); + + expect(fileName).toBe(`${MISSING_RULE_ID}.${id}.yml`); + expect(fileName.startsWith(".")).toBe(false); + }); +}); diff --git a/src/utils/ruleFileName.ts b/src/utils/ruleFileName.ts new file mode 100644 index 0000000..39a9863 --- /dev/null +++ b/src/utils/ruleFileName.ts @@ -0,0 +1,16 @@ +/** + * Placeholder used when a rule has no Rule Identifier. + * + * Without it, the exported file name would start with "." (e.g. "..yml"), + * which macOS treats as a hidden file. + */ +export const MISSING_RULE_ID = "NO_RULE_ID"; + +const toFileNamePart = (value: unknown): string => + (Array.isArray(value) ? value.join(",") : JSON.stringify(value)) ?? ""; + +/** + * Builds the file name for a rule exported as YAML: `..yml`. + */ +export const ruleFileName = (ruleId: unknown, id: string): string => + `${toFileNamePart(ruleId) || MISSING_RULE_ID}.${id}.yml`;