/* YAML-to-Go by Zhwt https://github.com/Zhwt/yaml-to-go A simple utility parses and translates YAML into a Go struct definition. */ function yamlToGo(yaml, typename, flatten = true) { let data; let scope; let go = ""; let tabs = 0; const seen = {}; const stack = []; let accumulator = ""; let innerTabs = 0; let parent = ""; try { data = jsyaml.load(yaml.replace(/:(\s*\d*)\.0/g, ":$1.1")); // hack that forces floats to stay as floats console.log(data) scope = data; } catch (e) { return { go: "", error: e.message }; } typename = format(typename || "AutoGenerated"); append(`type ${typename} `); parseScope(scope); return { go: flatten ? go += accumulator : go }; function parseScope(scope, depth = 0) { if (typeof scope === "object" && scope !== null) { if (Array.isArray(scope)) { let sliceType; const scopeLength = scope.length; for (let i = 0; i < scopeLength; i++) { const thisType = goType(scope[i]); if (!sliceType) sliceType = thisType; else if (sliceType != thisType) { sliceType = mostSpecificPossibleGoType(thisType, sliceType); if (sliceType == "interface{}") break; } } const slice = flatten && ["struct", "slice"].includes(sliceType) ? `[]${parent}` : `[]`; if (flatten && depth >= 2) appender(slice); else append(slice) if (sliceType == "struct") { const allFields = {}; // for each field counts how many times appears for (let i = 0; i < scopeLength; i++) { const keys = Object.keys(scope[i]) for (let k in keys) { let keyname = keys[k]; if (!(keyname in allFields)) { allFields[keyname] = { value: scope[i][keyname], count: 0 } } else { const existingValue = allFields[keyname].value; const currentValue = scope[i][keyname]; if (compareObjects(existingValue, currentValue)) { const comparisonResult = compareObjectKeys( Object.keys(currentValue), Object.keys(existingValue) ) if (!comparisonResult) { keyname = `${keyname}_${uuidv4()}`; allFields[keyname] = { value: currentValue, count: 0 }; } } } allFields[keyname].count++; } } // create a common struct with all fields found in the current array // omitempty dict indicates if a field is optional const keys = Object.keys(allFields), struct = {}, omitempty = {}; for (let k in keys) { const keyname = keys[k], elem = allFields[keyname]; struct[keyname] = elem.value; omitempty[keyname] = elem.count != scopeLength; } parseStruct(depth + 1, innerTabs, struct, omitempty); // finally parse the struct !! } else if (sliceType == "slice") { parseScope(scope[0], depth) } else { if (flatten && depth >= 2) { appender(sliceType || "interface{}"); } else { append(sliceType || "interface{}"); } } } else { if (flatten) { if (depth >= 2){ appender(parent) } else { append(parent) } } parseStruct(depth + 1, innerTabs, scope); } } else { if (flatten && depth >= 2){ appender(goType(scope)); } else { append(goType(scope)); } } } function parseStruct(depth, innerTabs, scope, omitempty) { if (flatten) { stack.push( depth >= 2 ? "\n" : "" ) } if (flatten && depth >= 2) { const parentType = `type ${parent}`; const scopeKeys = formatScopeKeys(Object.keys(scope)); // this can only handle two duplicate items // future improvement will handle the case where there could // three or more duplicate keys with different values if (parent in seen && compareObjectKeys(scopeKeys, seen[parent])) { stack.pop(); return } seen[parent] = scopeKeys; appender(`${parentType} struct {\n`); ++innerTabs; const keys = Object.keys(scope); for (let i in keys) { const keyname = getOriginalName(keys[i]); indenter(innerTabs) const typename = format(keyname) appender(typename+" "); parent = typename parseScope(scope[keys[i]], depth); appender(' `yaml:"'+keyname); if (omitempty && omitempty[keys[i]] === true) { appender(',omitempty'); } appender('"`\n'); } indenter(--innerTabs); appender("}"); } else { append("struct {\n"); ++tabs; const keys = Object.keys(scope); for (let i in keys) { const keyname = getOriginalName(keys[i]); indent(tabs); const typename = format(keyname); append(typename+" "); parent = typename parseScope(scope[keys[i]], depth); append(' `yaml:"'+keyname); if (omitempty && omitempty[keys[i]] === true) { append(',omitempty'); } append('"`\n'); } indent(--tabs); append("}"); } if (flatten) accumulator += stack.pop(); } function indent(tabs) { for (let i = 0; i < tabs; i++) go += '\t'; } function append(str) { go += str; } function indenter(tabs) { for (let i = 0; i < tabs; i++) stack[stack.length - 1] += '\t'; } function appender(str) { stack[stack.length - 1] += str; } // Sanitizes and formats a string to make an appropriate identifier in Go function format(str) { if (!str) return ""; else if (str.match(/^\d+$/)) str = "Num" + str; else if (str.charAt(0).match(/\d/)) { const numbers = {'0': "Zero_", '1': "One_", '2': "Two_", '3': "Three_", '4': "Four_", '5': "Five_", '6': "Six_", '7': "Seven_", '8': "Eight_", '9': "Nine_"}; str = numbers[str.charAt(0)] + str.substr(1); } return toProperCase(str).replace(/[^a-z0-9]/ig, "") || "NAMING_FAILED"; } // Determines the most appropriate Go type function goType(val) { if (val === null) return "interface{}"; switch (typeof val) { case "string": if (/\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(\+\d\d:\d\d|Z)/.test(val)) return "time.Time"; else return "string"; case "number": if (val % 1 === 0) { if (val > -2147483648 && val < 2147483647) return "int"; else return "int64"; } else return "float64"; case "boolean": return "bool"; case "object": if (Array.isArray(val)) return "slice"; return "struct"; default: return "interface{}"; } } // Given two types, returns the more specific of the two function mostSpecificPossibleGoType(typ1, typ2) { if (typ1.substr(0, 5) == "float" && typ2.substr(0, 3) == "int") return typ1; else if (typ1.substr(0, 3) == "int" && typ2.substr(0, 5) == "float") return typ2; else return "interface{}"; } // Proper cases a string according to Go conventions function toProperCase(str) { // https://github.com/golang/lint/blob/5614ed5bae6fb75893070bdc0996a68765fdd275/lint.go#L771-L810 const commonInitialisms = [ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS" ]; return str.replace(/(^|[^a-zA-Z])([a-z]+)/g, function(unused, sep, frag) { if (commonInitialisms.indexOf(frag.toUpperCase()) >= 0) return sep + frag.toUpperCase(); else return sep + frag[0].toUpperCase() + frag.substr(1).toLowerCase(); }).replace(/([A-Z])([a-z]+)/g, function(unused, sep, frag) { if (commonInitialisms.indexOf(sep + frag.toUpperCase()) >= 0) return (sep + frag).toUpperCase(); else return sep + frag; }); } function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function getOriginalName(unique) { const reLiteralUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i const uuidLength = 36; if (unique.length >= uuidLength) { const tail = unique.substr(-uuidLength); if (reLiteralUUID.test(tail)) { return unique.slice(0, -1 * (uuidLength + 1)) } } return unique } function compareObjects(objectA, objectB) { const object = "[object Object]"; return Object.prototype.toString.call(objectA) === object && Object.prototype.toString.call(objectB) === object; } function compareObjectKeys(itemAKeys, itemBKeys) { const lengthA = itemAKeys.length; const lengthB = itemBKeys.length; // nothing to compare, probably identical if (lengthA == 0 && lengthB == 0) return true; // duh if (lengthA != lengthB) return false; for (let item of itemAKeys) { if (!itemBKeys.includes(item)) return false; } return true; } function formatScopeKeys(keys) { for (let i in keys) { keys[i] = format(keys[i]); } return keys } } if (typeof module != 'undefined') { if (!module.parent) { if (process.argv.length > 2 && process.argv[2] === '-big') { bufs = [] process.stdin.on('data', function(buf) { bufs.push(buf) }) process.stdin.on('end', function() { const yaml = Buffer.concat(bufs).toString('utf8') console.log(yamlToGo(yaml).go) }) } else { process.stdin.on('data', function(buf) { const yaml = buf.toString('utf8') console.log(yamlToGo(yaml).go) }) } } else { module.exports = yamlToGo } }