20191021のGoに関する記事は7件です。

messagen: 制約に基づくメッセージ生成ツール

TL;DR

messagenはyamlかgolangで定義したテンプレートと制約に基づいてメッセージを生成するツールです。
具体的に何ができるのかは、利用例をご覧ください✌️

Intro

皆さんは診断メーカーをご存知でしょうか。Twitterで以下のようなツイートを一度は見かけたことがあると思います。

ところでこの診断メーカー、遊んだことがある方は多いと思いますが、診断を作ったことはありますか?
診断メーカーの診断作成画面では、以下のように「診断結果基本テキスト」から「リスト」を参照して作っていく仕組みになっています。

スクリーンショット 2019-09-22 18.43.09.png

messagenは診断メーカーのように、テンプレートとリストからメッセージを生成するツールです。YAMLかGo言語で定義を記述することで、メッセージを生成します。
例えば、診断メーカーのサンプルである好きなフルーツを表示する例は、messangenでは以下のように書くことができます。

Definitions:
  - Type: Root
    Templates: 
      - |-
        {{.User}}さんにオススメの果物は
        「{{.Fruit}}」
        です
  - Type: Fruit
    Templates: ["りんご", "みかん", "バナナ"]
$ messagen -f def.yaml --state User=Bob
Bobさんにオススメの果物は
「りんご」
です

診断メーカーと異なるのは、選択されるリスト(messagenでは定義と呼びます)に対して制約をかけられる点です。例えば、Bobはりんごが嫌いなので、それ以外から選ばなければならないとします。この場合、以下のように制約をかけることで、UserがBobの場合りんごが選択されなくなります。(詳細な記法については後ほど説明します。)

Definitions:
  - Type: Root
    Templates: 
      - |-
        {{.User}}さんにオススメの果物は
        「{{.Fruit}}」
        です
  - Type: Fruit
    Templates: ["みかん", "バナナ"]
  - Type: Fruit
    Templates: ["りんご"]
    # UserがBobである場合、りんごは選択されない
    Constraints: {"User!": "Bob"}

messagenで具体的にどのようなことができるのか知りたい方は利用例を先にご覧ください。

背景

messagenが診断メーカーや他のツールと比較してより多機能であることは、より複雑であることも意味します。「制約」は、この複雑さを許容可能にするほど重要な概念なのでしょうか?
この点についてご説明するために、私がmessagenを作り始めた背景についてご紹介します。

私はスタバ警察botというのを作っています。これはリプライで送られてきた画像がスタバかどうかを判定し、結果に応じてメッセージを返すというものです。

このメッセージは、判定結果に応じたテンプレートを元に、微妙に言い回しを毎回ランダムに生成しています。まさに診断メーカーと同じような機能が必要です。しかし加えて、次のような機能も必要であることがわかりました。

  • 排他的なメッセージの組み合わせを定義できる
    • 「一人称」の名前は「名前」だ。というメッセージで、一人称が「俺」の場合、名前は男性名だけが選ばれてほしいことがあります。(誰でも好きな一人称を使えば良いと思いますが、あくまで例です)
    • デバッグモードの時はメッセージをガラッと変える、とかもやりたくなります
  • 選択される単語の確率を変えたい

    • 10回に1回しか選ばれないレアメッセージとかですね
  • 生成されたメッセージのバリデーション

    • Twitterだと生成したメッセージの文字数が140文字を超えると困ります
  • 動的な値の代入ができる

    • スタバ警察だと、画像の判定確率をメッセージに含めたくなります
  • 言語からライブラリとして利用できる

    • スタバ警察はGo言語で実装されているので、メッセージ生成ライブラリもGo言語から呼び出したいです

このような要求を満たすため、messagenが生まれました。

詳説 messagen

冒頭のYAML定義について詳しく見てみましょう。

Definitions:
  - Type: Root
    Templates: 
      - |-
        {{.User}}さんにオススメの果物は
        「{{.Fruit}}」
        です
  - Type: Fruit
    Templates: ["りんご", "みかん", "バナナ"]

messagenでは、定義(Definition)の集合でメッセージを生成します。
定義はTypeとTemplatesを持ちます。TypeはそのDefinitionを参照する際のIDのようなものです。
TemplatesはそのDefinitionが選ばれた時に実際に生成するメッセージです。複数ある場合はランダムに選択されます。(正確にはPickerにより制御されるのですが、詳細は後ほど)
もしTemplate内部で他のDefinition Typeを参照している場合、先にそのDefinitionからメッセージを生成します。つまりDefinitionのツリー構造を深さ優先で解決していくわけです。上記の例では、「Root」というTypeを持つ定義のTemplateで「User」と「Fruit」Typeが参照されています。Fruit定義は3つのTemplateを持っているので、この中からランダムに一つが選ばれます。Userはどこにも定義がないので、実行時に与える前提です。
選択された定義とそれにより生成されたメッセージの集合をStateと呼びます。

messagenコマンドで上記yamlを実行すると、ランダムにメッセージが表示されます。
stateフラグを用いることで初期Stateを指定できます。ここではUserだけを与えていますが、カンマ区切りで複数与えることも可能です。

$ messagen -f def.yaml --state User=Bob
Bobさんにオススメの果物は
「りんご」
です

また、messagenはgolangのライブラリとしても使えます。
この場合、上記の例は次のように書くことができます。

func main() {
  rand.Seed(42)
    generator, _ := messagen.New()

    definitions := []*messagen.Definition{
        {
            Type: "Root",
            Templates: []string{"{{.User}}さんにオススメの果物は\n「{{.Fruit}}」\nです"},
        },
        {
            Type: "Fruit",
      Templates: []string{"りんご", "みかん", "バナナ"}
        },
  }

  initialState := map[string]string{"User": "Bob"}

    generator.Add(definitions...)
  messages, _ := generator.Generate("Root", initialState, 1)
  print(messages[0])
}

定義の重み付け

Weightキーでその定義が選択される確率を設定できます。同じTypeを持つ定義は幾つでも定義できるので、特定の定義だけ選ばれる確率を変えることができます。

definitions := []*messagen.Definition{
        {
            Type: "Fruit",
      Templates: []string{"りんご", "みかん", "バナナ"}
        },
    {
      Type: "Fruit",
      Templates: []string{"ドラゴンフルーツ"}
      Weight: 0.1 // 1/10の重みになる。デフォルトは1なので、この例では0.1/(1+0.1)の確率で選ばれる
    },
  }

定義ごとの制約

messagen最大の特徴が冒頭でも述べた定義の制約(Constraints)です。
各定義には、現在のStateを元に選ばれても良いかどうかの制約を与えることができます。
例えば、一人称に応じて名前を出し分ける例は以下になります。

Definitions:
  - Type: Root
    Templates: ["{{.Pronoun}}の名前は{{.Name}}。"]
  - Type: Pronoun
    Templates: ["あたし", "俺"]
  - Type: Name
    Templates: ["太郎", "次郎"]
    Constraints: {"Pronoun": "俺"}
  - Type: Name
    Templates: ["花子", "佳子"]
    Constraints: {"Pronoun": "あたし"}

この例では、一人称(Pronoun)に応じて、選択されるNameが決定されます。
例えば、上から3つ目のNameでは、ConstraintsでPronounが"俺"であることが要求されているため、"あたし"の場合は選択されません。

Constraints Operator

Constraintsでは、キーの最後に記号を付けることで特殊な挙動をさせることができます。

Operator 意味
? キーが存在しなくても選択可能になる
+ ?に加えて、存在しなかった場合はキーと値を追加する
! 指定された値でない場合に選択可能になる
/ 値を正規表現として解釈し、マッチすれば選択可能になる
:[Priority] プライオリティを指定する(後述)

例えば、先ほどの例で一人称のバリエーションを増やしたいとします。素朴には以下のように、Pronounを正規表現でマッチさせることができます。

