I need to read from a RSS feed that contains <enclosure>
element in Elm. While I can conveniently use YQL to get the JSON version of the feed directly, they comes in different shapes:
//case 1: As object
{ ... enclosure: { url: 'http://...' , length:12345, type:'audio/mpeg' } ... }
//case 2: As list, I'll take the first one
{ ... enclosure : [
{ url:'http://...', length:12345, type:'audio/mpeg'} ,
{ url:'http://...', length:12345, type:'audio/mpeg'} ,
...
]
}
//case 3: enclosure with type other than audio will be filtered out
{ ... enclosure: { url: 'http://...' , length:12345, type:'video/mp4' } ... }
//case 4: no enclosure, this will be filtered out too
{ ... }
To decode it into type like this:
type alias Model =
{ list : List Item }
type alias Item =
{ url : String }
We can do this one by one and compose it together. First we do the simple case of just decode the url and type as tuple for further processing.
import Json.Decode as Json
decodeSingleEnclosure : Json.Decoder (String, String)
decodeSingleEnclosure =
Json.object2 (,)
("url" := Json.string)
("type" := Json.string)
We need to do a validation on that enclosure type as in case 3. We will use andThen
to do it.
decodeSingleEnclosureUrl : Json.Decoder String
decodeSingleEnclosureUrl =
decodeSingleEnclosure
`Json.andThen`
(\(url, type_) ->
if Regex.contains (Regex.regex "^audio/") type_ then
Json.succeed url
else
Json.fail "not audio type"
)
To decode the list of enclosure in case 2, we first decode it as list, andThen
take the first item. Note that we use maybe
to allow failed decoding in list and filter it using filterMap
.
decodeEnclosureListUrl : Json.Decoder String
decodeEnclosureListUrl =
Json.list (Json.maybe decodeSingleEnclosureUrl)
`Json.andThen`
(\list -> -- List (Maybe String)
let
head = list
|> List.filterMap identity
|> List.head
in
case head of
Just first ->
Json.succeed first
Nothing ->
Json.fail "cannot get enclosure"
)
Now we can use oneOf
to deal with both case 1 and 2
decodeEnclosureUrl : Json.Decoder String
decodeEnclosureUrl =
Json.oneOf
[ "enclosure" := decodeSingleEnclosureUrl
, "enclosure" := decodeEnclosureListUrl
]
Fianlly, we deocde that url into list item. If no URL is found, the item is skipped
decodeItem: Json.Decoder (Maybe Item)
decodeItem =
Json.maybe (Json.object1 Item decodeEnclosureUrl)
decodeModel : Json.Decoder Model
decodeModel =
Json.map Model
( ("items" := Json.list decodeItem)
`Json.andThen`
(\list -> list
|> List.filterMap identity
|> Json.succeed
)
)
Check the whole exmaple at this gist. You can copy it to elm-lang.org/try to run it.
Decoding complex JSON in Elm is something I found quite hard at first. I need to wrap my head around and get familiar with the whole decoder concept. Again, function composition is the main key to deal with complex JSON. Try to arrive at the simplest case first and then gradually build on top of that.