Rubyライブラリによって実装されたリンター
公開: 2022-03-11「 linter 」または「 lint 」という言葉を聞いたとき、そのようなツールがどのように機能するか、またはそれが何をすべきかについて、すでに一定の期待を持っているでしょう。
あるToptal開発者が管理しているRubocopや、JSLint、ESLint、またはあまり知られていないか人気のないものについて考えているかもしれません。
この記事では、さまざまな種類のリンターを紹介します。 コード構文をチェックしたり、Abstract-Syntax-Treeを検証したりすることはありませんが、コードを検証します。 彼らは、実装が特定のインターフェースに準拠しているかどうかをチェックします。これは、字句的に(ダック・タイピングおよび従来のインターフェースに関して)だけでなく、意味的にも行われます。
それらに慣れるために、いくつかの実際的な例を分析してみましょう。 Railsの熱心な専門家でない場合は、最初にこれを読むことをお勧めします。
基本的なLintから始めましょう。
ActiveModel :: Lint :: Tests
このLintの動作については、Railsの公式ドキュメントで詳しく説明されています。
「 TestCaseにActiveModel::Lint::Testsを含めることで、オブジェクトがActiveModelAPIに準拠しているかどうかをテストできます。 オブジェクトが完全に準拠しているかどうか、または準拠していない場合はAPIのどの側面が実装されていないかを示すテストが含まれます。 アクションパックを使用するために、オブジェクトがすべてのAPIを実装する必要はないことに注意してください。 このモジュールは、すべての機能をすぐに使用できるようにする場合にのみガイダンスを提供することを目的としています。」
したがって、クラスを実装していて、 redirect_to, form_forなどの既存のRails機能でそれを使用したい場合は、いくつかのメソッドを実装する必要があります。 この機能はActiveRecordオブジェクトに限定されません。 それはあなたのオブジェクトでも機能しますが、彼らは適切にいんちきをすることを学ぶ必要があります。
実装
実装は比較的簡単です。 これは、テストケースに含めるために作成されたモジュールです。 test_で始まるメソッドは、フレームワークによって実装されます。 @modelインスタンス変数は、テストの前にユーザーによって設定されることが期待されます。
module ActiveModel module Lint module Tests def test_to_key assert_respond_to model, :to_key def model.persisted?() false end assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false" end def test_to_param assert_respond_to model, :to_param def model.to_key() [1] end def model.persisted?() false end assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" end ... private def model assert_respond_to @model, :to_model @model.to_model end使用法
class Person def persisted? false end def to_key nil end def to_param nil end # ... end # test/models/person_test.rb require "test_helper" class PersonTest < ActiveSupport::TestCase include ActiveModel::Lint::Tests setup do @model = Person.new end endActiveModel :: Serializer :: Lint :: Tests
アクティブモデルのシリアライザーは新しいものではありませんが、それらから学び続けることができます。 ActiveModel::Serializer::Lint::Testsを含めて、オブジェクトがActive ModelSerializersAPIに準拠しているかどうかを確認します。 そうでない場合、テストはどの部分が欠落しているかを示します。
ただし、ドキュメントには、セマンティクスをチェックしないという重要な警告があります。
「これらのテストは、戻り値のセマンティックの正しさを判断しようとはしていません。 たとえば、 serializable_hashを実装して常に{}を返すことができ、テストに合格します。 値が意味的に意味があることを確認するのはあなた次第です。」
つまり、インターフェイスの形状のみをチェックしています。 それがどのように実装されているか見てみましょう。
実装
これは、 ActiveModel::Lint::Testsの実装で少し前に見たものと非常に似ていますが、戻り値のアリティまたはクラスをチェックするため、場合によってはもう少し厳密になります。
module ActiveModel class Serializer module Lint module Tests # Passes if the object responds to <tt>read_attribute_for_serialization</tt> # and if it requires one argument (the attribute to be read). # Fails otherwise. # # <tt>read_attribute_for_serialization</tt> gets the attribute value for serialization # Typically, it is implemented by including ActiveModel::Serialization. def test_read_attribute_for_serialization assert_respond_to resource, :read_attribute_for_serialization, 'The resource should respond to read_attribute_for_serialization' actual_arity = resource.method(:read_attribute_for_serialization).arity # using absolute value since arity is: # 1 for def read_attribute_for_serialization(name); end # -1 for alias :read_attribute_for_serialization :send assert_equal 1, actual_arity.abs, "expected #{actual_arity.inspect}.abs to be 1 or -1" end # Passes if the object's class responds to <tt>model_name</tt> and if it # is in an instance of +ActiveModel::Name+. # Fails otherwise. # # <tt>model_name</tt> returns an ActiveModel::Name instance. # It is used by the serializer to identify the object's type. # It is not required unless caching is enabled. def test_model_name resource_class = resource.class assert_respond_to resource_class, :model_name assert_instance_of resource_class.model_name, ActiveModel::Name end ...使用法
ActiveModelSerializersがlintをテストケースに含めることでlintを使用する方法の例を次に示します。
module ActiveModelSerializers class ModelTest < ActiveSupport::TestCase include ActiveModel::Serializer::Lint::Tests setup do @resource = ActiveModelSerializers::Model.new end def test_initialization_with_string_keys klass = Class.new(ActiveModelSerializers::Model) do attributes :key end value = 'value' model_instance = klass.new('key' => value) assert_equal model_instance.read_attribute_for_serialization(:key), value endラック::リント
前の例では、セマンティクスは考慮されていませんでした。
ただし、 Rack::Lintはまったく別の獣です。 アプリケーションをラップできるのはRackミドルウェアです。この場合、ミドルウェアはリンターの役割を果たします。 リンターは、要求と応答がラックの仕様に従って構築されているかどうかを確認します。 これは、Rackアプリケーションを提供するRackサーバー(つまり、Puma)を実装していて、Rack仕様に準拠していることを確認したい場合に便利です。
または、非常にむき出しのアプリケーションを実装し、HTTPプロトコルに関連する単純な間違いをしないようにする場合に使用されます。
実装
module Rack class Lint def initialize(app) @app = app @content_length = nil end def call(env = nil) dup._call(env) end def _call(env) raise LintError, "No env given" unless env check_env env env[RACK_INPUT] = InputWrapper.new(env[RACK_INPUT]) env[RACK_ERRORS] = ErrorWrapper.new(env[RACK_ERRORS]) ary = @app.call(env) raise LintError, "response is not an Array, but #{ary.class}" unless ary.kind_of? Array raise LintError, "response array has #{ary.size} elements instead of 3" unless ary.size == 3 status, headers, @body = ary check_status status check_headers headers hijack_proc = check_hijack_response headers, env if hijack_proc && headers.is_a?(Hash) headers[RACK_HIJACK] = hijack_proc end check_content_type status, headers check_content_length status, headers @head_request = env[REQUEST_METHOD] == HEAD [status, headers, self] end ## === The Content-Type def check_content_type(status, headers) headers.each { |key, value| ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx, 204 or 304. if key.downcase == "content-type" if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i raise LintError, "Content-Type header found in #{status} response, not allowed" end return end } end ## === The Content-Length def check_content_length(status, headers) headers.each { |key, value| if key.downcase == 'content-length' ## There must not be a <tt>Content-Length</tt> header when the +Status+ is 1xx, 204 or 304. if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i raise LintError, "Content-Length header found in #{status} response, not allowed" end @content_length = value end } end ...アプリでの使用
非常に単純なエンドポイントを構築するとします。 「コンテンツなし」と応答する場合もありますが、意図的にミスを犯したため、50%のケースでコンテンツを送信します。