Definitions:
  - Type: Root
    Templates: ["{{.Pronoun}}の名前は{{.Name}}です"]
  - Type: Pronoun
    Templates: ["あたし", "わたくし", "俺", "わし"]
  - Type: Name
    Templates: ["太郎", "次郎"]
    Constraints: {"Pronoun/": "俺|わし"}
  - Type: Name
    Templates: ["花子", "佳子"]
    Constratins: {"Pronoun/": "あたし|わたくし"}

が、一人称がもっと増えてくると破綻の予感がします。
そこで代わりに+operatorを使ってみます。

Definitions:
  - Type: Root
    Templates: ["{{.Pronoun}}の名前は{{.Name}}です"]
  - Type: Pronoun
    Templates: ["あたし", "わたくし"]
    Constraints: {"Gender+": "女"}
  - Type: Pronoun
    Templates: ["俺", "わし"]
    Constraints: {"Gender+": "男"}
  - Type: Name
    Templates: ["太郎", "次郎"]
    Constraints: {"Gender": "男"}
  - Type: Name
    Templates: ["花子", "佳子"]
    Constratins: {"Gender": "女"}

こうすると、一人称を増やしても他の定義には影響を与えません。

制約の優先度

食べ物の説明文を考えます。

Definitions:
  - Type: Root
    Templates: ["この{{.Food}}は{{.Description}}です"]
  - Type: Food
    Templates: ["ケーキ", "ハンバーグ", "オムライス"]
  - Type: Description
    Templates: ["ふわふわ", "クリームたっぷり"]
    Constraints: {"Food": "ケーキ"}
  - Type: Description
    Templates: ["美味しそう"]
$ messagen run -f def.yaml
このケーキは美味しそうです

一番下のDescriptionは制約がないため、どんなFoodが選ばれても使われる可能性があります。
しかし、ケーキ専用のDescirptionを下から2番目の定義として作っているので、ケーキが選ばれたら必ずこの定義を使いたいとします。このような時はConstraints priorityを使います。

Definitions:
  - Type: Root
    Templates: ["この{{.Food}}は{{.Description}}です"]
  - Type: Food
    Templates: ["ケーキ", "ハンバーグ", "オムライス"]
  - Type: Description
    Templates: ["ふわふわ", "クリームたっぷり"]
    Constraints: {"Food:1": "ケーキ"} # キーの:1がConstraints Priority
  - Type: Description
    Templates: ["美味しそう"]

Type:[PriorityNum]と書くと、その制約にマッチした場合にプライオリティ分の値が加算されます。候補の定義のうち、最大のプライオリティを持つ定義が選択されます。(同じ値の場合は、Weightに従ってランダムに選ばれます)
プライオリティを明示しなかった場合は0として扱われます。
上記の例でFoodとしてケーキが選ばれた場合、上のDescriptionはプライオリティ1、下はプライオリティ0なので、必ず上のDescriptionが選ばれます。

定義の解決順

食べ物とその味の表現を表すテンプレートを考えます。

Definitions:
  - Type: Root
    Templates: ["{{.Adjective}}な{{.Food}}"]
  - Type: Adjective
    Templates: ["ふわふわ", "クリームたっぷり"]
    Constraints: {"FoodGenre+": "ケーキ"}
  - Type: Adjective
    Templates: ["ジューシー", "ボリューミー"]
    Constraints: {"FoodGenre+": "肉"}
  - Type: Food
    Templates: ["いちごケーキ", "モンブラン", "チーズケーキ", "ガトーショコラ"]
    Constraints: {"FoodGenre": "ケーキ"}
  - Type: Food
    Templates: ["ハンバーグ", "ステーキ"]
    Constraints: {"FoodGenre": "肉"}
    Weight: 0.5 # ここで指定しても、先にAdjectiveが選ばれるため意味がない

ケーキは4種類あるのに対して、肉は2種類しかないので、肉の登場確率をWeightを0.5にしました。しかし、テンプレート内の定義はデフォルトでは出てきた順に解決されるので、先にAdjectiveがそれぞれ同じ確率で選ばれてしまいます。肉の登場確率を下げるには、肉に関連するAdjectiveへWeightを与える必要があります。しかし、Weightを与えたい定義と実際にWeightを書く定義が離れていると混乱しますし、もっと複雑な定義になると破綻しそうです。
定義の解決順をOrderで指定すれば、この問題を解決することができます。

Definitions:
  - Type: Root
    Templates: ["{{.Adjective}}な{{.Food}}"]
    Order: ["Food"] # Foodから解決される. 省略されたTypeはTemplateの前から解決される
  - Type: Adjective
    Templates: ["ふわふわ", "クリームたっぷり"]
    Constraints: {"FoodGenre+": "ケーキ"}
  - Type: Adjective
    Templates: ["ジューシー", "ボリューミー"]
    Constraints: {"FoodGenre+": "肉"}
  - Type: Food
    Templates: ["いちごケーキ", "モンブラン", "チーズケーキ", "ガトーショコラ"]
    Constraints: {"FoodGenre": "ケーキ"}
  - Type: Food
    Templates: ["ハンバーグ", "ステーキ"]
    Constraints: {"FoodGenre": "肉"}
    Weight: 0.5 # 先にFoodが選ばれるのでOK

エイリアス

一つの定義から複数のメッセージを生成したい場合があります。

Definitions:
  - Type: Root
    Templates: ["「{{.Adjective}}」かつ「{{.Adjective}}」な{{.Food}}"]
  - Type: Adjective
    Templates: ["ふわふわ", "クリームたっぷり"]
  - Type: Food
    Templates: ["いちごケーキ", "モンブラン", "チーズケーキ", "ガトーショコラ"]

しかし、定義から生成されたメッセージはStateに保存され、以降は同じ値が参照されます。なので、上記の例では、2つのAdjectiveは必ず同じ値になります。

$ messagen run -f def.yaml
「ふわふわ」かつ「ふわふわ」なモンブラン

そこで、エイリアスを用いて同じ定義から複数のメッセージを取り出します。AllowDuplicateで同じ定義から重複した値が生成されることを許可するかどうかを決められます。

Aliases:

Definitions:
  - Type: Root
    Templates: ["「{{.Adjective}}」かつ「{{.AnotherAdjective}}」な{{.Food}}"]
    Alias:
      AnotherAdjective: {"Type": "Adjective", "AllowDuplicate": false}
  - Type: Adjective
    Templates: ["ふわふわ", "クリームたっぷり"]
  - Type: Food
    Templates: ["いちごケーキ", "モンブラン", "チーズケーキ", "ガトーショコラ"]
$ messagen run -f def.yaml
「ふわふわ」かつ「クリームたっぷり」なモンブラン

Picker & Validator

messagen組み込みの制約では表現できないメッセージ生成ルールを適用したい場合があります。例えば、特定の単語を含むテンプレートを優先して利用する、twitterに投稿するために文字数を140文字以下にするなどです。
これらを実現するために、picker / validatorという仕組みがあります。

pickerは定義やテンプレートを選択する際の挙動を制御するための関数で、以下のようなシグネチャになっています。

type TemplatePicker func(def *DefinitionWithAlias, state *State) (Templates, error)
type DefinitionPicker func(defs *Definitions, state *State) ([]*Definition, error)

それぞれ与えられた定義とStateから定義やテンプレートのフィルタ/並び替えを行い、戻り値として返します。

TemplatePicker

TemplatePickerは、ある定義が持つテンプレートリストから、どれをどの順番で取り出すかを制御するための関数です。
例えば"ん"を含むテンプレートを選択しないTemplatePickerは以下のように実装できます。

func IgnoreNTemplatePicker(def *DefinitionWithAlias, state *State) (Templates, error) {
    var newTemplates Templates
    for _, template := range def.Templates {
        msg, _, err := template.ExecuteWithIncompleteState(state)
        if err != nil {
            return nil, err
        }
        if !strings.Contains(string(msg), "ん") {
            newTemplates = append(newTemplates, template)
        }
    }
    return newTemplates, nil
}

