@@ -40,6 +40,7 @@ import Control.Monad.Trans
40
40
import Control.Monad.Trans.Resource
41
41
(runResourceT , ReleaseKey )
42
42
import Data.Acquire
43
+ import Data.Bifunctor (first )
43
44
import qualified Data.ByteString as B
44
45
import qualified Data.ByteString.Builder as BB
45
46
import qualified Data.ByteString.Char8 as BC8
@@ -49,6 +50,7 @@ import Data.Either
49
50
(partitionEithers )
50
51
import Data.Kind
51
52
(Type )
53
+ import qualified Data.Map.Strict as Map
52
54
import Data.Maybe
53
55
(fromMaybe , isNothing , mapMaybe , maybeToList )
54
56
import Data.String
@@ -75,10 +77,10 @@ import Prelude ()
75
77
import Prelude.Compat
76
78
import Servant.API
77
79
((:<|>) (.. ), (:>) , Accept (.. ), BasicAuth , Capture' ,
78
- CaptureAll , Description , EmptyAPI , Fragment ,
80
+ CaptureAll , DeepQuery , Description , EmptyAPI , Fragment ,
79
81
FramingRender (.. ), FramingUnrender (.. ), FromSourceIO (.. ),
80
82
Header' , If , IsSecure (.. ), NoContentVerb , QueryFlag ,
81
- QueryParam' , QueryParams , Raw , RawM , ReflectMethod (reflectMethod ),
83
+ QueryParam' , QueryParams , QueryString , Raw , RawM , ReflectMethod (reflectMethod ),
82
84
RemoteHost , ReqBody' , SBool (.. ), SBoolI (.. ), SourceIO ,
83
85
Stream , StreamBody' , Summary , ToSourceIO (.. ), Vault , Verb ,
84
86
WithNamedContext , WithResource , NamedRoutes )
@@ -90,6 +92,7 @@ import Servant.API.ContentTypes
90
92
import Servant.API.Modifiers
91
93
(FoldLenient , FoldRequired , RequestArgument ,
92
94
unfoldRequestArgument )
95
+ import Servant.API.QueryString (FromDeepQuery (.. ))
93
96
import Servant.API.ResponseHeaders
94
97
(GetHeaders , Headers , getHeaders , getResponse )
95
98
import Servant.API.Status
@@ -627,6 +630,105 @@ instance (KnownSymbol sym, HasServer api context)
627
630
examine v | v == " true" || v == " 1" || v == " " = True
628
631
| otherwise = False
629
632
633
+ -- | If you use @'QueryString'@ in one of the endpoints for your API,
634
+ -- this automatically requires your server-side handler to be a function
635
+ -- that takes an argument of type @Query@ (@[('ByteString', 'Maybe' 'ByteString')]@).
636
+ --
637
+ -- This lets you extract the whole query string. This is useful when the query string
638
+ -- can contain parameters with dynamic names, that you can't access with @'QueryParam'@.
639
+ --
640
+ -- Example:
641
+ --
642
+ -- > type MyApi = "books" :> QueryString :> Get '[JSON] [Book]
643
+ -- >
644
+ -- > server :: Server MyApi
645
+ -- > server = getBooksBy
646
+ -- > where getBooksBy :: Query -> Handler [Book]
647
+ -- > getBooksBy filters = ...filter books based on the dynamic filters provided...
648
+ instance
649
+ ( HasServer api context
650
+ )
651
+ => HasServer (QueryString :> api ) context where
652
+ ------
653
+ type ServerT (QueryString :> api ) m =
654
+ Query -> ServerT api m
655
+
656
+ hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api ) pc nt . s
657
+
658
+ route Proxy context subserver =
659
+ route (Proxy :: Proxy api ) context (passToServer subserver queryString)
660
+
661
+ -- | If you use @'DeepQuery' "symbol" a@ in one of the endpoints for your API,
662
+ -- this automatically requires your server-side handler to be a function
663
+ -- that takes an argument of type @a@.
664
+ --
665
+ -- This lets you extract an object from multiple parameters in the query string,
666
+ -- with its fields enclosed in brackets: `/books?filter[author][name]=value`. When
667
+ -- all the fields are known in advance, it can be done with @'QueryParam'@ (it can
668
+ -- still be tedious if you the object has many fields). When some fields are dynamic,
669
+ -- it cannot be done with @'QueryParam'.
670
+ --
671
+ -- The way the object is constructed from the extracted fields can be controlled by
672
+ -- providing an instance on @'FromDeepQuery'@
673
+ --
674
+ -- Example:
675
+ --
676
+ -- > type MyApi = "books" :> DeepQuery "filter" BookQuery :> Get '[JSON] [Book]
677
+ -- >
678
+ -- > server :: Server MyApi
679
+ -- > server = getBooksBy
680
+ -- > where getBooksBy :: BookQuery -> Handler [Book]
681
+ -- > getBooksBy query = ...filter books based on the dynamic filters provided...
682
+ instance
683
+ ( KnownSymbol sym , FromDeepQuery a , HasServer api context
684
+ , HasContextEntry (MkContextWithErrorFormatter context ) ErrorFormatters
685
+ )
686
+ => HasServer (DeepQuery sym a :> api ) context where
687
+ ------
688
+ type ServerT (DeepQuery sym a :> api ) m =
689
+ a -> ServerT api m
690
+
691
+ hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api ) pc nt . s
692
+
693
+ route Proxy context subserver = route (Proxy :: Proxy api ) context $
694
+ subserver `addParameterCheck` withRequest paramsCheck
695
+ where
696
+ rep = typeRep (Proxy :: Proxy DeepQuery )
697
+ formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
698
+
699
+ paramname = cs $ symbolVal (Proxy :: Proxy sym )
700
+ paramsCheck req =
701
+ let relevantParams :: [(T. Text , Maybe T. Text )]
702
+ relevantParams = mapMaybe isRelevantParam
703
+ . queryToQueryText
704
+ . queryString
705
+ $ req
706
+ isRelevantParam (name, value) = (, value) <$>
707
+ case T. stripPrefix paramname name of
708
+ Just " " -> Just " "
709
+ Just x | " [" `T.isPrefixOf` x -> Just x
710
+ _ -> Nothing
711
+ in case fromDeepQuery =<< traverse parseDeepParam relevantParams of
712
+ Left e -> delayedFailFatal $ formatError rep req
713
+ $ cs $ " Error parsing deep query parameter(s) "
714
+ <> paramname <> T. pack " failed: "
715
+ <> T. pack e
716
+ Right parsed -> return parsed
717
+
718
+ parseDeepParam :: (T. Text , Maybe T. Text ) -> Either String ([T. Text ], Maybe T. Text )
719
+ parseDeepParam (paramname, value) =
720
+ let parseParam " " = return []
721
+ parseParam n = reverse <$> go [] n
722
+ go parsed remaining = case T. take 1 remaining of
723
+ " [" -> case T. breakOn " ]" remaining of
724
+ (_ , " " ) -> Left $ " Error parsing deep param, missing closing ']': " <> T. unpack remaining
725
+ (name, " ]" ) -> return $ T. drop 1 name : parsed
726
+ (name, remaining') -> case T. take 2 remaining' of
727
+ " ][" -> go (T. drop 1 name : parsed) (T. drop 1 remaining')
728
+ _ -> Left $ " Error parsing deep param, incorrect brackets: " <> T. unpack remaining
729
+ _ -> Left $ " Error parsing deep param, missing opening '[': " <> T. unpack remaining
730
+ in (, value) <$> parseParam paramname
731
+
630
732
-- | Just pass the request to the underlying application and serve its response.
631
733
--
632
734
-- Example:
0 commit comments