tcat 0.2.2 → 0.3.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85733c0ed940c2fad6b032f5fed76ddc64dfacba3b3789294d60c8937f90dfdc
4
- data.tar.gz: 37fbf765f958b1111eb19e02eb0c7e9bfd7298a74f64971505aa238fd64ead3f
3
+ metadata.gz: f40a1e40fc125273e5928860f2afa18f22ad88a22a2b02d63f441b9197b0153d
4
+ data.tar.gz: 6e9d0c2f7bf65f9cc81a9764761092b76dce77faedc57836f3d9f4866b327b00
5
5
  SHA512:
6
- metadata.gz: 1ecf263b82fd1e236ed577d9d2be12e73b4a708b3e6e98ca3861817a82b3955c60acc2a5a389a2b8d925c298e0f46922e37f9000449718a40e5d80d2507be1c3
7
- data.tar.gz: 19bbb76b59696fe11a330d625c371383cc5e01d42dab4314182987597dccc850bd3504cd3c50891fbcb77fbc286d01d3a315794dcba0801dca4b953409c2ad27
6
+ metadata.gz: c402bffc3cc32eb481d930958107d08e278b362390cc4d6f62bbe9b8357ebc5501e74dfce68ca1326040a32ef9bc51274b8853c9b57e6cd7b0b98ca19c52cbf4
7
+ data.tar.gz: 83fb6ec4ec1e9731fbd7d8fc0288221676afd14904101c3063304ae687cb54cc83112a9baf28eb616251d3618452976e0e7bc6ba284ef49d3a49d20b19a4b6e9
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,42 @@ Or install it yourself as:
30
47
  $ gem install tcat
31
48
  ```
32
49
 
33
- ## Configuration
50
+ ## Usage
34
51
 
35
- Configure the gem before use:
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
-
46
- ### Basic Usage
47
66
 
48
- ```ruby
49
67
  # Create a query instance
50
68
  query = Tcat::Query.new('your_tracking_number')
51
69
 
52
70
  # Get shipment status
53
71
  status = query.status_code
54
72
  # Returns one of the following:
55
- # :done - Successfully delivered
56
- # :delivering - Out for delivery
57
- # :collected - Package collected
58
- # :in_transit - In transit
59
- # :returned - Return completed
60
- # :held - Held at post office
61
- # :rescheduled - Delivery time rescheduled
62
- # :forwarding - Being forwarded
63
- # :investigation - Under investigation
64
- # :rejected - Delivery rejected
65
- # :returning - In return process
66
- # :unknown - Unknown status
73
+ # :done - Successfully delivered
74
+ # :delivering - Out for delivery
75
+ # :collected - Package collected
76
+ # :in_transit - In transit
77
+ # :returned - Return completed
78
+ # :held - Held at post office
79
+ # :rescheduled - Delivery time rescheduled
80
+ # :forwarding - Being forwarded
81
+ # :investigation - Under investigation
82
+ # :rejected - Delivery rejected
83
+ # :returning - In return process
84
+ # :store_delivery - At convenience store for pickup
85
+ # :unknown - Unknown status
67
86
 
68
87
  # Get latest status details
69
88
  latest = query.latest_status
@@ -85,6 +104,84 @@ history.each do |item|
85
104
  end
