Создание модели Aeson из двух вызовов API WREQ - PullRequest
2 голосов
/ 03 июня 2019

Я пытаюсь решить проблему, когда я строю некоторые данные из HTTP-вызова, а затем на основе этих данных делаю еще один HTTP-вызов и обогащаю исходные данные информацией из второго вызова.

Iесть код, который принимает вызов API Spotify недавно воспроизведенный (JSON) через wreq в качестве ByteString и возвращает мой полностью сформированный тип данных «RecentPlayed».

Однако, чтобы получить жанр дорожки в SpotifyAPI, необходим второй HTTP-вызов для конечной точки исполнителя, я не совсем уверен, как я могу изменить свой тип данных Track, чтобы добавить поле «Жанр», которое я буду заполнять позже, я также не уверен, как на самом делеЗаполните его позже, очевидно, мне нужно пройтись по моей исходной структуре данных, вытащить идентификатор исполнителя, вызвать новый сервер - но я не уверен, как добавить эти дополнительные данные в исходный тип данных.

{-# 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-вызова.

1 Ответ

0 голосов
/ 04 июня 2019

Как вы, несомненно, знаете, в отличие от объектов Javascript, ADT Haskell не являются расширяемыми, поэтому вы не можете просто «добавить поле».В определенных обстоятельствах может иметь смысл включить поле с типом Maybe, изначально установленным на Nothing, которое затем заполняется.В редких случаях имеет смысл выполнить очень небезопасную операцию, включив поле с его окончательным типом, но его значения инициализируются снизу (т. Е. undefined) и заполняют его позже.

В качестве альтернативы вы можете переключиться накакой-то явно расширяемый тип записи, такой как HList.

. Однако наиболее простой подход, использующий систему типов Haskell по назначению, заключается во введении нового типа для представления дорожки.дополнен жанровой информацией.Если у вас есть дополнительные типы данных, включающие поля Track, которые вы хотите использовать повторно, они могут быть полиморфными в типе дорожки.Итак, учитывая ваши типы данных выше, вы должны ввести новый тип:

data Track' = Track'
  { playedAt :: String
  , externalUrls :: String
  , name :: String
  , artists :: [Artist]
  , genres :: [Genre]     -- added field
  , explicit :: Bool
  }

(для которого требуется расширение DuplicateRecordFields, чтобы сосуществовать с 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 таким способом.Это хорошее представление необходимых частей базового API Javascript, но это плохое представление для работы с остальной логикой вашей программы.

Предположительно, остальная часть вашей программы имеет дело главным образом со спискомотслеживает, и не должен заботиться о следующих next URL, так почему бы не сгенерировать полный список треков с расширенным жанром напрямую?

То есть, учитывая начальный список RecentlyPlayed и некоторые функции ввода-вывода дляполучите следующий список и найдите информацию о жанре:

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, ..})

для первой попытки.Конечно, вы захотите изменить это, чтобы не искать жанры одного и того же исполнителя снова и снова, но это идея.

...