Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# StationAPI Repository Guidelines

This guide explains how automation agents and human contributors should work with the StationAPI repository so releases stay predictable, auditable, and safe. Update this file whenever you change the workflow or behavior it documents.

## Project Layout
- `stationapi/src/domain/` – Entity definitions and repository abstractions. The `entity/` module mirrors the gRPC schema, `repository/` provides `async_trait`-based interfaces, and `normalize.rs` contains text normalization for search.
- `stationapi/src/use_case/` – Application logic. `interactor/query.rs` implements the `QueryUseCase` contract defined in `traits/query.rs`.
- `stationapi/src/infrastructure/` – SQLx repositories for PostgreSQL. `My*Repository` types share an `Arc<PgPool)` and integration tests live behind `#[cfg_attr(not(feature = "integration-tests"), ignore)]`.
- `stationapi/src/presentation/` – gRPC presentation layer. `presentation/controller/grpc.rs` wires `MyApi` to the generated server types. Health checks and reflection are enabled through `tonic-health` and `tonic-reflection`.
- `stationapi/proto/stationapi.proto` – The gRPC contract. `build.rs` uses `tonic-build` to generate server code and `FILE_DESCRIPTOR_SET`.
- `data/` – Canonical CSV datasets and schema definition (`create_table.sql`). Files follow the `N!table.csv` naming scheme to control import order. Detailed instructions are in `data/README.md`.
- `data_validator/` – CLI that verifies cross-file constraints (`cargo run -p data_validator`).
- `docker/` & `compose.yml` – Container definitions for the API and PostgreSQL 18. `docker/postgres/00_extensions.sql` ensures `pg_trgm` and `btree_gist` are installed.
- `Makefile` – Convenience targets for running tests (`make test-unit`, `make test-integration`, `make test-all`).

## Tooling and Environment
- Rust: Use the stable toolchain (`rustup default stable`). Docker images build with `rust:1`.
- Protobuf: `protoc` must exist when building locally (the Docker image installs it automatically).
- PostgreSQL: Version 15+ with `pg_trgm` and `btree_gist` extensions. Compose spins up PostgreSQL 18 with these extensions preloaded.
- Required environment variables:
- `DATABASE_URL` – SQLx connection string (e.g., `postgres://stationapi:stationapi@localhost/stationapi`).
- `DISABLE_GRPC_WEB` – `false` enables gRPC-Web; set to `true` for pure gRPC/HTTP2.
- `HOST` and `PORT` – listen address (defaults to `[::1]:50051`; Docker uses `0.0.0.0:50051`).
- `.env.test` exports `TEST_DATABASE_URL`, `RUST_LOG`, `RUST_BACKTRACE`, and `RUST_TEST_THREADS=1` for integration tests.
- Recommended: copy `.env` to `.env.local`, override local values, and rely on `dotenv::from_filename(".env.local")` during startup.

## Running and Deploying
- **Local development**
1. Prepare PostgreSQL and set `DATABASE_URL`.
2. `cargo run -p stationapi` rebuilds the schema from `data/create_table.sql`, imports every `data/*.csv`, and then boots the gRPC server. If extension creation fails, grant the needed privileges manually.
3. Health checks respond to `grpc.health.v1.Health/Check`; gRPC Reflection is available for tooling such as `grpcurl`.
- **Docker / Compose**
- `docker compose up --build` launches both the API and PostgreSQL containers. Source code mounts into `/app`, but hot reload is not provided; rebuild after code changes.
- Production images rely on `docker/api/Dockerfile`, which runs `cargo build -p stationapi --release` and copies `data/`.
- **gRPC-Web**
- The server accepts HTTP/1.1 and wraps handlers via `tonic_web::enable`. Disable this behavior by exporting `DISABLE_GRPC_WEB=true` to require HTTP/2 clients only.

## Data Management
- CSV import order depends on the numeric prefix (`1!`, `2!`, ...). When adding datasets, choose a prefix that preserves foreign-key dependencies.
- `data/create_table.sql` drops and recreates tables, indexes, and foreign keys. Update this script alongside any schema or CSV column changes.
- `data_validator` currently verifies that `5!station_station_types.csv` references valid station and type IDs. Extend the validator when new cross-references are introduced and keep the process fail-fast (panic on invalid data).

