initial version with basic model - todo implement logic
added a swagger-generator deriving coupontype automatically implemented cors for swagger to work fixed product modelmaster
commit
1d44db32f0
|
|
@ -0,0 +1,4 @@
|
|||
[Dolphin]
|
||||
Timestamp=2017,5,3,7,47,12
|
||||
Version=4
|
||||
ViewMode=1
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
# Created by https://www.gitignore.io/api/haskell
|
||||
|
||||
### Haskell ###
|
||||
dist
|
||||
dist-*
|
||||
cabal-dev
|
||||
*.o
|
||||
*.hi
|
||||
*.chi
|
||||
*.chs.h
|
||||
*.dyn_o
|
||||
*.dyn_hi
|
||||
.hpc
|
||||
.hsenv
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
*.prof
|
||||
*.aux
|
||||
*.hp
|
||||
*.eventlog
|
||||
.stack-work/
|
||||
cabal.project.local
|
||||
.HTF/
|
||||
|
||||
# End of https://www.gitignore.io/api/haskell
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
Copyright Author name here (c) 2017
|
||||
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of Author name here nor the names of other
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
Imagine the backend API of a typical coupon module that would be getting used in shopping cart applications.
|
||||
End-customers would typically be discovering these coupons via some promotion or deal sites (like CouponRaja or CouponDunia).
|
||||
Entering this coupon during the checkout process would give the customer some discount.
|
||||
|
||||
* Design & implement a REST API to create and read coupons.
|
||||
|
||||
* We should be able to define the applicable discount based on
|
||||
-- Flat discount amount per order
|
||||
-- Flat discount amount per item
|
||||
-- Percentage discount on total order amount
|
||||
|
||||
* We should be able to restrict coupon usage based on the following
|
||||
-- All products or list of products
|
||||
-- Orders placed between given start/end dates
|
||||
-- Orders with total amount higher than given value
|
||||
-- Limit coupon usage per customer (tracked by customer email), i.e. customer is allowed to use a coupon on 'N' times
|
||||
-- Limit coupon usage per product, i.e. coupon is allowed to be used 'N' number of times for a given product(s)
|
||||
-- Limit coupon usage, i.e. coupon is allowed to be used 'N' number of times across all customers and products
|
||||
|
||||
* Design & implement a REST API to validate coupon usage and respond with the final discount amount.
|
||||
The API can accept a list of items/products added to the shopping card,
|
||||
and the coupon that the customer is trying to apply.
|
||||
The API should respond with whether the coupon is applicable or not, and discount amount.
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
module Main where
|
||||
|
||||
import App
|
||||
|
||||
import SwaggerGen
|
||||
|
||||
main :: IO ()
|
||||
main = do genSwaggerDoc
|
||||
run "host=localhost port=5432 user=msfuser dbname=coupon password="
|
||||
|
||||
-- run "testSql.db"
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
name: coupon-servant
|
||||
version: 0.1.0.0
|
||||
-- synopsis:
|
||||
-- description:
|
||||
homepage: https://github.com/alaudiadae/coupon-servant#readme
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Author name here
|
||||
maintainer: alaudiadae@gmx.com
|
||||
copyright: 2017 Author name here
|
||||
category: Web
|
||||
build-type: Simple
|
||||
extra-source-files: README.md
|
||||
cabal-version: >=1.10
|
||||
|
||||
library
|
||||
hs-source-dirs: src
|
||||
exposed-modules: Lib,
|
||||
Api,
|
||||
App,
|
||||
Coupon,
|
||||
Models,
|
||||
SwaggerGen
|
||||
build-depends: base >= 4.7 && < 5
|
||||
, aeson
|
||||
, bytestring
|
||||
, lens
|
||||
, monad-logger
|
||||
, persistent
|
||||
, persistent-template
|
||||
, persistent-sqlite
|
||||
, persistent-postgresql
|
||||
, servant
|
||||
, servant-server
|
||||
, servant-swagger
|
||||
, string-conversions
|
||||
, swagger2
|
||||
, text
|
||||
, time
|
||||
, transformers
|
||||
, unordered-containers
|
||||
, wai
|
||||
, wai-cors
|
||||
, warp
|
||||
default-language: Haskell2010
|
||||
|
||||
executable coupon-servant-exe
|
||||
hs-source-dirs: app
|
||||
main-is: Main.hs
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
build-depends: base
|
||||
, coupon-servant
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite coupon-servant-test
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: test
|
||||
main-is: Spec.hs
|
||||
build-depends: base
|
||||
, coupon-servant
|
||||
, hspec
|
||||
, hspec-wai
|
||||
, hspec-wai-json
|
||||
, aeson
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
default-language: Haskell2010
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: https://github.com/githubuser/coupon-servant
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
module Api (module Api,module Models) where
|
||||
|
||||
import Data.Proxy
|
||||
import Data.Text
|
||||
import Models
|
||||
import Servant.API
|
||||
|
||||
type CouponApi =
|
||||
"coupon" :> "add" :> ReqBody '[JSON] Coupon :> Post '[JSON] (Maybe Coupon)
|
||||
:<|> "coupon" :> "get" :> Capture "name" Text :> Get '[JSON] (Maybe Coupon)
|
||||
:<|> "coupon" :> "del" :> Capture "name" Text :> Get '[JSON] (Maybe Coupon)
|
||||
|
||||
type BillCouponApi =
|
||||
"billcoupon" :> ReqBody '[JSON] BillCoupon :> Post '[JSON] CouponResult
|
||||
|
||||
type ServerApi = CouponApi :<|> BillCouponApi
|
||||
|
||||
api :: Proxy ServerApi
|
||||
api = Proxy
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
module App where
|
||||
|
||||
import Api
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.Logger (runStderrLoggingT)
|
||||
import Data.String.Conversions
|
||||
import Data.Text
|
||||
import Database.Persist
|
||||
import Database.Persist.Postgresql
|
||||
import Database.Persist.Sqlite
|
||||
import Network.Wai
|
||||
import Network.Wai.Handler.Warp as Warp
|
||||
import Network.Wai.Middleware.Cors
|
||||
import Servant
|
||||
-- import Network.Wai.Middleware.RequestLogger (logStdoutDev)
|
||||
|
||||
couponServer :: ConnectionPool -> Server CouponApi
|
||||
couponServer pool =
|
||||
couponAddH :<|> couponGetH :<|> couponDelH
|
||||
where
|
||||
couponAddH newCoupon = liftIO $ couponAdd newCoupon
|
||||
couponGetH code = liftIO $ couponGet code
|
||||
couponDelH code = liftIO $ couponDel code
|
||||
|
||||
couponAdd :: Coupon -> IO (Maybe Coupon)
|
||||
couponAdd newCoupon = flip runSqlPersistMPool pool $ do
|
||||
exists <- selectFirst [CouponCode ==. couponCode newCoupon] []
|
||||
case exists of
|
||||
Nothing -> Just <$> insert newCoupon
|
||||
Just _ -> return Nothing
|
||||
return Nothing
|
||||
|
||||
couponGet :: Text -> IO (Maybe Coupon)
|
||||
couponGet code = flip runSqlPersistMPool pool $ do
|
||||
mUser <- selectFirst [CouponCode ==. code] []
|
||||
return $ entityVal <$> mUser
|
||||
|
||||
couponDel :: Text -> IO (Maybe Coupon)
|
||||
couponDel code = flip runSqlPersistMPool pool $ do
|
||||
deleteWhere [CouponCode ==. code]
|
||||
return Nothing
|
||||
|
||||
billCouponServer :: ConnectionPool -> Server BillCouponApi
|
||||
billCouponServer pool = billCouponComputeH
|
||||
where billCouponComputeH bill = liftIO $ billCouponCompute bill
|
||||
-- return $ Applied 100
|
||||
billCouponCompute :: BillCoupon -> IO CouponResult
|
||||
billCouponCompute bill = do putStrLn $ show bill
|
||||
return $ Applied 100
|
||||
|
||||
|
||||
server :: ConnectionPool -> Server ServerApi
|
||||
server pool = couponServer pool :<|> billCouponServer pool
|
||||
|
||||
|
||||
app :: ConnectionPool -> Application
|
||||
app pool = cors (const $ Just policy) $ serve api $ server pool
|
||||
where
|
||||
policy = simpleCorsResourcePolicy { corsRequestHeaders = ["Content-Type"] }
|
||||
|
||||
mkPgApp :: String -> IO Application
|
||||
mkPgApp sqliteFile = do
|
||||
pool <- runStderrLoggingT $ createPostgresqlPool (cs sqliteFile) 5
|
||||
runSqlPool (runMigration migrateAll) pool
|
||||
return $ app pool
|
||||
|
||||
mkApp :: String -> IO Application
|
||||
mkApp sqliteFile = do
|
||||
pool <- runStderrLoggingT $ createSqlitePool (cs sqliteFile) 5
|
||||
runSqlPool (runMigration migrateAll) pool
|
||||
return $ app pool
|
||||
|
||||
run :: String -> IO ()
|
||||
run dbConnStr =
|
||||
Warp.run 3000 =<< mkPgApp dbConnStr
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
{-# LANGUAGE DeriveAnyClass #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE EmptyDataDecls #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
|
||||
module Coupon where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Types
|
||||
import Database.Persist.TH
|
||||
import GHC.Generics
|
||||
import Prelude
|
||||
|
||||
|
||||
data Product = Product {
|
||||
productName :: String,
|
||||
productPrice:: Int
|
||||
} deriving (Eq, Read, Show, Generic, FromJSON, ToJSON)
|
||||
|
||||
data BillCoupon = BillCoupon {
|
||||
customer :: String,
|
||||
coupon :: String,
|
||||
productList :: [Product]
|
||||
} deriving (Eq, Read, Show, Generic, FromJSON, ToJSON)
|
||||
|
||||
data CouponResult = Applied Int | Rejected String | Partial String
|
||||
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
|
||||
|
||||
data CouponType = ProductFlat Int | CartFlat Int | CartPercent Int
|
||||
deriving (Show, Read, Eq, Generic)
|
||||
|
||||
couponOption = defaultOptions { sumEncoding = ObjectWithSingleField }
|
||||
|
||||
instance FromJSON CouponType where
|
||||
parseJSON = genericParseJSON couponOption
|
||||
|
||||
instance ToJSON CouponType where
|
||||
toJSON = genericToJSON couponOption
|
||||
|
||||
derivePersistField "CouponType"
|
||||
|
||||
prodListEx = [Product {productName = "Water", productPrice = 15}]
|
||||
billCouponExample = BillCoupon { customer = "test@email.com", coupon = "FLAT100", productList = prodListEx}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
module Lib
|
||||
( startApp
|
||||
, app
|
||||
) where
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Aeson.TH
|
||||
import Network.Wai
|
||||
import Network.Wai.Handler.Warp
|
||||
import Servant
|
||||
|
||||
data User = User
|
||||
{ userId :: Int
|
||||
, userFirstName :: String
|
||||
, userLastName :: String
|
||||
} deriving (Eq, Show)
|
||||
|
||||
$(deriveJSON defaultOptions ''User)
|
||||
|
||||
type API = "users" :> Get '[JSON] [User]
|
||||
|
||||
startApp :: IO ()
|
||||
startApp = run 8080 app
|
||||
|
||||
app :: Application
|
||||
app = serve api server
|
||||
|
||||
api :: Proxy API
|
||||
api = Proxy
|
||||
|
||||
server :: Server API
|
||||
server = return users
|
||||
|
||||
users :: [User]
|
||||
users = [ User 1 "Isaac" "Newton"
|
||||
, User 2 "Albert" "Einstein"
|
||||
]
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE EmptyDataDecls #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
|
||||
module Models (module Models,module Coupon) where
|
||||
|
||||
import Coupon
|
||||
import Data.Aeson
|
||||
import Data.Text
|
||||
import Data.Time.Clock
|
||||
import Database.Persist.TH
|
||||
import GHC.Generics
|
||||
|
||||
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
|
||||
Customer
|
||||
email Text
|
||||
UniqueEmail email
|
||||
Primary email
|
||||
deriving Eq Read Show Generic
|
||||
-- Product
|
||||
-- name Text
|
||||
-- price Int
|
||||
-- code Text
|
||||
-- UniqueName name
|
||||
-- Foreign Coupon fkcoupon code
|
||||
-- Primary name
|
||||
-- deriving Eq Read Show Generic
|
||||
Coupon json
|
||||
code Text
|
||||
value CouponType
|
||||
min_price Int
|
||||
customer_limit Int
|
||||
usage_limit Int
|
||||
valid_from UTCTime default=now()
|
||||
valid_till UTCTime default=now()
|
||||
UniqueCode code
|
||||
Primary code
|
||||
deriving Eq Read Show Generic
|
||||
|]
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
module SwaggerGen where
|
||||
|
||||
import Api
|
||||
import Control.Lens
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Types (camelTo2)
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL8
|
||||
import Data.Swagger
|
||||
import Servant.Swagger
|
||||
|
||||
modifier :: String -> String
|
||||
modifier = drop 1 . dropWhile (/= '_') . camelTo2 '_'
|
||||
|
||||
prefixSchemaOptions :: SchemaOptions
|
||||
prefixSchemaOptions = defaultSchemaOptions { fieldLabelModifier = modifier }
|
||||
|
||||
instance ToSchema BillCoupon where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions
|
||||
instance ToSchema CouponType where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions
|
||||
instance ToSchema Coupon where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions
|
||||
instance ToSchema Product where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions
|
||||
instance ToSchema Customer where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions
|
||||
instance ToSchema CouponResult where declareNamedSchema = genericDeclareNamedSchema prefixSchemaOptions
|
||||
|
||||
swaggerDoc :: Swagger
|
||||
swaggerDoc = toSwagger api
|
||||
& host ?~ "localhost:3000"
|
||||
& info.title .~ "Coupon Api"
|
||||
& info.version .~ "v1"
|
||||
|
||||
genSwaggerDoc :: IO ()
|
||||
genSwaggerDoc = BL8.writeFile "swagger.json" (encode swaggerDoc)
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# This file was automatically generated by 'stack init'
|
||||
#
|
||||
# Some commonly used options have been documented as comments in this file.
|
||||
# For advanced use and comprehensive documentation of the format, please see:
|
||||
# http://docs.haskellstack.org/en/stable/yaml_configuration/
|
||||
|
||||
# Resolver to choose a 'specific' stackage snapshot or a compiler version.
|
||||
# A snapshot resolver dictates the compiler version and the set of packages
|
||||
# to be used for project dependencies. For example:
|
||||
#
|
||||
# resolver: lts-3.5
|
||||
# resolver: nightly-2015-09-21
|
||||
# resolver: ghc-7.10.2
|
||||
# resolver: ghcjs-0.1.0_ghc-7.10.2
|
||||
# resolver:
|
||||
# name: custom-snapshot
|
||||
# location: "./custom-snapshot.yaml"
|
||||
resolver: lts-8.13
|
||||
|
||||
# User packages to be built.
|
||||
# Various formats can be used as shown in the example below.
|
||||
#
|
||||
# packages:
|
||||
# - some-directory
|
||||
# - https://example.com/foo/bar/baz-0.0.2.tar.gz
|
||||
# - location:
|
||||
# git: https://github.com/commercialhaskell/stack.git
|
||||
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a
|
||||
# extra-dep: true
|
||||
# subdirs:
|
||||
# - auto-update
|
||||
# - wai
|
||||
#
|
||||
# A package marked 'extra-dep: true' will only be built if demanded by a
|
||||
# non-dependency (i.e. a user package), and its test suites and benchmarks
|
||||
# will not be run. This is useful for tweaking upstream packages.
|
||||
packages:
|
||||
- '.'
|
||||
# Dependency packages to be pulled from upstream that are not in the resolver
|
||||
# (e.g., acme-missiles-0.3)
|
||||
extra-deps: []
|
||||
|
||||
# Override default flag values for local packages and extra-deps
|
||||
flags: {}
|
||||
|
||||
# Extra package databases containing global packages
|
||||
extra-package-dbs: []
|
||||
|
||||
# Control whether we use the GHC we find on the path
|
||||
# system-ghc: true
|
||||
#
|
||||
# Require a specific version of stack, using version ranges
|
||||
# require-stack-version: -any # Default
|
||||
# require-stack-version: ">=1.4"
|
||||
#
|
||||
# Override the architecture used by stack, especially useful on Windows
|
||||
# arch: i386
|
||||
# arch: x86_64
|
||||
#
|
||||
# Extra directories used by stack for building
|
||||
# extra-include-dirs: [/path/to/dir]
|
||||
# extra-lib-dirs: [/path/to/dir]
|
||||
#
|
||||
# Allow a newer minor version of GHC than the snapshot specifies
|
||||
# compiler-check: newer-minor
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"swagger":"2.0","info":{"version":"v1","title":"Coupon Api"},"host":"localhost:3000","paths":{"/coupon/add":{"post":{"consumes":["application/json"],"produces":["application/json"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/Coupon"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/Coupon"},"description":""}}}},"/coupon/get/{name}":{"get":{"produces":["application/json"],"parameters":[{"required":true,"in":"path","name":"name","type":"string"}],"responses":{"404":{"description":"`name` not found"},"200":{"schema":{"$ref":"#/definitions/Coupon"},"description":""}}}},"/coupon/del/{name}":{"get":{"produces":["application/json"],"parameters":[{"required":true,"in":"path","name":"name","type":"string"}],"responses":{"404":{"description":"`name` not found"},"200":{"schema":{"$ref":"#/definitions/Coupon"},"description":""}}}},"/billcoupon":{"post":{"consumes":["application/json"],"produces":["application/json"],"parameters":[{"required":true,"schema":{"$ref":"#/definitions/BillCoupon"},"in":"body","name":"body"}],"responses":{"400":{"description":"Invalid `body`"},"200":{"schema":{"$ref":"#/definitions/CouponResult"},"description":""}}}}},"definitions":{"Coupon":{"required":["code","value","min_price","valid_from","valid_till"],"properties":{"code":{"type":"string"},"value":{"$ref":"#/definitions/CouponType"},"min_price":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"valid_from":{"$ref":"#/definitions/UTCTime"},"valid_till":{"$ref":"#/definitions/UTCTime"}},"type":"object"},"CouponType":{"properties":{"ProductFlat":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"CartFlat":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"CartPercent":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"}},"maxProperties":1,"minProperties":1,"type":"object"},"UTCTime":{"example":"2016-07-22T00:00:00Z","format":"yyyy-mm-ddThh:MM:ssZ","type":"string"},"CouponResult":{"properties":{"Applied":{"maximum":9223372036854775807,"minimum":-9223372036854775808,"type":"integer"},"Rejected":{"type":"string"},"Partial":{"type":"string"}},"maxProperties":1,"minProperties":1,"type":"object"},"BillCoupon":{"required":["list"],"properties":{"list":{"items":{"type":"string"},"type":"array"}},"minItems":1,"items":[{"type":"string"}],"maxItems":1,"type":"object"}}}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Main (main) where
|
||||
|
||||
import Lib (app)
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
|
||||
main :: IO ()
|
||||
main = hspec spec
|
||||
|
||||
spec :: Spec
|
||||
spec = with (return app) $ do
|
||||
describe "GET /users" $ do
|
||||
it "responds with 200" $ do
|
||||
get "/users" `shouldRespondWith` 200
|
||||
it "responds with [User]" $ do
|
||||
let users = "[{\"userId\":1,\"userFirstName\":\"Isaac\",\"userLastName\":\"Newton\"},{\"userId\":2,\"userFirstName\":\"Albert\",\"userLastName\":\"Einstein\"}]"
|
||||
get "/users" `shouldRespondWith` users
|
||||
Loading…
Reference in New Issue