86
105
  ```
87
106
 
107
+ ### Option 2: Via Cloudflare Worker (Recommended)
108
+
109
+ The Worker approach is recommended for production because:
110
+ - ✅ Secrets are stored securely in Cloudflare, not in your application
111
+ - ✅ Faster response times via Cloudflare's edge network
112
+ - ✅ No need to manage encryption in your Ruby app
113
+ - ✅ Can be used by any language/platform (not just Ruby)
114
+
115
+ First, deploy the Cloudflare Worker (see [worker/README.md](worker/README.md) for deployment instructions).
116
+
117
+ Then use the WorkerClient in your Ruby application. You can pass the Worker
118
+ URL (and optional Bearer token) directly, or configure them globally via
119
+ `Tcat.configure`:
120
+
121
+ ```ruby
122
+ # Option A: explicit per-instance arguments
123
+ client = Tcat::WorkerClient.new(
124
+ 'https://your-worker.workers.dev',
125
+ token: ENV['TCAT_WORKER_TOKEN'] # only needed when AUTH_TOKEN is set on the Worker
126
+ )
127
+
128
+ # Option B: global configuration, then construct without arguments
129
+ Tcat.configure do |config|
130
+ config.worker_url = 'https://your-worker.workers.dev'
131
+ config.worker_token = ENV['TCAT_WORKER_TOKEN']
132
+ end
133
+ client = Tcat::WorkerClient.new
134
+
135
+ # Explicit args always override configuration:
136
+ override = Tcat::WorkerClient.new('https://other.workers.dev', token: 'other-token')
137
+
138
+ # Get shipment status (same API as Query)
139
+ status = client.status_code('your_tracking_number')
140
+ # => :delivering
141
+
142
+ # Get latest status details
143
+ latest = client.latest_status('your_tracking_number')
144
+ if latest
145
+ puts "Status: #{latest.status}" # e.g. "配送中"
146
+ puts "Status code: #{latest.status_code}" # e.g. :delivering
147
+ puts "Time: #{latest.time}" # Time object
148
+ puts "Office: #{latest.office}" # e.g. "台北營業所"
149
+ end
150
+
151
+ # Get full shipment history
152
+ history = client.history('your_tracking_number')
153
+ history.each do |item|
154
+ puts "Status: #{item.status}"
155
+ puts "Time: #{item.time}"
156
+ puts "Office: #{item.office}"
157
+ puts "---"
158
+ end
159
+
160
+ # Check if Worker is healthy
161
+ if client.healthy?
162
+ puts "Worker is operational"
163
+ end
164
+ ```
165
+
166
+ **Custom timeout:**
167
+
168
+ ```ruby
169
+ # Default timeout is 30 seconds
170
+ client = Tcat::WorkerClient.new('https://your-worker.workers.dev', timeout: 60)
171
+ ```
172
+
173
+ **Error handling:**
174
+
175
+ ```ruby
176
+ begin
177
+ status = client.status_code('tracking_number')
178
+ rescue Tcat::WorkerClient::NetworkError => e
179
+ puts "Network error: #{e.message}"
180
+ rescue Tcat::WorkerClient::APIError => e
181
+ puts "API error: #{e.message}"
182
+ end
183
+ ```
184
+
88
185
  ### Status Code Explanation
89
186
 
90
187
  - `:done` - Successfully delivered
@@ -98,20 +195,52 @@ end
98
195
  - `:investigation` - Package is under investigation (e.g., address change, rejection)
99
196
  - `:rejected` - Delivery was rejected
100
197
  - `:returning` - Package is in return process
198
+ - `:store_delivery` - Handed over to a convenience store, awaiting recipient pickup
101
199
  - `:unknown` - Unknown status
102
200
 
103
- ## Development
201
+ ## Cloudflare Worker
104
202
 
105
- 1. Fork the project
106
- 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
107
- 3. Commit your changes (`git commit -am 'Add some amazing feature'`)
108
- 4. Push to the branch (`git push origin feature/amazing-feature`)
109
- 5. Create a Pull Request
203
+ This repository includes a Cloudflare Worker implementation that provides an HTTP API for T-Cat tracking.
204
+
205
+ ### Features
206
+
207
+ - 🌍 Deploy globally on Cloudflare's edge network
208
+ - 🔒 Securely store API credentials as Worker secrets
209
+ - 🚀 Fast response times from edge locations
210
+ - 🌐 CORS enabled for frontend applications
211
+ - 📱 Works with any programming language
212
+
213
+ ### Quick Start
214
+
215
+ ```bash
216
+ cd worker
217
+ npm install
218
+ npm run dev # Start local development server
219
+ ```
220
+
221
+ For full deployment instructions, see [worker/README.md](worker/README.md) and [worker/DEPLOYMENT.md](worker/DEPLOYMENT.md).
222
+
223
+ ## Comparison: Direct API vs Worker
224
+
225
+ | Feature | Direct API (`Query`) | Cloudflare Worker (`WorkerClient`) |
226
+ |---------|---------------------|-----------------------------------|
227
+ | Setup complexity | Low (just gem install) | Medium (requires Worker deployment) |
228
+ | Security | Secrets in your app | Secrets in Cloudflare |
229
+ | Performance | Direct to T-Cat API | Via Cloudflare edge |
230
+ | Multi-platform | Ruby only | Any language/platform |
231
+ | Cost | Free | Free tier available |
232
+ | Best for | Simple scripts, internal tools | Production apps, public APIs |
233
+
234
+ ## Development
110
235
 
111
236
  ### Running Tests
112
237
 
113
238
  ```bash
