Разбор ответа XML с использованием 'servant-client' и 'servant- xml' - PullRequest
1 голос
/ 15 марта 2020

Я хочу проанализировать ответ API в тип данных, используя библиотеки servant-client, servant-xml и xmlbf.

Это пример ответа API

<GoodreadsResponse>
   <Request>
      <authentication>true</authentication>
      <key>api_key</key>
      <method>search_index</method>
   </Request>
   <search>
      <query>Ender's Game</query>
      <results-start>1</results-start>
      <results-end>20</results-end>
   </search>
</GoodreadsResponse>

, и это тип данных, который я хочу проанализировать в

data GoodreadsRequest = 
        GoodreadsRequest { authentication :: Text
                         , key            :: Text
                         , method         :: Text
                         }


data GoodreadsSearch = 
        GoodreadsSearch { query        :: Text
                        , resultsStart :: Int
                        , resultsEnd   :: Int
                        }


data GoodreadsResponse = 
        GoodreadsResponse { goodreadsRequest :: GoodreadsRequest
                          , goodreadsSearch  :: GoodreadsSearch
                          }

Это тип API служащего, с которым я хочу его использовать

type API
  = "search" :> "index.xml" :> QueryParam "key" Key :> QueryParam "q" Query :> Get '[XML] GoodreadsResponse

, которая создает конечную точку, подобную этой

https://www.goodreads.com/search/index.xml?key=api_key&q=Ender%27s+Game

и после написания остальной части кода скаффолдинга (clientM, baseURL, клиентская среда и т. Д. c), ошибка I get is

No instance for (FromXml GoodreadsResponse) arising from a use of 'client'

Запись

instance FromXml GoodreadsResponse where
    fromXml = undefined

подавляет ошибку, поэтому я думаю, что я на правильном пути, но я не знаю, как go написать программу синтаксического анализа .


Редактировать: Результат из другой конечной точки, которая содержит список «работ»

<GoodreadsResponse>
   <Request>
      <authentication>true</authentication>
      <key>api_key</key>
      <method>search_index</method>
   </Request>
   <search>
      <query>Ender's Game</query>
      <results-start>1</results-start>
      <results-end>20</results-end>
      <results>
            <work>
                <id type="integer">2422333</id>
                <average_rating>4.30</average_rating>
                <best_book type="Book">
                    <id type="integer">375802</id>
                    <title>Ender's Game (Ender's Saga, #1)</title>
                </best_book>
            </work>
            <work>
                <id type="integer">4892733</id>
                <average_rating>2.49</average_rating>
                <best_book type="Book">
                    <id type="integer">44687</id>
                    <title>Enchanters' End Game (The Belgariad, #5)</title>
                </best_book>
            </work>
            <work>
                <id type="integer">293823</id>
                <average_rating>2.30</average_rating>
                <best_book type="Book">
                    <id type="integer">6393082</id>
                    <title>Ender's Game, Volume 1: Battle School (Ender's Saga)</title>
                 </best_book>
            </work>
      </results>
   </search>
</GoodreadsResponse>

для анализа в

data GoodreadsResponse = 
        GoodreadsResponse { goodreadsRequest :: GoodreadsRequest
                          , goodreadsSearch  :: GoodreadsSearch
                          }

data GoodreadsRequest = 
        GoodreadsRequest { authentication :: Text
                         , key            :: Text
                         , method         :: Text
                         }

data GoodreadsSearch = 
        GoodreadsSearch { query        :: Text
                        , resultsStart :: Int
                        , resultsEnd   :: Int
                        , results      :: GoodreadsSearchResults
                        }

data GoodreadsSearchResults = GooreadsSearchResults { works :: [Work] }

data Work = Work { workID               :: Int
                 , workAverageRating    :: Double
                 , workBestMatchingBook :: Book
                 }

data Book = Book { bookID    :: Int
                 , bookTitle :: Text
                 }