ExeuteWithIncompleteStateは、テンプレートが要求するTypeがStateに存在しなくても空文字として評価してメッセージを生成するメソッドです。例えば{{.A}} and {{.B}}!というテンプレートでStateが{"A": "V1"}だった場合、ExeuteWithIncompleteStateの結果はV1 and !となります。詳細はmessagenのgodoc(準備中)をご覧ください。

DefinitionPicker

DefinitionPickerはあるDefinition Typeをもつ定義からどれをどの順番で取り出すかを制御するための関数です。
例えば、messagen内部で、ある定義が制約を満たしているかどうかのチェックは、以下のようなDefinitionPickerとして実装されています。

func ConstraintsSatisfiedDefinitionPicker(definitio ns *Definitions, state *State) ([]*Definition, error) {
    var newDefinitions Definitions
    for _, def := range *definitions {
        if ok, err := def.CanBePicked(state); err != nil {
            return nil, err
        } else if ok {
            newDefinitions = append(newDefinitions, def)
        }
    }
    return newDefinitions, nil
}

def.CanBePickedは、stateが制約を満たしているかどうかチェックする関数です。

TemplateValidator

TemplateValidatorは、あるTemplateに対するstateがvalidかどうかを判定し、booleanを返す関数です。falseを返した場合、現在着目しているテンプレート以降の探索を打ち切ります。
TemplatePickerに似ていますが、stateが変更されるたびに呼び出されるので、適切に実装することで木構造を探索する際の枝刈りを行うことができます。例えばmessagenには、以下のような合計文字数をチェックするvalidatorのgeneratorであるMaxStrLenValidatorがあらかじめ用意されています。

func MaxStrLenValidator(maxLen int) TemplateValidator {
    return func(template *Template, state *State) (bool, error) {
        incompleteMsg, _, err := template.ExecuteWithIncompleteState(state)
        if err != nil {
            return false, err
        }
        return utf8.RuneCountInString(string(incompleteMsg)) <= maxLen, nil
    }
}

利用例

ここではmessagenがどのようなメッセージ生成を行えるのかを紹介します。

りょうくんグルメっぽい例

本家の方はちゃんと食べ物に合う表現を使っておられると思いますが、ここではジャンルに合った形容詞ならなんでも良いというルールで作ってみます。

Definitions:
 - Type: Root
   Templates: 
     - |-
       まじでこの世の全ての{{.FoodCategory}}好きに教えてあげたいんだが
       {{.Location}}には全ての人間を虜にする禁断の{{.FoodName}}がある
       これが{{.FoodDescription}}で超絶美味いからぜひ全国の{{.FoodCategory}}好き、
       {{.FoodCategory}}を愛する者たち、{{.FoodCategory}}を憎む者たち、全ての{{.FoodCategory}}関係者に伝われ
   OrderBy: ["FoodName"]

 - Type: FoodName
   Templates: ["サーロインステーキ", "リブステーキ"]
   Constraints: {"FoodCategory+": "肉", "Location+": "品川のhogeビル"}

 - Type: FoodName
    Templates: ["fugaハンバーグ"]
    Constraints: {"FoodCategory+": "肉", "Location+": "丸の内のfugaビル"}

 - Type: FoodName
    Templates: ["いちごケーキ", "モンブラン", "ガトーショコラ"]
    Constraints: {"FoodCategory+": "ケーキ", "Location+": "表参道のpiyoビル"}

 - Type: FoodDescription
    Templates: ["ジューシー", "ボリューミー"]
    Constraints: {"FoodCategory": "肉"}

 - Type: FoodDescription
   Templates: ["{{.CakeDescription}}かつ{{.AnotherCakeDescription}}"]
   Constraints: {"FoodCategory": "ケーキ"}
   Alias:
     AnotherCakeDescription: {"Type": CakeDescription, "AllowDuplicate": false}

 - Type: CakeDescription
   Templates: ["ふわふわ", "濃厚", "クリームたっぷり"]
$ messagen run -f gurume.yaml
まじでこの世の全てのケーキ好きに教えてあげたいんだが
表参道のpiyoビルには全ての人間を虜にする禁断のモンブランがある
これが濃厚かつふわふわで超絶美味いからぜひ全国のケーキ好き、
ケーキを愛する者たち、ケーキを憎む者たち、全てのケーキ関係者に伝われ

ガチャを作る例

診断メーカーでよく見るやつです。本家ではレアリティに応じて同じ文章をコピペしまくると言う荒技が用いられますが、messagenではWeightを用いてシンプルに表現できます。

Definitions:
  - Type: Root
    Templates:
      - |-
        フルーツ10連ガチャの結果↓
        {{.Item}}
        {{.Item2}}
        {{.Item3}}
        {{.Item4}}
        {{.Item5}}
        {{.Item6}}
        {{.Item7}}
        {{.Item8}}
        {{.Item9}}
        {{.Item10}}
    Aliases:
      Item2: &item {"Type": "Item", "AllowDuplicate": true}
      Item3: *item
      Item4: *item
      Item5: *item
      Item6: *item
      Item7: *item
      Item8: *item
      Item9: *item
      Item10: *item
  - Type: Item
    Templates: ["SR[ドラゴンフルーツ]", "SR[マンゴー]"]
    Weight: 0.1
  - Type: Item
    Templates: ["R[メロン]", "R[スイカ]"]
    Weight: 0.4
  - Type: Item
    Templates: ["N[りんご]", "N[みかん]", "N[バナナ]"]
$ messagen run -f gatya.yaml
フルーツ10連ガチャの結果↓
N[みかん]
N[みかん]
N[バナナ]
R[メロン]
N[バナナ]
R[メロン]
N[りんご]
R[メロン]
SR[マンゴー]
N[りんご]

キャッチコピーを生成する例

コピーメカ/キャッチコピー自動作成サイトをリスペクトした例です。本家同様、与えられたジャンルに応じて適切なキャッチコピーを生成します。

Definitions:
  - Type: Root
    Templates:
      - "無人島に持って行きたい{{.Product}}"
      - "{{.Product}}を{{.IfAction}}、自信が生まれる"
      - "{{.Product}}を{{.Action}}前に知っておいて欲しいこと"

  - Type: Action
    Templates: ["食べる"]
    Constraints: {"Genre": "Food"}
  - Type: Action
    Templates: ["買う"]
    Constraints: {"Genre": "Other"}

  - Type: IfAction
    Templates: ["食べれば"]
    Constraints: {"Genre": "Food"}
  - Type: IfAction
    Templates: ["買えば"]
    Constraints: {"Genre": "Other"}
$ messagen run -f copy.yaml --state Product=ラーメン,Genre=Food
ラーメンを食べれば、自信が生まれる

ポケモン名でいろは歌を作る例

制約によるメッセージ生成の極端な例として、こちらのブログで紹介されているような、ポケモン名でいろは歌を作る例を考えます。つまり、46文字を一度ずつだけ使ったポケモン名の列挙です。ただし、濁音/半濁音は取り除き()、長音は無視します。また、今回はズルをして、ケルディオとしています。

Definitions:
- Type: Root
   Templates: 
     - {{P1}} {{P2}} {{P3}} {{P4}} {{P5}} {{P6}} {{P7}} {{P8}} {{P9}} {{P10}} {{P11}} {{P12}} {{P13}}
   Alias:
     P1: &pokemon_alias {"Type": "Pokemon", "AllowDuplicate": false}
     P2: *pokemon_alias
     P3: *pokemon_alias
     P4: *pokemon_alias
     P5: *pokemon_alias
     P6: *pokemon_alias
     P7: *pokemon_alias
     P8: *pokemon_alias
     P9: *pokemon_alias
     P10: *pokemon_alias
     P11: *pokemon_alias
     P12: *pokemon_alias

 - Type: Pokemon
   Templates: ["フシギダネ", "ヒトカゲ", "ゼニガメ", "ヌオー", "ソーナノ",
                "キャタピー", "トランセル", "バタフリー", "ビードル", "コクーン", "スピアー",
                "ペロリーム", "チョボマキ", "ハブネーク", "ワタッコ",
                "ミュウ", "エレザード", "モンジャラ", "ケルディヲ",
    ]

