File size: 3,140 Bytes
048b1e8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | #!/usr/bin/env node
import fs from 'node:fs/promises'
import process from 'node:process'
import YAML from 'yaml'
const FLATTEN_MARKER = 'x-flatten-allOf'
const YAML_OPTIONS = {
indent: 2,
lineWidth: 0,
}
function isPlainObject(value) {
return value != null && typeof value === 'object' && !Array.isArray(value)
}
function isRefObject(value) {
return isPlainObject(value) && typeof value.$ref === 'string'
}
function getMovableKeys(node) {
return Object.keys(node).filter(
(key) => key !== 'allOf' && key !== FLATTEN_MARKER && !key.startsWith('x-'),
)
}
function moveSiblingPropertiesIntoAllOf(node) {
const movableKeys = getMovableKeys(node)
if (movableKeys.length === 0) {
if (node[FLATTEN_MARKER] !== true) {
node[FLATTEN_MARKER] = true
return true
}
return false
}
const moved = {}
for (const key of movableKeys) {
moved[key] = node[key]
delete node[key]
}
node.allOf.push(moved)
node[FLATTEN_MARKER] = true
return true
}
/**
* Move sibling properties into an allOf branch when the schema contains a
* referenced member. This keeps flattening hints consistent across the file.
*/
function flattenAllOf(node) {
if (Array.isArray(node)) {
let changed = false
for (const item of node) {
changed = flattenAllOf(item) || changed
}
return changed
}
if (!isPlainObject(node)) {
return false
}
let changed = false
const { allOf } = node
const hasRefInAllOf =
Array.isArray(allOf) && allOf.some((item) => isRefObject(item))
if (hasRefInAllOf) {
changed = moveSiblingPropertiesIntoAllOf(node) || changed
}
for (const value of Object.values(node)) {
changed = flattenAllOf(value) || changed
}
return changed
}
async function pathExists(path) {
try {
await fs.access(path)
return true
} catch {
return false
}
}
async function validateInputFile(filePath) {
if (!(await pathExists(filePath))) {
throw new Error(`file not found: ${filePath}`)
}
const stat = await fs.stat(filePath)
if (!stat.isFile()) {
throw new Error(`not a file: ${filePath}`)
}
}
async function readYamlFile(filePath) {
const raw = await fs.readFile(filePath, 'utf8')
try {
return YAML.parse(raw)
} catch (error) {
throw new Error(`parse error: ${error.message}`)
}
}
async function writeYamlFile(filePath, document) {
const output = YAML.stringify(document, YAML_OPTIONS)
const normalized = output.endsWith('\n') ? output : `${output}\n`
await fs.writeFile(filePath, normalized, 'utf8')
}
function printUsage() {
process.stderr.write('Usage: flatten-allof.mjs <openapi.yaml>\n')
}
async function main() {
const [filePath] = process.argv.slice(2)
if (!filePath) {
printUsage()
process.exitCode = 1
return
}
try {
await validateInputFile(filePath)
const parsed = await readYamlFile(filePath)
const changed = flattenAllOf(parsed)
if (changed) {
await writeYamlFile(filePath, parsed)
}
} catch (error) {
process.stderr.write(`flatten-allof: ${error.message}\n`)
process.exitCode = 1
}
}
await main()
|