Linter Diimplementasikan oleh Perpustakaan Ruby
Diterbitkan: 2022-03-11Ketika Anda mendengar kata " linter " atau " lint " Anda mungkin sudah memiliki harapan tertentu tentang cara kerja alat tersebut atau apa yang harus dilakukan.
Anda mungkin berpikir tentang Rubocop, yang dikelola oleh salah satu pengembang Toptal, atau tentang JSLint, ESLint, atau sesuatu yang kurang terkenal atau kurang populer.
Artikel ini akan memperkenalkan Anda pada jenis linter yang berbeda. Mereka tidak memeriksa sintaks kode juga tidak memverifikasi Abstrak-Sintaks-Tree, tetapi mereka memverifikasi kode. Mereka memeriksa apakah implementasi mematuhi antarmuka tertentu, tidak hanya secara leksikal (dalam hal pengetikan bebek dan antarmuka klasik) tetapi terkadang juga secara semantik.
Untuk membiasakan diri dengan mereka, mari kita menganalisis beberapa contoh praktis. Jika Anda bukan seorang profesional Rails yang rajin, Anda mungkin ingin membaca ini terlebih dahulu.
Mari kita mulai dengan Lint dasar.
ActiveModel::Lint::Tes
Perilaku Lint ini dijelaskan secara rinci dalam dokumentasi Rails resmi:
“Anda dapat menguji apakah suatu objek sesuai dengan Active Model API dengan menyertakan ActiveModel::Lint::Tests di TestCase Anda. Ini akan mencakup pengujian yang memberi tahu Anda apakah objek Anda sepenuhnya sesuai atau, jika tidak, aspek API mana yang tidak diterapkan. Perhatikan bahwa objek tidak diperlukan untuk mengimplementasikan semua API agar dapat bekerja dengan Paket Tindakan. Modul ini hanya dimaksudkan untuk memberikan panduan jika Anda ingin semua fitur di luar kotak.”
Jadi, jika Anda mengimplementasikan kelas dan ingin menggunakannya dengan fungsionalitas Rails yang ada seperti redirect_to, form_for , Anda perlu mengimplementasikan beberapa metode. Fungsionalitas ini tidak terbatas pada objek ActiveRecord . Ini dapat bekerja dengan objek Anda juga, tetapi mereka perlu belajar dukun dengan benar.
Penerapan
Pelaksanaannya relatif mudah. Ini adalah modul yang dibuat untuk dimasukkan dalam kasus uji. Metode yang dimulai dengan test_ akan diimplementasikan oleh kerangka kerja Anda. Diharapkan variabel instans @model akan disiapkan oleh pengguna sebelum pengujian:
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 endPenggunaan
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::Tes
Serializer model aktif bukanlah hal baru, tetapi kita dapat terus belajar darinya. Anda menyertakan ActiveModel::Serializer::Lint::Tests untuk memverifikasi apakah suatu objek sesuai dengan Active Model Serializers API. Jika tidak, tes akan menunjukkan bagian mana yang hilang.
Namun, di dokumen, Anda akan menemukan peringatan penting bahwa itu tidak memeriksa semantik:
“Tes ini tidak mencoba untuk menentukan kebenaran semantik dari nilai yang dikembalikan. Misalnya, Anda bisa mengimplementasikan serializable_hash untuk selalu mengembalikan {} , dan pengujian akan lulus. Terserah Anda untuk memastikan bahwa nilai-nilai itu bermakna secara semantik.”
Dengan kata lain, kami hanya memeriksa bentuk antarmuka. Sekarang mari kita lihat bagaimana penerapannya.
Penerapan
Ini sangat mirip dengan apa yang kita lihat beberapa saat yang lalu dengan implementasi ActiveModel::Lint::Tests , tetapi sedikit lebih ketat dalam beberapa kasus karena memeriksa arity atau kelas dari nilai yang dikembalikan:
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 ...Penggunaan
Berikut adalah contoh bagaimana ActiveModelSerializers menggunakan lint dengan memasukkannya ke dalam test case-nya:
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 endRak::Lint
Contoh sebelumnya tidak peduli dengan semantik .
Namun, Rack::Lint adalah binatang yang sama sekali berbeda. Ini adalah middleware Rack tempat Anda dapat membungkus aplikasi Anda. Middleware memainkan peran sebagai linter dalam kasus ini. Linter akan memeriksa apakah permintaan dan tanggapan dibuat sesuai dengan spesifikasi Rak. Ini berguna jika Anda menerapkan server Rack (yaitu, Puma) yang akan melayani aplikasi Rack dan Anda ingin memastikan bahwa Anda mengikuti spesifikasi Rack.
Atau, ini digunakan ketika Anda mengimplementasikan aplikasi yang sangat sederhana dan Anda ingin memastikan bahwa Anda tidak membuat kesalahan sederhana terkait dengan protokol HTTP.
Penerapan
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 ...Penggunaan di Aplikasi Anda
Katakanlah kita membangun titik akhir yang sangat sederhana. Terkadang itu harus merespons dengan "Tidak Ada Konten", tetapi kami membuat kesalahan yang disengaja dan kami akan mengirimkan beberapa konten dalam 50% kasus:

