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
11 changes: 11 additions & 0 deletions .devcontainer/cspell-dict/mountix-words.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
2dsphere
binstall
clippy
defaultauthdb
dotenv
dotenvy
dtolnay
geosearch
geospatial
INITDB
lldb
mockall
mongoimport
mongosh
mountix
robbyrussell
rustup
untap
28 changes: 28 additions & 0 deletions .github/workflows/test-and-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Test and Lint

on:
push:
branches-ignore:
- main
pull_request:
branches:
- '*'
workflow_dispatch:
workflow_call:

jobs:
test-and-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Check formatting
run: cargo fmt -- --check

- name: Run Clippy
run: cargo clippy -- -D warnings
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ Temporary Items

# mountix
.env

# Claude
.claude/*.local.json
8 changes: 8 additions & 0 deletions .zshrc
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# shellcheck disable=all

# Activate Mise
eval "$(mise activate zsh)"
export PATH="$HOME/.local/share/mise/shims:$PATH"

# If you come from bash you might have to change your $PATH.
# export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH
# Path to your Oh My Zsh installation.
export ZSH="$HOME/.oh-my-zsh"
export ZSH_CUSTOM="$ZSH/custom"

# Settings to persist history file
export HISTFILE=/root/.history/.zsh_history

# Clone zsh-autosuggestions plugin if it doesn't exist
if [ ! -d $ZSH_CUSTOM/plugins/zsh-autosuggestions ]; then
git clone https://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions
Expand Down
96 changes: 96 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# CLAUDE.md

このファイルは、このリポジトリでコードを扱う際にClaude Codeにガイダンスを提供します。

## プロジェクト概要

Mountixは、クリーンアーキテクチャパターンを使用してRustで構築された日本の山岳データAPIです。百名山データを含む山岳情報のRESTエンドポイントを提供し、MongoDBをデータストアとして使用しています。

## 開発コマンド

### ビルドと実行

- **ビルド**: `cargo build`
- **実行**: `cargo run`
- **チェック**: `cargo check`
- **フォーマット**: `cargo fmt`
- **リント**: `cargo clippy`
- **テスト**: `cargo test`
- **カバレッジ**: `cargo llvm-cov`

### 環境設定

`sample.env`を`.env`にコピーして設定:
- `DATABASE_URL`にMongoDB接続文字列
- `DATABASE_NAME=mountix_db`
- サーバーのホスト/ポート設定

### データベースセットアップ

開発環境(devcontainer)の場合、起動時にマイグレーションが完了します。

```bash
cd ./migrations/
./migrate.sh
cd ../
```

## インフラストラクチャ

インフラストラクチャは[Render](https://render.com/)を採用しています。

## アーキテクチャ

このプロジェクトは依存性逆転を伴う階層化クリーンアーキテクチャに従います:

### レイヤー構造(上位から下位)
1. **mountix-driver** (Controller/Presentation)
- Axum Webフレームワークのセットアップとルーティング
- HTTPハンドラーとJSONシリアライゼーション
- エントリーポイント: `startup/mod.rs`にサーバー設定
- ルート: `/api/v1/mountains`, `/api/v1/hc` (ヘルスチェック)

2. **mountix-app** (Use Case/Application)
- ビジネスロジックとアプリケーションワークフロー
- kernelとadapterレイヤー間の調整
- ユースケース実装を含む

3. **mountix-kernel** (Domain/Core)
- ドメインモデルとビジネスルール
- リポジトリトレイト定義(adapterで実装)
- 外部依存性のないコアビジネスロジック

4. **mountix-adapter** (Infrastructure)
- MongoDB永続化実装
- 外部サービス統合
- kernelのリポジトリトレイトを実装

### 依存関係ルール

- 上位レイヤーは下位レイヤーに依存可能
- 下位レイヤーは上位レイヤーに依存不可
- kernelとadapterはDIP(依存性逆転の原則)を使用

## 主要技術

- **Webフレームワーク**: Axum with tokio async runtime
- **データベース**: MongoDB(`DATABASE_URL`経由で接続)
- **ログ**: tracingによるJSON出力
- **環境**: dotenvy(.envファイル読み込み)
- **CORS**: API アクセス用に設定済み

## APIエンドポイント

- `GET /api/v1/mountains` - フィルタリング付き山岳リスト
- `GET /api/v1/mountains/{id}` - 特定の山岳取得
- `GET /api/v1/mountains/{id}/surroundings` - 周辺の山岳取得
- `GET /api/v1/mountains/geosearch` - 地理的検索
- `GET /api/v1/hc` - ヘルスチェック
- `GET /api/v1/hc/mongo` - MongoDBヘルスチェック

## 開発ノート

- 4つのクレートからなるRustワークスペースを使用
- 日本の山岳データドメイン(百名山)に従う
- 実行前に環境設定が必要
- クラウドデプロイにはRenderとMongoDB Atlasを推奨
20 changes: 19 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# Rust development environment for devcontainer
FROM rust:1.87.0-slim-bullseye

ENV LANG=ja_JP.UTF-8
ENV LANGUAGE=ja_JP:ja
ENV LC_ALL=ja_JP.UTF-8
ENV MISE_GLOBAL_CONFIG_FILE=/workspace/mise.toml

# Install packages required for development
RUN apt-get update && apt-get install -y \
# Locales
locales \
# Basic tools
git \
curl \
wget \
vim \
nano \
zsh \
unzip \
# Build tools
build-essential \
pkg-config \
Expand All @@ -19,11 +27,21 @@ RUN apt-get update && apt-get install -y \
lldb \
# Network tools
net-tools \
# Generate Japanese locale
&& echo "ja_JP.UTF-8 UTF-8" >> /etc/locale.gen \
&& locale-gen \
# Clean up
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Optimize Rust settings
RUN rustup component add rustfmt clippy llvm-tools-preview
RUN cargo install cargo-llvm-cov
RUN cargo install cargo-llvm-cov cargo-binstall

# Install Mise
COPY mise.toml /workspace/mise.toml
RUN cargo binstall mise
RUN mise install node

# Oh My Zsh
RUN sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
Expand Down
9 changes: 9 additions & 0 deletions migrations/devcontainer/migrate-in-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ mongoimport \
--collection=mountains \
--file="/docker-entrypoint-initdb.d/mountix_db-mountains.json"

echo "Creating geospatial index on mountains collection..."

mongosh \
--username="$MONGO_INITDB_ROOT_USERNAME" \
--password="$MONGO_INITDB_ROOT_PASSWORD" \
--authenticationDatabase=admin \
mountix_db \
--eval "db.mountains.createIndex({ location: '2dsphere' });"

echo "Completed migration"
19 changes: 16 additions & 3 deletions migrations/migrate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,23 @@ source ./.env
brew tap mongodb/brew
brew install mongodb-database-tools

echo "Migrating data into MongoDB..."

mongoimport $DATABASE_URL \
--collection=mountains \
--db=mountix_db \
--file="./data/mountix_db-mountains.json"
--collection=mountains \
--db=mountix_db \
--file="./data/mountix_db-mountains.json"

echo "Creating geospatial index on mountains collection..."

mongosh \
--username="$MONGO_INITDB_ROOT_USERNAME" \
--password="$MONGO_INITDB_ROOT_PASSWORD" \
--authenticationDatabase=admin \
mountix_db \
--eval "db.mountains.createIndex({ location: '2dsphere' });"

echo "Completed migration"

brew remove mongodb-database-tools
brew untap mongodb/brew
4 changes: 4 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[tools]
node = "22.16.0"
gh = "latest"
"npm:@anthropic-ai/claude-code" = "latest"
4 changes: 4 additions & 0 deletions mountix-adapter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ tokio = { version = "1.45.1", features = ["full"] }
serde = { version = "1.0.219", features = ["derive"] }
futures = "0.3.31"
mongodb = "3.2.3"

[dev-dependencies]
tokio-test = "0.4.4"
mockall = "0.13.0"
31 changes: 17 additions & 14 deletions mountix-adapter/src/model/mountain.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use mongodb::bson::{doc, Document};
use mongodb::options::FindOptions;
use mountix_kernel::model::mountain::{
Mountain, MountainBoxSearchCondition, MountainLocation, MountainSearchCondition,
Mountain, MountainBoxSearchCondition, MountainData, MountainLocation, MountainSearchCondition,
};
use mountix_kernel::model::Id;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -31,22 +31,24 @@ impl TryFrom<MountainDocument> for Mountain {
fn try_from(mountain_doc: MountainDocument) -> Result<Self, Self::Error> {
let mountain_id: Id<Mountain> = mountain_doc.id.into();

// MongoDBの地理的データ形式: [longitude, latitude]
// Rustの座標形式: (latitude, longitude)
let mountain_location = MountainLocation::new(
mountain_doc.location.coordinates[1],
mountain_doc.location.coordinates[0],
mountain_doc.location.coordinates[1], // latitude (緯度)
mountain_doc.location.coordinates[0], // longitude (経度)
mountain_doc.gsi_url,
);

Ok(Mountain::new(
mountain_id,
mountain_doc.name,
mountain_doc.name_kana,
mountain_doc.area,
mountain_doc.prefectures,
mountain_doc.elevation,
mountain_location,
mountain_doc.tags,
))
let data = MountainData {
name: mountain_doc.name,
name_kana: mountain_doc.name_kana,
area: mountain_doc.area,
prefectures: mountain_doc.prefectures,
elevation: mountain_doc.elevation,
location: mountain_location,
tags: mountain_doc.tags,
};
Ok(Mountain::new(mountain_id, data))
}
}

Expand Down Expand Up @@ -76,7 +78,7 @@ impl TryFrom<MountainSearchCondition> for MountainFindCommand {
and_doc.push(doc! {"tags": &tag_name});
}

if and_doc.len() > 0 {
if !and_doc.is_empty() {
filter.insert("$and", and_doc);
}

Expand Down Expand Up @@ -104,6 +106,7 @@ impl TryFrom<MountainBoxSearchCondition> for MountainFindBoxCommand {

fn try_from(sc: MountainBoxSearchCondition) -> Result<Self, Self::Error> {
let mut filter = Document::new();
// MongoDBの地理的ボックス検索では [longitude, latitude] の順序が必要
let mut and_doc = vec![
doc! {"location": {"$geoWithin": {"$box": [[sc.box_coordinates.bottom_left.0,sc.box_coordinates.bottom_left.1], [sc.box_coordinates.upper_right.0,sc.box_coordinates.upper_right.1]]}}},
];
Expand Down
34 changes: 19 additions & 15 deletions mountix-adapter/src/model/surrounding_mountain.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use mongodb::bson::{doc, Document};
use mountix_kernel::model::surrounding_mountain::{
SurroundingMountain, SurroundingMountainLocation, SurroundingMountainSearchCondition,
SurroundingMountain, SurroundingMountainData, SurroundingMountainLocation,
SurroundingMountainSearchCondition,
};
use mountix_kernel::model::Id;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -30,22 +31,24 @@ impl TryFrom<SurroundingMountainDocument> for SurroundingMountain {
fn try_from(mountain_doc: SurroundingMountainDocument) -> Result<Self, Self::Error> {
let mountain_id: Id<SurroundingMountain> = mountain_doc.id.into();

// MongoDBの地理的データ形式: [longitude, latitude]
// Rustの座標形式: (latitude, longitude)
let mountain_location = SurroundingMountainLocation::new(
mountain_doc.location.coordinates[1],
mountain_doc.location.coordinates[0],
mountain_doc.location.coordinates[1], // latitude (緯度)
mountain_doc.location.coordinates[0], // longitude (経度)
mountain_doc.gsi_url,
);

Ok(SurroundingMountain::new(
mountain_id,
mountain_doc.name,
mountain_doc.name_kana,
mountain_doc.area,
mountain_doc.prefectures,
mountain_doc.elevation,
mountain_location,
mountain_doc.tags,
))
let data = SurroundingMountainData {
name: mountain_doc.name,
name_kana: mountain_doc.name_kana,
area: mountain_doc.area,
prefectures: mountain_doc.prefectures,
elevation: mountain_doc.elevation,
location: mountain_location,
tags: mountain_doc.tags,
};
Ok(SurroundingMountain::new(mountain_id, data))
}
}

Expand All @@ -57,9 +60,10 @@ impl TryFrom<SurroundingMountainSearchCondition> for SurroundingMountainFindComm
type Error = anyhow::Error;

fn try_from(sc: SurroundingMountainSearchCondition) -> Result<Self, Self::Error> {
// MongoDBの地理的クエリでは [longitude, latitude] の順序が必要
let coordinates = (
sc.mountain.location.longitude,
sc.mountain.location.latitude,
sc.mountain.location.longitude, // longitude (経度)
sc.mountain.location.latitude, // latitude (緯度)
);
let filter = doc! {"$and": [{"location":{"$nearSphere": {"$geometry": { "type": "Point", "coordinates": [coordinates.0, coordinates.1]},"$minDistance": 0,"$maxDistance": sc.distance.0}}}, {"_id": {"$ne": &sc.mountain.id.value}}]};

Expand Down
Loading