Ruby 라이브러리에 의해 구현된 린터

게시 됨: 2022-03-11

" linter " 또는 " lint "라는 단어를 들었을 때 이미 그러한 도구가 어떻게 작동하는지 또는 무엇을 해야 하는지에 대한 특정 기대치를 가지고 있을 것입니다.

한 Toptal 개발자가 유지 관리하는 Rubocop이나 JSLint, ESLint 또는 덜 알려져 있거나 덜 인기 있는 것을 생각할 수 있습니다.

이 기사에서는 다양한 종류의 린터를 소개합니다. 그들은 코드 구문을 확인하거나 Abstract-Syntax-Tree를 확인하지 않지만 코드를 확인합니다. 그들은 구현이 어휘적으로(덕 타이핑 및 클래식 인터페이스 측면에서)뿐만 아니라 때로는 의미적으로도 특정 인터페이스를 준수하는지 확인합니다.

그것들에 익숙해지기 위해 몇 가지 실용적인 예를 분석해 봅시다. 열렬한 Rails 전문가가 아니라면 이 글을 먼저 읽어보세요.

기본 Lint부터 시작하겠습니다.

ActiveModel::Lint::테스트

이 Lint의 동작은 공식 Rails 문서에 자세히 설명되어 있습니다.

TestCaseActiveModel::Lint::Tests 를 포함하여 개체가 Active Model API와 호환되는지 여부를 테스트할 수 있습니다. 여기에는 개체가 완전히 호환되는지 여부 또는 그렇지 않은 경우 구현되지 않은 API 측면을 알려주는 테스트가 포함됩니다. Action Pack과 함께 작동하기 위해 모든 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 end

ActiveModel::Serializer::Lint::테스트

활성 모델 직렬 변환기는 새로운 것은 아니지만 계속 배울 수 있습니다. ActiveModel::Serializer::Lint::Tests 를 포함하여 개체가 Active Model Serializers API와 호환되는지 확인합니다. 그렇지 않은 경우 테스트에서 누락된 부품이 표시됩니다.

그러나 문서에서 의미 체계를 확인하지 않는다는 중요한 경고를 찾을 수 있습니다.

“이러한 테스트는 반환된 값의 의미론적 정확성을 결정하려고 시도하지 않습니다. 예를 들어, 항상 {} 를 반환하도록 serializable_hash 를 구현할 수 있으며 테스트는 통과합니다. 값이 의미적으로 의미가 있는지 확인하는 것은 사용자에게 달려 있습니다.”

즉, 인터페이스의 모양만 확인하고 있습니다. 이제 구현 방법을 살펴보겠습니다.

구현

이것은 방금 전에 ActiveModel::Lint::Tests 구현에서 본 것과 매우 유사하지만 반환된 값의 arity 또는 클래스를 확인하기 때문에 어떤 경우에는 좀 더 엄격합니다.

 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를 사용하는 방법의 예입니다.

 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 미들웨어입니다. 이 경우 미들웨어는 린터 역할을 합니다. Linter는 요청 및 응답이 Rack 사양에 따라 구성되었는지 확인합니다. 이는 랙 애플리케이션을 제공할 랙 서버(예: Puma)를 구현하고 랙 사양을 따르도록 하려는 경우에 유용합니다.

또는 매우 단순한 응용 프로그램을 구현하고 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 ...

앱에서의 사용

매우 간단한 엔드포인트를 구축한다고 가정해 보겠습니다. 때때로 "No Content"로 응답해야 하지만 우리는 고의적인 실수를 했으며 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 - 저장소 린트

Rails Event Store는 이벤트를 게시, 소비, 저장 및 검색하기 위한 라이브러리입니다. Rails 애플리케이션을 위한 Event-Driven Architecture 구현을 돕는 것을 목표로 합니다. 저장소, 매퍼, 디스패처, 스케줄러, 구독 및 직렬 변환기와 같은 작은 구성 요소로 구축된 모듈식 라이브러리입니다. 각 구성 요소는 상호 교환 가능한 구현을 가질 수 있습니다.

예를 들어, 기본 리포지토리는 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 Shared Examples 기능을 사용하여 구현됩니다.

 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

마무리

보시다시피 " linter" 는 우리가 일반적으로 생각하는 것보다 약간 더 넓은 의미를 가지고 있습니다. 상호 교환 가능한 공동 작업자가 필요한 라이브러리를 구현할 때마다 린터 제공을 고려하는 것이 좋습니다.

처음에 이러한 테스트를 통과하는 유일한 클래스가 라이브러리에서도 제공되는 클래스일지라도 소프트웨어 엔지니어로서 확장성을 진지하게 받아들이고 있다는 신호입니다. 또한 실수가 아니라 의식적으로 코드의 각 구성 요소에 대한 인터페이스에 대해 생각해야 합니다.

자원

  • ActiveModel: 모든 Ruby 개체가 ActiveRecord처럼 느껴지도록 만들기
  • RailsCast 활성 모델 정보
  • 전체 랙 린트 구현
  • Puma의 랙 린트 사용
  • Rails 이벤트 스토어에 대해 자세히 알아보기