1 Ответ

1 голос
/ 15 марта 2020

Ничего себе, в xmlbf нет примеров или предопределенных экземпляров, и в его документации также есть несколько ошибок. В любом случае, немного поиграв с ним, похоже, что вот как вы это делаете:

{-# LANGUAGE OverloadedStrings #-}

import Data.Text.Lazy (unpack)
import Text.Read (readEither)
import Xmlbf

instance FromXml GoodreadsRequest where
  fromXml = pElement "Request" $ do
    a <- pElement "authentication" pText
    k <- pElement "key" pText
    m <- pElement "method" pText
    pure GoodreadsRequest{ authentication = a, key = k, method = m }

instance FromXml GoodreadsSearch where
  fromXml = pElement "search" $ do
    q <- pElement "query" pText
    s <- pElement "results-start" pText
    s' <- either fail return . readEither $ unpack s
    e <- pElement "results-end" pText
    e' <- either fail return . readEither $ unpack e
    pure GoodreadsSearch{ query = q, resultsStart = s', resultsEnd = e' }

instance FromXml GoodreadsResponse where
  fromXml = pElement "GoodreadsResponse" $ do
    r <- fromXml
    s <- fromXml
    pure GoodreadsResponse{ goodreadsRequest = r, goodreadsSearch = s }

И вот он работает с вашим примером XML:

GHCi, version 8.8.2: https://www.haskell.org/ghc/  :? for help
Prelude> :l Main.hs
[1 of 1] Compiling Main             ( Main.hs, interpreted )
Ok, one module loaded.
*Main> :set -XOverloadedStrings
*Main> import Xmlbf.Xeno
*Main Xmlbf.Xeno> fromRawXml "<GoodreadsResponse>\n   <Request>\n      <authentication>true</authentication>\n      <key>api_key</key>\n      <method>search_index</method>\n   </Request>\n   <search>\n      <query>Ender's Game</query>\n      <results-start>1</results-start>\n      <results-end>20</results-end>\n   </search>\n</GoodreadsResponse>" >>= runParser fromXml :: Either String GoodreadsResponse
Right (GoodreadsResponse {goodreadsRequest = GoodreadsRequest {authentication = "true", key = "api_key", method = "search_index"}, goodreadsSearch = GoodreadsSearch {query = "Ender's Game", resultsStart = 1, resultsEnd = 20}})
*Main Xmlbf.Xeno>

Изменить: Вот как вы используете его в списках с другой конечной точкой:

{-# LANGUAGE OverloadedStrings #-}

import Control.Applicative (Alternative(many))
import Data.Text.Lazy (unpack)
import Text.Read (readEither)
import Xmlbf

instance FromXml GoodreadsResponse where
  fromXml = pElement "GoodreadsResponse" $ do
    r <- fromXml
    s <- fromXml
    pure GoodreadsResponse{ goodreadsRequest = r, goodreadsSearch = s }

instance FromXml GoodreadsRequest where
  fromXml = pElement "Request" $ do
    a <- pElement "authentication" pText
    k <- pElement "key" pText
    m <- pElement "method" pText
    pure GoodreadsRequest{ authentication = a, key = k, method = m }

instance FromXml GoodreadsSearch where
  fromXml = pElement "search" $ do
    q <- pElement "query" pText
    s <- pElement "results-start" pText
    s' <- either fail return . readEither $ unpack s
    e <- pElement "results-end" pText
    e' <- either fail return . readEither $ unpack e
    r <- fromXml
    pure GoodreadsSearch{ query = q, resultsStart = s', resultsEnd = e', results = r }

instance FromXml GoodreadsSearchResults where
  fromXml = pElement "results" $ do
    w <- many fromXml
    pure GooreadsSearchResults{ works = w }

instance FromXml Work where
  fromXml = pElement "work" $ do
    i <- pElement "id" pText -- the type attribute is ignored
    i' <- either fail return . readEither $ unpack i
    r <- pElement "average_rating" pText
    r' <- either fail return . readEither $ unpack r
    b <- fromXml
    pure Work{ workID = i', workAverageRating = r', workBestMatchingBook = b }

instance FromXml Book where
  fromXml = pElement "best_book" $ do -- the type attribute is ignored
    i <- pElement "id" pText -- the type attribute is ignored
    i' <- either fail return . readEither $ unpack i
    t <- pElement "title" pText
    pure Book{ bookID = i', bookTitle = t }

И результат:

GHCi, version 8.8.2: https://www.haskell.org/ghc/  :? for help
Prelude> :l Main.hs
[1 of 1] Compiling Main             ( Main.hs, interpreted )
Ok, one module loaded.
*Main> :set -XOverloadedStrings
*Main> import Xmlbf.Xeno
*Main Xmlbf.Xeno> fromRawXml "<GoodreadsResponse>\n   <Request>\n      <authentication>true</authentication>\n      <key>api_key</key>\n      <method>search_index</method>\n   </Request>\n   <search>\n      <query>Ender's Game</query>\n      <results-start>1</results-start>\n      <results-end>20</results-end>\n      <results>\n            <work>\n                <id type=\"integer\">2422333</id>\n                <average_rating>4.30</average_rating>\n                <best_book type=\"Book\">\n                    <id type=\"integer\">375802</id>\n                    <title>Ender's Game (Ender's Saga, #1)</title>\n                </best_book>\n            </work>\n            <work>\n                <id type=\"integer\">4892733</id>\n                <average_rating>2.49</average_rating>\n                <best_book type=\"Book\">\n                    <id type=\"integer\">44687</id>\n                    <title>Enchanters' End Game (The Belgariad, #5)</title>\n                </best_book>\n            </work>\n            <work>\n                <id type=\"integer\">293823</id>\n                <average_rating>2.30</average_rating>\n                <best_book type=\"Book\">\n                    <id type=\"integer\">6393082</id>\n                    <title>Ender's Game, Volume 1: Battle School (Ender's Saga)</title>\n                 </best_book>\n            </work>\n      </results>\n   </search>\n</GoodreadsResponse>" >>= runParser fromXml :: Either String GoodreadsResponse
Right (GoodreadsResponse {goodreadsRequest = GoodreadsRequest {authentication = "true", key = "api_key", method = "search_index"}, goodreadsSearch = GoodreadsSearch {query = "Ender's Game", resultsStart = 1, resultsEnd = 20, results = GooreadsSearchResults {works = [Work {workID = 2422333, workAverageRating = 4.3, workBestMatchingBook = Book {bookID = 375802, bookTitle = "Ender's Game (Ender's Saga, #1)"}},Work {workID = 4892733, workAverageRating = 2.49, workBestMatchingBook = Book {bookID = 44687, bookTitle = "Enchanters' End Game (The Belgariad, #5)"}},Work {workID = 293823, workAverageRating = 2.3, workBestMatchingBook = Book {bookID = 6393082, bookTitle = "Ender's Game, Volume 1: Battle School (Ender's Saga)"}}]}}})
*Main Xmlbf.Xeno>

Новая концепция ключа в этом - Control.Applicative.many. Он продолжает работать Alternative до тех пор, пока не выйдет из строя, а затем помещает все успешные результаты в список. В этом случае это означает повторение fromXml :: Parser Work до тех пор, пока оно не начнет выходить из строя (надеюсь, потому что не осталось <work> s). Обратите внимание, что есть один недостаток в том, как many работает в этом контексте (IMO, потому что интерфейс синтаксического анализатора xmlbf не очень хорош), а именно, что неправильно сформированный элемент <work> просто вызовет все от него через </results> игнорироваться, вместо того, чтобы всплывать ошибка. Вы можете использовать немного более сложный код, включающий pChildren, чтобы исправить это, если хотите.

...