packages feed

quick-process-0.0.3: quick-process.cabal

cabal-version: 3.0
name:          quick-process
version:       0.0.3
synopsis:      Run external processes verified at compilation/installation
description:
    The library checks program name during compilation, generates exec spec
    to be verified in tests, before installation or before launch.
    
    == Motivation
    #motivation#
    
    The strongest trait of Haskell language is its type system. This
    powerful type system gives infinite opportunities for experimenting with
    mapping relational entities onto application values in safer, more
    comprehensible and maintainable ways.
    
    Compare popularity of Java and Haskell languagues and number of SOL
    libraries in them:
    
    > > length $ words "Hasql Beam Reigh8 postgresql-typed persistent esqueleto Opaleye Rel8 Squeal Selda Groundhog"
    > 11
    > > length $ words "JPA Hibernate JOOQ EJB"
    > 4
    
    Haskell ecosystem counts 2.75 times more SQL libraries nonetheless
    according to <https://www.tiobe.com/tiobe-index/ TIOBE index> in 2025
    Java is 20 times more popular than Haskell and by
    <https://pypl.github.io/PYPL.html PYPL> 126 times!
    
    As far as I remember only <https://www.jooq.org/ JOOQ> resembles a type
    safe library. Other libraries require runtime environment to check
    compatibility of codebase with SQL queries.
    
    RDBMSs talk SQL and it are inherently text oriented for extenal clients.
    All these Haskell libraries first of all are trying to hide plain string
    manipulation behind type fence as deep as possible.
    
    Once I tried had to launch an external process in a Haskell program.
    Keeping in mind the 50-200x slope on SQL arena in Haskell, I expected to
    find at least a few libraries on <https://hackage.haskell.org/ hackage>
    providing some type safety layer between my application and execv
    syscall interface accepting a bare strings.
    
    The observation above motivated me experimenting with a type safe
    wrapper for <https://hackage.haskell.org/package/process process>
    library.
    
    Structure of command line arguments is way simpler than SQL. An external
    program can be modelled as a function with a side effect. Haskell has an
    amazing library for testing functions -
    <hackage.haskell.org/package/QuickCheck QuickCheck> including impure
    ones.
    
    Main concern of external programs - they are not shipped with the
    application. Recall
    <https://en.wikipedia.org/wiki/Dependency_hell PRM hell> phrase. These
    days situation with external explicit dependency resolution during
    software installation and upgrade improved by nix and bazel. Nix and
    bazel are powerful, because they can pack\/isolate\/unpack the whole
    dependency universe of a single app, but they are complicated systems
    with a steep learning curve. Plus nix is not supported on Windows.
    That’s why they’ve got limited popularity and lot of software is still
    distributed as a self-extracting archive assuming some dependencies are
    compatible and preinstalled manually.
    
    Explicit list of dependencies is manually currated.
    
    Language does not provide out of the box solution to build such list.
    Taking into account human factor explicit list of dependencies always
    has a chance to diverge from the full (effective) one. E.g. host system
    got newer version of dependency which behaves differently.
    
    Software installation out of prebuilt executables usually don’t run
    tests.
    
    == Goals
    #goals#
    
    quick-process defines following goals:
    
    -   provide DSL for describing a call spec of an external program
    -   generate types, from the call spec, compatible with application
        domain and arguments of an external program
    -   automatic discovery of call specs in code base
    -   check call spec compatibility during app development, testing and
        installation
    -   process launch and mapping call spec to CreadeProcess
    
    == Call spec verification
    #call-spec-verification#
    
    Often call spec can be verified with @--help@ key terminating command
    line arguments. It’s way easier than running the program in sandbox,
    because no files gerenration is required and validating after effects
    either. Help key validation support can be checked.
    
    == Examples
    #examples#
    
    === Constant argument
    #constant-argument#
    
    > {-# LANGUAGE TemplateHaskell #-}
    > module CallSpecs where
    > import System.Process.Quick
    >
    > $(genCallSpec [TrailingHelpValidate, SandboxValidate] "date" (ConstArg "+%Y" .*. HNil))
    
    > {-# LANGUAGE TemplateHaskell #-}
    > module CallSpecTest where
    >
    > import CallSpecs
    > import System.Process.Quick
    >
    > main :: IO ()
    > main = $(discoverAndVerifyCallSpecs
    >           (fromList [ TrailingHelpValidate
    >                     , SandboxValidate
    >                     ])
    >           3)
    
    > {-# LANGUAGE TemplateHaskell #-}
    > module Main where
    >
    > import CallSpecs
    > import System.Process.Quick
    >
    > main :: IO ()
    > main = callProcess Date
    
    @genCallSpec@ defines type @Date@ with nullary constructor and
    @CallSpec@ instance for it.
    
    @discoverAndVerifyCallSpecs@ discovers all types with @CallSpec@
    instances, generates 3 values per type ande executes help key check.
    There is not much to check besides exit code in Date spec.
    
    @callProcess@ is similar to
    <https://hackage.haskell.org/package/process/docs/System-Process.html#v:callProcess callProcess>
    from process library, but accepts typed input instead of strings.
    
    === Variable argument
    #variable-argument#
    
    > {-# LANGUAGE TemplateHaskell #-}
    > module CallSpecs where
    > import System.Process.Quick
    >
    > $(genCallSpec
    >   [TrailingHelpValidate, SandboxValidate]
    >   "/bin/cp"
    >   (   VarArg @(Refined (InFile "hs") FilePath) "source"
    >   .*. VarArg @(Refined (OutFile "*") FilePath) "destination"
    >   .*. HNil
    >   )
    >  )
    
    > {-# LANGUAGE TemplateHaskell #-}
    > module CallSpecTest where
    >
    > import CallSpecs
    > import System.Process.Quick
    >
    > main :: IO ()
    > main = $(discoverAndVerifyCallSpecs
    >           (fromList [ TrailingHelpValidate
    >                     , SandboxValidate
    >                     ])
    >           100)
    
    > {-# LANGUAGE TemplateHaskell #-}
    > module Main where
    >
    > import CallSpecs
    > import System.Process.Quick
    >
    > main :: IO ()
    > main =
    >   callProcess $ BinCp $(refinedTH "app.hs") $(refinedTH "app.bak")
    
    @CallSpec@ of cp command requires 2 parameters and here quick-process
    power start to show up. Refined constraint InFile ensures that first
    string is a valid file path to a Haskell source file. This part is
    delegated to <https://hackage.haskell.org/package/refined refined>
    library. HelpKey mode generates appropriate values, but they don’t point
    to real files on disk. Use Sandbox mode to actually launch process in a
    temporary dir with real files. In Sandbox @OutFile@ cause to check that
    the file appears on the path once process terminates.
    
    === Subcases
    #subcases#
    
    Call spec can be composed of sum types.
    
    > {-# LANGUAGE TemplateHaskell #-}
    > module CallSpecs where
    > import System.Process.Quick
    >
    > $(genCallSpec
    >   [TrailingHelpValidate, SandboxValidate]
    >   "find"
    >   (   ConstArg "."
    >   .*. Subcases
    >         "FindCases"
    >         [ Subcase "FindPrintf"
    >           (KeyArg @(Refined (Regex "^[%][fpactbnM%]$") String) "-printf" .*. HNil)
    >         , Subcase "FindExec"
    >           (KeyArg @(Refined (Regex "^(ls|file|du)$") String) "-exec" .*. ConstArgs (words "{} ;") .*. HNil)
    >         ]
    >   .*. HNil
    >   )
    >  )
    
    Note usage of @Regex@ predicate - thanks to
    <https://hackage.haskell.org/package/refined sbv> and z3 SMT solver
    values satisfing arbitrary TDFA regex can be generated.
    
    === Init Cascade
    #init-cascade#
    
    A call spec may require another command to be executed somewhere in the
    past e.g. most of git commands work only with initialize repository.
    
    > {-# LANGUAGE TemplateHaskell #-}
    > module CallSpecs where
    >
    > import CallSpecs.GitInit qualified as I
    > import System.Process.Quick
    >
    > $(genCallSpec
    >   [SandboxValidate]
    >   "git"
    >   (   ConstArg "remote"
    >   .*. StdErrMatches "^$"
    >   .*. StdOutMatches "^$"
    >   .*. Init @I.Git
    >   .*. HNil
    >   )
    >  )
    
    == Generated TH code inspection
    #generated-th-code-inspection#
    
    GHC prints generated TH code with pragma:
    
    > {-# OPTIONS_GHC -ddump-splices #-}

homepage:      http://github.com/yaitskov/quick-process
license:       BSD-3-Clause
author:        Daniil Iaitskov
maintainer:    dyaitskov@gmail.com
copyright:     Daniil Iaitkov 2025
category:      System
build-type:    Simple
bug-reports:   https://github.com/yaitskov/quick-process/issues
extra-doc-files:
  changelog.md
tested-with:
  GHC == 9.10.1, GHC == 9.12.2

source-repository head
  type:
    git
  location:
    https://github.com/yaitskov/quick-process.git

Flag leafopt
  Description: Enable leaf optimization
  Default: False

common base
  default-language: GHC2024
  ghc-options: -Wall
  default-extensions:
    DefaultSignatures
    NoImplicitPrelude
    OverloadedLabels
    OverloadedStrings
    TemplateHaskell
  build-depends:
      QuickCheck >= 2.14.3 && < 3
    , base >=4.7 && < 5
    , bytestring >= 0.12.1 && < 1
    , generic-lens >= 2.2.2 && < 3
    , lens >= 5.3.2 && < 6
    , relude >= 1.2.2 && < 2

library hlist-internal
  hs-source-dirs: hlist
  Build-Depends:       base >= 4.7 && < 5,
                       -- for Typeable '[] and '(:) with ghc-7.6
                       base-orphans < 1,
                       template-haskell < 3,
                       ghc-prim < 0.16,
                       mtl  >= 2.3.1 && < 3,
                       tagged < 1,
                       profunctors >= 5.6.2 && < 6,
                       array < 1
  Exposed-modules:     Data.HList,
                       Data.HList.CommonMain,
                       Data.HList.Data,
                       Data.HList.Dredge,
                       Data.HList.FakePrelude,
                       Data.HList.HArray,
                       Data.HList.HCurry,
                       Data.HList.HList,
                       Data.HList.HListPrelude,
                       Data.HList.HOccurs,
                       Data.HList.HTypeIndexed,
                       Data.HList.HSort,
                       Data.HList.HZip,
                       Data.HList.Keyword,
                       Data.HList.Label3,
                       Data.HList.Label5,
                       Data.HList.Label6,
                       Data.HList.Labelable,
                       Data.HList.MakeLabels,
                       Data.HList.Record,
                       Data.HList.RecordPuns,
                       Data.HList.RecordU,
                       Data.HList.TIC,
                       Data.HList.TIP,
                       Data.HList.TIPtuple,
                       Data.HList.TypeEqO,
                       Data.HList.Variant
  Other-modules:       LensDefs
  Default-Language:    Haskell2010
  Ghc-Options:         -Wall -fno-warn-missing-signatures
                       -fno-warn-orphans
                       -fno-warn-unticked-promoted-constructors
                       -Wno-star-is-type
  Default-Extensions:  ConstraintKinds
                       DataKinds
                       DeriveDataTypeable
                       EmptyDataDecls
                       FlexibleContexts
                       FlexibleInstances
                       FunctionalDependencies
                       GeneralizedNewtypeDeriving
                       GADTs
                       KindSignatures
                       MultiParamTypeClasses
                       PolyKinds
                       RankNTypes
                       ScopedTypeVariables
                       StandaloneDeriving
                       TypeFamilies
                       TypeOperators
                       UndecidableInstances
                       StarIsType
                       UndecidableSuperClasses
                       AllowAmbiguousTypes
                       RoleAnnotations
  Other-Extensions:    CPP
                       TemplateHaskell
                       OverlappingInstances

library multi-containers-internal
  hs-source-dirs: multi-containers
  exposed-modules:
    Data.Multimap.Table
    Data.Multimap.Table.Internal
  build-depends:
      base >=4.7 && <5
    , containers >=0.5.10.2 && <0.8
  default-language: Haskell2010
  ghc-options: -Wall

-- https://github.com/erikd/conduit-find/pull/17
library conduit-find-internal
  import: base
  hs-source-dirs: conduit-find
  ghc-options: -Wall -funbox-strict-fields
  if os(linux) && flag(leafopt)
     cpp-options: -DLEAFOPT=1
  default-extensions:
    ImplicitPrelude
  exposed-modules:
      Data.Cond, Data.Conduit.Find
  build-depends:
      attoparsec           >= 0.14.4 && < 1
    , conduit              >= 1.2 && < 2
    , conduit-combinators  >= 1.3.0 && < 2
    , conduit-extra        >= 1.3.6 && < 2
    , either               >= 5.0.2 && < 6
    , exceptions           >= 0.6 && < 1
    , filepath             >= 1.5.2 && < 2
    , mmorph               >= 1.2.0 && < 2
    , monad-control        >= 1.0 && < 2
    , mtl                  >= 2.3.1 && < 3
    , regex-posix          >= 0.96.0 && < 1
    , resourcet            >= 1.1 && < 2
    , semigroups           >= 0.20 && < 1
    , streaming-commons    >= 0.2.2 && < 1
    , text                 >= 2.0 && < 3
    , time                 >= 1.12.2 && < 2
    , transformers         >= 0.6.1 && < 1
    , transformers-base    >= 0.4.6 && < 1
    , transformers-either  >= 0.1.4 && < 1
    , unix-compat          >= 0.4.1.1 && < 1
    , unliftio-core        >= 0.2.1 && < 1

-- https://github.com/nikita-volkov/refined/pull/112
library refined-internal
  import: base
  hs-source-dirs: refined
  default-extensions:
    ImplicitPrelude
  cpp-options: -DHAVE_QUICKCHECK
  exposed-modules:
    Refined
  other-modules:
    Refined.Unsafe
    Refined.Unsafe.Type
  build-depends:
      deepseq          >= 1.4 && < 2
    , exceptions       < 1
    , hashable         >= 1.0 && < 2
    , mtl              < 3
    , template-haskell < 3
    , text             < 3
    , these-skinny     < 1

library
  import: base
  hs-source-dirs: src
  exposed-modules:
    System.Process.Quick
    System.Process.Quick.CallEffect
    System.Process.Quick.CallArgument
    System.Process.Quick.CallSpec
    System.Process.Quick.CallSpec.Init
    System.Process.Quick.CallSpec.Run
    System.Process.Quick.CallSpec.Subcases
    System.Process.Quick.CallSpec.Type
    System.Process.Quick.CallSpec.Verify
    System.Process.Quick.CallSpec.Verify.ImportOverlook
    System.Process.Quick.CallSpec.Verify.Sandbox
    System.Process.Quick.CallSpec.Verify.TrailingHelp
    System.Process.Quick.CallSpec.Verify.Type
    System.Process.Quick.OrphanArbitrary
    System.Process.Quick.Predicate
    System.Process.Quick.Predicate.ImplDir
    System.Process.Quick.Predicate.InDir
    System.Process.Quick.Predicate.InFile
    System.Process.Quick.Predicate.LowerCase
    System.Process.Quick.Predicate.Regex
    System.Process.Quick.Prelude
    System.Process.Quick.Pretty
    System.Process.Quick.Sbv.Arbitrary
    System.Process.Quick.TdfaToSbvRegex
    System.Process.Quick.Util
  build-depends:
      casing                  < 1
    , conduit                 < 2
    , conduit-find-internal
    , containers              < 1
    , directory               < 2
    , filepath                < 2
    , generic-data            < 2
    , generic-deriving        < 2
    , generic-random          < 2
    , hlist-internal
    , monad-time              < 1
    , mtl                     < 3
    , multi-containers-internal
    , pretty-simple           < 5
    , process                 < 2
    , refined-internal
    , regex-compat            < 1
    , regex-tdfa              < 2
    , safe-exceptions         < 1
    , sbv                     < 12
    , template-haskell        < 3
    , temporary               < 2
    , th-utilities            < 1
    , time                    < 2
    , trace-embrace           >= 1.2.0 && < 2
    , unix                    < 3
    , wl-pprint-text          < 2

test-suite verify-call-specs
  import: base
  type: exitcode-stdio-1.0
  main-is: VerifyCallSpecs.hs
  other-modules:
    CallSpecs.Find
    CallSpecs.Find.Type
  hs-source-dirs:
    verify-call-specs
  ghc-options: -Wall -rtsopts -threaded -main-is VerifyCallSpecs
  build-depends:
      quick-process
    , hlist-internal

test-suite sandbox-effect
  import: base
  type: exitcode-stdio-1.0
  main-is: SandBoxEffect.hs
  other-modules:
    CallSpecs.CpOne
    CallSpecs.CpManyToDir
    CallSpecs.Date
    CallSpecs.FindCases
    CallSpecs.GitInit
    CallSpecs.GitInitExit1
    CallSpecs.GitRemote
    CallSpecs.GitSubcases
  hs-source-dirs:
    sandbox-effect
  ghc-options: -Wall -rtsopts -threaded -main-is SandBoxEffect
  build-depends:
      quick-process
    , hlist-internal
    , refined-internal

test-suite test
  import: base
  type: exitcode-stdio-1.0
  main-is: Driver.hs
  other-modules:
    Discovery
    System.Process.Quick.Test.CallSpec
    System.Process.Quick.Test.CallSpec.Const
    System.Process.Quick.Test.CallSpec.VarArg
    System.Process.Quick.Test.CallSpec.VarArg.Refined
    System.Process.Quick.Test.Prelude
    System.Process.Quick.Test.Th
    Paths_quick_process
  autogen-modules:
    Paths_quick_process
  hs-source-dirs:
    test
  ghc-options: -Wall -rtsopts -threaded -main-is Driver
  build-depends:
      directory
    , hlist-internal
    , quickcheck-instances
    , refined-internal
    , th-utilities
    , tasty
    , tasty-discover
    , tasty-hunit
    , tasty-quickcheck
    , template-haskell
    , temporary
    , th-lift-instances
    , quick-process
    , unliftio