あいつの日誌β

働きながら旅しています。

gin 入門(4)

前回は CLI ツールを作成して記事の作成(タイトルのみ)を行えるようにしました。 今回は Markdown 形式で保存したテキストファイルから記事を作成するようにします。

仕様

  • Markdown 形式のファイルを読み込む
  • DB には Markdown の形式のまま保存する(Javascript側でパースする仕様と考えます)
  • Markdown 形式のまま保存するが、タイトルを抽出する必要がある

ということでタイトル部分を抽出する方法を考えます。

 テストケースを準備

テストケースを作成します。関数名がイマイチ。

package shared

import (
    "testing"
)

func TestFindTitle(t *testing.T) {
    matched := FindTitle([]byte("# this is title"))
    if string(matched) != "this is title" {
        t.Errorf("Expected 'this is title', got %s", matched)
    }
}

create shared/utils.go

package shared

func FindTitle(input []byte) []byte {
    return []byte("this is title")
}

テストを実行します

% go test shared/utils*
ok      command-line-arguments  0.006s

実装開始

とうわけで処理を考えます。怠惰な私は他人のモジュールにヒントを求めます。

https://github.com/russross/blackfriday/blob/master/block.go#L192

とりあえず文字列の先頭が '# ' の2文字で始まって、その後にスペースを含む文字列があったらそれ全部タイトルで良さげです。

Markdown に変換して HTML から H1 取り出してもいいんですが、そうするよりも自前で実装したほうがシンプルになりそうなので今回はそれで行こうと思います。

edit shared/utils.go:

package shared

func FindTitle(str string) string {
    if input[0] == '#' && input[1] == ' ' {
        return input[2:]
    }
    return nil
}

'#' と ' ' はダブルクォーテーションではなく、シングルクォーテーションです。 UTF-8エンコードした byte 値が欲しいからです。

ついでにテストケースを追加しておきます。

package shared

import (
    "testing"
)

func TestFindTitle(t *testing.T) {
    title := FindTitle("# this is title")
    if title != "this is title" {
        t.Errorf("Expected 'this is title', got %s", title)
    }

    title = FindTitle("## header2")
    if title != "" {
        t.Errorf("Expected epmpty string, got %s", title)
    }
}

テスト実行結果

% go test shared/utils* -v
=== RUN TestFindTitle
--- PASS: TestFindTitle (0.00s)
PASS
ok      command-line-arguments  0.006s

つなぎこむ

というわけで、あとは以下の作業が残っています。

  • 第一引数に filename を指定してテキストを読み込む
  • 必要な情報はテキストの1行目(仕様とします)
  • その1行目が FindTitle で空文字を返した場合はエラー

edit cli/main.go: 以下の箇所を修正します。

func (f *Publish) Run(args []string) int {
    fmt.Println("Publish", args)

    filename := args[0]
    fp, err := os.Open(filename)
    if err != nil {
        log.Fatalln("Error: ", err)
        return 1
    }
    defer fp.Close()
    reader := bufio.NewReaderSize(fp, 4096)

    // 最初の行はタイトル
    line, _, err := reader.ReadLine()
    if err != nil {
        log.Fatalln("Error: ", err)
        return 1
    }
    title := shared.FindTitle(string(line))
    if title == "" {
        log.Fatalln("Error: %s may be invalid markdown format.", filename)
        return 1
    }

    // 次の行は空白
    empty, _, err := reader.ReadLine()
    if err != nil {
        log.Fatalln("Error: ", err)
        return 1
    }
    if string(empty) != "" {
        log.Fatalln("Error: %s may be invalid markdown format.", filename)
        return 1
    }

    // ここから先が Desc
    var desc string
    for {
        line, _, err := reader.ReadLine()
        if err == io.EOF {
            break
        }
        desc += string(line) + "\n" // XXX: Best Practice を考える
    }

    dbmap := shared.NewDbMap(dsn)
    shared.CreateTablesIfNotExists(dbmap)
    article, err := shared.AddArticle(dbmap, title, desc)
    if err != nil {
        log.Fatalln("Error: ", err)
        return 1
    }
    fmt.Println("Created: ", article)
    return 0
}

func (f *Publish) Synopsis() string {
    return "publish <filename>"
}

あとは shared/db* を直しておきます。

edit shared/test_db.go

func TestAddArticle(t *testing.T) {
    dbmap := NewDbMap(dsn)
    CreateTablesIfNotExists(dbmap)
    defer dropAndClose(dbmap)
    AddArticle(dbmap, "Go is awesome", "Awesome description")
    rows, _ := dbmap.Select(Article{}, "SELECT * FROM articles")
    if len(rows) != 1 {
        t.Errorf("Expected 1 invoice rows, got %d", len(rows))
    }
    article := rows[0].(*Article)
    if article.Title != "Go is awesome" {
        t.Errorf("Expected Go is awesome, got %s", article.Title)
    }
    if article.Desc != "Awesome description" {
        t.Errorf("Expected Awesome description, got %s", article.Desc)
    }
}

edit shared/go.db

func AddArticle(dbmap *gorp.DbMap, title string, desc string) (article *Article, err error) {

    article = &Article{
        Title:   title,
        Desc:    desc,
        Created: time.Now().Unix(),
    }

    err = dbmap.Insert(article)
    return
}

テスト実行

% go test shared/*
ok      command-line-arguments  0.058s

というわけで以下のように適当な Markdown 形式のテキストファイルを作成します。

create cli/markdown.txt

# this is markdown

outline

## header 2

paragraph

cli を実行します

% go run cli/main.go publish cli/markdown.txt
Created:  &{2 this is markdown outline

## header 2

paragraph
 1440562341 0}

という訳で CLI で記事データを追加できるようになりました。 だんだん記事の内容が gin と関係なくなってきている...記事を書くの難しいな...

次回は記事一覧と記事詳細を返す JSON API を server に追加します。