【Node.js】Stripeライブラリの入出力がsnake_caseになっているのをどうにかしたい

## はじめに

Node.jsでStripeを操作する際には、[Stripe Node.js Library](https://github.com/stripe/stripe-node)を利用する事になる。ただ、このライブラリのインターフェースはsnake_caseであり、JavaScriptのcameCaseと乖離があり利用しにくいと感じていた。

そこで今回は、インターフェース（入出力）をcameCaseで行えるようにするために、ライブラリに対しカスタムの拡張を行う実装をやってみたいと思う。

ソースコード全体は以下。

https://github.com/yuta-katayama-23/node-express/commit/3f76584935654a0ca472a7d8f8f42fbfab9dca57

## 実際にやってみる

```js
import Stripe from 'stripe';
import { strict as assert } from 'assert';
import snakecaseKeys from 'snakecase-keys';
import camelcaseKeys from 'camelcase-keys';

export default () => {
	assert.ok(
		process.env.STRIPE_KEY_SECRET,
		'env STRIPE_KEY_SECRET must be required'
	);
	const stripe = Stripe(process.env.STRIPE_KEY_SECRET);

	// IF（入出力）をcamelCaseにした関数を別途定義（$〇〇 のように利用）
	Object.keys(stripe)
		.filter(
			(category) => !category.startsWith('_') && !category.match(/^[A-Z].+/)
		)
		.forEach((category) => {
			Object.keys(Object.getPrototypeOf(stripe[category]))
				.filter((func) => typeof stripe[category][func] === 'function')
				.forEach((func) => {
					stripe[category][`$${func}`] = async (...args) => {
						const snakecaseArgs = snakecaseKeys(args, { deep: true });
						const result = await stripe[category][func](...snakecasedArgs);
						return camelcaseKeys(result, { deep: true });
					};
				});
		});

	return stripe;
};
```

上記の実装でうまくいく理由を順を追ってみていく。

まず、やりたい事を整理すると、やりたいのは元の関数を上書きする事なので、まずは元の関数そのものを取得する必要があるが、それは[Create a customer](https://stripe.com/docs/api/customers/create?lang=node)の実装を見ればどこに関数があるかは検討を付けられ、`stripe.〇〇.△△`の`△△`が関数になる。というわけで、`△△`だけを取り出せればいい。

まず、`console.log(Object.keys(stripe))`の実行結果としては以下のようになる。

```
    [
      'VERSION',              'on',             'once',
      'off',                  '_api',           'account',
      'accountLinks',         'accounts',       'applePayDomains',
      'applicationFees',      'balance',        'balanceTransactions',
      'charges',              'countrySpecs',   'coupons',
      'creditNotes',          'customers',      'disputes',
      'ephemeralKeys',        'events',         'exchangeRates',
      'fileLinks',            'files',          'invoiceItems',
      'invoices',             'mandates',       'oauth',
      'paymentIntents',       'paymentLinks',   'paymentMethods',
      'payouts',              'plans',          'prices',
      'products',             'promotionCodes', 'quotes',
      'refunds',              'reviews',        'setupAttempts',
      'setupIntents',         'shippingRates',  'sources',
      'subscriptionItems',    'subscriptions',  'subscriptionSchedules',
      'taxCodes',             'taxRates',       'tokens',
      'topups',               'transfers',      'webhookEndpoints',
      'apps',                 'billingPortal',  'checkout',
      'financialConnections', 'identity',       'issuing',
      'radar',                'reporting',      'sigma',
      'terminal',             'testHelpers',    'treasury',
      'errors',               'webhooks',       '_prevRequestMetrics',
      '_enableTelemetry',     'StripeResource'
    ]
```

この内、`_`で始めっているもの、大文字で始まっているものは不要なのでこの時点で除外する事にする。

```js
	Object.keys(stripe).filter(
		(category) => !category.startsWith('_') && !category.match(/^[A-Z].+/)
	);
```

続いて、残ったオブジェクトの中から関数を取り出すが、それには[Object.getPrototypeOf()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/getPrototypeOf)を利用する。例えば、[Customers](https://stripe.com/docs/api/customers?lang=node)が持つ関数一覧は以下のようにして取得できる。

```js
	Object.keys(stripe)
		.filter(
			(category) => !category.startsWith('_') && !category.match(/^[A-Z].+/)
		)
		.forEach((category) => {
			if (category === 'customers')
				console.log(Object.getPrototypeOf(stripe[category]));
		});
```
```
    {
      create: [Function (anonymous)],
      retrieve: [Function (anonymous)],
      update: [Function (anonymous)],
      list: [Function (anonymous)],
      del: [Function (anonymous)],
      createFundingInstructions: [Function (anonymous)],
      deleteDiscount: [Function (anonymous)],
      listPaymentMethods: [Function (anonymous)],
      retrievePaymentMethod: [Function (anonymous)],
      search: [Function (anonymous)],
      retrieveCashBalance: [Function (anonymous)],
      updateCashBalance: [Function (anonymous)],
      createBalanceTransaction: [Function (anonymous)],
      retrieveBalanceTransaction: [Function (anonymous)],
      updateBalanceTransaction: [Function (anonymous)],
      listBalanceTransactions: [Function (anonymous)],
      retrieveCashBalanceTransaction: [Function (anonymous)],
      listCashBalanceTransactions: [Function (anonymous)],
      createSource: [Function (anonymous)],
      retrieveSource: [Function (anonymous)],
      updateSource: [Function (anonymous)],
      listSources: [Function (anonymous)],
      deleteSource: [Function (anonymous)],
      verifySource: [Function (anonymous)],
      createTaxId: [Function (anonymous)],
      retrieveTaxId: [Function (anonymous)],
      listTaxIds: [Function (anonymous)],
      deleteTaxId: [Function (anonymous)]
    }
```

つまり、他のカテゴリーの関数も取得するには、以下のように実装すればいいだろう。

```js
	Object.keys(stripe)
		.filter(
			(category) => !category.startsWith('_') && !category.match(/^[A-Z].+/)
		)
		.forEach((category) => {
			Object.keys(Object.getPrototypeOf(stripe[category]))
				.filter((func) => typeof stripe[category][func] === 'function')
				.forEach((func) => {
					// ここで各Stripeのオブジェクト（Customerなど）に対するcreate, retrieve,...などの関数が取得できる
				});
		});
```

ここまで行けば、後はケース変換のロジックを書けばいいだけ。

## まとめ

JavaScriptだからこその黒魔術感はあるが、一応やりたい事は出来た。しかしなぜNode.jsのStripeの公式ライブラリの入出力（インターフェース）はsnake_caseなんだろうか・・・。

## おまけのTips
### Jestで環境変数を設定する

プロダクトコードでよく利用される方法と同じ方法が使える。具体的には、[dotenv](https://github.com/motdotla/dotenv)をすればよく、以下のようになる。

```js
import * as dotenv from 'dotenv';
// 省略

dotenv.config();

// 省略
```

※ちなみに、直接`process.env`を設定する方法もあるが、それについては[別記事](https://dev.classmethod.jp/articles/set-environment-variables-for-testing-without-using-dotenv-in-jest/#toc-3)を参照ください。