[英]Creating an Aeson model from two wreq API calls
我正在尋求解決一個問題,該問題是我從HTTP調用構造一些數據,然后根據該數據進行另一個HTTP調用,並用第二個調用的信息豐富原始數據。
我有一些代碼,它通過wreq將Spotify最近播放的API調用(JSON)作為ByteString並返回我完全形成的“ RecentlyPlayed”數據類型。
但是,為了在Spotify API中獲得曲目的流派,需要對其藝術家端點進行第二次HTTP調用,我不太確定如何修改我的Track數據類型以在其中添加“流派”字段稍后再填充,我也不確定如何稍后再填充,顯然我需要遍歷原始數據結構,拉出藝術家ID,調用新服務器-但我不確定如何添加它將其他數據添加到原始數據類型。
{-# LANGUAGE OverloadedStrings #-}
module Types.RecentlyPlayed where
import qualified Data.ByteString.Lazy as L
import qualified Data.Vector as V
import Data.Aeson
import Data.Either
data Artist = Artist {
id :: String
, href :: String
, artistName :: String
} deriving (Show)
data Track = Track {
playedAt :: String
, externalUrls :: String
, name :: String
, artists :: [Artist]
, explicit :: Bool
} deriving (Show)
data Tracks = Tracks {
tracks :: [Track]
} deriving (Show)
data RecentlyPlayed = RecentlyPlayed {
recentlyPlayed :: Tracks
, next :: String
} deriving (Show)
instance FromJSON RecentlyPlayed where
parseJSON = withObject "items" $ \recentlyPlayed -> RecentlyPlayed
<$> recentlyPlayed .: "items"
<*> recentlyPlayed .: "next"
instance FromJSON Tracks where
parseJSON = withArray "items" $ \items -> Tracks
<$> mapM parseJSON (V.toList items)
instance FromJSON Track where
parseJSON = withObject "tracks" $ \tracks -> Track
<$> tracks .: "played_at"
<*> (tracks .: "track" >>= (.: "album") >>= (.: "external_urls") >>= (.: "spotify"))
<*> (tracks .: "track" >>= (.: "name"))
<*> (tracks .: "track" >>= (.: "artists"))
<*> (tracks .: "track" >>= (.: "explicit"))
instance FromJSON Artist where
parseJSON = withObject "artists" $ \artists -> Artist
<$> artists .: "id"
<*> artists .: "href"
<*> artists .: "name"
marshallRecentlyPlayedData :: L.ByteString -> Either String RecentlyPlayed
marshallRecentlyPlayedData recentlyPlayedTracks = eitherDecode recentlyPlayedTracks
( https://github.com/imjacobclark/Recify/blob/master/src/Types/RecentlyPlayed.hs )
這對於單個API調用非常有效,其用法如下所示:
recentlyPlayedTrackData <- liftIO $ (getCurrentUsersRecentlyPlayedTracks (textToByteString . getAccessToken . AccessToken $ accessTokenFileData))
let maybeMarshalledRecentlyPlayed = (marshallRecentlyPlayedData recentlyPlayedTrackData)
( https://github.com/imjacobclark/Recify/blob/master/src/Recify.hs#L53-L55 )
{-# LANGUAGE OverloadedStrings #-}
module Clients.Spotify.RecentlyPlayed where
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Char8 as B
import qualified Network.Wreq as W
import System.Environment
import Control.Monad.IO.Class
import Control.Lens
recentlyPlayerUri = "https://api.spotify.com/v1/me/player/recently-played"
getCurrentUsersRecentlyPlayedTracks :: B.ByteString -> IO L.ByteString
getCurrentUsersRecentlyPlayedTracks accessToken = do
let options = W.defaults & W.header "Authorization" .~ [(B.pack "Bearer ") <> accessToken]
text <- liftIO $ (W.getWith options recentlyPlayerUri)
return $ text ^. W.responseBody
( https://github.com/imjacobclark/Recify/blob/master/src/Clients/Spotify/RecentlyPlayed.hs )
我希望能夠調用第一個API,構造我的數據類型,調用第二個API,然后用第二個HTTP調用返回的數據豐富第一個數據類型。
如您所知,與Javascript對象不同,Haskell ADT不可擴展,因此您不能簡單地“添加字段”。 在某些情況下,包括一個Maybe
類型的字段(最初設置為Nothing
,然后填充)可能是有意義的。 極少數情況下,執行非常不安全的操作可能是有意義的,包括將字段包含為其最終類型,但是將其值初始化為bottom(即, undefined
),然后將其填充。
或者,您可以切換到某種顯式可擴展的記錄類型,例如HList
。
但是,最直接的方法是按預期使用Haskell類型系統的一種方法,就是引入一種新類型來表示一種增加了體裁信息的曲目。 如果您有其他包含要重用的“ Track
字段的數據類型,則可以使它們在跟蹤類型中變為多態。 因此,鑒於上述數據類型,您將引入新的類型:
data Track' = Track'
{ playedAt :: String
, externalUrls :: String
, name :: String
, artists :: [Artist]
, genres :: [Genre] -- added field
, explicit :: Bool
}
(這要求DuplicateRecordFields
擴展名與Track
共存),並使依賴類型在Track類型中變為多態:
data Tracks trk = Tracks
{ tracks :: [trk]
}
data RecentlyPlayed trk = RecentlyPlayed
{ recentlyPlayed :: Tracks trk
, next :: String
}
播放列表的轉換可以使用以下方法完成:
addGenre :: (Artist -> [Genre]) -> RecentlyPlayed Track -> RecentlyPlayed Track'
addGenre g (RecentlyPlayed (Tracks trks) nxt)
= RecentlyPlayed (Tracks (map cvtTrack trks)) nxt
where
cvtTrack (Track p e n a ex) = Track' p e n a (concatMap g a) ex
或者使用RecordWildCards
擴展名,該擴展名將更加可讀,尤其是對於非常大的記錄:
addGenre' :: (Artist -> [Genre]) -> RecentlyPlayed Track -> RecentlyPlayed Track'
addGenre' g RecentlyPlayed{recentlyPlayed = Tracks trks, ..}
= RecentlyPlayed{recentlyPlayed = Tracks (map cvtTrack trks), ..}
where
cvtTrack (Track{..}) = Track' { genres = concatMap g artists, .. }
或使用Lens方法,甚至使用deriving (Functor)
實例,而所有繁重的工作都由fmap
完成:
addGenre'' :: (Artist -> [Genre]) -> RecentlyPlayed Track -> RecentlyPlayed Track'
addGenre'' g = fmap cvtTrack
where
cvtTrack (Track{..}) = Track' { genres = concatMap g artists, .. }
盡管如果有多個擴展(例如,如果您發現要引入“ RecentlyPlayed artist track
類型),函子方法的縮放效果將不會很好。 在這種情況下, Data.Generics
方法可能效果很好。
但是,從更一般的設計角度來看,您可能會想問自己,為什么要嘗試以這種方式增加“ RecentlyPlayed
播放”表示形式。 這很好地表示了底層Javascript API的必需部分,但是在其余程序邏輯中卻很難使用。
大概,程序的其余部分主要處理音軌列表,並且不應該關注后續的next
URL,那么為什么不直接生成完整的體裁增強音軌列表呢?
也就是說,給定一個初始的“ RecentlyPlayed
播放過”列表和一些IO函數以獲取下一個列表並查找類型信息:
firstRecentlyPlayed :: RecentlyPlayed
getNextRecentlyPlayed :: String -> IO RecentlyPlayed
getGenresByArtist :: Artist -> IO [Genre]
您可能想要類似的東西:
getTracks :: IO [Track']
getTracks = go firstRecentlyPlayed
where go :: RecentlyPlayed -> IO [Track']
go (RecentlyPlayed (Tracks trks) next) = do
trks' <- mapM getGenre trks
rest <- go =<< getNextRecentlyPlayed next
return $ trks' ++ rest
getGenre Track{..} = do
artistGenres <- mapM getGenresByArtist artists
return (Track' {genres = concat artistGenres, ..})
第一次嘗試。 當然,您需要修改此設置,以避免一遍又一遍地查找同一位藝術家的流派,但這就是這個主意。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.