ab1207
ab1207

Reputation: 85

Elm: decoding json from http response and showing it

I'm kind of new to Elm and I find it very hard to decode a json from a http response.
The app I'm making is doing a call to gravatar and receives a profile. I'd like to extract some fields from the response and put in in a record, which in turn in shown in the view. This is my code:

-- MODEL

type alias MentorRecord =
    { displayName : String
    , aboutMe : String
    , currentLocation : String
    , thumbnailUrl : String
    }

type alias Model =
    { newMentorEmail : String
    , newMentor : MentorRecord
    , mentors : List MentorRecord
    }

init : ( Model, Cmd Msg )
init =
    ( Model "" (MentorRecord "" "" "" "") [], Cmd.none )

-- UPDATE

type Msg
    = MentorEmail String
    | AddMentor
    | GravatarMentor (Result Http.Error MentorRecord)
    | RemoveMentor

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        MentorEmail newEmail ->
            ( { model | newMentorEmail = newEmail }, Cmd.none )
        AddMentor ->
            ( model, getGravatarMentor model.newMentorEmail )
        GravatarMentor (Ok addedMentor) ->
            ( Model "" addedMentor (addedMentor :: model.mentors)
            , Cmd.none
            )
        GravatarMentor (Err _) ->
            ( model, Cmd.none )
        RemoveMentor ->
            ( model, Cmd.none )

-- VIEW
view : Model -> Html Msg
view model =
    div []
        [ input [ placeholder "Email adress mentor", onInput MentorEmail ] []
        , button [ onClick AddMentor ] [ text "Add Mentor" ]
        , br [] []
        , img [ src (createIconUrl model.newMentorEmail) ] []
        , div [] [ text model.newMentor.displayName ]
        , div [] [ toHtmlImgList model.mentors ]
        ]

toHtmlImgList : List MentorRecord -> Html Msg
toHtmlImgList mentors =
    ul [] (List.map toLiImg mentors)

toLiImg : MentorRecord -> Html Msg
toLiImg mentor =
    li [] [ img [ src mentor.thumbnailUrl ] [] ]

-- HTTP

getGravatarMentor : String -> Cmd Msg
getGravatarMentor newMentorEmail =
    Http.send GravatarMentor
        (Http.get (createProfileUrl newMentorEmail) decodeGravatarResponse)

createProfileUrl : String -> String
createProfileUrl email =
    "https://en.gravatar.com/" ++ MD5.hex email ++ ".json"

createIconUrl : String -> String
createIconUrl email =
    "https://www.gravatar.com/avatar/" ++ MD5.hex email

decodeGravatarResponse : Decoder MentorRecord
decodeGravatarResponse =
    let
        mentorDecoder =
            Json.Decode.Pipeline.decode MentorRecord
                |> Json.Decode.Pipeline.required "displayName" string
                |> Json.Decode.Pipeline.required "aboutMe" string
                |> Json.Decode.Pipeline.required "currentLocation" string
                |> Json.Decode.Pipeline.required "thumbnailUrl" string
    in
        at [ "entry" ] mentorDecoder

If a valid email address if filled in (i.e. one with a gravatar profile), you see the icon. But what this code also should do is extract name, location, about me info, thumbnailUrl from another http response, put it in a list, and show it in the view. And that's not happening if you click on 'Add mentor'

So I guess the decoding part isn't going very well, but I'm not sure (maybe because the nested element is in a list?).

A response from gravatar looks like this (removed some fields in entry):

{ "entry": [
    {
    "preferredUsername": "bla",
    "thumbnailUrl": "https://secure.gravatar.com/avatar/hashinghere",
    "displayName": "anne",
    "aboutMe": "Something...",
    "currentLocation": "Somewhere",
    }
]}

Code in Ellie app: https://ellie-app.com/n5dxHhvQPa1/1

Upvotes: 4

Views: 597

Answers (1)

Dogbert
Dogbert

Reputation: 222040

entry is an array. To decode the contents of the first element of the array, you need to use Json.Decode.index.

Change:

(at [ "entry" ]) mentorDecoder

to

(at [ "entry" ] << index 0) mentorDecoder

But the bigger problem here is that Gravatar does not support cross origin requests (CORS) but only JSONP. elm-http doesn't support JSONP. You can either use ports for that or use a third party service which enables you to make CORS requests to arbitrary sites. I've used the latter in the ellie link below but you should use ports or your own CORS proxy in a real production application.

I also made aboutMe and currentLocation optional as they weren't present in the profile I checked. Here's the link: https://ellie-app.com/pS2WKpJrFa1/0

The original functions:

createProfileUrl : String -> String
createProfileUrl email =
    "https://en.gravatar.com/" ++ MD5.hex email ++ ".json"

decodeGravatarResponse : Decoder MentorRecord
decodeGravatarResponse =
    let
        mentorDecoder =
            Json.Decode.Pipeline.decode MentorRecord
                |> Json.Decode.Pipeline.required "displayName" string
                |> Json.Decode.Pipeline.required "aboutMe" string
                |> Json.Decode.Pipeline.required "currentLocation" string
                |> Json.Decode.Pipeline.required "thumbnailUrl" string
    in
        at [ "entry" ] mentorDecoder

The changed functions:

createProfileUrl : String -> String
createProfileUrl email =
    "https://crossorigin.me/https://en.gravatar.com/" ++ MD5.hex email ++ ".json"

decodeGravatarResponse : Decoder MentorRecord
decodeGravatarResponse =
    let
        mentorDecoder =
            Json.Decode.Pipeline.decode MentorRecord
                |> Json.Decode.Pipeline.required "displayName" string
                |> Json.Decode.Pipeline.optional "aboutMe" string ""
                |> Json.Decode.Pipeline.optional "currentLocation" string ""
                |> Json.Decode.Pipeline.required "thumbnailUrl" string
    in
    (at [ "entry" ] << index 0) mentorDecoder

Upvotes: 3

Related Questions