A JSON (JavaScript Object Notation) egy ember számára is olvasható adat- és fájlformátum, amelyet gyakran használnak adatok cseréjére, különösen webes alkalmazásokban. Így nem meglepő, hogy különleges helyet foglal el az Elm nyelvben.
Az Elm a Json.Decode és a Json.Encode alapmodullal teszi lehetővé a JSON-adatok feldolgozását és írását.
A JSON-dekóderek segítségével deklarálhatod, milyen típusú adatra számítasz, és hogy az adatot milyen adatszerkezetre kell leképezni.
Importáljunk néhány aliast és típust a modulokból:
import Json.Decode as Decode exposing (Decoder, Error)
import Json.Encode as Encode exposing (Value)
Ha a Decode.decodeString : Decoder a -> String -> Result Error a függvényt egy dekóderrel és egy JSON stringgel hívod meg, akkor az sikeres lesz, ha a string megfelel a dekóder előírásainak, egyébként pedig hibát ad.
A dekóder definiálásakor nincs módod megvizsgálni a nyers JSON-adatot, ami szokatlannak tűnhet, ha imperatív nyelvekből érkezel.
A JSON-nak kevés alapvető adattípusa van:
"hello world"
true vagy false
34 vagy 0.12
null
Mindegyikhez tartozik egy-egy Decode-függvény:
Decode.decodeString Decode.string """\"hello\""""
--> Ok "hello"
Decode.decodeString Decode.string """null"""
--> Err ...
Decode.decodeString Decode.bool """true"""
--> Ok True
Decode.decodeString Decode.bool """0"""
--> Err ...
Decode.decodeString Decode.int """12"""
--> Ok 12
Decode.decodeString Decode.float """3.14"""
--> Ok 3.14
Decode.decodeString (Decode.null AnyValue) """null"""
--> Ok AnyValue
Decode.decodeString (Decode.null AnyValue) """true"""
--> Err ...
Figyeld meg, hogy a Decode.null segítségével te döntheted el, hogyan modellezed a null értéket a programodban: tetszőleges értéket kér argumentumként (például (), Nothing, vagy bármit, ami a programodhoz illik).
A JSON emellett két adatszerkezetet is definiál az alapvető adattípusok gyűjtésére:
[0, null, "none", []]
{"id": 1, "is_admin": false, "more_info": {}}
A Decode modul ezek dekódolásához is kínál függvényeket:
Decode.decodeString (Decode.list Decode.int) """[1, 2, 3]"""
--> Ok [1, 2, 3]
Decode.decodeString (Decode.list Decode.int) """[1, 2, 3, "not an int"]"""
--> Err ...
Decode.decodeString (Decode.dict Decode.int) """{ "key1": 17, "key2": 71 }"""
--> Ok (Dict.fromList [("key1", 17), ("key2", 71)])
Decode.decodeString (Decode.dict Decode.int) """{ "key1": 17, "key2": "seventy-one" }"""
--> Err ...
A Decode.list : Decoder a -> Decoder (List a) és a Decode.dict : Decoder a -> Decoder (Dict String a) függvény is egy dekódert vár argumentumként, amelynek az adatszerkezet minden egyes elemét dekódolnia kell.
Ha az argumentumként kapott dekóder nem tud dekódolni egy elemet egy tömb- vagy objektumértékből, akkor az egész dekóder meghiúsul, ami gyakori minta.
A technika lényege, hogy az egyszerű dekódereket bonyolultabbakká fűzzük össze. Egy példán keresztül több olyan függvényt is bemutatunk, amelyekkel tetszőlegesen összetett, valós adatokat feldolgozó dekódereket építhetsz.
Hogy gyakoroljuk az alapvető dekóderek összekapcsolását összetett JSON-adatok feldolgozására, építsünk dekódereket a GeoJSON-adatok (egy részhalmazának) feldolgozásához. A GeoJSON egy speciális, JSON-alapú adatformátum, amellyel földrajzi jellemzőket ábrázolnak, például helyeket, útvonalakat vagy régiókat, amelyeket 2D-s vagy 3D-s földrajzi koordinátákkal és további tulajdonságokkal adnak meg.
A GeoJSON Geometriaobjektum egy olyan objektum, amely tartalmaz egy type mezőt egy string értékkel (7 lehetséges típusérték van), és egy coordinates mezőt a földrajzi koordinátákkal (amelyek szerkezete a típustól függ).
Például:
{
"type": "Point",
"coordinates": [127.831, 26.461]
}
vagy
{
"type": "LineString",
"coordinates": [
[127.831, 26.461],
[127.829, 26.465],
[127.829, 26.469],
[127.83, 26.47]
]
}
A Geometriaobjektumot az Elmben a következő típussal ábrázolhatnánk
type Geometry
= Point (List Float)
| LineString (List (List Float))
| ...
Egy adott mező értékének megvizsgálásához használhatjuk a Decode.field : String -> Decoder a -> Decoder a függvényt, amely egy objektum adott kulcsának értékét dekódolja.
decodePointCoordinates : Decoder (List Float)
decodePointCoordinates =
Decode.field "coordinates" (Decode.list Decode.float)
decodeLineStringCoordinates : Decoder (List (List Float))
decodeLineStringCoordinates =
Decode.field "coordinates" (Decode.list (Decode.list Decode.float))
A dekóder által sikeresen feldolgozott érték átalakításához használhatjuk a Decode.map : (a -> value) -> Decoder a -> Decoder value függvényt vagy annak testvéreit, a Decode.map2 (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value függvénytől egészen a Decode.map8-ig.
Ebben az esetben a type mező értéke alapján szeretnénk eldönteni, melyik koordináta-dekódert használjuk, amire tökéletes megoldás a Decode.andThen : (a -> Decoder b) -> Decoder a -> Decoder b, valamint barátai, a Decode.succeed : a -> Decoder a és a Decode.fail : String -> Decoder a.
decodeGeometry : Decoder Geometry
decodeGeometry =
Decode.field "type" Decode.string
|> Decode.andThen
(\value ->
case value of
"Point" ->
decodePointCoordinates |> Decode.map Point
"LineString" ->
decodeLineStringCoordinates |> Decode.map LineString
_ ->
Decode.fail "Geometry not implemented yet, or invalid type value"
)
Jegyezzük meg, hogy ezen a ponton használhatnánk a Decode.andThen függvényt a koordináták érvényesítésére is (annak ellenőrzésére, hogy a koordináták párban vagy hármasban érkeznek-e, és hogy a koordinátaértékeknek van-e földrajzi értelmük).
Decode.decodeString decodeGeometry """{"type": "Point", "coordinates": [127.831, 26.461]}"""
--> Ok (Point [127.831, 26.461])
Decode.decodeString decodeGeometry """{"type": "LineString", "coordinates": [127.831, 26.461]}"""
--> Err ...
A GeoJSON Feature-objektum valamilyen térben körülhatárolt dolgot ábrázol: van egy type mezője a "Feature" értékkel, egy geometry mezője egy Geometriaobjektummal vagy null-lal, egy properties mezője egy tetszőleges JSON-objektummal vagy null-lal, és egy opcionális id mezője egy JSON string- vagy számértékkel.
{
"type": "Feature",
"geometry": null,
"properties": null
}
vagy
{
"type": "Feature",
"id": "0157",
"geometry": {
"type": "Point",
"coordinates": [127.831, 26.461]
},
"properties": {
"country": "Japan"
}
}
Kezdjük az id mezővel.
Ha létezik, akkor stringet vagy számot is tartalmazhat, így vagy bevezethetünk egy egyedi típust az érték számára, vagy mindig stringként menthetjük.
Ha két lehetséges dekóder közül kell választani, használhatjuk a Decode.oneOf : List (Decoder a) -> Decoder a függvényt.
decodeFeatureId : Decoder String
decodeFeatureId =
Decode.oneOf
[ Decode.string
, Decode.int |> Decode.map String.fromInt
, Decode.float |> Decode.map String.fromFloat
]
|> Decode.field "id"
Decode.decodeString decodeFeatureId """{"id": "seventeen"}"""
--> Ok "seventeen"
Decode.decodeString decodeFeatureId """{"id": 17}"""
--> Ok "17"
Decode.decodeString decodeFeatureId """{"type": "Point"}"""
--> Er ...
Hogy kifejezzük, hogy a mező opcionális, ugyanezt a technikát alkalmazhatnánk
decodeMaybeFeatureId : Decoder (Maybe String)
decodeMaybeFeatureId =
Decode.oneOf
[ decodeFeatureId |> Decode.map Just
, Decode.succeed Nothing
]
De használhatnánk helyette a Decode.maybe : Decoder a -> Decoder (Maybe a) függvényt is
decodeMaybeFeatureId : Decoder (Maybe String)
decodeMaybeFeatureId =
Decode.maybe decodeFeatureId
Decode.decodeString decodeMaybeFeatureId """{"id": 17}"""
--> Ok (Just "17")
Decode.decodeString decodeMaybeFeatureId """{"type": "Point"}"""
--> Ok Nothing
Figyeld meg, hogy a decodeMaybeFeatureId (vagy bármelyik Decode.maybe-dal becsomagolt dekóder) mindig sikeres lesz, mert mindig visszaeshet az Ok Nothing értékre, bár egy őt használó dekóder ettől még meghiúsulhat
Decode.decodeString (Decode.list decodeMaybeFeatureId) """{"id": 17}"""
--> Err ...
Most nézzük a geometry mezőt, amely vagy egy Geometriaobjektum, vagy null.
Ismét használhatnánk a Decode.oneOf-ot, de van jobb megoldás: a Decode.nullable : Decoder a -> Decoder (Maybe a).
decodeFeatureGeometry : Decoder (Maybe Geometry)
decodeFeatureGeometry =
Decode.nullable decodeGeometry
|> Decode.field "geometry"
Decode.decodeString decodeFeatureGeometry """{"geometry": {"type": "Point", "coordinates": [127.831, 26.461]}}"""
--> Ok (Just (Point [127.831, 26.461]))
Decode.decodeString decodeFeatureGeometry """{"geometry": null}"""
--> Ok Nothing
Decode.decodeString decodeFeatureGeometry """{"geometry": {}}"""
--> Err ...
A Decode.maybe és a Decode.nullable szignatúrája ugyanaz, a viselkedésük viszont különbözik, mert a Decode.nullable OK Nothing eredménye csak a konkrét null JSON-értékből származhat.
Végül a properties mező tetszőleges JSON-objektumot vagy null-t tartalmazhat, amire a Decode.value : Decoder Value való.
decodeProperties : Decoder (Maybe (Dict String Value))
decodeProperties =
Decode.nullable (Decode.dict Decode.value)
|> Decode.field "properties"
Decode.decodeString decodeProperties """{"properties": null}"""
--> Ok Nothing
Decode.decodeString decodeProperties """{"properties": {"country": "Japan"}}"""
--> Ok (Just (Dict.fromList [("country", <internals>)]))
Decode.decodeString decodeProperties """{"properties": 17}"""
--> Err ...
Figyeld meg, hogy a Value egy átlátszatlan típus, ami azt jelenti, hogy nem tudod könnyen megvizsgálni a tartalmát.
Használhatod úgy, hogy írsz hozzá egy dekódert, és lefuttatod a Decode.decodeValue : Decoder a -> Value -> Result Error a függvénnyel, vagy úgy, hogy egy porton keresztül közvetlenül a JavaScriptnek küldöd.
Készen állunk a Feature-objektumok feldolgozására:
type alias Feature =
{ id : Maybe String
, geometry : Maybe Geometry
, properties : Maybe (Dict String Value)
}
decodeFeature : Decoder Feature
decodeFeature =
Decode.field "type" Decode.string
|> Decode.andThen
(\value ->
if value /= "Feature" then
Decode.fail "not a Feature"
else
Decode.map3 Feature
decodeMaybeFeatureId
decodeFeatureGeometry
decodeProperties
)
Decode.decodeString decodeFeature """{"type": "Feature", "geometry": null, "properties": null}"""
--> Ok { geometry = Nothing, id = Nothing, properties = Nothing }
"""
{
"type": "Feature",
"id": "0157",
"geometry": {
"type": "Point",
"coordinates": [127.831, 26.461]
},
"properties": {
"country": "Japan"
}
}
"""
|> Decode.decodeString decodeFeature
--> Ok { geometry = Just (Point [127.831,26.461]), id = Just "0157", properties = Just (Dict.fromList [("country",<internals>)]) }
A kódolókkal érvényes JSON-t írhatsz Elm-értékekből az Encode.encode : Int -> Value -> String függvény segítségével.
Az első argumentum a végeredmény behúzásának mértékét adja meg, a második pedig a kiírandó JSON-érték.
Egy Value-t vagy a Decode.value-ból kaphatsz, vagy az egyik kódolóval állíthatod elő:
Encode.encode 0 (Encode.string "hello")
--> "hello"
Encode.encode 0 (Encode.bool True)
--> "true"
Encode.encode 0 (Encode.int 12)
--> "12"
Encode.encode 0 (Encode.float 3.14)
--> "3.14"
Encode.encode 0 Encode.null
--> "null"
Encode.encode 0 (Encode.list Encode.int [1, 2, 3])
--> "[1,2,3]"
Encode.encode 4 (Encode.list Encode.int [1, 2, 3])
--> "[\n 1,\n 2,\n 3\n]"
Encode.encode 0 (Encode.dict String.toLower Encode.int (Dict.fromList [("KEY1", 17), ("KEY2", 71)]))
--> "{\"key1\":17,\"key2\":71}"
Encode.encode 4 (Encode.dict String.toLower Encode.int (Dict.fromList [("KEY1", 17), ("KEY2", 71)]))
--> "{\n \"key1\": 17,\n \"key2\": 71\n}"
illetve az Encode.object : List ( String, Value ) -> Value függvénnyel
Encode.object
[ ( "key1", Encode.int 17 )
, ( "key2", Encode.int 71 )
]
|> Encode.encode 0
--> "{\"key1\":17,\"key2\":71}"
Definiáljunk kódolókat a korábban definiált GeoJSON-dekóderekhez
encodeGeometry : Geometry -> Value
encodeGeometry geometry =
case geometry of
Point coord ->
Encode.object
[ ( "type", Encode.string "Point" )
, ( "coordinates", Encode.list Encode.float coord )
]
LineString coord ->
Encode.object
[ ( "type", Encode.string "LineString" )
, ( "coordinates", Encode.list (Encode.list Encode.float) coord )
]
encodeFeature : Feature -> Value
encodeFeature feature =
let
maybeId =
case feature.id of
Nothing ->
[]
Just id ->
[ ( "id", Encode.string id ) ]
in
Encode.object
(maybeId
++ [ ( "type", Encode.string "Feature" )
, ( "geometry"
, case feature.geometry of
Nothing ->
Encode.null
Just geometry ->
encodeGeometry geometry
)
, ( "properties"
, case feature.properties of
Nothing ->
Encode.null
Just dict ->
Encode.dict identity identity dict
)
]
)
Általában jó teszt a dekóder-kódoló párokra, ha ellenőrzöd, hogy elmValue |> encoder |> decoder == elmValue.
Minél többet tanulsz a programozásról, annál inkább kezded azt gondolni, hogy ez több lehet, mint egy múló divat. Lehet, hogy még üzleti lehetőségek is vannak benne: az emberek code-ot írnak, szeretnék menteni, megosztani, együtt dolgozni rajta, és talán még visszajelzést is kapni.
Rendben, csináljuk, építsünk egy szolgáltatást, ami arra ösztönzi az embereket, hogy programozzanak: hup, hup, emberek, kezdjétek el használni a git-et! Nevezzük el... GitHupnak.
Nekiállsz egy REST JSON API-t építeni: felhasználók, pull requestek, kommentek, a backend gyorsan el is készül. Íme egy példa arra, milyen adatcsomagot szolgál ki a szerver, amikor egy pull request kommentjeinek listáját kéred:
[
{
"id": 256,
"pull_request_review_id": 42,
"user": {
"id": 101,
"login": "octodog",
"avatar_url": "https://githup.com/images/error/octodog_happy.gif",
"site_admin": false
},
"body": "Great stuff!",
"side": "RIGHT",
"_links": {
"self": {
"href": "https://api.githup.com/repos/octodog/Hello-World/pulls/comments/1"
},
"html": {
"href": "https://githup.com/octodog/Hello-World/pull/1#discussion-diff-1"
},
"pull_request": {
"href": "https://api.githup.com/repos/octodog/Hello-World/pulls/1"
}
}
},
{
"id": 11,
"pull_request_review_id": null,
"user": {
"id": 2,
"login": "hexacat",
"name": "Alex Kate",
"avatar_url": "https://githup.com/images/error/hexacat_happy.gif",
"site_admin": true
},
"body": "Amazing stuff!",
"side": "LEFT",
"_links": {
"self": {
"href": "https://api.githup.com/repos/octodog/Hello-World/pulls/comments/1"
}
}
}
]
Persze, a frontend Elmben készül majd, de először azt kell kiderítened, mik is azok a hírhedt dekóderek és enkóderek.
A vicces történetet félretéve, a feladat követelményei közvetlenül a GitHub REST API pull request review kommentekhez tartozó végpontjaiból származnak. Néhány mezőt eltávolítottunk a JSON-sémákból az egyszerűség kedvéért, de ez így is a lehető legközelebb áll a valós alkalmazásokhoz.
Sasszemmel észrevetted, hogy a kommenteknek és a felhasználóknak is van id mezője, ez a tökéletes kiindulópont.
Definiáld a decodeId függvényt, hogy egy JSON objektumot tudjon dekódolni, benne egy egész szám típusú id mezővel.
Decode.decodeString decodeId """{id: 10}"""
--> Ok 10
Az előbbi példában észreveszed, hogy nem minden felhasználónak van neve.
Definiáld a decodeName függvényt, hogy dekódolni tudjon egy stringet a name mezőben, ha van.
Nincs név? Semmi baj, egyszerűen add vissza a Nothing értéket.
Ennek a dekódernek soha nem szabad elbuknia.
Decode.decodeString decodeName """{"id": 10, "name": "Otto"}"""
--> Ok (Just Otto)
Decode.decodeString decodeName """null"""
--> Ok Nothing
A felhasználók dekódolásánál már majdnem a felénél tartunk, fejezzük be.
Definiáld a decodeUser függvényt, hogy dekódolni tudjon egy JSON felhasználó objektumot.
Ügyelj rá, hogy a decodeId és a decodeName függvényt használd a decodeUser definíciójában.
Decode.decodeString decodeUser
"""
{
"id": 101,
"login": "octodog",
"avatar_url": "https://githup.com/images/error/octodog_happy.gif",
"site_admin": false
}
"""
--> Ok
--> { id = 101
--> , name = Nothing
--> , login = "octodog"
--> , avatarUrl = "https://githup.com/images/error/octodog_happy.gif
--> , siteAdmin = False
--> }
A pull request áttekintésében néhány komment önálló, ezért nincs pull_request_review_id mezője.
A specifikáció azonban megemlíti, hogy ilyenkor is lennie kell egy pull_request_review_id mezőnek, csak az értéke null legyen, a szegény ember Nothing-ja.
Definiáld a decodePullRequestReviewId függvényt, hogy dekódolni tudjon egy egész számot a pull_request_review_id mezőben, ha van.
Ha nincs egész szám, akkor null-nak kell lennie; ha nincs null, a dekódernek el kell buknia.
Decode.decodeString decodePullRequestReviewId """{"pull_request_review_id": 3}"""
--> Ok (Just 3)
Decode.decodeString decodePullRequestReviewId """{"pull_request_review_id": null}"""
--> Ok Nothing
Decode.decodeString decodePullRequestReviewId """{"id": 3}"""
--> Err ...
Következik ez az érdekesnek tűnő side mező.
Egy pull request áttekintésében kommentelhetsz az eltávolított code-ra (a képernyő Left oldalán) vagy a hozzáadott code-ra (a képernyő Right oldalán).
Definiáld a decodeSide függvényt, hogy dekódolni tudja a side mezőben vagy a "LEFT", vagy a "RIGHT" értéket, és a megfelelő típusváltozathoz rendelje.
Decode.decodeString decodeSide """{"side": "LEFT"}"""
--> Ok Left
Decode.decodeString decodeSide """{"side": "middle?"}"""
--> Err ...
Minden kommenthez tartozik egy linkhalmaz, amely úgy tűnik, nem egységes a kommentek között.
Definiáld a decodeLinks függvényt, hogy dekódolni tudjon egy URL-gyűjteményt a _links mezőben.
A linkek neve és száma változhat, ügyelj rá, hogy ezt kezelni tudd.
Maguk a linkek JSON objektumok, amelyeknek mindig kell tartalmazniuk egy href mezőt.
Decode.decodeString decodeLinks """
{
"_links": {
"self": { "href": "https://api.githup.com/repos/octodog/Hello-World/pulls/comments/1" },
"html": { "href": "https://githup.com/octodog/Hello-World/pull/1#discussion-diff-1" },
"pull_request": { "href": "https://api.githup.com/repos/octodog/Hello-World/pulls/1" }
}
}
"""
--> Ok
--> (Dict.fromList
--> [ ( "self", "https://api.githup.com/repos/octodog/Hello-World/pulls/comments/1" )
--> , ( "html", "https://githup.com/octodog/Hello-World/pull/1#discussion-diff-1" )
--> , ( "pull_request", "https://api.githup.com/repos/octodog/Hello-World/pulls/1" )
--> ]
--> )
Majdnem megvagyunk, fejezzük be.
Definiáld a decodeComment és a decodeComments függvényt, hogy dekódolni tudjanak egyetlen JSON komment objektumot, illetve kommentek listáját.
Ügyelj rá, hogy a decodeComment-ben minden eddig definiált függvényt használj, kivéve a decodeName-et.
Természetesen a decodeComments-nek is a decodeComment-et kell használnia.
Még egy dolog hiányzik az egészből: hogy új kommentet tudj küldeni a szervernek.
Ehhez a dekóder ellenkezőjére van szükség: át kell alakítanod egy Elm Comment-et JSON Value-vá.
Definiáld az encodeComment függvényt, hogy bármely érvényes Comment-et kódolni tudjon.
A mezők sorrendje nem számít, amíg az enkóder olyan értéket állít elő, amelyet a decodeComment dekódolni tud.
Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) Elm nyelvet 28 fogalom110 feladat segítségével, valódi emberi mentorálással, mindez ingyen.