概要
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
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
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で出したものを保存している前提だからいっかなと、、
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": {...}
}
]
}
}
}
ついでなのでリストも作っておく。
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でフォームを作ろう