بررسی اجمالی: Linter Ruby Libraries |  تاپتال
انتشار: فروردین 27، 1400
بروزرسانی: 15 آذر 1404

بررسی اجمالی: Linter Ruby Libraries | تاپتال


وقتی کلمه "لنگر" یا "پرزاحتمالاً از قبل انتظارات خاصی در مورد نحوه عملکرد چنین ابزاری یا کاری که باید انجام دهد دارید.

ممکن است به این فکر کنید روبوکاپ، که یکی از توسعه دهندگان Toptal حفظ می کند، یا از JSLint، ESLint، یا چیزی کمتر شناخته شده یا کمتر محبوب است.

این مقاله شما را با انواع مختلفی از لینترها آشنا می کند. آنها نه نحو کد را بررسی می کنند و نه Abstract-Syntax-Tree را تأیید می کنند، اما کد را تأیید می کنند. آنها بررسی می کنند که آیا یک پیاده سازی به یک رابط خاص پایبند است، نه تنها از نظر لغوی (از نظر تایپ اردک و رابط های کلاسیک) بلکه گاهی اوقات نیز از نظر معنایی.

برای آشنایی با آنها، اجازه دهید چند مثال کاربردی را تحلیل کنیم. اگر حرفه ای مشتاق Rails نیستید، ممکن است بخواهید ابتدا این را بخوانید.

بیایید با یک لینت اولیه شروع کنیم.

ActiveModel::Lint::تست ها

رفتار این لینت به تفصیل در توضیح داده شده است اسناد رسمی ریل:

"شما می توانید تست کنید که آیا یک شی با API Active Model مطابقت دارد یا خیر ActiveModel::Lint::Tests در شما TestCase. شامل تست هایی می شود که به شما می گوید آیا شیء شما کاملاً سازگار است یا اگر نه، کدام جنبه های API پیاده سازی نشده اند. توجه داشته باشید که برای پیاده سازی همه APIها برای کار با Action Pack به یک شی نیاز نیست. این ماژول فقط در صورتی که بخواهید همه ویژگی ها را خارج از جعبه داشته باشید، راهنمایی ارائه می دهد.

بنابراین، اگر کلاسی را پیاده سازی می کنید و می خواهید از آن با قابلیت های موجود Rails مانند redirect_to, form_for، باید چند روش را پیاده سازی کنید. این قابلیت محدود به 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.rbrequire "test_helper"class PersonTest < ActiveSupport::TestCase  include ActiveModel::Lint::Tests  setup do    @model = Person.new  endend

ActiveModel::Serializer::Lint::تست ها

سریال سازهای مدل اکتیو جدید نیستند، اما می توانیم از آنها یاد بگیریم. شما شامل می شوید ActiveModel::Serializer::Lint::Tests برای بررسی اینکه آیا یک شی با Active Model Serializers API. اگر اینطور نباشد، آزمایش ها نشان می دهد که کدام قسمت ها از دست رفته اند.

با این حال، در اسناد، یک هشدار مهم پیدا خواهید کرد که معنایی را بررسی نمی کند:

«این آزمون ها تلاشی برای تعیین صحت معنایی مقادیر بازگشتی نمی کنند. به عنوان مثال، شما می توانید پیاده سازی کنید 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_serializationassert_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 :sendassert_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_nameresource_class = resource.classassert_respond_to resource_class, :model_nameassert_instance_of resource_class.model_name, ActiveModel::Name        end        ...

استفاده

در اینجا یک مثال از چگونگی ActiveModelSerializers از پرز با قرار دادن آن در مورد آزمایشی خود استفاده می کند:

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 سرویس می دهد و می خواهید اطمینان حاصل کنید که از مشخصات 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"endreturn        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.rbFoo = 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 append.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"]] } اول در یک ServerLint (که از آن به ارث می رسد Rack::Lint) سپس در ErrorChecker.

پرز در مواردی که از مشخصات پیروی نمی شود استثناهایی ایجاد می کند. جستجوگر استثناها را می گیرد و کد خطای 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 = "    @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 تایید می شود که دارای گواهی سازگاری با Rack است.

RailsEventStore - Repository Lint

فروشگاه رویداد ریل کتابخانه ای برای انتشار، مصرف، ذخیره و بازیابی رویدادها است. هدف آن کمک به شما در اجرای معماری رویداد محور برای برنامه Rails شماست. این یک کتابخانه مدولار است که با اجزای کوچکی مانند مخزن، نقشه بردار، توزیع کننده، زمان بندی، اشتراک ها و سریال ساز ساخته شده است. هر جزء می تواند یک پیاده سازی قابل تعویض داشته باشد.

برای مثال، مخزن پیش فرض از ActiveRecord استفاده می کند و طرح بندی جدول خاصی را برای ذخیره رویدادها در نظر می گیرد. با این حال، پیاده سازی شما می تواند از رام یا کار استفاده کند در حافظه بدون ذخیره رویدادها، که برای آزمایش مفید است.

اما چگونه می توانید بفهمید که آیا مؤلفه ای که پیاده سازی کرده اید به گونه ای رفتار می کند که کتابخانه انتظار دارد؟ با استفاده از لینتر ارائه شده، البته. و بسیار زیاد است. حدود 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_emptyend

و برخی از آنها کمی پیچیده تر هستند و به آنها مربوط می شود مسیرهای ناخوش:

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

تقریبا در 1400 خط کد روبی، من معتقدم این بزرگترین لنتر نوشته شده در روبی است. اما اگر از یک بزرگتر آگاه هستید، خبرم کن. بخش جالب این است که 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، که پیاده سازی را برای تأیید برمی گرداند. نمونه های آزمایشی با استفاده از RSpec داخلی گنجانده شده اند include_examples روش:

RSpec.describe EventRepository do    include_examples :event_repository    let(:repository) { EventRepository.new(serializer: YAML) }end

بسته بندی

همانطور که می بینید، "لنگر” معنای کمی گسترده تر از آنچه ما معمولاً در ذهن داریم دارد. هر زمان که کتابخانه ای را پیاده سازی می کنید که انتظار برخی از همکاران قابل تعویض را دارد، من شما را تشویق می کنم که یک لینتر تهیه کنید.

حتی اگر تنها کلاسی که در ابتدا چنین آزمون هایی را گذرانده است، کلاسی باشد که توسط کتابخانه شما ارائه می شود، این نشانه آن است که شما به عنوان یک مهندس نرم افزار توسعه پذیری را جدی می گیرید. همچنین شما را به چالش می کشد تا در مورد رابط هر جزء در کد خود فکر کنید، نه تصادفی بلکه آگاهانه.

منابع



منبع