次にValidatorを定義します。

func IrohaTemplateValidator(template *messagen.Template, state *messagen.State) (bool, error) {
    incompleteMsg, _, err := template.ExecuteWithIncompleteState(state)
    if err != nil {
        return false, err
    }
    return !HasDuplicatedRune(NormalizeKatakanaWord(string(incompleteMsg))), nil
}

HasDuplicatedRuneで、文字の重複がないかをチェックしています。また、NormalizeKatakanaWordで、濁音のハンドリング等を行っています。コード全体を記載すると長くなってしまうので、ご興味がある方はソースをご覧ください。

このvalidatorを適用したmessagenを実行します。

func main() {
    opt := &messagen.Option{
        TemplateValidators: []messagen.TemplateValidator{IrohaTemplateValidator},
    }
    generator, _ := messagen.New(opt)
    config, _ := messagen.ParseYamlFile("examples/iroha/pokemon.yaml")
    generator.AddDefinition(config.Definitions...)
    msg, _ := generator.Generate("Root", nil, 1)
    fmt.Println(msg)
}

実行すると以下のようになります。

$ go run main.go
[ワタッコ ペロリーム チョボマキ ミュウ モンジャラ ケルディヲ ゼニガメ ハブネーク ソーナノ ヌオー スピアー エレザード]

同じ文字を一度ずつ46文字使ったポケモン名の列挙ができていることがわかります。

(なお、実際にはもっと効率的ないろは歌生成アルゴリズムが存在します。もし高速にいろは歌を生成したくて仕方がないという奇特な方がいらっしゃれば、高速いろは歌生成CLIツールもご覧ください?)

スタバ警察の例

最後に、messagenを作るきっかけとなったスタバ警察の例です。スタバ警察では以下の要求がありました。

  • デバッグモードではデバッグメッセージを表示したい
  • 画像の判定結果の確信度に応じてメッセージを出し分けたい
  • (Twitterのbotなので)合計文字数を140字以下にしたい
    • これはTemplateValidatorで紹介したMaxStrLenValidatorで実現できます
  • 低確率で出現するメッセージを作りたい

以下はこれらを実現するyamlです。

Definitions:
  - Type: Root
    Templates:
      - |-
        ピピーッ❗️?⚡️スタバ警察です❗️??❗️
        {{.TweetCheck}}{{.SutabaDescription}}スタバ❗️❗️{{.GoodEmoji}}
        {{.LastMessage}}
    Constraints: {"Class": "Sutaba"}

  - Type: Root
    Templates:
      - |-
        ピピーッ❗️?⚡️スタバ警察です❗️??❗️
        アナタのツイート?は❌スタバ法❌第{{.RuleNum}}条?
        「スタバぢゃないツイートをスタバなうツイート?してゎイケナイ❗️」
        に違反しています?今スグ消しなサイ❗️❗️❗️❗️✌️??
    Constraints: {"Class": "Other", "Confidence/": "High|Medium"}

  - Type: Root
    Templates: ['{"class": "{{.Class}}", "confidence": "{{.Confidence}}"}']
    Constraints: {"Debug:1": "on"}

  - Type: TweetCheck
    Templates:
      - "{{.Exclamation}}このツイート{{.ThinkingEmoji}}{{.ThinkingEmoji}}{{.ThinkingEmoji}}..."

  - Type: TweetCheck
    Templates: ["アアーーー❗️なんだこれはーーー❗️❗️"]
    Weight: 0.5

  - {"Type": "Exclamation", "Templates": ["ムムッ", "ヤヤッ", "オオッ"]}
  - {"Type": "ThinkingEmoji", "Templates": ["?", "?"]}
  - {"Type": "GoodEmoji", "Templates": ["?", "?"]}

  - Type: SutabaDescription
    Templates: ["完全に", "間違いなく"]
    Constraints: {"Confidence": "High"}

  - Type: SutabaDescription
    Templates: ["おそらく", "多分"]
    Constraints: {"Confidence": "Medium"}

  - Type: LastMessage
    Templates:
      - "この調子でグッドなスタバツイートを心がけるようにッ❗️?‍?‍"
      - "市民の協力に感謝するッッッ?‍?‍❗"
$ messagen run -f sutaba.yaml -s Confidence=High,Class=Sutaba,Debug=off
ピピーッ❗️?⚡️スタバ警察です❗️??❗️
オオッこのツイート???...間違いなくスタバ❗️❗️?
市民の協力に感謝するッッッ?‍?‍❗

$ messagen run -f sutaba.yaml -s RuleNum=999,Confidence=High,Class=Other,Debug=off
ピピーッ❗️?⚡️スタバ警察です❗️??❗️
アナタのツイート?は❌スタバ法❌第999条?
「スタバぢゃないツイートをスタバなうツイート?してゎイケナイ❗️」
に違反しています?今スグ消しなサイ❗️❗️❗️❗️✌️??

$ messagen run -f sutaba.yaml -s Confidence=High,Class=Other,Debug=on
{"class": "Sutaba", "confidence": "High"}

おわり

messagenについて、背景、利用方法、利用例をご紹介しました。
もしご興味があればぜひ使って見てください?
まだドキュメントが足りない部分もあるので、不明点などあればTwitterでお気軽にご質問ください

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CircleCIでGitHubのPull Request時のみGoプロジェクトのチェックを実行する

やりたかったこと

GitHubのPull Request時にGoプロジェクトのビルド前チェックを行いたかったのでCircleCIで実現しました。
要件としては下記です。

  • errcheckやらlint等のチェックを実施
  • migrationを実施
    • DB処理を行うため、MySQLとRedisのイメージも利用
  • 処理時間の最大限短縮
    • 必要な外部ライブラリを取得済みのカスタムイメージを利用(イメージはプライベートリポジトリに保持)
    • vendorはキャッシュを利用
  • 処理結果をslackに通知

方針策定

migrationの確認はDBを立ち上げなくてはならず、なおかつDBの起動を待って処理を行わなくてはなりません。そのため、dockerizeコマンドをstepsに組み込みます。
今回は例としてMySQLとRedisのDockerイメージを使用しています。

また、チェック処理やmigrationには外部ライブラリの利用が必要となり、それらのインストールに時間がかかってしまいます。
処理時間の短縮を図るため、ライブラリを予めインストールしたカスタムイメージの利用とvendorのキャッシュ化を行います。

設定手順

GitHubをCircleCI連携するためにはプロジェクト直下に.circleciディレクトリを作成し、config.ymlファイルを配置する必要があります。
また、今回は必要なライブラリをインストール済みのカスタムイメージを利用するため、Dockerfileを用意する必要があります。このDockerfileの配置に関しても公式が配置箇所を指定しているのでそれに従います。

├── adapter
├── domain
├── usecase
├── hogehoge
└── .circleci/
    ├── config.yml
    └── images
        └── go
            └── Dockerfile

1. Dockerfile用意

今回はGo言語での開発のため、予めライブラリをgo getしたカスタムイメージを作成して利用することにします。
depも利用しているのでバージョン指定をしてdepのインストールも行います。
CircleCi純正のDockerイメージをベースに、カスタムしました。

.circleci/images/go/Dockerfile
FROM circleci/golang:1.11

ENV GOPATH /go
ENV DEP_VERSION 0.5.0

