Add YAML patching tests and implement serialization logic
- Introduced comprehensive tests for applying minimal set key patches in YAML, covering various scenarios including scalar operations, adding new keys, list operations, and handling of empty values. - Implemented serialization functions to convert JavaScript types to YAML format, ensuring proper handling of scalars, arrays, and objects. - Enhanced the `applyMinimalSetKeyPatches` function to support patching of top-level keys while preserving comments and formatting.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { assertEquals, assertExists } from "@std/assert";
|
||||
import { applyMinimalSetKeyPatches, SetKeyPatch } from "./yaml.ts";
|
||||
|
||||
Deno.test("YAML patching - basic scalar operations", () => {
|
||||
const initialYaml = `
|
||||
name: my-package
|
||||
version: 1.0.0
|
||||
description: A sample package
|
||||
`;
|
||||
|
||||
const patches: SetKeyPatch[] = [
|
||||
{ op: "set-key", path: "version", value: "1.1.0" },
|
||||
{ op: "set-key", path: "description", value: "Updated description" },
|
||||
];
|
||||
|
||||
const result = applyMinimalSetKeyPatches(initialYaml, patches);
|
||||
|
||||
assertExists(result);
|
||||
assertEquals(result.includes("version: 1.1.0"), true);
|
||||
assertEquals(result.includes("description: Updated description"), true);
|
||||
assertEquals(result.includes("name: my-package"), true);
|
||||
});
|
||||
|
||||
Deno.test("YAML patching - adding new keys", () => {
|
||||
const initialYaml = `
|
||||
name: my-package
|
||||
`;
|
||||
|
||||
const patches: SetKeyPatch[] = [
|
||||
{ op: "set-key", path: "version", value: "1.0.0" },
|
||||
{ op: "set-key", path: "private", value: true },
|
||||
];
|
||||
|
||||
const result = applyMinimalSetKeyPatches(initialYaml, patches);
|
||||
|
||||
assertExists(result);
|
||||
assertEquals(result.includes("version: 1.0.0"), true);
|
||||
assertEquals(result.includes("private: true"), true);
|
||||
});
|
||||
|
||||
Deno.test("YAML patching - list operations", () => {
|
||||
const initialYaml = `
|
||||
name: my-package
|
||||
tags:
|
||||
- node
|
||||
- typescript
|
||||
`;
|
||||
|
||||
const patches: SetKeyPatch[] = [
|
||||
{ op: "set-key", path: "tags", value: ["deno", "typescript", "yaml"] },
|
||||
{ op: "set-key", path: "dependencies", value: ["std", "testing"] },
|
||||
];
|
||||
|
||||
const result = applyMinimalSetKeyPatches(initialYaml, patches);
|
||||
|
||||
assertExists(result);
|
||||
assertEquals(result.includes("tags:"), true);
|
||||
assertEquals(result.includes("- deno"), true);
|
||||
assertEquals(result.includes("- typescript"), true);
|
||||
assertEquals(result.includes("- yaml"), true);
|
||||
assertEquals(result.includes("dependencies:"), true);
|
||||
assertEquals(result.includes("- std"), true);
|
||||
assertEquals(result.includes("- testing"), true);
|
||||
});
|
||||
|
||||
Deno.test("YAML patching - empty values and special characters", () => {
|
||||
const initialYaml = `
|
||||
name: my-package
|
||||
`;
|
||||
|
||||
const patches: SetKeyPatch[] = [
|
||||
{ op: "set-key", path: "empty", value: "" },
|
||||
{ op: "set-key", path: "special", value: "value:with:colons" },
|
||||
{ op: "set-key", path: "null", value: null },
|
||||
];
|
||||
|
||||
const result = applyMinimalSetKeyPatches(initialYaml, patches);
|
||||
|
||||
assertExists(result);
|
||||
assertEquals(result.includes('empty: ""'), true);
|
||||
assertEquals(result.includes('special: "value:with:colons"'), true);
|
||||
assertEquals(result.includes("null: null"), true);
|
||||
});
|
||||
|
||||
Deno.test("YAML patching - empty input", () => {
|
||||
const initialYaml = "";
|
||||
const patches: SetKeyPatch[] = [
|
||||
{ op: "set-key", path: "name", value: "new-package" },
|
||||
];
|
||||
|
||||
const result = applyMinimalSetKeyPatches(initialYaml, patches);
|
||||
|
||||
assertExists(result);
|
||||
assertEquals(result.trim(), "name: new-package");
|
||||
});
|
||||
|
||||
Deno.test("YAML patching - nested object values", () => {
|
||||
const initialYaml = `
|
||||
name: my-package
|
||||
`;
|
||||
|
||||
const patches: SetKeyPatch[] = [
|
||||
{
|
||||
op: "set-key",
|
||||
path: "config",
|
||||
value: {
|
||||
port: 3000,
|
||||
host: "localhost",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = applyMinimalSetKeyPatches(initialYaml, patches);
|
||||
|
||||
assertExists(result);
|
||||
assertEquals(result.includes("config:"), true);
|
||||
assertEquals(result.includes(" port: 3000"), true);
|
||||
assertEquals(result.includes(" host: localhost"), true);
|
||||
});
|
||||
|
||||
Deno.test("YAML patching - empty lists and objects", () => {
|
||||
const initialYaml = `
|
||||
name: my-package
|
||||
`;
|
||||
|
||||
const patches: SetKeyPatch[] = [
|
||||
{ op: "set-key", path: "emptyList", value: [] },
|
||||
{ op: "set-key", path: "emptyObject", value: {} },
|
||||
];
|
||||
|
||||
const result = applyMinimalSetKeyPatches(initialYaml, patches);
|
||||
|
||||
assertExists(result);
|
||||
assertEquals(result.includes("emptyList: []"), true);
|
||||
assertEquals(result.includes("emptyObject: {}"), true);
|
||||
});
|
||||
|
||||
Deno.test("YAML patching - preserves comments", () => {
|
||||
const initialYaml = `
|
||||
# Main package configuration
|
||||
name: my-package # The package name
|
||||
version: 1.0.0 # Current version
|
||||
description: A sample package
|
||||
|
||||
# Additional metadata
|
||||
author: John Doe # Package maintainer
|
||||
license: MIT # License type
|
||||
`;
|
||||
|
||||
const patches: SetKeyPatch[] = [
|
||||
{ op: "set-key", path: "version", value: "2.0.0" },
|
||||
{ op: "set-key", path: "description", value: "An updated package" },
|
||||
{ op: "set-key", path: "newProp", value: "new value" },
|
||||
];
|
||||
|
||||
const result = applyMinimalSetKeyPatches(initialYaml, patches);
|
||||
|
||||
// Verify all comments are preserved
|
||||
assertEquals(result.includes("# Main package configuration"), true);
|
||||
assertEquals(result.includes("# The package name"), true);
|
||||
assertEquals(result.includes("# Current version"), true);
|
||||
assertEquals(result.includes("# Additional metadata"), true);
|
||||
assertEquals(result.includes("# Package maintainer"), true);
|
||||
assertEquals(result.includes("# License type"), true);
|
||||
|
||||
// Verify the content is also correct
|
||||
assertEquals(result.includes("version: 2.0.0"), true);
|
||||
assertEquals(result.includes("description: An updated package"), true);
|
||||
assertEquals(result.includes("newProp: new value"), true);
|
||||
});
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
// Define the patch structure
|
||||
export interface SetKeyPatch {
|
||||
op: "set-key";
|
||||
path: string; // Still assuming simple, top-level key names
|
||||
value: any; // Allow any value, including arrays
|
||||
}
|
||||
|
||||
// Helper function specifically for serializing scalar types
|
||||
function serializeToYamlScalar(
|
||||
value: string | number | boolean | null,
|
||||
): string {
|
||||
if (typeof value === "string") {
|
||||
// Always quote empty strings and strings with special characters
|
||||
if (
|
||||
value === "" || // Empty string
|
||||
value.match(/[:{#}[],&*!|>'"%@`]/) || // Special YAML characters (added % and ")
|
||||
/^\d+(\.\d+)?([eE][+-]?\d+)?$/.test(value) || // Looks like a number
|
||||
value.includes(":") || // Contains colons
|
||||
["true", "false", "null", "yes", "no", "on", "off"].includes(
|
||||
value.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
// Simple strings without special chars/meaning don't need quotes
|
||||
return value;
|
||||
} else if (
|
||||
typeof value === "number" || typeof value === "boolean" || value === null
|
||||
) {
|
||||
return String(value);
|
||||
}
|
||||
// Default for unsupported scalar types (e.g., undefined)
|
||||
return "null";
|
||||
}
|
||||
|
||||
// Updated helper function to serialize various JS types to YAML string representations
|
||||
// Added baseIndentation parameter for handling nested lists correctly (though we only use it for top-level lists here)
|
||||
function serializeToYamlValue(
|
||||
value: any,
|
||||
baseIndentation: string = "",
|
||||
): string {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
return "[]"; // Use flow style for empty arrays for simplicity
|
||||
}
|
||||
// Determine indentation for list items (base + 2 spaces)
|
||||
const itemIndentation = baseIndentation + " ";
|
||||
// Format each item recursively, preceded by '- ' marker
|
||||
return "\n" +
|
||||
value.map((item) =>
|
||||
`${itemIndentation}- ${serializeToYamlValue(item, itemIndentation)}`
|
||||
).join("\n");
|
||||
// Note: serializeToYamlValue is used recursively here to handle nested arrays/objects if needed in future
|
||||
// However, the current `applyMinimalSetKeyPatches` only handles top-level keys.
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
// Basic object serialization (not requested, but good to consider)
|
||||
// This is highly simplified and doesn't handle nesting well without more context
|
||||
const itemIndentation = baseIndentation + " ";
|
||||
const entries = Object.entries(value);
|
||||
if (entries.length === 0) return "{}"; // Flow style empty objects
|
||||
return "\n" +
|
||||
entries.map(([key, val]) =>
|
||||
`${itemIndentation}${key}: ${
|
||||
serializeToYamlValue(val, itemIndentation)
|
||||
}`
|
||||
).join("\n");
|
||||
} else {
|
||||
// Handle scalars using the dedicated function
|
||||
return serializeToYamlScalar(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyMinimalSetKeyPatches(
|
||||
yamlString: string,
|
||||
patches: SetKeyPatch[],
|
||||
): string {
|
||||
let currentYaml = yamlString;
|
||||
|
||||
for (const patch of patches) {
|
||||
if (patch.op !== "set-key") continue;
|
||||
|
||||
// Still simplifying: Only handle top-level keys
|
||||
if (patch.path.includes(".") || patch.path.includes("[")) {
|
||||
console.warn(
|
||||
`Skipping patch for nested or invalid key path (not supported): ${patch.path}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = patch.path;
|
||||
|
||||
// Regex to find the key at the beginning of a line, optionally indented,
|
||||
// capturing the value and any trailing comment separately
|
||||
const regex = new RegExp(
|
||||
`^(\\s*)(${key}):\\s*([^#\\n]*?)\\s*(#.*)?\\s*$`,
|
||||
"m",
|
||||
);
|
||||
const match = currentYaml.match(regex);
|
||||
|
||||
if (match) {
|
||||
// Key found: Replace the existing line/block
|
||||
const [fullMatch, indentation, keyMatch, oldValue, inlineComment = ""] =
|
||||
match;
|
||||
const originalLine = fullMatch;
|
||||
|
||||
// Serialize the new value. Pass the key's indentation for list/object formatting.
|
||||
const serializedNewValue = serializeToYamlValue(patch.value, indentation);
|
||||
|
||||
let replacementString: string;
|
||||
// Check if the new value requires multi-line formatting (list/object starting with newline)
|
||||
if (serializedNewValue.startsWith("\n")) {
|
||||
// For lists/objects, the key line ends with just ':' plus any comment
|
||||
replacementString =
|
||||
`${indentation}${key}:${inlineComment}${serializedNewValue}`;
|
||||
} else {
|
||||
// For scalars, format as key: value plus any comment
|
||||
// Ensure proper spacing around the inline comment
|
||||
replacementString = `${indentation}${key}: ${serializedNewValue}${
|
||||
inlineComment ? " " + inlineComment : ""
|
||||
}`;
|
||||
}
|
||||
|
||||
// Find any standalone comments before this key and preserve them
|
||||
const lines = currentYaml.split("\n");
|
||||
const keyLineIndex = lines.findIndex((line) =>
|
||||
line.trim() === originalLine.trim()
|
||||
);
|
||||
let commentBlock = "";
|
||||
|
||||
// Look backwards from the key line to collect any standalone comments
|
||||
for (let i = keyLineIndex - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith("#")) {
|
||||
commentBlock = lines[i] + "\n" + commentBlock;
|
||||
} else if (line !== "") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the original line while preserving comments
|
||||
if (commentBlock) {
|
||||
currentYaml = currentYaml.replace(
|
||||
commentBlock + originalLine,
|
||||
commentBlock + replacementString,
|
||||
);
|
||||
} else {
|
||||
currentYaml = currentYaml.replace(originalLine, replacementString);
|
||||
}
|
||||
} else {
|
||||
// Key not found: Add the new key-value pair to the end
|
||||
const indentation = ""; // No indentation for new top-level keys
|
||||
const serializedNewValue = serializeToYamlValue(patch.value, indentation);
|
||||
|
||||
let newLineBlock: string;
|
||||
// Check if the new value requires multi-line formatting
|
||||
if (serializedNewValue.startsWith("\n")) {
|
||||
newLineBlock = `${key}:${serializedNewValue}`; // Key, colon, then newline and indented items/properties
|
||||
} else {
|
||||
newLineBlock = `${key}: ${serializedNewValue}`; // Key, colon, space, scalar value
|
||||
}
|
||||
|
||||
// Append the new line/block, ensuring proper spacing
|
||||
if (currentYaml.trim() === "") {
|
||||
currentYaml = newLineBlock; // If yaml was empty, just set it
|
||||
} else {
|
||||
// Add a newline before appending if needed
|
||||
currentYaml = currentYaml.replace(/\n*$/, "\n"); // Ensure single trailing newline
|
||||
currentYaml += newLineBlock + "\n"; // Append and add trailing newline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentYaml;
|
||||
}
|
||||
Reference in New Issue
Block a user