packages feed

servant-kotlin (empty) → 0.1.0.0

raw patch · 13 files changed

+1029/−0 lines, 13 filesdep +aesondep +basedep +containerssetup-changed

Dependencies added: aeson, base, containers, directory, formatting, hspec, http-api-data, lens, servant, servant-foreign, servant-kotlin, shelly, text, time, wl-pprint-text

Files

+ LICENSE view
@@ -0,0 +1,21 @@+MIT License
+
+Copyright (c) 2017 MATSUBARA Nobutada
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+ README.md view
@@ -0,0 +1,28 @@+# Servant Kotlin
+
+[![Hackage](https://img.shields.io/hackage/v/servant-kotlin.svg?style=flat)](https://hackage.haskell.org/package/servant-kotlin)
+[![Build Status](https://travis-ci.org/matsubara0507/servant-kotlin.svg?branch=master)](https://travis-ci.org/matsubara0507/servant-kotlin)
+[![Stackage LTS](http://stackage.org/package/servant-kotlin/badge/lts)](http://stackage.org/lts/package/servant-kotlin)
+[![Stackage Nightly](http://stackage.org/package/servant-kotlin/badge/nightly)](http://stackage.org/nightly/package/servant-kotlin)
+
+Generate Kotlin class to query your Servant API!
+
+## Example
+
+see [`example/Generater.hs`](/example/Generater.hs).
+
+this file generate [`TodoAPI.kt`](/example/com/github/matsubara0507/TodoAPI.kt).
+
+## dependencies for Kotlin
+
+- [kittinunf/Fuel](https://github.com/kittinunf/Fuel): The easiest HTTP networking library for Kotlin/Android
+- [google/gson](https://github.com/google/gson): A Java serialization/deserialization library to convert Java Objects into JSON and back
+
+## not yet, no Implements
+
+- [ ] Original QueryFlag
+- [ ] Original Header
+
+## Acknowledgement
+
+This package is greatly inspired by [`elm-export`](https://hackage.haskell.org/package/elm-export) and [`servant-elm`](https://hackage.haskell.org/package/servant-elm).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ example/Generater.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE DataKinds         #-}
+{-# LANGUAGE DeriveAnyClass    #-}
+{-# LANGUAGE DeriveGeneric     #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeOperators     #-}
+
+module Main where
+
+import           Data.Aeson                  (FromJSON, ToJSON)
+import           Data.Proxy                  (Proxy (Proxy))
+import           Data.Text                   (Text)
+import           GHC.Generics                (Generic)
+import           Servant.API                 ((:<|>) (..), (:>), Capture,
+                                              Delete, FormUrlEncoded, Get, JSON,
+                                              Post, Put, ReqBody)
+import           Servant.Kotlin
+import           Shelly                      (cd, run_, shelly, which)
+import           Web.Internal.FormUrlEncoded (FromForm)
+
+data Todo = Todo
+  { todoId :: Int
+  , title  :: Text
+  , done   :: Bool
+  } deriving (Generic, Show, Eq, KotlinType)
+
+instance FromJSON Todo
+instance ToJSON Todo
+instance FromForm Todo
+
+type CRUD = "todos" :> Get '[JSON] [Todo]
+       :<|> "todos" :> ReqBody '[JSON, FormUrlEncoded] Todo :> Post '[JSON] Todo
+       :<|> "todos" :> Capture "id" Int :> ReqBody '[JSON, FormUrlEncoded] Todo :> Put '[JSON] ()
+       :<|> "todos" :> Capture "id" Int :> Delete '[JSON] ()
+
+body :: [Text]
+body = mconcat
+  [ [ defKotlinImports ]
+  , generateKotlinForAPIClass "TodoAPI" $ mconcat
+      [ generateKotlinForDefDataClass (Proxy :: Proxy Todo)
+      , generateKotlinForAPI (Proxy :: Proxy CRUD)
+      ]
+  ]
+
+spec :: Spec
+spec = Spec ["com", "github", "matsubara0507"] "TodoAPI" body
+
+main :: IO ()
+main = do
+  specsToDir [spec] "example/src/main/java"
+  shelly $ do
+    cd "example"
+    which "gradle" >>= mapM_ (\_ -> run_ "gradle" ["build"])
+ servant-kotlin.cabal view
@@ -0,0 +1,95 @@+-- This file has been generated from package.yaml by hpack version 0.17.1.+--+-- see: https://github.com/sol/hpack++name:           servant-kotlin+version:        0.1.0.0+synopsis:       Automatically derive Kotlin class to query servant webservices+description:    See README at <https://github.com/matsubara0507/servant-kotlin#readme>+category:       Web+homepage:       https://github.com/matsubara0507/servant-kotlin#readme+author:         MATSUBARA Nobutada+maintainer:     MATSUBARA Nobutada+copyright:      2017 MATSUBARA Nobutada+license:        MIT+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files:+    README.md++library+  hs-source-dirs:+      src+  ghc-options: -Wall -fno-warn-orphans+  build-depends:+      base >= 4.7 && < 5+    , containers >= 0.5.7 && < 0.6.0+    , directory >= 1.3 && < 1.4+    , formatting >= 6.2.4 && < 6.3+    , lens >= 4.15 && < 4.16+    , servant >= 0.9 && < 0.12+    , servant-foreign >= 0.9 && < 0.12+    , text >= 1.2.2 && < 1.2.3+    , time >= 1.6 && < 1.9+    , wl-pprint-text >= 1.1 && < 1.2+  exposed-modules:+      Servant.Kotlin+      Servant.Kotlin.Type+      Servant.Kotlin.Internal.File+      Servant.Kotlin.Internal.Foreign+      Servant.Kotlin.Internal.Generate+  default-language: Haskell2010++test-suite spec+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  hs-source-dirs:+      src+      test+  ghc-options: -Wall+  build-depends:+      base >= 4.7 && < 5+    , containers >= 0.5.7 && < 0.6.0+    , directory >= 1.3 && < 1.4+    , formatting >= 6.2.4 && < 6.3+    , lens >= 4.15 && < 4.16+    , servant >= 0.9 && < 0.12+    , servant-foreign >= 0.9 && < 0.12+    , text >= 1.2.2 && < 1.2.3+    , time >= 1.6 && < 1.9+    , wl-pprint-text >= 1.1 && < 1.2+    , aeson >= 1.0 && < 1.3+    , hspec >= 2.4.1 && < 2.5+    , http-api-data >= 0.3.7 && < 0.3.8+  other-modules:+      Servant.Kotlin+      Servant.Kotlin.Internal.File+      Servant.Kotlin.Internal.Foreign+      Servant.Kotlin.Internal.Generate+      Servant.Kotlin.Type+      Servant.Kotlin.Internal.GenerateSpec+      Test.TestAPI+  default-language: Haskell2010++benchmark generate-test+  type: exitcode-stdio-1.0+  main-is: example/Generater.hs+  ghc-options: -Wall+  build-depends:+      base >= 4.7 && < 5+    , containers >= 0.5.7 && < 0.6.0+    , directory >= 1.3 && < 1.4+    , formatting >= 6.2.4 && < 6.3+    , lens >= 4.15 && < 4.16+    , servant >= 0.9 && < 0.12+    , servant-foreign >= 0.9 && < 0.12+    , text >= 1.2.2 && < 1.2.3+    , time >= 1.6 && < 1.9+    , wl-pprint-text >= 1.1 && < 1.2+    , aeson >= 1.0 && < 1.3+    , http-api-data >= 0.3.7 && < 0.3.8+    , servant-kotlin+    , shelly >= 1.6.8 && < 1.6.9+  default-language: Haskell2010
+ src/Servant/Kotlin.hs view
@@ -0,0 +1,29 @@+module Servant.Kotlin
+       ( generateKotlinForDefDataClass
+       , defKotlinImports
+       , generateKotlinForAPI
+       , generateKotlinForAPIWith
+       , generateKotlinForAPIClass
+       , KotlinOptions(..)
+       , UrlPrefix(..)
+       , defKotlinOptions
+       , Spec (..)
+       , specsToDir
+       -- * Convenience re-exports from the "Kotlin" module
+       , KotlinType
+       -- * Convenience re-exports from "Data.Proxy"
+       , Proxy(Proxy)
+       ) where
+
+import           Servant.Kotlin.Internal.File     (Spec (..), specsToDir)
+import           Servant.Kotlin.Internal.Generate (KotlinOptions (..),
+                                                   UrlPrefix (..),
+                                                   defKotlinImports,
+                                                   defKotlinOptions,
+                                                   generateKotlinForAPI,
+                                                   generateKotlinForAPIClass,
+                                                   generateKotlinForAPIWith,
+                                                   generateKotlinForDefDataClass)
+
+import           Data.Proxy                       (Proxy (Proxy))
+import           Servant.Kotlin.Type              (KotlinType)
+ src/Servant/Kotlin/Internal/File.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE OverloadedStrings #-}
+
+module Servant.Kotlin.Internal.File
+    ( Spec (..)
+    , specsToDir
+    ) where
+
+import           Data.Monoid      ((<>))
+import           Data.Text        (Text, pack, unpack)
+import qualified Data.Text        as Text
+import qualified Data.Text.IO     as Text
+import           Formatting       as F
+import           System.Directory (createDirectoryIfMissing)
+
+makePath :: [Text] -> Text
+makePath = Text.intercalate "/"
+
+data Spec = Spec
+  { namespace    :: [Text]
+  , filename     :: Text
+  , declarations :: [Text]
+  } deriving (Show)
+
+pathForSpec :: FilePath -> Spec -> [Text]
+pathForSpec rootDir spec = pack rootDir : namespace spec <> [filename spec]
+
+ensureDirectory :: FilePath -> Spec -> IO ()
+ensureDirectory rootDir spec = createDirectoryIfMissing True $ unpack dir
+  where
+    dir = makePath . init $ pathForSpec rootDir spec
+
+specToFile :: FilePath -> Spec -> IO ()
+specToFile rootDir spec = do
+  fprint ("Writing: " % F.stext % "\n") file
+  Text.writeFile (unpack file) body
+  where
+    path = pathForSpec rootDir spec
+    file = makePath path <> ".kt"
+    namespaceText = Text.intercalate "." (namespace spec)
+    body = Text.intercalate "\n\n" $
+             "package " <> namespaceText : declarations spec
+
+specsToDir :: [Spec] -> FilePath -> IO ()
+specsToDir specs rootDir = mapM_ processSpec specs
+  where
+    processSpec = ensureDirectory rootDir >> specToFile rootDir
+ src/Servant/Kotlin/Internal/Foreign.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE ScopedTypeVariables   #-}
+{-# LANGUAGE TypeFamilies          #-}
+
+module Servant.Kotlin.Internal.Foreign where
+
+import           Data.Proxy          (Proxy (Proxy))
+import           Servant.Foreign     (Foreign, GenerateList, HasForeign,
+                                      HasForeignType, Req, listFromAPI, typeFor)
+import           Servant.Kotlin.Type (KotlinClass, KotlinType, toKotlinType')
+
+data LangKotlin
+
+instance (KotlinType a) => HasForeignType LangKotlin KotlinClass a where
+  typeFor _ _ _ = toKotlinType' (Proxy :: Proxy a)
+
+getEndpoints ::
+  ( HasForeign LangKotlin KotlinClass api
+  , GenerateList KotlinClass (Foreign KotlinClass api))
+  => Proxy api
+  -> [Req KotlinClass]
+getEndpoints =
+  listFromAPI (Proxy :: Proxy LangKotlin) (Proxy :: Proxy KotlinClass)
+ src/Servant/Kotlin/Internal/Generate.hs view
@@ -0,0 +1,351 @@+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+
+module Servant.Kotlin.Internal.Generate
+    ( GenerateKotlin (..)
+    , generateKotlinForDefDataClass
+    , generateKotlinForDefDataClass'
+    , defKotlinImports
+    , generateKotlinForAPIClass
+    , generateKotlinForAPI
+    , generateKotlinForAPIWith
+    , KotlinOptions (..)
+    , defKotlinOptions
+    , UrlPrefix (..)
+    ) where
+
+import           Prelude                         hiding ((<$>))
+
+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    hiding ((<>))
+
+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 . 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" <+> textStrict className <> "(private val baseURL: String) {" ]
+  , [ docToText $ indent indentNum initialize ]
+  , fmap (docToText . vsep . fmap (indent indentNum . textStrict) . T.lines) body
+  , [ "}" ]
+  ]
+  where
+    initialize = vsep [ "init {", indent indentNum fuelManager, "}" ]
+    fuelManager = vsep
+      [ "FuelManager.instance.apply {"
+      , indent indentNum "basePath = baseURL"
+      , indent indentNum $ "baseHeaders = mapOf(" <> header <> ")"
+      , "}"
+      ]
+    header = hsep . punctuate comma $
+      fmap (\(k, v) -> dquotes k <+> "to" <+> 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 =
+      vsep
+        [ "fun" <+> fnName <> "(" <> args <> ") {"
+        , 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 =
+  (hsep . punctuate 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
+kotlinHeaderType header =
+  header ^. F.headerArg . F.argType . to kotlinTypeRef
+
+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
+kotlinQueryType arg =
+  arg ^. F.queryArgName . F.argType . to (kotlinTypeRef . wrapper)
+  where
+    wrapper = case arg ^. F.queryArgType of
+      F.Normal -> PrimitiveClass . KNullable
+      _        ->  id
+
+kotlinBodyArg :: Doc
+kotlinBodyArg = "body"
+
+kotlinHandlerArg :: Doc
+kotlinHandlerArg = "handler"
+
+kotlinTypeRef :: KotlinClass -> Doc
+kotlinTypeRef = stext . generateKotlin'
+
+
+mkRequest :: KotlinOptions -> F.Req KotlinClass -> Doc
+mkRequest opts request = "Fuel" <> align (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 . punctuate " + " $
+  dquotes "/" : punctuate (" + " <> dquotes "/") (map segmentToDoc segments)
+  where
+    segmentToDoc :: F.Segment KotlinClass -> Doc
+    segmentToDoc segment =
+      case F.unSegment segment of
+        F.Static path -> 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 . displayT . renderPretty 0.4 100
+
+stext :: Text -> Doc
+stext = text . L.fromStrict
+ src/Servant/Kotlin/Type.hs view
@@ -0,0 +1,185 @@+{-# LANGUAGE DefaultSignatures    #-}
+{-# LANGUAGE FlexibleContexts     #-}
+{-# LANGUAGE FlexibleInstances    #-}
+{-# LANGUAGE ScopedTypeVariables  #-}
+{-# LANGUAGE TypeOperators        #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+
+module Servant.Kotlin.Type
+    ( KotlinClass (..)
+    , KotlinPrimitiveClass (..)
+    , KotlinExClass (..)
+    , KotlinDataClass (..)
+    , KotlinFields (..)
+    , KotlinField (..)
+    , KotlinType (..)
+    , GenericKotlinType (..)
+    , GenericKotlinFields (..)
+    , toKotlinType'
+    ) where
+
+import           Data.Int     (Int16, Int32, Int64, Int8)
+import           Data.IntMap  (IntMap)
+import           Data.Map     (Map)
+import           Data.Maybe   (fromMaybe)
+import           Data.Proxy   (Proxy (..))
+import           Data.Text    (Text, pack)
+import           Data.Time    (UTCTime)
+import           GHC.Generics
+import           Servant.API  (NoContent (..))
+
+data KotlinClass
+  = PrimitiveClass KotlinPrimitiveClass
+  | DataClass KotlinDataClass
+  | ExClass KotlinExClass
+  deriving (Show, Eq)
+
+data KotlinPrimitiveClass
+  = KDouble
+  | KFloat
+  | KLong
+  | KInt
+  | KShort
+  | KByte
+  | KChar
+  | KBoolean
+  | KArray KotlinClass
+  | KString
+  | KUnit
+  | KNullable KotlinClass
+  | KAny
+  deriving (Show, Eq)
+
+data KotlinExClass
+  = KList KotlinClass
+  | KHashMap KotlinClass KotlinClass
+  | KPair KotlinClass KotlinClass
+  | KTime
+  deriving (Show, Eq)
+
+data KotlinDataClass = KotlinDataClass Text KotlinFields deriving (Show, Eq)
+
+data KotlinFields
+  = Node KotlinField
+  | Brunch KotlinFields KotlinFields
+  deriving (Show, Eq)
+
+data KotlinField = KotlinField Text KotlinClass deriving (Show, Eq)
+
+class KotlinType a where
+  toKotlinType :: a -> Maybe KotlinClass
+  toKotlinType = genericToKotlinType . from
+  default toKotlinType :: (Generic a, GenericKotlinType (Rep a)) =>
+    a -> Maybe KotlinClass
+
+class GenericKotlinType f where
+  genericToKotlinType :: f a -> Maybe KotlinClass
+
+instance (Datatype d, GenericKotlinFields f)
+  => GenericKotlinType (D1 d f) where
+  genericToKotlinType datatype = fmap DataClass $
+    KotlinDataClass (pack $ datatypeName datatype)
+      <$> genericToKotlinFields (unM1 datatype)
+
+class GenericKotlinFields f where
+  genericToKotlinFields :: f a -> Maybe KotlinFields
+
+instance (Constructor c, GenericKotlinFields f)
+  => GenericKotlinFields (C1 c f) where
+  genericToKotlinFields constructor =
+    if conIsRecord constructor then
+      genericToKotlinFields (unM1 constructor)
+    else
+      Nothing
+
+instance GenericKotlinFields (f :+: g) where
+  genericToKotlinFields _ = Nothing
+
+instance (Selector s, GenericKotlinType a)
+  => GenericKotlinFields (S1 s a) where
+  genericToKotlinFields selector =
+    case selName selector of
+      ""   -> Nothing
+      name ->
+        Node . KotlinField (pack name)
+          <$> genericToKotlinType (undefined :: a p)
+
+instance (GenericKotlinFields f, GenericKotlinFields g)
+  => GenericKotlinFields (f :*: g) where
+  genericToKotlinFields _ =
+    Brunch <$> genericToKotlinFields (undefined :: f p)
+           <*> genericToKotlinFields (undefined :: g p)
+
+instance GenericKotlinFields U1 where
+  genericToKotlinFields _ = Nothing
+
+instance KotlinType a => GenericKotlinType (Rec0 a) where
+  genericToKotlinType _ = toKotlinType (Proxy :: Proxy a)
+
+instance KotlinType a => KotlinType [a] where
+  toKotlinType _ = ExClass . KList <$> toKotlinType (Proxy :: Proxy a)
+
+instance KotlinType a => KotlinType (Maybe a) where
+  toKotlinType _ =
+    PrimitiveClass . KNullable <$> toKotlinType (Proxy :: Proxy a)
+
+instance KotlinType () where
+  toKotlinType _ = Just $ PrimitiveClass KUnit
+
+instance KotlinType Text where
+  toKotlinType _ = Just $ PrimitiveClass KString
+
+instance KotlinType UTCTime where
+  toKotlinType _ = Just $ ExClass KTime
+
+instance KotlinType Float where
+  toKotlinType _ = Just $ PrimitiveClass KFloat
+
+instance KotlinType Double where
+  toKotlinType _ = Just $ PrimitiveClass KDouble
+
+instance KotlinType Int where
+  toKotlinType _ = Just $ PrimitiveClass KInt
+
+instance KotlinType Int8 where
+  toKotlinType _ = Just $ PrimitiveClass KByte
+
+instance KotlinType Int16 where
+  toKotlinType _ = Just $ PrimitiveClass KShort
+
+instance KotlinType Int32 where
+  toKotlinType _ = Just $ PrimitiveClass KInt
+
+instance KotlinType Int64 where
+  toKotlinType _ = Just $ PrimitiveClass KLong
+
+instance KotlinType Char where
+  toKotlinType _ = Just $ PrimitiveClass KChar
+
+instance KotlinType Bool where
+  toKotlinType _ = Just $ PrimitiveClass KBoolean
+
+instance (KotlinType a, KotlinType b) => KotlinType (a, b) where
+  toKotlinType _ = fmap ExClass $
+    KPair
+      <$> toKotlinType (Proxy :: Proxy a)
+      <*> toKotlinType (Proxy :: Proxy b)
+
+instance (KotlinType a) => KotlinType (Proxy a) where
+  toKotlinType _ = toKotlinType (undefined :: a)
+
+instance (KotlinType k, KotlinType v) => KotlinType (Map k v) where
+  toKotlinType _ = fmap ExClass $
+    KHashMap
+      <$> toKotlinType (Proxy :: Proxy k)
+      <*> toKotlinType (Proxy :: Proxy v)
+
+instance (KotlinType v) => KotlinType (IntMap v) where
+  toKotlinType _ =
+    PrimitiveClass . KArray <$> toKotlinType (Proxy :: Proxy v)
+
+instance KotlinType NoContent where
+  toKotlinType _ = Just $ PrimitiveClass KUnit
+
+toKotlinType' :: (KotlinType a) => a -> KotlinClass
+toKotlinType' = fromMaybe (PrimitiveClass KAny) . toKotlinType
+ test/Servant/Kotlin/Internal/GenerateSpec.hs view
@@ -0,0 +1,139 @@+{-# LANGUAGE DataKinds         #-}
+{-# LANGUAGE DeriveAnyClass    #-}
+{-# LANGUAGE DeriveGeneric     #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeOperators     #-}
+
+module Servant.Kotlin.Internal.GenerateSpec
+    ( main
+    , spec
+    ) where
+
+import           Data.Proxy                       (Proxy (Proxy))
+import           Data.Text                        (Text)
+import qualified Data.Text                        as T
+import           GHC.Generics                     (Generic)
+import           Servant.Kotlin.Internal.Generate
+import           Servant.Kotlin.Type
+import           Test.Hspec                       (Spec, context, describe,
+                                                   hspec, it, shouldBe)
+import           Test.TestAPI
+
+data Todo = Todo
+  { todoId :: Int
+  , title  :: Text
+  , done   :: Bool
+  } deriving (Generic, Show, Eq, KotlinType)
+
+data Hoge
+  = Hoge1 Int
+  | Hoge2 Bool
+  deriving (Generic, Show, Eq, KotlinType)
+
+main :: IO ()
+main = hspec spec
+
+spec :: Spec
+spec = do
+  describe "generateKotlinForDefDataClass :: Proxy a -> [Text]" $ do
+    it "sample Todo type" $
+      generateKotlinForDefDataClass (Proxy :: Proxy Todo) `shouldBe`
+        [ "data class Todo(val todoId: Int, val title: String, val done: Boolean)" ]
+    it "not data class, e.g. Int" $
+      generateKotlinForDefDataClass (Proxy :: Proxy Int) `shouldBe` []
+    it "return Nothing e.g. Product Type" $
+      generateKotlinForDefDataClass (Proxy :: Proxy Hoge) `shouldBe` []
+
+  describe "generateKotlinForAPIClass :: Text -> [Text] -> [Text]" $ do
+    it "body is empty" $
+      generateKotlinForAPIClass "TodoAPI" [] `shouldBe`
+        [ "class TodoAPI(private val baseURL: String) {"
+        , T.intercalate "\n"
+            [ "    init {"
+            , "        FuelManager.instance.apply {"
+            , "            basePath = baseURL"
+            , "            baseHeaders = mapOf(\"Content-Type\" to \"application/json\", \"Device\" to \"Android\")"
+            , "        }"
+            , "    }"
+            ]
+        , "}"
+        ]
+    it "body is non empty" $
+      generateKotlinForAPIClass "TodoAPI" ["aaa", "bbb\nccc"] `shouldBe`
+        [ "class TodoAPI(private val baseURL: String) {"
+        , T.intercalate "\n"
+            [ "    init {"
+            , "        FuelManager.instance.apply {"
+            , "            basePath = baseURL"
+            , "            baseHeaders = mapOf(\"Content-Type\" to \"application/json\", \"Device\" to \"Android\")"
+            , "        }"
+            , "    }"
+            ]
+        , "    aaa"
+        , T.intercalate "\n"
+            [ "    bbb"
+            , "    ccc"
+            ]
+        , "}"
+        ]
+
+  describe "generateKotlinForAPI :: Proxy api -> [Text]" $
+    it "sample CRUD" $
+      generateKotlinForAPI (Proxy :: Proxy TestApi) `shouldBe`
+        [ T.intercalate "\n"
+            [ "fun getOne(handler: (Request, Response, Result<Int, FuelError>) -> Unit) {"
+            , "    Fuel.get(\"/\" + \"one\")"
+            , "        .responseObject(handler)"
+            , "}"
+            ]
+        , T.intercalate "\n"
+            [ "fun postTwo(body: String, handler: (Request, Response, Result<Int?, FuelError>) -> Unit) {"
+            , "    Fuel.post(\"/\" + \"two\")"
+            , "        .body(Gson().toJson(body, String::class.java))"
+            , "        .responseObject(handler)"
+            , "}"
+            ]
+        , T.intercalate "\n"
+            [ "fun getBooksById(capture_id: Int, handler: (Request, Response, Result<Book, FuelError>) -> Unit) {"
+            , "    Fuel.get(\"/\" + \"books\" + \"/\" + capture_id)"
+            , "        .responseObject(handler)"
+            , "}"
+            ]
+        , T.intercalate "\n"
+            [ "fun getBooksByTitle(capture_title: String, handler: (Request, Response, Result<Book, FuelError>) -> Unit) {"
+            , "    Fuel.get(\"/\" + \"books\" + \"/\" + capture_title)"
+            , "        .responseObject(handler)"
+            , "}"
+            ]
+        , T.intercalate "\n"
+            [ "fun getBooks(query_published: Boolean, query_sort: String?, query_year: Int?, query_filters: List<Boolean?>, handler: (Request, Response, Result<List<Book>, FuelError>) -> Unit) {"
+            , "    Fuel.get(\"/\" + \"books\")"
+            , "        .responseObject(handler)"
+            , "}"
+            ]
+        , T.intercalate "\n"
+            [ "fun postBooks(body: Book, handler: (Request, Response, Result<Unit, FuelError>) -> Unit) {"
+            , "    Fuel.post(\"/\" + \"books\")"
+            , "        .body(Gson().toJson(body, Book::class.java))"
+            , "        .responseObject(handler)"
+            , "}"
+            ]
+        , T.intercalate "\n"
+            [ "fun getNothing(handler: (Request, Response, Result<Unit, FuelError>) -> Unit) {"
+            , "    Fuel.get(\"/\" + \"nothing\")"
+            , "        .responseObject(handler)"
+            , "}"
+            ]
+        , T.intercalate "\n"
+            [ "fun putNothing(handler: (Request, Response, Result<Unit, FuelError>) -> Unit) {"
+            , "    Fuel.put(\"/\" + \"nothing\")"
+            , "        .responseObject(handler)"
+            , "}"
+            ]
+        , T.intercalate "\n"
+            [ "fun getWithaheader(header_myTextHeader: String, header_MyIntHeader: Int, handler: (Request, Response, Result<String, FuelError>) -> Unit) {"
+            , "    Fuel.get(\"/\" + \"with-a-header\")"
+            , "        .responseObject(handler)"
+            , "}"
+            ]
+        ]
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/Test/TestAPI.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE DataKinds      #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric  #-}
+{-# LANGUAGE TypeOperators  #-}
+
+module Test.TestAPI where
+
+import           Data.Aeson          (ToJSON)
+import           Data.Proxy          (Proxy (Proxy))
+import           Data.Text           (Text)
+import           GHC.Generics        (Generic)
+import           Servant.API         ((:<|>), (:>), Capture, Get, GetNoContent,
+                                      Header, JSON, NoContent, Post,
+                                      PostNoContent, Put, QueryFlag, QueryParam,
+                                      QueryParams, ReqBody)
+import           Servant.Kotlin.Type (KotlinType)
+
+data Book = Book
+    { title :: Text
+    } deriving (Generic, KotlinType, Show, Eq)
+
+instance ToJSON Book
+
+type TestApi =
+       "one"
+         :> Get '[JSON] Int
+  :<|> "two"
+         :> ReqBody '[JSON] Text
+         :> Post '[JSON] (Maybe Int)
+  :<|> "books"
+         :> Capture "id" Int
+         :> Get '[JSON] Book
+  :<|> "books"
+         :> Capture "title" Text
+         :> Get '[JSON] Book
+  :<|> "books"
+         :> QueryFlag "published"
+         :> QueryParam "sort" Text
+         :> QueryParam "year" Int
+         :> QueryParams "filters" (Maybe Bool)
+         :> Get '[JSON] [Book]
+  :<|> "books"
+         :> ReqBody '[JSON] Book
+         :> PostNoContent '[JSON] NoContent
+  :<|> "nothing"
+         :> GetNoContent '[JSON] NoContent
+  :<|> "nothing"
+         :> Put '[JSON] () -- old way to specify no content
+  :<|> "with-a-header"
+         :> Header "myTextHeader" Text
+         :> Header "MyIntHeader" Int
+         :> Get '[JSON] Text
+
+testApi :: Proxy TestApi
+testApi = Proxy