RUN curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep
RUN chmod +x $GOPATH/bin/dep

RUN go get -u golang.org/x/tools/cmd/goimports
RUN go get -u github.com/kisielk/errcheck
RUN go get -u golang.org/x/lint/golint
RUN go get -u bitbucket.org/liamstask/goose/cmd/goose
RUN go get -u github.com/client9/misspell/cmd/misspell

2. DockerHubにアップロード

CircleCIからDockerfileを利用する場合、用意したDockerfileをDockerHubにアップロードする必要があります。
方法としては直接アップロードする方式とGitHub等と連携して自動で更新する方法があるようです。今回は直接アップロードを行います。
また、下記操作をする前に予めリポジトリは任意の名前で作成してください。

$ cd path-to-project # プロジェクト直下へ
$ docker build .circleci/images/go
$ docker login
$ docker push hogehoge/foo:1.0.0 # リポジトリ名称とタグは任意

3. CircleCIプロジェクト設定

CircleCIをGitHubに連携する方法に関してはこちらの記事がとても参考になりました。
今回は割愛します。
サンプルのconfig.ymlを作成するところまで進めてください。

4. config.yml用意

config.ymlはCircleCIのyml書式に従って記述します。
作成したconfig.ymlはコミットしてmasterブランチまで反映させてください。

.circleci/config.yml
version: 2

jobs:
  check:
    environment:
      - GOPATH: /go
      - APP_ENV: circleci
    working_directory: /go/src/github.com/path-to-project
    docker:
      - image: hogehoge/foo:1.0.0
        auth:
          username: hogeuser
          password: $DOCKERHUB_PASSWORD
      - image: circleci/mysql:5.7-ram
        environment:
          MYSQL_ROOT_PASSWORD: rootpw
          MYSQL_DATABASE: test
          MYSQL_USER: circleci
          MYSQL_PASSWORD: circleci
      - image: circleci/redis:5.0-alpine

    steps:
      - checkout

      - run: dockerize -wait tcp://127.0.0.1:3306 -timeout 120s
      - run: dockerize -wait tcp://127.0.0.1:6379 -timeout 120s

      - restore_cache:
          keys:
            - v1-vendor-{{ checksum "Gopkg.lock" }}
      - run:
          name: ensure
          command: |
            if [ ! -d vendor ]; then
              dep ensure -vendor-only
            fi
      - save_cache:
          key: v1-vendor-{{ checksum "Gopkg.lock" }}
          paths:
            - vendor

      - run: goose -env circleci up

      - run: go test -v ./...
      - run: find . -type f -name '*.go' -not -path "./vendor/*" -print0 | xargs -0 goimports -l | xargs -r false
      - run: find . -type f -name '*.go' -not -path "./vendor/*" -print0 | xargs -0 misspell -error
      - run: # その他もろもろの確認処理

workflows:
  version: 2
  setup-install-check:
    jobs:
      - check

5. CircleCIプロジェクト設定の調整

上記の設定のみでは要件が満たせないため、プロジェクトの設定を調整します。

実行タイミングの設定

初期設定ではすべてのコミットに対してCircleCIが実行されます。
今回はPull Request時のみにCircleCIを実行したいため、設定を変更します。

CircleCIプロジェクトからSetting > Advanced Settings に進み、「Only build pull requests」をOnに設定。
スクリーンショット 2019-10-21 18.29.14.png

これによって、PR時にのみ実行されるようになりますが、defaultブランチに対してのコミットは常に実行される点、注意です。
※defaultブランチはリポジトリごとに設定可能です。GitHubのSettingsから確認をしてみてください。

Slack通知の設定

CircleCIの実行結果をSlackに通知する設定を入れます。

CircleCIプロジェクトからSetting > Chat Notifications に進み、Slackの「Webhook URL」を設定。
SlackのWebhook URLは画面内のCircleCI Integrationリンクから遷移して取得することが可能です。
スクリーンショット 2019-10-21 18.30.17.png

環境変数の設定

DockerHubにカスタムイメージを置く際、セキュリティの関係でプライベートリポジトリを使う必要がある場合があります。カスタムイメージを取得する際に、CircleCIがユーザ情報を保持する必要があるのですが、config.ymlに記載するのは良くないでしょう。
そのため、環境変数にカスタムイメージ取得用アカウントのパスワードを持たせ、config.ymlから呼び出しています。

CircleCIプロジェクトからSetting > Environment Variables に進み、Add Variableをクリック。
スクリーンショット 2019-10-21 18.32.01.png

出てきたダイアログに環境変数名とその値を記載してAdd Variableをクリック。
スクリーンショット 2019-10-21 18.32.46.png

config.yml説明

ポイントだけ少し補足説明します。

Docker周り

今回はプライベートリポジトリにアップロードしているカスタムイメージを利用するため、最初のimage(実体はGo用のイメージ)に対してauthを設定しています。
$DOCKERHUB_PASSWORDは前項で設定した環境変数の値が適応されます。

MySQLイメージのenvironmentに各設定値を入力することで、その設定で起動されます。MYSQL_DATABASEを設定することでtestという名前のDBスキーマも作ることができます。

    docker:
      - image: hogehoge/foo:1.0.0
        auth:
          username: hogeuser
          password: $DOCKERHUB_PASSWORD
      - image: circleci/mysql:5.7-ram
        environment:
          MYSQL_ROOT_PASSWORD: rootpw
          MYSQL_DATABASE: test
          MYSQL_USER: circleci
          MYSQL_PASSWORD: circleci
      - image: circleci/redis:5.0-alpine

DB起動待機

今回はredisとmysqlのDockerイメージを利用するが、それらの起動を待つために下記の処理を行っています。

      - run: dockerize -wait tcp://127.0.0.1:3306 -timeout 120s
      - run: dockerize -wait tcp://127.0.0.1:6379 -timeout 120s

キャッシュの利用

depを利用して取得したライブラリはvendorに格納されるため、vendorをキャッシュ化しています。
depはライブラリの依存関係がGopkg.lockに反映されるため、Gopkg.lockのチェックサム値をキーとしてキャッシュを保存します。

処理としては下記の流れです。

  1. キャッシュが存在したら取得する(restore_cache)
  2. キャッシュが存在しなかった(vendorがリストアされなかった)場合、dep ensureを行う。
  3. vendorをキャッシュに保存する(save_cache)

なお、dep ensureの際にGopkg.lockが更新され、キャッシュ保存時にキー値が変わってしまう恐れがあるため -vendor-onlyをつけています。

      - restore_cache:
          keys:
            - v1-vendor-{{ checksum "Gopkg.lock" }}
      - run:
          name: ensure
          command: |
            if [ ! -d vendor ]; then
              dep ensure -vendor-only
            fi
      - save_cache:
          key: v1-vendor-{{ checksum "Gopkg.lock" }}
          paths:
            - vendor

※ 保持しているキャッシュを削除したい場合は、v1-vendor-の数値を書き換えるらしいです。

おわりに

Goのサンプルが少なめだったので参考になれば嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでtar.gzを出力する

勉強用にtar.gzのサンプルを作りました。ソースコード自身をtar.gzに格納圧縮し、そのバイト列を標準出力に表示し、output.tar.gzに保存し、終了します。bytes.Buffer、gzip.Writer、tar.Writerは全てio.Writerインターフェースを実装しているので順番に噛み合わせてtar.gzされたバイト列を出力できます。

ソース

tar.gz.go
package main

import (
    "archive/tar"
    "bytes"
    "compress/gzip"
    "fmt"
    "io"
    "os"
)

func writefile(w *tar.Writer,name string){
    if file, err := os.Open(name) ; err == nil{
        fi, _:=file.Stat()
        hdr := new(tar.Header)
        hdr.Size=fi.Size()
        hdr.Name=fi.Name()
        w.WriteHeader(hdr)
        io.Copy(w, file)
        file.Close()
    }
}
func main() {
    var buf bytes.Buffer
    gz := gzip.NewWriter(&buf)
    tw := tar.NewWriter(gz)
    writefile(tw,"tar.gz.go")
    tw.Close()
    gz.Close()
    data:=buf.Bytes()
    fmt.Print(data)
    if file, err := os.Create("output.tar.gz") ; err == nil{
        file.Write(data)
        file.Close()
    }
}

補足

  • 1ファイルならtarする必要がないよね
    • そのとおりです。実際はwritefile()を複数呼んでファイルを追加します。
  • 直接ファイルに書き込めばいいのでは?
    • 圧縮後の中身を見たかったので一度バイト列に出力しました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GoとGAEでWebhookを使ったプライベートなShopifyアプリを作る

今回はShopifyアプリをGoogle Cloud PlatformのGAE(Google App Engine)を使ってGoで書いてみました。

Shopifyとは?

Shopify 日本語サイト
Shopifyとは、カナダ産のEC(e-Commerce)のSaaSです。ひと昔前風に言うと、GMOメイクショップや、フューチャーショップ、Baseの様なEC向けのASPです。
現在、カナダだけではなく、北米はもとより世界中で利用されており、昨年くらいからアジアオセアニア地域でのマーケティングにお力を入れ始めた、ECプラットフォーム界の巨人です。
グローバルに展開しているサービスなので、世界中のローカルの決済等が利用でき、越境EC向けのECプラットフォームです。

標準機能も素晴らしいのですが、Shopifyアプリというサードパーティーのアプリでその機能を拡張できる点も魅力的な点です。
すでに世界中で様々なShopifyアプリが開発されており、日本国内でも増えてきています。
スクリーンショット 2019-10-19 20.48.24.png

Shopifyアプリとは?

じゃあ、具体的にShopifyアプリってどうなってんの?というと、基本的にShopifyのWebAPIを叩くsomethingです。
APIには伝統的なRESTのAPIと最近流行りのGraphQLがあります。
shopify_app_arc@2x.png

Shopify内の何かしらのイベントに対応したEventWebhookでキックされる感じです。
APIとアプリにはいくつか種類があって、全体的なAPIと、管理画面上の要素をごにょごにょするAPIとかフロント用のAPIとか実店舗や倉庫向けのPOSアプリ用のAPIとかもありますよくわかりません。とりあえず今回の要件に必要な分だけ調べました。

Shopifyアプリには、大きく分けて公開されてて誰でも利用することができるPublic Appsと、その店舗専用みたいなPrivate Appsがあります。

Private Apps

ShopifyのAppストアに出さないそのショップ専用のアプリ。Public Appと違って、認証周りが楽。トークンとか取得しなくて良い。https://{API_key}:{password}@{shop}.myshopify.com/admin/api/2019-10/shop.jsonみたいにBasic認証でリクエストして処理できる。今回使ったのはこれ

Public Apps

ShopifyのAPPストアからインストールできる。APIの接続には認証とトークンが必要。一般的なAppsといったらこっち。
App Storeに公開しなくても使える。その場合はPrivate Appsっぽい扱いができる。Private Appじゃないメリットは後述のEmbedded SDKとかが使えること。管理画面系のアプリはPublicにしないとできない。

自分が作ったアプリを広く売りたいなら通常のShopify App

Public AppsはShopifyの利用者に向け販売できます。だいたいのアプリがサブスクリプション方式なので、定期的な収入が欲しい方は作ってみてはいかがでしょうか?
英語など多言語に対応すれば市場はものすごく大きいので、ポテンシャルは高いです。ただ、現状だと日本語と日本にしか対応していないアプリは厳しそうです。実際、現在日本語で提供されているアプリも、決済系かロジ系が多く、アプリじゃなくて自社サービスと連携させるアプリを出して、自社サービスで利益を出すタイプが多い様です。

Embedded apps

なぜこれがPublic AppsとPrivate Appsと同じレベルで説明されているのかわからないけど、上記のPublic Appsで使える管理画面系のアプリのためのSDKがあり、これらを使う様なアプリはEmbedded appsと呼ばれる。

  • Embedded App SDK
    • 管理画面に埋め込む系の何かを作る時用のSDK
  • POS App SDK
    • iPadとかのタブレットなどで使えるShopify POSアプリがあり、それ用のSDK。今は非推奨。iOS9以降は対応してないっぽい。
  • Shopify App Bridge
    • Embedded App SDK、POS SDKに代わる新しいしくみ。JavaScriptのライブラリで、管理画面やShopify POS Appのフロントエンド向け。

どの言語でShopify Appを作るか?

ぶっちゃけどれでも良い。APIとのやり取りがメインなので、何だったらCでもアセンブラでも良い。
RESTかGraphQL叩ければ良い。ただ、いろんな言語でShopify API用のライブラリが出てるのでそれらを使うと楽。

公式に推してるのはRuby on Rails

https://github.com/Shopify/shopify_app
最初、Railsで書いてたけど、Railsが動くApp Serverを立てるのとそのメンテが面倒だったので途中でやめた。たぶんHerokuとか使えば良いんだろうけど、嫌いなのでテンション下がってやめた。今回はクライアントにとってもHerokuとRailsを使うメリットはあまり無かった。

Shopify自体がRubyで書かれていて、UAがRubyでAPPにアクセスしてくる。

Shopifyって中は何で書いてるんだろう?って思ってたんですが、Wenhookに関してはアクセスしてくるUAがRubyとなってるので、中でも結構Rubyが使われてるのかも。 Matzすごい

Ruby以外の言語のライブラリ

Ruby on Railsで書くのを止めた段階で候補はGoPHPしかなかったんだけど、ざっくり調べて見た。

世界中で使われてるSaaSなだけあって、とにかくいっぱいある。ただ、日本語対応とか日本向けはまだまだ少ないので、日本人的には辛い。日本人頑張らねば
スクリーンショット 2019-10-19 18.36.26.pngスクリーンショット 2019-10-19 20.48.05.png
PHPが公式のリポジトリにあるにも関わらずメンテされていなかったので、何となく避けた。Laravelのライブラリは開発が盛んで盛り上がっているみたいだったので、少し興味を持ったけど今回はLaravel使うほどのものでもなかったのでGoでベタ書きすることにした。結局100行ちょいのmain.go1ファイルで終わった。

go-shopifyを使ってgolangでShopify Appを書く

goでShopify Appを書く場合、go-shopifyを使うと色々と便利。大体のShopifyのオブジェクトの構造体が準備されてるし、.Create()で追加したり色々できる。
とりあえずgo getしてインポートする。

$ go get github.com/bold-commerce/go-shopify
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "strings"
    "time"

    goshopify "github.com/tao-s/go-shopify"//これ
)

go-shoipfyの基本的な使い方

まずはAPIの認証。 Private AppPublic Appで少し違う

Public AppのShopify APIの認証

API KeyAPI Secretをセットしてトークンを取得する

// Create an app somewhere.
app := goshopify.App{
    ApiKey: "abcd",
    ApiSecret: "efgh",
    RedirectUrl: "https://example.com/shopify/callback",
    Scope: "read_products,read_orders",
}

API KeyAPI SecretShopifyのパートナーページのアプリ管理で取得できる。
スクリーンショット 2019-10-21 11.51.47.png

次にトークン取得

// Create an oauth-authorize url for the app and redirect to it.
// In some request handler, you probably want something like this:
func MyHandler(w http.ResponseWriter, r *http.Request) {
    shopName := r.URL.Query().Get("shop")
    authUrl := app.AuthorizeURL(shopName)//shopNameはストアの管理画面のドメイン。
    http.Redirect(w, r, authUrl, http.StatusFound)//アプリ認証画面にリダイレクトさせる
}
// 認証画面からの戻りの画面
func MyCallbackHandler(w http.ResponseWriter, r *http.Request) {
    // Check that the callback signature is valid
    if ok, _ := app.VerifyAuthorizationURL(r.URL); !ok {//戻りの検証
        http.Error(w, "Invalid Signature", http.StatusUnauthorized)
        return
    }

    query := r.URL.Query()
    shopName := query.Get("shop")
    code := query.Get("code")
    token, err := app.GetAccessToken(shopName, code)//トークンを取得。ここで取得したトークンはDBとかに保存しとく

    // Do something with the token, like store it in a DB.
}