# 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 Dalam kasus seperti itu, Rack::Lint akan mencegat respons, memverifikasinya, dan memunculkan pengecualian:
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'Penggunaan di Puma
Dalam contoh ini kita melihat bagaimana Puma membungkus aplikasi yang sangat sederhana lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } lambda { |env| [200, { "X-Header" => "Works" }, ["Hello"]] } pertama di ServerLint (yang mewarisi dari Rack::Lint ) kemudian di ErrorChecker .
Lint memunculkan pengecualian jika spesifikasi tidak diikuti. Pemeriksa menangkap pengecualian dan mengembalikan kode kesalahan 500. Kode pengujian memverifikasi bahwa pengecualian tidak terjadi:
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" endBegitulah cara Puma diverifikasi agar kompatibel dengan Rak bersertifikat.
RailsEventStore - Repositori Lint
Rails Event Store adalah perpustakaan untuk menerbitkan, menggunakan, menyimpan, dan mengambil acara. Ini bertujuan untuk membantu Anda dalam mengimplementasikan Arsitektur Berbasis Peristiwa untuk aplikasi Rails Anda. Ini adalah perpustakaan modular yang dibangun dengan komponen kecil seperti repositori, mapper, dispatcher, scheduler, subscriptions, dan serializer. Setiap komponen dapat memiliki implementasi yang dapat dipertukarkan.
Misalnya, repositori default menggunakan ActiveRecord dan mengasumsikan tata letak tabel tertentu untuk menyimpan acara. Namun, implementasi Anda dapat menggunakan ROM atau bekerja dalam memori tanpa menyimpan peristiwa, yang berguna untuk pengujian.
Tetapi bagaimana Anda bisa tahu jika komponen yang Anda implementasikan berperilaku seperti yang diharapkan perpustakaan? Dengan menggunakan linter yang disediakan tentunya. Dan itu sangat besar. Ini mencakup sekitar 80 kasus. Beberapa di antaranya relatif sederhana:
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 endDan beberapa sedikit lebih kompleks dan berhubungan dengan jalan yang tidak bahagia:
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) endPada hampir 1.400 baris kode Ruby, saya yakin ini adalah linter terbesar yang ditulis dalam Ruby. Tetapi jika Anda mengetahui yang lebih besar, beri tahu saya. Bagian yang menarik adalah 100% tentang semantik.
Ini sangat menguji antarmuka juga, tetapi saya akan mengatakan itu adalah renungan mengingat ruang lingkup artikel ini.
Penerapan
Linter repositori diimplementasikan menggunakan fungsionalitas Contoh Bersama 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 # ...Penggunaan
Linter ini, mirip dengan yang lain, mengharapkan Anda untuk menyediakan beberapa metode, yang paling penting repository , yang mengembalikan implementasi untuk diverifikasi. Contoh pengujian disertakan menggunakan metode include_examples RSpec bawaan:
RSpec.describe EventRepository do include_examples :event_repository let(:repository) { EventRepository.new(serializer: YAML) } endMembungkus
Seperti yang Anda lihat, " linter" memiliki arti yang sedikit lebih luas daripada yang biasanya kita pikirkan. Setiap kali Anda menerapkan perpustakaan yang mengharapkan beberapa kolaborator yang dapat dipertukarkan, saya mendorong Anda untuk mempertimbangkan untuk menyediakan linter.
Bahkan jika satu-satunya kelas yang lulus tes seperti itu pada awalnya adalah kelas yang juga disediakan oleh perpustakaan Anda, itu pertanda bahwa Anda sebagai insinyur perangkat lunak menganggap serius ekstensibilitas. Ini juga akan menantang Anda untuk memikirkan antarmuka untuk setiap komponen dalam kode Anda, tidak secara tidak sengaja tetapi secara sadar.
Sumber daya
- ActiveModel: Jadikan Objek Ruby Seperti ActiveRecord
- RailsCast Tentang Model Aktif
- Implementasi Rak Penuh Lint
- Penggunaan Rack Lint di Puma
- Pelajari Lebih Lanjut Tentang Toko Acara Rails
