بروزرسانی: 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 endendActiveModel::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بسته بندی
همانطور که می بینید، "لنگر” معنای کمی گسترده تر از آنچه ما معمولاً در ذهن داریم دارد. هر زمان که کتابخانه ای را پیاده سازی می کنید که انتظار برخی از همکاران قابل تعویض را دارد، من شما را تشویق می کنم که یک لینتر تهیه کنید.
حتی اگر تنها کلاسی که در ابتدا چنین آزمون هایی را گذرانده است، کلاسی باشد که توسط کتابخانه شما ارائه می شود، این نشانه آن است که شما به عنوان یک مهندس نرم افزار توسعه پذیری را جدی می گیرید. همچنین شما را به چالش می کشد تا در مورد رابط هر جزء در کد خود فکر کنید، نه تصادفی بلکه آگاهانه.
منابع
منبع