トークンが取得できたらAPI叩ける。

// Create an app somewhere.
app := goshopify.App{
    ApiKey: "abcd",
    ApiSecret: "efgh",
    RedirectUrl: "https://example.com/shopify/callback",
    Scope: "read_products",
}

// Create a new API client
client := goshopify.NewClient(app, "shopname", "token")//取得したshopnameとトークンを渡してクライアントを初期化

// Fetch the number of products.
numProducts, err := client.Product.Count(nil)//APIをCallする

Private AppのShopify APIの認証

Private Appの場合は、APIキー取得するとこが違う。Public Appはパートナーページだったけど、Pricvate Appはショップのアプリ管理から取得する。
Shopify Admin Menu
グループ 1.png
プライベートアプリ管理画面
取得した** API Key*パスワード*で初期化。トークンは空文字ですぐクライアント作れる。
このプライベートアプリがどのAPIにアクアセスできるか?とかはストアのプライベートアプリの管理画面で設定する。

// Create an app somewhere.
app := goshopify.App{
    ApiKey: "apikey",
    Password: "apipassword",//API Secretじゃなくてpassword
}
// Create a new API client (notice the token parameter is the empty string)
client := goshopify.NewClient(app, "shopname", "")

アプリの起動はWebhookで

アプリの起動方法は2種類あって、あらかじめshopifyのEventに登録しておく方法と、Webhookを使う方法。プライベートアプリならWebhookが楽。
Webhookの設定は、ストアの管理画面の通知メニューから設定する。
Shopify Admin Settingメニュー
ここでWebhookの送信先とかリクエストの検証用キーとかが設定できる。
Shopify Admin Setting 通知メニュー

Private Appはトークンいらない代わりにWebhookリクエストの検証が必要。じゃないとショップ以外の悪意ある第三者からのリクエストも処理しちゃう。

func ValidateWebhook(httpRequest *http.Request) (bool) {
    shopifyApp := goshopify.App{ApiSecret: "ratz"}
    return shopifyApp.VerifyWebhookRequest(httpRequest)
}

Webhookを起点にShopify APIを叩く

Webhookのパース

今回は受注のタイミングで支払いステータスを変更したいって要件だったので、注文作成のWebhookを設定した。その際、作成された注文の情報がWebhookのリクエストに入ってくるので、必要な情報だけを取得する。
go-shopifyでもできるんだけど、諸事情によりここだけ手作業でやった。

b, err := ioutil.ReadAll(r.Body)//リクエストのbodyをパース
defer r.Body.Close()
if err != nil {
    panic(err)
}
var jsonData Webhook//jsonの形式に合わせたstructを初期化
if err := json.Unmarshal(b, &jsonData); err != nil {//jsonをパースして構造体に値を入れる。
    panic(err)
}
//Webgook Webhookの構造体
type Webhook = struct {
    ID              json.Number `json:id`//これがID。受注番号とは違う。
    Gateway         string      `json:gateway`
    FinancialStatus string      `json:financial_status`//支払いステータス 読み取り専用
}

Shopify Admin APIを叩いてTransactionを追加する

Shopifyの受注データの支払いステータスは読み取り専用で直接変更できない。変更するためにはTransactionという支払いデータを追加して、そのステータスをsuccessに設定する必要がある。具体的にはOrderのIDを使ってtransactionを追加する。

ntr := goshopify.Transaction{
    ParentID:   &tr[0].ID,//親のトランサクションID、受注時に正成される
    Status:     "success",
    Kind:       "sale",//必須項目
    Amount:     tr[0].Amount,
    Gateway:    tr[0].Gateway,
    Currency:   tr[0].Currency,
    OrderID:    orderID,
    SourceName: "external",
    Source:     "external",//ここが今回のハマりポイント
}
rtr, err := client.Transaction.Create(orderID, ntr) //API叩いて追加

ここで今回最大にハマったのが、このSourceってパラメータ。なんと公式ドキュメントに書いてないw
なのに必須要素で、'external'に指定しないとTransactionが追加できない。公式のドキュメントにも載ってないのでgo-shopifyにも定義されてないw
仕方が無いのでgo-shopifyをいじって、Sourceを設定できる様にして解決。そのPR

これで無事、受注ステータスをShopify Appから変更できる様になりました。いやー、最後の最後でハマった...

環境はまたGAE使いました

Goの簡単な環境作るならこれがGoogle Cloud PlatformApp Engineが一番楽ですね。運用も楽だし、速い、安い。

[PR]株式会社クロスキューブではShopifyアプリの開発も請け負っております!

最後に、弊社株式会社クロスキューブでは、Shopifyアプリ開発やShopifyを使ったECサイト構築のお仕事も大募集中です!
何かShopifyアプリでお困りの方がいらっしゃいましたらぜひお問合せ願います

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Drive Apiの始め方

Google DriveをGoでいじる

目標: GoのDriverでapiをいじれるようにする。
動機: Goの勉強がてらGCPにも慣れたかった。
前提: GoがインストールされているMacOS Mojave 10.14.5。GoogleAccountを持っている。

Google Drive Apiの公式の通りに進めると...

  • quickstart.goを実行した際に、URLで認証を行う必要があるのですが、自分が実行した際に以下のような感じでアクセスができない状態になってしまいました。(なんていうのか知らん) スクリーンショット 2019-10-21 11.46.01.png

あれ、でもなんか安全じゃないけど見に行けるボタンみたいなのあったよなーと思い、開発者ツールから euZgNeにかかっているdisplay: noneを取り除き、アクセスしました。前まで普通に表示されていたと思うのですが、なぜかを知っている方がいたら教えていただきたい。。
スクリーンショット 2019-10-21 11.50.47.png

あとは許可ボタンを押し続ける作業です。この時、サンプルコードで設定されているドライバーのスコープ(これはそのアプリケーションのGoogle Driveへのアクセス権限を表すスコープの意味)がreadonlyになっているのでアップロードしたいとか権限を調整したい時にはここを見てgoogle.ConfigFromJSON(b, drive.DriveScope)の部分を変更してください。
このとき、tokenは1スコープに対して1tokenの関係なので(多分)、readonlyでtoken.jsonを作っていたならそれは削除してください。サンプルコードではスコープの変更によってtoken.jsonを使い分けができるようには作られていないので削除する必要があります。

まとめ

自分の躓いたポイントで諦める人が出ないことを祈って初めてQiitaで簡単に書きましたが、書くのって難しい。。

GoogleApiのDocument

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Drive Apiの始め方(つまづきポイントメイン)

Google DriveをGoでいじる

目標: GoのDriverでapiをいじれるようにする。
動機: Goの勉強がてらGCPにも慣れたかった。
前提: GoがインストールされているMacOS Mojave 10.14.5。GoogleAccountを持っている。

Google Drive Apiの公式の通りに進めると...

  • quickstart.goを実行した際に、URLで認証を行う必要があるのですが、自分が実行した際に以下のような感じでアクセスができない状態になってしまいました。(なんていうのか知らん)

スクリーンショット 2019-10-21 11.42.23.png

スクリーンショット 2019-10-21 11.46.01.png

あれ、でもなんか安全じゃないけど見に行けるボタンみたいなのあったよなーと思い、開発者ツールから euZgNeにかかっているdisplay: noneを取り除き、アクセスしました。前まで普通に表示されていたと思うのですが、なぜかを知っている方がいたら教えていただきたい。。
スクリーンショット 2019-10-21 11.50.47.png

