tcat 0.2.2 → 0.4.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 +211 -33
- data/lib/tcat/configuration.rb +7 -0
- data/lib/tcat/query.rb +30 -11
- data/lib/tcat/version.rb +1 -1
- data/lib/tcat/worker_client.rb +161 -0
- data/lib/tcat.rb +1 -0
- metadata +3 -6
- data/.rspec +0 -3
- data/.rubocop.yml +0 -25
- data/.tool-versions +0 -1
- data/Rakefile +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ed5fe975474cdb30f8d660719b0a59d0ea5f1813b4e86433c95cd2ee82c795e
|
|
4
|
+
data.tar.gz: 025bf6aa6cfc46a4437d72e1d8abc966853606c1ba58b56ea5f6334a541b063e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b59a640cf706df074a2daa9bac89dfed2d96b1cfa1720278afe00b66ca8991cf64c096e4fca3701b05e7f43009b71f5f2e57186c902b339000d76db6098f4b13
|
|
7
|
+
data.tar.gz: 4d678f18e49855e6faf85ac8f4b0969f7c3c6d7d8a176686cd31ecc875e871a36ac3452a19dc9e5c7cd02ef559fdccf4e727085984c52d0066456f1b8b27302c
|
data/README.md
CHANGED
|
@@ -2,13 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
A Ruby gem for tracking T-Cat (Taiwan Pelican Express) shipment status. Provides a simple and easy-to-use API interface.
|
|
4
4
|
|
|
5
|
+
## ✨ Two Ways to Use Tcat
|
|
6
|
+
|
|
7
|
+
### 1. Ruby Gem (This Repository)
|
|
8
|
+
Perfect for Ruby/Rails applications with direct integration.
|
|
9
|
+
|
|
10
|
+
### 2. Cloudflare Worker API
|
|
11
|
+
Looking for a serverless solution? Check out **[worker/README.md](worker/README.md)**
|
|
12
|
+
|
|
13
|
+
Benefits of the Worker version:
|
|
14
|
+
- 🌍 Global edge deployment for faster queries worldwide
|
|
15
|
+
- 🔒 Secure secret storage in environment variables
|
|
16
|
+
- 🌐 HTTP API accessible from any platform (JavaScript, Python, cURL, etc.)
|
|
17
|
+
- 💰 Cost-effective: 100K free requests/day
|
|
18
|
+
- 📱 Perfect for frontend apps, mobile apps, or microservices
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
5
22
|
## Features
|
|
6
23
|
|
|
7
|
-
- Track shipment status
|
|
8
|
-
- Secure encrypted requests
|
|
9
|
-
- Simple API interface
|
|
10
|
-
- Non-blocking HTTP requests
|
|
11
|
-
- Comprehensive error handling
|
|
24
|
+
- 📦 Track shipment status
|
|
25
|
+
- 🔐 Secure encrypted requests
|
|
26
|
+
- 🌐 Simple API interface
|
|
27
|
+
- ⚡ Non-blocking HTTP requests
|
|
28
|
+
- 🛡️ Comprehensive error handling
|
|
12
29
|
|
|
13
30
|
## Installation
|
|
14
31
|
|
|
@@ -30,40 +47,47 @@ Or install it yourself as:
|
|
|
30
47
|
$ gem install tcat
|
|
31
48
|
```
|
|
32
49
|
|
|
33
|
-
##
|
|
50
|
+
## Usage
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
There are two ways to use this gem:
|
|
53
|
+
|
|
54
|
+
1. **Direct API Access** - Query T-Cat API directly from your Ruby application
|
|
55
|
+
2. **Cloudflare Worker** - Query through a Cloudflare Worker proxy (recommended for production)
|
|
56
|
+
|
|
57
|
+
### Option 1: Direct API Access
|
|
58
|
+
|
|
59
|
+
Configure the gem with your T-Cat API credentials:
|
|
36
60
|
|
|
37
61
|
```ruby
|
|
38
62
|
Tcat.configure do |config|
|
|
39
63
|
config.secret_string = 'your_secret_string'
|
|
40
64
|
config.secret_key = 'your_secret_key'
|
|
41
65
|
end
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Usage
|
|
45
66
|
|
|
46
|
-
|
|
67
|
+
# Two equivalent shapes — pick whichever fits your code:
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
# Create a query instance
|
|
69
|
+
# A) tracking number bound at construction (one-shot)
|
|
50
70
|
query = Tcat::Query.new('your_tracking_number')
|
|
51
|
-
|
|
52
|
-
# Get shipment status
|
|
53
71
|
status = query.status_code
|
|
72
|
+
|
|
73
|
+
# B) tracking number per call (reuse one client for many lookups,
|
|
74
|
+
# matches Tcat::WorkerClient's shape)
|
|
75
|
+
query = Tcat::Query.new
|
|
76
|
+
status = query.status_code('your_tracking_number')
|
|
54
77
|
# Returns one of the following:
|
|
55
|
-
# :done
|
|
56
|
-
# :delivering
|
|
57
|
-
# :collected
|
|
58
|
-
# :in_transit
|
|
59
|
-
# :returned
|
|
60
|
-
# :held
|
|
61
|
-
# :rescheduled
|
|
62
|
-
# :forwarding
|
|
63
|
-
# :investigation
|
|
64
|
-
# :rejected
|
|
65
|
-
# :returning
|
|
66
|
-
# :
|
|
78
|
+
# :done - Successfully delivered
|
|
79
|
+
# :delivering - Out for delivery
|
|
80
|
+
# :collected - Package collected
|
|
81
|
+
# :in_transit - In transit
|
|
82
|
+
# :returned - Return completed
|
|
83
|
+
# :held - Held at post office
|
|
84
|
+
# :rescheduled - Delivery time rescheduled
|
|
85
|
+
# :forwarding - Being forwarded
|
|
86
|
+
# :investigation - Under investigation
|
|
87
|
+
# :rejected - Delivery rejected
|
|
88
|
+
# :returning - In return process
|
|
89
|
+
# :store_delivery - At convenience store for pickup
|
|
90
|
+
# :unknown - Unknown status
|
|
67
91
|
|
|
68
92
|
# Get latest status details
|
|
69
93
|
latest = query.latest_status
|
|
@@ -85,6 +109,84 @@ history.each do |item|
|
|
|
85
109
|
end
|
|
86
110
|
```
|
|
87
111
|
|
|
112
|
+
### Option 2: Via Cloudflare Worker (Recommended)
|
|
113
|
+
|
|
114
|
+
The Worker approach is recommended for production because:
|
|
115
|
+
- ✅ Secrets are stored securely in Cloudflare, not in your application
|
|
116
|
+
- ✅ Faster response times via Cloudflare's edge network
|
|
117
|
+
- ✅ No need to manage encryption in your Ruby app
|
|
118
|
+
- ✅ Can be used by any language/platform (not just Ruby)
|
|
119
|
+
|
|
120
|
+
First, deploy the Cloudflare Worker (see [worker/README.md](worker/README.md) for deployment instructions).
|
|
121
|
+
|
|
122
|
+
Then use the WorkerClient in your Ruby application. You can pass the Worker
|
|
123
|
+
URL (and optional Bearer token) directly, or configure them globally via
|
|
124
|
+
`Tcat.configure`:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
# Option A: explicit per-instance arguments
|
|
128
|
+
client = Tcat::WorkerClient.new(
|
|
129
|
+
'https://your-worker.workers.dev',
|
|
130
|
+
token: ENV['TCAT_WORKER_TOKEN'] # only needed when AUTH_TOKEN is set on the Worker
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Option B: global configuration, then construct without arguments
|
|
134
|
+
Tcat.configure do |config|
|
|
135
|
+
config.worker_url = 'https://your-worker.workers.dev'
|
|
136
|
+
config.worker_token = ENV['TCAT_WORKER_TOKEN']
|
|
137
|
+
end
|
|
138
|
+
client = Tcat::WorkerClient.new
|
|
139
|
+
|
|
140
|
+
# Explicit args always override configuration:
|
|
141
|
+
override = Tcat::WorkerClient.new('https://other.workers.dev', token: 'other-token')
|
|
142
|
+
|
|
143
|
+
# Get shipment status (same API as Query)
|
|
144
|
+
status = client.status_code('your_tracking_number')
|
|
145
|
+
# => :delivering
|
|
146
|
+
|
|
147
|
+
# Get latest status details
|
|
148
|
+
latest = client.latest_status('your_tracking_number')
|
|
149
|
+
if latest
|
|
150
|
+
puts "Status: #{latest.status}" # e.g. "配送中"
|
|
151
|
+
puts "Status code: #{latest.status_code}" # e.g. :delivering
|
|
152
|
+
puts "Time: #{latest.time}" # Time object
|
|
153
|
+
puts "Office: #{latest.office}" # e.g. "台北營業所"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Get full shipment history
|
|
157
|
+
history = client.history('your_tracking_number')
|
|
158
|
+
history.each do |item|
|
|
159
|
+
puts "Status: #{item.status}"
|
|
160
|
+
puts "Time: #{item.time}"
|
|
161
|
+
puts "Office: #{item.office}"
|
|
162
|
+
puts "---"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Check if Worker is healthy
|
|
166
|
+
if client.healthy?
|
|
167
|
+
puts "Worker is operational"
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Custom timeout:**
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Default timeout is 30 seconds
|
|
175
|
+
client = Tcat::WorkerClient.new('https://your-worker.workers.dev', timeout: 60)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Error handling:**
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
begin
|
|
182
|
+
status = client.status_code('tracking_number')
|
|
183
|
+
rescue Tcat::WorkerClient::NetworkError => e
|
|
184
|
+
puts "Network error: #{e.message}"
|
|
185
|
+
rescue Tcat::WorkerClient::APIError => e
|
|
186
|
+
puts "API error: #{e.message}"
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
88
190
|
### Status Code Explanation
|
|
89
191
|
|
|
90
192
|
- `:done` - Successfully delivered
|
|
@@ -98,20 +200,52 @@ end
|
|
|
98
200
|
- `:investigation` - Package is under investigation (e.g., address change, rejection)
|
|
99
201
|
- `:rejected` - Delivery was rejected
|
|
100
202
|
- `:returning` - Package is in return process
|
|
203
|
+
- `:store_delivery` - Handed over to a convenience store, awaiting recipient pickup
|
|
101
204
|
- `:unknown` - Unknown status
|
|
102
205
|
|
|
103
|
-
##
|
|
206
|
+
## Cloudflare Worker
|
|
104
207
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
208
|
+
This repository includes a Cloudflare Worker implementation that provides an HTTP API for T-Cat tracking.
|
|
209
|
+
|
|
210
|
+
### Features
|
|
211
|
+
|
|
212
|
+
- 🌍 Deploy globally on Cloudflare's edge network
|
|
213
|
+
- 🔒 Securely store API credentials as Worker secrets
|
|
214
|
+
- 🚀 Fast response times from edge locations
|
|
215
|
+
- 🌐 CORS enabled for frontend applications
|
|
216
|
+
- 📱 Works with any programming language
|
|
217
|
+
|
|
218
|
+
### Quick Start
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
cd worker
|
|
222
|
+
npm install
|
|
223
|
+
npm run dev # Start local development server
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
For full deployment instructions, see [worker/README.md](worker/README.md) and [worker/DEPLOYMENT.md](worker/DEPLOYMENT.md).
|
|
227
|
+
|
|
228
|
+
## Comparison: Direct API vs Worker
|
|
229
|
+
|
|
230
|
+
| Feature | Direct API (`Query`) | Cloudflare Worker (`WorkerClient`) |
|
|
231
|
+
|---------|---------------------|-----------------------------------|
|
|
232
|
+
| Setup complexity | Low (just gem install) | Medium (requires Worker deployment) |
|
|
233
|
+
| Security | Secrets in your app | Secrets in Cloudflare |
|
|
234
|
+
| Performance | Direct to T-Cat API | Via Cloudflare edge |
|
|
235
|
+
| Multi-platform | Ruby only | Any language/platform |
|
|
236
|
+
| Cost | Free | Free tier available |
|
|
237
|
+
| Best for | Simple scripts, internal tools | Production apps, public APIs |
|
|
238
|
+
|
|
239
|
+
## Development
|
|
110
240
|
|
|
111
241
|
### Running Tests
|
|
112
242
|
|
|
113
243
|
```bash
|
|
244
|
+
# Run all tests
|
|
114
245
|
$ bundle exec rake spec
|
|
246
|
+
|
|
247
|
+
# Run specific test file
|
|
248
|
+
$ bundle exec rspec spec/tcat/worker_client_spec.rb
|
|
115
249
|
```
|
|
116
250
|
|
|
117
251
|
### Local Installation
|
|
@@ -124,12 +258,56 @@ $ bundle exec rake install
|
|
|
124
258
|
|
|
125
259
|
Bug reports and pull requests are welcome.
|
|
126
260
|
|
|
261
|
+
1. Fork the project
|
|
262
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
263
|
+
3. Commit your changes (`git commit -am 'Add some amazing feature'`)
|
|
264
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
265
|
+
5. Create a Pull Request
|
|
266
|
+
|
|
127
267
|
## License
|
|
128
268
|
|
|
129
269
|
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
130
270
|
|
|
131
271
|
## Changelog
|
|
132
272
|
|
|
273
|
+
### 0.4.0
|
|
274
|
+
|
|
275
|
+
- `Tcat::Query#status_code`, `#history`, `#latest_status` now accept an optional tracking-number argument, mirroring `Tcat::WorkerClient`'s shape
|
|
276
|
+
- `Tcat::Query.new` may be called without a tracking number for stateless reuse: `Tcat::Query.new.status_code('1234567890')`
|
|
277
|
+
- Existing one-shot construction (`Tcat::Query.new(tn).status_code`) is still supported
|
|
278
|
+
|
|
279
|
+
### 0.3.5
|
|
280
|
+
|
|
281
|
+
- Added optional `worker_url` and `worker_token` to `Tcat.configure`; explicit `Tcat::WorkerClient.new(url, token:)` arguments still take precedence
|
|
282
|
+
- `Tcat::WorkerClient.new` now accepts no arguments when `worker_url` is set in configuration
|
|
283
|
+
|
|
284
|
+
### 0.3.4
|
|
285
|
+
|
|
286
|
+
- Bundled the 0.3.3 fixes for release on RubyGems
|
|
287
|
+
|
|
288
|
+
### 0.3.3
|
|
289
|
+
|
|
290
|
+
- Worker now subtracts the Taiwan UTC+8 offset when emitting ISO timestamps (previously every event was reported 8 hours later than reality)
|
|
291
|
+
- Worker forwards only the `name=value` portion of `Set-Cookie` to the upstream `Cookie:` header (RFC 6265)
|
|
292
|
+
- Worker rejects `/query` with HTTP 401 when `AUTH_TOKEN` is not configured (fail-closed)
|
|
293
|
+
- 5xx responses no longer leak `error.message`; details are written to `console.error` (visible via `wrangler tail`)
|
|
294
|
+
|
|
295
|
+
### 0.3.2
|
|
296
|
+
|
|
297
|
+
- Added optional Bearer token auth to the Cloudflare Worker (`AUTH_TOKEN` secret)
|
|
298
|
+
- `Tcat::WorkerClient` accepts a `token:` keyword arg that is sent as `Authorization: Bearer <token>`
|
|
299
|
+
- Worker compares tokens in constant time; CORS allow-headers gain `Authorization`
|
|
300
|
+
|
|
301
|
+
### 0.3.1
|
|
302
|
+
|
|
303
|
+
- Tightened `tcat.gemspec` so the published gem no longer bundles the `worker/` subproject or dev-only configs
|
|
304
|
+
|
|
305
|
+
### 0.3.0
|
|
306
|
+
|
|
307
|
+
- Added `Tcat::WorkerClient` to query a Cloudflare Worker proxy instead of T-Cat directly
|
|
308
|
+
- Added new status mapping: `:store_delivery` for `轉交超商配達` (handed over to convenience store)
|
|
309
|
+
- Added the `worker/` Cloudflare Worker subproject (separate from the gem release)
|
|
310
|
+
|
|
133
311
|
### 0.2.2
|
|
134
312
|
|
|
135
313
|
- Fixed status parsing to handle HTML tags in API responses
|
data/lib/tcat/configuration.rb
CHANGED
|
@@ -3,11 +3,18 @@
|
|
|
3
3
|
module Tcat
|
|
4
4
|
# Configuration class handles settings for Tcat
|
|
5
5
|
class Configuration
|
|
6
|
+
# T-Cat API credentials (used by Tcat::Query)
|
|
6
7
|
attr_accessor :secret_string, :secret_key
|
|
7
8
|
|
|
9
|
+
# Cloudflare Worker proxy settings (used by Tcat::WorkerClient)
|
|
10
|
+
# Both are optional; explicit constructor args take precedence.
|
|
11
|
+
attr_accessor :worker_url, :worker_token
|
|
12
|
+
|
|
8
13
|
def initialize
|
|
9
14
|
@secret_string = nil
|
|
10
15
|
@secret_key = nil
|
|
16
|
+
@worker_url = nil
|
|
17
|
+
@worker_token = nil
|
|
11
18
|
end
|
|
12
19
|
end
|
|
13
20
|
end
|
data/lib/tcat/query.rb
CHANGED
|
@@ -11,7 +11,11 @@ module Tcat
|
|
|
11
11
|
class Query
|
|
12
12
|
DeliveryItem = Struct.new(:status, :status_code, :time, :office, :last_update, keyword_init: true)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# @param tracking_number [String, nil] Default tracking number used when
|
|
15
|
+
# the per-call methods are invoked without an explicit argument. Pass nil
|
|
16
|
+
# to construct a stateless client and supply the tracking number on each
|
|
17
|
+
# call (matching Tcat::WorkerClient's shape).
|
|
18
|
+
def initialize(tracking_number = nil)
|
|
15
19
|
@secret_string = Tcat.configuration.secret_string
|
|
16
20
|
@secret_key = Tcat.configuration.secret_key
|
|
17
21
|
validate_secrets!
|
|
@@ -25,22 +29,27 @@ module Tcat
|
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
# Get current delivery status code
|
|
32
|
+
# @param tracking_number [String, nil] Overrides the constructor value.
|
|
28
33
|
# @return [Symbol] Status code (:done, :delivering, :collected, :in_transit, :unknown)
|
|
29
|
-
def status_code
|
|
30
|
-
|
|
34
|
+
def status_code(tracking_number = nil)
|
|
35
|
+
tn = resolve_tracking_number(tracking_number)
|
|
36
|
+
response_body = @http_client.post(data(tn))
|
|
31
37
|
parse_status_code(response_body) if response_body
|
|
32
|
-
rescue HttpClient::RequestError
|
|
38
|
+
rescue HttpClient::RequestError
|
|
33
39
|
# Log error or handle it appropriately
|
|
34
40
|
nil
|
|
35
41
|
end
|
|
36
42
|
|
|
37
43
|
# Get complete delivery history
|
|
44
|
+
# @param tracking_number [String, nil] Overrides the constructor value.
|
|
38
45
|
# @return [Array<DeliveryItem>] Array of delivery status items, sorted by time (newest first)
|
|
39
|
-
def history
|
|
40
|
-
|
|
46
|
+
def history(tracking_number = nil)
|
|
47
|
+
tn = resolve_tracking_number(tracking_number)
|
|
48
|
+
response_body = @http_client.post(data(tn))
|
|
41
49
|
if response_body
|
|
42
50
|
result = Ox.load(response_body, mode: :hash, with_cdata: true)
|
|
43
51
|
return [] if result.dig(:Result, :Status) != '0'
|
|
52
|
+
|
|
44
53
|
extract_delivery_history(result)
|
|
45
54
|
end
|
|
46
55
|
rescue StandardError => e
|
|
@@ -49,10 +58,12 @@ module Tcat
|
|
|
49
58
|
end
|
|
50
59
|
|
|
51
60
|
# Get latest delivery status with details
|
|
61
|
+
# @param tracking_number [String, nil] Overrides the constructor value.
|
|
52
62
|
# @return [DeliveryItem, nil] Latest delivery status or nil if no history
|
|
53
|
-
def latest_status
|
|
54
|
-
items = history
|
|
63
|
+
def latest_status(tracking_number = nil)
|
|
64
|
+
items = history(tracking_number)
|
|
55
65
|
return nil if items.empty?
|
|
66
|
+
|
|
56
67
|
items.first
|
|
57
68
|
end
|
|
58
69
|
|
|
@@ -75,9 +86,16 @@ module Tcat
|
|
|
75
86
|
warn e.backtrace.first(5).join("\n") if $DEBUG
|
|
76
87
|
end
|
|
77
88
|
|
|
78
|
-
def
|
|
89
|
+
def resolve_tracking_number(arg)
|
|
90
|
+
tn = arg || @tracking_number
|
|
91
|
+
raise ArgumentError, 'tracking number required' if tn.nil? || tn.to_s.empty?
|
|
92
|
+
|
|
93
|
+
tn
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def data(tracking_number)
|
|
79
97
|
{
|
|
80
|
-
ConsignmentNo:
|
|
98
|
+
ConsignmentNo: tracking_number,
|
|
81
99
|
f: 5,
|
|
82
100
|
isForeign: 'N',
|
|
83
101
|
secret: generate_secret,
|
|
@@ -193,7 +211,8 @@ module Tcat
|
|
|
193
211
|
'搬家(調查處理中)' => :investigation,
|
|
194
212
|
'拒收(調查處理中)' => :investigation,
|
|
195
213
|
'拒收' => :rejected,
|
|
196
|
-
'客樂得貨物退回中' => :returning
|
|
214
|
+
'客樂得貨物退回中' => :returning,
|
|
215
|
+
'轉交超商配達' => :store_delivery
|
|
197
216
|
}.freeze
|
|
198
217
|
|
|
199
218
|
def parse_status_message(statuses)
|
data/lib/tcat/version.rb
CHANGED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Tcat
|
|
8
|
+
# WorkerClient provides a simple interface to query T-Cat tracking via Cloudflare Worker
|
|
9
|
+
# This is an alternative to the direct Query class that goes through your deployed Worker
|
|
10
|
+
class WorkerClient
|
|
11
|
+
class WorkerError < StandardError; end
|
|
12
|
+
class NetworkError < WorkerError; end
|
|
13
|
+
class APIError < WorkerError; end
|
|
14
|
+
|
|
15
|
+
DeliveryItem = Struct.new(:status, :status_code, :time, :office, :last_update, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
attr_reader :worker_url
|
|
18
|
+
|
|
19
|
+
# Initialize a new WorkerClient
|
|
20
|
+
# @param worker_url [String, nil] The URL of your deployed Cloudflare
|
|
21
|
+
# Worker. Falls back to `Tcat.configuration.worker_url` when omitted.
|
|
22
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
23
|
+
# @param token [String, nil] Bearer token sent in the Authorization
|
|
24
|
+
# header when the Worker is configured with AUTH_TOKEN. Falls back to
|
|
25
|
+
# `Tcat.configuration.worker_token` when omitted.
|
|
26
|
+
def initialize(worker_url = nil, timeout: 30, token: nil)
|
|
27
|
+
url = worker_url || Tcat.configuration.worker_url
|
|
28
|
+
raise ArgumentError, 'Worker URL must be configured' if url.nil? || url.empty?
|
|
29
|
+
|
|
30
|
+
@worker_url = url.chomp('/')
|
|
31
|
+
@timeout = timeout
|
|
32
|
+
@token = token || Tcat.configuration.worker_token
|
|
33
|
+
validate_url!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Query a tracking number through the Worker
|
|
37
|
+
# @param tracking_number [String] The tracking number to query
|
|
38
|
+
# @return [Hash] Parsed response from the Worker
|
|
39
|
+
def query(tracking_number)
|
|
40
|
+
raise ArgumentError, 'Tracking number cannot be nil or empty' if tracking_number.nil? || tracking_number.empty?
|
|
41
|
+
|
|
42
|
+
uri = URI("#{@worker_url}/query")
|
|
43
|
+
uri.query = URI.encode_www_form(no: tracking_number)
|
|
44
|
+
|
|
45
|
+
response = make_request(uri)
|
|
46
|
+
parse_response(response)
|
|
47
|
+
rescue SocketError, Net::HTTPError => e
|
|
48
|
+
raise NetworkError, "Network error: #{e.message}"
|
|
49
|
+
rescue JSON::ParserError => e
|
|
50
|
+
raise APIError, "Invalid JSON response: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get the current status code for a tracking number
|
|
54
|
+
# @param tracking_number [String] The tracking number to query
|
|
55
|
+
# @return [Symbol, nil] Status code symbol or nil if error
|
|
56
|
+
def status_code(tracking_number)
|
|
57
|
+
result = query(tracking_number)
|
|
58
|
+
result[:status_code]&.to_sym
|
|
59
|
+
rescue WorkerError => e
|
|
60
|
+
warn "Error getting status: #{e.message}" if $DEBUG
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get the complete delivery history
|
|
65
|
+
# @param tracking_number [String] The tracking number to query
|
|
66
|
+
# @return [Array<DeliveryItem>] Array of delivery history items
|
|
67
|
+
def history(tracking_number)
|
|
68
|
+
result = query(tracking_number)
|
|
69
|
+
items = result[:items] || []
|
|
70
|
+
items.map { |item| parse_delivery_item(item) }
|
|
71
|
+
rescue WorkerError => e
|
|
72
|
+
warn "Error getting history: #{e.message}" if $DEBUG
|
|
73
|
+
[]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Get the latest delivery status
|
|
77
|
+
# @param tracking_number [String] The tracking number to query
|
|
78
|
+
# @return [DeliveryItem, nil] Latest delivery item or nil
|
|
79
|
+
def latest_status(tracking_number)
|
|
80
|
+
result = query(tracking_number)
|
|
81
|
+
return nil unless result[:latest]
|
|
82
|
+
|
|
83
|
+
parse_delivery_item(result[:latest])
|
|
84
|
+
rescue WorkerError => e
|
|
85
|
+
warn "Error getting latest status: #{e.message}" if $DEBUG
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Check if the Worker is healthy
|
|
90
|
+
# @return [Boolean] true if Worker responds to health check
|
|
91
|
+
def healthy?
|
|
92
|
+
uri = URI("#{@worker_url}/health")
|
|
93
|
+
response = make_request(uri)
|
|
94
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
95
|
+
data[:status] == 'ok'
|
|
96
|
+
rescue StandardError
|
|
97
|
+
false
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def validate_url!
|
|
103
|
+
uri = URI.parse(@worker_url)
|
|
104
|
+
unless %w[http https].include?(uri.scheme)
|
|
105
|
+
raise ArgumentError, 'Invalid Worker URL: must be http or https'
|
|
106
|
+
end
|
|
107
|
+
rescue URI::InvalidURIError => e
|
|
108
|
+
raise ArgumentError, "Invalid Worker URL: #{e.message}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def make_request(uri)
|
|
112
|
+
response = setup_http(uri).request(build_request(uri))
|
|
113
|
+
raise APIError, "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
|
114
|
+
|
|
115
|
+
response
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def setup_http(uri)
|
|
119
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
120
|
+
http.use_ssl = uri.scheme == 'https'
|
|
121
|
+
http.read_timeout = @timeout
|
|
122
|
+
http.open_timeout = @timeout
|
|
123
|
+
http
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def build_request(uri)
|
|
127
|
+
request = Net::HTTP::Get.new(uri)
|
|
128
|
+
request['Accept'] = 'application/json'
|
|
129
|
+
request['Authorization'] = "Bearer #{@token}" if @token
|
|
130
|
+
request
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def parse_response(response)
|
|
134
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
135
|
+
|
|
136
|
+
if data[:status] == 'error'
|
|
137
|
+
raise APIError, data[:message] || 'Unknown error from Worker'
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
data
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def parse_delivery_item(item)
|
|
144
|
+
DeliveryItem.new(
|
|
145
|
+
status: item[:status],
|
|
146
|
+
status_code: item[:status_code]&.to_sym,
|
|
147
|
+
time: parse_time(item[:time]),
|
|
148
|
+
office: item[:office],
|
|
149
|
+
last_update: parse_time(item[:last_update])
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def parse_time(time_str)
|
|
154
|
+
return nil if time_str.nil? || time_str.empty?
|
|
155
|
+
|
|
156
|
+
Time.parse(time_str)
|
|
157
|
+
rescue ArgumentError
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
data/lib/tcat.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tcat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zac
|
|
@@ -60,18 +60,15 @@ executables: []
|
|
|
60
60
|
extensions: []
|
|
61
61
|
extra_rdoc_files: []
|
|
62
62
|
files:
|
|
63
|
-
- ".rspec"
|
|
64
|
-
- ".rubocop.yml"
|
|
65
|
-
- ".tool-versions"
|
|
66
63
|
- LICENSE.txt
|
|
67
64
|
- README.md
|
|
68
|
-
- Rakefile
|
|
69
65
|
- lib/tcat.rb
|
|
70
66
|
- lib/tcat/configuration.rb
|
|
71
67
|
- lib/tcat/encryption_service.rb
|
|
72
68
|
- lib/tcat/http_client.rb
|
|
73
69
|
- lib/tcat/query.rb
|
|
74
70
|
- lib/tcat/version.rb
|
|
71
|
+
- lib/tcat/worker_client.rb
|
|
75
72
|
- sig/tcat.rbs
|
|
76
73
|
homepage: https://rubygems.org/gems/tcat
|
|
77
74
|
licenses:
|
|
@@ -92,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
92
89
|
- !ruby/object:Gem::Version
|
|
93
90
|
version: '0'
|
|
94
91
|
requirements: []
|
|
95
|
-
rubygems_version:
|
|
92
|
+
rubygems_version: 4.0.6
|
|
96
93
|
specification_version: 4
|
|
97
94
|
summary: A Ruby gem for tracking packages using the Tcat system.
|
|
98
95
|
test_files: []
|
data/.rspec
DELETED
data/.rubocop.yml
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
AllCops:
|
|
2
|
-
TargetRubyVersion: 2.7
|
|
3
|
-
Exclude:
|
|
4
|
-
- 'bin/*'
|
|
5
|
-
|
|
6
|
-
# Style/StringLiterals:
|
|
7
|
-
# Enabled: true
|
|
8
|
-
# EnforcedStyle: double_quotes
|
|
9
|
-
|
|
10
|
-
# Style/StringLiteralsInInterpolation:
|
|
11
|
-
# Enabled: true
|
|
12
|
-
# EnforcedStyle: double_quotes
|
|
13
|
-
|
|
14
|
-
Metrics/AbcSize:
|
|
15
|
-
Exclude:
|
|
16
|
-
- 'lib/tcat/query.rb'
|
|
17
|
-
|
|
18
|
-
Metrics/MethodLength:
|
|
19
|
-
Exclude:
|
|
20
|
-
- 'lib/tcat/query.rb'
|
|
21
|
-
|
|
22
|
-
Layout/LineLength:
|
|
23
|
-
Exclude:
|
|
24
|
-
- 'lib/tcat/query.rb'
|
|
25
|
-
- tcat.gemspec
|
data/.tool-versions
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
ruby 3.3.8
|