@@ -37,13 +37,15 @@ import Control.Monad.Trans
37
37
(liftIO )
38
38
import Control.Monad.Trans.Resource
39
39
(runResourceT )
40
+ import Data.Bifunctor (first )
40
41
import qualified Data.ByteString as B
41
42
import qualified Data.ByteString.Builder as BB
42
43
import qualified Data.ByteString.Char8 as BC8
43
44
import qualified Data.ByteString.Lazy as BL
44
45
import Data.Constraint (Constraint , Dict (.. ))
45
46
import Data.Either
46
47
(partitionEithers )
48
+ import qualified Data.Map.Strict as Map
47
49
import Data.Maybe
48
50
(fromMaybe , isNothing , mapMaybe , maybeToList )
49
51
import Data.String
@@ -70,7 +72,7 @@ import Prelude ()
70
72
import Prelude.Compat
71
73
import Servant.API
72
74
((:<|>) (.. ), (:>) , Accept (.. ), BasicAuth , Capture' ,
73
- CaptureAll , Description , EmptyAPI , Fragment ,
75
+ CaptureAll , DeepQuery , Description , EmptyAPI , Fragment ,
74
76
FramingRender (.. ), FramingUnrender (.. ), FromSourceIO (.. ),
75
77
Header' , If , IsSecure (.. ), NoContentVerb , QueryFlag ,
76
78
QueryParam' , QueryParams , QueryString , Raw , ReflectMethod (reflectMethod ),
@@ -610,6 +612,89 @@ instance
610
612
route Proxy context subserver =
611
613
route (Proxy :: Proxy api ) context (passToServer subserver queryString)
612
614
615
+ -- | If you use @'DeepQuery' "symbol" a@ in one of the endpoints for your API,
616
+ -- this automatically requires your server-side handler to be a function
617
+ -- that takes an argument of type @a@.
618
+ --
619
+ -- This lets you extract an object from multiple parameters in the query string,
620
+ -- with its fields enclosed in brackets: `/books?filter[author][name]=value`. When
621
+ -- all the fields are known in advance, it can be done with @'QueryParam'@ (it can
622
+ -- still be tedious if you the object has many fields). When some fields are dynamic,
623
+ -- it cannot be done with @'QueryParam'.
624
+ --
625
+ -- The way the object is constructed from the extracted fields can be controlled by
626
+ -- providing an instance on @'FromDeepQuery'@
627
+ --
628
+ -- Example:
629
+ --
630
+ -- > type MyApi = "books" :> DeepQuery "filter" BookQuery :> Get '[JSON] [Book]
631
+ -- >
632
+ -- > server :: Server MyApi
633
+ -- > server = getBooksBy
634
+ -- > where getBooksBy :: BookQuery -> Handler [Book]
635
+ -- > getBooksBy query = ...filter books based on the dynamic filters provided...
636
+ instance
637
+ ( KnownSymbol sym , FromDeepQuery a , HasServer api context
638
+ , HasContextEntry (MkContextWithErrorFormatter context ) ErrorFormatters
639
+ )
640
+ => HasServer (DeepQuery sym a :> api ) context where
641
+ ------
642
+ type ServerT (DeepQuery sym a :> api ) m =
643
+ a -> ServerT api m
644
+
645
+ hoistServerWithContext _ pc nt s = hoistServerWithContext (Proxy :: Proxy api ) pc nt . s
646
+
647
+ route Proxy context subserver = route (Proxy :: Proxy api ) context $
648
+ subserver `addParameterCheck` withRequest paramsCheck
649
+ where
650
+ rep = typeRep (Proxy :: Proxy DeepQuery )
651
+ formatError = urlParseErrorFormatter $ getContextEntry (mkContextWithErrorFormatter context)
652
+
653
+ paramname = cs $ symbolVal (Proxy :: Proxy sym )
654
+ paramsCheck req =
655
+ let relevantParams :: [(T. Text , Maybe T. Text )]
656
+ relevantParams = filter (isRelevantParam . fst )
657
+ . queryToQueryText
658
+ . queryString
659
+ $ req
660
+ isRelevantParam name = name == paramname || (paramname <> " [" ) `T.isPrefixOf` name
661
+ in case fromDeepQuery =<< traverse parseDeepParam relevantParams of
662
+ Left e -> delayedFailFatal $ formatError rep req
663
+ $ cs $ " Error parsing deep query parameter(s) "
664
+ <> paramname <> T. pack " failed: "
665
+ <> T. pack e
666
+ Right parsed -> return parsed
667
+
668
+ parseDeepParam :: (T. Text , Maybe T. Text ) -> Either String ([T. Text ], Maybe T. Text )
669
+ parseDeepParam (paramname, value) =
670
+ let parseParam " " = return []
671
+ parseParam n = reverse <$> go [] n
672
+ go parsed remaining = case T. take 1 remaining of
673
+ " [" -> case T. breakOn " ]" remaining of
674
+ (_ , " " ) -> Left $ " Error parsing deep param, missing closing ']': " <> T. unpack remaining
675
+ (name, " ]" ) -> return $ T. drop 1 name : parsed
676
+ (name, remaining') -> case T. take 2 remaining' of
677
+ " ][" -> go (T. drop 1 name : parsed) (T. drop 1 remaining')
678
+ _ -> Left $ " Error parsing deep param, incorrect brackets: " <> T. unpack remaining
679
+ _ -> Left $ " Error parsing deep param, missing opening '[': " <> T. unpack remaining
680
+ in (, value) <$> parseParam paramname
681
+
682
+ -- | Extract a deep object from (possibly nested) query parameters.
683
+ -- a param like @filter[a][b][c]=d@ will be represented as
684
+ -- @'(["a", "b", "c"], Just "d")'@. Note that a parameter with no
685
+ -- nested field is possible: @filter=a@ will be represented as
686
+ -- @'([], Just "a")'@
687
+ class FromDeepQuery a where
688
+ fromDeepQuery :: [([T. Text ], Maybe T. Text )] -> Either String a
689
+
690
+ instance FromHttpApiData a => FromDeepQuery (Map. Map T. Text a ) where
691
+ fromDeepQuery params =
692
+ let parseParam ([k], Just rawV) = (k,) <$> first T. unpack (parseQueryParam rawV)
693
+ parseParam (_, Nothing ) = Left " Empty map value"
694
+ parseParam ([] , _) = Left " Empty map parameter"
695
+ parseParam (_ , Just _) = Left " Nested map values"
696
+ in Map. fromList <$> traverse parseParam params
697
+
613
698
-- | Just pass the request to the underlying application and serve its response.
614
699
--
615
700
-- Example:
0 commit comments