Module: IceJade::Transport

Included in:
Getter::Client, Poster::Client
Defined in:
lib/ice_jade/transport.rb

Overview

HTTP 传输层共享逻辑

Poster::Client 与 Getter::Client 共用的底层能力: base_url 拼接、默认头合并、Net::HTTP 执行、超时指数退避重试、 状态码/JSON 解析/Response 包装。

设计为 instance 方法模块,由两个 Client 类 include 后复用, 各自只关心自己负责的 HTTP 动词与参数构造,传输细节由 Transport 统一。

Constant Summary collapse

HTTP_STATUS_TEXT =

内置 HTTP 状态码文本(兼容各 Ruby 版本)

{
  100 => 'Continue', 101 => 'Switching Protocols',
  200 => 'OK', 201 => 'Created', 202 => 'Accepted',
  204 => 'No Content',
  301 => 'Moved Permanently', 302 => 'Found', 304 => 'Not Modified',
  400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden',
  404 => 'Not Found', 405 => 'Method Not Allowed',
  429 => 'Too Many Requests',
  500 => 'Internal Server Error', 502 => 'Bad Gateway',
  503 => 'Service Unavailable'
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#base_urlObject (readonly)

Returns the value of attribute base_url.



35
36
37
# File 'lib/ice_jade/transport.rb', line 35

def base_url
  @base_url
end

#default_headersObject (readonly)

Returns the value of attribute default_headers.



35
36
37
# File 'lib/ice_jade/transport.rb', line 35

def default_headers
  @default_headers
end

#max_retriesObject (readonly)

Returns the value of attribute max_retries.



35
36
37
# File 'lib/ice_jade/transport.rb', line 35

def max_retries
  @max_retries
end

#open_timeoutObject (readonly)

Returns the value of attribute open_timeout.



35
36
37
# File 'lib/ice_jade/transport.rb', line 35

def open_timeout
  @open_timeout
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



35
36
37
# File 'lib/ice_jade/transport.rb', line 35

def timeout
  @timeout
end

Instance Method Details

#adapt_response(response) ⇒ Hash

将 Net::HTTP 响应适配为 IceJade::Response 所需的 Hash

Parameters:

  • response (Net::HTTPResponse)

Returns:

  • (Hash)

    形如 { 'ok' => bool, 'code' => int, 'message' => str, 'data' => data }



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/ice_jade/transport.rb', line 113

def adapt_response(response)
  body = response.body.to_s
  code = response.code.to_i

  data = begin
           JSON.parse(body)
         rescue JSON::ParserError
           body
         end

  {
    'ok' => code >= 200 && code < 300,
    'code' => code,
    'message' => HTTP_STATUS_TEXT[code] || 'Unknown',
    'data' => data
  }
end

#apply_headers(req, headers) ⇒ Object

将头 Hash 写入 Net::HTTP 请求对象



50
51
52
53
54
# File 'lib/ice_jade/transport.rb', line 50

def apply_headers(req, headers)
  headers.each do |k, v|
    req[k.to_s] = v.to_s
  end
end

#build_multipart(params) ⇒ Object

multipart 边界生成与 MIME 推断 —— POST 专用,Getter 不使用, 但为保持 Transport 的自包含性一并放在这里,供 Poster::Client 复用。



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/ice_jade/transport.rb', line 133

def build_multipart(params)
  boundary = "----IceJadeTransportBoundary#{SecureRandom.hex(16)}"
  body = +"".b

  params.each do |key, value|
    if value.is_a?(String) && File.exist?(value)
      filename = File.basename(value)
      file_data = File.binread(value)
      mime = mime_type(filename)
      body << "--#{boundary}\r\n"
      body << %(Content-Disposition: form-data; name="#{key}"; filename="#{filename}") << "\r\n"
      body << "Content-Type: #{mime}" << "\r\n\r\n"
      body << file_data.b
      body << "\r\n"
    else
      body << "--#{boundary}\r\n"
      body << %(Content-Disposition: form-data; name="#{key}") << "\r\n\r\n"
      body << value.to_s
      body << "\r\n"
    end
  end

  body << "--#{boundary}--\r\n"
  [body, boundary]
end

#build_uri(url) ⇒ URI::Generic

拼接 base_url 与相对路径,或直接解析完整 URL

Parameters:

  • url (String)

    完整 URL 或相对路径(需先设置 base_url)

Returns:

  • (URI::Generic)


41
42
43
44
45
46
47
# File 'lib/ice_jade/transport.rb', line 41

def build_uri(url)
  if base_url && !url.to_s.start_with?('http')
    URI.parse("#{base_url}#{url}")
  else
    URI.parse(url.to_s)
  end
end

#execute(uri, request) ⇒ IceJade::Response

执行 Net::HTTP 请求,超时指数退避重试,异常包装为 Response

Parameters:

  • uri (URI::Generic)
  • request (Net::HTTPGenericRequest)

Returns:



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/ice_jade/transport.rb', line 71

def execute(uri, request)
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == 'https'
  http.open_timeout = open_timeout
  http.read_timeout = timeout

  retries = 0
  begin
    response = http.request(request)
    Response.new(adapt_response(response))
  rescue Net::OpenTimeout, Net::ReadTimeout => e
    retries += 1
    if retries <= max_retries
      sleep(2**retries)
      retry
    end
    Response.new({ 'ok' => false, 'code' => 0,
                   'message' => "Timeout after #{max_retries} retries: #{e.message}", 'data' => nil })
  rescue StandardError => e
    Response.new({ 'ok' => false, 'code' => 0, 'message' => e.message, 'data' => nil })
  ensure
    http.finish if http.started?
  end
end

#init_transport(base_url:, headers:, timeout:, open_timeout:, max_retries:) ⇒ Object

公共初始化参数解析,供两个 Client 的 #initialize 调用

Parameters:

  • base_url (String, nil)

    基础 URL,后续请求可传相对路径

  • headers (Hash)

    默认请求头(每次请求自动合并,同名会被覆盖)

  • timeout (Integer)

    读取超时(秒)

  • open_timeout (Integer)

    连接超时(秒)

  • max_retries (Integer)

    超时后最大重试次数



27
28
29
30
31
32
33
# File 'lib/ice_jade/transport.rb', line 27

def init_transport(base_url:, headers:, timeout:, open_timeout:, max_retries:)
  @base_url = base_url ? base_url.to_s.chomp('/') : nil
  @default_headers = normalize_headers(headers)
  @timeout = timeout
  @open_timeout = open_timeout
  @max_retries = max_retries
end

#merge_headers(base, extra) ⇒ Object

默认头与额外头合并(额外同名覆盖)



62
63
64
# File 'lib/ice_jade/transport.rb', line 62

def merge_headers(base, extra)
  base.merge(extra)
end

#mime_type(filename) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/ice_jade/transport.rb', line 159

def mime_type(filename)
  ext = File.extname(filename).downcase
  case ext
  when '.png'            then 'image/png'
  when '.jpg', '.jpeg'   then 'image/jpeg'
  when '.gif'            then 'image/gif'
  when '.webp'           then 'image/webp'
  when '.bmp'            then 'image/bmp'
  when '.pdf'            then 'application/pdf'
  when '.txt'            then 'text/plain'
  when '.json'           then 'application/json'
  when '.zip'            then 'application/zip'
  when '.doc'            then 'application/msword'
  when '.docx'           then 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
  when '.xls'            then 'application/vnd.ms-excel'
  when '.xlsx'           then 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  else                        'application/octet-stream'
  end
end

#normalize_headers(h) ⇒ Object

头键值统一字符串化



57
58
59
# File 'lib/ice_jade/transport.rb', line 57

def normalize_headers(h)
  h.transform_keys(&:to_s).transform_values(&:to_s)
end