Skip to content

Commit

Permalink
feat(yaml): Support YAML 1.1 features
Browse files Browse the repository at this point in the history
  • Loading branch information
tommy351 committed May 29, 2024
1 parent a3d9ff4 commit 3485f8a
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 21 deletions.
11 changes: 11 additions & 0 deletions .changeset/witty-cherries-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@kosko/yaml": major
"@kosko/helm": major
"@kosko/kustomize": major
---

Support the following YAML 1.1 features to match the behavior of [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml).

- Numbers starting with `0` (e.g. `0777`) are interpreted as octal numbers, instead of decimal numbers.
- YAML 1.2 octal number `0o` prefix is still supported.
- YAML 1.1 booleans (`yes`, `no`, `on`, `off`, `y`, `n`) are interpreted as booleans, instead of strings.
3 changes: 1 addition & 2 deletions packages/yaml/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,12 @@
"@kosko/common-utils": "workspace:^",
"@kosko/log": "workspace:^",
"fast-safe-stringify": "^2.1.1",
"js-yaml": "^4.1.0"
"yaml": "^2.4.2"
},
"devDependencies": {
"@kosko/build-scripts": "workspace:^",
"@kosko/jest-preset": "workspace:^",
"@kosko/test-utils": "workspace:^",
"@types/js-yaml": "^4.0.5",
"@types/node-fetch": "^2.6.2",
"cross-fetch": "^3.1.5",
"execa": "^5.1.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/yaml/src/__tests__/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { FetchMockStatic } from "fetch-mock";
import { Pod } from "kubernetes-models/v1/Pod";
import { TempDir, makeTempDir } from "@kosko/test-utils";
import { isRecord } from "@kosko/common-utils";
import yaml from "js-yaml";
import yaml from "yaml";

// eslint-disable-next-line @typescript-eslint/no-var-requires
jest.mock("../fetch", () => require("fetch-mock").sandbox());
Expand Down Expand Up @@ -340,7 +340,7 @@ describe("loadUrl", () => {
}
};
const fetchFn = jest.fn().mockResolvedValue(
new Response(yaml.dump(data), {
new Response(yaml.stringify(data), {
headers: {
"Content-Type": "application/yaml"
}
Expand Down
76 changes: 76 additions & 0 deletions packages/yaml/src/__tests__/yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { parse } from "../yaml";

test.each([
// YAML 1.1
{ input: "0123", expected: 83 },
// YAML 1.2
{ input: "0o123", expected: 83 },
// Decimal
{ input: "123", expected: 123 },
// String
{ input: `"0123"`, expected: "0123" },
// YAML 1.1 in key
{ input: "0123: value", expected: { 83: "value" } },
// YAML 1.2 in key
{ input: "0o123: value", expected: { 83: "value" } }
])("octal number: $input -> $expected", ({ input, expected }) => {
expect(parse(input)).toEqual([expected]);
});

test.each([
// Yes
{ input: "yes", expected: true },
{ input: "Yes", expected: true },
{ input: "YES", expected: true },
// No
{ input: "no", expected: false },
{ input: "No", expected: false },
{ input: "NO", expected: false },
// On
{ input: "on", expected: true },
{ input: "On", expected: true },
{ input: "ON", expected: true },
// Off
{ input: "off", expected: false },
{ input: "Off", expected: false },
{ input: "OFF", expected: false },
// Y
{ input: "y", expected: true },
{ input: "Y", expected: true },
// N
{ input: "n", expected: false },
{ input: "N", expected: false },
// Yes in key
{ input: "yes: value", expected: { true: "value" } },
// No in key
{ input: "no: value", expected: { false: "value" } },
// Yes in value
{ input: "key: yes", expected: { key: true } },
// No in value
{ input: "key: no", expected: { key: false } }
])("boolean: $input -> $expected", ({ input, expected }) => {
expect(parse(input)).toEqual([expected]);
});

test("anchor", () => {
expect(
parse(`
a: &anchor
foo: bar
b: *anchor
`)
).toEqual([{ a: { foo: "bar" }, b: { foo: "bar" } }]);
});

test("map merging", () => {
expect(
parse(`
a: &src-a
a: 1
b: &src-b
b: 2
result:
<<: [*src-a, *src-b]
`)
).toEqual([{ a: { a: 1 }, b: { b: 2 }, result: { a: 1, b: 2 } }]);
});
4 changes: 2 additions & 2 deletions packages/yaml/src/load.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { loadAll } from "js-yaml";
import { parse } from "./yaml";
import { readFile } from "node:fs/promises";
import { getResourceModule, ResourceKind } from "./module";
import logger, { LogLevel } from "@kosko/log";
Expand Down Expand Up @@ -67,7 +67,7 @@ export async function loadString(
options: LoadOptions = {}
): Promise<Manifest[]> {
const { transform = (x) => x } = options;
const input = loadAll(content).filter((x) => x != null);
const input = parse(content).filter((x) => x != null);
const manifests: Manifest[] = [];

for (const entry of input) {
Expand Down
56 changes: 56 additions & 0 deletions packages/yaml/src/yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { parseAllDocuments, visit } from "yaml";

const reLegacyOctNum = /^0[0-7]+$/;

// Match all-lowercase, all-uppercase, and title-case variants
const legacyTrues = new Set(["y", "Y", "yes", "Yes", "YES", "on", "On", "ON"]);
const legacyFalses = new Set(["n", "N", "no", "No", "NO", "off", "Off", "OFF"]);

/**
* The purpose of this function is to match the behavior of [sigs.k8s.io/yaml]
* as closely as possible.
*
* [sigs.k8s.io/yaml] uses [go-yaml](https://github.com/go-yaml/yaml) under the
* hood, which supports most of YAML 1.2, but preserves some YAML 1.1 behaviors
* for backwards compatibility. You can see [here](https://github.com/go-yaml/yaml?tab=readme-ov-file#compatibility)
* for more information.
*
* [sigs.k8s.io/yaml]: https://pkg.go.dev/sigs.k8s.io/yaml
* [go-yaml]: https://github.com/go-yaml/yaml
*/
export function parse(content: string): unknown[] {
const documents = parseAllDocuments(content, {
// Enable map merging
merge: true
});
const results = [];

for (const doc of documents) {
visit(doc, {
Scalar(key, node) {
if (node.type !== "PLAIN" || !node.source) return;

// Handle YAML 1.1 octal numbers
if (
typeof node.value === "number" &&
reLegacyOctNum.test(node.source)
) {
node.value = parseInt(node.source, 8);
}

// Handle YAML 1.1 booleans
if (typeof node.value === "string") {
if (legacyTrues.has(node.source)) {
node.value = true;
} else if (legacyFalses.has(node.source)) {
node.value = false;
}
}
}
});

results.push(doc.toJSON());
}

return results;
}
42 changes: 27 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3485f8a

Please sign in to comment.