ice-jade 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +104 -11
- data/config/cradle/poster_test.yml +41 -0
- data/examples/comparison_getter.rb +193 -0
- data/examples/comparison_poster.rb +195 -0
- data/examples/cradle_usage.rb +92 -0
- data/examples/getter_usage.rb +85 -0
- data/examples/poster_usage.rb +89 -0
- data/examples/quantum_usage.rb +113 -0
- data/lib/http_getter.rb +159 -0
- data/lib/ice_jade/getter/client.rb +78 -0
- data/lib/ice_jade/getter.rb +3 -0
- data/lib/ice_jade/poster/client.rb +7 -136
- data/lib/ice_jade/transport.rb +179 -0
- data/lib/ice_jade/version.rb +1 -1
- data/lib/ice_jade.rb +1 -0
- metadata +16 -4
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
|
-
require 'securerandom'
|
|
7
|
-
require_relative '../response'
|
|
8
|
-
require_relative '../error'
|
|
3
|
+
require_relative '../transport'
|
|
9
4
|
|
|
10
5
|
module IceJade
|
|
11
6
|
module Poster
|
|
@@ -13,6 +8,7 @@ module IceJade
|
|
|
13
8
|
#
|
|
14
9
|
# 零外部依赖,支持 JSON / Form / Multipart 三种常用提交方式,
|
|
15
10
|
# 内置超时控制、指数退避重试、统一 Response 包装,与 Quantum 模块风格一致。
|
|
11
|
+
# 底层传输能力(URI 拼接、请求执行、响应适配、超时重试)复用 IceJade::Transport,
|
|
16
12
|
#
|
|
17
13
|
# @example 基础用法
|
|
18
14
|
# poster = IceJade::Poster::Client.new(
|
|
@@ -24,7 +20,7 @@ module IceJade
|
|
|
24
20
|
# puts resp.data if resp.success?
|
|
25
21
|
#
|
|
26
22
|
class Client
|
|
27
|
-
|
|
23
|
+
include IceJade::Transport
|
|
28
24
|
|
|
29
25
|
# @param base_url [String, nil] 基础 URL,后续请求可传相对路径
|
|
30
26
|
# @param headers [Hash] 默认请求头(每次请求自动合并,同名会被覆盖)
|
|
@@ -32,11 +28,9 @@ module IceJade
|
|
|
32
28
|
# @param open_timeout [Integer] 连接超时(秒),默认 10
|
|
33
29
|
# @param max_retries [Integer] 超时后最大重试次数,默认 2
|
|
34
30
|
def initialize(base_url: nil, headers: {}, timeout: 60, open_timeout: 10, max_retries: 2)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@open_timeout = open_timeout
|
|
39
|
-
@max_retries = max_retries
|
|
31
|
+
init_transport(base_url: base_url, headers: headers,
|
|
32
|
+
timeout: timeout, open_timeout: open_timeout,
|
|
33
|
+
max_retries: max_retries)
|
|
40
34
|
end
|
|
41
35
|
|
|
42
36
|
# ------------------------------------------------------------------
|
|
@@ -101,14 +95,7 @@ module IceJade
|
|
|
101
95
|
|
|
102
96
|
private
|
|
103
97
|
|
|
104
|
-
|
|
105
|
-
if base_url && !url.to_s.start_with?('http')
|
|
106
|
-
URI.parse("#{base_url}#{url}")
|
|
107
|
-
else
|
|
108
|
-
URI.parse(url.to_s)
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
98
|
+
# 构造 POST 请求对象(JSON / Form / 自定义 Content-Type 三种序列化分支)
|
|
112
99
|
def build_request(uri, body, content_type, headers)
|
|
113
100
|
req = Net::HTTP::Post.new(uri)
|
|
114
101
|
apply_headers(req, headers)
|
|
@@ -127,122 +114,6 @@ module IceJade
|
|
|
127
114
|
|
|
128
115
|
req
|
|
129
116
|
end
|
|
130
|
-
|
|
131
|
-
def apply_headers(req, headers)
|
|
132
|
-
headers.each do |k, v|
|
|
133
|
-
req[k.to_s] = v.to_s
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def normalize_headers(h)
|
|
138
|
-
h.transform_keys(&:to_s).transform_values(&:to_s)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def merge_headers(base, extra)
|
|
142
|
-
base.merge(extra)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def execute(uri, request)
|
|
146
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
147
|
-
http.use_ssl = uri.scheme == 'https'
|
|
148
|
-
http.open_timeout = open_timeout
|
|
149
|
-
http.read_timeout = timeout
|
|
150
|
-
|
|
151
|
-
retries = 0
|
|
152
|
-
begin
|
|
153
|
-
response = http.request(request)
|
|
154
|
-
Response.new(adapt_response(response))
|
|
155
|
-
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
156
|
-
retries += 1
|
|
157
|
-
if retries <= max_retries
|
|
158
|
-
sleep(2**retries)
|
|
159
|
-
retry
|
|
160
|
-
end
|
|
161
|
-
Response.new({ 'ok' => false, 'code' => 0,
|
|
162
|
-
'message' => "Timeout after #{max_retries} retries: #{e.message}", 'data' => nil })
|
|
163
|
-
rescue StandardError => e
|
|
164
|
-
Response.new({ 'ok' => false, 'code' => 0, 'message' => e.message, 'data' => nil })
|
|
165
|
-
ensure
|
|
166
|
-
http.finish if http.started?
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# 内置 HTTP 状态码文本(兼容各 Ruby 版本)
|
|
171
|
-
HTTP_STATUS_TEXT = {
|
|
172
|
-
100 => 'Continue', 101 => 'Switching Protocols',
|
|
173
|
-
200 => 'OK', 201 => 'Created', 202 => 'Accepted',
|
|
174
|
-
204 => 'No Content',
|
|
175
|
-
301 => 'Moved Permanently', 302 => 'Found', 304 => 'Not Modified',
|
|
176
|
-
400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden',
|
|
177
|
-
404 => 'Not Found', 405 => 'Method Not Allowed',
|
|
178
|
-
429 => 'Too Many Requests',
|
|
179
|
-
500 => 'Internal Server Error', 502 => 'Bad Gateway',
|
|
180
|
-
503 => 'Service Unavailable'
|
|
181
|
-
}.freeze
|
|
182
|
-
|
|
183
|
-
def adapt_response(response)
|
|
184
|
-
body = response.body.to_s
|
|
185
|
-
code = response.code.to_i
|
|
186
|
-
|
|
187
|
-
data = begin
|
|
188
|
-
JSON.parse(body)
|
|
189
|
-
rescue JSON::ParserError
|
|
190
|
-
body
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
{
|
|
194
|
-
'ok' => code >= 200 && code < 300,
|
|
195
|
-
'code' => code,
|
|
196
|
-
'message' => HTTP_STATUS_TEXT[code] || 'Unknown',
|
|
197
|
-
'data' => data
|
|
198
|
-
}
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def build_multipart(params)
|
|
202
|
-
boundary = "----IceJadePosterBoundary#{SecureRandom.hex(16)}"
|
|
203
|
-
body = +"".b
|
|
204
|
-
|
|
205
|
-
params.each do |key, value|
|
|
206
|
-
if value.is_a?(String) && File.exist?(value)
|
|
207
|
-
filename = File.basename(value)
|
|
208
|
-
file_data = File.binread(value)
|
|
209
|
-
mime = mime_type(filename)
|
|
210
|
-
body << "--#{boundary}\r\n"
|
|
211
|
-
body << %(Content-Disposition: form-data; name="#{key}"; filename="#{filename}") << "\r\n"
|
|
212
|
-
body << "Content-Type: #{mime}" << "\r\n\r\n"
|
|
213
|
-
body << file_data.b
|
|
214
|
-
body << "\r\n"
|
|
215
|
-
else
|
|
216
|
-
body << "--#{boundary}\r\n"
|
|
217
|
-
body << %(Content-Disposition: form-data; name="#{key}") << "\r\n\r\n"
|
|
218
|
-
body << value.to_s
|
|
219
|
-
body << "\r\n"
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
body << "--#{boundary}--\r\n"
|
|
224
|
-
[body, boundary]
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def mime_type(filename)
|
|
228
|
-
ext = File.extname(filename).downcase
|
|
229
|
-
case ext
|
|
230
|
-
when '.png' then 'image/png'
|
|
231
|
-
when '.jpg', '.jpeg' then 'image/jpeg'
|
|
232
|
-
when '.gif' then 'image/gif'
|
|
233
|
-
when '.webp' then 'image/webp'
|
|
234
|
-
when '.bmp' then 'image/bmp'
|
|
235
|
-
when '.pdf' then 'application/pdf'
|
|
236
|
-
when '.txt' then 'text/plain'
|
|
237
|
-
when '.json' then 'application/json'
|
|
238
|
-
when '.zip' then 'application/zip'
|
|
239
|
-
when '.doc' then 'application/msword'
|
|
240
|
-
when '.docx' then 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
241
|
-
when '.xls' then 'application/vnd.ms-excel'
|
|
242
|
-
when '.xlsx' then 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
243
|
-
else 'application/octet-stream'
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
117
|
end
|
|
247
118
|
end
|
|
248
119
|
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
require_relative 'response'
|
|
8
|
+
require_relative 'error'
|
|
9
|
+
|
|
10
|
+
module IceJade
|
|
11
|
+
# HTTP 传输层共享逻辑
|
|
12
|
+
#
|
|
13
|
+
# Poster::Client 与 Getter::Client 共用的底层能力:
|
|
14
|
+
# base_url 拼接、默认头合并、Net::HTTP 执行、超时指数退避重试、
|
|
15
|
+
# 状态码/JSON 解析/Response 包装。
|
|
16
|
+
#
|
|
17
|
+
# 设计为 instance 方法模块,由两个 Client 类 include 后复用,
|
|
18
|
+
# 各自只关心自己负责的 HTTP 动词与参数构造,传输细节由 Transport 统一。
|
|
19
|
+
module Transport
|
|
20
|
+
# 公共初始化参数解析,供两个 Client 的 #initialize 调用
|
|
21
|
+
#
|
|
22
|
+
# @param base_url [String, nil] 基础 URL,后续请求可传相对路径
|
|
23
|
+
# @param headers [Hash] 默认请求头(每次请求自动合并,同名会被覆盖)
|
|
24
|
+
# @param timeout [Integer] 读取超时(秒)
|
|
25
|
+
# @param open_timeout [Integer] 连接超时(秒)
|
|
26
|
+
# @param max_retries [Integer] 超时后最大重试次数
|
|
27
|
+
def init_transport(base_url:, headers:, timeout:, open_timeout:, max_retries:)
|
|
28
|
+
@base_url = base_url ? base_url.to_s.chomp('/') : nil
|
|
29
|
+
@default_headers = normalize_headers(headers)
|
|
30
|
+
@timeout = timeout
|
|
31
|
+
@open_timeout = open_timeout
|
|
32
|
+
@max_retries = max_retries
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :base_url, :default_headers, :timeout, :open_timeout, :max_retries
|
|
36
|
+
|
|
37
|
+
# 拼接 base_url 与相对路径,或直接解析完整 URL
|
|
38
|
+
#
|
|
39
|
+
# @param url [String] 完整 URL 或相对路径(需先设置 base_url)
|
|
40
|
+
# @return [URI::Generic]
|
|
41
|
+
def build_uri(url)
|
|
42
|
+
if base_url && !url.to_s.start_with?('http')
|
|
43
|
+
URI.parse("#{base_url}#{url}")
|
|
44
|
+
else
|
|
45
|
+
URI.parse(url.to_s)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 将头 Hash 写入 Net::HTTP 请求对象
|
|
50
|
+
def apply_headers(req, headers)
|
|
51
|
+
headers.each do |k, v|
|
|
52
|
+
req[k.to_s] = v.to_s
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 头键值统一字符串化
|
|
57
|
+
def normalize_headers(h)
|
|
58
|
+
h.transform_keys(&:to_s).transform_values(&:to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 默认头与额外头合并(额外同名覆盖)
|
|
62
|
+
def merge_headers(base, extra)
|
|
63
|
+
base.merge(extra)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 执行 Net::HTTP 请求,超时指数退避重试,异常包装为 Response
|
|
67
|
+
#
|
|
68
|
+
# @param uri [URI::Generic]
|
|
69
|
+
# @param request [Net::HTTPGenericRequest]
|
|
70
|
+
# @return [IceJade::Response]
|
|
71
|
+
def execute(uri, request)
|
|
72
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
73
|
+
http.use_ssl = uri.scheme == 'https'
|
|
74
|
+
http.open_timeout = open_timeout
|
|
75
|
+
http.read_timeout = timeout
|
|
76
|
+
|
|
77
|
+
retries = 0
|
|
78
|
+
begin
|
|
79
|
+
response = http.request(request)
|
|
80
|
+
Response.new(adapt_response(response))
|
|
81
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
82
|
+
retries += 1
|
|
83
|
+
if retries <= max_retries
|
|
84
|
+
sleep(2**retries)
|
|
85
|
+
retry
|
|
86
|
+
end
|
|
87
|
+
Response.new({ 'ok' => false, 'code' => 0,
|
|
88
|
+
'message' => "Timeout after #{max_retries} retries: #{e.message}", 'data' => nil })
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
Response.new({ 'ok' => false, 'code' => 0, 'message' => e.message, 'data' => nil })
|
|
91
|
+
ensure
|
|
92
|
+
http.finish if http.started?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# 内置 HTTP 状态码文本(兼容各 Ruby 版本)
|
|
97
|
+
HTTP_STATUS_TEXT = {
|
|
98
|
+
100 => 'Continue', 101 => 'Switching Protocols',
|
|
99
|
+
200 => 'OK', 201 => 'Created', 202 => 'Accepted',
|
|
100
|
+
204 => 'No Content',
|
|
101
|
+
301 => 'Moved Permanently', 302 => 'Found', 304 => 'Not Modified',
|
|
102
|
+
400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden',
|
|
103
|
+
404 => 'Not Found', 405 => 'Method Not Allowed',
|
|
104
|
+
429 => 'Too Many Requests',
|
|
105
|
+
500 => 'Internal Server Error', 502 => 'Bad Gateway',
|
|
106
|
+
503 => 'Service Unavailable'
|
|
107
|
+
}.freeze
|
|
108
|
+
|
|
109
|
+
# 将 Net::HTTP 响应适配为 IceJade::Response 所需的 Hash
|
|
110
|
+
#
|
|
111
|
+
# @param response [Net::HTTPResponse]
|
|
112
|
+
# @return [Hash] 形如 { 'ok' => bool, 'code' => int, 'message' => str, 'data' => data }
|
|
113
|
+
def adapt_response(response)
|
|
114
|
+
body = response.body.to_s
|
|
115
|
+
code = response.code.to_i
|
|
116
|
+
|
|
117
|
+
data = begin
|
|
118
|
+
JSON.parse(body)
|
|
119
|
+
rescue JSON::ParserError
|
|
120
|
+
body
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
'ok' => code >= 200 && code < 300,
|
|
125
|
+
'code' => code,
|
|
126
|
+
'message' => HTTP_STATUS_TEXT[code] || 'Unknown',
|
|
127
|
+
'data' => data
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# multipart 边界生成与 MIME 推断 —— POST 专用,Getter 不使用,
|
|
132
|
+
# 但为保持 Transport 的自包含性一并放在这里,供 Poster::Client 复用。
|
|
133
|
+
def build_multipart(params)
|
|
134
|
+
boundary = "----IceJadeTransportBoundary#{SecureRandom.hex(16)}"
|
|
135
|
+
body = +"".b
|
|
136
|
+
|
|
137
|
+
params.each do |key, value|
|
|
138
|
+
if value.is_a?(String) && File.exist?(value)
|
|
139
|
+
filename = File.basename(value)
|
|
140
|
+
file_data = File.binread(value)
|
|
141
|
+
mime = mime_type(filename)
|
|
142
|
+
body << "--#{boundary}\r\n"
|
|
143
|
+
body << %(Content-Disposition: form-data; name="#{key}"; filename="#{filename}") << "\r\n"
|
|
144
|
+
body << "Content-Type: #{mime}" << "\r\n\r\n"
|
|
145
|
+
body << file_data.b
|
|
146
|
+
body << "\r\n"
|
|
147
|
+
else
|
|
148
|
+
body << "--#{boundary}\r\n"
|
|
149
|
+
body << %(Content-Disposition: form-data; name="#{key}") << "\r\n\r\n"
|
|
150
|
+
body << value.to_s
|
|
151
|
+
body << "\r\n"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
body << "--#{boundary}--\r\n"
|
|
156
|
+
[body, boundary]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def mime_type(filename)
|
|
160
|
+
ext = File.extname(filename).downcase
|
|
161
|
+
case ext
|
|
162
|
+
when '.png' then 'image/png'
|
|
163
|
+
when '.jpg', '.jpeg' then 'image/jpeg'
|
|
164
|
+
when '.gif' then 'image/gif'
|
|
165
|
+
when '.webp' then 'image/webp'
|
|
166
|
+
when '.bmp' then 'image/bmp'
|
|
167
|
+
when '.pdf' then 'application/pdf'
|
|
168
|
+
when '.txt' then 'text/plain'
|
|
169
|
+
when '.json' then 'application/json'
|
|
170
|
+
when '.zip' then 'application/zip'
|
|
171
|
+
when '.doc' then 'application/msword'
|
|
172
|
+
when '.docx' then 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
173
|
+
when '.xls' then 'application/vnd.ms-excel'
|
|
174
|
+
when '.xlsx' then 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
175
|
+
else 'application/octet-stream'
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
data/lib/ice_jade/version.rb
CHANGED
data/lib/ice_jade.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ice-jade
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Frampt
|
|
@@ -9,8 +9,9 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: A lightweight Ruby SDK featuring Quantum (IM robot messaging)
|
|
13
|
-
(zero-dependency HTTP POST
|
|
12
|
+
description: A lightweight Ruby SDK featuring Quantum (IM robot messaging), Getter
|
|
13
|
+
& Poster (zero-dependency HTTP GET & POST clients with JSON/Form/Multipart support
|
|
14
|
+
and sharing a common IceJade::Transport layer).
|
|
14
15
|
email:
|
|
15
16
|
- 18995691365@189.cn
|
|
16
17
|
executables:
|
|
@@ -23,6 +24,14 @@ files:
|
|
|
23
24
|
- README.md
|
|
24
25
|
- bin/cradle
|
|
25
26
|
- bin/cradle-instance
|
|
27
|
+
- config/cradle/poster_test.yml
|
|
28
|
+
- examples/comparison_getter.rb
|
|
29
|
+
- examples/comparison_poster.rb
|
|
30
|
+
- examples/cradle_usage.rb
|
|
31
|
+
- examples/getter_usage.rb
|
|
32
|
+
- examples/poster_usage.rb
|
|
33
|
+
- examples/quantum_usage.rb
|
|
34
|
+
- lib/http_getter.rb
|
|
26
35
|
- lib/http_poster.rb
|
|
27
36
|
- lib/ice_jade.rb
|
|
28
37
|
- lib/ice_jade/client_base.rb
|
|
@@ -30,11 +39,14 @@ files:
|
|
|
30
39
|
- lib/ice_jade/cradle/server.rb
|
|
31
40
|
- lib/ice_jade/cradle/yaml_server.rb
|
|
32
41
|
- lib/ice_jade/error.rb
|
|
42
|
+
- lib/ice_jade/getter.rb
|
|
43
|
+
- lib/ice_jade/getter/client.rb
|
|
33
44
|
- lib/ice_jade/poster.rb
|
|
34
45
|
- lib/ice_jade/poster/client.rb
|
|
35
46
|
- lib/ice_jade/quantum/client.rb
|
|
36
47
|
- lib/ice_jade/quantum/message.rb
|
|
37
48
|
- lib/ice_jade/response.rb
|
|
49
|
+
- lib/ice_jade/transport.rb
|
|
38
50
|
- lib/ice_jade/version.rb
|
|
39
51
|
homepage: https://www.srdcloud.cn/codehub/Frampt/ice-jade
|
|
40
52
|
licenses:
|
|
@@ -56,5 +68,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
56
68
|
requirements: []
|
|
57
69
|
rubygems_version: 4.0.10
|
|
58
70
|
specification_version: 4
|
|
59
|
-
summary: IM webhook SDK & general-purpose HTTP POST client
|
|
71
|
+
summary: IM webhook SDK & general-purpose HTTP POST/GET client
|
|
60
72
|
test_files: []
|