{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Servant.PY.Internal
( PythonGenerator
, ReturnStyle(..)
, PythonRequest(..)
, PyRequestArgs(..)
, CommonGeneratorOptions(..)
, defCommonGeneratorOptions
, defaultPyIndent
, indent
, Indent
, indenter
, makePyUrl
, makePyUrl'
, segmentToStr
, capturesToFormatArgs
, toValidFunctionName
, functionName
, toPyHeader
, retrieveHeaders
, getHeaderDict
, retrieveHeaderText
, toPyDict
, toPyParams
, getParams
, paramNames
, captures
, getMethod
, hasBody
, withFormattedCaptures
, buildDocString
, buildHeaderDict
, functionArguments
, formatBuilder
, remainingReqCall
) where
import Control.Lens hiding (List)
import qualified Data.CharSet as Set
import qualified Data.CharSet.Unicode.Category as Set
import Data.Data
import Data.Maybe (isJust)
import Data.Monoid ( (<>) )
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import GHC.TypeLits
import Servant.Foreign
type PythonGenerator = [PythonRequest] -> Text
data PythonRequest = TypedPythonRequest (Req Text)
| UnTypedPythonRequest (Req NoContent)
deriving (Eq, Show)
type Indent = (" " :: Symbol)
indent :: Proxy Indent
indent = Proxy
defaultPyIndent :: Proxy Indent -> Text
defaultPyIndent = indenter 4
indenter :: Int -> Proxy Indent -> Text
indenter width space = mconcat $ width `replicate` (T.pack . symbolVal) space
{-# INLINE indenter #-}
data ReturnStyle = DangerMode
| RawResponse
data PyRequestArgs = PyRequestArgs {
hasHeaders :: Bool
, hasParams :: Bool
, hasData :: Bool
} deriving (Show)
data CommonGeneratorOptions = CommonGeneratorOptions
{
functionNameBuilder :: FunctionName -> Text
, requestBody :: Text
, urlPrefix :: Text
, indentation :: Proxy Indent -> Text
, returnMode :: ReturnStyle
}
defCommonGeneratorOptions :: CommonGeneratorOptions
defCommonGeneratorOptions = CommonGeneratorOptions
{
functionNameBuilder = snakeCase
, requestBody = "data"
, urlPrefix = "http://localhost:8000"
, indentation = defaultPyIndent
, returnMode = DangerMode
}
toValidFunctionName :: Text -> Text
toValidFunctionName t =
case T.uncons t of
Just (x,xs) ->
setFirstChar x `T.cons` T.filter remainder xs
Nothing -> "_"
where
setFirstChar c = if Set.member c firstLetterOK then c else '_'
remainder c = Set.member c remainderOK
firstLetterOK = filterBmpChars $ mconcat
[ Set.fromDistinctAscList "_"
, Set.lowercaseLetter
, Set.uppercaseLetter
, Set.titlecaseLetter
, Set.modifierLetter
, Set.otherLetter
, Set.letterNumber
]
remainderOK = firstLetterOK
<> filterBmpChars (mconcat
[ Set.nonSpacingMark
, Set.spacingCombiningMark
, Set.decimalNumber
, Set.connectorPunctuation
])
functionName :: CommonGeneratorOptions -> PythonRequest -> Text
functionName opts (TypedPythonRequest req) = toValidFunctionName (functionNameBuilder opts $ req ^. reqFuncName)
functionName opts (UnTypedPythonRequest req) = toValidFunctionName (functionNameBuilder opts $ req ^. reqFuncName)
filterBmpChars :: Set.CharSet -> Set.CharSet
filterBmpChars = Set.filter (< '\65536')
toPyDict :: Text -> [Text] -> Text
toPyDict offset dict
| null dict = "{}"
| otherwise = "{" <> T.intercalate (",\n" <> offset) insides <> "}"
where insides = combiner <$> dict
combiner a = "\"" <> a <> "\": " <> a
getParams :: Text -> PythonRequest -> Text
getParams offset (TypedPythonRequest req) = toPyParams offset $ req ^.. reqUrl.queryStr.traverse
getParams offset (UnTypedPythonRequest req) = toPyParams offset $ req ^.. reqUrl.queryStr.traverse
toPyParams :: Text -> [QueryArg f] -> Text
toPyParams _ [] = ""
toPyParams offset qargs = toPyDict offset paramList
where paramList = fmap (\qarg -> qarg ^. queryArgName.argName._PathSegment) qargs
paramNames :: PythonRequest -> [Text]
paramNames (TypedPythonRequest req) = map (view $ queryArgName . argPath) $ req ^.. reqUrl.queryStr.traverse
paramNames (UnTypedPythonRequest req) = map (view $ queryArgName . argPath) $ req ^.. reqUrl.queryStr.traverse
toPyHeader :: HeaderArg f -> Text
toPyHeader (HeaderArg n)
= toValidFunctionName ("header" <> n ^. argName . _PathSegment)
toPyHeader (ReplaceHeaderArg n p)
| pn `T.isPrefixOf` p = pv <> " + \"" <> rp <> "\""
| pn `T.isSuffixOf` p = "\"" <> rp <> "\" + " <> pv
| pn `T.isInfixOf` p = "\"" <> T.replace pn ("\" + " <> pv <> " + \"") p
<> "\""
| otherwise = p
where
pv = toValidFunctionName ("header" <> n ^. argName . _PathSegment)
pn = "{" <> n ^. argName . _PathSegment <> "}"
rp = T.replace pn "" p
buildHeaderDict :: [HeaderArg f] -> Text
buildHeaderDict [] = ""
buildHeaderDict hs = "{" <> headers <> "}"
where headers = T.intercalate ", " $ map headerStr hs
headerStr h = "\"" <> h ^. headerArg . argPath <> "\": "
<> toPyHeader h
getHeaderDict :: PythonRequest -> Text
getHeaderDict (TypedPythonRequest req) = buildHeaderDict $ req ^. reqHeaders
getHeaderDict (UnTypedPythonRequest req) = buildHeaderDict $ req ^. reqHeaders
retrieveHeaders :: PythonRequest -> [Text]
retrieveHeaders (TypedPythonRequest req) = retrieveHeaderText <$> req ^. reqHeaders
retrieveHeaders (UnTypedPythonRequest req) = retrieveHeaderText <$> req ^. reqHeaders
retrieveHeaderText :: forall f. HeaderArg f -> Text
retrieveHeaderText h = h ^. headerArg . argPath
functionArguments :: forall f. Req f -> Text
functionArguments req =
mconcat [ T.intercalate ", " args]
where
args = captures' req ++ qparam ++ body ++ headers
qparam = map ((<>) "param_" . view (queryArgName . argPath)) queryParams
body = if isJust $ req ^. reqBody
then ["data"]
else []
queryParams = req ^.. reqUrl . queryStr . traverse
headers = map ((<>) "header_"
. view (headerArg . argPath)
) $ req ^. reqHeaders
captures :: PythonRequest -> [Text]
captures (TypedPythonRequest req) = captures' req
captures (UnTypedPythonRequest req) = captures' req
captures' :: forall f. Req f -> [Text]
captures' req = map (view argPath . captureArg)
. filter isCapture
$ req ^. reqUrl.path
makePyUrl :: CommonGeneratorOptions -> PythonRequest -> Text -> Text
makePyUrl opts (TypedPythonRequest req) offset = makePyUrl' opts req offset
makePyUrl opts (UnTypedPythonRequest req) offset = makePyUrl' opts req offset
makePyUrl' :: forall f. CommonGeneratorOptions -> Req f -> Text -> Text
makePyUrl' opts req offset = "\"" <> url <> "\""
where url = urlPrefix opts <> "/" <> getSegments pathParts
<> withFormattedCaptures offset pathParts
pathParts = req ^.. reqUrl.path.traverse
getSegments :: forall f. [Segment f] -> Text
getSegments segments = if null segments
then ""
else T.intercalate "/" (map segmentToStr segments) <> "\""
withFormattedCaptures :: Text -> [Segment f] -> Text
withFormattedCaptures offset segments = formattedCaptures (capturesToFormatArgs segments)
where formattedCaptures [] = ""
formattedCaptures xs = ".format(\n" <> offset
<> T.intercalate (",\n" <> offset) (map formatBuilder xs)
<> ")"
formatBuilder :: Text -> Text
formatBuilder val = val <> "=parse.quote(str("<> val <> "))"
segmentToStr :: Segment f -> Text
segmentToStr (Segment (Static s)) = s ^. _PathSegment
segmentToStr (Segment (Cap s)) = "{" <> s ^. argName . _PathSegment <> "}"
capturesToFormatArgs :: [Segment f] -> [Text]
capturesToFormatArgs segments = map getSegment $ filter isCapture segments
where getSegment (Segment (Cap a)) = getCapture a
getSegment _ = ""
getCapture s = s ^. argName . _PathSegment
captureArgsWithTypes :: [Segment Text] -> [Text]
captureArgsWithTypes segments = map getSegmentArgType (filter isCapture segments)
where getSegmentArgType (Segment (Cap a)) = pathPart a <> " (" <> a ^. argType <> ")"
getSegmentArgType _ = ""
pathPart s = s ^. argName . _PathSegment
buildDocString :: PythonRequest -> CommonGeneratorOptions -> Text -> Text
buildDocString (TypedPythonRequest req) opts returnVal = buildDocString' req opts args returnVal
where args = captureArgsWithTypes $ req ^.. reqUrl.path.traverse
buildDocString (UnTypedPythonRequest req) opts returnVal = buildDocString' req opts args returnVal
where args = capturesToFormatArgs $ req ^.. reqUrl.path.traverse
buildDocString' :: forall f. Req f -> CommonGeneratorOptions -> [Text] -> Text -> Text
buildDocString' req opts args returnVal = T.toUpper method <> " \"" <> url <> "\n"
<> includeArgs <> "\n\n"
<> indent' <> "Returns:\n"
<> indent' <> indent' <> returnVal
where method = decodeUtf8 $ req ^. reqMethod
url = getSegments $ req ^.. reqUrl.path.traverse
includeArgs = if null args then "" else argDocs
argDocs = indent' <> "Args:\n"
<> indent' <> indent' <> T.intercalate ("\n" <> indent' <> indent') args
indent' = indentation opts indent
getMethod :: PythonRequest -> Text
getMethod (TypedPythonRequest req) = decodeUtf8 $ req ^. reqMethod
getMethod (UnTypedPythonRequest req) = decodeUtf8 $ req ^. reqMethod
hasBody :: PythonRequest -> Bool
hasBody (TypedPythonRequest req) = isJust (req ^. reqBody)
hasBody (UnTypedPythonRequest req) = isJust (req ^. reqBody)
remainingReqCall :: PyRequestArgs -> Int -> Text
remainingReqCall reqArgs width
| null argsAsList = ")"
| length argsAsList == 1 = ",\n" <> offset <> head argsAsList <> ")\n"
| otherwise = ",\n" <> offset <> T.intercalate (",\n" <> offset) argsAsList <> ")\n"
where argsAsList = requestArgsToList reqArgs
offset = mconcat $ replicate width " "
requestArgsToList :: PyRequestArgs -> [Text]
requestArgsToList reqArgs = map snd . filter fst $ zip bools strings
where bools = [hasHeaders reqArgs, hasParams reqArgs, hasData reqArgs]
strings = ["headers=headers", "params=params", "json=data"]