簡體   English   中英

通過兩個wreq API調用創建Aeson模型

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM