Kurzusok
/
Elm
Elm
/
Feladatok
/
GitHup API
GitHup API

GitHup API

Tanulófeladat

Bevezetés

JSON

JSON

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.

Az alapvető adattípusok dekóderei

A JSON-nak kevés alapvető adattípusa van:

  • String: kettős idézőjelek közé írva: "hello world"
  • Boolean: true vagy false
  • Number: például 34 vagy 0.12
  • Null: az adat hiánya, 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).

Dekóderek összekapcsolása

A JSON emellett két adatszerkezetet is definiál az alapvető adattípusok gyűjtésére:

  • Array: más adattípusok gyűjteménye: [0, null, "none", []]
  • Object: kulcs-érték párok gyűjteménye: {"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.

GeoJSON

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.

Geometriaobjektum

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 ...
Feature-objektum

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>)]) }

JSON-kódolók

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.

Utasítások

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.

Note

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.

1. ID-t kérek!

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

2. Hogy is hívnak?

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

3. Felhasználó? Alig ismerem!

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
    -->   }

4. Erős, független komment vagyok

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 ...

5. Mellékállás

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 ...

6. A linkek ébredése

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" )
    -->       ]
    -->   )

7. Igen, kommentek

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.

8. Minden összeáll

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.

Szerkesztés GitHubon A hivatkozás új ablakban vagy lapon nyílik meg
Elm Exercism

Készen állsz elkezdeni a(z) GitHup API feladatot?

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.