lib/llhttp/parser.rb
# frozen_string_literal: true module LLHttp # [public] Wraps an llhttp context for parsing http requests and responses. # # class Delegate < LLHttp::Delegate # def on_message_begin # ... # end # # ... # end # # parser = LLHttp::Parser.new(Delegate.new, type: :request) # parser << "GET / HTTP/1.1\r\n\r\n" # parser.finish # # ... # # Introspection # # * `LLHttp::Parser#content_length` returns the content length of the current request. # * `LLHttp::Parser#method_name` returns the method name of the current response. # * `LLHttp::Parser#status_code` returns the status code of the current response. # * `LLHttp::Parser#http_major` returns the major http version of the current request/response. # * `LLHttp::Parser#http_minor` returns the minor http version of the current request/response. # * `LLHttp::Parser#keep_alive?` returns `true` if there might be more messages. # # Finishing # # Call `LLHttp::Parser#finish` when processing is complete for the current request or response. # class Parser LLHTTP_TYPES = {both: 0, request: 1, response: 2}.freeze CALLBACKS = %i[ on_message_begin on_headers_complete on_message_complete on_chunk_header on_chunk_complete on_url_complete on_status_complete on_header_field_complete on_header_value_complete ].freeze CALLBACKS_WITH_DATA = %i[ on_url on_status on_header_field on_header_value on_body ].freeze # [public] The parser type; one of: `:both`, `:request`, or `:response`. # attr_reader :type def initialize(delegate, type: :both) @type, @delegate = type.to_sym, delegate @callbacks = Callbacks.new (CALLBACKS + CALLBACKS_WITH_DATA).each do |callback| if delegate.respond_to?(callback) @callbacks[callback] = method(callback).to_proc end end @pointer = LLHttp.rb_llhttp_init(LLHTTP_TYPES.fetch(@type), @callbacks) ObjectSpace.define_finalizer(self, self.class.free(@pointer)) end # [public] Parse the given data. # def parse(data) errno = LLHttp.llhttp_execute(@pointer, data, data.length) raise build_error(errno) if errno > 0 end alias_method :<<, :parse # [public] Get the content length of the current request. # def content_length LLHttp.rb_llhttp_content_length(@pointer) end # [public] Get the method of the current response. # def method_name LLHttp.rb_llhttp_method_name(@pointer) end # [public] Get the status code of the current response. # def status_code LLHttp.rb_llhttp_status_code(@pointer) end # [public] Get the major http version of the current request/response. # def http_major LLHttp.rb_llhttp_http_major(@pointer) end # [public] Get the minor http version of the current request/response. # def http_minor LLHttp.rb_llhttp_http_minor(@pointer) end # [public] Returns `true` if there might be more messages. # def keep_alive? LLHttp.llhttp_should_keep_alive(@pointer) == 1 end # [public] Tells the parser we are finished. # def finish LLHttp.llhttp_finish(@pointer) end # [public] Get ready to parse the next request/response. # def reset LLHttp.llhttp_reset(@pointer) end CALLBACKS.each do |callback| class_eval( <<~RB, __FILE__, __LINE__ + 1 private def #{callback} @delegate.#{callback} 0 rescue -1 end RB ) end CALLBACKS_WITH_DATA.each do |callback| class_eval( <<~RB, __FILE__, __LINE__ + 1 private def #{callback}(buffer, length) @delegate.#{callback}(buffer.get_bytes(0, length)) end RB ) end private def build_error(errno) Error.new("Error Parsing data: #{LLHttp.llhttp_errno_name(errno)} #{LLHttp.llhttp_get_error_reason(@pointer)}") end def self.free(pointer) proc { LLHttp.rb_llhttp_free(pointer) } end end end