経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

はじめてOSSに新機能追加した

f:id:wafuwafu13:20210605205045p:plain


owという、Lots of built-in validationsOSSに、BigIntのバリデーションを追加した。

github.com
github.com

owに関しては2月頃に3回、型整備のPRを出してマージされていたので、コードには少し馴染みがあった。
https://wafuwafu13.hateblo.jp/#sindresorhusow

OSSの型整備より一歩踏み込んだことがしたいと思い、issueを消化することを目標に最近やっていた。
実際、null, undefinedに起因するバグがあったのでそれを消化できた。

Fix `ow.object` to return `ArgumentError` when `null` or `undefined` is passed by wafuwafu13 · Pull Request #211 · sindresorhus/ow · GitHub

そして今回、BigIntのサポートをするという、約3年前のissueがあったので拾ってやってみた。

Support BigInt validator · Issue #54 · sindresorhus/ow · GitHub

owの構造と、何をどう追加したのかを雑にメモしておく。

まず、README.mdにも載っている以下のコードでArgumentErrorがどのようにして起こるのかをみていく。

import ow from 'ow';

const unicorn = input => {
	ow(input, ow.string.minLength(5));

	// …
};

unicorn('yo');
//=> ArgumentError: Expected string `input` to have a minimum length of `5`, got `yo`


source/predicate/string.tsに、ow.stringに関するコードが定義されており、minLengthの定義は以下のようになっている。

ow/string.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

minLength(length: number): this {
    return this.addValidator({
	message: (value, label) => `Expected ${label} to have a minimum length of \`${length}\`, got \`${value}\``,
	validator: value => value.length >= length,
	negatedMessage: (value, label) => `Expected ${label} to have a maximum length of \`${length - 1}\`, got \`${value}\``
    });
}

このvalue => value.length >= lengthのバリデーターは、source/predicates/predicate.tsで発火される。
messageの発火も同じ部分である。

ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

...
result = validator(value);
...
const errorMessage = message(value, label_, result);

このvalidatorの実行結果がtrueでなければ、最終的に独自定義のArgumentErrorが投げられる。
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

throw new ArgumentError(message, main, errors);

ArgumentErrorが投げられるのはこれ以外にも、ow(input, ow.type.foo)においてinputtypeの型が合っていない場合がある。
例えばtest/string.tsでテストされているような場合がある。

ow/string.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

t.throws(() => {
	ow(12 as any, ow.string);
}, 'Expected argument to be of type `string` but received type `number`');

このバリデーションは、Predicateクラスのコンストラクタで定義されている。
ow/predicate.ts at 49841a260cd431985b91d319b091dd59096775a3 · sindresorhus/ow · GitHub

this.addValidator({
	message: (value, label) => {
		// We do not include type in this label as we do for other messages, because it would be redundant.
		const label_ = label?.slice(this.type.length + 1);

		// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
		return `Expected ${label_ || 'argument'} to be of type \`${this.type}\` but received type \`${is(value)}\``;
	},
	validator: value => (is as any)[typeString](value)
});


ここのバリデーションで使われているisは、owと同じ作者が作ったライブラリである。
github.com

今回やったissueに立ち返ってみると、isBigInt detectionがサポートされたので、それをowにも取り込もう、というものだった。
Support BigInt detection · Issue #39 · sindresorhus/is · GitHub

最初は、source/predicates/number.tsbigintメソッドを足して、ow(BigInt(9007199254740991), ow.number.bigint)で判定させようと思ったが、numberbigintで型が違うため、コンストラクタのバリデーションで意図せず弾かれてしまった。
新たにBigIntPredicateクラスを定義し、意図してコンストラクタのバリデーションで弾かれる方針で実装を進めた。

NodeのBigIntサポートが10.4.0からなのでテストが思うように動かなかったりいろいろしたが、無事マージされた。
このOSSの作者はFull-Time Open-Sourcererでレスポンスもレビューも手早くて時差があるものの開発体験がとても良かった。
ソースコードの核となる部分の7割くらいを理解できれば、issueのバグを潰したり新機能開発ができるという肌感がある。