njtransit 1.0.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 +7 -0
- data/.claude/commands/njtransit.md +196 -0
- data/.mcp.json.example +12 -0
- data/.mcp.json.sample +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +87 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +37 -0
- data/CLAUDE.md +159 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/Rakefile +12 -0
- data/docs/plans/2025-01-24-njtransit-gem-design.md +112 -0
- data/docs/plans/2026-01-24-bus-api-design.md +119 -0
- data/docs/plans/2026-01-24-gtfs-implementation.md +2216 -0
- data/docs/plans/2026-01-24-gtfs-loader-design.md +351 -0
- data/docs/superpowers/plans/2026-03-26-dev-infra-and-agent.md +480 -0
- data/lefthook.yml +17 -0
- data/lib/njtransit/client.rb +291 -0
- data/lib/njtransit/configuration.rb +49 -0
- data/lib/njtransit/error.rb +50 -0
- data/lib/njtransit/gtfs/database.rb +145 -0
- data/lib/njtransit/gtfs/importer.rb +124 -0
- data/lib/njtransit/gtfs/models/route.rb +59 -0
- data/lib/njtransit/gtfs/models/stop.rb +63 -0
- data/lib/njtransit/gtfs/queries/routes_between.rb +62 -0
- data/lib/njtransit/gtfs/queries/schedule.rb +75 -0
- data/lib/njtransit/gtfs.rb +119 -0
- data/lib/njtransit/railtie.rb +9 -0
- data/lib/njtransit/resources/base.rb +35 -0
- data/lib/njtransit/resources/bus/enrichment.rb +105 -0
- data/lib/njtransit/resources/bus.rb +95 -0
- data/lib/njtransit/resources/bus_gtfs.rb +34 -0
- data/lib/njtransit/resources/rail.rb +47 -0
- data/lib/njtransit/resources/rail_gtfs.rb +27 -0
- data/lib/njtransit/tasks.rb +74 -0
- data/lib/njtransit/version.rb +5 -0
- data/lib/njtransit.rb +40 -0
- data/sig/njtransit.rbs +4 -0
- metadata +177 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# GTFS Static Data Loader Design
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-01-24
|
|
4
|
+
**Issue:** [#3 - Add GTFS static data loader for complete/deterministic dataset](https://github.com/jayrav13/njtransit/issues/3)
|
|
5
|
+
**Status:** Approved
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
The real-time Bus API has limitations that prevent building a complete dataset:
|
|
10
|
+
|
|
11
|
+
| Limitation | Impact |
|
|
12
|
+
|------------|--------|
|
|
13
|
+
| No lat/lon in `stops()` response | Can't plot stops on a map |
|
|
14
|
+
| `departures()` only shows ~1hr window | Miss routes with later trips |
|
|
15
|
+
| No route discovery | Can't find "all routes from A to B" |
|
|
16
|
+
|
|
17
|
+
GTFS static data solves these gaps and enables enriching real-time API responses with complete information.
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─────────────────────────────────────────────────────────┐
|
|
23
|
+
│ NJTransit Gem │
|
|
24
|
+
├─────────────────────────────────────────────────────────┤
|
|
25
|
+
│ │
|
|
26
|
+
│ ┌─────────────┐ ┌─────────────────────────┐ │
|
|
27
|
+
│ │ Bus API │─────────▶│ GTFS (enrichment) │ │
|
|
28
|
+
│ │ (real-time)│ │ │ │
|
|
29
|
+
│ └─────────────┘ │ ┌───────────────────┐ │ │
|
|
30
|
+
│ │ │ Sequel + SQLite │ │ │
|
|
31
|
+
│ ┌─────────────┐ │ └───────────────────┘ │ │
|
|
32
|
+
│ │ GTFS API │─────────▶│ │ │ │
|
|
33
|
+
│ │ (static) │ │ ▼ │ │
|
|
34
|
+
│ └─────────────┘ │ ~/.local/share/ │ │
|
|
35
|
+
│ │ njtransit/gtfs.sqlite3 │ │
|
|
36
|
+
│ └─────────────────────────┘ │
|
|
37
|
+
└─────────────────────────────────────────────────────────┘
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Key decisions:**
|
|
41
|
+
|
|
42
|
+
- **Sequel gem** for database access (lightweight, clean DSL)
|
|
43
|
+
- **SQLite** stored in XDG-compliant location, configurable via `NJTransit.configure`
|
|
44
|
+
- **GTFS import is a deployment step** - errors raised if missing at runtime
|
|
45
|
+
- **Bus API enriches responses by default** with GTFS data
|
|
46
|
+
|
|
47
|
+
## File Structure
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
lib/njtransit/
|
|
51
|
+
├── gtfs.rb # Main GTFS module (import, new, status)
|
|
52
|
+
├── gtfs/
|
|
53
|
+
│ ├── database.rb # Sequel connection, schema setup
|
|
54
|
+
│ ├── importer.rb # Parses CSV files, populates DB
|
|
55
|
+
│ ├── models/
|
|
56
|
+
│ │ ├── agency.rb # Sequel::Model classes
|
|
57
|
+
│ │ ├── route.rb
|
|
58
|
+
│ │ ├── stop.rb
|
|
59
|
+
│ │ ├── trip.rb
|
|
60
|
+
│ │ ├── stop_time.rb
|
|
61
|
+
│ │ ├── calendar_date.rb
|
|
62
|
+
│ │ └── shape.rb
|
|
63
|
+
│ └── queries/
|
|
64
|
+
│ ├── routes_between.rb # Find routes serving two stops
|
|
65
|
+
│ └── schedule.rb # Full day schedule for route/stop
|
|
66
|
+
├── tasks.rb # Rake task definitions
|
|
67
|
+
└── railtie.rb # Auto-load tasks in Rails
|
|
68
|
+
|
|
69
|
+
# Updated existing files:
|
|
70
|
+
lib/njtransit/configuration.rb # Add gtfs_database_path option
|
|
71
|
+
lib/njtransit/resources/bus.rb # Add enrichment logic
|
|
72
|
+
lib/njtransit/error.rb # Add GTFSNotImportedError
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Database Schema
|
|
76
|
+
|
|
77
|
+
```sql
|
|
78
|
+
CREATE TABLE agencies (
|
|
79
|
+
agency_id TEXT PRIMARY KEY,
|
|
80
|
+
agency_name TEXT,
|
|
81
|
+
agency_url TEXT,
|
|
82
|
+
agency_timezone TEXT
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE routes (
|
|
86
|
+
route_id TEXT PRIMARY KEY,
|
|
87
|
+
agency_id TEXT,
|
|
88
|
+
route_short_name TEXT,
|
|
89
|
+
route_long_name TEXT,
|
|
90
|
+
route_type INTEGER,
|
|
91
|
+
route_color TEXT
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE TABLE stops (
|
|
95
|
+
stop_id TEXT PRIMARY KEY,
|
|
96
|
+
stop_code TEXT,
|
|
97
|
+
stop_name TEXT,
|
|
98
|
+
stop_lat REAL,
|
|
99
|
+
stop_lon REAL,
|
|
100
|
+
zone_id TEXT
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
CREATE TABLE trips (
|
|
104
|
+
trip_id TEXT PRIMARY KEY,
|
|
105
|
+
route_id TEXT,
|
|
106
|
+
service_id TEXT,
|
|
107
|
+
trip_headsign TEXT,
|
|
108
|
+
direction_id INTEGER,
|
|
109
|
+
shape_id TEXT
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE TABLE stop_times (
|
|
113
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
114
|
+
trip_id TEXT,
|
|
115
|
+
stop_id TEXT,
|
|
116
|
+
arrival_time TEXT,
|
|
117
|
+
departure_time TEXT,
|
|
118
|
+
stop_sequence INTEGER
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE calendar_dates (
|
|
122
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
123
|
+
service_id TEXT,
|
|
124
|
+
date TEXT,
|
|
125
|
+
exception_type INTEGER
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
CREATE TABLE shapes (
|
|
129
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
130
|
+
shape_id TEXT,
|
|
131
|
+
shape_pt_lat REAL,
|
|
132
|
+
shape_pt_lon REAL,
|
|
133
|
+
shape_pt_sequence INTEGER
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
-- Indexes
|
|
137
|
+
CREATE INDEX idx_stops_stop_code ON stops(stop_code);
|
|
138
|
+
CREATE INDEX idx_routes_short_name ON routes(route_short_name);
|
|
139
|
+
CREATE INDEX idx_trips_route_id ON trips(route_id);
|
|
140
|
+
CREATE INDEX idx_trips_service_id ON trips(service_id);
|
|
141
|
+
CREATE INDEX idx_stop_times_trip_id ON stop_times(trip_id);
|
|
142
|
+
CREATE INDEX idx_stop_times_stop_id ON stop_times(stop_id);
|
|
143
|
+
CREATE INDEX idx_calendar_dates_service_id_date ON calendar_dates(service_id, date);
|
|
144
|
+
CREATE INDEX idx_shapes_shape_id ON shapes(shape_id);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## GTFS Import API
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
# Import (deployment time)
|
|
151
|
+
NJTransit::GTFS.import("/path/to/bus_data/")
|
|
152
|
+
NJTransit::GTFS.import("/path/to/bus_data/", force: true) # Re-import, clears existing
|
|
153
|
+
|
|
154
|
+
# Status check
|
|
155
|
+
NJTransit::GTFS.status
|
|
156
|
+
# => {
|
|
157
|
+
# imported: true,
|
|
158
|
+
# path: "~/.local/share/njtransit/gtfs.sqlite3",
|
|
159
|
+
# routes: 261,
|
|
160
|
+
# stops: 16594,
|
|
161
|
+
# trips: 45843,
|
|
162
|
+
# imported_at: 2026-01-24
|
|
163
|
+
# }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Rake Tasks
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
# In user's Rakefile
|
|
170
|
+
require 'njtransit/tasks'
|
|
171
|
+
|
|
172
|
+
# Provides:
|
|
173
|
+
# rake njtransit:gtfs:import[/path/to/bus_data]
|
|
174
|
+
# rake njtransit:gtfs:status
|
|
175
|
+
# rake njtransit:gtfs:clear
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Rails apps auto-load tasks via Railtie.
|
|
179
|
+
|
|
180
|
+
## GTFS Query API
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
gtfs = NJTransit::GTFS.new
|
|
184
|
+
|
|
185
|
+
# Stops
|
|
186
|
+
gtfs.stops.all # => [Stop, Stop, ...]
|
|
187
|
+
gtfs.stops.find("stop_id_value") # => Stop (by stop_id)
|
|
188
|
+
gtfs.stops.find_by_code("21681") # => Stop (by stop_code)
|
|
189
|
+
gtfs.stops.where(zone_id: "NB") # => [Stop, ...]
|
|
190
|
+
|
|
191
|
+
# Routes
|
|
192
|
+
gtfs.routes.all
|
|
193
|
+
gtfs.routes.find("197") # => Route (by route_id or short_name)
|
|
194
|
+
|
|
195
|
+
# Route discovery
|
|
196
|
+
gtfs.routes_between(from: "WBRK", to: "PABT")
|
|
197
|
+
# => ["194", "197", "198"]
|
|
198
|
+
|
|
199
|
+
# Schedule
|
|
200
|
+
gtfs.schedule(route: "197", stop: "WBRK", date: Date.today)
|
|
201
|
+
# => [{ trip_id: "...", arrival_time: "06:45:00", departure_time: "06:45:00" }, ...]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Model Objects
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
stop = gtfs.stops.find_by_code("21681")
|
|
208
|
+
stop.stop_id # => "1"
|
|
209
|
+
stop.stop_code # => "21681"
|
|
210
|
+
stop.stop_name # => "WILLOWBROOK MALL"
|
|
211
|
+
stop.lat # => 40.8523
|
|
212
|
+
stop.lon # => -74.2567
|
|
213
|
+
stop.to_h # => { stop_id: "1", stop_code: "21681", ... }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Bus API Enrichment
|
|
217
|
+
|
|
218
|
+
Enrichment is **ON by default**. GTFS data is merged into Bus API responses automatically.
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
# Enriched (default)
|
|
222
|
+
client.bus.stops(route: "191", direction: "New York")
|
|
223
|
+
# => [{ "stop_id" => "21681", "stop_name" => "WILLOWBROOK MALL",
|
|
224
|
+
# "stop_lat" => 40.8523, "stop_lon" => -74.2567 }, ...]
|
|
225
|
+
|
|
226
|
+
# Opt-out
|
|
227
|
+
client.bus.stops(route: "191", direction: "New York", enrich: false)
|
|
228
|
+
# => Original unenriched response
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Enrichment by Method
|
|
232
|
+
|
|
233
|
+
| Method | Enrichment |
|
|
234
|
+
|--------|------------|
|
|
235
|
+
| `stops(route:, direction:)` | Add lat/lon from GTFS |
|
|
236
|
+
| `departures(stop:)` | Add stop lat/lon, route long_name |
|
|
237
|
+
| `stop_name(stop:)` | Add lat/lon |
|
|
238
|
+
| `stops_nearby(lat:, lon:)` | Already has coords, add zone_id |
|
|
239
|
+
| `vehicles_nearby(lat:, lon:)` | Add route long_name |
|
|
240
|
+
|
|
241
|
+
### Error Handling
|
|
242
|
+
|
|
243
|
+
If GTFS hasn't been imported, enriched calls raise `GTFSNotImportedError`:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
client.bus.stops(route: "191", direction: "New York")
|
|
247
|
+
# => raises NJTransit::GTFSNotImportedError:
|
|
248
|
+
# "GTFS data not found. Run: rake njtransit:gtfs:import[/path/to/bus_data]"
|
|
249
|
+
#
|
|
250
|
+
# Detected GTFS files at: ./docs/api/njtransit/bus_data/
|
|
251
|
+
# Hint: rake njtransit:gtfs:import[./docs/api/njtransit/bus_data/]
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Calls with `enrich: false` work without GTFS.
|
|
255
|
+
|
|
256
|
+
## Configuration
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
NJTransit.configure do |config|
|
|
260
|
+
# Existing
|
|
261
|
+
config.username = "..."
|
|
262
|
+
config.password = "..."
|
|
263
|
+
|
|
264
|
+
# New
|
|
265
|
+
config.gtfs_database_path = "/custom/path/gtfs.sqlite3" # optional
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Default Path Resolution
|
|
270
|
+
|
|
271
|
+
1. `config.gtfs_database_path` if set
|
|
272
|
+
2. `$XDG_DATA_HOME/njtransit/gtfs.sqlite3` if XDG_DATA_HOME set
|
|
273
|
+
3. `~/.local/share/njtransit/gtfs.sqlite3`
|
|
274
|
+
|
|
275
|
+
### Auto-Detection Paths
|
|
276
|
+
|
|
277
|
+
For helpful error messages, these paths are checked for GTFS files:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
GTFS_SEARCH_PATHS = [
|
|
281
|
+
"./bus_data",
|
|
282
|
+
"./vendor/bus_data",
|
|
283
|
+
"./docs/api/njtransit/bus_data",
|
|
284
|
+
"#{Gem.loaded_specs['njtransit']&.gem_dir}/data/bus_data"
|
|
285
|
+
]
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Dependencies
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
# Gemfile additions
|
|
292
|
+
gem "sequel", "~> 5.0"
|
|
293
|
+
gem "sqlite3", "~> 2.0"
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Testing Strategy
|
|
297
|
+
|
|
298
|
+
### Unit Tests (mocked, fast)
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
spec/njtransit/gtfs/importer_spec.rb # CSV parsing, error handling
|
|
302
|
+
spec/njtransit/gtfs/models/*_spec.rb # Model attributes, queries
|
|
303
|
+
spec/njtransit/gtfs/queries/*_spec.rb # routes_between, schedule logic
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Integration Tests (real SQLite, fixtures)
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
spec/integration/gtfs_spec.rb # Full import/query workflows
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Fixtures
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
spec/fixtures/gtfs/
|
|
316
|
+
├── agency.txt # 1 agency
|
|
317
|
+
├── routes.txt # 10 routes
|
|
318
|
+
├── stops.txt # 50 stops
|
|
319
|
+
├── trips.txt # 100 trips
|
|
320
|
+
├── stop_times.txt # 500 stop times
|
|
321
|
+
├── calendar_dates.txt
|
|
322
|
+
└── shapes.txt
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Bus Enrichment Tests
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
spec/njtransit/resources/bus_spec.rb
|
|
329
|
+
- Responses include GTFS fields when enriched
|
|
330
|
+
- enrich: false skips enrichment
|
|
331
|
+
- GTFSNotImportedError raised when DB missing
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Deployment
|
|
335
|
+
|
|
336
|
+
GTFS import runs once during deployment:
|
|
337
|
+
|
|
338
|
+
```dockerfile
|
|
339
|
+
# Dockerfile
|
|
340
|
+
RUN bundle exec rake njtransit:gtfs:import[/app/vendor/bus_data/]
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
# Capistrano
|
|
345
|
+
after 'deploy:published', 'njtransit:gtfs:import'
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```yaml
|
|
349
|
+
# CI/CD pipeline
|
|
350
|
+
- run: bundle exec rake njtransit:gtfs:import[./bus_data/]
|
|
351
|
+
```
|