{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} module Servant.Kotlin.Internal.Generate ( GenerateKotlin (..) , generateKotlinForDefDataClass , generateKotlinForDefDataClass' , defKotlinImports , generateKotlinForAPIClass , generateKotlinForAPI , generateKotlinForAPIWith , KotlinOptions (..) , defKotlinOptions , UrlPrefix (..) ) where import Control.Lens (to, (^.)) import Data.List (nub) import Data.Maybe (catMaybes, fromMaybe) import Data.Monoid ((<>)) import Data.Proxy (Proxy) import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Text.Lazy as L import Servant.API (NoContent (..)) import qualified Servant.Foreign as F import Servant.Kotlin.Internal.Foreign (LangKotlin, getEndpoints) import Servant.Kotlin.Type import Text.PrettyPrint.Leijen.Text (Doc, (<+>)) import qualified Text.PrettyPrint.Leijen.Text as PP class GenerateKotlin a where generateKotlin :: a -> [Text] generateKotlin' :: a -> Text generateKotlin' = T.concat . generateKotlin instance GenerateKotlin KotlinClass where generateKotlin (PrimitiveClass c) = generateKotlin c generateKotlin (ExClass c) = generateKotlin c generateKotlin (DataClass (KotlinDataClass name _)) = [name] instance GenerateKotlin KotlinPrimitiveClass where generateKotlin KDouble = ["Double"] generateKotlin KFloat = ["Float"] generateKotlin KLong = ["Long"] generateKotlin KInt = ["Int"] generateKotlin KShort = ["Short"] generateKotlin KByte = ["Byte"] generateKotlin KChar = ["Char"] generateKotlin KBoolean = ["Boolean"] generateKotlin (KArray c) = ["Array<" <> generateKotlin' c <> ">"] generateKotlin KString = ["String"] generateKotlin KUnit = ["Unit"] generateKotlin (KNullable c) = [generateKotlin' c <> "?"] generateKotlin KAny = ["Any"] instance GenerateKotlin KotlinExClass where generateKotlin (KList c) = ["List<" <> generateKotlin' c <> ">"] generateKotlin (KHashMap k v) = ["HashMap<" <> generateKotlin' k <> ", " <> generateKotlin' v <> ">"] generateKotlin (KPair a b) = ["Pair<" <> generateKotlin' a <> ", " <> generateKotlin' b <> ">"] generateKotlin KTime = ["Time"] instance GenerateKotlin KotlinDataClass where generateKotlin (KotlinDataClass name fields) = [ "data class " <> name <> "(" <> generateKotlin' fields <> ")" ] instance GenerateKotlin KotlinFields where generateKotlin (Node field) = generateKotlin field generateKotlin (Brunch a b) = [generateKotlin' a <> ", " <> generateKotlin' b] instance GenerateKotlin KotlinField where generateKotlin (KotlinField name c) = ["val " <> name <> ": " <> generateKotlin' c] generateKotlinForDefDataClass' :: KotlinClass -> [Text] generateKotlinForDefDataClass' (DataClass c) = generateKotlin c generateKotlinForDefDataClass' _ = [] generateKotlinForDefDataClass :: (KotlinType a) => Proxy a -> [Text] generateKotlinForDefDataClass = maybe [] generateKotlinForDefDataClass' . toKotlinType --- defKotlinImports :: Text defKotlinImports = docToText . PP.vsep $ fmap ("import" <+>) [ "com.github.kittinunf.fuel.Fuel" , "com.github.kittinunf.fuel.core.FuelError" , "com.github.kittinunf.fuel.core.FuelManager" , "com.github.kittinunf.fuel.core.Request" , "com.github.kittinunf.fuel.core.Response" , "com.github.kittinunf.fuel.gson.responseObject" , "com.github.kittinunf.result.Result" , "com.google.gson.Gson" ] --- generateKotlinForAPIClass :: Text -> [Text] -> [Text] generateKotlinForAPIClass className body = mconcat [ [ docToText $ "class" <+> PP.textStrict className <> "(private val baseURL: String) {" ] , [ docToText $ PP.indent indentNum initialize ] , fmap (docToText . PP.vsep . fmap (PP.indent indentNum . PP.textStrict) . T.lines) body , [ "}" ] ] where initialize = PP.vsep [ "init {", PP.indent indentNum fuelManager, "}" ] fuelManager = PP.vsep [ "FuelManager.instance.apply {" , PP.indent indentNum "basePath = baseURL" , PP.indent indentNum $ "baseHeaders = mapOf(" <> header <> ")" , "}" ] header = PP.hsep . PP.punctuate PP.comma $ fmap (\(k, v) -> PP.dquotes k <+> "to" <+> PP.dquotes v) [("Content-Type", "application/json"), ("Device", "Android")] --- {-| Generate Kotlin code for the API with default options. Returns a list of Kotlin functions to query your Servant API from Kotlin. -} generateKotlinForAPI :: ( F.HasForeign LangKotlin KotlinClass api , F.GenerateList KotlinClass (F.Foreign KotlinClass api)) => Proxy api -> [Text] generateKotlinForAPI = generateKotlinForAPIWith defKotlinOptions {-| Generate Kotlin code for the API with custom options. -} generateKotlinForAPIWith :: ( F.HasForeign LangKotlin KotlinClass api , F.GenerateList KotlinClass (F.Foreign KotlinClass api)) => KotlinOptions -> Proxy api -> [Text] generateKotlinForAPIWith opts = nub . fmap (docToText . generateKotlinForRequest opts) . getEndpoints indentNum :: Int indentNum = 4 {-| Generate an Kotlin function for one endpoint. -} generateKotlinForRequest :: KotlinOptions -> F.Req KotlinClass -> Doc generateKotlinForRequest opts request = funcDef where funcDef = PP.vsep [ "fun" <+> fnName <> "(" <> args <> ") {" , PP.indent indentNum kotlinRequest , "}" ] fnName = request ^. F.reqFuncName . to (T.replace "-" "" . F.camelCase) . to stext args = mkArgs opts request kotlinRequest = mkRequest opts request mkArgs :: KotlinOptions -> F.Req KotlinClass -> Doc mkArgs opts request = (PP.hsep . PP.punctuate PP.comma . concat) [ urlPrefixArg , headerArgs , urlCaptureArgs , queryArgs , requestBodyArg , handlerArg ] where urlPrefixArg :: [Doc] urlPrefixArg = case urlPrefix opts of Dynamic -> ["urlBase: String"] Static _ -> [] headerArgs :: [Doc] headerArgs = [ kotlinHeaderArg header <> ": " <> kotlinHeaderType header | header <- request ^. F.reqHeaders ] urlCaptureArgs :: [Doc] urlCaptureArgs = [ kotlinCaptureArg segment <> ": " <> kotlinCaptureType segment | segment <- request ^. F.reqUrl . F.path, F.isCapture segment ] queryArgs :: [Doc] queryArgs = [ kotlinQueryArg arg <> ": " <> kotlinQueryType arg | arg <- request ^. F.reqUrl . F.queryStr ] requestBodyArg :: [Doc] requestBodyArg = maybe [] (\body -> [kotlinBodyArg <> ": " <> kotlinTypeRef body]) $ request ^. F.reqBody handlerArg :: [Doc] handlerArg = [kotlinHandlerArg <> ": " <> handlerType] where handlerType = "(Request, Response, Result<" <> returnType <> ", FuelError>) -> Unit" returnType :: Doc returnType = kotlinTypeRef . fromMaybe (PrimitiveClass KUnit) $ request ^. F.reqReturnType kotlinHeaderArg :: F.HeaderArg KotlinClass -> Doc kotlinHeaderArg header = "header_" <> header ^. F.headerArg . F.argName . to (stext . T.replace "-" "_" . F.unPathSegment) kotlinHeaderType :: F.HeaderArg KotlinClass -> Doc #if MIN_VERSION_servant_foreign(0,11,0) kotlinHeaderType header = header ^. F.headerArg . F.argType . to kotlinTypeRef #else kotlinHeaderType header = header ^. F.headerArg . F.argType . to (kotlinTypeRef . wrapper) where wrapper = PrimitiveClass . KNullable #endif kotlinCaptureArg :: F.Segment KotlinClass -> Doc kotlinCaptureArg segment = "capture_" <> F.captureArg segment ^. F.argName . to (stext . F.unPathSegment) kotlinCaptureType :: F.Segment KotlinClass -> Doc kotlinCaptureType segment = F.captureArg segment ^. F.argType . to kotlinTypeRef kotlinQueryArg :: F.QueryArg KotlinClass -> Doc kotlinQueryArg arg = "query_" <> arg ^. F.queryArgName . F.argName . to (stext . F.unPathSegment) kotlinQueryType :: F.QueryArg KotlinClass -> Doc #if MIN_VERSION_servant_foreign(0,11,0) kotlinQueryType arg = arg ^. F.queryArgName . F.argType . to kotlinTypeRef #else kotlinQueryType arg = arg ^. F.queryArgName . F.argType . to (kotlinTypeRef . wrapper) where wrapper = case arg ^. F.queryArgType of F.Normal -> PrimitiveClass . KNullable _ -> id #endif kotlinBodyArg :: Doc kotlinBodyArg = "body" kotlinHandlerArg :: Doc kotlinHandlerArg = "handler" kotlinTypeRef :: KotlinClass -> Doc kotlinTypeRef = stext . generateKotlin' mkRequest :: KotlinOptions -> F.Req KotlinClass -> Doc mkRequest opts request = "Fuel" <> PP.align (PP.vsep methodChain) where methodChain = catMaybes [ Just $ mconcat [".", method, "(", url, ")"] , body , Just ".responseObject(handler)" ] method = request ^. F.reqMethod . to (stext . T.toLower . T.decodeUtf8) url = mkUrl opts (request ^. F.reqUrl . F.path) <> mkQueryParams request body = fmap (\b -> mconcat [ ".body(Gson().toJson(" , kotlinBodyArg , ", " , kotlinTypeRef b , "::class.java))" ] ) $ request ^. F.reqBody mkUrl :: KotlinOptions -> [F.Segment KotlinClass] -> Doc mkUrl _opts segments = mconcat . PP.punctuate " + " $ PP.dquotes "/" : PP.punctuate (" + " <> PP.dquotes "/") (map segmentToDoc segments) where segmentToDoc :: F.Segment KotlinClass -> Doc segmentToDoc segment = case F.unSegment segment of F.Static path -> PP.dquotes (stext (F.unPathSegment path)) F.Cap _arg -> kotlinCaptureArg segment -- TODO: implements mkQueryParams :: F.Req KotlinClass -> Doc mkQueryParams _request = "" -- if null (request ^. F.reqUrl . F.queryStr) then -- empty -- else -- " +" <+> dquotes "?" <+> "+" <+> -- "params.joinToString(" <> dquotes "&" <> ")" {-| Options to configure how code is generated. -} data KotlinOptions = KotlinOptions { urlPrefix :: UrlPrefix , emptyResponseKotlinTypes :: [KotlinClass] -- ^ Types that represent an empty Http response. , stringKotlinTypes :: [KotlinClass] -- ^ Types that represent a String. } data UrlPrefix = Static Text | Dynamic {-| Default options for generating Kotlin code. The default options are: > { urlPrefix = Static "" > , emptyResponseKotlinTypes = [ toKotlinType NoContent ] > , stringKotlinTypes = [ toKotlinType "" ] > } -} defKotlinOptions :: KotlinOptions defKotlinOptions = KotlinOptions { urlPrefix = Static "" , emptyResponseKotlinTypes = [ toKotlinType' NoContent , toKotlinType' () ] , stringKotlinTypes = [ toKotlinType' ("" :: String) , toKotlinType' ("" :: Text) ] } --- docToText :: Doc -> Text docToText = L.toStrict . PP.displayT . PP.renderPretty 0.4 100 stext :: Text -> Doc stext = PP.text . L.fromStrict