
<!-- path: tutorial/index.md -->
# チュートリアル

このプログラミング言語を学ぶのに役立つチュートリアルを紹介します。

## 一般向け

- [言語の基礎を学べるインタラクティブツアー](https://tour.moonbitlang.com)
- [初心者向けツアー](tour.md)
- [ネイティブ CLI クイックスタート](cli-quickstart.md)
- [1 つの MoonBit プロジェクトで作るフルスタック](fullstack-one-project.md)

## 言語移行ガイド

- [Go プログラマ向け MoonBit](for-go-programmers/index.md)

<!-- path: tutorial/tour.md -->
## 初心者向け MoonBit ツアー

このガイドは初学者向けであり、5 分で終わる駆け足のツアーではありません。MoonBit が可能にする、よりモダンで関数型的な書き方にまだ親しみのない人に向けて、簡潔で分かりやすい解説を目指しています。

すぐに言語仕様そのものを学びたい場合は、[言語の概要](../language/index.md) を参照してください。

### インストール

**拡張機能**

現在、MoonBit の開発サポートは VS Code 拡張機能を通じて提供されています。[VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=moonbit.moonbit-lang) にアクセスして、MoonBit の言語サポートをダウンロードしてください。

**ツールチェーン**

> （推奨）上の拡張機能をインストール済みであれば、アクションメニューで `Install moonbit toolchain` を実行するだけでランタイムを直接インストールできるため、この節は飛ばせます。![ランタイムのインストール](imgs/runtime-installation.png)

インストールスクリプトも用意しています。Linux と macOS では次の方法でインストールできます：

```bash
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
```

Windows ユーザーは PowerShell を使います：

```powershell
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser; irm https://cli.moonbitlang.com/install/powershell.ps1 | iex
```

これにより、MoonBit は `$HOME/.moon` に自動的にインストールされ、`PATH` に追加されます。

インストール後に `moon` が見つからない場合は、環境変数を反映させるためにターミナルまたは VS Code を再起動してみてください。

現時点の MoonBit はまだ本番利用向けではなく、活発に開発が進められています。MoonBit を更新するには、上記のコマンドをもう一度実行してください。

`moon help` を実行すると多くのサブコマンドが表示されます。ただし、ここで必要になるのは `build`、`run`、`new` だけです。

プロジェクト（より正確にはモジュール）を作成するには、`moon new <path>` を実行します。ここで `path` はプロジェクトを配置したい場所です。例えば `moon new examine` を実行すると、次のようになります：

```default
examine
├── Agents.md
├── cmd
│   └── main
│       ├── main.mbt
│       └── moon.pkg
├── LICENSE
├── moon.mod.json
├── moon.pkg
├── examine_test.mbt
├── examine.mbt
├── README.mbt.md
└── README.md -> README.mbt.md
```

この中には `cmd/main` ライブラリがあり、そこにはプログラムの入口となる `fn main` が含まれています。`cd examine && moon run cmd/main` を実行してみてください。

このチュートリアルでは、プロジェクト名を `examine`、現在の作業ディレクトリも `examine` と仮定します。

### 例: テストに合格した学生を探す

この例では、複数の学生の点数が与えられたとき、何人がテストに合格したかを求めてみます。

そのために、まずデータ型を定義し、必要な関数を洗い出して、テストを書きます。その後で関数を実装します。

特に断りがない限り、以下の定義は `examine.mbt` ファイルに記述します。

#### データ型

MoonBit の [基本データ型](../language/fundamentals.md#built-in-data-structures) には次のものがあります：

- `Unit`
- `Bool`
- `Int`, `UInt`, `Int64`, `UInt64`, `Byte`, ...
- `Float`, `Double`
- `Char`, `String`, ...
- `Array[T]`, ...
- タプルなど

学生 ID と点数を持つ構造体をプリミティブ型で表すには、学生 ID（型は `String`）と点数（型は `Double`）からなる 2 要素タプル `(String, Double)` を使えます。しかし、この表現は直感的ではありません。例えば、学生 ID と身長を持つ構造体のような他の型と区別できないからです。

そこで、[struct](../language/fundamentals.md#struct) を使って独自のデータ型を宣言します：

```moonbit
struct Student {
  id : String
  score : Double
}
```

試験は合格か不合格かのどちらかなので、判定結果は [enum](../language/fundamentals.md#enum) を使って次のように定義できます：

```moonbit
enum ExamResult {
  Pass
  Fail
}
```

#### 関数

[関数](../language/fundamentals.md#functions) とは、入力を受け取り、結果を返すコード片です。

この例では、学生が試験に合格したかどうかを判定する必要があります：

```moonbit
fn is_qualified(student : Student, criteria: Double) -> ExamResult {
  ...
}
```

この関数は、先ほど定義した `Student` 型の値 `student` と、科目や国ごとに基準が異なり得るため `Double` 型で表した基準値 `criteria` を受け取り、`ExamResult` を返します。

`...` 構文を使うと、関数をいったん未実装のままにできます。

また、試験に合格した学生が何人いるかを求める必要があります：

```moonbit
fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  ...
}
```

MoonBit では関数は第一級の値として扱われます。つまり、関数を変数に束縛したり、引数として渡したり、戻り値として受け取ったりできます。この関数は、学生の構造体の配列と、各学生が合格したかどうかを判定する別の関数を受け取ります。

#### テストを書く

関数の期待される振る舞いを定義するために、インラインテストを記述できます。これは、プログラムをリファクタリングしたときに回帰が起きないことを確かめるのにも役立ちます。

```moonbit
test "is qualified" {
  assert_eq(is_qualified(Student::{ id : "0", score : 50.0 }, 60.0), Fail)
  assert_eq(is_qualified(Student::{ id : "1", score : 60.0 }, 60.0), Pass)
  assert_eq(is_qualified(Student::{ id : "2", score : 13.0 }, 7.0), Pass)
}
```

`ExamResult` に `Show` と `Eq` が実装されていないことを示すエラーメッセージが出ます。

`Show` と `Eq` は **trait** です。MoonBit の trait は、ある型が提供すべき共通操作を定義します。

例えば `Eq` では、同じ型の 2 つの値を `op_equal` という関数で比較できることを定義します：

```moonbit
trait Eq {
  op_equal(Self, Self) -> Bool
}
```

一方 `Show` では、値を `String` に変換するか、`Logger` を使って出力できることを定義します：

```moonbit
trait Show {
  output(Self, &Logger) -> Unit
  to_string(Self) -> String
}
```

そして `assert_eq` はこれらの trait を使って、渡された 2 つの値を比較し、等しくない場合に表示できるよう、型パラメータに制約を付けています：

```moonbit
fn assert_eq![A : Eq + Show](value : A, other : A) -> Unit {
  ...
}
```

`ExamResult` に対して `Eq` と `Show` を実装する必要があります。方法は 2 つあります。

1. 明示的な実装を定義する方法です：
   ```moonbit
   impl Eq for ExamResult with equal(self, other) {
     match (self, other) {
       (Pass, Pass) | (Fail, Fail) => true
       _ => false
     }
   }
   ```

   ここでは [パターンマッチング](../language/fundamentals.md#pattern-matching) を使って `ExamResult` の各ケースを判定しています。
2. もう 1 つの方法は [derive](../language/derive.md) を使うことです。`Eq` と `Show` は[組み込み trait](../language/methods.md#builtin-traits) であり、`ExamResult` の出力はかなり単純です：
   ```moonbit
   enum ExamResult {
     Pass
     Fail
   } derive(Show)
   ```

trait を実装できたので、テストの実装を続けます：

```moonbit
test "count qualified students" {
  let students = [
    { id: "0", score: 10.0 },
    { id: "1", score: 50.0 },
    { id: "2", score: 61.0 },
  ]
  let criteria1 = fn(student) { is_qualified(student, 10) }
  let criteria2 = fn(student) { is_qualified(student, 50) }
  assert_eq(count_qualified_students(students, criteria1), 3)
  assert_eq(count_qualified_students(students, criteria2), 2)
}
```

ここでは [ラムダ式](../language/fundamentals.md#local-functions) により、先ほど定義した `is_qualified` を再利用して異なる基準を作成しています。

`moon test` を実行して、テストが成功するかどうかを確認できます。

#### 関数を実装する

`is_qualified` 関数は、単純な比較で実装できます：

```moonbit
fn is_qualified(student : Student, criteria : Double) -> ExamResult {
  if student.score >= criteria {
    Pass
  } else {
    Fail
  }
}
```

MoonBit では、最後の式の結果が関数の戻り値になり、`if` 式では各分岐の結果がその式の値になります。

`count_qualified_students` 関数では、各学生が合格したかどうかを調べるために配列を走査する必要があります。

素朴な方法としては、可変変数と [`for` ループ](../language/fundamentals.md#for-loop) を使います：

```moonbit
fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  let mut count = 0
  for i = 0; i < students.length(); i = i + 1 {
    if is_qualified(students[i]) == Pass {
      count += 1
    }
  }
  count
}
```

しかし、これは境界チェックが発生するため効率的でもなく、直感的でもありません。そこで `for` ループを [`for .. in` ループ](../language/fundamentals.md#for--in-loop) に置き換えられます：

```moonbit
fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  let mut count = 0
  for student in students {
    if is_qualified(student) == Pass { count += 1}
  }
  count
}
```

さらに別の方法として、[イテレータ](../language/fundamentals.md#iterator) 向けに定義された関数を使うこともできます：

```moonbit
fn count_qualified_students(
  students : Array[Student],
  is_qualified : (Student) -> ExamResult
) -> Int {
  students.iter().filter(fn(student) { is_qualified(student) == Pass }).count()
}
```

これで、先ほど定義したテストは通るはずです。

### ライブラリを公開する

最初の MoonBit ライブラリの完成、おめでとうございます。

これを他の開発者と共有すれば、同じことを繰り返し実装する必要がなくなります。

ただし、その前にもう少しやることがあります。

#### 可視性を調整する

自分たちのプログラムが他の人からどう使われるかを確認するために、MoonBit には [ブラックボックステスト](../language/tests.md#blackbox-tests-and-whitebox-tests) という仕組みがあります。

先ほど定義した `test` ブロックを、新しいファイル `top_test.mbt` に移してみましょう。

すると、次のようなエラーが出ます：

- `is_qualified` と `count_qualified_students` が未束縛になっている
- `Fail` と `Pass` が未定義になっている
- `Student` が struct 型ではなく、フィールド `id` が見つからない、など

これらはすべて可視性の問題に由来します。デフォルトでは、定義した関数は現在のパッケージ（現在のフォルダで区切られる範囲）の外部からは見えません。また、型はデフォルトで抽象型として扱われるため、`Student` 型と `ExamResult` 型が存在することしか分かりません。ブラックボックステストを使えば、他の人に公開したいものに、意図した可視性が正しく付いているかを確認できます。

他の人が関数を使えるようにするには、`fn` の前に `pub` を付けて関数を公開する必要があります。

他の人が型を構築して内容を読めるようにするには、`struct` と `enum` の前に `pub(all)` を付けて型を公開する必要があります。

また、`count qualified students` のテストに型注釈を追加するため、少し修正する必要があります：

```moonbit
test "count qualified students" {
  let students: Array[@examine.Student] = [
    { id: "0", score: 10.0 },
    { id: "1", score: 50.0 },
    { id: "2", score: 61.0 },
  ]
  let criteria1 = fn(student) { @examine.is_qualified(student, 10) }
  let criteria2 = fn(student) { @examine.is_qualified(student, 50) }
  assert_eq(@examine.count_qualified_students(students, criteria1), 3)
  assert_eq(@examine.count_qualified_students(students, criteria2), 2)
}
```

ここでは、パッケージ名である `@examine` を付けて型と関数にアクセスしています。これは他の人があなたのパッケージを使うときの書き方ですが、ブラックボックステスト内では省略できます。

これで再びコンパイルが通り、テストも成功するはずです。

#### ライブラリを公開する

準備ができたら、このプロジェクトを MoonBit のモジュールレジストリである [mooncakes.io](https://mooncakes.io) に公開できます。そこでは他にも興味深いプロジェクトを見つけられます。

1. `moon login` を実行し、案内に従って既存の GitHub アカウントでアカウントを作成します。
2. `moon.mod.json` のプロジェクト名を `<your github account name>/<project name>` に変更します。`moon.pkg` に他に影響する箇所がないか、`moon check` を実行して確認します。
3. `moon publish` を実行すれば完了です。これで他の人がこのプロジェクトを利用できるようになります。

デフォルトでは、このプロジェクトは [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) の下で公開されます。これは誰でも利用できる寛容なライセンスです。`moon.mod.json` の `license` フィールドと `LICENSE` の内容を変更すれば、[MulanPSL 2.0](https://spdx.org/licenses/MulanPSL-2.0.html) のような他のライセンスも使えます。

#### まとめ

ここまでで、MoonBit のごく基本的でありながら、決して単純ではない機能を学びました。しかし、MoonBit は機能豊富なマルチパラダイム言語です。文法や基本型についてさらに知りたい場合は [言語ツアー](https://tour.moonbitlang.com) を参照し、そのほかのドキュメントも読んで MoonBit への理解を深めてください。

<!-- path: tutorial/cli-quickstart.md -->
## ネイティブ CLI クイックスタート

このクイックスタートでは、シンプルで実用的な MoonBit CLI の構成を示します。

- 引数解析とビジネスロジックはライブラリパッケージに置く
- `cmd/main` は小さく保つ
- ネイティブ I/O には `moonbitlang/async` を使う
- ネットワークに触れずに純粋な部分をテストする

この例では `moonbitlang/async` を使用します。現時点では native バックエンドのサポートが最も充実しています。

### プロジェクトを作成する

通常の MoonBit モジュールから始めます。

```bash
moon new download_cli
cd download_cli
moon add moonbitlang/async@0.19.2
```

`argparse` はすでに標準ライブラリに含まれているため、このクイックスタートでは `moonbitlang/async` のみ追加します。

`moon.mod.json` で preferred target を native に設定すると、`moon run` と `moon build` は `moonbitlang/async` のサポートが最も良いバックエンドを既定で使います。

```json
{
  "name": "username/download_cli",
  "version": "0.1.0",
  "deps": {
    "moonbitlang/async": "0.19.2"
  },
  "preferred-target": "native"
}
```

最終的な構成は次のようになります。

```text
download_cli
├── cmd
│   └── main
│       ├── main.mbt
│       └── moon.pkg
├── cli_test.mbt
├── config.mbt
├── download.mbt
├── moon.mod.json
└── moon.pkg
```

### パース処理はライブラリパッケージに置く

ルートパッケージで CLI の契約を定義します。コマンドの形を定義し、argv を型付きの設定値に変換します。

```moonbit
pub struct Config {
  url : String
  output : String?
} derive(Eq)

///|
pub fn command() -> @argparse.Command {
  @argparse.Command(
    "moon-fetch",
    about="Download a URL to stdout or a file",
    options=[
      @argparse.OptionArg(
        "output",
        short='o',
        about="Write the response body to this file",
      ),
    ],
    positionals=[
      @argparse.PositionArg(
        "url",
        about="HTTP or HTTPS URL to download",
        num_args=@argparse.ValueRange::single(),
      ),
    ],
  )
}

///|
pub fn parse_config(argv : ArrayView[String]) -> Config raise {
  let matches = @argparse.parse(command(), argv~)
  let values : Map[String, Array[String]] = matches.values
  guard values is { "url": [url], "output"? : output_paths, .. } else {
    fail("missing url")
  }
  let output = match output_paths {
    Some([output, ..]) => Some(output)
    _ => None
  }
  { url, output }
}
```

パッケージ記述子では、`moonbitlang/core` の `argparse` と実装で使う async ライブラリをインポートします。

```moonbit
import {
  "moonbitlang/core/argparse",
  "moonbitlang/core/test",
  "moonbitlang/async/fs",
  "moonbitlang/async/http",
  "moonbitlang/async/stdio",
}
```

### async I/O は 1 つの関数に集約する

`run` が実際のダウンロード処理を行います。`-o` が渡された場合は本文をファイルへストリームし、それ以外は標準出力へ直接書き出します。

```moonbit
pub async fn run(config : Config) -> Unit {
  let (response, body) = @http.get_stream(config.url)
  defer body.close()

  guard response.code is (200..<300) else {
    fail("download failed: \{response.code} \{response.reason}")
  }

  match config.output {
    Some(path) => {
      let file = @fs.create(path, permission=0o644)
      defer file.close()
      file.write_reader(body)
      @stdio.stderr.write("saved \{config.url} to \{path}\n")
    }
    None => @stdio.stdout.write_reader(body)
  }
}
```

### `main` は薄く保つ

`cmd/main` は基本的に配線だけを担当します。argv を読み、設定を組み立て、ライブラリのエントリポイントを呼び出します。

```moonbit
import {
  "moonbit-community/cli-quickstart-doc" @app,
  "moonbitlang/core/env",
  "moonbitlang/async",
}

options(
  "is-main": true,
)
```

```moonbit
async fn main {
  let argv = @env.args()
  let config = @app.parse_config(argv[1:])
  @app.run(config)
}
```

### コマンドを実行する

レスポンス本文を標準出力に書き出す場合：

```bash
moon run cmd/main https://example.com/feed.xml
```

ファイルに書き出す場合：

```bash
moon run cmd/main https://example.com/feed.xml -o feed.xml
```

ネイティブバイナリをビルドする場合：

```bash
moon build --target native
```

### 純粋な部分をテストする

パーサーと設定組み立てロジックは I/O を行わないため、テストしやすいまま保てます。

```moonbit
///|
test "parse config for stdout" {
  let config = parse_config(["https://example.com/feed.xml"])
  assert_eq(config.url, "https://example.com/feed.xml")
  @test.assert_eq(config.output, None)
}

///|
test "parse config for file output" {
  let config = parse_config(["https://example.com/feed.xml", "-o", "feed.xml"])
  assert_eq(config.url, "https://example.com/feed.xml")
  guard config.output is Some(path) else { fail("expected an output path") }
  assert_eq(path, "feed.xml")
}
```

テストは次で実行します。

```bash
moon test
```

CLI が大きくなっても、同じ分離方針を維持してください。

- 入力のパースと検証はライブラリパッケージで行う
- 副作用は少数の async 関数に閉じ込める
- `cmd/main` は配線に専念させる

<!-- path: tutorial/fullstack-one-project.md -->
## 1 つの MoonBit プロジェクトで作るフルスタック

このチュートリアルでは、1 つの MoonBit モジュールで小さなフルスタックアプリを構築します。

共有のバリデーションルール一式を実装し、次の両方で使います。

- `frontend/`：ローカル警告を表示し、バックエンドを呼び出す
- `backend/`：再度バリデーションして JSON 応答を返す

ポイントは `supported-targets` です。

- `frontend/` は `js`
- `backend/` は `native`
- `shared/` はターゲット非依存

### 前提条件

- MoonBit ツールチェーンがインストール済み
- API テスト用に `hurl` がインストール済み

### ステップ 1: モジュールを作成する

```bash
moon new fullstack_one_project
cd fullstack_one_project
moon add moonbitlang/async@0.19.2
moon add moonbit-community/rabbita
```

プロジェクト構成：

```text
fullstack_one_project
├── Makefile
├── moon.mod.json
├── backend
│   ├── api.hurl
│   ├── index.html
│   ├── main.mbt
│   └── moon.pkg
├── frontend
│   ├── main.mbt
│   └── moon.pkg
└── shared
    ├── moon.pkg
    ├── shared_test.mbt
    └── task.mbt
```

モジュール設定：

```json
{
  "name": "moonbit-community/fullstack-one-project-doc",
  "version": "0.1.0",
  "deps": {
    "moonbitlang/async": "0.19.2",
    "moonbit-community/rabbita": "0.11.5"
  },
  "preferred-target": "native",
  "supported-targets": "+wasm+wasm-gc+js+native"
}
```

### ステップ 2: 共有ドメインバリデーションを実装する

`shared/` に `derive(ToJson, FromJson)` 付きのリクエスト/レスポンス型と、`suberror` ベースのバリデータを定義します。フロントエンドとバックエンドの両方がこのパッケージをインポートします。

```moonbit
import {
  "moonbitlang/core/json" @json,
}
```

```moonbit
pub(all) struct SubmitTitleRequest {
  title : String
} derive(Eq, ToJson, FromJson)

///|
pub(all) suberror TitleValidationError {
  EmptyTitle
  TooLong(Int)
  ForbiddenHash
} derive(Eq, ToJson, FromJson)

///|
pub(all) enum SubmitTitleResponse {
  Accepted(String)
  ValidationError(TitleValidationError)
  InvalidJson
} derive(Eq, ToJson, FromJson)

///|
pub fn validate_request(
  request : SubmitTitleRequest,
) -> Unit raise TitleValidationError {
  let title = request.title.trim().to_owned()
  if title.length() == 0 {
    raise EmptyTitle
  } else if title.length() > 24 {
    raise TooLong(title.length())
  } else if title.rev_find("#") is Some(_) {
    raise ForbiddenHash
  }
}

///|
pub fn warning_text(err : TitleValidationError) -> String {
  match err {
    EmptyTitle => "title cannot be empty"
    TooLong(length) => "title is too long (\{length}), max is 24"
    ForbiddenHash => "title cannot contain '#'"
  }
}

///|
pub impl Show for SubmitTitleResponse with output(self, logger) {
  let text = match self {
    Accepted(title) => "accepted: \{title}"
    ValidationError(err) => "validation_error: \{warning_text(err)}"
    InvalidJson => "invalid_json: invalid request json"
  }
  logger.write_string(text)
}
```

### ステップ 3: フロントエンド（`js`）を実装する

フロントエンドの挙動：

- 共有ルールでタイトルをローカル検証する
- 有効ならバックエンド `/submit` に `POST` する
- バックエンドの応答テキストを表示する

```moonbit
import {
  "moonbit-community/fullstack-one-project-doc/shared" @shared,
  "moonbitlang/core/json" @json,
  "moonbit-community/rabbita" @rabbita,
  "moonbit-community/rabbita/html" @html,
  "moonbit-community/rabbita/http" @rhttp,
}

supported_targets = "js"

options(
  "is-main": true,
)
```

```moonbit
fn main {
  let app = @rabbita.cell(
    model={ title: "", warning: None, server_message: None },
    update=(dispatch, msg, model) => {
      match msg {
        Edit(title) => {
          let warning = local_warning(title)
          (@rabbita.none, { title, warning, server_message: None })
        }
        Submit =>
          match model.warning {
            Some(message) =>
              (
                @rabbita.none,
                { ..model, server_message: Some("not sent: \{message}") },
              )
            None => {
              let request = @shared.SubmitTitleRequest::{ title: model.title }
              let request_json = request.to_json().stringify()
              let expect : @rhttp.Expecting[@rabbita.Cmd, Unit] = @rhttp.Expecting::Text(result => {
                  dispatch(ServerReplied(result))
                },
              )
              let cmd = @rhttp.post(
                "http://127.0.0.1:8080/submit",
                @rhttp.Body::Text(request_json),
                expect~,
              )
              (
                cmd,
                { ..model, server_message: Some("sending json request...") },
              )
            }
          }
        ServerReplied(result) => {
          let server_message = match result {
            Ok(raw_json) =>
              try {
                let response : @shared.SubmitTitleResponse = @json.from_json(
                  @json.parse(raw_json),
                )
                Some("\{response}")
              } catch {
                _ => Some("invalid backend response json")
              }
            Err(err) => Some("request failed: \{err}")
          }
          (@rabbita.none, { ..model, server_message, })
        }
      }
    },
    view=(dispatch, model) => {
      let warning_line = match model.warning {
        Some(message) => p("warning: \{message}")
        None => p("local validation passed")
      }
      let server_line = match model.server_message {
        Some(response) => p("backend response: \{response}")
        None => p("backend response: (none yet)")
      }
      let value = model.title
      div([
        h2("Shared Validation Demo"),
        input(
          input_type=Text,
          value~,
          on_input=text => dispatch(Edit(text)),
          nothing,
        ),
        button(on_click=dispatch(Submit), "Submit as JSON"),
        warning_line,
        server_line,
      ])
    },
  )
  @rabbita.new(app).mount("app")
}
```

### ステップ 4: バックエンド（`native`）を実装する

バックエンドの挙動：

- 静的ファイル `backend/index.html` を `GET /` で配信する
- フロントエンドのビルド成果物を `GET /frontend.js` で配信する
- 共有バリデーションで `POST /submit` を処理し、JSON レスポンスを返す

```moonbit
import {
  "moonbit-community/fullstack-one-project-doc/shared" @shared,
  "moonbitlang/core/json" @json,
  "moonbitlang/async",
  "moonbitlang/async/fs" @fs,
  "moonbitlang/async/http" @http,
  "moonbitlang/async/socket" @socket,
  "moonbitlang/async/stdio",
}

supported_targets = "native"

options(
  "is-main": true,
)
```

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Shared Validation Demo</title>
  </head>
  <body>
    <h1>Shared Validation Demo</h1>
    <p>Backend serves this page and the built frontend bundle.</p>
    <div id="app"></div>
    <script src="/frontend.js"></script>
  </body>
</html>
```

```moonbit
async fn main {
  @stdio.stdout.write("starting backend on http://127.0.0.1:8080\n")
  let server = @http.Server(@socket.Addr::parse("127.0.0.1:8080")) catch {
    err => {
      @stdio.stdout.write("failed to start backend: \{err}\n")
      return
    }
  }

  server.run_forever((request, body, conn) => {
    match (request.meth, request.path) {
      (Get, "/") =>
        send_file(
          conn, index_html_path, html_headers, "missing backend/index.html",
        )
      (Get, "/frontend.js") =>
        send_file(
          conn, frontend_js_path, js_headers, "missing frontend bundle; run `moon build frontend --target js`",
        )
      (Post, "/submit") => {
        let raw_body = body.read_all().text() catch { _ => "" }
        let response : @shared.SubmitTitleResponse = try {
          let request : @shared.SubmitTitleRequest = @json.from_json(
            @json.parse(raw_body),
          )
          try {
            @shared.validate_request(request)
            @shared.SubmitTitleResponse::Accepted(
              request.title.trim().to_owned(),
            )
          } catch {
            err => @shared.SubmitTitleResponse::ValidationError(err)
          }
        } catch {
          _ => @shared.SubmitTitleResponse::InvalidJson
        }
        let code = match response {
          @shared.SubmitTitleResponse::Accepted(_) => 200
          _ => 400
        }
        let reason = if code == 200 { "OK" } else { "BadRequest" }
        conn
        ..send_response(code, reason, extra_headers=json_headers)
        ..write(response.to_json().stringify())
        .end_response()
      }
      _ =>
        conn
        ..send_response(404, "NotFound", extra_headers=text_headers)
        ..write("Not Found")
        .end_response()
    }
  })
}
```

### ステップ 5: Makefile のショートカットを使う

```makefile
.PHONY: help build-frontend run-backend check test api-test verify verify-all clean

help:
	@echo "Targets:"
	@echo "  make build-frontend  Build frontend JS bundle"
	@echo "  make run-backend     Run backend server on 127.0.0.1:8080"
	@echo "  make check           Run moon check for all targets"
	@echo "  make test            Run moon test for all targets"
	@echo "  make api-test        Run Hurl API tests against local backend"
	@echo "  make verify          Run check + test"
	@echo "  make verify-all      Run verify + api-test"
	@echo "  make clean           Remove build artifacts"

build-frontend:
	moon build frontend --target js

run-backend:
	moon run backend --target native

check:
	moon check --deny-warn --target all

test:
	moon test --deny-warn --target all

api-test: build-frontend
	@command -v hurl >/dev/null 2>&1 || { echo "hurl is required for api-test"; exit 1; }
	@set -eu; \
		moon run backend --target native >/tmp/fullstack-one-project-backend.log 2>&1 & \
		pid=$$!; \
		trap 'kill $$pid >/dev/null 2>&1 || true' EXIT INT TERM; \
		sleep 1; \
		hurl --test backend/api.hurl

verify: check test

verify-all: verify api-test

clean:
	rm -rf _build
```

一般的なワークフロー:

```bash
make build-frontend
make run-backend
```

その後、ブラウザで `http://127.0.0.1:8080/` を開きます。

### ステップ 6: Hurl で API をテストする

Hurl テストスイート：

```hurl
GET http://127.0.0.1:8080/
HTTP 200
[Asserts]
body contains "<div id=\"app\"></div>"

GET http://127.0.0.1:8080/frontend.js
HTTP 200
[Asserts]
body contains "function"

POST http://127.0.0.1:8080/submit
Content-Type: application/json
{
  "title": "Write docs"
}
HTTP 200
[Asserts]
jsonpath "$[0]" == "Accepted"
jsonpath "$[1]" == "Write docs"

POST http://127.0.0.1:8080/submit
Content-Type: application/json
{
  "title": "bad #title"
}
HTTP 400
[Asserts]
jsonpath "$[0]" == "ValidationError"
jsonpath "$[1]" == "ForbiddenHash"

POST http://127.0.0.1:8080/submit
Content-Type: application/json
{
  "title": "01234567890123456789012345"
}
HTTP 400
[Asserts]
jsonpath "$[0]" == "ValidationError"
jsonpath "$[1][0]" == "TooLong"
jsonpath "$[1][1]" == 26

POST http://127.0.0.1:8080/submit
Content-Type: application/json
```
{"title":
```
HTTP 400
[Asserts]
jsonpath "$" == "InvalidJson"
```

実行方法：

```bash
make api-test
```

このテストで検証する内容：

- 静的 `GET /` と `GET /frontend.js`
- 受理される送信（`200`）
- 不正なタイトルの送信が拒否される（`400`）
- 不正な JSON 入力が拒否される（`400`）

### ステップ 7: 全体を検証する

```bash
make verify-all
```

実行される内容：

- `moon check --deny-warn --target all`
- `moon test --deny-warn --target all`
- Hurl API テスト

これで、フロントエンドとバックエンドが同じバリデーション契約とエラーモデルを共有する、実行可能なプロジェクトが完成します。
