ice-jade 0.6.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/examples/comparison_getter.rb +193 -0
- data/examples/getter_usage.rb +85 -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 +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f57e6758c88b25a57a7891bed83b7416ae9f511e780bcde4be30bb6f211ee3b3
|
|
4
|
+
data.tar.gz: 902380ad3fb6f0bfd2bd21f171bc3aa99a9538590455ba4a41f2b582a8a0d326
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14d027172321795ad155cc0cd3bbb8f196d18db00b20c9d51cb5c990c84184633bafffd637e7d50c10c1018e70319efc9b136de35b8e1bd364d112a84d79a10a
|
|
7
|
+
data.tar.gz: e7ddcc89ef4fde4cd0ee17f9ee81d03f4c7fec1f6c8a0c5329741bbf298d77422131c40b86dadb941b93ff6256409b80366c8fd3cd9fc6bac7601601841fc7ca
|
data/README.md
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
AIGC:
|
|
3
|
+
ContentProducer: '001191110102MAD55U9H0F10002'
|
|
4
|
+
ContentPropagator: '001191110102MAD55U9H0F10002'
|
|
5
|
+
Label: '1'
|
|
6
|
+
ProduceID: '2bf760a7-7607-4806-8069-7ba5b57e6265'
|
|
7
|
+
PropagateID: '2bf760a7-7607-4806-8069-7ba5b57e6265'
|
|
8
|
+
ReservedCode1: '8dccec88-f08e-4a37-b7b8-a490337b1a1c'
|
|
9
|
+
ReservedCode2: '8dccec88-f08e-4a37-b7b8-a490337b1a1c'
|
|
10
|
+
---
|
|
11
|
+
|
|
1
12
|
# ice-jade
|
|
2
13
|
|
|
3
14
|
ice-jade 是一个基于 Ruby 标准库的轻量级工具集,零外部依赖。最初定位是 IM Webhook SDK,现已扩展为包含多个独立分支的通用 HTTP 工具包:
|
|
4
15
|
|
|
5
16
|
- **Quantum** — IM Webhook 客户端
|
|
6
17
|
- **Poster** — 通用 HTTP POST 客户端
|
|
18
|
+
- **Getter** — 通用 HTTP GET 客户端
|
|
7
19
|
- **HttpPoster** — 独立模块级 HTTP POST 工具
|
|
20
|
+
- **HttpGetter** — 独立模块级 HTTP GET 工具
|
|
8
21
|
- **Cradle** — HTTP 测试服务器
|
|
9
22
|
- **YAMLServer** — YAML 配置驱动的测试服务器实例
|
|
10
23
|
|
|
@@ -60,7 +73,7 @@ client.upload_and_send_file("/path/to/report.pdf")
|
|
|
60
73
|
|
|
61
74
|
## 分支二:Poster
|
|
62
75
|
|
|
63
|
-
`IceJade::Poster::Client` 是通用 HTTP POST 客户端,面向对象设计,支持 JSON / Form / Multipart,内置超时重试和统一 `Response` 包装。
|
|
76
|
+
`IceJade::Poster::Client` 是通用 HTTP POST 客户端,面向对象设计,支持 JSON / Form / Multipart,内置超时重试和统一 `Response` 包装。GET 请求请使用并列的 `IceJade::Getter::Client`。
|
|
64
77
|
|
|
65
78
|
### 快速开始
|
|
66
79
|
|
|
@@ -92,15 +105,43 @@ resp = poster.post('/hook', '<xml>...</xml>', content_type: 'application/xml')
|
|
|
92
105
|
| 分支 | 定位 | 典型场景 |
|
|
93
106
|
|------|------|----------|
|
|
94
107
|
| Quantum | IM Webhook 专用 SDK | 向企业 IM 群机器人发消息 |
|
|
95
|
-
| Poster | 通用 HTTP POST 客户端 |
|
|
108
|
+
| Poster | 通用 HTTP POST 客户端 | JSON/Form/Multipart 提交、Webhook、文件上传 |
|
|
109
|
+
| Getter | 通用 HTTP GET 客户端 | 查询接口、健康检查、资源拉取 |
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
三者共享 `IceJade::Transport`、`IceJade::Response` 与 `IceJade::Error`,响应处理风格一致。
|
|
98
112
|
|
|
99
|
-
详细文档:`documents/poster-api.md`
|
|
113
|
+
详细文档:`documents/poster-api.md`、`documents/getter-api.md`
|
|
100
114
|
|
|
101
115
|
---
|
|
102
116
|
|
|
103
|
-
## 分支三:
|
|
117
|
+
## 分支三:Getter
|
|
118
|
+
|
|
119
|
+
`IceJade::Getter::Client` 是通用 HTTP GET 客户端,与 `Poster::Client` 并列,专责 GET 请求。底层传输能力(URI 拼接、超时重试、Response 包装)复用 `IceJade::Transport`,两者仅 HTTP 动词与参数构造不同。
|
|
120
|
+
|
|
121
|
+
### 快速开始
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
require 'ice_jade'
|
|
125
|
+
|
|
126
|
+
getter = IceJade::Getter::Client.new(
|
|
127
|
+
base_url: 'https://api.example.com',
|
|
128
|
+
headers: { 'Authorization' => 'Bearer token' },
|
|
129
|
+
timeout: 30
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# GET 带查询参数
|
|
133
|
+
resp = getter.get('/users', params: { page: 1, size: 20 })
|
|
134
|
+
puts resp.data if resp.success?
|
|
135
|
+
|
|
136
|
+
# GET 自定义请求头
|
|
137
|
+
resp = getter.get('/headers', headers: { 'X-Request-ID' => 'uuid-5678' })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
详细文档:`documents/getter-api.md`
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 分支四:HttpPoster(独立模块)
|
|
104
145
|
|
|
105
146
|
`HttpPoster` 是 `lib/http_poster.rb` 中定义的纯标准库 HTTP POST 工具,零 gem 依赖。它是独立于 ice-jade gem 的扁平化脚本,适合不想引入任何 gem 的场景。
|
|
106
147
|
|
|
@@ -135,7 +176,47 @@ puts res # => Hash(解析后的 JSON)
|
|
|
135
176
|
|
|
136
177
|
---
|
|
137
178
|
|
|
138
|
-
##
|
|
179
|
+
## 分支五:HttpGetter(独立模块)
|
|
180
|
+
|
|
181
|
+
`HttpGetter` 是 `lib/http_getter.rb` 中定义的纯标准库 HTTP GET 工具,零 gem 依赖。它与 `HttpPoster` 并列,构成独立的 POST / GET 工具对,适合不想引入任何 gem 的场景。
|
|
182
|
+
|
|
183
|
+
### 快速开始
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
require_relative 'lib/http_getter'
|
|
187
|
+
|
|
188
|
+
# GET JSON(模块静态方法)
|
|
189
|
+
res = HttpGetter.get_json(
|
|
190
|
+
'https://api.example.com/users',
|
|
191
|
+
{ page: 1, size: 20 },
|
|
192
|
+
{ 'Authorization' => 'Bearer token' },
|
|
193
|
+
{ timeout: 30 }
|
|
194
|
+
)
|
|
195
|
+
puts res # => Hash(解析后的 JSON)
|
|
196
|
+
|
|
197
|
+
# GET 纯文本(不做 JSON 解析)
|
|
198
|
+
text = HttpGetter.get_text('https://example.com/robots.txt')
|
|
199
|
+
puts text # => 原始字符串
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 与 Getter 的对比
|
|
203
|
+
|
|
204
|
+
| 维度 | HttpGetter | IceJade::Getter::Client |
|
|
205
|
+
|------|------------|-------------------------|
|
|
206
|
+
| 调用方式 | 模块静态方法 | 先 `new` 实例化再调用 |
|
|
207
|
+
| URL 处理 | 只能传完整 URL | 支持 `base_url` + 相对路径 |
|
|
208
|
+
| 参数风格 | 位置参数 `(url,p,h,opts)` | 关键字参数 `(params:)` |
|
|
209
|
+
| 成功返回 | Hash / String | `IceJade::Response` |
|
|
210
|
+
| 错误处理 | 抛 `TimeoutError` / `HttpError` | 包装为 `Response`(不抛异常) |
|
|
211
|
+
| 原始文本 | `get_text` 直接返回 String | 统一 Response(`data` 自动解析) |
|
|
212
|
+
| 所属体系 | 独立脚本(无依赖) | ice-jade gem 分支 |
|
|
213
|
+
| 典型场景 | 快速脚本、教学、零依赖 | 正式项目、与 Quantum 混用 |
|
|
214
|
+
|
|
215
|
+
详细文档:`documents/http-getter-api.md`
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 分支六:Cradle(HTTP 测试服务器)
|
|
139
220
|
|
|
140
221
|
Cradle 是轻量级 HTTP 测试服务器,基于 Ruby 标准库 `socket` + `thread`,零外部依赖。用于本地测试 Poster、Quantum 或其他 HTTP 客户端。
|
|
141
222
|
|
|
@@ -167,7 +248,7 @@ server.start
|
|
|
167
248
|
|
|
168
249
|
---
|
|
169
250
|
|
|
170
|
-
##
|
|
251
|
+
## 分支七:YAMLServer(YAML 配置驱动的测试实例)
|
|
171
252
|
|
|
172
253
|
YAMLServer 通过载入 YAML 配置文件启动定制化的 HTTP 测试实例,无需写 Ruby 代码注册路由,适合频繁切换测试场景。
|
|
173
254
|
|
|
@@ -222,17 +303,21 @@ Cradle 和 YAMLServer 都可以通过扩展代码添加新功能,变成满足
|
|
|
222
303
|
```
|
|
223
304
|
lib/
|
|
224
305
|
ice_jade.rb # 主入口
|
|
225
|
-
http_poster.rb #
|
|
306
|
+
http_poster.rb # 独立 POST 模块(零依赖)
|
|
307
|
+
http_getter.rb # 独立 GET 模块(零依赖)
|
|
226
308
|
ice_jade/
|
|
227
309
|
version.rb
|
|
228
310
|
error.rb
|
|
229
311
|
response.rb
|
|
230
312
|
client_base.rb
|
|
313
|
+
transport.rb # Poster/Getter 共享传输层
|
|
231
314
|
quantum/
|
|
232
315
|
client.rb
|
|
233
316
|
message.rb
|
|
234
317
|
poster/
|
|
235
|
-
client.rb
|
|
318
|
+
client.rb # POST 客户端
|
|
319
|
+
getter/
|
|
320
|
+
client.rb # GET 客户端
|
|
236
321
|
cradle/
|
|
237
322
|
server.rb # 原生服务器
|
|
238
323
|
yaml_server.rb # YAML 配置层
|
|
@@ -247,13 +332,17 @@ config/cradle/
|
|
|
247
332
|
examples/
|
|
248
333
|
quantum_usage.rb # Quantum 用法示例
|
|
249
334
|
poster_usage.rb # Poster + YAMLServer 联测
|
|
335
|
+
getter_usage.rb # Getter + YAMLServer 联测
|
|
250
336
|
comparison_poster.rb # HttpPoster vs Poster 对比
|
|
337
|
+
comparison_getter.rb # HttpGetter vs Getter 对比
|
|
251
338
|
cradle_usage.rb # Cradle 服务器用法
|
|
252
339
|
|
|
253
340
|
documents/
|
|
254
341
|
im-quantum-api.md # Quantum API 文档
|
|
255
342
|
poster-api.md # Poster API 文档
|
|
343
|
+
getter-api.md # Getter API 文档
|
|
256
344
|
http-poster-api.md # HttpPoster 文档
|
|
345
|
+
http-getter-api.md # HttpGetter 文档
|
|
257
346
|
cradle-api.md # Cradle 文档
|
|
258
347
|
yaml-server-api.md # YAMLServer 文档
|
|
259
348
|
cradle-extension.md # 扩展指南
|
|
@@ -268,8 +357,10 @@ documents/
|
|
|
268
357
|
```
|
|
269
358
|
ice-jade/
|
|
270
359
|
├── Quantum # IM Webhook 客户端
|
|
271
|
-
├── Poster # 通用 HTTP POST 客户端
|
|
272
|
-
├──
|
|
360
|
+
├── Poster # 通用 HTTP POST 客户端 ┐
|
|
361
|
+
├── Getter # 通用 HTTP GET 客户端 ┤ 共享 Transport
|
|
362
|
+
├── HttpPoster # 独立 POST 模块(零 gem 依赖) ┐
|
|
363
|
+
├── HttpGetter # 独立 GET 模块(零 gem 依赖) ┘ 并列独立工具对
|
|
273
364
|
├── Cradle # HTTP 测试服务器
|
|
274
365
|
└── YAMLServer # YAML 配置层(基于 Cradle)
|
|
275
366
|
```
|
|
@@ -322,3 +413,5 @@ end
|
|
|
322
413
|
|
|
323
414
|
- Quantum:文件上传 30MB,消息频率 20条/分钟
|
|
324
415
|
- Cradle:不支持 WebSocket、HTTPS、持久连接
|
|
416
|
+
|
|
417
|
+
> AI生成
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# HttpGetter vs IceJade::Getter 同步对比示例
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# 本文件把两种调用方式并排展示,均面向本地 Cradle YAML 实例,
|
|
9
|
+
# 通过 YAML 配置启动,无需外部网络,直接本地闭环验证。
|
|
10
|
+
#
|
|
11
|
+
# 左侧:HttpGetter —— 独立模块,静态方法,异常驱动
|
|
12
|
+
# 右侧:IceJade::Getter —— 面向对象,实例化复用,Response 驱动
|
|
13
|
+
#
|
|
14
|
+
# =============================================================================
|
|
15
|
+
|
|
16
|
+
require_relative '../lib/http_getter'
|
|
17
|
+
require_relative '../lib/ice_jade'
|
|
18
|
+
|
|
19
|
+
puts "=" * 70
|
|
20
|
+
puts "HttpGetter (独立模块) vs IceJade::Getter (ice-jade gem 分支)"
|
|
21
|
+
puts "目标: 本地 Cradle YAML 实例"
|
|
22
|
+
puts "=" * 70
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# 启动 Cradle YAML 实例
|
|
26
|
+
# =============================================================================
|
|
27
|
+
config_path = File.join(__dir__, '../config/cradle/poster_test.yml')
|
|
28
|
+
instance = IceJade::Cradle::YAMLServer.new(config_path)
|
|
29
|
+
thread = Thread.new { instance.start }
|
|
30
|
+
sleep 0.5
|
|
31
|
+
|
|
32
|
+
base_url = "http://#{instance.config['host']}:#{instance.config['port']}"
|
|
33
|
+
|
|
34
|
+
# IceJade::Getter 实例(复用配置)
|
|
35
|
+
getter = IceJade::Getter::Client.new(
|
|
36
|
+
base_url: base_url,
|
|
37
|
+
headers: { 'X-Custom-Header' => 'ice-jade' },
|
|
38
|
+
timeout: 30
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
|
|
43
|
+
# ==========================================================================
|
|
44
|
+
# 场景 1:GET 查询参数回显
|
|
45
|
+
# ==========================================================================
|
|
46
|
+
puts "\n【场景 1】GET → /echo (查询参数)"
|
|
47
|
+
puts "-" * 70
|
|
48
|
+
|
|
49
|
+
# --- HttpGetter ---
|
|
50
|
+
begin
|
|
51
|
+
res = HttpGetter.get_json(
|
|
52
|
+
"#{base_url}/echo",
|
|
53
|
+
{ page: 1, size: 20 },
|
|
54
|
+
{ 'X-Custom-Header' => 'ice-jade' },
|
|
55
|
+
{ timeout: 30 }
|
|
56
|
+
)
|
|
57
|
+
puts "[HttpGetter] Query echoed: #{res.is_a?(Hash) ? res['query'] : res}"
|
|
58
|
+
rescue HttpGetter::HttpError => e
|
|
59
|
+
puts "[HttpGetter] 失败: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- IceJade::Getter ---
|
|
63
|
+
resp = getter.get('/echo', params: { page: 1, size: 20 })
|
|
64
|
+
puts "[IceJade::Getter] Query echoed: #{resp.data.is_a?(Hash) ? resp.data['query'] : 'N/A'}"
|
|
65
|
+
puts " success?: #{resp.success?}, code: #{resp.code}"
|
|
66
|
+
|
|
67
|
+
# ==========================================================================
|
|
68
|
+
# 场景 2:GET 自定义请求头回显
|
|
69
|
+
# ==========================================================================
|
|
70
|
+
puts "\n【场景 2】GET → /headers (自定义头)"
|
|
71
|
+
puts "-" * 70
|
|
72
|
+
|
|
73
|
+
# --- HttpGetter ---
|
|
74
|
+
begin
|
|
75
|
+
res = HttpGetter.get_json(
|
|
76
|
+
"#{base_url}/headers",
|
|
77
|
+
{},
|
|
78
|
+
{ 'X-Custom-Header' => 'ice-jade', 'X-Request-ID' => 'uuid-5678' },
|
|
79
|
+
{ timeout: 30 }
|
|
80
|
+
)
|
|
81
|
+
echoed = res.is_a?(Hash) ? res['headers'] : {}
|
|
82
|
+
puts "[HttpGetter] X-Custom-Header: #{echoed['X-Custom-Header']}"
|
|
83
|
+
puts "[HttpGetter] X-Request-Id: #{echoed['X-Request-Id']}"
|
|
84
|
+
rescue HttpGetter::HttpError => e
|
|
85
|
+
puts "[HttpGetter] 失败: #{e.message}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# --- IceJade::Getter ---
|
|
89
|
+
resp = getter.get('/headers', headers: { 'X-Request-ID' => 'uuid-5678' })
|
|
90
|
+
echoed = resp.data.is_a?(Hash) ? resp.data['headers'] : {}
|
|
91
|
+
puts "[IceJade::Getter] X-Custom-Header: #{echoed['X-Custom-Header']}"
|
|
92
|
+
puts "[IceJade::Getter] X-Request-Id: #{echoed['X-Request-Id']}"
|
|
93
|
+
puts " success?: #{resp.success?}, code: #{resp.code}"
|
|
94
|
+
|
|
95
|
+
# ==========================================================================
|
|
96
|
+
# 场景 3:GET 纯文本(get_text 返回原始字符串,不做 JSON 解析)
|
|
97
|
+
# ==========================================================================
|
|
98
|
+
puts "\n【场景 3】GET 纯文本 → /echo (get_text)"
|
|
99
|
+
puts "-" * 70
|
|
100
|
+
|
|
101
|
+
# --- HttpGetter ---
|
|
102
|
+
begin
|
|
103
|
+
res = HttpGetter.get_text(
|
|
104
|
+
"#{base_url}/echo",
|
|
105
|
+
{ ping: 'pong' },
|
|
106
|
+
{},
|
|
107
|
+
{ timeout: 30 }
|
|
108
|
+
)
|
|
109
|
+
puts "[HttpGetter] 原始字符串: #{res.class} (#{res.to_s[0, 60]}...)"
|
|
110
|
+
rescue HttpGetter::HttpError => e
|
|
111
|
+
puts "[HttpGetter] 失败: #{e.message}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# --- IceJade::Getter ---
|
|
115
|
+
# Getter 没有 raw 选项,统一返回 Response,data 已解析
|
|
116
|
+
resp = getter.get('/echo', params: { ping: 'pong' })
|
|
117
|
+
puts "[IceJade::Getter] 解析后 data: #{resp.data.class}"
|
|
118
|
+
puts " success?: #{resp.success?}, code: #{resp.code}"
|
|
119
|
+
|
|
120
|
+
# ==========================================================================
|
|
121
|
+
# 场景 4:错误处理(HTTP 404)
|
|
122
|
+
# ==========================================================================
|
|
123
|
+
puts "\n【场景 4】错误处理 → /status/404"
|
|
124
|
+
puts "-" * 70
|
|
125
|
+
|
|
126
|
+
# --- HttpGetter ---
|
|
127
|
+
begin
|
|
128
|
+
HttpGetter.get_json("#{base_url}/status/404", {}, {}, { timeout: 30 })
|
|
129
|
+
rescue HttpGetter::HttpError => e
|
|
130
|
+
puts "[HttpGetter] 捕获异常: #{e.message}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# --- IceJade::Getter ---
|
|
134
|
+
resp = getter.get('/status/404')
|
|
135
|
+
puts "[IceJade::Getter] 不抛异常,直接检查响应:"
|
|
136
|
+
puts " success?: #{resp.success?}, code: #{resp.code}, message: #{resp.message}"
|
|
137
|
+
|
|
138
|
+
# ==========================================================================
|
|
139
|
+
# 场景 5:通用 get(url 已含 query,params 追加)
|
|
140
|
+
# ==========================================================================
|
|
141
|
+
puts "\n【场景 5】通用 get → /echo?foo=1 (params 追加)"
|
|
142
|
+
puts "-" * 70
|
|
143
|
+
|
|
144
|
+
# --- HttpGetter ---
|
|
145
|
+
begin
|
|
146
|
+
res = HttpGetter.get(
|
|
147
|
+
"#{base_url}/echo?foo=1",
|
|
148
|
+
{ bar: '2' },
|
|
149
|
+
{ 'X-Request-ID' => 'uuid-1234' },
|
|
150
|
+
{ timeout: 30 }
|
|
151
|
+
)
|
|
152
|
+
puts "[HttpGetter] Query echoed: #{res.is_a?(Hash) ? res['query'] : res}"
|
|
153
|
+
rescue HttpGetter::HttpError => e
|
|
154
|
+
puts "[HttpGetter] 失败: #{e.message}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# --- IceJade::Getter ---
|
|
158
|
+
resp = getter.get('/echo?foo=1', params: { bar: '2' },
|
|
159
|
+
headers: { 'X-Request-ID' => 'uuid-1234' })
|
|
160
|
+
puts "[IceJade::Getter] Query echoed: #{resp.data.is_a?(Hash) ? resp.data['query'] : 'N/A'}"
|
|
161
|
+
puts " success?: #{resp.success?}, code: #{resp.code}"
|
|
162
|
+
|
|
163
|
+
# ==========================================================================
|
|
164
|
+
# 结构差异速查表
|
|
165
|
+
# ==========================================================================
|
|
166
|
+
puts "\n" + "=" * 70
|
|
167
|
+
puts "结构差异速查表"
|
|
168
|
+
puts "=" * 70
|
|
169
|
+
puts <<~DIFF
|
|
170
|
+
|
|
171
|
+
┌──────────────────┬──────────────────────────┬──────────────────────────┐
|
|
172
|
+
│ 维度 │ HttpGetter │ IceJade::Getter │
|
|
173
|
+
├──────────────────┼──────────────────────────┼──────────────────────────┤
|
|
174
|
+
│ 调用方式 │ 模块静态方法 │ 先 new 实例化再调用 │
|
|
175
|
+
│ URL 处理 │ 只能传完整 URL │ 支持 base_url + 相对路径 │
|
|
176
|
+
│ 参数风格 │ 位置参数 (url,p,h,opts) │ 关键字参数 (params:) │
|
|
177
|
+
│ 配置复用 │ 每次调用都传 headers │ 初始化一次,多次复用 │
|
|
178
|
+
│ 成功返回 │ Hash / String │ IceJade::Response │
|
|
179
|
+
│ 错误处理 │ 抛 TimeoutError/HttpError│ 包装为 Response │
|
|
180
|
+
│ 判断结果 │ begin/rescue │ resp.success? │
|
|
181
|
+
│ 原始文本 │ get_text 直接返回 String │ 统一 Response(data 解析)│
|
|
182
|
+
│ 所属体系 │ 独立脚本 │ ice-jade gem 分支 │
|
|
183
|
+
│ 适合场景 │ 快速脚本、教学、零依赖 │ 正式项目、与 Quantum 混用│
|
|
184
|
+
└──────────────────┴──────────────────────────┴──────────────────────────┘
|
|
185
|
+
|
|
186
|
+
DIFF
|
|
187
|
+
puts "示例运行完毕。"
|
|
188
|
+
|
|
189
|
+
ensure
|
|
190
|
+
instance.stop
|
|
191
|
+
thread.join
|
|
192
|
+
puts "\nCradle YAML 实例已关闭。"
|
|
193
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Getter 用法示例:调用 Cradle YAML 实例
|
|
5
|
+
#
|
|
6
|
+
# 本示例通过 YAML 配置启动定制化的 Cradle 测试实例,
|
|
7
|
+
# 然后用 IceJade::Getter::Client 向它发起 GET 请求,本地闭环验证。
|
|
8
|
+
#
|
|
9
|
+
# Getter 与 Poster 并列:Poster 专责 POST,Getter 专责 GET,
|
|
10
|
+
# 两者复用同一套 IceJade::Transport 底层(URI 拼接、超时重试、Response 包装)。
|
|
11
|
+
|
|
12
|
+
require_relative '../lib/ice_jade'
|
|
13
|
+
|
|
14
|
+
# =============================================================================
|
|
15
|
+
# 1. 启动 Cradle YAML 实例
|
|
16
|
+
# =============================================================================
|
|
17
|
+
puts "=== 启动 Cradle YAML 实例 ==="
|
|
18
|
+
config_path = File.join(__dir__, '../config/cradle/poster_test.yml')
|
|
19
|
+
instance = IceJade::Cradle::YAMLServer.new(config_path)
|
|
20
|
+
thread = Thread.new { instance.start }
|
|
21
|
+
sleep 0.5
|
|
22
|
+
|
|
23
|
+
getter = IceJade::Getter::Client.new(
|
|
24
|
+
base_url: "http://#{instance.config['host']}:#{instance.config['port']}",
|
|
25
|
+
headers: { 'X-Custom-Header' => 'ice-jade' }
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# 1. GET 查询参数回显
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
puts "\n=== 1. GET → /echo (查询参数) ==="
|
|
34
|
+
resp = getter.get('/echo', params: { page: 1, size: 20 })
|
|
35
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}, Message: #{resp.message}"
|
|
36
|
+
puts "Query echoed: #{resp.data.is_a?(Hash) ? resp.data['query'] : 'N/A'}"
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------------
|
|
39
|
+
# 2. GET 回显请求头(默认头 + 额外头合并校验)
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
puts "\n=== 2. GET → /headers (自定义头) ==="
|
|
42
|
+
resp = getter.get('/headers', headers: { 'X-Request-ID' => 'uuid-5678' })
|
|
43
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}"
|
|
44
|
+
# 注意:Net::HTTP 会将头名规范化为 X-Request-Id(每段首字母大写)
|
|
45
|
+
echoed_headers = resp.data.is_a?(Hash) ? resp.data['headers'] : {}
|
|
46
|
+
puts "X-Custom-Header: #{echoed_headers['X-Custom-Header']}"
|
|
47
|
+
puts "X-Request-Id: #{echoed_headers['X-Request-Id']}"
|
|
48
|
+
|
|
49
|
+
# ------------------------------------------------------------------
|
|
50
|
+
# 3. GET 错误处理(HTTP 404)
|
|
51
|
+
# ------------------------------------------------------------------
|
|
52
|
+
puts "\n=== 3. GET 错误处理 → /status/404 ==="
|
|
53
|
+
resp = getter.get('/status/404')
|
|
54
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}, Message: #{resp.message}"
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# 4. GET 错误处理(HTTP 500)
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
puts "\n=== 4. GET 错误处理 → /status/500 ==="
|
|
60
|
+
resp = getter.get('/status/500')
|
|
61
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}, Message: #{resp.message}"
|
|
62
|
+
|
|
63
|
+
# ------------------------------------------------------------------
|
|
64
|
+
# 5. GET 已含 query 的 URL(params 追加而非覆盖)
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
puts "\n=== 5. GET → /echo?foo=1 (params 追加) ==="
|
|
67
|
+
resp = getter.get('/echo?foo=1', params: { bar: '2' })
|
|
68
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}"
|
|
69
|
+
puts "Query echoed: #{resp.data.is_a?(Hash) ? resp.data['query'] : 'N/A'}"
|
|
70
|
+
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
# 6. GET 健康检查
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
puts "\n=== 6. GET → /health ==="
|
|
75
|
+
resp = getter.get('/health')
|
|
76
|
+
puts "Success: #{resp.success?}, Code: #{resp.code}"
|
|
77
|
+
puts "Status: #{resp.data.is_a?(Hash) ? resp.data['status'] : 'N/A'}"
|
|
78
|
+
|
|
79
|
+
puts "\n=== 所有测试通过 ==="
|
|
80
|
+
|
|
81
|
+
ensure
|
|
82
|
+
instance.stop
|
|
83
|
+
thread.join
|
|
84
|
+
puts "\nCradle YAML 实例已关闭。"
|
|
85
|
+
end
|
data/lib/http_getter.rb
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# http_getter.rb
|
|
2
|
+
# Ruby 纯标准库 HTTP GET 工具模块
|
|
3
|
+
# 零外部依赖,支持查询参数 / JSON / 透传响应
|
|
4
|
+
#
|
|
5
|
+
# =============================================================================
|
|
6
|
+
# 说明:本模块是独立工具脚本,不依赖 ice-jade gem。
|
|
7
|
+
#
|
|
8
|
+
# 如果你正在使用 ice-jade 项目,推荐优先使用 IceJade::Getter::Client
|
|
9
|
+
# (位于 lib/ice_jade/getter/client.rb),原因:
|
|
10
|
+
#
|
|
11
|
+
# - 面向对象:初始化时配置 base_url/headers/timeout,后续复用
|
|
12
|
+
# - 统一响应:返回 IceJade::Response(ok, code, message, data, raw)
|
|
13
|
+
# - 异常友好:超时/HTTP 错误均包装为 Response,不抛异常
|
|
14
|
+
# - 与 Quantum 分支共享同一套响应体系
|
|
15
|
+
#
|
|
16
|
+
# HttpGetter 适合:
|
|
17
|
+
# - 不想引入任何 gem 依赖的独立脚本
|
|
18
|
+
# - 快速测试、一次性请求
|
|
19
|
+
# - 学习/教学用途,代码更扁平直观
|
|
20
|
+
#
|
|
21
|
+
# 对比示例见:examples/comparison_getter.rb
|
|
22
|
+
# =============================================================================
|
|
23
|
+
|
|
24
|
+
require 'net/http'
|
|
25
|
+
require 'json'
|
|
26
|
+
require 'uri'
|
|
27
|
+
|
|
28
|
+
module HttpGetter
|
|
29
|
+
class Error < StandardError; end
|
|
30
|
+
class TimeoutError < Error; end
|
|
31
|
+
class HttpError < Error; end
|
|
32
|
+
|
|
33
|
+
# ============ 核心 GET 方法 ============
|
|
34
|
+
#
|
|
35
|
+
# @param url [String] 完整 URL 或路径;若已含 query,params 会追加在其后
|
|
36
|
+
# @param params [Hash] 查询参数,自动用 URI.encode_www_form 编码到 query string
|
|
37
|
+
# @param headers [Hash] 自定义请求头(自动覆盖默认头)
|
|
38
|
+
# @param opts [Hash] 选项: :timeout, :open_timeout, :max_retries
|
|
39
|
+
#
|
|
40
|
+
# @return [Hash/String] 解析后的 JSON 或原始响应体
|
|
41
|
+
#
|
|
42
|
+
def self.get(url, params = {}, headers = {}, opts = {})
|
|
43
|
+
uri = URI.parse(url)
|
|
44
|
+
unless params.empty?
|
|
45
|
+
extra = URI.encode_www_form(params)
|
|
46
|
+
uri.query = uri.query ? "#{uri.query}&#{extra}" : extra
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
req = Net::HTTP::Get.new(uri)
|
|
50
|
+
req['User-Agent'] = headers['User-Agent'] || 'ruby-http-getter/1.0'
|
|
51
|
+
req['Accept'] = headers['Accept'] || 'application/json'
|
|
52
|
+
|
|
53
|
+
# 认证头(支持 Bearer Token)
|
|
54
|
+
if headers['Authorization'] || headers[:authorization]
|
|
55
|
+
req['Authorization'] = headers['Authorization'] || headers[:authorization]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 追加其余自定义头
|
|
59
|
+
headers.each do |k, v|
|
|
60
|
+
next if %w[Accept Authorization User-Agent].include?(k.to_s)
|
|
61
|
+
req[k.to_s] = v.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
execute(uri, req, opts)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ============ 快捷方法 ============
|
|
68
|
+
|
|
69
|
+
# GET JSON(API 调用最常用)
|
|
70
|
+
# 自动设置 Accept: application/json,期望服务端返回 JSON
|
|
71
|
+
def self.get_json(url, params = {}, headers = {}, opts = {})
|
|
72
|
+
headers = headers.merge('Accept' => 'application/json')
|
|
73
|
+
get(url, params, headers, opts)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# GET 纯文本(原始响应体,不做 JSON 解析)
|
|
77
|
+
# 自动设置 Accept: text/plain,返回原始 String
|
|
78
|
+
def self.get_text(url, params = {}, headers = {}, opts = {})
|
|
79
|
+
headers = headers.merge('Accept' => 'text/plain')
|
|
80
|
+
opts = opts.merge(__raw__: true)
|
|
81
|
+
get(url, params, headers, opts)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def self.execute(uri, request, opts = {})
|
|
87
|
+
timeout = opts[:timeout] || 60
|
|
88
|
+
open_timeout = opts[:open_timeout] || 10
|
|
89
|
+
max_retries = opts[:max_retries] || 2
|
|
90
|
+
raw = opts[:__raw__] == true
|
|
91
|
+
|
|
92
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
93
|
+
http.use_ssl = uri.scheme == 'https'
|
|
94
|
+
http.open_timeout = open_timeout
|
|
95
|
+
http.read_timeout = timeout
|
|
96
|
+
|
|
97
|
+
retries = 0
|
|
98
|
+
begin
|
|
99
|
+
response = http.request(request)
|
|
100
|
+
parse(response, raw)
|
|
101
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
102
|
+
retries += 1
|
|
103
|
+
if retries <= max_retries
|
|
104
|
+
sleep(2 ** retries)
|
|
105
|
+
retry
|
|
106
|
+
end
|
|
107
|
+
raise TimeoutError, "请求超时(已重试 #{max_retries} 次): #{e.message}"
|
|
108
|
+
ensure
|
|
109
|
+
http.finish if http.started?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.parse(response, raw = false)
|
|
114
|
+
body = response.body.to_s
|
|
115
|
+
code = response.code.to_i
|
|
116
|
+
|
|
117
|
+
if code >= 400
|
|
118
|
+
begin
|
|
119
|
+
err = JSON.parse(body)
|
|
120
|
+
msg = err.dig('error', 'message') || body
|
|
121
|
+
rescue
|
|
122
|
+
msg = body
|
|
123
|
+
end
|
|
124
|
+
raise HttpError, "HTTP #{code}: #{msg}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
return body if raw
|
|
128
|
+
|
|
129
|
+
JSON.parse(body) rescue body
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# ============ 使用示例 ============
|
|
134
|
+
if __FILE__ == $0
|
|
135
|
+
# 示例 1: GET JSON(调用公开 API)
|
|
136
|
+
begin
|
|
137
|
+
res = HttpGetter.get_json(
|
|
138
|
+
'https://httpbin.org/get',
|
|
139
|
+
{ page: 1, size: 20 },
|
|
140
|
+
{ 'Authorization' => 'Bearer your-token-here' },
|
|
141
|
+
{ timeout: 30 }
|
|
142
|
+
)
|
|
143
|
+
puts res['args']
|
|
144
|
+
rescue HttpGetter::HttpError => e
|
|
145
|
+
puts "请求失败: #{e.message}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# 示例 2: GET 纯文本
|
|
149
|
+
# res = HttpGetter.get_text('https://httpbin.org/robots.txt')
|
|
150
|
+
# puts res # => 原始字符串
|
|
151
|
+
|
|
152
|
+
# 示例 3: 通用 GET(自定义头,url 已含 query,params 追加)
|
|
153
|
+
# res = HttpGetter.get(
|
|
154
|
+
# 'https://httpbin.org/get?foo=1',
|
|
155
|
+
# { bar: '2' },
|
|
156
|
+
# { 'X-Request-ID' => 'uuid-5678' }
|
|
157
|
+
# )
|
|
158
|
+
# puts res['args'] # => {"foo"=>"1", "bar"=>"2"}
|
|
159
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../transport'
|
|
4
|
+
|
|
5
|
+
module IceJade
|
|
6
|
+
module Getter
|
|
7
|
+
# 通用 HTTP GET 客户端
|
|
8
|
+
#
|
|
9
|
+
# 与 IceJade::Poster::Client 并列,专用负责 GET 请求。
|
|
10
|
+
# 底层传输能力(URI 拼接、请求执行、响应适配、超时重试)复用 IceJade::Transport,
|
|
11
|
+
# 与 Poster 仅 HTTP 动词与参数构造不同:GET 幂等,无请求体,参数走 query string。
|
|
12
|
+
#
|
|
13
|
+
# 零外部依赖,内置超时控制、指数退避重试、统一 Response 包装,
|
|
14
|
+
# 与 Quantum 模块风格一致,错误不抛异常。
|
|
15
|
+
#
|
|
16
|
+
# @example 基础用法
|
|
17
|
+
# getter = IceJade::Getter::Client.new(
|
|
18
|
+
# base_url: 'https://api.example.com',
|
|
19
|
+
# headers: { 'Authorization' => 'Bearer token' },
|
|
20
|
+
# timeout: 30
|
|
21
|
+
# )
|
|
22
|
+
# resp = getter.get('/users', params: { page: 1, size: 20 })
|
|
23
|
+
# puts resp.data if resp.success?
|
|
24
|
+
#
|
|
25
|
+
class Client
|
|
26
|
+
include IceJade::Transport
|
|
27
|
+
|
|
28
|
+
# @param base_url [String, nil] 基础 URL,后续请求可传相对路径
|
|
29
|
+
# @param headers [Hash] 默认请求头(每次请求自动合并,同名会被覆盖)
|
|
30
|
+
# @param timeout [Integer] 读取超时(秒),默认 60
|
|
31
|
+
# @param open_timeout [Integer] 连接超时(秒),默认 10
|
|
32
|
+
# @param max_retries [Integer] 超时后最大重试次数,默认 2
|
|
33
|
+
def initialize(base_url: nil, headers: {}, timeout: 60, open_timeout: 10, max_retries: 2)
|
|
34
|
+
init_transport(base_url: base_url, headers: headers,
|
|
35
|
+
timeout: timeout, open_timeout: open_timeout,
|
|
36
|
+
max_retries: max_retries)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# 通用 GET
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
# 发送通用 GET 请求
|
|
44
|
+
#
|
|
45
|
+
# 与 Poster 的 post 系列方法对称:返回 IceJade::Response,错误不抛异常。
|
|
46
|
+
# GET 幂等,超时重试同样安全。
|
|
47
|
+
#
|
|
48
|
+
# @param url [String] 完整 URL 或相对路径(需先设置 base_url)
|
|
49
|
+
# @param params [Hash, nil] 查询参数,自动用 URI.encode_www_form 编码到 query string;
|
|
50
|
+
# 若 url 已含 query,则在其后追加
|
|
51
|
+
# @param headers [Hash] 额外请求头(与默认头合并,同名覆盖)
|
|
52
|
+
# @return [IceJade::Response]
|
|
53
|
+
#
|
|
54
|
+
# @example 带查询参数
|
|
55
|
+
# resp = getter.get('/users', params: { page: 1, size: 20 })
|
|
56
|
+
# puts resp.data if resp.success?
|
|
57
|
+
#
|
|
58
|
+
# @example 自定义请求头
|
|
59
|
+
# resp = getter.get('/headers', headers: { 'X-Request-ID' => 'uuid-5678' })
|
|
60
|
+
#
|
|
61
|
+
# @example 错误响应
|
|
62
|
+
# resp = getter.get('/status/404')
|
|
63
|
+
# resp.success? # => false
|
|
64
|
+
# resp.code # => 404
|
|
65
|
+
def get(url, params: nil, headers: {})
|
|
66
|
+
uri = build_uri(url)
|
|
67
|
+
if params && !params.empty?
|
|
68
|
+
extra = URI.encode_www_form(params)
|
|
69
|
+
uri.query = uri.query ? "#{uri.query}&#{extra}" : extra
|
|
70
|
+
end
|
|
71
|
+
merged_headers = merge_headers(default_headers, normalize_headers(headers))
|
|
72
|
+
req = Net::HTTP::Get.new(uri)
|
|
73
|
+
apply_headers(req, merged_headers)
|
|
74
|
+
execute(uri, req)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -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:
|
|
@@ -24,10 +25,13 @@ files:
|
|
|
24
25
|
- bin/cradle
|
|
25
26
|
- bin/cradle-instance
|
|
26
27
|
- config/cradle/poster_test.yml
|
|
28
|
+
- examples/comparison_getter.rb
|
|
27
29
|
- examples/comparison_poster.rb
|
|
28
30
|
- examples/cradle_usage.rb
|
|
31
|
+
- examples/getter_usage.rb
|
|
29
32
|
- examples/poster_usage.rb
|
|
30
33
|
- examples/quantum_usage.rb
|
|
34
|
+
- lib/http_getter.rb
|
|
31
35
|
- lib/http_poster.rb
|
|
32
36
|
- lib/ice_jade.rb
|
|
33
37
|
- lib/ice_jade/client_base.rb
|
|
@@ -35,11 +39,14 @@ files:
|
|
|
35
39
|
- lib/ice_jade/cradle/server.rb
|
|
36
40
|
- lib/ice_jade/cradle/yaml_server.rb
|
|
37
41
|
- lib/ice_jade/error.rb
|
|
42
|
+
- lib/ice_jade/getter.rb
|
|
43
|
+
- lib/ice_jade/getter/client.rb
|
|
38
44
|
- lib/ice_jade/poster.rb
|
|
39
45
|
- lib/ice_jade/poster/client.rb
|
|
40
46
|
- lib/ice_jade/quantum/client.rb
|
|
41
47
|
- lib/ice_jade/quantum/message.rb
|
|
42
48
|
- lib/ice_jade/response.rb
|
|
49
|
+
- lib/ice_jade/transport.rb
|
|
43
50
|
- lib/ice_jade/version.rb
|
|
44
51
|
homepage: https://www.srdcloud.cn/codehub/Frampt/ice-jade
|
|
45
52
|
licenses:
|
|
@@ -61,5 +68,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
61
68
|
requirements: []
|
|
62
69
|
rubygems_version: 4.0.10
|
|
63
70
|
specification_version: 4
|
|
64
|
-
summary: IM webhook SDK & general-purpose HTTP POST client
|
|
71
|
+
summary: IM webhook SDK & general-purpose HTTP POST/GET client
|
|
65
72
|
test_files: []
|