OpenAPI Linstをローカルで実行

# 技術要素
- Node.js
- OpenAPI
- Swagger（で生成したyamlで検証しています）
- Github

# Github ActionsでOpenAPI Lintを実行
こんな感じで設定してあげればPull Request作成時にLintを実行してくれます。
```.github/workflows/test.yaml
- name: OpenAPI Lint Checks
- uses: nwestfall/openapi-action@v1.0.2
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    file: FILE_PATH
```
<参考>

https://github.com/nwestfall/openapi-action

# ローカルでOpenAPI Lintを実行
本題。
Lintエラーの対応結果をプッシュして確認するのは非常に辛かったのでローカルで実行できないか調査してみました。

## npxコマンドでチェック
以下のコマンドでチェックできます。（基本はこれで問題ないと思います）
```
npx @redocly/cli lint path-to-root-file.yaml
```
<参考>

https://github.com/Redocly/redocly-cli

## 出力内容をフィルタリング
上記コマンドで実行すると何故かGithub Actionsで実行した結果と異なったのでコードを書いてフィルタリングしてみました。
（開発元のソースを流用して変えたのは引数処理とexec関数内のとこだけです）
```index.js
// reference from https://github.com/nwestfall/openapi-action
const openapi = require('@redocly/openapi-core');
const yamlAst = require('yaml-ast-parser');

function getLineColLocation(location) {
  if (location.pointer === undefined) return location;

  const { source, pointer, reportOnKey } = location;
  const ast = source.getAst(yamlAst.safeLoad);
  const astNode = getAstNodeByPointer(ast, pointer, !!reportOnKey);
  var startPosition = 1;
  var endPosition = 1;
  if (astNode != undefined && astNode.startPosition != undefined)
    startPosition = astNode.startPosition;
  if (astNode != undefined && astNode.endPosition != undefined)
    endPosition = astNode.endPosition;
  const pos = positionsToLoc(source.body, startPosition, endPosition);
  return {
    ...pos
  };
}

function positionsToLoc(
  source,
  startPos,
  endPos,
) {
  let currentLine = 1;
  let currentCol = 1;
  let start = { line: 1, col: 1 };

  for (let i = 0; i < endPos - 1; i++) {
    if (i === startPos - 1) {
      start = { line: currentLine, col: currentCol + 1 };
    }
    if (source[i] === '\n') {
      currentLine++;
      currentCol = 1;
      if (i === startPos - 1) {
        start = { line: currentLine, col: currentCol };
      }

      if (source[i + 1] === '\r') i++; // TODO: test it
      continue;
    }
    currentCol++;
  }

  const end = startPos === endPos ? { ...start } : { line: currentLine, col: currentCol + 1 };
  return { start, end };
}

function unescapePointer(fragment) {
  return decodeURIComponent(fragment.replace(/~1/g, '/').replace(/~0/g, '~'));
}

function parsePointer(pointer) {
  return pointer.substr(2).split('/').map(unescapePointer);
}

function getAstNodeByPointer(root, pointer, reportOnKey) {
  const pointerSegments = parsePointer(pointer);
  if (root === undefined) {
    return undefined;
  }

  let currentNode = root;
  for (const key of pointerSegments) {
    if (currentNode.kind === yamlAst.Kind.MAP) {
      const mapping = currentNode.mappings.find((m) => m.key.value === key);
      if (!mapping || !mapping.value) break;
      currentNode = mapping.value;
    } else if (currentNode.kind === yamlAst.Kind.SEQ) {
      const elem = currentNode.items[parseInt(key, 10)];
      if (!elem) break;
      currentNode = elem;
    }
  }

  if (!reportOnKey) {
    return currentNode;
  } else {
    const parent = currentNode.parent;
    if (!parent) return currentNode;
    if (parent.kind === yamlAst.Kind.SEQ) {
      return currentNode;
    } else if (parent.kind === yamlAst.Kind.MAPPING) {
      return parent.key;
    } else {
      return currentNode;
    }
  }
}

async function exec(file) {
  try {
    const config = await openapi.loadConfig(undefined);
    const lintData = await openapi.lint({
      ref: file,
      config: config
    });

    const findings = [];
    const excludeRules = ['security-defined'];
    const excludePaths = ['#/paths/~1/get/responses'];
    for (var i = 0; i < lintData.length; i++) {
      const finding = lintData[i];
      const location = finding.location[0];
      if (!location.pointer.startsWith('#/paths') ||
        location.pointer.includes('sample') ||
        excludePaths.includes(location.pointer) ||
        excludeRules.includes(finding.ruleId)) continue;
      const line = getLineColLocation(location);
      findings.push({
        path: file,
        start_line: line.start.line,
        end_line: line.end.line,
        title: `${finding.ruleId} - ${location.pointer}`,
        message: finding.message,
        annotation_level: finding.severity === 'error' ? 'failure' : finding.severity == 'warn' ? 'warning' : 'notice'
      });
    }

    if (findings.length > 0) {
      console.dir(findings);
    } else {
      console.log('\x1b[32mNo annotations.');
    }
  } catch (e) {
    console.error(e)
  } finally {
    console.log('');
  }
}

if (argv.length < 3) {
  console.log('Usage: node index.yaml [target yaml path]');
  console.log('');
  process.exit(1);
} else {
  exec(argv[2]).then(() => {
    process.exit(0);
  });
}
```
```bash
node index.js
```

# さいごに
なかなか情報がないので開発元のソースを流用して作成してみました。
実は設定用のYAMLがあってそれを食わせるだけでフィルタリングできるんじゃないかと思ったりしてますが、そこまでは調査できていません。。。
ご存知の方がいらっしゃれば是非ご教示ください！