発生しているエラー
Committee::InvalidRequest:
#/paths/~1contracts/post/requestBody/content/multipart~1form-data/schema/properties/original_file expected string, but received ActionDispatch::Http::UploadedFile: #<ActionDispatch::Http::UploadedFile:0x00007fa77e1f36a0>
何がしたいのか
multipart/form-data
形式でファイルアップロードを受け取るようなAPIを定義している。
APIはOpenAPI3.0の仕様に基づいて設計していて、例えば以下のようになる。
requestBody:
original_file:
image/png:
schema:
type: string
format: binary
Swaggerのドキュメントにもある。
このAPIに対して committee
gem を使ってRspecを書きたい。
テストの内容
# formのパラメータ
let(:file_upload_form) {
{
article_id: 1_000,
name: 'This is good article',
# set real existing file
original_file: fixture_file_upload(
Rails.root.join('sample_files/sample.docx'),
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
}
}
Requestとして使うパラメータは上記のように定義していて、アップロードするファイルはテスト用に用意したDocxファイル。
これを以下のようにPOSTしてAPIのフォーマットを assert_schema_conform
メソッドで検証している。
it 'uploads file' do
post api_v1_images_path,
headers: authenticated_header(user),
params: file_upload_form
expect(response).to have_http_status(:success)
assert_schema_conform
end
エラーの原因
APIのRequestの仕様が type: string
なのに、テストでは ActionDispatch
のオブジェクトが設定されているので、期待しているのと違う、っていうエラーが発生している。
しかし、ファイルアップロードのテストなのでStringでは内部の処理がうごかないので、変えることもできない。これは困った。
という状況
エラーの発生箇所
エラーは以下の OpenAPIParser
の処理で発生していう。
[lib/openapi_parser/schema_validators/string_validator.rb#L11]
(https://github.com/ota42y/openapi_parser/blob/44c640cc103bbbb9e8029e41a8889e8fd9350902/lib/openapi_parser/schema_validators/string_validator.rb#L11)
def coerce_and_validate(value, schema, **_keyword_args)
return OpenAPIParser::ValidateError.build_error_result(value, schema) unless value.kind_of?(String)
# ... 以下省略 ...
end
単純に value
が String
クラスかどうかをみていて、String
じゃない場合はErrorになっている。今回はここが ActionDispatch
のオブジェクトなので value.kind_of?(String)
が偽となりエラーが発生してしまう。
type: string
であってもformat: binary
の場合はString
クラス以外も許す
ファイルのように、format: binay
の場合はエラーをRaiseしないように条件を変えてやれば良い。そのためのパッチを作成する。
バッチとして該当のメソッド coerce_and_validate(value, schema, **_keyword_args)
を定義した任意のModuleを作成する。
module StringValidatorPatch
def coerce_and_validate(value, schema, **keyword_args)
# この処理を変更
# https://github.com/ota42y/openapi_parser/blob/61874f0190a86c09bdfb78de5f51cfb6ae16068b/lib/openapi_parser/schema_validators/string_validator.rb#L11
if !value.is_a?(String) && schema.format != 'binary'
return OpenAPIParser::ValidateError.build_error_result(value, schema)
end
# --- ここまで
value, err = check_enum_include(value, schema)
return [nil, err] if err
value, err = pattern_validate(value, schema)
return [nil, err] if err
unless @datetime_coerce_class.nil?
value, err = coerce_date_time(value, schema)
return [nil, err] if err
end
value, err = validate_max_min_length(value, schema)
return [nil, err] if err
value, err = validate_email_format(value, schema)
return [nil, err] if err
value, err = validate_uuid_format(value, schema)
return [nil, err] if err
[value, nil]
end
end
このように、変更したいメソッドだけを再定義したModuleを定義する。このModuleをパッチを当てたいクラス、今回はOpenAPIParser::SchemaValidator::StringValidator
クラスを再オープンし、Module#prepend
を使ってメソッドを上書きする。
Module#prependのリファレンス
class OpenAPIParser::SchemaValidator::StringValidator
prepend StringValidatorPatch
end
これで Committee::InvalidRequest
エラーが発生しなくなる
影響を最低限に抑える
パッチを当てることでCommittee::InvalidRequest
エラーを回避することはできるが、グローバルなスコープでパッチを当ててしまうと全体に影響が出てしまう。
この変更はファイルアップロードを伴うテストの時だけ有効にしたい。
そのためRspecの必要なコンテキストでだけ反映されるように context 'some context' do ... end
の中で定義することを考える。
RSpec.describe SomeController, type: :request do
context 'some context' do
# context内でパッチを当てる
module StringValidatorPatch
def coerce_and_validate(value, schema, **keyword_args)
if !value.is_a?(String) && schema.format != 'binary'
return OpenAPIParser::ValidateError.build_error_result(value, schema)
end
# (以下略)
end
end
class OpenAPIParser::SchemaValidator::StringValidator
prepend StringValidatorPatch
end
# ここまでパッチ
it 'uploads file' do
post api_v1_images_path,
headers: authenticated_header(user),
params: file_upload_form
expect(response).to have_http_status(:success)
assert_schema_conform
end
end
end
しかし、ブロックのスコープは定数を分離したり名前空間を設定したりしないので、ブロック内でクラス定義などの処理は避けたい
Rubocopの Lint/ConstantDefinitionInBlockにもひっかかってしまう。
Rspecで同様の定数定義を行う場合は、stub_const()
を使って定義する。
stub_const()
を使って必要な箇所にパッチを適用する
クラスの再オープンをclass
キーワードを使わずに行うには Class.newを使う。
ブロックを渡すことで、クラス定義も行える
Foo = Class.new {|c|
def hello; 'hello'; end
}
puts Foo.new.hello # => 'hello'
これを使って、パッチを当てたクラスを定義する。パッチ用のStringValidatorPatch
moduleは spec/support
ディレクトリに string_validator_patch.rb
として保存し、読み込まれるようにしておく。
patched = Class.new(OpenAPIParser::SchemaValidator::StringValidator) do |klass|
klass.prepend StringValidatorPatch
end
stub_const('OpenAPIParser::SchemaValidator::StringValidator', patched)
Class.new()
の引数にクラスを渡すと親クラスとして扱われるので、上記の場合は patched
が OpenAPIParser::SchemaValidator::StringValidator
の子クラスとなる。
それを stub_const()
を使って定数定義をしてやれば、パッチを当てたクラスを利用することができる。
この処理を必要に応じて before
や let
で実装する。
まとめ
-
Committee::InvalidRequest
エラーを解消するには、Validatorクラスにパッチを当てる - パッチを当てるには当該メソッドだけを実装したModuleを定義し、
prepend
を使って反映する - RSpecでは
Class.new()
とstub_const()
を使って局所的にパッチをあてる
module StringValidatorPatch
def coerce_and_validate(value, schema, **keyword_args)
# この処理を変更
# https://github.com/ota42y/openapi_parser/blob/61874f0190a86c09bdfb78de5f51cfb6ae16068b/lib/openapi_parser/schema_validators/string_validator.rb#L11
if !value.is_a?(String) && schema.format != 'binary'
return OpenAPIParser::ValidateError.build_error_result(value, schema)
end
# --- ここまで
value, err = check_enum_include(value, schema)
return [nil, err] if err
value, err = pattern_validate(value, schema)
return [nil, err] if err
unless @datetime_coerce_class.nil?
value, err = coerce_date_time(value, schema)
return [nil, err] if err
end
value, err = validate_max_min_length(value, schema)
return [nil, err] if err
value, err = validate_email_format(value, schema)
return [nil, err] if err
value, err = validate_uuid_format(value, schema)
return [nil, err] if err
[value, nil]
end
end
RSpec.describe SomeController, type: :request do
context 'some context' do
let(:file_upload_form) {
{
article_id: 1_000,
name: 'This is good article',
original_file: fixture_file_upload(
Rails.root.join('sample_files/sample.docx'),
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
}
}
before do
# context内でパッチを当てる
patched = Class.new(OpenAPIParser::SchemaValidator::StringValidator) do |klass|
klass.prepend StringValidatorPatch
end
stub_const('OpenAPIParser::SchemaValidator::StringValidator', patched)
end
it 'uploads file' do
post api_v1_images_path,
headers: authenticated_header(user),
params: file_upload_form
expect(response).to have_http_status(:success)
assert_schema_conform
end
end
end