あとは許可ボタンを押し続ける作業です。この時、サンプルコードで設定されているドライバーのスコープ(これはそのアプリケーションのGoogle Driveへのアクセス権限を表すスコープの意味)がreadonlyになっているのでアップロードしたいとか権限を調整したい時にはここを見てgoogle.ConfigFromJSON(b, drive.DriveScope)の部分を変更してください。
このとき、tokenは1スコープに対して1tokenの関係なので(多分)、readonlyでtoken.jsonを作っていたならそれは削除してください。サンプルコードではスコープの変更によってtoken.jsonを使い分けができるようには作られていないので削除する必要があります。

まとめ

自分の躓いたポイントで諦める人が出ないことを祈って初めてQiitaで簡単に書きましたが、書くのって難しい。。

GoogleApiのDocument

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでプロセス監視のTUIツールを作ったら便利だった件

ども、バナナとナタデココにハマっているゴリラです。つまり食物繊維大好きゴリラ。

最近なぜかプロセスをkillすることが多くて、毎度コマンド打つの面倒だったので2日くらいかけてTUIツールを作ってみました。

今日はそのツールの紹介と実装の話をしていきます。

どんな感じ?

こんな感じ。

pst.gif

対応OS

  • Mac OS(Catalinaで動いたことは確認できている)
  • Linux

一応Windowsでも動くはずですが、動作確認していないのでダメだったらごめんなさい。
Windowsで動いたとしてもプロセスの詳細情報は見れないですが、今後対応予定なので、お待ち頂ければと思います。

画面構成

画面は全部で以下の3つがあります。

  • processes(現在動いているプロセス一覧)
  • process info(選択したプロセスの詳細情報)
  • process tree(選択したプロセスのツリー)

画面に加えて、プロセス一覧を絞り込むための入力欄があります。自分のPCでは大体400〜くらいのプロセスが動いているので、絞り込みの機能は必須でした。

使い方

作ったばっかりなので、最低限の機能(個人的に)しか実装していなくて
- プロセスの絞り込み
- プロセスの強制終了(SIG_KILL)
くらいしかないです。

プロセスの矯正終了はプロセス一覧とプロセスツリーのパネルでKを押下すると確認ダイアログがポップするので、Killを選択するとプロセスを強制終了します。

image.png

実装のお話

今回の実装にあたり、主に以下のライブラリを使いました。

プロセス周り

プロセス一覧はgo-psで取得し、pidを使ってpsコマンドで詳細情報を取っています。
プロセスを取得するにはシステムコールを使用しますが、OSごとに取れる情報が異なる上、システムコールのインターフェイスも異なります。
システムコールのこと調べても良くわからなかった上、OSの差分吸収はとても面倒だったので最低限pid、ppidとコマンドを取得できるgo-psを使いました。

欲を言えば、もっとプロセスの細かい情報を取得したかったのですが、そこか諦めて外部コマンドに頼ることにしました。。
システムコールを学ぶにはC言語から学んだ方が良いらしいので、ちょっと勉強しようかな…

話は戻して、pidとppidが取れるので、どのプロセスが子プロセスを持っているのか判定できます。
そこでプロセスツリーを作れると思い、そのインターフェイスを実装しました。

プロセスの構造体はとてもシンプルで次になっています。

type Process struct {
    Pid   int
    PPid  int
    Cmd   string
    Child []Process
}

抜粋ですがChild に子プロセスを次のようにして格納しています。
ちなみに、pidが0のプロセスはシステムコールで取得できますが、こちらはpsコマンドで情報を取れない上、
すべてのプロセスの大本なので表示する意味がないためskipするようにしています。

pids := make(map[int]Process)
for _, proc := range processes {
    // skip pid 0
    if proc.Pid() == 0 {
        continue
    }
    pids[proc.Pid()] = Process{
        Pid:  proc.Pid(),
        PPid: proc.PPid(),
        Cmd:  proc.Executable(),
    }
}

// add child processes
for _, p := range processes {
    if p.Pid() == p.PPid() {
        continue
    }

    if proc, ok := pids[p.PPid()]; ok {
        proc.Child = append(proc.Child, pids[p.Pid()])
        pids[p.PPid()] = proc
    }
}

プロセスの詳細情報はこんな感じでpsコマンドを使って取ってきてそのまま表示させています。

func (p *ProcessManager) Info(pid int) (string, error) {
    // TODO implements windows
    if runtime.GOOS == "windows" {
        return "", nil
    }

    if pid == 0 {
        return "", nil
    }

    buf := bytes.Buffer{}
    cmd := exec.Command("ps", "-o", "pid,ppid,%cpu,%mem,lstart,user,command", "-p", strconv.Itoa(pid))
    cmd.Stdout = &buf
    cmd.Stderr = &buf
    if err := cmd.Run(); err != nil {
        return "", err
    }

    return buf.String(), nil
}

-oを使用することでフォーマットを自由にカスタマイズできるので、必要そうな情報だけピックアップしました。
どんな情報が取れるかはman psのkeywordの部分を参照して下さい。

-pはプロセスIDを指定することでそのプロセスIDの情報だけ取得できます。
こういった出力をカスタマイズできるコマンドはGoでツールを作るときにありがたいです。

ちなみに、Windowsはpsコマンドがない(はず…)ので一旦TODOにしています。Windows機購入したので届いてから対応しようと思います。

画面周り

画面周りはtviewを使いました。以前DockerのTUI Clientを作ったときはgocuiというライブラリを使っていましたが、こちらはメンテされていない上、ダイアログといったコンポーネント画面を作るのはめんどくさいです。

一応、コンポーネントを簡単に作れるgocui-componentというライブラリを作ったのですが、tviewが圧倒的に高機能かつ便利だったので採用しました。

tviewの優れたところはGridFlexといったレイアウトを簡単に作れるところです。
TUIはウェブと違って結構画面周りはめんどくさいので、こういった画面のサポートが強力なライブラリはすごくありがたいです。

抜粋ですが、画面のレイアウトはこんな感じで実装しています。

infoGrid := tview.NewGrid().SetRows(0, 0).
    AddItem(g.ProcInfoView, 0, 0, 1, 1, 0, 0, true).
    AddItem(g.ProcessTreeView, 1, 0, 1, 1, 0, 0, true)

grid := tview.NewGrid().SetRows(1, 0).
    SetColumns(30, 0).
    AddItem(g.FilterInput, 0, 0, 1, 1, 0, 0, true).
    AddItem(g.ProcessManager, 1, 0, 1, 1, 0, 0, true).
    AddItem(infoGrid, 1, 1, 1, 1, 0, 0, true)

g.Pages = tview.NewPages().
    AddAndSwitchToPage("main", grid, true)

gridを作って、AddItemを使って画面をgridに格納する事ができます。SetColumnsは縦幅を定義する関数で、縦に3つ分けたいなら3つの画面のそれぞれの縦幅を渡せばあとはよしなにやってくれます。めっちゃ簡単。
更に、gridgridに格納することもできるので、縦2分割して右側を横2分割する、といったレイアウトも簡単につくれます。とても便利。

まだぼくも良くわかっていないところもあるので、きちんと理解したらtviewの使い方に関する記事を書こうと思います。

作ってみた感想

プロセスやシステムコール周りについて調べて少しだけ理解した(つもり)ので良い収穫でした。
OSのような低レイヤー周りはやっぱり奥が深くて難しいですが、面白いです。
最近30日でOSを作る本が結構良いらしいので、ちょっと買って勉強しようかなと思います。

そして、このツールは意外と便利なのでぜひ使ってみてください。感想お待ちしています。

余談

Macのシステムコールについて調べたら良さげの資料を見つけたので、一応共有しておきます。

https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/sysctl.3.html

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む