njtransit 1.1.4 → 1.2.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/CHANGELOG.md +27 -0
- data/CLAUDE.md +2 -0
- data/README.md +18 -0
- data/docs/index.md +52 -0
- data/lib/njtransit/client.rb +59 -8
- data/lib/njtransit/configuration.rb +8 -2
- data/lib/njtransit/token_stores.rb +114 -0
- data/lib/njtransit/version.rb +1 -1
- data/lib/njtransit.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 034142d76cecb98cbd1b456d75052081a766217e684d249e2f4a2541db35ff07
|
|
4
|
+
data.tar.gz: b9a8c8209fbb6602bf6b10daa4594a8c59ba1a843c48aa8ea63ee79104fdf286
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3114f42fa2517def6d6eba2b1f58059a9d21825d6993a730804899538f2627384e118b391e46988e5e0dee7120b2eede76adabb69f495c2c6d172e47427e48d2
|
|
7
|
+
data.tar.gz: 85929dbf8bfb5d3f5bf879973d1aca00e9e8f82e6953be6d6853f4bfaba83db2f2ed43b8de3198c70e078d791942b68706a7c0367450d1a2c3862c1b2afe197a
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.2.0] - 2026-11-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Pluggable auth token store (`config.token_store` / `Client.new(token_store:)`): any object with `read`, `write` and `delete`, including `Rails.cache`. Lets long-running processes reuse a token across restarts instead of hitting NJ Transit's daily token cap
|
|
12
|
+
- `NJTransit::TokenStores::Memory` (the default, shared per process) and `NJTransit::TokenStores::File` (JSON file, mode 0600, atomic writes under an exclusive `flock`, safe across processes)
|
|
13
|
+
- Assigning a `token_store` without `read`/`write`/`delete` raises `ArgumentError`
|
|
14
|
+
- `Client#token_key` and `Client.default_token_store`
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Tokens are keyed by API host and username (`njtransit:token:<base_url>:<username>`), not host alone
|
|
19
|
+
- Raw GTFS-RT requests now detect an expired token (a JSON `Invalid token.` body, or a 401/403), re-authenticate and retry once, as JSON requests already did. A JSON error body on a feed now raises `APIError` instead of being returned as feed bytes
|
|
20
|
+
- Clearing an expired token deletes the stored copy only if it is still the stale one, so clients sharing a store don't discard each other's fresh tokens
|
|
21
|
+
- A failing token store always warns on stderr (regardless of `log_level`) and is bypassed instead of raising
|
|
22
|
+
|
|
23
|
+
### Deprecated
|
|
24
|
+
|
|
25
|
+
- `Client.token_cache`: use `Client.default_token_store` or configure `token_store`
|
|
26
|
+
|
|
27
|
+
## [1.1.5] - 2026-11-14
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- GitHub Pages landing page (`docs/index.md`); https://jayravaliya.com/njtransit/ no longer 404s
|
|
32
|
+
- `CLAUDE.md` Documentation Sync row for the landing page
|
|
33
|
+
|
|
7
34
|
## [1.1.4] - 2026-04-26
|
|
8
35
|
|
|
9
36
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -12,6 +12,7 @@ Any change that affects public behavior, configuration, dependencies, or the rel
|
|
|
12
12
|
|---|---|
|
|
13
13
|
| New or changed public API (method signature, return shape, error class raised) | `README.md` (capability section + relevant examples), `CHANGELOG.md` |
|
|
14
14
|
| New resource class or API host | `CLAUDE.md` Architecture section, `README.md` "Two Clients" table, `CHANGELOG.md` |
|
|
15
|
+
| Quick Start, install or headline capabilities | `README.md`, `docs/index.md` (the GitHub Pages landing page at jayravaliya.com/njtransit) |
|
|
15
16
|
| New environment variable | `README.md` "Environment Variables" table, `CHANGELOG.md` if user-visible |
|
|
16
17
|
| GTFS schema or data-layer changes | `CLAUDE.md` "GTFS Static Layer" section, `README.md` "GTFS Static Data" section if it surfaces in usage |
|
|
17
18
|
| New rake task | `README.md` (where rake tasks are listed), `CHANGELOG.md` |
|
|
@@ -55,6 +56,7 @@ SQLite-backed offline schedule data. Import from GTFS zip, then query via `NJTra
|
|
|
55
56
|
- **`enrich` flag**: Bus API methods default to `enrich: true`, which joins GTFS static data (lat/lon, route names). Use `enrich: false` if GTFS isn't imported or you don't need it.
|
|
56
57
|
- **`mode` parameter**: Bus methods default to `mode: "BUS"`. Pass `"HBLR"`, `"NLR"`, `"RL"`, or `"ALL"` for light rail.
|
|
57
58
|
- **`stops_nearby` radius**: The radius parameter is in **feet**, not miles.
|
|
59
|
+
- **Token store**: Auth tokens go through a pluggable `token_store` (`lib/njtransit/token_stores.rb`): any object with `read`/`write`/`delete`, such as `TokenStores::File` or `Rails.cache`. The default is one shared `TokenStores::Memory` per process. Expired tokens are deleted from the store and the request is retried once, on both the JSON and raw (GTFS-RT) paths.
|
|
58
60
|
- **Raw responses**: GTFS-RT methods (`bus_gtfs`, `rail_gtfs`) return binary protobuf data via `post_form_raw`.
|
|
59
61
|
|
|
60
62
|
## Testing
|
data/README.md
CHANGED
|
@@ -61,6 +61,24 @@ client.bus.stops_nearby(lat: 40.878, lon: -74.221, radius: 2000, enrich: false)
|
|
|
61
61
|
client.bus.routes(mode: "HBLR") # Hudson-Bergen Light Rail
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
## Keeping Auth Tokens Across Restarts
|
|
65
|
+
|
|
66
|
+
NJ Transit caps how many auth tokens you can create per day (as low as 5–10 on some endpoints). By default the gem caches tokens in memory, so every new process authenticates again. A service that restarts often (deploys, crash loops, containers) can hit the cap and be locked out for the rest of the day.
|
|
67
|
+
|
|
68
|
+
Configure a `token_store` to reuse tokens across restarts:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
NJTransit.configure do |config|
|
|
72
|
+
# A JSON file (created with mode 0600)
|
|
73
|
+
config.token_store = NJTransit::TokenStores::File.new("tmp/njtransit_tokens.json")
|
|
74
|
+
|
|
75
|
+
# ...or, in Rails, any ActiveSupport cache (persists if its backend does)
|
|
76
|
+
config.token_store = Rails.cache
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A token store is any object with `read(key)`, `write(key, token)` and `delete(key)`; anything else raises `ArgumentError` when you assign it. Tokens are keyed by API host and username, so clients and processes sharing a store share one token per account. When the API reports an expired token (a JSON `Invalid token.` error, or a 401/403 on a GTFS-RT feed), the gem deletes it from the store (unless another client already replaced it), authenticates, stores the new one and retries the request once. If the store itself fails, the gem warns on stderr and falls back to authenticating, so a broken cache never breaks requests. `TokenStores::File` locks a `<path>.lock` file around each write, so several processes can share one file.
|
|
81
|
+
|
|
64
82
|
## Two Clients, One Gem
|
|
65
83
|
|
|
66
84
|
NJ Transit splits its API across two hosts. The gem handles this with two clients:
|
data/docs/index.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: NJTransit Ruby gem
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# NJTransit
|
|
6
|
+
|
|
7
|
+
A Ruby gem for NJ Transit's real-time and schedule data: buses, trains and light rail.
|
|
8
|
+
|
|
9
|
+
[](https://rubygems.org/gems/njtransit)
|
|
10
|
+
|
|
11
|
+
- **Real-time departures** for buses, trains and light rail
|
|
12
|
+
- **Train tracking:** GPS positions, speed and delay for every active train
|
|
13
|
+
- **Full schedules** from GTFS static data, not just the next hour
|
|
14
|
+
- **GTFS-RT feeds:** raw protobuf alerts, trip updates and vehicle positions
|
|
15
|
+
|
|
16
|
+
## Get started
|
|
17
|
+
|
|
18
|
+
Register at [developer.njtransit.com](https://developer.njtransit.com/registration) for API credentials, then:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Gemfile
|
|
22
|
+
gem 'njtransit'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require 'njtransit'
|
|
27
|
+
|
|
28
|
+
NJTransit.configure do |config|
|
|
29
|
+
config.username = ENV['NJTRANSIT_USERNAME']
|
|
30
|
+
config.password = ENV['NJTRANSIT_PASSWORD']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Next trains from NY Penn Station
|
|
34
|
+
NJTransit.rail_client.rail.train_schedule_19(station: "NY")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Documentation
|
|
38
|
+
|
|
39
|
+
- [README](https://github.com/jayrav13/njtransit#readme): every capability, both clients, GTFS static data, environment variables and the Claude Code skill
|
|
40
|
+
- [CHANGELOG](https://github.com/jayrav13/njtransit/blob/main/CHANGELOG.md)
|
|
41
|
+
- [RubyGems](https://rubygems.org/gems/njtransit)
|
|
42
|
+
- [Source on GitHub](https://github.com/jayrav13/njtransit)
|
|
43
|
+
|
|
44
|
+
## Design plans
|
|
45
|
+
|
|
46
|
+
- [Gem design](plans/2025-01-24-njtransit-gem-design.md)
|
|
47
|
+
- [Bus API design](plans/2026-01-24-bus-api-design.md)
|
|
48
|
+
- [GTFS loader design](plans/2026-01-24-gtfs-loader-design.md)
|
|
49
|
+
- [GTFS implementation](plans/2026-01-24-gtfs-implementation.md)
|
|
50
|
+
- [Dev infrastructure and agent](superpowers/plans/2026-03-26-dev-infra-and-agent.md)
|
|
51
|
+
|
|
52
|
+
MIT licensed.
|
data/lib/njtransit/client.rb
CHANGED
|
@@ -15,11 +15,12 @@ module NJTransit
|
|
|
15
15
|
class Client
|
|
16
16
|
DEFAULT_AUTH_PATH = "/api/BUSDV2/authenticateUser"
|
|
17
17
|
|
|
18
|
-
attr_reader :username, :password, :log_level, :base_url, :timeout, :auth_path
|
|
18
|
+
attr_reader :username, :password, :log_level, :base_url, :timeout, :auth_path, :token_store
|
|
19
19
|
|
|
20
20
|
def initialize(username:, password:, log_level: "silent", base_url: Configuration::DEFAULT_BASE_URL,
|
|
21
|
-
timeout: Configuration::DEFAULT_TIMEOUT, auth_path: DEFAULT_AUTH_PATH)
|
|
21
|
+
timeout: Configuration::DEFAULT_TIMEOUT, auth_path: DEFAULT_AUTH_PATH, token_store: nil)
|
|
22
22
|
@username = username
|
|
23
|
+
@token_store = token_store ? TokenStores.validate!(token_store) : self.class.default_token_store
|
|
23
24
|
@password = password
|
|
24
25
|
@log_level = log_level
|
|
25
26
|
@base_url = base_url
|
|
@@ -77,7 +78,7 @@ module NJTransit
|
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
def authenticate!
|
|
80
|
-
cached =
|
|
81
|
+
cached = read_stored_token
|
|
81
82
|
if cached
|
|
82
83
|
@token = cached
|
|
83
84
|
return @token
|
|
@@ -95,7 +96,7 @@ module NJTransit
|
|
|
95
96
|
end
|
|
96
97
|
|
|
97
98
|
@token = result["UserToken"]
|
|
98
|
-
|
|
99
|
+
write_stored_token(@token)
|
|
99
100
|
@token
|
|
100
101
|
end
|
|
101
102
|
|
|
@@ -104,17 +105,32 @@ module NJTransit
|
|
|
104
105
|
@token
|
|
105
106
|
end
|
|
106
107
|
|
|
108
|
+
# Forget this client's token. The stored copy is deleted only if it is still the
|
|
109
|
+
# token this client held, so a fresh token another client just stored survives.
|
|
107
110
|
def clear_token!
|
|
111
|
+
stale = @token
|
|
108
112
|
@token = nil
|
|
109
|
-
|
|
113
|
+
store_call(:delete, token_key) if stale.nil? || read_stored_token == stale
|
|
110
114
|
end
|
|
111
115
|
|
|
116
|
+
# Store key for this client's token: one per API host and username.
|
|
117
|
+
def token_key
|
|
118
|
+
"njtransit:token:#{base_url}:#{username}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.default_token_store
|
|
122
|
+
@default_token_store ||= TokenStores::Memory.new
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Deprecated: use default_token_store, or configure a token_store.
|
|
112
126
|
def self.token_cache
|
|
113
|
-
|
|
127
|
+
warn "[njtransit] Client.token_cache is deprecated; use Client.default_token_store " \
|
|
128
|
+
"or configure token_store. Keys are now \"njtransit:token:<base_url>:<username>\"."
|
|
129
|
+
default_token_store.to_h
|
|
114
130
|
end
|
|
115
131
|
|
|
116
132
|
def self.clear_token_cache!
|
|
117
|
-
|
|
133
|
+
default_token_store.clear
|
|
118
134
|
end
|
|
119
135
|
|
|
120
136
|
private
|
|
@@ -159,15 +175,22 @@ module NJTransit
|
|
|
159
175
|
raise ConnectionError, e.message
|
|
160
176
|
end
|
|
161
177
|
|
|
162
|
-
def request_form_raw(method, path, params = {})
|
|
178
|
+
def request_form_raw(method, path, params = {}, retry_auth: true)
|
|
163
179
|
params[:token] = token
|
|
164
180
|
response = raw_connection.public_send(method) do |req|
|
|
165
181
|
req.url(path)
|
|
166
182
|
req.body = params
|
|
167
183
|
end
|
|
168
184
|
|
|
185
|
+
if retry_auth && raw_auth_failure?(response)
|
|
186
|
+
clear_token!
|
|
187
|
+
return request_form_raw(method, path, params, retry_auth: false)
|
|
188
|
+
end
|
|
189
|
+
|
|
169
190
|
raise error_for_status(response.status).new(error_message(response), response: response) unless response.success?
|
|
170
191
|
|
|
192
|
+
# Feeds are protobuf, which never starts with "{": a JSON body is an API error.
|
|
193
|
+
check_api_error!(parse_body(response.body)) if json_body?(response.body)
|
|
171
194
|
response.body
|
|
172
195
|
rescue Faraday::TimeoutError => e
|
|
173
196
|
raise TimeoutError, e.message
|
|
@@ -179,6 +202,34 @@ module NJTransit
|
|
|
179
202
|
result.is_a?(Hash) && result["errorMessage"] == "Invalid token."
|
|
180
203
|
end
|
|
181
204
|
|
|
205
|
+
# An expired token on a feed: a JSON "Invalid token." error, or a 401/403.
|
|
206
|
+
def raw_auth_failure?(response)
|
|
207
|
+
return true if [401, 403].include?(response.status)
|
|
208
|
+
|
|
209
|
+
response.success? && json_body?(response.body) && token_expired?(parse_body(response.body))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def json_body?(body)
|
|
213
|
+
body.is_a?(String) && body.start_with?("{")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def read_stored_token
|
|
217
|
+
store_call(:read, token_key)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def write_stored_token(token)
|
|
221
|
+
store_call(:write, token_key, token)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# A broken store must not break requests: fall back to authenticating, but always
|
|
225
|
+
# warn (whatever log_level is), since each fallback spends a capped token.
|
|
226
|
+
def store_call(method, *)
|
|
227
|
+
token_store.public_send(method, *)
|
|
228
|
+
rescue StandardError => e
|
|
229
|
+
warn "[njtransit] token store #{method} failed: #{e.class}: #{e.message}"
|
|
230
|
+
nil
|
|
231
|
+
end
|
|
232
|
+
|
|
182
233
|
def check_api_error!(result)
|
|
183
234
|
return unless result.is_a?(Hash) && result["errorMessage"]
|
|
184
235
|
|
|
@@ -8,7 +8,7 @@ module NJTransit
|
|
|
8
8
|
DEFAULT_TIMEOUT = 30
|
|
9
9
|
|
|
10
10
|
attr_accessor :username, :password, :base_url, :timeout, :gtfs_database_path
|
|
11
|
-
attr_reader :log_level
|
|
11
|
+
attr_reader :log_level, :token_store
|
|
12
12
|
|
|
13
13
|
def initialize
|
|
14
14
|
@username = ENV.fetch("NJTRANSIT_USERNAME", nil)
|
|
@@ -29,13 +29,19 @@ module NJTransit
|
|
|
29
29
|
@log_level = level
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Any object with read, write and delete (e.g. NJTransit::TokenStores::File, Rails.cache); nil for in-memory.
|
|
33
|
+
def token_store=(store)
|
|
34
|
+
@token_store = store.nil? ? nil : TokenStores.validate!(store)
|
|
35
|
+
end
|
|
36
|
+
|
|
32
37
|
def to_h
|
|
33
38
|
{
|
|
34
39
|
username: username,
|
|
35
40
|
password: password,
|
|
36
41
|
log_level: log_level,
|
|
37
42
|
base_url: base_url,
|
|
38
|
-
timeout: timeout
|
|
43
|
+
timeout: timeout,
|
|
44
|
+
token_store: token_store
|
|
39
45
|
}
|
|
40
46
|
end
|
|
41
47
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
module NJTransit
|
|
8
|
+
# Where clients keep NJ Transit auth tokens. NJ Transit caps token creation
|
|
9
|
+
# (as low as 5-10 a day on some endpoints), so a long-running process that
|
|
10
|
+
# restarts should reuse its token instead of authenticating on every boot.
|
|
11
|
+
#
|
|
12
|
+
# A token store is any object that responds to:
|
|
13
|
+
#
|
|
14
|
+
# read(key) -> token String or nil
|
|
15
|
+
# write(key, token)
|
|
16
|
+
# delete(key)
|
|
17
|
+
#
|
|
18
|
+
# That is the ActiveSupport::Cache::Store interface, so `Rails.cache`
|
|
19
|
+
# works as-is (it persists only if its backend does).
|
|
20
|
+
module TokenStores
|
|
21
|
+
INTERFACE = %i[read write delete].freeze
|
|
22
|
+
|
|
23
|
+
def self.validate!(store)
|
|
24
|
+
missing = INTERFACE.reject { |m| store.respond_to?(m) }
|
|
25
|
+
return store if missing.empty?
|
|
26
|
+
|
|
27
|
+
raise ArgumentError, "token_store must respond to #{INTERFACE.join(", ")} " \
|
|
28
|
+
"(missing #{missing.join(", ")}): got #{store.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Process-local store: tokens are lost when the process exits.
|
|
32
|
+
# Clients share one of these unless configured otherwise.
|
|
33
|
+
class Memory
|
|
34
|
+
def initialize
|
|
35
|
+
@tokens = {}
|
|
36
|
+
@mutex = Mutex.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def read(key)
|
|
40
|
+
@mutex.synchronize { @tokens[key] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write(key, token)
|
|
44
|
+
@mutex.synchronize { @tokens[key] = token }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete(key)
|
|
48
|
+
@mutex.synchronize { @tokens.delete(key) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def clear
|
|
52
|
+
@mutex.synchronize { @tokens.clear }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The live hash behind the store; only for the deprecated Client.token_cache.
|
|
56
|
+
def to_h
|
|
57
|
+
@tokens
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# JSON file store: tokens survive process restarts. Writes take an
|
|
62
|
+
# exclusive flock on "<path>.lock" (so concurrent processes and instances
|
|
63
|
+
# don't lose each other's keys) and replace the file atomically, mode 0600.
|
|
64
|
+
class File
|
|
65
|
+
attr_reader :path
|
|
66
|
+
|
|
67
|
+
def initialize(path)
|
|
68
|
+
@path = path
|
|
69
|
+
@mutex = Mutex.new
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def read(key)
|
|
73
|
+
load[key]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def write(key, token)
|
|
77
|
+
update { |tokens| tokens.merge(key => token) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def delete(key)
|
|
81
|
+
update { |tokens| tokens.key?(key) ? tokens.except(key) : nil }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Read-modify-write under the lock; the block returns the new hash, or nil for no change.
|
|
87
|
+
def update
|
|
88
|
+
@mutex.synchronize do
|
|
89
|
+
FileUtils.mkdir_p(::File.dirname(path))
|
|
90
|
+
::File.open("#{path}.lock", ::File::RDWR | ::File::CREAT, 0o600) do |lock|
|
|
91
|
+
lock.flock(::File::LOCK_EX)
|
|
92
|
+
tokens = yield(load)
|
|
93
|
+
save(tokens) if tokens
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def load
|
|
99
|
+
return {} unless ::File.exist?(path)
|
|
100
|
+
|
|
101
|
+
data = JSON.parse(::File.read(path))
|
|
102
|
+
data.is_a?(Hash) ? data : {}
|
|
103
|
+
rescue JSON::ParserError
|
|
104
|
+
{}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def save(tokens)
|
|
108
|
+
tmp = "#{path}.#{Process.pid}.#{SecureRandom.hex(6)}.tmp"
|
|
109
|
+
::File.open(tmp, ::File::WRONLY | ::File::CREAT | ::File::EXCL, 0o600) { |f| f.write(JSON.generate(tokens)) }
|
|
110
|
+
::File.rename(tmp, path)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/njtransit/version.rb
CHANGED
data/lib/njtransit.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: njtransit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jay Ravaliya
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: csv
|
|
@@ -135,6 +135,7 @@ files:
|
|
|
135
135
|
- LICENSE.txt
|
|
136
136
|
- README.md
|
|
137
137
|
- Rakefile
|
|
138
|
+
- docs/index.md
|
|
138
139
|
- docs/plans/2025-01-24-njtransit-gem-design.md
|
|
139
140
|
- docs/plans/2026-01-24-bus-api-design.md
|
|
140
141
|
- docs/plans/2026-01-24-gtfs-implementation.md
|
|
@@ -162,6 +163,7 @@ files:
|
|
|
162
163
|
- lib/njtransit/resources/rail_gtfs.rb
|
|
163
164
|
- lib/njtransit/skill.md
|
|
164
165
|
- lib/njtransit/tasks.rb
|
|
166
|
+
- lib/njtransit/token_stores.rb
|
|
165
167
|
- lib/njtransit/version.rb
|
|
166
168
|
- sig/njtransit.rbs
|
|
167
169
|
homepage: https://github.com/jayrav13/njtransit
|