Created
May 23, 2024 10:56
-
-
Save elisherer/9263d2f12672139edd997e7a572269f4 to your computer and use it in GitHub Desktop.
JSON 2 YAML naive solution
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import toYaml from "./toYaml"; | |
describe("toYaml", () => { | |
test("string", () => { | |
expect(toYaml("Hello World")).toEqual("Hello World\n"); | |
// keywords | |
expect(toYaml("null")).toEqual('"null"\n'); | |
expect(toYaml("true")).toEqual('"true"\n'); | |
expect(toYaml("false")).toEqual('"false"\n'); | |
// escapes | |
expect(toYaml(`"hello"`)).toEqual(`'"hello"'\n`); | |
expect(toYaml(`'hello'`)).toEqual(`"'hello'"\n`); | |
expect(toYaml(`'"'`)).toEqual('"false"\n'); | |
}); | |
test("boolean", () => { | |
expect(toYaml(true)).toEqual("true\n"); | |
expect(toYaml(false)).toEqual("false\n"); | |
}); | |
test("null", () => { | |
expect(toYaml(null)).toEqual("\n"); | |
expect(toYaml(undefined)).toEqual("\n"); | |
}); | |
test("object", () => { | |
expect(toYaml({ a: 1, b: "2", c: true, d: ["hello", "world"] })).toEqual( | |
` | |
a: 1 | |
b: "2" | |
c: true | |
d: | |
- hello | |
- world | |
`.substring(1), | |
); | |
}); | |
test("object (nested)", () => { | |
expect(toYaml({ a: { b: { c: 1 } } })).toEqual( | |
` | |
a: | |
b: | |
c: 1 | |
`.substring(1), | |
); | |
}); | |
test("array", () => { | |
expect("\n" + toYaml([{ a: "hello" }, { a: "hello", b: "world" }, "hello world"])).toEqual(` | |
- a: hello | |
- a: hello | |
b: world | |
- hello world | |
`); | |
}); | |
test("array (nested)", () => { | |
expect(toYaml(["hello", ["hello", "world"]])).toEqual( | |
` | |
- hello | |
- - hello | |
- world | |
`.substring(1), | |
); | |
}); | |
test("array inside object", () => { | |
expect(toYaml({ x: [{ a: "hello" }, { a: "hello", b: "world" }, "hello world"] })).toEqual( | |
` | |
x: | |
- a: hello | |
- a: hello | |
b: world | |
- hello world | |
`.substring(1), | |
); | |
}); | |
test("complex", () => { | |
expect( | |
toYaml({ | |
employees: { | |
employee: [ | |
{ | |
id: "1", | |
firstName: "Tom", | |
lastName: "Cruise", | |
group: "A", | |
}, | |
{ | |
id: "2", | |
firstName: "Maria", | |
lastName: "Sharapova", | |
group: "B", | |
}, | |
{ | |
id: "007", | |
firstName: "James", | |
lastName: "Bond", | |
group: "A", | |
}, | |
], | |
}, | |
}), | |
).toEqual( | |
` | |
employees: | |
employee: | |
- id: "1" | |
firstName: Tom | |
lastName: Cruise | |
group: "A" | |
- id: "2" | |
firstName: "Maria" | |
lastName: "Sharapova" | |
group: B | |
- id: "007" | |
firstName: "James" | |
lastName: Bond | |
group: "A" | |
`.substring(1), | |
); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface CompareValue { | |
key: string; | |
value: any; | |
} | |
export type CompareFunction = (first: CompareValue, second: CompareValue) => number; | |
export type Options = { | |
preserve_undefined?: boolean; | |
show_nulls?: boolean; | |
cmp?: CompareFunction; | |
}; | |
const MUST_QUOTE_LITERALS = new Set(["null", "true", "false", "~", ""]), | |
TAB_WIDTH = 2, | |
// eslint-disable-next-line no-control-regex | |
MUST_DOUBLE_QUOTE_REGEXP = /[\x00-\x06\b\t\n\v\f\r\x0e-\x1a\x1c-\x1f"]/i, | |
MUST_QUOTE_REGEXP = /^[\s.#:&*|>!?-]|: |\s$|(^-?\d+(\.\d+)?(e[-+]?\d+)?$)|(^\d{4}-\d\d-\d\d)/i; | |
const DEFAULT_OPTIONS: Options = {}, | |
EMPTY_STRING = "", | |
TAB = " ".repeat(TAB_WIDTH), | |
DEFAULT_HANDLER = () => undefined, | |
NULL_HANDLER = () => EMPTY_STRING, | |
BOOLEAN_HANDLER = (x: any) => (x ? "true" : "false"), | |
NUMBER_HANDLER = (x: any) => { | |
// !!float | |
if (Number.isNaN(x)) { | |
return ".nan"; | |
} | |
if (x === Infinity) { | |
return ".inf"; | |
} | |
if (x === -Infinity) { | |
return "-.inf"; | |
} | |
return x; | |
}, | |
STRING_HANDLER = (x: any) => { | |
if ((x.length <= 5 && MUST_QUOTE_LITERALS.has(x.toLowerCase())) || MUST_QUOTE_REGEXP.test(x)) { | |
const doubleQuoted = JSON.stringify(x); | |
const containsDoubleQuotes = doubleQuoted.indexOf('"', 1) !== doubleQuoted.length - 1; | |
return containsDoubleQuotes ? doubleQuoted : `'${doubleQuoted.slice(1, -1).replace(/'/g, "\\'")}'`; | |
} | |
return x; | |
}, | |
SUPPORTED_TYPES = ["boolean", "string", "number", "object", "undefined"]; | |
function typeOf(obj: any) { | |
if (obj === null) { | |
return "null"; | |
} | |
if (Array.isArray(obj)) { | |
return "array"; | |
} | |
const type = typeof obj; | |
return SUPPORTED_TYPES.includes(type) ? type : "default"; | |
} | |
function toYaml(data: any, options: Options = DEFAULT_OPTIONS) { | |
if (typeof data === "undefined") { | |
return "\n"; | |
} | |
let indent = EMPTY_STRING; | |
const cmp = | |
options.cmp && | |
(f => { | |
return (node: any) => { | |
return (a: string, b: string) => { | |
const aobj = { key: a, value: node[a] }; | |
const bobj = { key: b, value: node[b] }; | |
return f(aobj, bobj); | |
}; | |
}; | |
})(options.cmp); | |
const handlers: Record<string, (x: any, parentType?: string | -1, index?: number) => any> = { | |
default: DEFAULT_HANDLER, // for non supported yaml value types | |
null: NULL_HANDLER, | |
boolean: BOOLEAN_HANDLER, | |
undefined: () => (options.preserve_undefined ? EMPTY_STRING : undefined), | |
number: NUMBER_HANDLER, | |
string: STRING_HANDLER, | |
array: (x, parentType) => { | |
if (x.length === 0) { | |
return "[]"; | |
} | |
if (parentType) { | |
indent += TAB; | |
} | |
let output = EMPTY_STRING; | |
for (let i = 0; i < x.length; i++) { | |
let handlerOutput = handlers[typeOf(x[i])](x[i], "array", i); | |
if (typeof handlerOutput === "undefined") { | |
handlerOutput = EMPTY_STRING; // no hiding objects inside arrays to preserve length and indexes of array | |
} | |
output += | |
(parentType === "array" && i === 0 ? EMPTY_STRING : (!parentType && i === 0 ? EMPTY_STRING : "\n") + indent) + | |
"- " + | |
handlerOutput; | |
} | |
indent = indent.substring(0, indent.length - TAB_WIDTH); // remove last tab | |
return output; | |
}, | |
object: function (x, parentType) { | |
let keys = Object.keys(x); | |
if (keys.length === 0) { | |
return "{}"; | |
} | |
if (cmp) { | |
keys = keys.sort(cmp(x)); | |
} | |
if (parentType) { | |
indent += TAB; | |
} | |
let output = EMPTY_STRING; | |
for (let i = 0; i < keys.length; i++) { | |
const val = x[keys[i]]; | |
if ((val === null || typeof val === "undefined") && !options.show_nulls) { | |
continue; | |
} | |
const handlerOutput = handlers[typeOf(val)](val, "object"); | |
output += | |
(parentType === "array" && i === 0 ? EMPTY_STRING : (!parentType && i === 0 ? EMPTY_STRING : "\n") + indent) + | |
keys[i] + | |
": " + | |
handlerOutput; | |
} | |
indent = indent.substring(0, indent.length - TAB_WIDTH); // remove last tab | |
return output; | |
}, | |
}; | |
return handlers[typeOf(data)](data) + "\n"; | |
} | |
export default toYaml; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment