diff --git a/.gitignore b/.gitignore index 4c0d513..32d8d9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ *~ -.stack-work/ +.ghc.environment* +dist-newstyle/ +dist/ profiles.yaml result diff --git a/.hlint.yaml b/.hlint.yaml index 9ccda89..42b7af5 100644 --- a/.hlint.yaml +++ b/.hlint.yaml @@ -9,37 +9,37 @@ ####################### - modules: - - {name: Control.Monad.Error, within: []} - - {name: [Data.Aeson], as: Aeson} - - {name: Data.ByteString, as: B } - - {name: Data.ByteString.Char8, as: BC } - - {name: Data.ByteString.Lazy, as: BL } - - {name: Data.ByteString.Lazy.Char8, as: BLC } - - {name: Data.List, as: List } - - {name: Data.Text, as: T } - - {name: Data.Text.Lazy, as: TL } - - {name: Data.Text.Encoding, as: TE } + - { name: Control.Monad.Error, within: [] } + - { name: [Data.Aeson], as: Aeson } + - { name: Data.ByteString, as: B } + - { name: Data.ByteString.Char8, as: BC } + - { name: Data.ByteString.Lazy, as: BL } + - { name: Data.ByteString.Lazy.Char8, as: BLC } + - { name: Data.List, as: List } + - { name: Data.Text, as: T } + - { name: Data.Text.Lazy, as: TL } + - { name: Data.Text.Encoding, as: TE } ########################## # EXTENSION RESTRICTIONS # ########################## - extensions: - - default: false # All extension are banned by default - - name: # Only these listed extensions can be used - - DeriveGeneric - - GADTs - - OverloadedStrings - - QuasiQuotes - - RecordWildCards - - ScopedTypeVariables + - default: false # All extension are banned by default + - name: # Only these listed extensions can be used + - DeriveGeneric + - GADTs + - OverloadedStrings + - QuasiQuotes + - RecordWildCards + - ScopedTypeVariables ################ # CUSTOM RULES # ################ # Replace a $ b $ c with a . b $ c -- group: {name: dollar, enabled: true} +- group: { name: dollar, enabled: true } # Generalise map to fmap, ++ to <> -- group: {name: generalise, enabled: true} +- group: { name: generalise, enabled: true } diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c8d6d1a..0000000 --- a/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -## Define the base image: -FROM utdemir/ghc-musl:v24-ghc902 - -## Configure shell: -SHELL ["/bin/bash", "-o", "pipefail", "-c"] - -## Install upx: -RUN apk add --no-cache upx - -## Add application files: -COPY / /app - -## Change the working directory: -WORKDIR /app/ - -## Rename cabal.project.freeze: -RUN mv cabal.project.freeze.tmpl cabal.project.freeze - -## Update cabal: -RUN cabal update - -## Build static executable: -RUN cabal build --enable-executable-static && \ - cp "$(cabal list-bin decafcli)" /app/decafcli && \ - strip /app/decafcli && \ - upx /app/decafcli diff --git a/LICENSE b/LICENSE index d951ae4..ba0fb6e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2020-2022, Teloscube Pte Ltd +Copyright (c) 2020-2023, Teloscube Pte Ltd Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index 8c1c9ff..99e075a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# DECAF API Client Suite For Haskell +# DECAF CLI Application and Haskell Client Library -A Haskell client library to DECAF API and a small application demonstrating -library usage and providing some utilities for working with DECAF instances. +This repository mainly provides a Haskell client library for DECAF APIs and a +small application (1) demonstrating library usage and (2) providing some +utilities for working with DECAF Instances. > **TODO:** Provide full README. @@ -36,6 +37,7 @@ $ decafcli --help decafcli - DECAF Command-line Client Application Usage: decafcli [--version] COMMAND + DECAF Client Available options: @@ -101,19 +103,19 @@ nix-shell Build: ```sh -cabal build +cabal build -O0 ``` -Open VSCode if you wish to do so: +Test: ```sh -code . +cabal test -O0 ``` Run hlint: ```sh -hlint app/ src/ test/ +hlint app/ src/ tests/ ``` Format codebase: @@ -122,6 +124,12 @@ Format codebase: fourmolu -i app/ src/ test/ ``` +Run: + +```sh +cabal run -O0 decafcli -- --help +``` + ## Release Process ```console @@ -129,28 +137,15 @@ $ nix-shell [inside-nix-shell]$ ./release.sh -n ``` -... whereby `` follows [Haskell PVP -Specification](https://pvp.haskell.org/), such as `1.2.3.4`. Note that -there is no `v` prefix. +... where `` follows [Haskell PVP Specification], such as `1.2.3.4`. -## Static Compilation +Note that there is no `v` prefix. -We are able to statically build, strip and compress the executable inside a -Docker image. Following command performs the necessary compilation, copy the -file to host machine and inform the user about the path to the copied file: - -```sh -./compile-static.sh -``` +## License -> **IMPORTANT:** Note that everytime we update Nix sources or add a new -> dependency to Haskell project, we must regenerate the freeze file: -> -> ```sh -> cabal freeze -> mv cabal.project.freeze cabal.project.freeze.tmpl -> ``` +[BSD 3-Clause License]. Copyright (c) 2020-2023, Teloscube Pte Ltd. -## License + -[BSD 3-Clause License](./LICENSE). Copyright (c) 2020-2022, Teloscube Pte Ltd. +[BSD 3-Clause License]: ./LICENSE +[Haskell PVP Specification]: https://pvp.haskell.org diff --git a/Setup.hs b/Setup.hs deleted file mode 100644 index 9a994af..0000000 --- a/Setup.hs +++ /dev/null @@ -1,2 +0,0 @@ -import Distribution.Simple -main = defaultMain diff --git a/app/Decaf/Client/Cli/SubCommands/Tui.hs b/app/Decaf/Client/Cli/SubCommands/Tui.hs index 0ebf38e..b0fdd44 100644 --- a/app/Decaf/Client/Cli/SubCommands/Tui.hs +++ b/app/Decaf/Client/Cli/SubCommands/Tui.hs @@ -75,31 +75,31 @@ app = } -handleEvent :: TuiState -> Brick.BrickEvent () e -> Brick.EventM () (Brick.Next TuiState) -handleEvent s (Brick.VtyEvent ev) = case ev of - Vty.EvKey Vty.KEsc [] -> Brick.halt s - Vty.EvKey (Vty.KChar 'q') _ -> Brick.halt s - Vty.EvKey (Vty.KChar 'a') _ -> Brick.continue (s {tuiStateWorld = TuiStateWorldAbout}) - Vty.EvKey (Vty.KChar 'r') _ -> reloadTuiProfileInfos s >>= Brick.continue - Vty.EvKey _ _ -> Brick.continue s - Vty.EvMouseDown _n _i _but _mods -> Brick.continue s - Vty.EvMouseUp _n _i _mBut -> Brick.continue s - Vty.EvResize _n _i -> Brick.continue s - Vty.EvPaste _bs -> Brick.continue s - Vty.EvLostFocus -> Brick.continue s - Vty.EvGainedFocus -> Brick.continue s -handleEvent b _ = Brick.continue b +handleEvent :: Brick.BrickEvent () e -> Brick.EventM () TuiState () +handleEvent (Brick.VtyEvent ev) = case ev of + Vty.EvKey Vty.KEsc [] -> Brick.halt + Vty.EvKey (Vty.KChar 'q') _ -> Brick.halt + Vty.EvKey (Vty.KChar 'a') _ -> Brick.modify (\s -> s {tuiStateWorld = TuiStateWorldAbout}) + Vty.EvKey (Vty.KChar 'r') _ -> reloadTuiProfileInfos + Vty.EvKey _ _ -> pure () + Vty.EvMouseDown _n _i _but _mods -> pure () + Vty.EvMouseUp _n _i _mBut -> pure () + Vty.EvResize _n _i -> pure () + Vty.EvPaste _bs -> pure () + Vty.EvLostFocus -> pure () + Vty.EvGainedFocus -> pure () +handleEvent _ = pure () attrMap :: Brick.AttrMap attrMap = Brick.attrMap Vty.defAttr - [ ("info", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.black}) - , ("muted", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.cyan}) - , ("success", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.green}) - , ("warning", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.yellow}) - , ("failure", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.red}) + [ (Brick.attrName "info", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.black}) + , (Brick.attrName "muted", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.cyan}) + , (Brick.attrName "success", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.green}) + , (Brick.attrName "warning", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.yellow}) + , (Brick.attrName "failure", Vty.defAttr {Vty.attrForeColor = Vty.SetTo Vty.red}) ] @@ -159,20 +159,18 @@ mkProfilesTableRow TuiProfileInfo {..} = , widget tuiProfileInfoCountPolicy ] where - widget (Left err) = Brick.withAttr "failure" $ Brick.txt err - widget (Right sv) = Brick.withAttr "success" $ Brick.txt sv + widget (Left err) = Brick.withAttr (Brick.attrName "failure") $ Brick.txt err + widget (Right sv) = Brick.withAttr (Brick.attrName "success") $ Brick.txt sv -- * Event Logic -reloadTuiProfileInfos :: TuiState -> Brick.EventM () TuiState -reloadTuiProfileInfos state@TuiState {..} = do +reloadTuiProfileInfos :: Brick.EventM () TuiState () +reloadTuiProfileInfos = do + TuiState {..} <- Brick.get infos <- liftIO $ getTuiProfileInfos tuiStateProfiles - pure $ - state - { tuiStateWorld = TuiStateWorldProfileInfos infos - } + Brick.modify (\s -> s {tuiStateWorld = TuiStateWorldProfileInfos infos}) getTuiProfileInfos :: [DC.DecafProfile] -> IO [TuiProfileInfo] @@ -225,7 +223,7 @@ getBaristaVersion client = do getEstateVersion :: DC.DecafClient -> IO (Either T.Text T.Text) getEstateVersion client = do value <- DC.runDecafEstateJson (DC.path "version") client - case ACD.parseEither (ACD.at ["value"] ACD.auto) value of + case ACD.parseEither ACD.text value of Left err -> pure $ Left (T.pack err) Right sv -> pure $ Right sv diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..1b97420 --- /dev/null +++ b/cabal.project @@ -0,0 +1,2 @@ +packages: . +write-ghc-environment-files: always diff --git a/cabal.project.freeze.tmpl b/cabal.project.freeze.tmpl deleted file mode 100644 index 085512a..0000000 --- a/cabal.project.freeze.tmpl +++ /dev/null @@ -1,188 +0,0 @@ -active-repositories: hackage.haskell.org:merge -constraints: any.HUnit ==1.6.2.0, - any.OneTuple ==0.3.1, - any.QuickCheck ==2.14.2, - any.StateVar ==1.2.2, - any.aeson ==2.0.3.0, - any.aeson-combinators ==0.1.0.1, - any.ansi-terminal ==0.11.3, - any.ansi-wl-pprint ==0.6.9, - any.appar ==0.1.8, - any.array ==0.5.4.0, - any.asn1-encoding ==0.9.6, - any.asn1-parse ==0.9.5, - any.asn1-types ==0.3.4, - any.assoc ==1.0.2, - any.async ==2.2.4, - any.attoparsec ==0.14.4, - any.auto-update ==0.1.6, - any.base ==4.15.1.0, - any.base-compat ==0.11.2, - any.base-compat-batteries ==0.11.2, - any.base-orphans ==0.8.7, - any.base16-bytestring ==1.0.2.0, - any.base64-bytestring ==1.2.1.0, - any.basement ==0.0.15, - any.bifunctors ==5.5.13, - any.binary ==0.8.8.0, - any.blaze-builder ==0.4.2.2, - any.blaze-html ==0.9.1.2, - any.blaze-markup ==0.8.2.8, - any.brick ==0.68.1, - any.bsb-http-chunked ==0.0.0.4, - any.byteorder ==1.0.4, - any.bytestring ==0.10.12.1, - any.call-stack ==0.4.0, - any.case-insensitive ==1.2.1.0, - any.cereal ==0.5.8.3, - any.clock ==0.8.3, - any.code-page ==0.2.1, - any.colour ==2.3.6, - any.comonad ==5.0.8, - any.conduit ==1.3.4.2, - any.conduit-extra ==1.3.6, - any.config-ini ==0.2.4.0, - any.connection ==0.3.1, - any.containers ==0.6.4.1, - any.contravariant ==1.5.5, - any.cookie ==0.4.5, - any.cryptonite ==0.29, - any.data-clist ==0.2, - any.data-default-class ==0.1.2.0, - any.data-fix ==0.3.2, - any.deepseq ==1.4.5.0, - any.directory ==1.3.6.2, - any.distributive ==0.6.2.1, - any.dlist ==1.0, - any.doctest ==0.18.2, - any.easy-file ==0.2.2, - any.exceptions ==0.10.4, - any.fail ==4.9.0.0, - any.fast-logger ==3.1.1, - any.filepath ==1.4.2.1, - any.ghc ==9.0.2, - any.ghc-bignum ==1.1, - any.ghc-boot ==9.0.2, - any.ghc-boot-th ==9.0.2, - any.ghc-heap ==9.0.2, - any.ghc-paths ==0.1.0.12, - any.ghc-prim ==0.7.0, - any.ghci ==9.0.2, - any.hashable ==1.3.5.0, - any.haskell-src-exts ==1.23.1, - any.haskell-src-meta ==0.8.11, - any.hourglass ==0.2.12, - any.hpc ==0.6.1.0, - any.hspec ==2.8.5, - any.hspec-core ==2.8.5, - any.hspec-discover ==2.8.5, - any.hspec-expectations ==0.8.2, - any.http-client ==0.7.13.1, - any.http-client-tls ==0.3.6.1, - any.http-conduit ==2.3.8, - any.http-date ==0.0.11, - any.http-types ==0.12.3, - any.http2 ==3.0.3, - any.indexed-traversable ==0.1.2, - any.indexed-traversable-instances ==0.1.1.1, - any.integer-gmp ==1.1, - any.integer-logarithms ==1.0.3.1, - any.iproute ==1.7.12, - any.libyaml ==0.1.2, - any.megaparsec ==9.2.2, - any.memory ==0.16.0, - any.microlens ==0.4.12.0, - any.microlens-mtl ==0.2.0.2, - any.microlens-th ==0.4.3.10, - any.mime-types ==0.1.1.0, - any.monad-control ==1.0.3.1, - any.monad-parallel ==0.7.2.5, - any.mono-traversable ==1.0.15.3, - any.mtl ==2.2.2, - any.nats ==1.1.2, - any.network ==3.1.2.7, - any.network-byte-order ==0.1.6, - any.network-uri ==2.6.4.1, - any.old-locale ==1.0.0.7, - any.old-time ==1.1.0.3, - any.optparse-applicative ==0.16.1.0, - any.parallel ==3.2.2.0, - any.parsec ==3.1.14.0, - any.parser-combinators ==1.3.0, - any.pem ==0.2.4, - any.pretty ==1.1.3.6, - any.primitive ==0.7.3.0, - any.process ==1.6.13.2, - any.psqueues ==0.2.7.3, - any.quickcheck-io ==0.2.0, - any.random ==1.2.1.1, - any.regex-base ==0.94.0.2, - any.regex-compat ==0.95.2.1, - any.regex-posix ==0.96.0.1, - any.resourcet ==1.2.6, - any.rts ==1.0.2, - any.safe ==0.3.19, - any.scientific ==0.3.7.0, - any.scotty ==0.12, - any.semialign ==1.2.0.1, - any.semigroupoids ==5.3.7, - any.setenv ==0.1.1.3, - any.simple-sendfile ==0.2.30, - any.socks ==0.6.1, - any.split ==0.2.3.5, - any.splitmix ==0.1.0.4, - any.stm ==2.5.0.0, - any.streaming-commons ==0.2.2.4, - any.strict ==0.4.0.1, - any.string-interpolate ==0.3.1.2, - any.syb ==0.7.2.2, - any.tagged ==0.8.6.1, - any.template-haskell ==2.17.0.0, - any.terminfo ==0.4.1.5, - any.text ==1.2.5.0, - any.text-conversions ==0.3.1.1, - any.text-short ==0.1.5, - any.text-zipper ==0.11, - any.tf-random ==0.5, - any.th-abstraction ==0.4.5.0, - any.th-compat ==0.1.4, - any.th-expand-syns ==0.4.10.0, - any.th-lift ==0.8.2, - any.th-orphans ==0.13.14, - any.th-reify-many ==0.1.10, - any.these ==1.1.1.1, - any.time ==1.9.3, - any.time-compat ==1.9.6.1, - any.time-manager ==0.0.0, - any.tls ==1.5.8, - any.transformers ==0.5.6.2, - any.transformers-base ==0.4.6, - any.transformers-compat ==0.6.6, - any.typed-process ==0.2.10.1, - any.unix ==2.7.2.2, - any.unix-compat ==0.5.4, - any.unix-time ==0.4.8, - any.unliftio ==0.2.22.0, - any.unliftio-core ==0.2.0.1, - any.unordered-containers ==0.2.17.0, - any.utf8-string ==1.0.2, - any.uuid-types ==1.0.5, - any.vault ==0.3.1.5, - any.vector ==0.12.3.1, - any.vector-algorithms ==0.8.0.4, - any.void ==0.7.3, - any.vty ==5.33, - any.wai ==3.2.3, - any.wai-extra ==3.1.12.1, - any.wai-logger ==2.4.0, - any.warp ==3.3.21, - any.witherable ==0.4.2, - any.word-wrap ==0.5, - any.word8 ==0.1.3, - any.x509 ==1.7.7, - any.x509-store ==1.6.9, - any.x509-system ==1.6.7, - any.x509-validation ==1.6.12, - any.yaml ==0.11.8.0, - any.zlib ==0.6.3.0 -index-state: hackage.haskell.org 2022-09-28T00:41:23Z diff --git a/compile-static.sh b/compile-static.sh deleted file mode 100755 index 955a71d..0000000 --- a/compile-static.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -## Build the Docker image statically: -docker build -t decafcli . >&2 - -## Create a Docker container from the Docker image built above and get its identifier: -_id="$(docker create decafcli)" - -## Create a temporary directory and define the output path: -_path="$(mktemp -d)/decafcli" - -## Copy the statically compiled executable: -docker cp "${_id}:/app/decafcli" "${_path}" - -## Remove the container: -docker rm -v "${_id}" >&2 - -## Inform the enduser about the path: -echo "${_path}" diff --git a/decaf-client.cabal b/decaf-client.cabal index c1fed50..a4c595f 100644 --- a/decaf-client.cabal +++ b/decaf-client.cabal @@ -1,6 +1,6 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.34.7. +-- This file has been generated from package.yaml by hpack version 0.35.2. -- -- see: https://github.com/sol/hpack @@ -13,7 +13,7 @@ homepage: https://github.com/teloscube/decaf-client-haskell#readme bug-reports: https://github.com/teloscube/decaf-client-haskell/issues author: Vehbi Sinan Tunalioglu maintainer: vst@vsthost.com -copyright: 2020-2022 Teloscube Pte Ltd +copyright: 2020-2023 Teloscube Pte Ltd license: BSD3 license-file: LICENSE build-type: Simple @@ -49,6 +49,7 @@ library , bytestring , case-insensitive , exceptions + , http-client , http-conduit , http-types , network-uri @@ -97,11 +98,12 @@ test-suite decaf-client-doctest other-modules: Paths_decaf_client hs-source-dirs: - ./ - ghc-options: -Wall -Wunused-packages -threaded + tests/doctest + ghc-options: -Wall -threaded build-depends: base >=4.7 && <5 - , doctest + , decaf-client + , doctest-parallel default-language: Haskell2010 test-suite decaf-client-test @@ -111,7 +113,7 @@ test-suite decaf-client-test Mocking Paths_decaf_client hs-source-dirs: - test + tests/test ghc-options: -Wall -Wunused-packages -threaded -rtsopts -with-rtsopts=-N build-depends: aeson diff --git a/default.nix b/default.nix index ca7d167..d13d26a 100644 --- a/default.nix +++ b/default.nix @@ -1,3 +1,137 @@ -{ ... }: +{ sources ? import ./nix/sources.nix +, compiler ? "default" +, system ? builtins.currentSystem +, dockerImageRegistry ? "registry.docker.decafhub.com" +, dockerImageRepository ? null +, dockerImageTag ? null +, ... +}: -(import ./nix { }).thisPackageInstallable +let + ################## + ## LOAD NIXPKGS ## + ################## + + ## Import nixpkgs pinned by niv: + pkgs = import sources.nixpkgs { inherit system; }; + + ################## + ## LOAD HELPERS ## + ################## + + ## Load the YAML reader: + readYAML = pkgs.callPackage ./nix/lib/read-yaml.nix { }; + + ## Load Haskell package factory: + mkHaskell = pkgs.callPackage ./nix/lib/mk-haskell.nix { }; + + ## Load Haskell application factory: + mkHaskellApp = pkgs.callPackage ./nix/lib/mk-haskell-app.nix { }; + + ## Load Docker image factory for Haskell application: + mkHaskellDocker = pkgs.callPackage ./nix/lib/mk-haskell-docker.nix { }; + + ############# + ## HASKELL ## + ############# + + ## Get Haskell packages in the project: + thisHaskellPackages = { + main = { + name = "decaf-client"; + path = ./.; + }; + subs = [ ]; + }; + + ## Get Haskell packages in the project as a list: + thisHaskellPackagesAll = [ thisHaskellPackages.main ] ++ thisHaskellPackages.subs; + + ## Get the main Haskell package specification: + packageSpec = readYAML (thisHaskellPackages.main.path + "/package.yaml"); + + ## Get base Haskell package set: + baseHaskell = if compiler == "default" then pkgs.haskellPackages else pkgs.haskell.packages.${compiler}; + + ## Get this Haskell package set: + thisHaskell = mkHaskell { + haskell = baseHaskell; + packages = thisHaskellPackagesAll; + overrides = self: super: { }; + }; + + ########### + ## SHELL ## + ########### + + ## Prepare Nix shell: + thisShell = thisHaskell.shellFor { + ## Define packages for the shell: + packages = p: builtins.map (x: p.${x.name}) thisHaskellPackagesAll; + + ## Enable Hoogle: + withHoogle = false; + + ## Build inputs for development shell: + buildInputs = [ + ## Haskell related build inputs: + thisHaskell.apply-refact + thisHaskell.cabal-fmt + thisHaskell.cabal-install + thisHaskell.cabal2nix + thisHaskell.fourmolu + thisHaskell.haskell-language-server + thisHaskell.hlint + thisHaskell.hpack + thisHaskell.weeder + + ## Other build inputs for various development requirements: + pkgs.docker-client + pkgs.git + pkgs.git-chglog + ]; + + ## Shell hook for development shell: + shellHook = '' + ## Environment variables: + export PROJECT_DEV_ROOT="$(git rev-parse --show-toplevel)" + + ## Shell aliases: + alias riched="${pkgs.rich-cli}/bin/rich --emoji --center --width 120 --panel rounded --theme rrt --hyperlinks" + alias devsh-help="riched --pager ''${PROJECT_DEV_ROOT}/README.md" + alias devsh-welcome="riched ''${PROJECT_DEV_ROOT}/README.md" + alias devsh-makedev="hpack && fourmolu -i app/ src/ test/ && hlint app/ src/ test/ && cabal build -O0 && cabal v1-test" + + ## Greet: + devsh-welcome + echo + echo '**Run devsh-welcome to see Welcome notice again**' | riched -m - + echo '**Run devsh-help to see help notice**' | riched -m - + ''; + }; + + ################# + ## APPLICATION ## + ################# + + ## Get the installable application (only static executable): + thisApp = mkHaskellApp { + drv = thisHaskell.${thisHaskellPackages.main.name}; + }; + + ############ + ## DOCKER ## + ############ + + thisDocker = mkHaskellDocker { + app = thisApp; + registry = dockerImageRegistry; + repository = if isNull dockerImageRepository then packageSpec.name else dockerImageRepository; + tag = if isNull dockerImageTag then packageSpec.version else dockerImageTag; + }; +in +if pkgs.lib.inNixShell then thisShell else { + shell = thisShell; + app = thisApp; + docker = thisDocker; +} diff --git a/nix/default.nix b/nix/default.nix deleted file mode 100644 index 0fe1bb4..0000000 --- a/nix/default.nix +++ /dev/null @@ -1,60 +0,0 @@ -{ compiler ? "ghc90" -, ... -}: - -let - ## Import sources: - sources = import ./sources.nix; - - ## Import telosnix: - telosnix = import sources.telosnix { }; - - ## Import nixpkgs: - pkgs = import telosnix.pkgs-sources.unstable { }; - - ## Get Haskell for package development purposes: - haskell = telosnix.tools.haskell.getHaskell - { - pkgs = pkgs; - compiler = compiler; - }; - - ## Get this package: - thisPackage = haskell.callCabal2nixWithOptions "decaf-client" ../. "" { }; - - ## Get this package's Haskell dependencies: - thisPackageDeps = pkgs.haskell.lib.compose.getHaskellBuildInputs thisPackage; - - ## Get our GHC for development: - thisGhc = haskell.ghcWithPackages (_: thisPackageDeps); - - ## Get Haskell development tools: - haskell-dev-tools = with haskell; - [ - ## Our GHC with all packages required to build and test our package: - thisGhc - - ## Various haskell tools: - apply-refact - cabal-install - cabal2nix - fourmolu - haskell-language-server - hlint - hpack - ]; - - ## Get the installable package: - thisPackageInstallable = pkgs.haskell.lib.dontCheck (pkgs.haskell.lib.justStaticExecutables thisPackage); -in -{ - sources = sources; - telosnix = telosnix; - pkgs = pkgs; - haskell = haskell; - thisPackage = thisPackage; - thisPackageDeps = thisPackageDeps; - ghc = thisGhc; - haskell-dev-tools = haskell-dev-tools; - thisPackageInstallable = thisPackageInstallable; -} diff --git a/nix/lib/mk-haskell-app.nix b/nix/lib/mk-haskell-app.nix new file mode 100644 index 0000000..0ac1d68 --- /dev/null +++ b/nix/lib/mk-haskell-app.nix @@ -0,0 +1,31 @@ +{ pkgs, ... }: + +## Function that makes a Haskell application. +{ drv +, name ? drv.pname +, nativeBuildInputs ? [ ] +, binPaths ? [ ] +}: +let + ## We need these inputs at buildtime: + extraNativeBuildInputs = [ + pkgs.git + pkgs.makeWrapper + ] ++ nativeBuildInputs; + + ## We need these inputs at runtime: + binPath = pkgs.lib.makeBinPath binPaths; + + ## Post-fixup process: + extraPostFixup = '' + wrapProgram $out/bin/${name} --prefix PATH : ${binPath} + ''; +in +pkgs.haskell.lib.justStaticExecutables ( + drv.overrideAttrs (oldAttrs: + rec { + nativeBuildInputs = (oldAttrs.nativeBuildInputs or [ ]) ++ extraNativeBuildInputs; + postFixup = (oldAttrs.postFixup or "") + extraPostFixup; + } + ) +) diff --git a/nix/lib/mk-haskell-docker.nix b/nix/lib/mk-haskell-docker.nix new file mode 100644 index 0000000..f0d7359 --- /dev/null +++ b/nix/lib/mk-haskell-docker.nix @@ -0,0 +1,38 @@ +{ pkgs, ... }: + +## Function that makes a Docker image for the given Haskell application. +{ app +, name ? app.pname +, registry +, repository +, tag +, entrypoint ? "${app}/bin/${name}" +, cmd ? null +}: +pkgs.dockerTools.buildImage { + name = registry + "/" + repository; + tag = tag; + created = "now"; + + copyToRoot = pkgs.buildEnv { + name = "image-root"; + paths = [ pkgs.cacert ]; + pathsToLink = [ "/etc" ]; + }; + + runAsRoot = '' + #!${pkgs.runtimeShell} + ${pkgs.dockerTools.shadowSetup} + mkdir /tmp + chmod 777 /tmp + groupadd -r users + useradd -r -g users patron + ''; + + config = { + Entrypoint = [ "${app}/bin/${name}" ]; + Cmd = cmd; + + User = "patron"; + }; +} diff --git a/nix/lib/mk-haskell.nix b/nix/lib/mk-haskell.nix new file mode 100644 index 0000000..64514be --- /dev/null +++ b/nix/lib/mk-haskell.nix @@ -0,0 +1,15 @@ +{ pkgs, ... }: + +## Function that makes a Haskell. +{ haskell +, packages +, overrides +}: +let + packageFromSpec = self: { name, path }: { + ${name} = self.callCabal2nix name path { }; + }; +in +haskell.override { + overrides = self: super: builtins.foldl' (a: c: a // packageFromSpec self c) (overrides self super) packages; +} diff --git a/nix/lib/read-yaml.nix b/nix/lib/read-yaml.nix new file mode 100644 index 0000000..d89721a --- /dev/null +++ b/nix/lib/read-yaml.nix @@ -0,0 +1,27 @@ +## This file is a verbatim copy of: +## +## https://github.com/cdepillabout/stacklock2nix/blob/8408f57e929ca713e508f45dc3d846eca20c3379/nix/build-support/stacklock2nix/read-yaml.nix + +{ runCommand, remarshal }: + +# Read a YAML file into a Nix datatype using IFD. +# +# Similar to: +# +# > builtins.fromJSON (builtins.readFile ./somefile) +# +# but takes an input file in YAML instead of JSON. +# +# readYAML :: Path -> a +# +# where `a` is the Nixified version of the input file. +path: + +let + jsonOutputDrv = + runCommand + "from-yaml" + { nativeBuildInputs = [ remarshal ]; } + "remarshal -if yaml -i \"${path}\" -of json -o \"$out\""; +in +builtins.fromJSON (builtins.readFile jsonOutputDrv) diff --git a/nix/sources.json b/nix/sources.json index d842c0e..2f2a3df 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -1,14 +1,14 @@ { - "telosnix": { - "branch": "v0.0.4", - "description": null, + "nixpkgs": { + "branch": "release-23.05", + "description": "Nix Packages collection", "homepage": null, - "owner": "telostat", - "repo": "telos.nix", - "rev": "4d45ee0f57fb6979781b726cdf87eb68f4ef672d", - "sha256": "0my9pmbvyfpj3n747zmwhl6fk8mhnmirl18zmlpy29g1pcd891k2", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "79947b703cc6937356ae1a9fdbca9d906e035ee1", + "sha256": "1rjd62bmpa7nzg9hz1f3ijbvwj5widwgpak89f3l9vfagpjph82f", "type": "tarball", - "url": "https://github.com/telostat/telos.nix/archive/4d45ee0f57fb6979781b726cdf87eb68f4ef672d.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/79947b703cc6937356ae1a9fdbca9d906e035ee1.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/nix/sources.nix b/nix/sources.nix index 9a01c8a..fe3dadf 100644 --- a/nix/sources.nix +++ b/nix/sources.nix @@ -10,33 +10,34 @@ let let name' = sanitizeName name + "-src"; in - if spec.builtin or true then - builtins_fetchurl { inherit (spec) url sha256; name = name'; } - else - pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; + if spec.builtin or true then + builtins_fetchurl { inherit (spec) url sha256; name = name'; } + else + pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; fetch_tarball = pkgs: name: spec: let name' = sanitizeName name + "-src"; in - if spec.builtin or true then - builtins_fetchTarball { name = name'; inherit (spec) url sha256; } - else - pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; + if spec.builtin or true then + builtins_fetchTarball { name = name'; inherit (spec) url sha256; } + else + pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; fetch_git = name: spec: let ref = - if spec ? ref then spec.ref else + spec.ref or ( if spec ? branch then "refs/heads/${spec.branch}" else - if spec ? tag then "refs/tags/${spec.tag}" else - abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"; - submodules = if spec ? submodules then spec.submodules else false; + if spec ? tag then "refs/tags/${spec.tag}" else + abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!" + ); + submodules = spec.submodules or false; submoduleArg = let nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; emptyArgWithWarning = - if submodules == true + if submodules then builtins.trace ( @@ -44,15 +45,15 @@ let + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " + "does not support them" ) - {} - else {}; + { } + else { }; in - if nixSupportsSubmodules - then { inherit submodules; } - else emptyArgWithWarning; + if nixSupportsSubmodules + then { inherit submodules; } + else emptyArgWithWarning; in - builtins.fetchGit - ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); + builtins.fetchGit + ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); fetch_local = spec: spec.path; @@ -86,16 +87,16 @@ let hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; hasThisAsNixpkgsPath = == ./.; in - if builtins.hasAttr "nixpkgs" sources - then sourcesNixpkgs - else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then - import {} - else - abort - '' - Please specify either (through -I or NIX_PATH=nixpkgs=...) or - add a package called "nixpkgs" to your sources.json. - ''; + if builtins.hasAttr "nixpkgs" sources + then sourcesNixpkgs + else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then + import { } + else + abort + '' + Please specify either (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; # The actual fetching function. fetch = pkgs: name: spec: @@ -115,13 +116,13 @@ let # the path directly as opposed to the fetched source. replace = name: drv: let - saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; + saneName = stringAsChars (c: if (builtins.match "[a-zA-Z0-9]" c) == null then "_" else c) name; ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; in - if ersatz == "" then drv else - # this turns the string into an actual Nix path (for both absolute and - # relative paths) - if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; + if ersatz == "" then drv else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; # Ports of functions for older nix versions @@ -132,7 +133,7 @@ let ); # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 - range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); + range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); @@ -143,43 +144,46 @@ let concatStrings = builtins.concatStringsSep ""; # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 - optionalAttrs = cond: as: if cond then as else {}; + optionalAttrs = cond: as: if cond then as else { }; # fetchTarball version that is compatible between all the versions of Nix builtins_fetchTarball = { url, name ? null, sha256 }@attrs: let inherit (builtins) lessThan nixVersion fetchTarball; in - if lessThan nixVersion "1.12" then - fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) - else - fetchTarball attrs; + if lessThan nixVersion "1.12" then + fetchTarball ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else + fetchTarball attrs; # fetchurl version that is compatible between all the versions of Nix builtins_fetchurl = { url, name ? null, sha256 }@attrs: let inherit (builtins) lessThan nixVersion fetchurl; in - if lessThan nixVersion "1.12" then - fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) - else - fetchurl attrs; + if lessThan nixVersion "1.12" then + fetchurl ({ inherit url; } // (optionalAttrs (name != null) { inherit name; })) + else + fetchurl attrs; # Create the final "sources" from the config mkSources = config: - mapAttrs ( - name: spec: - if builtins.hasAttr "outPath" spec - then abort - "The values in sources.json should not have an 'outPath' attribute" - else - spec // { outPath = replace name (fetch config.pkgs name spec); } - ) config.sources; + mapAttrs + ( + name: spec: + if builtins.hasAttr "outPath" spec + then + abort + "The values in sources.json should not have an 'outPath' attribute" + else + spec // { outPath = replace name (fetch config.pkgs name spec); } + ) + config.sources; # The "config" used by the fetchers mkConfig = { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null - , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile) + , sources ? if sourcesFile == null then { } else builtins.fromJSON (builtins.readFile sourcesFile) , system ? builtins.currentSystem , pkgs ? mkPkgs sources system }: rec { @@ -191,4 +195,4 @@ let }; in -mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } +mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/package.yaml b/package.yaml index a8b65d6..48fb56c 100644 --- a/package.yaml +++ b/package.yaml @@ -1,96 +1,97 @@ -name: decaf-client -version: 0.0.1 -github: "teloscube/decaf-client-haskell" -license: BSD3 -author: "Vehbi Sinan Tunalioglu" -maintainer: "vst@vsthost.com" -copyright: "2020-2022 Teloscube Pte Ltd" +name: decaf-client +version: 0.0.1 +github: "teloscube/decaf-client-haskell" +license: BSD3 +author: "Vehbi Sinan Tunalioglu" +maintainer: "vst@vsthost.com" +copyright: "2020-2023 Teloscube Pte Ltd" extra-source-files: -- README.md -- CHANGELOG.md + - README.md + - CHANGELOG.md -synopsis: DECAF API Client Suite for Haskell -category: Web +synopsis: DECAF API Client Suite for Haskell +category: Web -description: Please see the README on GitHub at +description: Please see the README on GitHub at dependencies: -- base >= 4.7 && < 5 + - base >= 4.7 && < 5 library: source-dirs: src ghc-options: - - -Wall - - -Wunused-packages - dependencies: - - aeson - - base64-bytestring - - bytestring - - case-insensitive - - exceptions - - http-conduit - - http-types - - network-uri - - text - - yaml - -executables: - decafcli: - main: Main.hs - source-dirs: app - ghc-options: - -Wall - -Wunused-packages - - -threaded - - -rtsopts - - -with-rtsopts=-N - dependencies: + dependencies: - aeson - - aeson-combinators - - blaze-html - - blaze-markup - - brick + - base64-bytestring - bytestring - - data-default-class - - decaf-client + - case-insensitive - exceptions + - http-client + - http-conduit - http-types - - monad-parallel - - optparse-applicative - - scotty - - string-interpolate + - network-uri - text - - vector - - vty - - warp - yaml +executables: + decafcli: + main: Main.hs + source-dirs: app + ghc-options: + - -Wall + - -Wunused-packages + - -threaded + - -rtsopts + - -with-rtsopts=-N + dependencies: + - aeson + - aeson-combinators + - blaze-html + - blaze-markup + - brick + - bytestring + - data-default-class + - decaf-client + - exceptions + - http-types + - monad-parallel + - optparse-applicative + - scotty + - string-interpolate + - text + - vector + - vty + - warp + - yaml + tests: decaf-client-test: - main: Main.hs - source-dirs: test + main: Main.hs + source-dirs: tests/test ghc-options: - - -Wall - - -Wunused-packages - - -threaded - - -rtsopts - - -with-rtsopts=-N + - -Wall + - -Wunused-packages + - -threaded + - -rtsopts + - -with-rtsopts=-N dependencies: - - aeson - - bytestring - - decaf-client - - exceptions - - hspec - - text - - unordered-containers + - aeson + - bytestring + - decaf-client + - exceptions + - hspec + - text + - unordered-containers decaf-client-doctest: - main: doctest.hs - source-dirs: . + main: doctest.hs + source-dirs: tests/doctest ghc-options: - - -Wall - - -Wunused-packages - - -threaded + - -Wall + - -threaded dependencies: - - doctest + - decaf-client + - doctest-parallel diff --git a/release.sh b/release.sh index b179e91..d8e87cf 100755 --- a/release.sh +++ b/release.sh @@ -73,23 +73,10 @@ git commit -m "chore(release): ${_version}" _log "Tagging version..." git tag -a -m "Release ${_version}" "${_version}" -_log "Build statically linked executable..." -_outfile_raw="$(./compile-static.sh)" - -_log "Copying and renaming statically linked executable (${_outfile_raw})..." -_outfile="decafcli-Linux-x86_64-static" -cp "${_outfile_raw}" "${_outfile}" - _log "Pushing changes to remote..." git push --follow-tags origin main _log "Creating the release..." gh release create "${_version}" --title "v${_version}" --generate-notes -_log "Uploading release artifacts..." -gh release upload "${_version}" "${_outfile}" - -_log "Cleaning up release artifacts..." -rm "${_outfile}" - _log "Finished!" diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 06b50ab..0000000 --- a/shell.nix +++ /dev/null @@ -1,30 +0,0 @@ -{ ... }: - -let - ## Import this codebase's Nix helper set: - nix = import ./nix { }; - - ## Get packages: - pkgs = nix.pkgs; -in -pkgs.mkShell { - buildInputs = [ - ## Fancy stuff: - pkgs.figlet - pkgs.lolcat - - ## Release stuff: - pkgs.busybox - pkgs.gh - pkgs.git - pkgs.git-chglog - ] ++ nix.haskell-dev-tools; - - shellHook = '' - figlet -w 999 "DECAF CLIENT DEV SHELL" | lolcat -S 42 - - ## Make sure that doctest finds correct GHC executable and libraries: - export NIX_GHC=${nix.ghc}/bin/ghc - export NIX_GHC_LIBDIR=${nix.ghc}/lib/${nix.ghc.meta.name} - ''; -} diff --git a/src/Decaf/Client/DecafCredentials.hs b/src/Decaf/Client/DecafCredentials.hs index f578aed..7587b18 100644 --- a/src/Decaf/Client/DecafCredentials.hs +++ b/src/Decaf/Client/DecafCredentials.hs @@ -9,6 +9,11 @@ import Decaf.Client.Internal.Utils (commonAesonOptions) import GHC.Generics (Generic) +-- $setup +-- +-- >>> :set -XOverloadedStrings + + -- | Data definition for available DECAF credentials types. -- -- >>> Data.Aeson.encode (DecafCredentialsHeader "some-header-value") diff --git a/src/Decaf/Client/DecafProfile.hs b/src/Decaf/Client/DecafProfile.hs index f7d2eeb..c474d20 100644 --- a/src/Decaf/Client/DecafProfile.hs +++ b/src/Decaf/Client/DecafProfile.hs @@ -21,6 +21,12 @@ import GHC.Generics (Generic) import GHC.Stack (HasCallStack) +-- $setup +-- +-- >>> :set -XOverloadedStrings +-- >>> :set -XTypeApplications + + -- | A DECAF Instance user profile for constructing a -- 'Decaf.Client.DecafClient.DecafClient'. -- diff --git a/src/Decaf/Client/DecafRemote.hs b/src/Decaf/Client/DecafRemote.hs index 3bf7e9d..6236254 100644 --- a/src/Decaf/Client/DecafRemote.hs +++ b/src/Decaf/Client/DecafRemote.hs @@ -12,6 +12,11 @@ import qualified Network.URI as U import Text.Read (readMaybe) +-- $setup +-- +-- >>> :set -XOverloadedStrings + + -- | Type definition for addressing a remote DECAF Instance. -- -- >>> DecafRemote "example.com" Nothing False diff --git a/src/Decaf/Client/DecafRequest.hs b/src/Decaf/Client/DecafRequest.hs index a78a6b1..67f6de7 100644 --- a/src/Decaf/Client/DecafRequest.hs +++ b/src/Decaf/Client/DecafRequest.hs @@ -14,6 +14,7 @@ import Decaf.Client.DecafCredentials (DecafCredentials (DecafCredentialsHeader)) import Decaf.Client.DecafRemote (DecafRemote (..), remoteToUrl) import Decaf.Client.Internal.Utils (commonAesonOptions, dropTrailing) import GHC.Generics (Generic) +import qualified Network.HTTP.Client.MultipartFormData as Http.Client import Network.HTTP.Types ( Header, QueryText, @@ -24,6 +25,11 @@ import Paths_decaf_client (version) import Text.Printf (printf) +-- $setup +-- +-- >>> :set -XOverloadedStrings + + -- * Data Definitions @@ -42,6 +48,7 @@ data DecafRequest = DecafRequest , decafRequestTrailingSlash :: !Bool , decafRequestQuery :: !QueryText , decafRequestPayload :: !(Maybe DecafRequestPayload) + , decafRequestParts :: !(Maybe [Http.Client.Part]) , decafRequestCheckResponse :: !Bool } @@ -61,6 +68,7 @@ instance Show DecafRequest where , " decafRequestTrailingSlash = " <> show (decafRequestTrailingSlash x) , " decafRequestQuery = " <> show (decafRequestQuery x) , " decafRequestPayload = " <> show (decafRequestPayload x) + , " decafRequestParts = " <> maybe "None" (const "Some") (decafRequestParts x) , " decafRequestCheckResponse = " <> show (decafRequestCheckResponse x) , "}" ] @@ -164,6 +172,8 @@ decafGraphqlQueryNoVars = flip MkDecafGraphqlQuery (Aeson.object []) -- | Initializes a request with DECAF Instance URL and authentication credentials. -- +-- >>> import Decaf.Client.DecafRemote (DecafRemote(..)) +-- >>> import Decaf.Client.DecafCredentials (DecafCredentials(..)) -- >>> initRequest (DecafRemote "example.com" Nothing False) (DecafCredentialsHeader "OUCH") -- DecafRequest { -- decafRequestRemote = http://example.com @@ -176,6 +186,7 @@ decafGraphqlQueryNoVars = flip MkDecafGraphqlQuery (Aeson.object []) -- decafRequestTrailingSlash = False -- decafRequestQuery = [] -- decafRequestPayload = Nothing +-- decafRequestParts = None -- decafRequestCheckResponse = True -- } initRequest :: DecafRemote -> DecafCredentials -> DecafRequest @@ -198,6 +209,7 @@ defaultRequest = , decafRequestTrailingSlash = False , decafRequestQuery = [] , decafRequestPayload = Nothing + , decafRequestParts = Nothing , decafRequestCheckResponse = True } @@ -273,6 +285,7 @@ remote = setRemote -- decafRequestTrailingSlash = False -- decafRequestQuery = [] -- decafRequestPayload = Nothing +-- decafRequestParts = None -- decafRequestCheckResponse = True -- } setNamespace :: DecafRequestPath -> DecafRequestCombinator @@ -294,6 +307,7 @@ setNamespace n request = request {decafRequestNamespace = n} -- decafRequestTrailingSlash = False -- decafRequestQuery = [] -- decafRequestPayload = Nothing +-- decafRequestParts = None -- decafRequestCheckResponse = True -- } namespace :: T.Text -> DecafRequestCombinator @@ -488,6 +502,19 @@ noPayload :: DecafRequestCombinator noPayload = setNoPayload +-- ** Multipart/Form-Data Combinators + + +-- | Sets multipart/form-data parts for 'DecafRequest'. +setParts :: [Http.Client.Part] -> DecafRequestCombinator +setParts ps request = request {decafRequestParts = Just ps} + + +-- | Sets no multipart/form-data parts for 'DecafRequest'. +setNoParts :: DecafRequestCombinator +setNoParts request = request {decafRequestParts = Nothing} + + -- ** Response Checkers diff --git a/src/Decaf/Client/Internal/Http.hs b/src/Decaf/Client/Internal/Http.hs index 9814078..47d46bc 100644 --- a/src/Decaf/Client/Internal/Http.hs +++ b/src/Decaf/Client/Internal/Http.hs @@ -27,11 +27,17 @@ import Decaf.Client.DecafRequest (DecafRequest (..), DecafRequestPayload (..), u import Decaf.Client.DecafResponse (DecafResponse (..)) import Decaf.Client.Internal.Utils (compose) import GHC.Stack (HasCallStack) +import qualified Network.HTTP.Client.MultipartFormData as Http.Client import qualified Network.HTTP.Conduit as HC import qualified Network.HTTP.Simple as HS import Network.HTTP.Types (Status (statusCode), queryTextToQuery) +-- $setup +-- +-- >>> :set -XOverloadedStrings + + -- * HTTP Request Runners @@ -51,7 +57,7 @@ performDecafRequest => DecafRequest -> m (DecafResponse BL.ByteString) performDecafRequest request = do - response <- mkResponse <$> (HS.httpLBS (compileRequest request) `catch` throwHttpException request) + response <- mkResponse <$> (HS.httpLBS =<< compileRequest request `catch` throwHttpException request) when (decafRequestCheckResponse request) (assert2xx response) pure response @@ -113,8 +119,14 @@ type RequestFieldSetter = DecafRequest -> HS.Request -> HS.Request -- | Compiles a DECAF client 'DecafRequest' into a "http-conduit" 'H.Request'. -compileRequest :: DecafRequest -> HS.Request -compileRequest request = compiler request HS.defaultRequest +compileRequest :: MonadIO m => DecafRequest -> m HS.Request +compileRequest request = do + let bareRequest = compiler request HS.defaultRequest + case decafRequestParts request of + Nothing -> pure bareRequest + Just sp -> do + modifiedRequest <- Http.Client.formDataBody sp bareRequest + pure $ HS.setRequestMethod (BC.pack (show (decafRequestMethod request))) modifiedRequest -- | Request compiler. diff --git a/tests/doctest/doctest.hs b/tests/doctest/doctest.hs new file mode 100644 index 0000000..f1f4785 --- /dev/null +++ b/tests/doctest/doctest.hs @@ -0,0 +1,8 @@ +module Main where + +import System.Environment (getArgs) +import Test.DocTest (mainFromCabal) + + +main :: IO () +main = mainFromCabal "decaf-client" =<< getArgs diff --git a/test/Main.hs b/tests/test/Main.hs similarity index 100% rename from test/Main.hs rename to tests/test/Main.hs diff --git a/test/Mocking.hs b/tests/test/Mocking.hs similarity index 100% rename from test/Mocking.hs rename to tests/test/Mocking.hs