# foo.rb # run with rackup foo.rb Foo = Rack::Builder.new do use Rack::Lint use Rack::ContentLength app = proc do |env| if rand > 0.5 no_content = Rack::Utils::HTTP_STATUS_CODES.invert['No Content'] [no_content, { 'Content-Type' => 'text/plain' }, ['bummer no content with content']] else ok = Rack::Utils::HTTP_STATUS_CODES.invert['OK'] [ok, { 'Content-Type' => 'text/plain' }, ['good']] end end run app end.to_app このような場合、 Rack::Lintは応答をインターセプトして検証し、例外を発生させます。
Rack::Lint::LintError: Content-Type header found in 204 response, not allowed /Users/dev/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/lint.rb:21:in `assert' /Users/dev/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/rack-2.2.3/lib/rack/lint.rb:710:in `block in check_content_type'プーマでの使用法
この例では、Pumaが非常に単純なアプリケーションlambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] }をどのようにラップするかを確認します。 lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] }最初にServerLint ( Rack::Lintから継承)で、次にErrorCheckerで。
仕様に従わない場合、lintは例外を発生させます。 チェッカーは例外をキャッチし、エラーコード500を返します。テストコードは、例外が発生しなかったことを確認します。
class TestRackServer < Minitest::Test class ErrorChecker def initialize(app) @app = app @exception = nil end attr_reader :exception, :env def call(env) begin @app.call(env) rescue Exception => e @exception = e [ 500, {}, ["Error detected"] ] end end end class ServerLint < Rack::Lint def call(env) check_env env @app.call(env) end end def setup @simple = lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } @server = Puma::Server.new @simple port = (@server.add_tcp_listener "127.0.0.1", 0).addr[1] @tcp = "http://127.0.0.1:#{port}" @stopped = false end def test_lint @checker = ErrorChecker.new ServerLint.new(@simple) @server.app = @checker @server.run hit(["#{@tcp}/test"]) stop refute @checker.exception, "Checker raised exception" endこのようにして、Pumaはラック互換の認定を受けていることが確認されています。
RailsEventStore-リポジトリLint
Rails Event Storeは、イベントを公開、消費、保存、および取得するためのライブラリです。 これは、Railsアプリケーションにイベント駆動型アーキテクチャを実装するのに役立つことを目的としています。 これは、リポジトリ、マッパー、ディスパッチャー、スケジューラー、サブスクリプション、シリアライザーなどの小さなコンポーネントで構築されたモジュラーライブラリです。 各コンポーネントは、互換性のある実装を持つことができます。
たとえば、デフォルトのリポジトリはActiveRecordを使用し、イベントを保存するために特定のテーブルレイアウトを想定しています。 ただし、実装では、イベントを保存せずにROMを使用したり、メモリ内で作業したりできます。これは、テストに役立ちます。
しかし、実装したコンポーネントがライブラリが期待する方法で動作するかどうかをどのように知ることができますか? もちろん、付属のリンターを使用します。 そして、それは計り知れません。 約80件をカバーしています。 それらのいくつかは比較的単純です:
specify 'adds an initial event to a new stream' do repository.append_to_stream([event = SRecord.new], stream, version_none) expect(read_events_forward(repository).first).to eq(event) expect(read_events_forward(repository, stream).first).to eq(event) expect(read_events_forward(repository, stream_other)).to be_empty endそして、いくつかはもう少し複雑で、不幸な道に関連しています:
it 'does not allow linking same event twice in a stream' do repository.append_to_stream( [SRecord.new(event_id: "a1b49edb")], stream, version_none ).link_to_stream(["a1b49edb"], stream_flow, version_none) expect do repository.link_to_stream(["a1b49edb"], stream_flow, version_0) end.to raise_error(EventDuplicatedInStream) endほぼ1,400行のRubyコードで、Rubyで記述された最大のリンターだと思います。 しかし、もっと大きなものを知っているなら、私に知らせてください。 興味深い部分は、セマンティクスについて100%であるということです。
インターフェイスも徹底的にテストしますが、この記事の範囲を考えると、それは後からの考えだと思います。
実装
リポジトリリンターは、RSpec共有例機能を使用して実装されます。
module RubyEventStore ::RSpec.shared_examples :event_repository do let(:helper) { EventRepositoryHelper.new } let(:specification) { Specification.new(SpecificationReader.new(repository, Mappers::NullMapper.new)) } let(:global_stream) { Stream.new(GLOBAL_STREAM) } let(:stream) { Stream.new(SecureRandom.uuid) } let(:stream_flow) { Stream.new('flow') } # ... it 'just created is empty' do expect(read_events_forward(repository)).to be_empty end specify 'append_to_stream returns self' do repository .append_to_stream([event = SRecord.new], stream, version_none) .append_to_stream([event = SRecord.new], stream, version_0) end # ...使用法
このリンターは、他のリンターと同様に、いくつかのメソッド、最も重要なのは検証対象の実装を返すrepositoryを提供することを期待しています。 テスト例は、組み込みのinclude_examplesメソッドを使用して含まれています。
RSpec.describe EventRepository do include_examples :event_repository let(:repository) { EventRepository.new(serializer: YAML) } endまとめ
ご覧のとおり、「リンター」は、私たちが通常考えているものよりも少し広い意味を持っています。 互換性のある共同作業者を期待するライブラリを実装するときはいつでも、リンターの提供を検討することをお勧めします。
最初にそのようなテストに合格する唯一のクラスがライブラリによって提供されるクラスであるとしても、ソフトウェアエンジニアとしてのあなたが拡張性を真剣に受け止めていることを示しています。 また、偶然ではなく意識的に、コード内の各コンポーネントのインターフェイスについて考えることも難しくなります。
資力
- ActiveModel:RubyオブジェクトをActiveRecordのように感じさせる
- RailsCastアクティブモデルについて
- フルラックリントの実装
- プーマでのラックリントの使用
- Railsイベントストアの詳細