## Testing and Quality
- **Unit tests** – `cargo test --lib --package stationapi` or `make test-unit`; focus on entities and repository mocks without a database.
- **Integration tests** – `source .env.test && cargo test --lib --package stationapi --features integration-tests` or `make test-integration`. Use a dedicated schema behind `TEST_DATABASE_URL` and clean it up after runs.
- **Full suite** – `make test-all` runs unit then integration tests sequentially. Set `RUST_LOG=debug` to inspect SQL queries during debugging.
- **Linting and formatting** – Run `cargo fmt` and `cargo clippy --all-targets --all-features` before committing. Resolve new Clippy warnings unless an existing `#![allow]` covers the case.
- **Data verification** – Execute `cargo run -p data_validator` whenever CSVs change and record results in pull requests.

## gRPC Endpoint Overview
- **Stations** – `GetStationById`, `GetStationByIdList`, `GetStationsByGroupId`, `GetStationsByCoordinates`, `GetStationsByLineId`, `GetStationsByName`, `GetStationsByLineGroupId`. `QueryInteractor` enriches stations with lines, companies, station numbers, and train types.
- **Lines** – `GetLineById`, `GetLineByIdList`, `GetLinesByName`. Results include company data and computed line symbols based on repository helpers.
- **Routes** – `GetRoutes`, `GetRoutesMinimal`. The minimal variant returns `RouteMinimalResponse` with deduplicated `LineMinimal` data; paging tokens are currently empty (pagination not implemented).
- **Train types** – `GetTrainTypesByStationId`, `GetRouteTypes`. Train types aggregate by line group and include related lines plus optional train type metadata.
- **Connected routes** – `GetConnectedRoutes`. `QueryInteractor::get_connected_stations` is not implemented yet and returns an empty vector; update the use-case and infrastructure layers together when adding real logic.
- Changes to the service contract require coordinated updates to `proto/stationapi.proto`, regenerated code via `tonic-build`, and corresponding adjustments in both presentation and use-case layers.

## Contribution Guidelines
- Document the commands you executed (for example, ``cargo fmt && cargo clippy --all-targets --all-features && make test-unit``) and their outcomes in every pull request.
- For database, gRPC, or schema updates, add architectural notes under `docs/` and synchronize README references so onboarding materials stay accurate.
- When modifying `QueryInteractor`, ensure the enrichment steps (companies, train types, line symbols) still behave as expected. Double-check helper methods such as `update_station_vec_with_attributes` and `build_route_tree_map`.
- Introducing new tables, endpoints, or feature flags must come with matching updates to this document and any other affected guidance.

## Maintenance
Keep this guide aligned with the repository. If a workflow, environment requirement, or endpoint changes, update AGENTS.md in the same pull request so automation agents and contributors work from current instructions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

The gRPC-Web API for provides nearby japanese train station.

## Documentation

- For automation agent and contributor workflows, see [AGENTS.md](AGENTS.md).

## Data Contribution

