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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/commands/njtransit.md +196 -0
  3. data/.mcp.json.example +12 -0
  4. data/.mcp.json.sample +11 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +87 -0
  7. data/.ruby-version +1 -0
  8. data/CHANGELOG.md +37 -0
  9. data/CLAUDE.md +159 -0
  10. data/CODE_OF_CONDUCT.md +84 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +148 -0
  13. data/Rakefile +12 -0
  14. data/docs/plans/2025-01-24-njtransit-gem-design.md +112 -0
  15. data/docs/plans/2026-01-24-bus-api-design.md +119 -0
  16. data/docs/plans/2026-01-24-gtfs-implementation.md +2216 -0
  17. data/docs/plans/2026-01-24-gtfs-loader-design.md +351 -0
  18. data/docs/superpowers/plans/2026-03-26-dev-infra-and-agent.md +480 -0
  19. data/lefthook.yml +17 -0
  20. data/lib/njtransit/client.rb +291 -0
  21. data/lib/njtransit/configuration.rb +49 -0
  22. data/lib/njtransit/error.rb +50 -0
  23. data/lib/njtransit/gtfs/database.rb +145 -0
  24. data/lib/njtransit/gtfs/importer.rb +124 -0
  25. data/lib/njtransit/gtfs/models/route.rb +59 -0
  26. data/lib/njtransit/gtfs/models/stop.rb +63 -0
  27. data/lib/njtransit/gtfs/queries/routes_between.rb +62 -0
  28. data/lib/njtransit/gtfs/queries/schedule.rb +75 -0
  29. data/lib/njtransit/gtfs.rb +119 -0
  30. data/lib/njtransit/railtie.rb +9 -0
  31. data/lib/njtransit/resources/base.rb +35 -0
  32. data/lib/njtransit/resources/bus/enrichment.rb +105 -0
  33. data/lib/njtransit/resources/bus.rb +95 -0
  34. data/lib/njtransit/resources/bus_gtfs.rb +34 -0
  35. data/lib/njtransit/resources/rail.rb +47 -0
  36. data/lib/njtransit/resources/rail_gtfs.rb +27 -0
  37. data/lib/njtransit/tasks.rb +74 -0
  38. data/lib/njtransit/version.rb +5 -0
  39. data/lib/njtransit.rb +40 -0
  40. data/sig/njtransit.rbs +4 -0
  41. 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,12 @@
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]
@@ -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
+ ```