LoginSignup
5
3

More than 3 years have passed since last update.

Elm で Google Firebase CloudStoreからデータを取得するデコーダを作ったメモ

Last updated at Posted at 2019-06-23

概要

elmのdecoderについて、詳しい解説をしてくれているサイトはいくつもある。(Elm Decoder a からいろいろ理解ってしまおう / Elm 用の CSV デコーダーを作った
実際に、どう使うのか、が、すぐにはできなかったのでメモしておく。

対象とするJSON

Cloud Firestore REST API の使用を参考にして、firestoreからデータを取得する。

コレクション

ドキュメント

コレクションのデコード

こちらのほうがシンプル。

対象とするJSON

firestoreが返すjsonは以下のようなもの。

{
  "documents": [
    {
      "name": "projects/garden-2a6de/databases/(default)/documents/publish/all/characters/05TsdJiaAyAPH8RLgsqt",
      "fields": {
        "labo": {
          "stringValue": "旧第一研究所"
        },
        "name": {
          "stringValue": "狐狸"
        },
        "characterId": {
          "stringValue": "05TsdJiaAyAPH8RLgsqt"
        }
      },
      "createTime": "2019-06-15T00:13:36.394340Z",
      "updateTime": "2019-06-15T00:13:36.394340Z"
    }
  ]
}

デコード先のレコード

上記のJSONから以下のレコードのリストを取得したい。

type alias CharacterListItem =
    { characterId : String
    , name : String
    , labo : String
    }

ネストしたオブジェクトのフィールドのデコードと配列のデコード

{
  "documents": [...]
}

このときは、Docode.atとDecode.listを使用する。

characterListDecoder : Decoder (List CharacterListItem)
characterListDecoder =
    D.at [ "documents" ] (D.list characterListItemDecoder)

オブジェクトのデコード

ネストしたオブジェクトのデコード

では、配列の中身のオブジェクトを見ていく。
これもまたネストしている。

{
    "fields": {...},
      "createTime": "2019-06-15T00:13:36.394340Z",
      "updateTime": "2019-06-15T00:13:36.394340Z"
    }
}

fieldsはキャラクターのほうでも使うので、関数化してみた。

import FirestoreApi as FSApi

characterListItemFieldDecoder : Decoder CharacterListItem
characterListItemFieldDecoder =
    FSApi.fields characterListItemFieldDecoder
FirestoreApi.elm
fields : Decoder a -> Decoder a
fields decoder =
    D.at [ "fields" ] decoder

オブジェクトのデコード

ネストが……ネストが深い…

{
        "labo": {
          "stringValue": "旧第一研究所"
        },
        "name": {
          "stringValue": "狐狸"
        },
        "characterId": {
          "stringValue": "05TsdJiaAyAPH8RLgsqt"
        }
}
characterListItemDecoder : Decoder CharacterListItem
characterListItemDecoder =
    D.succeed CharacterListItem
        |> required "characterId" FSApi.string
        |> required "name" FSApi.string
        |> required "labo" FSApi.string
FirestoreApi.elm
string : Decoder String
string =
    D.at [ "stringValue" ] D.string

完成!

ドキュメントのデコード

対象のJSON

深い深い深い! 実際のデータは長すぎるので省略して載せます。

