json-spec-elm-servant 0.3.0.0 → 0.3.1.0
raw patch · 3 files changed
+102/−10 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Data.JsonSpec.Elm.Servant: BodyEncoder :: Type Void -> Expression Void -> Param
+ Data.JsonSpec.Elm.Servant: Capture :: Text -> PathParam
+ Data.JsonSpec.Elm.Servant: HeaderParam :: HeaderParam -> Param
+ Data.JsonSpec.Elm.Servant: OptionalHeader :: Text -> HeaderParam
+ Data.JsonSpec.Elm.Servant: OptionalQP :: Text -> QP
+ Data.JsonSpec.Elm.Servant: PathParam :: PathParam -> Param
+ Data.JsonSpec.Elm.Servant: QueryParam :: QP -> Param
+ Data.JsonSpec.Elm.Servant: RequiredHeader :: Text -> HeaderParam
+ Data.JsonSpec.Elm.Servant: RequiredQP :: Text -> QP
+ Data.JsonSpec.Elm.Servant: Static :: Text -> PathParam
+ Data.JsonSpec.Elm.Servant: [elmType] :: Param -> Type Void
+ Data.JsonSpec.Elm.Servant: [encoder] :: Param -> Expression Void
+ Data.JsonSpec.Elm.Servant: class Elmable e
+ Data.JsonSpec.Elm.Servant: class IsParam a
+ Data.JsonSpec.Elm.Servant: data HeaderParam
+ Data.JsonSpec.Elm.Servant: data Param
+ Data.JsonSpec.Elm.Servant: data PathParam
+ Data.JsonSpec.Elm.Servant: data QP
+ Data.JsonSpec.Elm.Servant: endpoints :: Elmable e => [Param] -> Definitions ()
+ Data.JsonSpec.Elm.Servant: instance Data.JsonSpec.Elm.Servant.IsParam (Servant.API.QueryParam.QueryParam' more name typ) => Data.JsonSpec.Elm.Servant.IsParam (Servant.API.QueryParam.QueryParam' (other : more) name typ)
+ Data.JsonSpec.Elm.Servant: instance GHC.Classes.Eq Data.JsonSpec.Elm.Servant.QP
+ Data.JsonSpec.Elm.Servant: instance GHC.TypeLits.KnownSymbol name => Data.JsonSpec.Elm.Servant.IsParam (Servant.API.QueryParam.QueryParam' (Servant.API.Modifiers.Optional : more) name typ)
+ Data.JsonSpec.Elm.Servant: instance GHC.TypeLits.KnownSymbol name => Data.JsonSpec.Elm.Servant.IsParam (Servant.API.QueryParam.QueryParam' (Servant.API.Modifiers.Required : more) name typ)
+ Data.JsonSpec.Elm.Servant: param :: IsParam a => Definitions Param
Files
- json-spec-elm-servant.cabal +1/−1
- src/Data/JsonSpec/Elm/Servant.hs +95/−6
- test/Api.hs +6/−3
json-spec-elm-servant.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: json-spec-elm-servant-version: 0.3.0.0+version: 0.3.1.0 synopsis: Generated elm code for servant APIs. description: Generate Elm encoders, decoders, and API requests for an Servant API, where the shape of the data
src/Data/JsonSpec/Elm/Servant.hs view
@@ -4,6 +4,7 @@ {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-}@@ -14,7 +15,22 @@ {-# LANGUAGE ViewPatterns #-} module Data.JsonSpec.Elm.Servant (+ -- * Generating Elm Clients servantDefs,++ -- * Extensions+ {-|+ The symbols in this section are mainly exposed in case you are using+ some extensions to the standard servant types and need to build some+ companion extensions to generate proper Elm types for them. For most+ normal usage you will probably just use 'servantDefs'.+ -}+ Elmable(..),+ IsParam(..),+ Param(..),+ PathParam(..),+ HeaderParam(..),+ QP(..), ) where @@ -41,7 +57,7 @@ Nothing), Semigroup((<>)), ($), (.), (<$>), Eq, Int, error, reverse) import Servant.API (ReflectMethod(reflectMethod), (:<|>), (:>), Capture, Header', Headers, JSON, NamedRoutes, NoContent, NoContentVerb, Optional,- ReqBody', Required, ToServantApi, Verb)+ QueryParam', ReqBody', Required, ToServantApi, Verb) import qualified Data.Set as Set import qualified Data.Text as Text import qualified Data.Text.Encoding as TE@@ -52,6 +68,14 @@ import qualified Language.Elm.Type as Type +{-|+ This function will traverse the @api@ type, generating elm definitions for:+ * Http requests for each endpoint, including encoders and decoders for+ anonymous elm types.+ * Named Elm types (i.e. Any 'Specification' that is bound to a name using+ 'JsonLet'+ * Decoders and Encoders for named elm types.+-} servantDefs :: forall api. (Elmable api) => Proxy api -> Set Definition servantDefs _ = builtins@@ -162,7 +186,7 @@ ( F . F <$> p req "decoder") [ pat "Api.Req.Left" [Pat.Var 0] $ "Result.Ok" `Expr.App` patVar 0- , pat "Api.Req.Right" [Pat.Var 0] $ + , pat "Api.Req.Right" [Pat.Var 0] $ Expr.Case ( Expr.apps@@ -189,7 +213,15 @@ ] +{-| Class of servant APIs for which Elm client code can be generated. -} class Elmable e where+ {-|+ Collect all the Elm definitions needed to implement a client for+ the API. This is called recursively on our walk down the API tree,+ and the @['Param']@ argument contains all the request parameters+ (like 'Capture', 'ReqBody', etc) that have been encountered so far on+ whatever particular branch . It will start out empty at the API root.+ -} endpoints :: [Param] -> Definitions () instance (Elmable a, Elmable b) => Elmable (a :<|> b) where endpoints params = do@@ -248,6 +280,10 @@ pure () +{-|+ Obtain a value-level request parameter type from the type-level servant+ parameter type.+-} class IsParam a where param :: Definitions Param instance (KnownSymbol name) => IsParam (Capture name tpy) where@@ -268,9 +304,9 @@ IsParam (ReqBody' (Required : mods) (JSON : accept) a) where param = do- typ <- typeOf @(DecodingSpec a)+ elmType <- typeOf @(DecodingSpec a) encoder <- encoderOf @(DecodingSpec a)- pure $ BodyEncoder typ encoder+ pure $ BodyEncoder {elmType, encoder} instance {- IsParam (ReqBody' (other : mods) (JSON : accept) a) -} {-# overlaps #-} (IsParam (ReqBody' mods '[JSON] a)) =>@@ -285,6 +321,24 @@ param = param @(ReqBody' mods accept a) instance (KnownSymbol segment) => IsParam (segment :: Symbol) where param = pure $ PathParam (Static (sym @segment))+instance {- IsParam (QueryParam' (Optional : more) name typ) -}+ (KnownSymbol name)+ =>+ IsParam (QueryParam' (Optional : more) name typ)+ where+ param = pure $ QueryParam (OptionalQP (sym @name))+instance {- IsParam (QueryParam' (Required : more) name typ) -}+ (KnownSymbol name)+ =>+ IsParam (QueryParam' (Required : more) name typ)+ where+ param = pure $ QueryParam (RequiredQP (sym @name))+instance {- IsParam (QueryParam' (other : more) name typ) -}+ {-# overlaps #-} (IsParam (QueryParam' more name typ))+ =>+ IsParam (QueryParam' (other : more) name typ)+ where+ param = param @(QueryParam' more name typ) requestFunctionName@@ -332,6 +386,9 @@ (\case PathParam (Capture _) -> Just "Basics.String" PathParam (Static _) -> Nothing+ QueryParam (RequiredQP _) -> Just "Basics.String"+ QueryParam (OptionalQP _) ->+ Just ("Basics.Maybe" `Type.App` "Basics.String") HeaderParam (RequiredHeader _) -> Just "Basics.String" HeaderParam (OptionalHeader _) -> Just ("Basics.Maybe" `Type.App` "Basics.String")@@ -398,7 +455,29 @@ Capture _ -> Expr.Var param_ | param_@(PathParam pp) <- params ]- , Expr.List []+ , Expr.apps+ "List.filterMap"+ [ "Basics.identity"+ , Expr.List+ [ let+ name :: Text+ name = case qp of { RequiredQP n -> n; OptionalQP n -> n}+ + queryExpr :: Expression Param+ queryExpr =+ Expr.apps+ "Maybe.map"+ [ "Url.Builder.string" `Expr.App` Expr.String name+ , case qp of+ RequiredQP _ ->+ "Maybe.Just" `Expr.App` Expr.Var param_+ OptionalQP _ -> Expr.Var param_+ ]+ in+ queryExpr+ | param_@(QueryParam qp) <- params+ ]+ ] ] body :: Expression Param@@ -429,7 +508,17 @@ data Param = PathParam PathParam | HeaderParam HeaderParam- | BodyEncoder (Type Void) (Expression Void)+ | QueryParam QP+ | BodyEncoder+ { elmType :: Type Void+ , encoder :: Expression Void+ }+ deriving stock (Eq)+++data QP+ = RequiredQP Text+ | OptionalQP Text deriving stock (Eq)
test/Api.hs view
@@ -62,9 +62,10 @@ import Prelude (Applicative(pure), Either(Left, Right), Functor(fmap), Traversable(traverse), (.), (<$>), Eq, Int, Ord) import Servant.API (FromHttpApiData(parseHeader, parseQueryParam),- GenericMode((:-)), StdMethod(GET), (:>), Capture, DeleteNoContent, Get,- Header, Header', Headers, JSON, NamedRoutes, NoContent, Optional, Post,- PostNoContent, ReqBody, ReqBody', Required, Strict, ToHttpApiData, Verb)+ GenericMode((:-)), StdMethod(GET), (:>), Capture, DeleteNoContent,+ Get, Header, Header', Headers, JSON, NamedRoutes, NoContent, Optional,+ Post, PostNoContent, QueryParam', ReqBody, ReqBody', Required, Strict,+ ToHttpApiData, Verb) import Web.Cookie (SetCookie) import qualified Data.Map as Map import qualified Data.Set as Set@@ -117,6 +118,8 @@ , dashboard :: mode :- "dashboard"+ :> QueryParam' '[Required, Strict] "foo" Text+ :> QueryParam' '[Optional, Strict] "bar" Text :> Get '[JSON] DashboardData , addInvite :: mode