This project includes a comprehensive dataset of Japanese railway information in the `data/` directory. The data is maintained in CSV format and contributions are primarily targeted at Japanese speakers. For detailed information about data structure and contribution guidelines, please refer to [data/README.md](data/README.md).
Expand Down
2 changes: 1 addition & 1 deletion data/3!stations.csv
Original file line number Diff line number Diff line change
Expand Up @@ -7143,7 +7143,7 @@ station_cd,station_g_cd,station_name,station_name_k,station_name_r,station_name_
3400208,3400208,岡町,オカマチ,Okamachi,Okamachi,冈町,오카마치,45,,,,,34002,27,561-0881,大阪府豊中市中桜塚1-1-1,135.465103,34.778949,0000-00-00,0000-00-00,0,3400208
3400209,3400209,豊中,トヨナカ,Toyonaka,Toyonaka,丰中,도요나카,46,,,,,34002,27,560-0021,大阪府豊中市本町1-1-1,135.462073,34.787248,0000-00-00,0000-00-00,0,3400209
3400210,3400210,蛍池,ホタルガイケ,Hotarugaike,Hotarugaike,萤池,호타루가이케,47,,,,,34002,27,560-0033,大阪府豊中市蛍池中町3-1,135.449238,34.794365,0000-00-00,0000-00-00,0,3400210
3400211,3400211,石橋阪大前,イシバシハンダイマエ,Ishibashi handai-mae,Ishibashi handai-mae,石桥,이시바시,48,,,,,34002,27,563-0032,大阪府池田市石橋2-18-1,135.445487,34.808254,0000-00-00,0000-00-00,0,3400211
3400211,3400211,石橋阪大前,イシバシハンダイマエ,Ishibashi handai-mae,Ishibashi handai-mae,石桥阪大前,이시바시한다이마에,48,,,,,34002,27,563-0032,大阪府池田市石橋2-18-1,135.445487,34.808254,0000-00-00,0000-00-00,0,3400211
3400212,3400212,池田,イケダ,Ikeda,Ikeda,池田,이케다,49,,,,,34002,27,563-0056,大阪府池田市栄町1-1,135.425797,34.821544,0000-00-00,0000-00-00,0,3400212
3400213,1162908,川西能勢口,カワニシノセグチ,Kawanishi-noseguchi,Kawanishi-noseguchi,川西能势口,가와니시노세구치,50,,,,,34002,28,666-0015,兵庫県川西市小花1-1-10,135.413393,34.827654,0000-00-00,0000-00-00,0,3400213
3400214,3400214,雲雀丘花屋敷,ヒバリガオカハナヤシキ,Hibarigaoka-hanayashiki,Hibarigaoka-hanayashiki,云雀丘花屋敷,히바리가오카하나야시키,51,,,,,34002,28,665-0805,兵庫県宝塚市雲雀丘1-1-10,135.402946,34.827357,0000-00-00,0000-00-00,0,3400214
Expand Down
16 changes: 16 additions & 0 deletions stationapi/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
"StopCondition",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
"StationMinimal",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
"LineMinimal",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
"RouteMinimal",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
"RouteMinimalResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.protoc_arg("--experimental_allow_proto3_optional")
.file_descriptor_set_path(out_dir.join("stationapi_descriptor.bin"))
.compile_protos(&["proto/stationapi.proto"], &["proto"])?;
Expand Down
2 changes: 1 addition & 1 deletion stationapi/proto
Submodule proto updated 1 files
+37 −0 stationapi.proto
41 changes: 37 additions & 4 deletions stationapi/src/presentation/controller/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ use crate::{
},
presentation::error::PresentationalError,
proto::{
station_api_server::StationApi, GetConnectedStationsRequest, GetLineByIdRequest,
GetLinesByNameRequest, GetRouteRequest, GetStationByCoordinatesRequest,
station_api_server::StationApi, GetConnectedStationsRequest, GetLineByIdListRequest,
GetLineByIdRequest, GetLinesByNameRequest, GetRouteRequest, GetStationByCoordinatesRequest,
GetStationByGroupIdRequest, GetStationByIdListRequest, GetStationByIdRequest,
GetStationByLineIdRequest, GetStationsByLineGroupIdRequest, GetStationsByNameRequest,
GetTrainTypesByStationIdRequest, MultipleLineResponse, MultipleStationResponse,
MultipleTrainTypeResponse, Route, RouteResponse, RouteTypeResponse, SingleLineResponse,
SingleStationResponse,
MultipleTrainTypeResponse, Route, RouteMinimalResponse, RouteResponse, RouteTypeResponse,
SingleLineResponse, SingleStationResponse,
},
use_case::{interactor::query::QueryInteractor, traits::query::QueryUseCase},
};
Expand Down Expand Up @@ -214,6 +214,21 @@ impl StationApi for MyApi {
}
}

async fn get_routes_minimal(
&self,
request: tonic::Request<GetRouteRequest>,
) -> Result<tonic::Response<RouteMinimalResponse>, tonic::Status> {
let from_id = request.get_ref().from_station_group_id;
let to_id = request.get_ref().to_station_group_id;

match self.query_use_case.get_routes_minimal(from_id, to_id).await {
Ok(response) => {
return Ok(Response::new(response));
}
Err(err) => Err(PresentationalError::from(err).into()),
}
}

async fn get_route_types(
&self,
request: tonic::Request<GetRouteRequest>,
Expand Down Expand Up @@ -256,6 +271,24 @@ impl StationApi for MyApi {
}))
}

async fn get_line_by_id_list(
&self,
request: tonic::Request<GetLineByIdListRequest>,
) -> Result<tonic::Response<MultipleLineResponse>, tonic::Status> {
let line_ids = &request.get_ref().line_ids;

let lines = match self.query_use_case.get_lines_by_id_vec(line_ids).await {
Ok(lines) => lines,
Err(err) => {
return Err(PresentationalError::OtherError(anyhow::anyhow!(err).into()).into())
}
};

Ok(Response::new(MultipleLineResponse {
lines: lines.into_iter().map(|line| line.into()).collect(),
}))
}

async fn get_lines_by_name(
&self,
request: tonic::Request<GetLinesByNameRequest>,
Expand Down
Loading