239
+ # Run all tests
114
240
  $ bundle exec rake spec
241
+
242
+ # Run specific test file
243
+ $ bundle exec rspec spec/tcat/worker_client_spec.rb
115
244
  ```
116
245
 
117
246
  ### Local Installation
@@ -124,12 +253,50 @@ $ bundle exec rake install
124
253
 
125
254
  Bug reports and pull requests are welcome.
126
255
 
256
+ 1. Fork the project
257
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
258
+ 3. Commit your changes (`git commit -am 'Add some amazing feature'`)
259
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
260
+ 5. Create a Pull Request
261
+
127
262
  ## License
128
263
 
129
264
  This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
130
265
 
131
266
  ## Changelog
132
267
 
268
+ ### 0.3.5
269
+
270
+ - Added optional `worker_url` and `worker_token` to `Tcat.configure`; explicit `Tcat::WorkerClient.new(url, token:)` arguments still take precedence
271
+ - `Tcat::WorkerClient.new` now accepts no arguments when `worker_url` is set in configuration
272
+
273
+ ### 0.3.4
274
+
275
+ - Bundled the 0.3.3 fixes for release on RubyGems
276
+
277
+ ### 0.3.3
278
+
279
+ - Worker now subtracts the Taiwan UTC+8 offset when emitting ISO timestamps (previously every event was reported 8 hours later than reality)
280
+ - Worker forwards only the `name=value` portion of `Set-Cookie` to the upstream `Cookie:` header (RFC 6265)
281
+ - Worker rejects `/query` with HTTP 401 when `AUTH_TOKEN` is not configured (fail-closed)
282
+ - 5xx responses no longer leak `error.message`; details are written to `console.error` (visible via `wrangler tail`)
283
+
284
+ ### 0.3.2
285
+
286
+ - Added optional Bearer token auth to the Cloudflare Worker (`AUTH_TOKEN` secret)
287
+ - `Tcat::WorkerClient` accepts a `token:` keyword arg that is sent as `Authorization: Bearer <token>`
288
+ - Worker compares tokens in constant time; CORS allow-headers gain `Authorization`
289
+
290
+ ### 0.3.1
291
+
292
+ - Tightened `tcat.gemspec` so the published gem no longer bundles the `worker/` subproject or dev-only configs
293
+
294
+ ### 0.3.0
295
+
296
+ - Added `Tcat::WorkerClient` to query a Cloudflare Worker proxy instead of T-Cat directly
297
+ - Added new status mapping: `:store_delivery` for `轉交超商配達` (handed over to convenience store)
298
+ - Added the `worker/` Cloudflare Worker subproject (separate from the gem release)
299
+
133
300
  ### 0.2.2
134
301
 
135
302
  - Fixed status parsing to handle HTML tags in API responses
@@ -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
@@ -193,7 +193,8 @@ module Tcat
193
193
  '搬家(調查處理中)' => :investigation,
194
194
  '拒收(調查處理中)' => :investigation,
195
195
  '拒收' => :rejected,
196
- '客樂得貨物退回中' => :returning
196
+ '客樂得貨物退回中' => :returning,
197
+ '轉交超商配達' => :store_delivery
197
198
  }.freeze
198
199
 
199
200
  def parse_status_message(statuses)
data/lib/tcat/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tcat
4
- VERSION = '0.2.2'
4
+ VERSION = '0.3.5'
5
5
  end
@@ -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
@@ -3,6 +3,7 @@
3
3
  require 'tcat/configuration'
4
4
  require_relative 'tcat/version'
5
5
  require_relative 'tcat/query'
6
+ require_relative 'tcat/worker_client'
6
7
 
7
8
  # Tcat module provides functionality for tracking packages
8
9
  module Tcat
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.2.2
4
+ version: 0.3.5
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: 3.6.9
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
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
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
data/Rakefile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- require 'rubocop/rake_task'
9
-
10
- RuboCop::RakeTask.new
11
-
12
- task default: %i[spec rubocop]