課題20: 完全なCLIアプリケーション
課題概要
これはGo Foundationsコースの最終課題です。これまで学んだすべての知識を統合して、完全に機能するCLIアプリケーションを構築します。プロジェクト構成、設計、実装、テスト、ドキュメントまで、実際の開発プロセス全体を経験します。
マンダトリー要件(80点)
プロジェクト概要
ノート管理CLI(notecli) を作成してください。
機能要件:
- ノートの作成、編集、削除、一覧表示
- タグによる分類
- 検索機能
- JSONファイルによるデータ永続化
- 設定ファイルのサポート
要件1: プロジェクト構造(15点)
以下のディレクトリ構造でプロジェクトを構成してください。
notecli/
├── cmd/
│ └── notecli/
│ └── main.go
├── internal/
│ ├── cli/
│ │ └── cli.go
│ ├── note/
│ │ ├── note.go
│ │ └── note_test.go
│ ├── storage/
│ │ ├── storage.go
│ │ └── storage_test.go
│ └── config/
│ └── config.go
├── go.mod
├── go.sum
├── Makefile
├── README.md
└── .gitignore
要件2: データモデル(15点)
実装ファイル: internal/note/note.go
package note
import (
"time"
)
type Note struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Tags []string `json:"tags"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// New は新しいノートを作成します
func New(id int, title, content string, tags []string) *Note {
// TODO: 実装
}
// Update はノートを更新します
func (n *Note) Update(title, content string, tags []string) {
// TODO: 実装
}
// HasTag は指定されたタグを持つか確認します
func (n *Note) HasTag(tag string) bool {
// TODO: 実装
}
// MatchesSearch は検索クエリにマッチするか確認します
func (n *Note) MatchesSearch(query string) bool {
// TODO: 実装
// タイトルまたはコンテンツに検索文字列が含まれるか
}
テストファイル: internal/note/note_test.go
package note
import "testing"
func TestNote(t *testing.T) {
t.Run("New note", func(t *testing.T) {
// TODO: 新規作成のテスト
})
t.Run("Update note", func(t *testing.T) {
// TODO: 更新のテスト
})
t.Run("HasTag", func(t *testing.T) {
// TODO: タグ検索のテスト
})
t.Run("MatchesSearch", func(t *testing.T) {
// TODO: 検索マッチングのテスト
})
}
要件3: ストレージ層(20点)
実装ファイル: internal/storage/storage.go
package storage
import (
"encoding/json"
"os"
"sync"
"github.com/username/notecli/internal/note"
)
type Store struct {
filepath string
notes map[int]*note.Note
nextID int
mu sync.RWMutex
}
// NewStore はストアを初期化します
func NewStore(filepath string) (*Store, error) {
// TODO: 実装
// 1. ストア構造体を作成
// 2. ファイルが存在すればロード
}
// Add はノートを追加します
func (s *Store) Add(title, content string, tags []string) (*note.Note, error) {
// TODO: 実装
}
// Get は指定IDのノートを取得します
func (s *Store) Get(id int) (*note.Note, bool) {
// TODO: 実装
}
// Update はノートを更新します
func (s *Store) Update(id int, title, content string, tags []string) error {
// TODO: 実装
}
// Delete はノートを削除します
func (s *Store) Delete(id int) error {
// TODO: 実装
}
// List はすべてのノートを取得します
func (s *Store) List() []*note.Note {
// TODO: 実装
}
// Search は検索クエリにマッチするノートを取得します
func (s *Store) Search(query string) []*note.Note {
// TODO: 実装
}
// FilterByTag はタグでフィルタリングします
func (s *Store) FilterByTag(tag string) []*note.Note {
// TODO: 実装
}
// load はファイルからデータを読み込みます
func (s *Store) load() error {
// TODO: 実装
}
// save はデータをファイルに保存します
func (s *Store) save() error {
// TODO: 実装
}
要件4: CLI インターフェース(30点)
実装ファイル: internal/cli/cli.go
package cli
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/username/notecli/internal/storage"
)
type CLI struct {
store *storage.Store
}
func New() (*CLI, error) {
// ホームディレクトリにデータファイルを配置
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
dataFile := filepath.Join(home, ".notecli.json")
store, err := storage.NewStore(dataFile)
if err != nil {
return nil, err
}
return &CLI{store: store}, nil
}
func (c *CLI) Run() error {
if len(os.Args) < 2 {
c.printUsage()
return nil
}
command := os.Args[1]
switch command {
case "add":
return c.addNote(os.Args[2:])
case "list":
return c.listNotes(os.Args[2:])
case "show":
return c.showNote(os.Args[2:])
case "edit":
return c.editNote(os.Args[2:])
case "delete":
return c.deleteNote(os.Args[2:])
case "search":
return c.searchNotes(os.Args[2:])
case "tag":
return c.filterByTag(os.Args[2:])
case "help":
c.printUsage()
return nil
default:
return fmt.Errorf("unknown command: %s", command)
}
}
func (c *CLI) printUsage() {
fmt.Println("Note Manager CLI")
fmt.Println("\nUsage:")
fmt.Println(" notecli <command> [options]")
fmt.Println("\nCommands:")
fmt.Println(" add Add a new note")
fmt.Println(" list List all notes")
fmt.Println(" show Show a note")
fmt.Println(" edit Edit a note")
fmt.Println(" delete Delete a note")
fmt.Println(" search Search notes")
fmt.Println(" tag Filter notes by tag")
fmt.Println(" help Show this help message")
}
func (c *CLI) addNote(args []string) error {
fs := flag.NewFlagSet("add", flag.ExitOnError)
title := fs.String("title", "", "Note title (required)")
content := fs.String("content", "", "Note content")
tags := fs.String("tags", "", "Comma-separated tags")
fs.Parse(args)
if *title == "" {
return fmt.Errorf("title is required")
}
tagList := []string{}
if *tags != "" {
tagList = strings.Split(*tags, ",")
for i := range tagList {
tagList[i] = strings.TrimSpace(tagList[i])
}
}
note, err := c.store.Add(*title, *content, tagList)
if err != nil {
return err
}
fmt.Printf("Note added: [%d] %s\n", note.ID, note.Title)
return nil
}
func (c *CLI) listNotes(args []string) error {
// TODO: 実装
// すべてのノートを一覧表示
}
func (c *CLI) showNote(args []string) error {
// TODO: 実装
// 指定IDのノートを詳細表示
}
func (c *CLI) editNote(args []string) error {
// TODO: 実装
// ノートを編集
}
func (c *CLI) deleteNote(args []string) error {
// TODO: 実装
// ノートを削除
}
func (c *CLI) searchNotes(args []string) error {
// TODO: 実装
// キーワードでノートを検索
}
func (c *CLI) filterByTag(args []string) error {
// TODO: 実装
// タグでフィルタリング
}
使用例
# ノートを追加
$ notecli add -title "Go学習メモ" -content "contextパッケージについて" -tags "go,programming"
Note added: [1] Go学習メモ
# すべてのノートを一覧表示
$ notecli list
[1] Go学習メモ
Tags: go, programming
Created: 2024-01-15 10:00:00
# ノートを表示
$ notecli show -id 1
[1] Go学習メモ
Tags: go, programming
Created: 2024-01-15 10:00:00
Updated: 2024-01-15 10:00:00
Content:
contextパッケージについて
# 検索
$ notecli search -query "context"
Found 1 note(s):
[1] Go学習メモ
# タグでフィルタ
$ notecli tag -tag "go"
[1] Go学習メモ
# ノートを編集
$ notecli edit -id 1 -title "Go Context学習" -content "context.Contextの使い方"
Note updated: [1] Go Context学習
# ノートを削除
$ notecli delete -id 1
Note deleted: [1]
ボーナス課題(20点)
ボーナス1: 設定ファイル(5点)
設定ファイル(~/.notecli.yaml)のサポートを追加してください。
実装ファイル: internal/config/config.go
package config
import (
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
type Config struct {
DataFile string `yaml:"data_file"`
DefaultTag string `yaml:"default_tag"`
DateFormat string `yaml:"date_format"`
}
func Load() (*Config, error) {
// TODO: 実装
// 1. ~/.notecli.yamlを読み込み
// 2. 存在しなければデフォルト設定を返す
}
func (c *Config) Save() error {
// TODO: 実装
}
ボーナス2: エクスポート機能(5点)
ノートをMarkdown形式でエクスポートする機能を追加してください。
# 単一ノートをエクスポート
$ notecli export -id 1 -output note.md
# すべてのノートをエクスポート
$ notecli export -all -output notes/
ボーナス3: インポート機能(5点)
Markdown形式のファイルからノートをインポートする機能を追加してください。
$ notecli import -file note.md
ボーナス4: カラー出力(5点)
ターミナルでカラー出力を使って見やすくしてください。
package cli
import "fmt"
const (
ColorReset = "\033[0m"
ColorRed = "\033[31m"
ColorGreen = "\033[32m"
ColorYellow = "\033[33m"
ColorBlue = "\033[34m"
ColorCyan = "\033[36m"
)
func colorize(color, text string) string {
return color + text + ColorReset
}
func (c *CLI) listNotes(args []string) error {
notes := c.store.List()
for _, note := range notes {
fmt.Printf("%s [%d] %s\n",
colorize(ColorCyan, "Note"),
note.ID,
colorize(ColorGreen, note.Title))
}
return nil
}
評価基準
| 項目 | 配点 | 詳細 |
|---|---|---|
| プロジェクト構造 | 15点 | 適切なディレクトリ構造 |
| データモデル | 15点 | Note構造体と関連メソッド |
| ストレージ層 | 20点 | 永続化とCRUD操作 |
| CLI インターフェース | 30点 | すべてのコマンドが動作する |
| **ボーナス1: 設定** | 5点 | YAML設定ファイルのサポート |
| **ボーナス2: エクスポート** | 5点 | Markdownエクスポート |
| **ボーナス3: インポート** | 5点 | Markdownインポート |
| **ボーナス4: カラー出力** | 5点 | 見やすいカラー表示 |
提出方法
以下を含むGitリポジトリとして提出してください:
notecli/
├── cmd/
├── internal/
├── go.mod
├── go.sum
├── Makefile
├── README.md # 使い方とビルド方法
└── .gitignore
README.mdに含めるべき内容:
- プロジェクト概要
- インストール方法
- 使用例
- ビルド方法
- テスト実行方法
- [ ] プロジェクト構造が適切に構成されている
- [ ] Note構造体が実装されている
- [ ] ストレージ層が実装されている
- [ ] すべてのCLIコマンドが動作する
- [ ] ユニットテストが含まれている
- [ ] Makefileが含まれている
- [ ] README.mdが詳細に書かれている
- [ ] go.modが適切に設定されている
- 段階的に実装: まず基本機能を実装し、動作確認してから次へ
- テスト駆動: 各機能のテストを先に書く
- エラー処理: すべてのエラーを適切に処理
- ユーザビリティ: わかりやすいエラーメッセージ
- ドキュメント: コードコメントとREADME.mdを充実させる
- Project Layout
- Effective Go
- Go Code Review Comments
- Writing CLI Applications
必須要件チェックリスト
ヒント
学習リソース
最後に
これはあなたのGo学習の集大成です。丁寧に、そして楽しんで実装してください。完成したアプリケーションは、実際に日常で使える実用的なツールになります。
頑張ってください!