0
0

More than 1 year has passed since last update.

OpenAPI Linstをローカルで実行

Posted at

技術要素

  • 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

<参考>

ローカルでOpenAPI Lintを実行

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

npxコマンドでチェック

以下のコマンドでチェックできます。(基本はこれで問題ないと思います)

npx @redocly/cli lint path-to-root-file.yaml

<参考>

出力内容をフィルタリング

上記コマンドで実行すると何故か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);
  });
}
node index.js

さいごに

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0