بررسی اجمالی: 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.rb
require "test_helper"

class PersonTest < ActiveSupport::TestCase
  include ActiveModel::Lint::Tests

  setup do
    @model = Person.new
  end
end

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_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 از پرز با قرار دادن آن در مورد آزمایشی خود استفاده می کند:

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"
          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"]] } اول در یک 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_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

تقریبا در 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

بسته بندی

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

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

منابع





منبع

Matthew Newman

Matthew Newman Matthew has over 15 years of experience in database management and software development, with a strong focus on full-stack web applications. He specializes in Django and Vue.js with expertise deploying to both server and serverless environments on AWS. He also works with relational databases and large datasets
[ Back To Top ]