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
data/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# NJTransit
|
|
2
|
+
|
|
3
|
+
A Ruby gem for NJ Transit's real-time and schedule data — buses, trains, and light rail. Built to be easy to drop into AI agents, chatbots, and creative projects that need live transit data.
|
|
4
|
+
|
|
5
|
+
## What You Can Do
|
|
6
|
+
|
|
7
|
+
- **Real-time departures** — "When is the next bus/train?" with live arrival times and delay status
|
|
8
|
+
- **Train tracking** — GPS positions, speed, and delay info for every active train
|
|
9
|
+
- **Schedule lookups** — Full timetables via GTFS static data, not just the next hour
|
|
10
|
+
- **Stop discovery** — Find nearby stops by coordinates
|
|
11
|
+
- **Route planning** — Find which routes connect two stops
|
|
12
|
+
- **Light rail** — Hudson-Bergen, Newark, and RiverLINE via the same API
|
|
13
|
+
- **GTFS-RT feeds** — Raw protobuf feeds for alerts, trip updates, and vehicle positions
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### 1. Get API Credentials
|
|
18
|
+
|
|
19
|
+
Register at [developer.njtransit.com](https://developer.njtransit.com/registration) to get a username and password.
|
|
20
|
+
|
|
21
|
+
### 2. Install
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
gem 'njtransit'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 3. Configure
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'njtransit'
|
|
31
|
+
|
|
32
|
+
NJTransit.configure do |config|
|
|
33
|
+
config.username = ENV['NJTRANSIT_USERNAME']
|
|
34
|
+
config.password = ENV['NJTRANSIT_PASSWORD']
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 4. Start Querying
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# Two clients: one for buses/light rail, one for trains
|
|
42
|
+
client = NJTransit.client
|
|
43
|
+
rail_client = NJTransit.rail_client
|
|
44
|
+
|
|
45
|
+
# When is the next bus from Port Authority?
|
|
46
|
+
client.bus.departures(stop: "PABT", enrich: false)
|
|
47
|
+
|
|
48
|
+
# Next trains from NY Penn Station
|
|
49
|
+
rail_client.rail.train_schedule_19(station: "NY")
|
|
50
|
+
|
|
51
|
+
# Where is train #3837 right now?
|
|
52
|
+
rail_client.rail.train_stop_list(train_id: "3837")
|
|
53
|
+
|
|
54
|
+
# What stops are within 2000 feet of me?
|
|
55
|
+
client.bus.stops_nearby(lat: 40.878, lon: -74.221, radius: 2000, enrich: false)
|
|
56
|
+
# radius is in feet
|
|
57
|
+
|
|
58
|
+
# Light rail routes
|
|
59
|
+
client.bus.routes(mode: "HBLR") # Hudson-Bergen Light Rail
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Two Clients, One Gem
|
|
63
|
+
|
|
64
|
+
NJ Transit splits its API across two hosts. The gem handles this with two clients:
|
|
65
|
+
|
|
66
|
+
| Client | Host | What it covers |
|
|
67
|
+
|--------|------|----------------|
|
|
68
|
+
| `NJTransit.client` | pcsdata.njtransit.com | Buses, light rail, bus GTFS-RT |
|
|
69
|
+
| `NJTransit.rail_client` | raildata.njtransit.com | Trains, rail GTFS-RT |
|
|
70
|
+
|
|
71
|
+
Both authenticate automatically. The bus client also supports light rail by passing a `mode` parameter (`HBLR`, `NLR`, `RL`, or `ALL`).
|
|
72
|
+
|
|
73
|
+
## Capabilities Overview
|
|
74
|
+
|
|
75
|
+
### Bus & Light Rail (`client.bus`)
|
|
76
|
+
|
|
77
|
+
Real-time departures, routes, stops, directions, nearby stops/vehicles, and trip tracking. Most methods accept an `enrich` flag — set `enrich: false` if you haven't imported GTFS static data.
|
|
78
|
+
|
|
79
|
+
### Rail (`rail_client.rail`)
|
|
80
|
+
|
|
81
|
+
Train schedules (real-time and full-day), station alerts and delay messages, train stop lists, and live vehicle positions for every active train.
|
|
82
|
+
|
|
83
|
+
### GTFS Static Data
|
|
84
|
+
|
|
85
|
+
Full offline schedules imported into a local SQLite database. Useful for answering "what's the schedule tomorrow?" when the real-time API only shows the next hour.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# Import once
|
|
89
|
+
NJTransit::GTFS.import("/path/to/gtfs/data")
|
|
90
|
+
|
|
91
|
+
# Then query
|
|
92
|
+
gtfs = NJTransit::GTFS.new
|
|
93
|
+
gtfs.schedule(route: "191", stop: "27005", date: Date.new(2026, 3, 28))
|
|
94
|
+
gtfs.routes_between(from: "WBRK", to: "PABT")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Rake tasks are also available: `rake njtransit:gtfs:import`, `rake njtransit:gtfs:status`, `rake njtransit:gtfs:clear`.
|
|
98
|
+
|
|
99
|
+
### GTFS-RT Feeds
|
|
100
|
+
|
|
101
|
+
Raw protobuf feeds for real-time alerts, trip updates, and vehicle positions:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
client.bus_gtfs.alerts # Bus alerts
|
|
105
|
+
client.bus_gtfs.vehicle_positions # Bus vehicle positions
|
|
106
|
+
rail_client.rail_gtfs.trip_updates # Rail trip updates
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A newer G2 version of the bus feeds is also available via `client.bus_gtfs_g2`.
|
|
110
|
+
|
|
111
|
+
## Using with Claude Code
|
|
112
|
+
|
|
113
|
+
If you have [Claude Code](https://claude.ai/code) installed, the `/njtransit` skill lets you ask transit questions directly from your terminal:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
/njtransit when is the next train from NY Penn to Trenton?
|
|
117
|
+
/njtransit what buses stop near 40.878, -74.221?
|
|
118
|
+
/njtransit is the Northeast Corridor delayed?
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Claude writes and runs Ruby code against the gem to answer your question. It's a good way to explore what the API can do without writing code yourself.
|
|
122
|
+
|
|
123
|
+
## Environment Variables
|
|
124
|
+
|
|
125
|
+
| Variable | Description | Default |
|
|
126
|
+
|----------|-------------|---------|
|
|
127
|
+
| `NJTRANSIT_USERNAME` | API username | — |
|
|
128
|
+
| `NJTRANSIT_PASSWORD` | API password | — |
|
|
129
|
+
| `NJTRANSIT_LOG_LEVEL` | `silent`, `info`, or `debug` | `silent` |
|
|
130
|
+
| `NJTRANSIT_BASE_URL` | Bus API base URL | `https://pcsdata.njtransit.com` |
|
|
131
|
+
| `NJTRANSIT_TIMEOUT` | Request timeout (seconds) | `30` |
|
|
132
|
+
| `NJTRANSIT_GTFS_DATABASE_PATH` | SQLite database path | `~/.local/share/njtransit/gtfs.sqlite3` |
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
bin/setup # Install dependencies
|
|
138
|
+
bundle exec rspec # Run tests (153 specs)
|
|
139
|
+
bin/console # Interactive prompt
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Contributing
|
|
143
|
+
|
|
144
|
+
Bug reports and pull requests are welcome at [github.com/jayrav13/njtransit](https://github.com/jayrav13/njtransit).
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
MIT — see [LICENSE](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# NJTransit Ruby Gem Design
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A developer-friendly Ruby gem for interacting with NJTransit's API. Designed for both personal use and open-source distribution.
|
|
6
|
+
|
|
7
|
+
## Design Decisions
|
|
8
|
+
|
|
9
|
+
| Aspect | Decision | Rationale |
|
|
10
|
+
|--------|----------|-----------|
|
|
11
|
+
| Name | `njtransit` | Simple, direct, recognizable |
|
|
12
|
+
| Ruby version | 3.2+ | Modern Ruby, allows newest syntax |
|
|
13
|
+
| HTTP client | Faraday + Typhoeus adapter | Typhoeus performance with Faraday middleware flexibility |
|
|
14
|
+
| Logging | Environment-based (silent/info/debug) | Selective logging based on `NJTRANSIT_LOG_LEVEL` |
|
|
15
|
+
| Testing | RSpec | Most common for gems, expressive syntax |
|
|
16
|
+
| API pattern | Global config + explicit instances | Convenience for simple apps, flexibility for advanced use |
|
|
17
|
+
| Errors | Comprehensive hierarchy | Granular error handling for all HTTP status codes |
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
### Module Structure
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
lib/
|
|
25
|
+
├── njtransit.rb # Entry point, global config
|
|
26
|
+
└── njtransit/
|
|
27
|
+
├── version.rb
|
|
28
|
+
├── configuration.rb # Config object with env var defaults
|
|
29
|
+
├── client.rb # HTTP client (Faraday + Typhoeus)
|
|
30
|
+
├── error.rb # Error class hierarchy
|
|
31
|
+
└── resources/ # One file per API domain
|
|
32
|
+
└── base.rb # Base class for resources
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Usage Patterns
|
|
36
|
+
|
|
37
|
+
**Global configuration (convenience):**
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
NJTransit.configure do |config|
|
|
41
|
+
config.api_key = ENV['NJTRANSIT_API_KEY']
|
|
42
|
+
config.log_level = 'debug' # silent, info, or debug
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
client = NJTransit.client
|
|
46
|
+
client.stations.list
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Explicit instance (flexibility):**
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
client = NJTransit::Client.new(
|
|
53
|
+
api_key: "your_key",
|
|
54
|
+
log_level: "info"
|
|
55
|
+
)
|
|
56
|
+
client.stations.list
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Both patterns use the same `Client` class - no code duplication.
|
|
60
|
+
|
|
61
|
+
### Configuration
|
|
62
|
+
|
|
63
|
+
Supports environment variables with sensible defaults:
|
|
64
|
+
|
|
65
|
+
- `NJTRANSIT_API_KEY` - API key (required for most endpoints)
|
|
66
|
+
- `NJTRANSIT_LOG_LEVEL` - Logging verbosity (silent/info/debug, default: silent)
|
|
67
|
+
- `NJTRANSIT_BASE_URL` - API base URL (overridable for testing)
|
|
68
|
+
- `NJTRANSIT_TIMEOUT` - Request timeout in seconds (default: 30)
|
|
69
|
+
|
|
70
|
+
### Error Hierarchy
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
NJTransit::Error
|
|
74
|
+
├── ClientError (4xx)
|
|
75
|
+
│ ├── BadRequestError (400)
|
|
76
|
+
│ ├── AuthenticationError (401)
|
|
77
|
+
│ ├── ForbiddenError (403)
|
|
78
|
+
│ ├── NotFoundError (404)
|
|
79
|
+
│ ├── MethodNotAllowedError (405)
|
|
80
|
+
│ ├── ConflictError (409)
|
|
81
|
+
│ ├── GoneError (410)
|
|
82
|
+
│ ├── UnprocessableEntityError (422)
|
|
83
|
+
│ └── RateLimitError (429)
|
|
84
|
+
├── ServerError (5xx)
|
|
85
|
+
│ ├── InternalServerError (500)
|
|
86
|
+
│ ├── BadGatewayError (502)
|
|
87
|
+
│ ├── ServiceUnavailableError (503)
|
|
88
|
+
│ └── GatewayTimeoutError (504)
|
|
89
|
+
└── ConnectionError
|
|
90
|
+
└── TimeoutError
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
All errors include the original response for inspection.
|
|
94
|
+
|
|
95
|
+
### Logging Levels
|
|
96
|
+
|
|
97
|
+
- **silent** (default): No output
|
|
98
|
+
- **info**: Request URLs and response status codes
|
|
99
|
+
- **debug**: Full request/response headers and bodies
|
|
100
|
+
|
|
101
|
+
## Next Steps
|
|
102
|
+
|
|
103
|
+
1. Drop NJTransit API documentation into `docs/api/`
|
|
104
|
+
2. Review available endpoints
|
|
105
|
+
3. Create resource classes for each API domain (stations, routes, schedules, etc.)
|
|
106
|
+
4. Add comprehensive specs with mocked responses
|
|
107
|
+
5. Document usage in README
|
|
108
|
+
|
|
109
|
+
## Notes
|
|
110
|
+
|
|
111
|
+
- `docs/api/` is gitignored - API docs are private/behind auth
|
|
112
|
+
- Full API coverage is the goal - establish patterns early, then the rest becomes mechanical
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# NJ Transit Bus API Integration Design
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Add support for the NJ Transit BUSDV2 API, providing access to bus schedules, real-time departures, stops, routes, and vehicle locations.
|
|
6
|
+
|
|
7
|
+
## Configuration
|
|
8
|
+
|
|
9
|
+
Replace `api_key` with `username` and `password`. Update default `base_url` to production endpoint.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
NJTransit.configure do |c|
|
|
13
|
+
c.username = ENV["NJTRANSIT_USERNAME"]
|
|
14
|
+
c.password = ENV["NJTRANSIT_PASSWORD"]
|
|
15
|
+
c.base_url = "https://pcsdata.njtransit.com" # new default
|
|
16
|
+
c.timeout = 30
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Authentication
|
|
21
|
+
|
|
22
|
+
Lazy authentication by default:
|
|
23
|
+
|
|
24
|
+
1. First API call checks for cached token
|
|
25
|
+
2. If no token, call `authenticateUser` with username/password
|
|
26
|
+
3. Cache token in memory on client instance
|
|
27
|
+
4. If response contains `{"errorMessage": "Invalid token."}`, re-authenticate once and retry
|
|
28
|
+
5. If re-auth fails, raise `NJTransit::AuthenticationError`
|
|
29
|
+
|
|
30
|
+
No cross-process token persistence. Each client instance manages its own token.
|
|
31
|
+
|
|
32
|
+
## Client Changes
|
|
33
|
+
|
|
34
|
+
The Bus API requires `multipart/form-data` POST requests (not JSON).
|
|
35
|
+
|
|
36
|
+
Add `post_form` method to client:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
def post_form(path, params = {})
|
|
40
|
+
response = connection.post(path) do |req|
|
|
41
|
+
req.body = params # Faraday handles form encoding
|
|
42
|
+
end
|
|
43
|
+
handle_response(response)
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The `Bus` resource injects the token automatically into all requests.
|
|
48
|
+
|
|
49
|
+
## Bus Resource
|
|
50
|
+
|
|
51
|
+
Single flat resource at `client.bus` with hardcoded `mode: "BUS"`. Future modes (light rail, etc.) will be separate resources using the same underlying API.
|
|
52
|
+
|
|
53
|
+
### Methods
|
|
54
|
+
|
|
55
|
+
| Method | Required Params | Optional Params | API Endpoint |
|
|
56
|
+
|--------|----------------|-----------------|--------------|
|
|
57
|
+
| `locations` | - | - | `getLocations` |
|
|
58
|
+
| `routes` | - | - | `getBusRoutes` |
|
|
59
|
+
| `directions` | `route:` | - | `getBusDirectionsData` |
|
|
60
|
+
| `stops` | `route:`, `direction:` | `name_contains:` | `getStops` |
|
|
61
|
+
| `stop_name` | `stop_number:` | - | `getStopName` |
|
|
62
|
+
| `route_trips` | `location:`, `route:` | - | `getRouteTrips` |
|
|
63
|
+
| `departures` | `stop:` | `route:`, `direction:` | `getBusDV` |
|
|
64
|
+
| `trip_stops` | `internal_trip_number:`, `sched_dep_time:` | `timing_point_id:` | `getTripStops` |
|
|
65
|
+
| `stops_nearby` | `lat:`, `lon:`, `radius:` | `route:`, `direction:` | `getBusLocationsData` |
|
|
66
|
+
| `vehicles_nearby` | `lat:`, `lon:`, `radius:` | - | `getVehicleLocations` |
|
|
67
|
+
|
|
68
|
+
### Return Values
|
|
69
|
+
|
|
70
|
+
All methods return raw hashes/arrays as returned by the API. No object wrapping.
|
|
71
|
+
|
|
72
|
+
## Error Handling
|
|
73
|
+
|
|
74
|
+
The Bus API returns errors in response body, not HTTP status codes.
|
|
75
|
+
|
|
76
|
+
Detection:
|
|
77
|
+
- Check response for `errorMessage` key
|
|
78
|
+
- `"Invalid token."` → re-authenticate, retry once, raise `AuthenticationError` if still failing
|
|
79
|
+
- Other `errorMessage` values → raise `NJTransit::APIError`
|
|
80
|
+
|
|
81
|
+
New error class:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class APIError < Error; end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Existing errors (ConnectionError, TimeoutError, HTTP status errors) remain unchanged.
|
|
88
|
+
|
|
89
|
+
## File Structure
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
lib/njtransit/
|
|
93
|
+
├── configuration.rb # Modified: username/password, new base_url
|
|
94
|
+
├── client.rb # Modified: post_form, token management, bus accessor
|
|
95
|
+
├── error.rb # Modified: add APIError
|
|
96
|
+
└── resources/
|
|
97
|
+
├── base.rb # Unchanged
|
|
98
|
+
└── bus.rb # New
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Usage Example
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
NJTransit.configure do |c|
|
|
105
|
+
c.username = "myuser"
|
|
106
|
+
c.password = "mypass"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
client = NJTransit.client
|
|
110
|
+
|
|
111
|
+
# Get all routes
|
|
112
|
+
client.bus.routes
|
|
113
|
+
|
|
114
|
+
# Get real-time departures at Port Authority
|
|
115
|
+
client.bus.departures(stop: "PABT")
|
|
116
|
+
|
|
117
|
+
# Find vehicles near Newark
|
|
118
|
+
client.bus.vehicles_nearby(lat: 40.737, lon: -74.170, radius: 2000)
|
|
119
|
+
```
|