{
  "name": "projects/garden-2a6de/databases/(default)/documents/characters/05TsdJiaAyAPH8RLgsqt",
  "fields": {
    "kana": {
      "stringValue": "コリ"
    },
    "isPublished": {
      "booleanValue": true
    },
    "cards": {
      "arrayValue": {
        "values": [
          {
            "mapValue": {
              "fields": {
                "cardId": {
                  "stringValue": "B000"
                },
                "tags": {
                  "arrayValue": {
                    "values": [
                      {
                        "mapValue": {
                          "fields": {
                            "level": {
                              "integerValue": "0"
                            },
                            "name": {
                              "stringValue": "移動"
                            }
                          }
                        }
                      },
                      {
                        "mapValue": {
                          "fields": {
                            "name": {
                              "stringValue": "基本能力"
                            },
                            "level": {
                              "integerValue": "0"
                            }
                          }
                        }
                      }
                    ]
                  }
                },
                "maxRange": {
                  "integerValue": "0"
                },
                ...
              }
            }
          }
      }
    ]
  }
}

デコード先のレコード

深くなったり長くなっている原因はレコードに色々持たせているからです。
あれも欲しいこれも欲しいとやっていると大きくなってしまう。。。

type alias Character =
    { storeUserId : String
    , characterId : String
    , name : String
    , kana : String
    , organ : String
    , trait : String
    , mutagen : String
    , cards : Array Card.CardData
    , reason : String
    , labo : String
    , memo : String
    , activePower : Int
    , isPublished : Bool
    , cardImage : String
    , cardImageData : String
    , characterImage : String
    , characterImageData : String
    , cardImageCreatorName : String
    , cardImageCreatorSite : String
    , cardImageCreatorUrl : String
    }

type alias CardData =
    { cardId : Maybe CardId
    , cardName : String
    , cardType : String
    , kind : String
    , exp : Int
    , timing : String
    , cost : Int
    , range : Int
    , maxRange : Int
    , target : String
    , maxLevel : Int
    , effect : String
    , description : String
    , tags : List Tag
    , imgMain : String
    , illustedByName : String
    , illustedByUrl : String
    , imgFrame : String
    , frameByName : String
    , frameByUrl : String
    , deleteFlag : Int
    }

type alias Tag =
    { name : String
    , level : Int
    }

ネストのデコード

とりあえず、以下の構造ではある。

{
    "fields" : {}
}

一つずつ対応していこう。

characterDecoderFromFireStoreApi : Decoder Character
characterDecoderFromFireStoreApi =
    FSApi.fields characterDecoderFromFireStoreApiHealper

fieldsの次は、中身。



characterDecoderFromFireStoreApiHealper : Decoder Character
characterDecoderFromFireStoreApiHealper =
    Decode.succeed Character
        |> required "storeUserId" FSApi.string
        |> required "characterId" FSApi.string
        |> required "name" FSApi.string
        |> required "kana" FSApi.string
        |> required "organ" FSApi.string
        |> optional "trait" FSApi.string ""
        |> optional "mutagen" FSApi.string ""
        |> optional "cards" (FSApi.array Card.cardDecoderFromFireStoreApi) (Array.fromList [])
        |> optional "reason" FSApi.string ""
        |> optional "labo" FSApi.string ""
        |> optional "memo" FSApi.string ""
        |> optional "activePower" FSApi.int 4
        |> optional "isPublished" FSApi.bool False
        |> optional "cardImage" FSApi.string ""
        |> optional "cardImageData" FSApi.string ""
        |> optional "characterImage" FSApi.string ""
        |> optional "characterImageData" FSApi.string ""
        |> optional "cardImageCreatorName" FSApi.string ""
        |> optional "cardImageCreatorSite" FSApi.string ""
        |> optional "cardImageCreatorUrl" FSApi.string ""

boolとintが欲しくなったので作ります。
Cardはちょっと別で作る。
intは決め打ちですけど、elmでintで出したものを保存している前提だからいっかなと、、

FirestoreApi.elm
int : Decoder Int
int =
    D.map (\x -> Maybe.withDefault 0 (String.toInt x)) <| D.at [ "integerValue" ] D.string

bool : Decoder Bool
bool =
    D.at [ "booleanValue" ] D.bool

配列のデコード

配列が妙に深い、大きい気がするんですけど、elmのarrayってjsにportで渡すとマップになってるのかな。。

{
    "cards": {
      "arrayValue": {
        "values": [
          {
            "mapValue": {
              "fields": {...}
           }
         ]
        }
    }          
}

ついでなのでリストも作っておく。

FirestoreApi.elm
array : Decoder a -> Decoder (Array a)
array decoder =
    D.at [ "arrayValue", "values" ] <| D.array (D.at [ "mapValue", "fields" ] decoder)

list : Decoder a -> Decoder (List a)
list decoder =
    D.at [ "arrayValue", "values" ] <| D.list (D.at [ "mapValue", "fields" ] decoder)

カードのデコード

深い深いと思ってましたが、上記で必要なものはほぼ揃ったんですよね。
ぱっと見大きくても、一つずつ分解して考えると実はシンプル。



cardDecoderFromFireStoreApi : Decoder CardData
cardDecoderFromFireStoreApi =
    D.succeed CardData
        |> required "cardId" (D.map CardId.fromString FSApi.string)
        |> required "cardName" FSApi.string
        |> required "cardType" FSApi.string
        |> required "kind" FSApi.string
        |> required "exp" FSApi.int
        |> required "timing" FSApi.string
        |> required "cost" FSApi.int
        |> required "range" FSApi.int
        |> required "maxRange" FSApi.int
        |> required "target" FSApi.string
        |> required "maxLevel" FSApi.int
        |> required "effect" FSApi.string
        |> required "description" FSApi.string
        |> required "tags" Tag.tagsDecoderFromFireStoreApi
        |> required "imgMain" FSApi.string
        |> required "illustedByName" FSApi.string
        |> required "illustedByUrl" FSApi.string
        |> required "imgFrame" FSApi.string
        |> required "frameByName" FSApi.string
        |> required "frameByUrl" FSApi.string
        |> required "deleteFlag" FSApi.int

タグのデコード

項目が少ないのでパイプラインなしです。

tagDecoderFromFireStoreApi : Decoder Tag
tagDecoderFromFireStoreApi =
    D.map2 Tag
        (D.field "name" FSApi.string)
        (D.field "level" FSApi.int)

できたもの

以下で動作しています。

この時点のソース

参考

Elm Decoder a からいろいろ理解ってしまおう
Elm 用の CSV デコーダーを作った
elm/json
elm-decode-pipeline
Firebaseで保存したDictionary(連想配列)なJSONをElmでテーブル表示してみた
Firebase Cloud Functions + Firestore超入門
Cloud Firestore REST API の使用
Elmで冗長な書き方をしていたデコーダなどをリファクタリングしたメモ
arowM/elm-form-decoderでフォームを作ろう

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3