Support collection and sorting query parameters
This commit is contained in:
parent
d032f9b294
commit
070487f924
2 changed files with 60 additions and 128 deletions
133
src/Lib.hs
133
src/Lib.hs
|
@ -2,6 +2,7 @@
|
|||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
module Lib
|
||||
|
@ -15,6 +16,7 @@ import qualified Control.Lens as Database.SQLite
|
|||
import Control.Monad.IO.Class
|
||||
import Data.Aeson
|
||||
import Data.Aeson.TH
|
||||
import qualified Data.List as L
|
||||
import Data.Maybe
|
||||
import Data.OpenApi
|
||||
( HasDescription (description),
|
||||
|
@ -35,6 +37,7 @@ import Database.SQLite.Simple
|
|||
import Database.SQLite.Simple.FromField
|
||||
import Database.SQLite.Simple.Internal
|
||||
import Database.SQLite.Simple.Ok
|
||||
import Database.SQLite.Simple.QQ
|
||||
import GHC.Generics
|
||||
import Network.Wai
|
||||
import Network.Wai.Handler.Warp
|
||||
|
@ -251,6 +254,8 @@ type SearchCards =
|
|||
\: `:` (matches)"
|
||||
:> "search"
|
||||
:> QueryParam' '[Description "Query string"] "q" T.Text
|
||||
:> QueryParam' '[Description "Sorting method"] "sort_by" T.Text
|
||||
:> QueryParam' '[Description "Search across collection or all cards"] "in_collection" T.Text
|
||||
:> Get '[JSON] [Card]
|
||||
|
||||
type TutorAPI =
|
||||
|
@ -307,86 +312,66 @@ status =
|
|||
{ apiVersion = packageVersion
|
||||
}
|
||||
|
||||
cards :: Maybe T.Text -> Handler [Card]
|
||||
cards q =
|
||||
return
|
||||
[ Card
|
||||
{ scryfall_id = fromJust $ fromText "f6cd7465-9dd0-473c-ac5e-dd9e2f22f5f6",
|
||||
name = "Esika, God of the Tree // The Prismatic Bridge",
|
||||
set_code = "KHM",
|
||||
collector_number = "168",
|
||||
rarity = "mythic",
|
||||
color_identity = "WUBGR",
|
||||
oracle_text = Nothing,
|
||||
prices =
|
||||
Prices
|
||||
{ usd = Just "9.07",
|
||||
usd_foil = Just "10.77",
|
||||
eur = Just "7.79",
|
||||
eur_foil = Just "12.27",
|
||||
tix = Just "1.08"
|
||||
}
|
||||
},
|
||||
Card
|
||||
{ scryfall_id = fromJust $ fromText "d761ff73-0717-4ee4-996b-f5547bcf9b2f",
|
||||
name = "Go-Shintai of Life's Origin",
|
||||
set_code = "NEC",
|
||||
collector_number = "66",
|
||||
rarity = "mythic",
|
||||
color_identity = "WUBGR",
|
||||
oracle_text = Just "{W}{U}{B}{R}{G}, {T}: Return target enchantment card from your graveyard to the battlefield.\nWhenever Go-Shintai of Life's Origin or another nontoken Shrine enters the battlefield under your control, create a 1/1 colorless Shrine enchantment creature token.",
|
||||
prices =
|
||||
Prices
|
||||
{ usd = Just "23.28",
|
||||
usd_foil = Nothing,
|
||||
eur = Just "23.05",
|
||||
eur_foil = Nothing,
|
||||
tix = Nothing
|
||||
}
|
||||
},
|
||||
Card
|
||||
{ scryfall_id = fromJust $ fromText "e2539ff7-2b7d-47e3-bd77-3138a6c42d2b",
|
||||
name = "Godsire",
|
||||
set_code = "ALA",
|
||||
collector_number = "170",
|
||||
rarity = "mythic",
|
||||
color_identity = "WGR",
|
||||
oracle_text = Just "Vigilance\n{T}: Create an 8/8 Beast creature token that's red, green, and white.",
|
||||
prices =
|
||||
Prices
|
||||
{ usd = Just "7.22",
|
||||
usd_foil = Just "16.56",
|
||||
eur = Just "4.00",
|
||||
eur_foil = Just "13.95",
|
||||
tix = Just "0.02"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
searchCards :: FilePath -> Maybe T.Text -> Handler [Card]
|
||||
searchCards dbFile q =
|
||||
searchCards :: FilePath -> Maybe T.Text -> Maybe T.Text -> Maybe T.Text -> Handler [Card]
|
||||
searchCards dbFile q sortBy inCollection =
|
||||
liftIO results
|
||||
where
|
||||
search = Search.parse $ fromMaybe "" q
|
||||
|
||||
defaultOrderings :: [Query]
|
||||
defaultOrderings =
|
||||
[ "rarities.rarity_ord DESC",
|
||||
"length(cards.color_identity) DESC",
|
||||
[sql|CASE WHEN length(cards.color_identity) > 0 THEN '0'
|
||||
ELSE cards.color_identity END ASC|],
|
||||
"cards.name ASC"
|
||||
]
|
||||
|
||||
orderSql :: Query
|
||||
orderSql =
|
||||
"ORDER BY " <> case sortBy of
|
||||
Just "price" -> sqlJoin "," $ "CAST(COALESCE(card_prices.usd, card_prices.usd_foil) as decimal) DESC" : defaultOrderings
|
||||
_ -> sqlJoin "," defaultOrderings
|
||||
|
||||
joins :: [Query]
|
||||
joins =
|
||||
[ [sql|JOIN card_prices ON (cards.scryfall_id = card_prices.scryfall_id)
|
||||
AND card_prices.date = (SELECT value FROM vars WHERE key = :last_update_key)|],
|
||||
[sql|JOIN rarities ON (cards.rarity = rarities.rarity)|]
|
||||
]
|
||||
|
||||
joinSql :: Query
|
||||
joinSql = case inCollection of
|
||||
Just "no" -> sqlJoin "" joins
|
||||
_ -> sqlJoin "JOIN copies ON (cards.scryfall_id = copies.scryfall_id)" $ joins
|
||||
|
||||
results :: IO [Card]
|
||||
results = withConnection dbFile $ \conn ->
|
||||
queryNamed
|
||||
conn
|
||||
"SELECT cards.scryfall_id, \
|
||||
\ cards.name, \
|
||||
\ cards.set_code, \
|
||||
\ cards.collector_number, \
|
||||
\ cards.rarity, \
|
||||
\ cards.color_identity, \
|
||||
\ cards.oracle_text, \
|
||||
\ card_prices.usd, \
|
||||
\ card_prices.usd_foil, \
|
||||
\ card_prices.eur, \
|
||||
\ card_prices.eur_foil, \
|
||||
\ card_prices.tix \
|
||||
\FROM cards \
|
||||
\JOIN card_prices ON (cards.scryfall_id = card_prices.scryfall_id) \
|
||||
\ AND card_prices.date = (SELECT value FROM vars WHERE key = :last_update_key) \
|
||||
\LIMIT 5"
|
||||
( sqlJoin
|
||||
" "
|
||||
[ [sql|
|
||||
SELECT cards.scryfall_id,
|
||||
cards.name,
|
||||
cards.set_code,
|
||||
cards.collector_number,
|
||||
cards.rarity,
|
||||
cards.color_identity,
|
||||
cards.oracle_text,
|
||||
card_prices.usd,
|
||||
card_prices.usd_foil,
|
||||
card_prices.eur,
|
||||
card_prices.eur_foil,
|
||||
card_prices.tix
|
||||
FROM cards|],
|
||||
joinSql,
|
||||
orderSql,
|
||||
"LIMIT 10"
|
||||
]
|
||||
)
|
||||
[":last_update_key" := ("last_update" :: T.Text)]
|
||||
|
||||
sqlJoin :: Query -> [Query] -> Query
|
||||
sqlJoin joiner snippets =
|
||||
mconcat $ L.intercalate [joiner] [[s] | s <- snippets]
|
||||
|
|
|
@ -12,60 +12,7 @@ main :: IO ()
|
|||
main = hspec spec
|
||||
|
||||
spec :: Spec
|
||||
spec = with (return app) $ do
|
||||
spec = with (return $ app "tutor.db") $ do
|
||||
describe "GET /search" $ do
|
||||
it "responds with 200" $ do
|
||||
get "/search" `shouldRespondWith` 200
|
||||
it "responds with [Card]" $ do
|
||||
let cards =
|
||||
[json|[
|
||||
{
|
||||
"scryfall_id": "f6cd7465-9dd0-473c-ac5e-dd9e2f22f5f6",
|
||||
"name": "Esika, God of the Tree // The Prismatic Bridge",
|
||||
"set_code": "KHM",
|
||||
"collector_number": "168",
|
||||
"rarity": "mythic",
|
||||
"color_identity": "WUBGR",
|
||||
"oracle_text": null,
|
||||
"prices": {
|
||||
"usd": "9.07",
|
||||
"usd_foil": "10.77",
|
||||
"eur": "7.79",
|
||||
"eur_foil": "12.27",
|
||||
"tix": "1.08"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scryfall_id": "d761ff73-0717-4ee4-996b-f5547bcf9b2f",
|
||||
"name": "Go-Shintai of Life's Origin",
|
||||
"set_code": "NEC",
|
||||
"collector_number": "66",
|
||||
"rarity": "mythic",
|
||||
"color_identity": "WUBGR",
|
||||
"oracle_text": "{W}{U}{B}{R}{G}, {T}: Return target enchantment card from your graveyard to the battlefield.\nWhenever Go-Shintai of Life's Origin or another nontoken Shrine enters the battlefield under your control, create a 1/1 colorless Shrine enchantment creature token.",
|
||||
"prices": {
|
||||
"usd": "23.28",
|
||||
"usd_foil": null,
|
||||
"eur": "23.05",
|
||||
"eur_foil": null,
|
||||
"tix": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"scryfall_id": "e2539ff7-2b7d-47e3-bd77-3138a6c42d2b",
|
||||
"name": "Godsire",
|
||||
"set_code": "ALA",
|
||||
"collector_number": "170",
|
||||
"rarity": "mythic",
|
||||
"color_identity": "WGR",
|
||||
"oracle_text": "Vigilance\n{T}: Create an 8/8 Beast creature token that's red, green, and white.",
|
||||
"prices": {
|
||||
"usd": "7.22",
|
||||
"usd_foil": "16.56",
|
||||
"eur": "4.00",
|
||||
"eur_foil": "13.95",
|
||||
"tix": "0.02"
|
||||
}
|
||||
}
|
||||
]|]
|
||||
get "/search" `shouldRespondWith` cards
|
||||
|
|
Loading…
Reference in a new issue