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.
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
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
- attr_reader :base_url, :default_headers, :timeout, :open_timeout, :max_retries
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
- @base_url = base_url ? base_url.to_s.chomp('/') : nil
36
- @default_headers = normalize_headers(headers)
37
- @timeout = timeout
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
- def build_uri(url)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IceJade
4
- VERSION = '0.5.0'
4
+ VERSION = '0.7.0'
5
5
  end
data/lib/ice_jade.rb CHANGED
@@ -10,4 +10,5 @@ require_relative 'ice_jade/client_base'
10
10
  require_relative 'ice_jade/quantum/client'
11
11
  require_relative 'ice_jade/quantum/message'
12
12
  require_relative 'ice_jade/poster'
13
+ require_relative 'ice_jade/getter'
13
14
  require_relative 'ice_jade/cradle'
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.5.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) and Poster
13
- (zero-dependency HTTP POST client with JSON/Form/Multipart support).
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: []