├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cabal-ttc-bounds-lower.project ├── cabal-ttc-bounds-upper.project ├── cabal.project ├── project ├── TODO.md ├── announcement │ ├── ttc-haskell-1.0.0.0-haskell-cafe.md │ └── ttc-haskell-1.0.0.0-reddit.md ├── log │ ├── 20191123-design.md │ └── 20210609-design.md └── release │ ├── ttc-haskell-0.0.0.1.md │ ├── ttc-haskell-0.0.0.2.md │ ├── ttc-haskell-0.0.0.3.md │ ├── ttc-haskell-0.0.0.4.md │ ├── ttc-haskell-0.1.0.0.md │ ├── ttc-haskell-0.1.0.1.md │ ├── ttc-haskell-0.2.0.0.md │ ├── ttc-haskell-0.2.1.0.md │ ├── ttc-haskell-0.2.2.0.md │ ├── ttc-haskell-0.2.3.0.md │ ├── ttc-haskell-0.3.0.0.md │ ├── ttc-haskell-0.4.0.0.md │ ├── ttc-haskell-1.0.0.0.md │ ├── ttc-haskell-1.1.0.0.md │ ├── ttc-haskell-1.1.0.1.md │ ├── ttc-haskell-1.1.0.2.md │ ├── ttc-haskell-1.1.1.0.md │ ├── ttc-haskell-1.1.1.1.md │ ├── ttc-haskell-1.2.0.0.md │ ├── ttc-haskell-1.2.1.0.md │ ├── ttc-haskell-1.3.0.0.md │ ├── ttc-haskell-1.4.0.0.md │ └── ttc-haskell-1.5.0.0.md ├── stack-8.10.7.yaml ├── stack-8.8.4.yaml ├── stack-9.0.2.yaml ├── stack-9.10.1.yaml ├── stack-9.12.1.yaml ├── stack-9.2.8.yaml ├── stack-9.4.8.yaml ├── stack-9.6.6.yaml ├── stack-9.8.4.yaml ├── stack.yaml ├── ttc-examples ├── LICENSE ├── README.md ├── enum │ └── enum.hs ├── invalid │ ├── Username.hs │ └── invalid.hs ├── mkuvalid │ ├── Username.hs │ └── mkuvalid.hs ├── mkuvalidqq │ ├── Username.hs │ └── mkuvalidqq.hs ├── mkvalid │ ├── Username.hs │ └── mkvalid.hs ├── prompt │ ├── CreditCard.hs │ └── prompt.hs ├── ttc-examples.cabal ├── uname │ ├── Username.hs │ └── uname.hs ├── uvalidof │ ├── Username.hs │ └── uvalidof.hs ├── valid │ ├── Username.hs │ └── valid.hs ├── validof │ ├── Username.hs │ └── validof.hs └── wrapper │ └── wrapper.hs └── ttc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Setup.hs ├── src └── Data │ ├── TTC.hs │ └── TTC │ └── Wrapper.hs ├── test ├── Data │ └── TTC │ │ ├── Test.hs │ │ └── Wrapper │ │ └── Test.hs ├── Spec.hs └── TestTypes │ └── Ex.hs └── ttc.cabal /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | pull_request: 9 | branches: 10 | - develop 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | test-ghc-os: 17 | name: GHC ${{ matrix.ghc }} on ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | os: 23 | - ubuntu-latest 24 | ghc: 25 | - "8.8.4" 26 | - "8.10.7" 27 | - "9.0.2" 28 | - "9.2.8" 29 | - "9.4.8" 30 | - "9.6.6" 31 | - "9.8.4" 32 | - "9.10.1" 33 | - "9.12.1" 34 | steps: 35 | - name: "checkout" 36 | uses: actions/checkout@v4 37 | 38 | - name: "setup Haskell" 39 | uses: haskell-actions/setup@v2 40 | id: setup 41 | with: 42 | ghc-version: ${{ matrix.ghc }} 43 | cabal-version: latest 44 | cabal-update: false 45 | enable-stack: true 46 | stack-version: latest 47 | 48 | - name: "update Cabal" 49 | run: cabal update 50 | 51 | - name: "setup environment" 52 | run: | 53 | GHC_VERSION=$(ghc --numeric-version) 54 | CABAL_OPTS="" 55 | if [ -f "cabal-${GHC_VERSION}.project" ] ; then 56 | CABAL_OPTS="--project-file=cabal-${GHC_VERSION}.project" 57 | fi 58 | echo "CABAL_OPTS=${CABAL_OPTS}" | tee -a "${GITHUB_ENV}" 59 | STACK_YAML="stack-${GHC_VERSION}.yaml" 60 | echo "STACK_YAML=${STACK_YAML}" | tee -a "${GITHUB_ENV}" 61 | TS_YYYYMM="$(date +%Y%m)" 62 | echo "TS_YYYYMM=${TS_YYYYMM}" | tee -a "${GITHUB_ENV}" 63 | 64 | - name: "cabal: configure build" 65 | run: | 66 | cabal configure $CABAL_OPTS --enable-tests --enable-benchmarks --disable-documentation 67 | cabal build all --dry-run $CABAL_OPTS 68 | 69 | - name: "cabal: restore cache" 70 | uses: actions/cache/restore@v4 71 | id: cache-cabal 72 | env: 73 | key: ${{ runner.os }}-${{ env.TS_YYYYMM }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }} 74 | with: 75 | path: ${{ steps.setup.outputs.cabal-store }} 76 | key: ${{ env.key }}-${{ hashFiles('**/plan.json') }} 77 | restore-keys: ${{ env.key }}- 78 | 79 | - if: ${{ !steps.cache-cabal.outputs.cache-hit }} 80 | name: "cabal: install dependencies" 81 | run: cabal build all $CABAL_OPTS --only-dependencies 82 | 83 | - if: ${{ !steps.cache-cabal.outputs.cache-hit }} 84 | name: "cabal: save cache" 85 | uses: actions/cache/save@v4 86 | with: 87 | path: ${{ steps.setup.outputs.cabal-store }} 88 | key: ${{ steps.cache-cabal.outputs.cache-primary-key }} 89 | 90 | - name: "cabal: ttc: build" 91 | run: cabal build ttc $CABAL_OPTS 92 | 93 | - name: "cabal: ttc: test" 94 | run: cabal test ttc $CABAL_OPTS 95 | 96 | - name: "cabal: ttc: haddock" 97 | run: cabal haddock ttc $CABAL_OPTS 98 | 99 | - name: "cabal: ttc: examples" 100 | run: cabal build ttc-examples $CABAL_OPTS 101 | 102 | - if: matrix.ghc == '8.8.4' 103 | name: "ttc lower bounds: setup environment" 104 | run: | 105 | cabal clean 106 | rm -rf ~/.cabal/store 107 | CABAL_OPTS="--project-file=cabal-ttc-bounds-lower.project" 108 | echo "CABAL_OPTS=${CABAL_OPTS}" | tee -a "${GITHUB_ENV}" 109 | 110 | - if: matrix.ghc == '8.8.4' 111 | name: "ttc lower bounds: configure build" 112 | run: | 113 | cabal configure $CABAL_OPTS --enable-tests --enable-benchmarks --disable-documentation 114 | cabal build ttc ttc-examples --dry-run $CABAL_OPTS 115 | 116 | - if: matrix.ghc == '8.8.4' 117 | name: "ttc lower bounds: restore cache" 118 | uses: actions/cache/restore@v4 119 | id: cache-ttc-bounds-lower 120 | env: 121 | key: ${{ runner.os }}-${{ env.TS_YYYYMM }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}-ttc-bounds-lower 122 | with: 123 | path: ${{ steps.setup.outputs.cabal-store }} 124 | key: ${{ env.key }}-${{ hashFiles('**/plan.json') }} 125 | restore-keys: ${{ env.key }}- 126 | 127 | - if: ${{ matrix.ghc == '8.8.4' && !steps.cache-ttc-bounds-lower.outputs.cache-hit }} 128 | name: "ttc lower bounds: install dependencies" 129 | run: cabal build ttc ttc-examples $CABAL_OPTS --only-dependencies 130 | 131 | - if: ${{ matrix.ghc == '8.8.4' && !steps.cache-ttc-bounds-lower.outputs.cache-hit }} 132 | name: "ttc lower bounds: save cache" 133 | uses: actions/cache/save@v4 134 | with: 135 | path: ${{ steps.setup.outputs.cabal-store }} 136 | key: ${{ steps.cache-ttc-bounds-lower.outputs.cache-primary-key }} 137 | 138 | - if: matrix.ghc == '8.8.4' 139 | name: "ttc lower bounds: build" 140 | run: cabal build ttc $CABAL_OPTS 141 | 142 | - if: matrix.ghc == '8.8.4' 143 | name: "ttc lower bounds: test" 144 | run: cabal test ttc $CABAL_OPTS 145 | 146 | - if: matrix.ghc == '8.8.4' 147 | name: "ttc lower bounds: haddock" 148 | run: cabal haddock ttc $CABAL_OPTS 149 | 150 | - if: matrix.ghc == '8.8.4' 151 | name: "ttc lower bounds: examples" 152 | run: cabal build ttc-examples $CABAL_OPTS 153 | 154 | - if: matrix.ghc == '9.12.1' 155 | name: "ttc upper bounds: setup environment" 156 | run: | 157 | cabal clean 158 | rm -rf ~/.cabal/store 159 | CABAL_OPTS="--project-file=cabal-ttc-bounds-upper.project" 160 | echo "CABAL_OPTS=${CABAL_OPTS}" | tee -a "${GITHUB_ENV}" 161 | 162 | - if: matrix.ghc == '9.12.1' 163 | name: "ttc upper bounds: configure build" 164 | run: | 165 | cabal configure $CABAL_OPTS --enable-tests --enable-benchmarks --disable-documentation 166 | cabal build ttc ttc-examples --dry-run $CABAL_OPTS 167 | 168 | - if: matrix.ghc == '9.12.1' 169 | name: "ttc upper bounds: restore cache" 170 | uses: actions/cache/restore@v4 171 | id: cache-ttc-bounds-upper 172 | env: 173 | key: ${{ runner.os }}-${{ env.TS_YYYYMM }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }}-ttc-bounds-upper 174 | with: 175 | path: ${{ steps.setup.outputs.cabal-store }} 176 | key: ${{ env.key }}-${{ hashFiles('**/plan.json') }} 177 | restore-keys: ${{ env.key }}- 178 | 179 | - if: ${{ matrix.ghc == '9.12.1' && !steps.cache-ttc-bounds-upper.outputs.cache-hit }} 180 | name: "ttc upper bounds: install dependencies" 181 | run: cabal build ttc ttc-examples $CABAL_OPTS --only-dependencies 182 | 183 | - if: ${{ matrix.ghc == '9.12.1' && !steps.cache-ttc-bounds-upper.outputs.cache-hit }} 184 | name: "ttc upper bounds: save cache" 185 | uses: actions/cache/save@v4 186 | with: 187 | path: ${{ steps.setup.outputs.cabal-store }} 188 | key: ${{ steps.cache-ttc-bounds-upper.outputs.cache-primary-key }} 189 | 190 | - if: matrix.ghc == '9.12.1' 191 | name: "ttc upper bounds: build" 192 | run: cabal build ttc $CABAL_OPTS 193 | 194 | - if: matrix.ghc == '9.12.1' 195 | name: "ttc upper bounds: test" 196 | run: cabal test ttc $CABAL_OPTS 197 | 198 | - if: matrix.ghc == '9.12.1' 199 | name: "ttc upper bounds: haddock" 200 | run: cabal haddock ttc $CABAL_OPTS 201 | 202 | - if: matrix.ghc == '9.12.1' 203 | name: "ttc upper bounds: examples" 204 | run: cabal build ttc-examples $CABAL_OPTS 205 | 206 | - name: "stack: configure build" 207 | run: stack --system-ghc ls dependencies json > stack-deps.json 208 | 209 | - name: "stack: restore cache" 210 | uses: actions/cache/restore@v4 211 | id: cache-stack 212 | env: 213 | key: ${{ runner.os }}-${{ env.TS_YYYYMM }}-ghc-${{ steps.setup.outputs.ghc-version }}-stack 214 | with: 215 | path: ~/.stack 216 | key: ${{ env.key }}-${{ hashFiles('stack-deps.json') }} 217 | restore-keys: ${{ env.key }}- 218 | 219 | - name: "stack: ttc: build" 220 | run: stack build ttc --system-ghc --test --bench --no-run-tests --no-run-benchmarks 221 | 222 | - name: "stack: ttc: test" 223 | run: stack test ttc --system-ghc 224 | 225 | - name: "stack: ttc: haddock" 226 | run: stack haddock ttc --system-ghc 227 | 228 | - name: "stack: ttc: examples" 229 | run: stack build ttc-examples --system-ghc 230 | 231 | - if: ${{ !steps.cache-stack.outputs.cache-hit }} 232 | name: "stack: save cache" 233 | uses: actions/cache/save@v4 234 | with: 235 | path: ~/.stack 236 | key: ${{ steps.cache-stack.outputs.cache-primary-key }} 237 | 238 | test-ghc884-cabal3000: 239 | name: Cabal ${{ matrix.cabal }} (GHC ${{ matrix.ghc }}) 240 | runs-on: ${{ matrix.os }} 241 | strategy: 242 | fail-fast: false 243 | matrix: 244 | os: 245 | - ubuntu-latest 246 | ghc: 247 | - "8.8.4" 248 | cabal: 249 | - "3.0.0.0" 250 | steps: 251 | - name: "checkout" 252 | uses: actions/checkout@v4 253 | 254 | - name: "setup Haskell" 255 | uses: haskell-actions/setup@v2 256 | id: setup 257 | with: 258 | ghc-version: ${{ matrix.ghc }} 259 | cabal-version: ${{ matrix.cabal }} 260 | cabal-update: true 261 | 262 | - name: "setup environment" 263 | run: | 264 | GHC_VERSION=$(ghc --numeric-version) 265 | CABAL_OPTS="" 266 | if [ -f "cabal-${GHC_VERSION}.project" ] ; then 267 | CABAL_OPTS="--project-file=cabal-${GHC_VERSION}.project" 268 | fi 269 | echo "CABAL_OPTS=${CABAL_OPTS}" | tee -a "${GITHUB_ENV}" 270 | TS_YYYYMM="$(date +%Y%m)" 271 | echo "TS_YYYYMM=${TS_YYYYMM}" | tee -a "${GITHUB_ENV}" 272 | 273 | - name: "configure build" 274 | run: | 275 | cabal v2-configure $CABAL_OPTS --enable-tests --enable-benchmarks --disable-documentation 276 | cabal v2-build all --dry-run $CABAL_OPTS 277 | 278 | - name: "restore cache" 279 | uses: actions/cache/restore@v4 280 | id: cache-cabal 281 | env: 282 | key: ${{ runner.os }}-${{ env.TS_YYYYMM }}-ghc-${{ steps.setup.outputs.ghc-version }}-cabal-${{ steps.setup.outputs.cabal-version }} 283 | with: 284 | path: ${{ steps.setup.outputs.cabal-store }} 285 | key: ${{ env.key }}-${{ hashFiles('**/plan.json') }} 286 | restore-keys: ${{ env.key }}- 287 | 288 | - if: ${{ !steps.cache-cabal.outputs.cache-hit }} 289 | name: "install dependencies" 290 | run: cabal v2-build all $CABAL_OPTS --only-dependencies 291 | 292 | - if: ${{ !steps.cache-cabal.outputs.cache-hit }} 293 | name: "save cache" 294 | uses: actions/cache/save@v4 295 | with: 296 | path: ${{ steps.setup.outputs.cabal-store }} 297 | key: ${{ steps.cache-cabal.outputs.cache-primary-key }} 298 | 299 | - name: "ttc: build" 300 | run: cabal v2-build ttc $CABAL_OPTS 301 | 302 | - name: "ttc: test" 303 | run: cabal v2-test ttc $CABAL_OPTS 304 | 305 | - name: "ttc: haddock" 306 | run: cabal v2-haddock ttc $CABAL_OPTS 307 | 308 | - name: "ttc: examples" 309 | run: cabal v2-build ttc-examples $CABAL_OPTS 310 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # project 2 | /build/ 3 | 4 | # cabal 5 | cabal*.project.local 6 | cabal*.project.local~ 7 | /dist-newstyle/ 8 | 9 | # nix 10 | result* 11 | 12 | # stack 13 | .stack-work 14 | *.yaml.lock 15 | 16 | # vi 17 | .*.swp 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019-2025 Travis Cardwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Make configuration 3 | 4 | ifeq ($(origin .RECIPEPREFIX), undefined) 5 | $(error GNU Make 4.0 or later required) 6 | endif 7 | .RECIPEPREFIX := > 8 | 9 | SHELL := bash 10 | .SHELLFLAGS := -o nounset -o errexit -o pipefail -c 11 | 12 | MAKEFLAGS += --no-builtin-rules 13 | MAKEFLAGS += --warn-undefined-variables 14 | 15 | .DEFAULT_GOAL := build 16 | 17 | MODE ?= cabal 18 | 19 | ifeq ($(MODE), cabal) 20 | GHC_VERSION ?= $(shell ghc --version | sed 's/.* //') 21 | CABAL_ARGS := --with-ghc ghc-$(GHC_VERSION) 22 | ifneq ($(origin PROJECT_FILE), undefined) 23 | CABAL_ARGS += "--project-file=$(PROJECT_FILE)" 24 | else 25 | PROJECT_FILE_AUTO := cabal-$(GHC_VERSION).project 26 | ifneq (,$(wildcard $(PROJECT_FILE_AUTO))) 27 | CABAL_ARGS += "--project-file=$(PROJECT_FILE_AUTO)" 28 | endif 29 | endif 30 | ifneq ($(origin CONFIG), undefined) 31 | $(error CONFIG set in cabal MODE) 32 | endif 33 | ifneq ($(origin RESOLVER), undefined) 34 | $(error RESOLVER set in cabal MODE) 35 | endif 36 | else ifeq ($(MODE), stack) 37 | STACK_ARGS := 38 | ifneq ($(origin CONFIG), undefined) 39 | STACK_ARGS += --stack-yaml "$(CONFIG)" 40 | endif 41 | ifneq ($(origin RESOLVER), undefined) 42 | STACK_ARGS += --resolver "$(RESOLVER)" 43 | endif 44 | ifneq ($(origin GHC_VERSION), undefined) 45 | $(error GHC_VERSION set in stack MODE) 46 | endif 47 | ifneq ($(origin CABAL_ARGS), undefined) 48 | $(error CABAL_ARGS set in stack MODE) 49 | endif 50 | else 51 | $(error unknown MODE: $(MODE)) 52 | endif 53 | 54 | ############################################################################## 55 | # Functions 56 | 57 | define all_files 58 | find . -not -path '*/\.*' -type f 59 | endef 60 | 61 | define die 62 | (echo "error: $(1)" ; false) 63 | endef 64 | 65 | define get_version 66 | $(shell grep '^version:' $(1) | sed 's/^version: *//') 67 | endef 68 | 69 | define hs_files 70 | find . -not -path '*/\.*' -type f -name '*.hs' 71 | endef 72 | 73 | ############################################################################## 74 | # Rules 75 | 76 | build: hr 77 | build: # build package * 78 | ifeq ($(MODE), stack) 79 | > stack build $(STACK_ARGS) --test --bench --no-run-tests --no-run-benchmarks 80 | else 81 | > cabal build all $(CABAL_ARGS) --enable-tests --enable-benchmarks 82 | endif 83 | .PHONY: build 84 | 85 | clean: # clean packages 86 | > @rm -rf stack*.yaml.lock 87 | > @rm -rf .stack-work ttc/.stack-work ttc-examples/.stack-work 88 | > @rm -f cabal*.project.local 89 | > @rm -rf dist-newstyle 90 | .PHONY: clean 91 | 92 | clean-all: clean 93 | clean-all: #internal# clean package and remove build artifacts 94 | #> @rm -rf build 95 | .PHONY: clean-all 96 | 97 | coverage: hr 98 | coverage: # run tests with code coverage * 99 | ifeq ($(MODE), stack) 100 | > stack test $(STACK_ARGS) --coverage 101 | else 102 | > cabal test all $(CABAL_ARGS) \ 103 | > --enable-coverage --enable-tests --test-show-details=always 104 | endif 105 | .PHONY: coverage 106 | 107 | doc-api: hr 108 | doc-api: # build API documentation * 109 | ifeq ($(MODE), stack) 110 | > stack haddock $(STACK_ARGS) 111 | else 112 | > cabal haddock all $(CABAL_ARGS) 113 | endif 114 | .PHONY: doc-api 115 | 116 | grep: # grep all non-hidden files for expression E 117 | > $(eval E:= "") 118 | > @test -n "$(E)" || $(call die,"usage: make grep E=expression") 119 | > @$(call all_files) | xargs grep -Hn '$(E)' || true 120 | .PHONY: grep 121 | 122 | help: # show this help 123 | > @if command -v column >/dev/null 2>&1 \ 124 | > ; then \ 125 | > grep '^[a-zA-Z0-9_-]\+:[^#]*# ' $(MAKEFILE_LIST) \ 126 | > | sed 's/^\([^:]\+\):[^#]*# \(.*\)/make \1\t\2/' \ 127 | > | column -t -s $$'\t' \ 128 | > ; else \ 129 | > grep '^[a-zA-Z0-9_-]\+:[^#]*# ' $(MAKEFILE_LIST) \ 130 | > | sed 's/^\([^:]\+\):[^#]*# \(.*\)/make \1\t\2/' \ 131 | > ; fi 132 | > @echo 133 | > @echo "Cabal mode (MODE=cabal)" 134 | > @echo " * Set GHC_VERSION to specify a GHC version." 135 | > @echo " * Set PROJECT_FILE to specify a cabal.project file." 136 | > @echo 137 | > @echo "Stack mode (MODE=stack)" 138 | > @echo " * Set CONFIG to specify a stack.yaml file." 139 | > @echo " * Set RESOLVER to specify a Stack resolver." 140 | .PHONY: help 141 | 142 | hlint: # run hlint on all Haskell source 143 | > @$(call hs_files) | xargs hlint 144 | .PHONY: hlint 145 | 146 | hr: #internal# display a horizontal rule 147 | > @command -v hr >/dev/null 2>&1 && hr -t || true 148 | .PHONY: hr 149 | 150 | hsgrep: # grep all Haskell source for expression E 151 | > $(eval E := "") 152 | > @test -n "$(E)" || $(call die,"usage: make hsgrep E=expression") 153 | > @$(call hs_files) | xargs grep -Hn '$(E)' || true 154 | .PHONY: hsgrep 155 | 156 | hsrecent: # show N most recently modified Haskell files 157 | > $(eval N := "10") 158 | > @find . -not -path '*/\.*' -type f -name '*.hs' -printf '%T+ %p\n' \ 159 | > | sort --reverse \ 160 | > | head -n $(N) 161 | .PHONY: hsrecent 162 | 163 | hssloc: # count lines of Haskell source 164 | > @$(call hs_files) | xargs wc -l | tail -n 1 | sed 's/^ *\([0-9]*\).*$$/\1/' 165 | .PHONY: hssloc 166 | 167 | ignored: # list files ignored by git 168 | > @git ls-files . --ignored --exclude-standard --others 169 | .PHONY: ignored 170 | 171 | recent: # show N most recently modified files 172 | > $(eval N := "10") 173 | > @find . -not -path '*/\.*' -type f -printf '%T+ %p\n' \ 174 | > | sort --reverse \ 175 | > | head -n $(N) 176 | .PHONY: recent 177 | 178 | test: hr 179 | test: # run tests, optionally for pattern P * 180 | > $(eval P := "") 181 | ifeq ($(MODE), stack) 182 | > @test -z "$(P)" \ 183 | > && stack test $(STACK_ARGS) \ 184 | > || stack test $(STACK_ARGS) --test-arguments '--pattern $(P)' 185 | else 186 | > @test -z "$(P)" \ 187 | > && cabal test all $(CABAL_ARGS) \ 188 | > --enable-tests --test-show-details=always \ 189 | > || cabal test all $(CABAL_ARGS) \ 190 | > --enable-tests --test-show-details=always \ 191 | > --test-option '--pattern=$(P)' 192 | endif 193 | .PHONY: test 194 | 195 | test-all: # run all configured tests and build examples using MODE 196 | ifeq ($(MODE), stack) 197 | > @make test-build CONFIG=stack-8.8.4.yaml 198 | > @make test-build CONFIG=stack-8.10.7.yaml 199 | > @make test-build CONFIG=stack-9.0.2.yaml 200 | > @make test-build CONFIG=stack-9.2.8.yaml 201 | > @make test-build CONFIG=stack-9.4.8.yaml 202 | > @make test-build CONFIG=stack-9.6.6.yaml 203 | > @make test-build CONFIG=stack-9.8.2.yaml 204 | > @make test-build CONFIG=stack-9.10.1.yaml 205 | else 206 | > @make test-build GHC_VERSION=8.8.4 207 | > @make test-build GHC_VERSION=8.10.7 208 | > @make test-build GHC_VERSION=9.0.2 209 | > @make test-build GHC_VERSION=9.2.8 210 | > @make test-build GHC_VERSION=9.4.8 211 | > @make test-build GHC_VERSION=9.6.6 212 | > @make test-build GHC_VERSION=9.8.2 213 | > @make test-build GHC_VERSION=9.10.1 214 | endif 215 | .PHONY: test-all 216 | 217 | test-bounds-lower: ttc-test-bounds-lower 218 | test-bounds-lower: # test lower bounds (Cabal only) 219 | .PHONY: test-bounds-lower 220 | 221 | test-bounds-upper: ttc-test-bounds-upper 222 | test-bounds-upper: # test upper bounds (Cabal only) 223 | .PHONY: test-bounds-upper 224 | 225 | test-build: hr 226 | test-build: build 227 | test-build: test 228 | test-build: doc-api 229 | test-build: # build, run tests, build API documentation * 230 | .PHONY: test-build 231 | 232 | test-nightly: # run tests for the latest Stackage nightly release (Stack only) 233 | > @make test MODE=stack RESOLVER=nightly 234 | .PHONY: test-nightly 235 | 236 | todo: # search for TODO items 237 | > @find . -type f \ 238 | > -not -path '*/\.*' \ 239 | > -not -path './build/*' \ 240 | > -not -path './project/*' \ 241 | > -not -path ./Makefile \ 242 | > | xargs grep -Hn TODO \ 243 | > | grep -v '^Binary file ' \ 244 | > || true 245 | .PHONY: todo 246 | 247 | ttc: hr 248 | ttc: # build ttc package * 249 | ifeq ($(MODE), stack) 250 | > stack build ttc $(STACK_ARGS) \ 251 | > --test --bench --no-run-tests --no-run-benchmarks 252 | else 253 | > cabal build ttc $(CABAL_ARGS) --enable-tests --enable-benchmarks 254 | endif 255 | .PHONY: ttc 256 | 257 | ttc-coverage: hr 258 | ttc-coverage: # ttc: run tests with code coverage * 259 | ifeq ($(MODE), stack) 260 | > stack test ttc $(STACK_ARGS) --coverage 261 | else 262 | > cabal test ttc $(CABAL_ARGS) \ 263 | > --enable-coverage --enable-tests --test-show-details=always 264 | endif 265 | .PHONY: ttc-coverage 266 | 267 | ttc-doc-api: hr 268 | ttc-doc-api: # ttc: build API documentation * 269 | ifeq ($(MODE), stack) 270 | > stack haddock ttc $(STACK_ARGS) 271 | else 272 | > cabal haddock ttc $(CABAL_ARGS) 273 | endif 274 | .PHONY: ttc-doc-api 275 | 276 | ttc-examples: hr 277 | ttc-examples: # build all buildable ttc-examples * 278 | ifeq ($(MODE), stack) 279 | > stack build ttc-examples $(STACK_ARGS) 280 | else 281 | > cabal build ttc-examples $(CABAL_ARGS) 282 | endif 283 | .PHONY: ttc-examples 284 | 285 | ttc-example-enum: hr 286 | ttc-example-enum: # build and run ttc-example-enum * 287 | ifeq ($(MODE), stack) 288 | > stack run $(STACK_ARGS) ttc-example-enum 289 | else 290 | > cabal run $(CABAL_ARGS) ttc-example-enum 291 | endif 292 | .PHONY: ttc-example-enum 293 | 294 | ttc-example-invalid: hr 295 | ttc-example-invalid: # build ttc-example-invalid, which should fail * 296 | ifeq ($(MODE), stack) 297 | > stack build ttc-examples $(STACK_ARGS) \ 298 | > --flag ttc-examples:ttc-example-invalid 299 | else 300 | > cabal build ttc-examples $(CABAL_ARGS) -f ttc-example-invalid 301 | endif 302 | .PHONY: ttc-example-invalid 303 | 304 | ttc-example-mkvalid: hr 305 | ttc-example-mkvalid: # build and run ttc-example-mkvalid * 306 | ifeq ($(MODE), stack) 307 | > stack run $(STACK_ARGS) ttc-example-mkvalid 308 | else 309 | > cabal run $(CABAL_ARGS) ttc-example-mkvalid 310 | endif 311 | .PHONY: ttc-example-mkvalid 312 | 313 | ttc-example-mkuvalid: hr 314 | ttc-example-mkuvalid: # build and run ttc-example-mkuvalid * 315 | ifeq ($(MODE), stack) 316 | > stack run $(STACK_ARGS) ttc-example-mkuvalid 317 | else 318 | > cabal run $(CABAL_ARGS) ttc-example-mkuvalid 319 | endif 320 | .PHONY: ttc-example-mkuvalid 321 | 322 | ttc-example-prompt: hr 323 | ttc-example-prompt: # build and run ttc-example-prompt * 324 | ifeq ($(MODE), stack) 325 | > stack run $(STACK_ARGS) ttc-example-prompt 326 | else 327 | > cabal run $(CABAL_ARGS) ttc-example-prompt 328 | endif 329 | .PHONY: ttc-example-prompt 330 | 331 | ttc-example-uname: hr 332 | ttc-example-uname: # build and run ttc-example-uname * 333 | ifeq ($(MODE), stack) 334 | > stack run $(STACK_ARGS) ttc-example-uname 335 | else 336 | > cabal run $(CABAL_ARGS) ttc-example-uname 337 | endif 338 | .PHONY: ttc-example-uname 339 | 340 | ttc-example-uvalidof: hr 341 | ttc-example-uvalidof: # build and run ttc-example-uvalidof * 342 | ifeq ($(MODE), stack) 343 | > stack run $(STACK_ARGS) ttc-example-uvalidof 344 | else 345 | > cabal run $(CABAL_ARGS) ttc-example-uvalidof 346 | endif 347 | .PHONY: ttc-example-uvalidof 348 | 349 | ttc-example-mkuvalidqq: hr 350 | ttc-example-mkuvalidqq: # build and run ttc-example-mkuvalidqq * 351 | ifeq ($(MODE), stack) 352 | > stack run $(STACK_ARGS) ttc-example-mkuvalidqq 353 | else 354 | > cabal run $(CABAL_ARGS) ttc-example-mkuvalidqq 355 | endif 356 | .PHONY: ttc-example-mkuvalidqq 357 | 358 | ttc-example-valid: hr 359 | ttc-example-valid: # build and run ttc-example-valid * 360 | ifeq ($(MODE), stack) 361 | > stack run $(STACK_ARGS) ttc-example-valid 362 | else 363 | > cabal run $(CABAL_ARGS) ttc-example-valid 364 | endif 365 | .PHONY: ttc-example-valid 366 | 367 | ttc-example-validof: hr 368 | ttc-example-validof: # build and run ttc-example-validof * 369 | ifeq ($(MODE), stack) 370 | > stack run $(STACK_ARGS) ttc-example-validof 371 | else 372 | > cabal run $(CABAL_ARGS) ttc-example-validof 373 | endif 374 | .PHONY: ttc-example-validof 375 | 376 | ttc-example-wrapper: hr 377 | ttc-example-wrapper: # build and run ttc-example-wrapper * 378 | ifeq ($(MODE), stack) 379 | > stack run $(STACK_ARGS) ttc-example-wrapper 380 | else 381 | > cabal run $(CABAL_ARGS) ttc-example-wrapper 382 | endif 383 | .PHONY: ttc-example-wrapper 384 | 385 | ttc-repl: # ttc: enter a REPL * 386 | ifeq ($(MODE), stack) 387 | > stack repl ttc $(STACK_ARGS) 388 | else 389 | > cabal repl ttc $(CABAL_ARGS) 390 | endif 391 | .PHONY: ttc-repl 392 | 393 | ttc-sdist: # ttc: create a source tarball for Hackage 394 | > $(eval BRANCH := $(shell git rev-parse --abbrev-ref HEAD)) 395 | > @test "${BRANCH}" = "main" || $(call die,"not in main branch") 396 | ifeq ($(MODE), stack) 397 | > @stack sdist ttc 398 | else 399 | > @cabal sdist ttc 400 | endif 401 | .PHONY: ttc-sdist 402 | 403 | ttc-test: hr 404 | ttc-test: # ttc: run tests, optionally for pattern P * 405 | > $(eval P := "") 406 | ifeq ($(MODE), stack) 407 | > @test -z "$(P)" \ 408 | > && stack test ttc $(STACK_ARGS) \ 409 | > || stack test ttc $(STACK_ARGS) --test-arguments '--pattern $(P)' 410 | else 411 | > @test -z "$(P)" \ 412 | > && cabal test ttc $(CABAL_ARGS) \ 413 | > --enable-tests --test-show-details=always \ 414 | > || cabal test ttc $(CABAL_ARGS) \ 415 | > --enable-tests --test-show-details=always \ 416 | > --test-option '--pattern=$(P)' 417 | endif 418 | .PHONY: ttc-test 419 | 420 | ttc-test-all: # ttc: run all configured tests and build examples using MODE 421 | ifeq ($(MODE), stack) 422 | > @make ttc-test-build CONFIG=stack-8.8.4.yaml 423 | > @make ttc-test-build CONFIG=stack-8.10.7.yaml 424 | > @make ttc-test-build CONFIG=stack-9.0.2.yaml 425 | > @make ttc-test-build CONFIG=stack-9.2.8.yaml 426 | > @make ttc-test-build CONFIG=stack-9.4.8.yaml 427 | > @make ttc-test-build CONFIG=stack-9.6.6.yaml 428 | > @make ttc-test-build CONFIG=stack-9.8.2.yaml 429 | > @make ttc-test-build CONFIG=stack-9.10.1.yaml 430 | else 431 | > @make ttc-test-build GHC_VERSION=8.8.4 432 | > @make ttc-test-build GHC_VERSION=8.10.7 433 | > @make ttc-test-build GHC_VERSION=9.0.2 434 | > @make ttc-test-build GHC_VERSION=9.2.8 435 | > @make ttc-test-build GHC_VERSION=9.4.8 436 | > @make ttc-test-build GHC_VERSION=9.6.6 437 | > @make ttc-test-build GHC_VERSION=9.8.2 438 | > @make ttc-test-build GHC_VERSION=9.10.1 439 | endif 440 | .PHONY: ttc-test-all 441 | 442 | ttc-test-bounds-lower: # ttc: test lower bounds (Cabal only) 443 | > @make ttc-test-build MODE=cabal \ 444 | > CABAL_ARGS="--project-file=cabal-ttc-bounds-lower.project" 445 | .PHONY: ttc-test-bounds-lower 446 | 447 | ttc-test-bounds-upper: # ttc: test upper bounds (Cabal only) 448 | > @make ttc-test-build MODE=cabal \ 449 | > CABAL_ARGS="--project-file=cabal-ttc-bounds-upper.project" 450 | .PHONY: ttc-test-bounds-upper 451 | 452 | ttc-test-build: hr 453 | ttc-test-build: ttc 454 | ttc-test-build: ttc-test 455 | ttc-test-build: ttc-doc-api 456 | ttc-test-build: ttc-examples 457 | ttc-test-build: # ttc: build, run tests, build API documentation, build examples * 458 | .PHONY: ttc-test-build 459 | 460 | ttc-test-nightly: # ttc: run tests for the latest Stackage nightly release (Stack only) 461 | > @make ttc-test MODE=stack RESOLVER=nightly 462 | .PHONY: ttc-test-nightly 463 | 464 | version: # show current versions 465 | > @echo "ttc $(call get_version, ttc/ttc.cabal)" 466 | > @echo "ttc-examples $(call get_version, ttc-examples/ttc-examples.cabal)" 467 | .PHONY: version 468 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TTC: Textual Type Classes 2 | 3 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 4 | [![GitHub CI](https://github.com/ExtremaIS/ttc-haskell/workflows/CI/badge.svg?branch=main)](https://github.com/ExtremaIS/ttc-haskell/actions) 5 | 6 | TTC, an initialism of _Textual Type Classes_, is a library that provides 7 | `Render` and `Parse` type classes for conversion between data types and 8 | textual data types (strings). Use the `Show` and `Read` type classes for 9 | debugging/development, and use the `Render` and `Parse` type classes for your 10 | own purposes. The library also provides various ways to validate constants at 11 | compile-time. 12 | 13 | ## Packages 14 | 15 | This repository is used to maintain multiple, related Haskell packages. 16 | 17 | * Package `ttc` uses a `Textual` type class for conversion between textual 18 | data types. This package uses simple types and compiles quickly, but the 19 | supported textual data types are fixed. It is not possible for users to 20 | add support for additional textual data types. 21 | * Package `ttc-examples` contains `ttc` usage examples. 22 | 23 | ## Related Work 24 | 25 | ### Rendering and Parsing 26 | 27 | The [relude][] library has polymorphic versions of `show` and `readEither` in 28 | `Relude.String.Conversion`, as well as various type classes for converting 29 | between string types. This does not encourage using `Show` and `Read` 30 | instances with syntactically valid Haskell syntax, and it encourages the using 31 | of the `String` data type. 32 | 33 | The [rio][] library has a `Display` type class with a similar goal as 34 | `TTC.Render`. Since the library encourages a uniform usage of textual data 35 | types, `Display` only provides functions for rendering to `Text` and a builder 36 | format. It does not have a type class similar to `TTC.Parse`. 37 | 38 | The [text-display][] library defines a `Display` type class intended to render 39 | user-facing text. It uses a `Builder` type internally and renders to a `Text` 40 | value. 41 | 42 | Harry Garrood has an interesting series of blog posts about type classes and 43 | `Show`: 44 | 45 | * [Down with Show! Part 1: Rules of thumb for when to use a type class][] 46 | * [Down with Show! Part 2: What's wrong with the Show type class][] 47 | * [Down with Show! Part 3: A replacement for Show][] 48 | 49 | [relude]: 50 | [rio]: 51 | [text-display]: 52 | [Down with Show! Part 1: Rules of thumb for when to use a type class]: 53 | [Down with Show! Part 2: What's wrong with the Show type class]: 54 | [Down with Show! Part 3: A replacement for Show]: 55 | 56 | ### Validating Constants 57 | 58 | The [qq-literals][] library creates a `QuasiQuoter` from a parse function of 59 | type `String -> Either String a`. The functionality is similar to TTC's 60 | `mkUntypedValidQQ` function. The `mkUntypedValidQQ` function allows the user 61 | to choose the name of the `QuasiQuoter` because a name like `valid` is 62 | preferred when used via a qualified import while a name like `username` may be 63 | preferred when not using qualified imports. Note that `mkUntypedValidQQ` also 64 | splices in an explicit type signature. 65 | 66 | The [validated-literals][] library has a `Validate` type class that is similar 67 | to `TTC.Parse` but supports conversion between arbitrary types, not just from 68 | textual data types. Template Haskell functions are provided to perform 69 | validation at compile-time. Result types must either have `Lift` instances or 70 | equivalent implementations. 71 | 72 | Chris Done posted [a gist][] about implementing statically checked overloaded 73 | strings. 74 | 75 | [qq-literals]: 76 | [validated-literals]: 77 | [a gist]: 78 | 79 | ### String Type Conversion 80 | 81 | There are a number of libraries that simplify conversion between string types. 82 | 83 | The following libraries provide type classes with two type variables. The 84 | primary benefit of this approach is that one can add support for any string 85 | type. The drawback of this approach is that implementations of `Render` and 86 | `Parse` using such a type class would have to be done via a fixed type, 87 | resulting in unnecessary conversion when using other types. 88 | 89 | * [string-conv][] 90 | * [string-conversions][] 91 | 92 | The following library provide type classes with a single type variable, but 93 | conversion is done via a fixed type. 94 | 95 | * [hxt-regex-xmlschema][] has a `StringLike` type class and does conversion 96 | via the `String` type 97 | * [ListLike][] has a `StringLike` type class and does conversion via the 98 | `String` type 99 | * [monoid-subclasses][] provides a `TextualMonoid` type class that provides an 100 | abstract API over textual types, using `String` as the underlying type 101 | * [stringlike][] converts via the `Text` type 102 | * [tagsoup][] has a `StringLike` type class that provides an abstract API over 103 | textual types and a `castString` function that converts via the `String` 104 | type 105 | * [text-conversions][] converts via the `Text` type 106 | * [textual][] (deprecated) converts via the `String` type 107 | 108 | [string-conv]: 109 | [string-conversions]: 110 | [hxt-regex-xmlschema]: 111 | [ListLike]: 112 | [monoid-subclasses]: 113 | [stringlike]: 114 | [tagsoup]: 115 | [text-conversions]: 116 | [textual]: 117 | 118 | ### Arbitrary Type Conversion 119 | 120 | There are also a number of libraries that provide type classes for conversion 121 | between arbitrary types, including string types. 122 | 123 | * [basement][] provides type classes for conversion that may fail as well as 124 | conversion that cannot fail 125 | * [convertible][] 126 | * [witch][] provides type classes for conversion that may fail as well as 127 | conversion that cannot fail 128 | 129 | [basement]: 130 | [convertible]: 131 | [witch]: 132 | 133 | ## Project 134 | 135 | ### Links 136 | 137 | * GitHub: 138 | * GitHub Actions CI: 139 | 140 | ### Branches 141 | 142 | The `main` branch is reserved for releases. It may be considered stable, and 143 | `HEAD` is always the latest release. 144 | 145 | The `develop` branch is the primary development branch. It contains changes 146 | that have not yet been released, and it is not necessarily stable. 147 | 148 | [Hackage revisions][] are made for metadata changes, such as relaxation of 149 | constraints when new versions of dependencies are released. The `.cabal` 150 | metadata in the `main` branch may therefore not match that of Hackage. The 151 | `.cabal` metadata in the `develop` branch may match, *unless* work is being 152 | done on a new release that contains other changes. 153 | 154 | [Hackage revisions]: 155 | 156 | ### Tags 157 | 158 | All releases are tagged in the `main` branch. Release tags are signed using 159 | the [`security@extrema.is` GPG key][]. 160 | 161 | [`security@extrema.is` GPG key]: 162 | 163 | ### Contribution 164 | 165 | Issues and feature requests are tracked on GitHub: 166 | 167 | 168 | Issues may also be submitted via email to . 169 | 170 | ### License 171 | 172 | This project is released under the [MIT License][] as specified in the 173 | [`LICENSE`][] file. 174 | 175 | [MIT License]: 176 | [`LICENSE`]: 177 | -------------------------------------------------------------------------------- /cabal-ttc-bounds-lower.project: -------------------------------------------------------------------------------- 1 | packages: 2 | ttc/ttc.cabal 3 | ttc-examples/ttc-examples.cabal 4 | 5 | with-compiler: ghc-8.8.4 6 | 7 | constraints: 8 | -- https://hackage.haskell.org/package/base 9 | -- GHC boot library 10 | base == 4.13.0.0 11 | 12 | -- https://hackage.haskell.org/package/bytestring 13 | -- GHC boot library 14 | , bytestring == 0.10.10.1 15 | 16 | -- https://hackage.haskell.org/package/tasty 17 | , tasty == 1.2.3 18 | 19 | -- https://hackage.haskell.org/package/tasty-hunit 20 | , tasty-hunit == 0.10.0.3 21 | 22 | -- https://hackage.haskell.org/package/template-haskell 23 | -- GHC boot library 24 | , template-haskell == 2.15.0.0 25 | 26 | -- https://hackage.haskell.org/package/text 27 | -- GHC boot library 28 | , text == 1.2.4.0 29 | 30 | -- https://hackage.haskell.org/package/text-short 31 | , text-short == 0.1.3 32 | -------------------------------------------------------------------------------- /cabal-ttc-bounds-upper.project: -------------------------------------------------------------------------------- 1 | packages: 2 | ttc/ttc.cabal 3 | ttc-examples/ttc-examples.cabal 4 | 5 | with-compiler: ghc-9.12.1 6 | 7 | constraints: 8 | -- https://hackage.haskell.org/package/base 9 | -- GHC boot library 10 | base == 4.21.0.0 11 | 12 | -- https://hackage.haskell.org/package/bytestring 13 | -- GHC boot library 14 | , bytestring == 0.12.2.0 15 | 16 | -- https://hackage.haskell.org/package/tasty 17 | , tasty == 1.5.2 18 | 19 | -- https://hackage.haskell.org/package/tasty-hunit 20 | , tasty-hunit == 0.10.2 21 | 22 | -- https://hackage.haskell.org/package/template-haskell 23 | -- GHC boot library 24 | , template-haskell == 2.23.0.0 25 | 26 | -- https://hackage.haskell.org/package/text 27 | -- GHC boot library 28 | , text == 2.1.2 29 | 30 | -- https://hackage.haskell.org/package/text-short 31 | , text-short == 0.1.6 32 | -------------------------------------------------------------------------------- /cabal.project: -------------------------------------------------------------------------------- 1 | packages: 2 | ttc/ttc.cabal 3 | ttc-examples/ttc-examples.cabal 4 | -------------------------------------------------------------------------------- /project/TODO.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` TODO 2 | 3 | ## Functionality 4 | 5 | ## Tests 6 | 7 | ## Compatibility 8 | 9 | ### Future 10 | 11 | * [`linear-builder`](https://github.com/Bodigrim/linear-builder) 12 | support requires GHC 9.2 or later. 13 | 14 | * Support for GHC 8.8 and Cabal 3.0 may be removed after 2025-07-15. 15 | 16 | ## Documentation 17 | 18 | ## Examples 19 | 20 | ## Project 21 | -------------------------------------------------------------------------------- /project/announcement/ttc-haskell-1.0.0.0-haskell-cafe.md: -------------------------------------------------------------------------------- 1 | From: Travis Cardwell 2 | To: Haskell Cafe 3 | Subject: [ANN] ttc-1.0.0.0 - Textual Type Classes 4 | 5 | I am happy to announce the 1.0 release of TTC (Textual Type Classes). 6 | 7 | The library provides the following functionality: 8 | 9 | * The Textual type class is used to convert between common textual data 10 | types. It can be used to write functions that accept or return values 11 | of any of these textual data types. 12 | 13 | * The Render type class is used to render a value as text. Avoid bugs 14 | by only using Show for debugging/development purposes. 15 | 16 | * The Parse type class is used to parse a value from text. Unlike Read, 17 | it has support for error messages. 18 | 19 | * Validate constants at compile-time using Parse instances. 20 | 21 | Links: 22 | 23 | * GitHub: https://github.com/ExtremaIS/ttc-haskell 24 | * Hackage: https://hackage.haskell.org/package/ttc 25 | * Guided Tour: https://www.extrema.is/articles/ttc-textual-type-classes 26 | 27 | Best regards, 28 | 29 | Travis 30 | 31 | ---- 32 | 33 | Post: 34 | -------------------------------------------------------------------------------- /project/announcement/ttc-haskell-1.0.0.0-reddit.md: -------------------------------------------------------------------------------- 1 | Title: [ANN] ttc-1.0.0.0 - Textual Type Classes 2 | 3 | I am happy to announce the 1.0 release of TTC (Textual Type Classes). 4 | 5 | The library provides the following functionality: 6 | 7 | * The Textual type class is used to convert between common textual data 8 | types. It can be used to write functions that accept or return values 9 | of any of these textual data types. 10 | 11 | * The Render type class is used to render a value as text. Avoid bugs 12 | by only using Show for debugging/development purposes. 13 | 14 | * The Parse type class is used to parse a value from text. Unlike Read, 15 | it has support for error messages. 16 | 17 | * Validate constants at compile-time using Parse instances. 18 | 19 | Links: 20 | 21 | * [GitHub](https://github.com/ExtremaIS/ttc-haskell) 22 | * [Hackage](https://hackage.haskell.org/package/ttc) 23 | * [Guided Tour](https://www.extrema.is/articles/ttc-textual-type-classes) 24 | 25 | ---- 26 | 27 | Post: 28 | -------------------------------------------------------------------------------- /project/log/20191123-design.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` Design Log 2 | 3 | Author 4 | : Travis Cardwell 5 | 6 | Date 7 | : 2019-11-23 8 | 9 | ## History 10 | 11 | When I started programming in Haskell, I used `Show` and `Read` instances as a 12 | way to render and parse strings in whatever format worked best for my 13 | programs. As I gained experience, I started to implement `Render` and `Parse` 14 | type classes in my projects. My initial motivation was to avoid use of the 15 | `String` type, but I found that reserving `Show` and `Read` instances for 16 | debugging/development while using `Render` and `Parse` instances for other 17 | purposes is convenient. I was already accustomed to the distinction from the 18 | `__repr__` and `__str__` methods in Python. 19 | 20 | After many years of writing `Render` and `Parse` implementations in various 21 | projects, I decided to create a library for the purpose. I think it provides 22 | enough benefit to justify an additional dependency. 23 | 24 | I hoped to name the package `textual` but found that such a package already 25 | existed. I am generally not a fan of using initialisms or acronyms for 26 | project names, but I decided to use one in this case because it can also be 27 | used as a relatively short name for the qualified import. 28 | 29 | ## Design Goals 30 | 31 | The primary design goal is to provide `Render` and `Parse` type classes for 32 | conversion between data types and custom-purpose strings. Using them with 33 | human-readable strings is the most common use case, but it is not a 34 | requirement. These type classes are separate from the `Show` and `Read` type 35 | classes, which can be used for debugging/development, with automatically 36 | derived instances in many cases. 37 | 38 | The secondary design goal is to make it easier to work with the many textual 39 | data types that are used in Haskell, which seems to be a common source of 40 | frustration. While using `Text` for all text is a noble goal (when working 41 | with encodings that are isomorphic with a subset of Unicode), one often has to 42 | deal with other types when working with third-party libraries. For example, 43 | most libraries (including this one) use the `String` type for error messages, 44 | many libraries have data types that require use of `Show` and `Read` 45 | instances, some libraries use `ByteString` because they support more than 46 | text, `FilePath` is a type alias for `String`, etc. The `Render` and `Parse` 47 | type classes make it easy to create instances via whatever textual data type 48 | is most natural, with minimal boilerplate. One does not need to keep in mind 49 | which types are used in the implementations, as TTC conversion should "do the 50 | right thing." 51 | 52 | Choosing which types to support in the `Textual` type class was not obvious. 53 | I often make heavy use of `Builder` types, so early implementations included 54 | support for such types. I eventually refactored the code so that `Textual` 55 | only converts between what I consider the core textual data types and use 56 | separate functions for conversion to/from `Builder` and `Short` types. The 57 | refactored code is more elegant and just as easy to use. The disadvantage is 58 | that I am not able to take advantage of the different `Text` `Builder` type 59 | constructors; all conversions are via lazy `Text`. If this causes an 60 | unacceptable hit on performance, I am open to changing the design in a future 61 | version. 62 | -------------------------------------------------------------------------------- /project/log/20210609-design.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` Design Log 2 | 3 | Author 4 | : Travis Cardwell 5 | 6 | Date 7 | : 2021-06-09 8 | 9 | ## Feedback 10 | 11 | The first public release of TTC was on November 23, 2019. I did not make a 12 | widespread announcement at that time because there were some things that I 13 | wanted to get done before doing so. Friends and coworkers knew about the 14 | project, but I did not get any feedback. 15 | 16 | It took longer than I expected before I was ready to make a widespread 17 | announcement, which I did on June 3, 2021 accompanying the `1.0.0.0` release. 18 | I was able to receive a lot of great feedback from the Haskell community! 19 | Based on this feedback, I decided to make one minor change and two major 20 | changes, to be released in the `1.1.0.0` release. 21 | 22 | ## Changes 23 | 24 | ### Default Instances 25 | 26 | When implementing a shared library, one must be careful about defining 27 | instances to type classes. Since a type can have at most one instance of a 28 | type class, users of a library are not able to write their own instance for a 29 | type if an instance already exists. 30 | 31 | TTC exported a `Data.TTC.Instances` module that provided some default `Render` 32 | and `Parse` instances for some core types. Use of these instances was 33 | optional, as a convenience to users. There was concern that users might 34 | import the module within the implementation of a library, causing frustration 35 | to users of that library. 36 | 37 | I changed the implementation to provide default instances instead, removing 38 | the `Data.TTC.Instances` module. With the new implementation, users can load 39 | the instance for a specific type (as opposed to all the core types) using a 40 | line like the following: 41 | 42 | ```haskell 43 | instance TTC.Render Int64 44 | ``` 45 | 46 | Such an orphan instance is still not advised in a shared library, but this 47 | method makes it more likely that the user will see the warning in the API 48 | documentation. 49 | 50 | ### Builder and Short `Textual` Instances 51 | 52 | The initial design of TTC included `Textual` instances of builder types, but 53 | I ended up removing them and using auxiliary functions instead because it 54 | made the implementation more elegant. After hearing some feedback, I thought 55 | about the design again and decided to add instances for builder and short 56 | types. The implementation is less elegant, but having these instances can 57 | make usage of the type class more concise and may even improve performance in 58 | some cases. 59 | 60 | ### `HasCallStack` Usage 61 | 62 | Through the feedback, I learned of many other Haskell libraries that provide 63 | type classes for type conversion. Taylor Fausak told me about his 64 | [witch](https://hackage.haskell.org/package/witch) library and asked me what I 65 | thought about it. (It is a nice library!) Reading through the code, I 66 | noticed that he uses `HasCallStack` with unsafe functions, which results in 67 | better error messages on failure. I like that idea and added `HasCallStack` 68 | constraints to the unsafe functions in TTC. 69 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.0.0.1.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.0.0.1` Release Notes 2 | 3 | Date 4 | : 2019-11-23 5 | 6 | ## Overview 7 | 8 | This is the first public release of TTC. 9 | 10 | The following changes are planned for the upcoming `0.1.0.0` release: 11 | 12 | * improved documentation 13 | * more example code 14 | * continuous integration 15 | * Hackage package 16 | * Stackage package 17 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.0.0.2.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.0.0.2` Release Notes 2 | 3 | Date 4 | : 2019-11-28 5 | 6 | ## Overview 7 | 8 | This is a patch release of TTC, made in preparation for release to Hackage. 9 | There are no changes to the code in this release. 10 | 11 | The following changes are planned for the upcoming `0.1.0.0` release: 12 | 13 | * improved documentation 14 | * more example code 15 | * continuous integration 16 | * Hackage package 17 | * Stackage package 18 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.0.0.3.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.0.0.3` Release Notes 2 | 3 | Date 4 | : 2019-11-28 5 | 6 | ## Overview 7 | 8 | This is a patch release of TTC, adding continuous integration support. There 9 | are no changes to the code in this release. 10 | 11 | The following changes are planned for the upcoming `0.1.0.0` release: 12 | 13 | * improved documentation 14 | * more example code 15 | * Hackage package 16 | * Stackage package 17 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.0.0.4.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.0.0.4` Release Notes 2 | 3 | Date 4 | : 2019-11-30 5 | 6 | ## Overview 7 | 8 | This is a patch release of TTC, preparing for release to Hackage. There are 9 | no changes to the code in this release. 10 | 11 | This version will be used to create a Hackage package candidate. If any 12 | issues are found, they will be resolved before making a release. When the 13 | package is ready, version `0.1.0.0` will be released. 14 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.1.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.1.0.0` Release Notes 2 | 3 | Date 4 | : 2019-12-01 5 | 6 | ## Overview 7 | 8 | This is the first major release of TTC. 9 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.1.0.1.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.1.0.1` Release Notes 2 | 3 | Date 4 | : 2019-12-02 5 | 6 | ## Overview 7 | 8 | This is a patch release of TTC. It bumps the `time` dependency version upper 9 | bound. Note that this dependency is only used in example executables, not the 10 | library itself. There are no changes to the code in this release. 11 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.2.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.2.0.0` Release Notes 2 | 3 | Date 4 | : 2019-12-15 5 | 6 | ## Overview 7 | 8 | This is a major release of TTC. The primary change is the addition of untyped 9 | validation functions. There are no changes to the existing API. 10 | 11 | The examples are now split into a separate (`ttc-examples`) package, which is 12 | not in Hackage. They are still in the `examples` directory, existing examples 13 | have been refactored, and new examples have been added. The primary reason 14 | for using a separate package is to avoid listing example dependencies on the 15 | Hackage page, which is misleading. In previous versions, the `time` package 16 | is listed as a TTC dependency, but it is only used in a (gated) example. 17 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.2.1.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.2.1.0` Release Notes 2 | 3 | Date 4 | : 2020-05-11 5 | 6 | ## Overview 7 | 8 | This is a patch release of TTC. It includes project management changes and 9 | bumps the `tasty` dependency version upper bound. There are no changes to the 10 | code in this release. 11 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.2.2.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.2.2.0` Release Notes 2 | 3 | Date 4 | : 2020-05-17 5 | 6 | ## Overview 7 | 8 | This is a patch release of TTC. It includes a bump of the `template-haskell` 9 | dependency version upper bound. The `lift` example is updated for 10 | compatibility, but there are no other changes to the code in this release. 11 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.2.3.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.2.3.0` Release Notes 2 | 3 | Date 4 | : 2020-09-25 5 | 6 | ## Overview 7 | 8 | This is a patch release of TTC. It includes a bump of the `bytestring` 9 | dependency version upper bound. There are no other changes to the code in 10 | this release. 11 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.3.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.3.0.0` Release Notes 2 | 3 | Date 4 | : 2020-11-03 5 | 6 | ## Overview 7 | 8 | This is a major release of TTC. The `Parse` type class now uses `Textual` 9 | error messages, and the following function is added: 10 | 11 | * `maybeParseWithRead` 12 | 13 | To migrate existing code, `Parse` instances must be changed to return 14 | `Textual` errors instead of `String` errors. Calls to `parse` functions may 15 | be refactored to make use of the `Textual` error messages. If a call to a 16 | `parse` function breaks with an arbitrary type error, it may be an indicator 17 | that the error message is not used; consider using a `parseMaybe` function 18 | instead. 19 | 20 | This release should contain all of the API changes for the upcoming `1.0.0.0` 21 | release. 22 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-0.4.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `0.4.0.0` Release Notes 2 | 3 | Date 4 | : 2021-03-27 5 | 6 | ## Overview 7 | 8 | This is a major release of TTC, adding support for GHC 9. 9 | 10 | The following functions are added: 11 | 12 | * `renderTLB` renders to a `Text` `Builder`. 13 | * `renderBSB` renders to a `ByteString` `Builder`. 14 | * `renderSBS` renders to a `ShortByteString`. 15 | 16 | The following bugs are fixed: 17 | 18 | * `parseEnum'` is changed to return `Textual` error messages. 19 | 20 | To migrate existing code, only uses of `parseEnum'` need to be updated. 21 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.0.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.0.0.0` Release Notes 2 | 3 | Date 4 | : 2021-06-03 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README](https://github.com/ExtremaIS/ttc-haskell#readme) for details. 21 | 22 | ## This Release 23 | 24 | This `1.0.0.0` release accompanies the first widespread announcement. There 25 | are no changes to the API. 26 | 27 | ### Compatibility 28 | 29 | TTC is currently tested with [GHC 8.2.2][] ([Stackage LTS 11.22][]) through 30 | [GHC 9.0.1][]. The `.cabal` file uses Cabal version 1.24 (included with 31 | GHC 8.2.2), so it should build fine on relatively old Haskell installations 32 | as well as current installations. 33 | 34 | To use this release with a Stackage snapshot that does not include it, add 35 | the following to your `stack.yaml` configuration: 36 | 37 | ```yaml 38 | extra-deps: 39 | - ttc-1.0.0.0 40 | ``` 41 | 42 | [GHC 8.2.2]: 43 | [Stackage LTS 11.22]: 44 | [GHC 9.0.1]: 45 | 46 | ### Issues 47 | 48 | There are no known issues at this time. 49 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.1.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.1.0.0` Release Notes 2 | 3 | Date 4 | : 2021-06-10 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README](https://github.com/ExtremaIS/ttc-haskell#readme) for details. 21 | 22 | ## This Release 23 | 24 | This release includes changes made based on community feedback. 25 | 26 | ### Major Changes 27 | 28 | #### Default Instances 29 | 30 | The `Data.TTC.Instances` module is removed. The `Render` and `Parse` type 31 | classes now provide default instances, which allows you to load instances for 32 | specific types using a line like the following: 33 | 34 | ```haskell 35 | instance TTC.Render Int64 36 | ``` 37 | 38 | See the API documentation for details. 39 | 40 | Note that one must be careful about defining instances when implementing a 41 | shared library. Since a type can have at most one instance of a type class, 42 | users of the library are not able to write their own instance for a type if an 43 | instance already exists. 44 | 45 | #### Builder and Short `Textual` Instances 46 | 47 | The `Text` `Builder`, `ByteString` `Builder`, and `ShortByteString` types are 48 | now instances of the `Textual` type class. Usage of auxiliary functions is no 49 | longer necessary. 50 | 51 | ### Minor Changes 52 | 53 | #### `HasCallStack` Usage 54 | 55 | Unsafe functions now make use of `HasCallStack`, resulting in better error 56 | messages on failure. 57 | 58 | ### Compatibility 59 | 60 | TTC is currently tested with [GHC 8.2.2][] ([Stackage LTS 11.22][]) through 61 | [GHC 9.0.1][]. The `.cabal` file uses Cabal version 1.24 (included with 62 | GHC 8.2.2), so it should build fine on relatively old Haskell installations 63 | as well as current installations. 64 | 65 | To use this release with a Stackage snapshot that does not include it, add 66 | the following to your `stack.yaml` configuration: 67 | 68 | ```yaml 69 | extra-deps: 70 | - ttc-1.1.0.0 71 | ``` 72 | 73 | [GHC 8.2.2]: 74 | [Stackage LTS 11.22]: 75 | [GHC 9.0.1]: 76 | 77 | ### Issues 78 | 79 | There are no known issues at this time. 80 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.1.0.1.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.1.0.1` Release Notes 2 | 3 | Date 4 | : 2021-06-25 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README](https://github.com/ExtremaIS/ttc-haskell#readme) for details. 21 | 22 | ## This Release 23 | 24 | This release makes changes to the [Nix][] configuration. There are no changes 25 | to the TTC API. 26 | 27 | [Nix]: 28 | 29 | ### Nix Configuration 30 | 31 | * The `shell.nix` file now supports testing against the following GHC versions 32 | using known working `nixpkgs` revisions. 33 | * GHC 8.2.2 34 | * GHC 8.4.4 35 | * GHC 8.6.5 36 | * GHC 8.8.4 37 | * GHC 8.10.4 38 | * GHC 9.0.1 39 | 40 | * The `default.nix` file is removed. 41 | 42 | ### Compatibility 43 | 44 | TTC is currently tested with [GHC 8.2.2][] ([Stackage LTS 11.22][]) through 45 | [GHC 9.0.1][]. The `.cabal` file uses Cabal version 1.24 (included with 46 | GHC 8.2.2), so it should build fine on relatively old Haskell installations 47 | as well as current installations. 48 | 49 | To use this release with a Stackage snapshot that does not include it, add 50 | the following to your `stack.yaml` configuration: 51 | 52 | ```yaml 53 | extra-deps: 54 | - ttc-1.1.0.1 55 | ``` 56 | 57 | [GHC 8.2.2]: 58 | [Stackage LTS 11.22]: 59 | [GHC 9.0.1]: 60 | 61 | ### Issues 62 | 63 | There are no known issues at this time. 64 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.1.0.2.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.1.0.2` Release Notes 2 | 3 | Date 4 | : 2021-08-23 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README](https://github.com/ExtremaIS/ttc-haskell#readme) for details. 21 | 22 | ## This Release 23 | 24 | This release makes changes for compatibility with GHC 9.2.1 25 | ([rc1](https://discourse.haskell.org/t/ghc-9-2-1-rc1-is-now-available/2915)). 26 | There are no changes to the TTC API. 27 | 28 | ### Compatibility 29 | 30 | TTC is currently tested with [GHC 8.2.2][] ([Stackage LTS 11.22][]) through 31 | [GHC 9.0.1][]. The `.cabal` file uses Cabal version 1.24 (included with 32 | GHC 8.2.2), so it should build fine on relatively old Haskell installations 33 | as well as current installations. 34 | 35 | To use this release with a Stackage snapshot that does not include it, add 36 | the following to your `stack.yaml` configuration: 37 | 38 | ```yaml 39 | extra-deps: 40 | - ttc-1.1.0.2 41 | ``` 42 | 43 | [GHC 8.2.2]: 44 | [Stackage LTS 11.22]: 45 | [GHC 9.0.1]: 46 | 47 | ### Issues 48 | 49 | There are no known issues at this time. 50 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.1.1.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.1.1.0` Release Notes 2 | 3 | Date 4 | : 2021-12-25 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README][] for details. 21 | 22 | [README]: 23 | 24 | ## This Release 25 | 26 | This release bumps dependency versions to support [text-2.0][]. There are no 27 | changes to the TTC API. 28 | 29 | [text-2.0]: 30 | 31 | ### Compatibility 32 | 33 | TTC is currently tested with [GHC 8.2.2][] through [GHC 9.2.1][]. The 34 | `.cabal` file uses Cabal version 1.24 (included with GHC 8.2.2), so it should 35 | build fine on relatively old Haskell installations as well as current 36 | installations. 37 | 38 | To use this release with a Stackage snapshot that does not include it, add 39 | the following to your `stack.yaml` configuration: 40 | 41 | ```yaml 42 | extra-deps: 43 | - ttc-1.1.1.0 44 | ``` 45 | 46 | [GHC 8.2.2]: 47 | [GHC 9.2.1]: 48 | 49 | ### Issues 50 | 51 | There are no known issues at this time. 52 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.1.1.1.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.1.1.1` Release Notes 2 | 3 | Date 4 | : 2022-03-01 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README][] for details. 21 | 22 | [README]: 23 | 24 | ## This Release 25 | 26 | This is a patch release that makes updates to the package infrastructure. 27 | There are no changes to the TTC API. 28 | 29 | ### Compatibility 30 | 31 | TTC is currently tested with [GHC 8.2.2][] through [GHC 9.2.1][]. The 32 | `.cabal` file uses Cabal version 1.24 (included with GHC 8.2.2), so it should 33 | build fine on relatively old Haskell installations as well as current 34 | installations. 35 | 36 | To use this release with a Stackage snapshot that does not include it, add 37 | the following to your `stack.yaml` configuration: 38 | 39 | ```yaml 40 | extra-deps: 41 | - ttc-1.1.1.1 42 | ``` 43 | 44 | [GHC 8.2.2]: 45 | [GHC 9.2.1]: 46 | 47 | ### Issues 48 | 49 | There are no known issues at this time. 50 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.2.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.2.0.0` Release Notes 2 | 3 | Date 4 | : 2022-03-18 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README][] for details. 21 | 22 | [README]: 23 | 24 | ## This Release 25 | 26 | This release adds some functions for some common error message patterns in 27 | `Parse` instances. 28 | 29 | ### Major Changes 30 | 31 | #### New `Parse` Functions 32 | 33 | The `withError` function provides a convenient way to return the same error 34 | message for any parse error. Versions of this function with concrete error 35 | types are also available. 36 | 37 | The `prefixError` function provides a convenient way to add a common prefix to 38 | parse errors. Versions of this function with concrete error types are also 39 | available. 40 | 41 | ### Compatibility 42 | 43 | TTC is currently tested with [GHC 8.2.2][] through [GHC 9.2.2][]. The 44 | `.cabal` file uses Cabal version 1.24 (included with GHC 8.2.2), so it should 45 | build fine on relatively old Haskell installations as well as current 46 | installations. 47 | 48 | To use this release with a Stackage snapshot that does not include it, add 49 | the following to your `stack.yaml` configuration: 50 | 51 | ```yaml 52 | extra-deps: 53 | - ttc-1.2.0.0 54 | ``` 55 | 56 | [GHC 8.2.2]: 57 | [GHC 9.2.2]: 58 | 59 | ### Issues 60 | 61 | There are no known issues at this time. 62 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.2.1.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.2.1.0` Release Notes 2 | 3 | Date 4 | : 2023-03-21 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README][] for details. 21 | 22 | [README]: 23 | 24 | ## This Release 25 | 26 | TTC `1.2.0.0` was released more than one year ago, and a number of updates to 27 | dependency constraints have since been registered as [Hackage revisions][]. 28 | This maintenance release updates the package (tarball and `main` branch) to 29 | the latest state. 30 | 31 | This release also includes changes to the project management infrastructure. 32 | One important change is that both lower and upper bounds of dependencies are 33 | now tested in CI. 34 | 35 | [Hackage revisions]: 36 | 37 | ### Compatibility 38 | 39 | Build software: 40 | 41 | | Software | TTC 1.2.0.0 | TTC 1.2.1.0 | 42 | | ----------------- | ------------- | ------------- | 43 | | [GHC][] | 8.2.2 ~ 9.2.1 | 8.2.2 ~ 9.6.1 | 44 | | [cabal-install][] | 1.24 ~ 3.6 | 1.24 ~ 3.10 | 45 | 46 | Library dependencies: 47 | 48 | | Package | TTC 1.2.0.0 | TTC 1.2.1.0 | 49 | | -------------------- | ------------------- | ------------------- | 50 | | [base][] | `>=4.7 && <5` | `>=4.10.1 && <4.19` | 51 | | [bytestring][] | `>=0.10.8 && <0.12` | `>=0.10.8 && <0.12` | 52 | | [template-haskell][] | `>=2.12 && <2.19` | `>=2.12 && <2.21` | 53 | | [text][] | `>=1.2.3 && <2.1` | `>=1.2.2 && <2.1` | 54 | 55 | Test dependencies: 56 | 57 | | Package | TTC 1.2.0.0 | TTC 1.2.1.0 | 58 | | --------------- | ----------------- | ---------------- | 59 | | [tasty][] | `>=1.0 && <1.5` | `>=0.11 && <1.5` | 60 | | [tasty-hunit][] | `>=0.10 && <0.11` | `>=0.8 && <0.11` | 61 | 62 | To use this release with a Stackage snapshot that does not include it, add 63 | the following to your `stack.yaml` configuration: 64 | 65 | ```yaml 66 | extra-deps: 67 | - ttc-1.2.1.0 68 | ``` 69 | 70 | [GHC]: 71 | [cabal-install]: 72 | [base]: 73 | [bytestring]: 74 | [template-haskell]: 75 | [text]: 76 | [tasty]: 77 | [tasty-hunit]: 78 | 79 | ### Issues 80 | 81 | There are no known issues at this time. 82 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.3.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.3.0.0` Release Notes 2 | 3 | Date 4 | : 2023-09-17 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README][] for details. 21 | 22 | [README]: 23 | 24 | ## This Release 25 | 26 | This release adds support for validating constants at compile-time without 27 | having to type `valid` and adds a new set of `Parse` functions. 28 | 29 | ### Major Changes 30 | 31 | #### Minimal Compile-Time Validation Syntax 32 | 33 | The `valid` function uses a `Parse` instance to validate a constant at 34 | compile-time, requiring that the parsed type has a `Lift` instance. This 35 | release adds an `IsString` instance for typed Template Haskell expressions so 36 | that string syntax automatically calls `valid`. See the orphan instances 37 | section of the documentation for details and usage examples. 38 | 39 | #### New `Parse` Functions 40 | 41 | The `parseOrFail` set of functions fail using `MonadFail` on error. 42 | 43 | ### Compatibility 44 | 45 | TTC is currently tested with [GHC 8.2.2][] through [GHC 9.6.2][]. The 46 | `.cabal` file uses Cabal version 1.24 (included with GHC 8.2.2), so it should 47 | build fine on relatively old Haskell installations as well as current 48 | installations. 49 | 50 | To use this release with a Stackage snapshot that does not include it, add 51 | the following to your `stack.yaml` configuration: 52 | 53 | ```yaml 54 | extra-deps: 55 | - ttc-1.3.0.0 56 | ``` 57 | 58 | [GHC 8.2.2]: 59 | [GHC 9.6.2]: 60 | 61 | ### Issues 62 | 63 | There are no known issues at this time. 64 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.4.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.4.0.0` Release Notes 2 | 3 | Date 4 | : 2023-12-03 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README][] for details. 21 | 22 | [README]: 23 | 24 | ## This Release 25 | 26 | This release adds support for the `ShortText` type from the 27 | [`text-short`](https://hackage.haskell.org/package/text-short) package. 28 | 29 | ### Major Changes 30 | 31 | #### `ShortText` Support 32 | 33 | Support for `ShortText` is added using the `ST` suffix. Support is added to 34 | the `Textual` type class, and `ST`-suffix functions are added to the rest of 35 | the API. 36 | 37 | Thank you to [`@Qqwy`](https://github.com/Qqwy) for requesting this feature! 38 | 39 | ### Compatibility 40 | 41 | TTC is currently tested with [GHC 8.2.2][] through [GHC 9.8.1][]. The 42 | `.cabal` file uses Cabal version 1.24 (included with GHC 8.2.2), so it should 43 | build fine on relatively old Haskell installations as well as current 44 | installations. 45 | 46 | To use this release with a Stackage snapshot that does not include it, add 47 | the following to your `stack.yaml` configuration: 48 | 49 | ```yaml 50 | extra-deps: 51 | - ttc-1.4.0.0 52 | ``` 53 | 54 | [GHC 8.2.2]: 55 | [GHC 9.8.1]: 56 | 57 | ### Issues 58 | 59 | There are no known issues at this time. 60 | -------------------------------------------------------------------------------- /project/release/ttc-haskell-1.5.0.0.md: -------------------------------------------------------------------------------- 1 | # `ttc-haskell` `1.5.0.0` Release Notes 2 | 3 | Date 4 | : 2025-01-02 5 | 6 | ## Overview 7 | 8 | TTC, an initialism of _Textual Type Classes_, is a library that provides the 9 | following functionality: 10 | 11 | * The `Textual` type class is used to convert between common textual data 12 | types. It can be used to write functions that accept or return values of 13 | any of these textual data types. 14 | * The `Render` type class is used to render a value as text. Avoid bugs by 15 | only using `Show` for debugging/development purposes. 16 | * The `Parse` type class is used to parse a value from text. Unlike `Read`, 17 | it has support for error messages. 18 | * Validate constants at compile-time using `Parse` instances. 19 | 20 | See the [README][] for details. 21 | 22 | [README]: 23 | 24 | ## This Release 25 | 26 | A new generation of this library is under development. It will be released as 27 | a separate package, and TTC will still be maintained, so TTC users will not be 28 | forced to upgrade. Some features are being backported to TTC, when possible. 29 | This release contains a number of these changes, in addition to the usual 30 | maintenance (compatibility) changes. 31 | 32 | ### Added Wrappers 33 | 34 | Module `Data.TTC.Wrapper` defines TTC instances for `newtype` wrappers around 35 | textual data types. This makes it trivial to add TTC support for such data 36 | types, using `deriving via`, reducing the temptation to just use `Show` 37 | instances in early development of a project. 38 | 39 | ### `TypeApplications` Changes 40 | 41 | TTC provides type-specific functions using suffixes to indicate the type, like 42 | in `withErrorT`, so that users can avoid `TypeApplications` if desired. This 43 | is possible because the supported textual data types is fixed. The new 44 | library instead encourages use of `TypeApplications`, and the type argument 45 | order of some functions is changes to make doing so easier. These changes are 46 | backported to TTC for consistency. 47 | 48 | If you use TTC with `TypeApplications`, you may need to change the order of 49 | type arguments in some places. 50 | 51 | ### Added Instances 52 | 53 | Missing `RenderDefault` and `ParseDefault` instances for `TLB.Builder`, 54 | `ST.ShortText`, `BSB.Builder`, and `SBS.ShortByteString` are added. 55 | 56 | `RenderDefault` and `ParseDefault` instances for `Bool` are added. 57 | 58 | ### Default Instances 59 | 60 | TTC provides default `Render` and `Parse` instances for common data types, 61 | allowing users to optionally load them. 62 | 63 | This release adds Template Haskell functions that can do the same thing. One 64 | benefit is that it is more concise when loading default instances for many 65 | types. 66 | 67 | ### Compatibility 68 | 69 | TTC is currently tested with [GHC 8.8.4][] through [GHC 9.12.1][]. The 70 | `.cabal` file uses Cabal version 3.0.0.0 (included in GHC 8.8.4), so it should 71 | build fine on relatively old Haskell installations as well as current 72 | installations. Note that support for software released more than five years 73 | ago has been removed. 74 | 75 | To use this release with a Stackage snapshot that does not include it, add 76 | the following to your `stack.yaml` configuration: 77 | 78 | ```yaml 79 | extra-deps: 80 | - ttc-1.5.0.0 81 | ``` 82 | 83 | [GHC 8.8.4]: 84 | [GHC 9.12.1]: 85 | 86 | ### Issues 87 | 88 | There are no known issues at this time. 89 | -------------------------------------------------------------------------------- /stack-8.10.7.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-18.28 2 | 3 | packages: 4 | - ./ttc 5 | - ./ttc-examples 6 | 7 | flags: 8 | ttc-examples: 9 | optparse-applicative_ge_0_18: false 10 | -------------------------------------------------------------------------------- /stack-8.8.4.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-16.31 2 | 3 | packages: 4 | - ./ttc 5 | - ./ttc-examples 6 | 7 | flags: 8 | ttc-examples: 9 | optparse-applicative_ge_0_18: false 10 | -------------------------------------------------------------------------------- /stack-9.0.2.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-19.33 2 | 3 | packages: 4 | - ./ttc 5 | - ./ttc-examples 6 | 7 | flags: 8 | ttc-examples: 9 | optparse-applicative_ge_0_18: false 10 | -------------------------------------------------------------------------------- /stack-9.10.1.yaml: -------------------------------------------------------------------------------- 1 | resolver: nightly-2025-01-01 2 | 3 | packages: 4 | - ./ttc 5 | - ./ttc-examples 6 | -------------------------------------------------------------------------------- /stack-9.12.1.yaml: -------------------------------------------------------------------------------- 1 | resolver: ghc-9.12.1 2 | 3 | packages: 4 | - ./ttc 5 | - ./ttc-examples 6 | 7 | extra-deps: 8 | - ansi-terminal-1.1.2 9 | - ansi-terminal-types-1.1 10 | - call-stack-0.4.0 11 | - colour-2.3.6 12 | - hashable-1.5.0.0 13 | - optparse-applicative-0.18.1.0 14 | - prettyprinter-1.7.1 15 | - prettyprinter-ansi-terminal-1.1.3 16 | - tagged-0.8.9 17 | - tasty-1.5.2 18 | - tasty-hunit-0.10.2 19 | - text-short-0.1.6 20 | - transformers-compat-0.7.2 21 | -------------------------------------------------------------------------------- /stack-9.2.8.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-20.26 2 | 3 | packages: 4 | - ./ttc 5 | - ./ttc-examples 6 | 7 | flags: 8 | ttc-examples: 9 | optparse-applicative_ge_0_18: false 10 | -------------------------------------------------------------------------------- /stack-9.4.8.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-21.25 2 | 3 | packages: 4 | - ./ttc 5 | - ./ttc-examples 6 | 7 | flags: 8 | ttc-examples: 9 | optparse-applicative_ge_0_18: false 10 | -------------------------------------------------------------------------------- /stack-9.6.6.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-22.43 2 | 3 | packages: 4 | - ./ttc 5 | - ./ttc-examples 6 | -------------------------------------------------------------------------------- /stack-9.8.4.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-23.2 2 | 3 | packages: 4 | - ./ttc 5 | - ./ttc-examples 6 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | stack-9.8.4.yaml -------------------------------------------------------------------------------- /ttc-examples/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019-2025 Travis Cardwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ttc-examples/README.md: -------------------------------------------------------------------------------- 1 | # TTC Examples 2 | 3 | ## `uname` 4 | 5 | This is a minimal example of using `Render` and `Parse` instances. A username 6 | is parsed, rendered, and printed to the screen. 7 | 8 | Source: 9 | 10 | * [Username.hs](uname/Username.hs) 11 | * [uname.hs](uname/uname.hs) 12 | 13 | To run the example, run the following command from the `ttc-haskell` project 14 | directory: 15 | 16 | $ make ttc-example-uname 17 | 18 | ## `prompt` 19 | 20 | This is another example of using `Render` and `Parse` instances. The program 21 | prompts for credit card details, with error handling, and then prints out the 22 | normalized values as well as information about expiration. 23 | 24 | Source: 25 | 26 | * [CreditCard.hs](prompt/CreditCard.hs) 27 | * [prompt.hs](prompt/prompt.hs) 28 | 29 | To run the example, run the following command from the `ttc-haskell` project 30 | directory: 31 | 32 | $ make ttc-example-prompt 33 | 34 | ## `enum` 35 | 36 | This is an example of `parseEnum` usage. The program prints a greeting in 37 | the specified language, with support for case-insensitive prefixes. 38 | 39 | Source: 40 | 41 | * [enum.hs](prompt/enum.hs) 42 | 43 | To run the example, run the following command from the `ttc-haskell` project 44 | directory: 45 | 46 | $ make ttc-example-enum 47 | 48 | ## `valid` 49 | 50 | This is a minimal example of using `valid` to validate a constant at 51 | compile-time. The `Username` data type wraps `String` and is able to derive a 52 | `Lift` instance automatically using the `DeriveLift` extension. Note that 53 | HLint is currently unable to check source that makes use of typed expression 54 | slices. 55 | 56 | Source: 57 | 58 | * [Username.hs](valid/Username.hs) 59 | * [valid.hs](valid/valid.hs) 60 | 61 | To run the example, run the following command from the `ttc-haskell` project 62 | directory: 63 | 64 | $ make ttc-example-valid 65 | 66 | ## `invalid` 67 | 68 | This is a minimal example of using `valid` to validate a constant at 69 | compile-time. It is the same as the `valid` example except that the username 70 | is not valid, causing a compile-time validation error. 71 | 72 | Source: 73 | 74 | * [Username.hs](invalid/Username.hs) 75 | * [invalid.hs](invalid/invalid.hs) 76 | 77 | To run the example, run the following command from the `ttc-haskell` project 78 | directory: 79 | 80 | $ make ttc-example-invalid 81 | 82 | The compilation fails due to the invalid username. 83 | 84 | ## `validof` 85 | 86 | This is an example of using `validOf` to validate a constant at compile-time. 87 | The `Username` data type does not need a `Lift` instance because the `String` 88 | is compiled in. The `String` is parsed twice: once at compile-time for 89 | validation, and again at run-time. 90 | 91 | Source: 92 | 93 | * [Username.hs](validof/Username.hs) 94 | * [validof.hs](validof/validof.hs) 95 | 96 | To run the example, run the following command from the `ttc-haskell` project 97 | directory: 98 | 99 | $ make ttc-example-validof 100 | 101 | ## `mkvalid` 102 | 103 | This is an example of using `mkValid` to make a `valid` function to validate a 104 | constant at compile-time. 105 | 106 | Source: 107 | 108 | * [Username.hs](mkvalid/Username.hs) 109 | * [mkvalid.hs](mkvalid/mkvalid.hs) 110 | 111 | To run the example, run the following command from the `ttc-haskell` project 112 | directory: 113 | 114 | $ make ttc-example-mkvalid 115 | 116 | ## `uvalidof` 117 | 118 | This is an example of using `untypedValidOf` to validate a constant at 119 | compile-time. The `Username` data type does not need a `Lift` instance 120 | because the `String` is compiled in. The `String` is parsed twice: once at 121 | compile-time for validation, and again at run-time. Note that HLint works 122 | fine because typed expression slices are not used. 123 | 124 | Source: 125 | 126 | * [Username.hs](uvalidof/Username.hs) 127 | * [uvalidof.hs](uvalidof/uvalidof.hs) 128 | 129 | To run the example, run the following command from the `ttc-haskell` project 130 | directory: 131 | 132 | $ make ttc-example-uvalidof 133 | 134 | ## `mkuvalid` 135 | 136 | This is an example of using `mkUntypedValid` to make a `valid` function to 137 | validate a constant at compile-time. Note that HLint works fine because typed 138 | expression slices are not used. 139 | 140 | Source: 141 | 142 | * [Username.hs](mkuvalid/Username.hs) 143 | * [mkuvalid.hs](mkuvalid/mkuvalid.hs) 144 | 145 | To run the example, run the following command from the `ttc-haskell` project 146 | directory: 147 | 148 | $ make ttc-example-mkuvalid 149 | 150 | ## `mkuvalidqq` 151 | 152 | This is an example of using `mkUntypedValidQQ` to make a `valid` quasi-quoter 153 | to validate a constant at compile-time. Note that HLint works fine because 154 | typed expression slices are not used. 155 | 156 | Source: 157 | 158 | * [Username.hs](mkuvalidqq/Username.hs) 159 | * [mkuvalidqq.hs](mkuvalidqq/mkuvalidqq.hs) 160 | 161 | To run the example, run the following command from the `ttc-haskell` project 162 | directory: 163 | 164 | $ make ttc-example-mkuvalidqq 165 | 166 | ## `wrapper` 167 | 168 | This example demonstrates use of [DerivingVia][] to define TTC instances via a 169 | wrapper type. 170 | 171 | [DerivingVia]: 172 | 173 | Source: 174 | 175 | * [wrapper.hs](wrapper/wrapper.hs) 176 | 177 | To run the example, run the following command from the `ttc-haskell` project 178 | directory: 179 | 180 | $ make ttc-example-wrapper 181 | -------------------------------------------------------------------------------- /ttc-examples/enum/enum.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : example of parseEnum usage 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This example program prints greetings in multiple languages. The language 9 | -- is specified using a command line option and is parsed using 10 | -- 'TTC.parseEnum', supporting case-insensitive prefixes. 11 | ------------------------------------------------------------------------------ 12 | 13 | {-# LANGUAGE CPP #-} 14 | 15 | #if defined(MIN_VERSION_ansi_wl_pprint) 16 | #if MIN_VERSION_ansi_wl_pprint (1,0,2) 17 | {-# OPTIONS_GHC -Wno-warnings-deprecations #-} 18 | #endif 19 | #endif 20 | 21 | module Main (main) where 22 | 23 | -- https://hackage.haskell.org/package/ansi-wl-pprint 24 | #if !MIN_VERSION_optparse_applicative (0,18,0) 25 | import qualified Text.PrettyPrint.ANSI.Leijen as Doc 26 | import Text.PrettyPrint.ANSI.Leijen (Doc) 27 | #endif 28 | 29 | -- https://hackage.haskell.org/package/base 30 | #if !MIN_VERSION_base (4,11,0) 31 | import Data.Monoid ((<>)) 32 | #endif 33 | 34 | -- https://hackage.haskell.org/package/optparse-applicative 35 | import qualified Options.Applicative as OA 36 | #if MIN_VERSION_optparse_applicative (0,18,0) 37 | import Options.Applicative.Help.Pretty (Doc) 38 | #endif 39 | 40 | -- https://hackage.haskell.org/package/prettyprinter 41 | #if MIN_VERSION_optparse_applicative (0,18,0) 42 | import qualified Prettyprinter as Doc 43 | #endif 44 | 45 | -- https://hackage.haskell.org/package/ttc 46 | import qualified Data.TTC as TTC 47 | 48 | ------------------------------------------------------------------------------ 49 | 50 | -- Supported languages 51 | data Language 52 | = Chinese 53 | | English 54 | | French 55 | | German 56 | | Japanese 57 | | Korean 58 | | Spanish 59 | deriving (Bounded, Enum, Eq, Ord, Show) 60 | 61 | instance TTC.Parse Language where 62 | parse = TTC.parseEnum True True 63 | (TTC.fromS "unknown language") 64 | (TTC.fromS "ambiguous language") 65 | 66 | instance TTC.Render Language where 67 | render = TTC.renderWithShow 68 | 69 | -- Greetings for each language 70 | greeting :: Language -> String 71 | greeting Chinese = "你好!" 72 | greeting English = "Hello!" 73 | greeting French = "Bonjour!" 74 | greeting German = "Hallo!" 75 | greeting Japanese = "こんにちは!" 76 | greeting Korean = "안녕하세요!" 77 | greeting Spanish = "¡Hola!" 78 | 79 | ------------------------------------------------------------------------------ 80 | 81 | docString :: String -> Doc 82 | #if MIN_VERSION_optparse_applicative (0,18,0) 83 | docString = Doc.pretty 84 | #else 85 | docString = Doc.string 86 | #endif 87 | 88 | ------------------------------------------------------------------------------ 89 | 90 | -- Print a greeting in the specified language 91 | -- 92 | -- Languages are usually represented using standard language codes, but 93 | -- English language names are used in this example. 94 | main :: IO () 95 | main = putStrLn . greeting =<< OA.execParser pinfo 96 | where 97 | pinfo :: OA.ParserInfo Language 98 | pinfo = OA.info (OA.helper <*> langOption) $ mconcat 99 | [ OA.fullDesc 100 | , OA.footerDoc $ Just langHelp 101 | ] 102 | 103 | langOption :: OA.Parser Language 104 | langOption = OA.option (OA.eitherReader TTC.parse) $ mconcat 105 | [ OA.long "lang" 106 | , OA.short 'l' 107 | , OA.metavar "LANG" 108 | , OA.value defaultLanguage 109 | , OA.showDefaultWith TTC.render 110 | , OA.help "Greeting language" 111 | ] 112 | 113 | defaultLanguage :: Language 114 | defaultLanguage = English 115 | 116 | langHelp :: Doc 117 | langHelp = 118 | ((docString "Languages:" <> Doc.line) <>) . Doc.indent 2 $ Doc.vcat 119 | [ docString $ TTC.render lang 120 | | lang <- [minBound :: Language ..] 121 | ] 122 | -------------------------------------------------------------------------------- /ttc-examples/invalid/Username.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Username 4 | -- Description : username data type 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines a username data type, with 'TTC.Render' and 'TTC.Parse' 9 | -- instances, as an example of TTC usage. 10 | -- 11 | -- The constructor for 'Username' is not exported. The 'TTC.Parse' instance 12 | -- serves as a "smart constructor," ensuring that all values are valid. 13 | -- 14 | -- This version of the module provides a trivial example of deriving 15 | -- 'THS.Lift'. 16 | -- 17 | -- The 'Username' data type derives 'THS.Lift', using the @DeriveLift@ 18 | -- language extension. The underlying 'String' already has a 'THS.Lift' 19 | -- instance. 20 | ------------------------------------------------------------------------------ 21 | 22 | {-# LANGUAGE DeriveLift #-} 23 | 24 | module Username (Username) where 25 | 26 | -- https://hackage.haskell.org/package/base 27 | import Control.Monad (unless, when) 28 | import Data.Bifunctor (first) 29 | import Data.Char (isAsciiLower) 30 | 31 | -- https://hackage.haskell.org/package/template-haskell 32 | import qualified Language.Haskell.TH.Syntax as THS 33 | 34 | -- https://hackage.haskell.org/package/ttc 35 | import qualified Data.TTC as TTC 36 | 37 | ------------------------------------------------------------------------------ 38 | 39 | -- | A 'Username' must consist of 3 to 12 lowercase ASCII letters. 40 | newtype Username = Username { usernameString :: String } 41 | deriving (Eq, Ord, Show, THS.Lift) 42 | 43 | instance TTC.Parse Username where 44 | parse = TTC.asS $ \s -> first TTC.fromS $ do 45 | unless (all isAsciiLower s) $ Left "username has invalid character(s)" 46 | let len = length s 47 | when (len < 3) $ Left "username has fewer than 3 characters" 48 | when (len > 12) $ Left "username has more than 12 characters" 49 | pure $ Username s 50 | 51 | instance TTC.Render Username where 52 | render = TTC.convert . usernameString 53 | -------------------------------------------------------------------------------- /ttc-examples/invalid/invalid.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : example of compile-time validation error 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- 'TTC.valid' is used to create validated constants. The sample username 9 | -- is (in)validated at compile-time. 10 | ------------------------------------------------------------------------------ 11 | 12 | {-# LANGUAGE TemplateHaskell #-} 13 | 14 | module Main (main) where 15 | 16 | -- https://hackage.haskell.org/package/ttc 17 | import qualified Data.TTC as TTC 18 | 19 | -- (ttc-examples:ttc-example-invalid) 20 | import Username (Username) 21 | 22 | ------------------------------------------------------------------------------ 23 | 24 | sample :: Username 25 | sample = $$(TTC.valid "bad-username") 26 | 27 | ------------------------------------------------------------------------------ 28 | 29 | main :: IO () 30 | main = print sample 31 | -------------------------------------------------------------------------------- /ttc-examples/mkuvalid/Username.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Username 4 | -- Description : username data type 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines a username data type, with 'TTC.Render' and 'TTC.Parse' 9 | -- instances, as an example of TTC usage. 10 | -- 11 | -- The constructor for 'Username' is not exported. The 'TTC.Parse' instance 12 | -- serves as a "smart constructor," ensuring that all values are valid. 13 | -- 14 | -- This version of the module uses 'TTC.mkUntypedValid' to make a @valid@ 15 | -- function that does not require a 'Language.Haskell.TH.Syntax.Lift' 16 | -- instance. 17 | ------------------------------------------------------------------------------ 18 | 19 | {-# LANGUAGE TemplateHaskell #-} 20 | 21 | module Username 22 | ( -- * Type 23 | Username 24 | -- * API 25 | , valid 26 | ) where 27 | 28 | -- https://hackage.haskell.org/package/base 29 | import Control.Monad (unless, when) 30 | import Data.Bifunctor (first) 31 | import Data.Char (isAsciiLower) 32 | 33 | -- https://hackage.haskell.org/package/text 34 | import qualified Data.Text as T 35 | import Data.Text (Text) 36 | 37 | -- https://hackage.haskell.org/package/ttc 38 | import qualified Data.TTC as TTC 39 | 40 | ------------------------------------------------------------------------------ 41 | -- $Type 42 | 43 | -- | A 'Username' must consist of 3 to 12 lowercase ASCII letters. 44 | newtype Username = Username { usernameText :: Text } 45 | deriving (Eq, Ord, Show) 46 | 47 | instance TTC.Parse Username where 48 | parse = TTC.asT $ \t -> first TTC.fromS $ do 49 | unless (T.all isAsciiLower t) $ Left "username has invalid character(s)" 50 | let len = T.length t 51 | when (len < 3) $ Left "username has fewer than 3 characters" 52 | when (len > 12) $ Left "username has more than 12 characters" 53 | pure $ Username t 54 | 55 | instance TTC.Render Username where 56 | render = TTC.convert . usernameText 57 | 58 | ------------------------------------------------------------------------------ 59 | -- $API 60 | 61 | -- | Validate a 'Username' at compile-time 62 | $(TTC.mkUntypedValid "valid" ''Username) 63 | -------------------------------------------------------------------------------- /ttc-examples/mkuvalid/mkuvalid.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : example of compile-time validation 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- 'TTC.mkUntypedValid' is used to create @valid@ function, to validate 9 | -- constants of a specific type. The sample username is validated at 10 | -- compile-time. 11 | ------------------------------------------------------------------------------ 12 | 13 | {-# LANGUAGE TemplateHaskell #-} 14 | 15 | module Main (main) where 16 | 17 | -- (ttc-examples:ttc-example-mkuvalid) 18 | import qualified Username 19 | import Username (Username) 20 | 21 | ------------------------------------------------------------------------------ 22 | 23 | sample :: Username 24 | sample = $(Username.valid "tcard") 25 | 26 | ------------------------------------------------------------------------------ 27 | 28 | main :: IO () 29 | main = print sample 30 | -------------------------------------------------------------------------------- /ttc-examples/mkuvalidqq/Username.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Username 4 | -- Description : username data type 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines a username data type, with 'TTC.Render' and 'TTC.Parse' 9 | -- instances, as an example of TTC usage. 10 | -- 11 | -- The constructor for 'Username' is not exported. The 'TTC.Parse' instance 12 | -- serves as a "smart constructor," ensuring that all values are valid. 13 | -- 14 | -- This version of the module uses 'TTC.mkUntypedValidQQ' to make a @valid@ 15 | -- quasi-quoter, which does not require a 'Language.Haskell.TH.Syntax.Lift' 16 | -- instance. 17 | ------------------------------------------------------------------------------ 18 | 19 | {-# LANGUAGE TemplateHaskell #-} 20 | 21 | module Username 22 | ( -- * Type 23 | Username 24 | -- * API 25 | , valid 26 | ) where 27 | 28 | -- https://hackage.haskell.org/package/base 29 | import Control.Monad (unless, when) 30 | import Data.Bifunctor (first) 31 | import Data.Char (isAsciiLower) 32 | 33 | -- https://hackage.haskell.org/package/text 34 | import qualified Data.Text as T 35 | import Data.Text (Text) 36 | 37 | -- https://hackage.haskell.org/package/ttc 38 | import qualified Data.TTC as TTC 39 | 40 | ------------------------------------------------------------------------------ 41 | -- $Type 42 | 43 | -- | A 'Username' must consist of 3 to 12 lowercase ASCII letters. 44 | newtype Username = Username { usernameText :: Text } 45 | deriving (Eq, Ord, Show) 46 | 47 | instance TTC.Parse Username where 48 | parse = TTC.asT $ \t -> first TTC.fromS $ do 49 | unless (T.all isAsciiLower t) $ Left "username has invalid character(s)" 50 | let len = T.length t 51 | when (len < 3) $ Left "username has fewer than 3 characters" 52 | when (len > 12) $ Left "username has more than 12 characters" 53 | pure $ Username t 54 | 55 | instance TTC.Render Username where 56 | render = TTC.convert . usernameText 57 | 58 | ------------------------------------------------------------------------------ 59 | -- $API 60 | 61 | -- | Validate a 'Username' at compile-time 62 | $(TTC.mkUntypedValidQQ "valid" ''Username) 63 | -------------------------------------------------------------------------------- /ttc-examples/mkuvalidqq/mkuvalidqq.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : example of compile-time validation 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- 'TTC.mkUntypedValidQQ' is used to create @valid@ quasi-quoter, to validate 9 | -- constants of a specific type. The sample username is validated at 10 | -- compile-time. 11 | ------------------------------------------------------------------------------ 12 | 13 | {-# LANGUAGE QuasiQuotes #-} 14 | 15 | module Main (main) where 16 | 17 | -- (ttc-examples:ttc-example-mkuvalidqq) 18 | import qualified Username 19 | import Username (Username) 20 | 21 | ------------------------------------------------------------------------------ 22 | 23 | sample :: Username 24 | sample = [Username.valid|tcard|] 25 | 26 | ------------------------------------------------------------------------------ 27 | 28 | main :: IO () 29 | main = print sample 30 | -------------------------------------------------------------------------------- /ttc-examples/mkvalid/Username.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Username 4 | -- Description : username data type 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines a username data type, with 'TTC.Render' and 'TTC.Parse' 9 | -- instances, as an example of TTC usage. 10 | -- 11 | -- The constructor for 'Username' is not exported. The 'TTC.Parse' instance 12 | -- serves as a "smart constructor," ensuring that all values are valid. 13 | -- 14 | -- This version of the module uses 'TTC.mkValid' to make a @valid@ function 15 | -- that does not require a 'Language.Haskell.TH.Syntax.Lift' instance. 16 | ------------------------------------------------------------------------------ 17 | 18 | {-# LANGUAGE TemplateHaskell #-} 19 | 20 | module Username 21 | ( -- * Type 22 | Username 23 | -- * API 24 | , valid 25 | ) where 26 | 27 | -- https://hackage.haskell.org/package/base 28 | import Control.Monad (unless, when) 29 | import Data.Bifunctor (first) 30 | import Data.Char (isAsciiLower) 31 | 32 | -- https://hackage.haskell.org/package/text 33 | import qualified Data.Text as T 34 | import Data.Text (Text) 35 | 36 | -- https://hackage.haskell.org/package/ttc 37 | import qualified Data.TTC as TTC 38 | 39 | ------------------------------------------------------------------------------ 40 | -- $Type 41 | 42 | -- | A 'Username' must consist of 3 to 12 lowercase ASCII letters. 43 | newtype Username = Username { usernameText :: Text } 44 | deriving (Eq, Ord, Show) 45 | 46 | instance TTC.Parse Username where 47 | parse = TTC.asT $ \t -> first TTC.fromS $ do 48 | unless (T.all isAsciiLower t) $ Left "username has invalid character(s)" 49 | let len = T.length t 50 | when (len < 3) $ Left "username has fewer than 3 characters" 51 | when (len > 12) $ Left "username has more than 12 characters" 52 | pure $ Username t 53 | 54 | instance TTC.Render Username where 55 | render = TTC.convert . usernameText 56 | 57 | ------------------------------------------------------------------------------ 58 | -- $API 59 | 60 | -- | Validate a 'Username' at compile-time 61 | $(TTC.mkValid "valid" ''Username) 62 | -------------------------------------------------------------------------------- /ttc-examples/mkvalid/mkvalid.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : example of compile-time validation 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- 'TTC.mkValid' is used to create @valid@ function, to validate constants of 9 | -- a specific type. The sample username is validated at compile-time. 10 | ------------------------------------------------------------------------------ 11 | 12 | {-# LANGUAGE TemplateHaskell #-} 13 | 14 | module Main (main) where 15 | 16 | -- (ttc-examples:ttc-example-mkvalid) 17 | import qualified Username 18 | import Username (Username) 19 | 20 | ------------------------------------------------------------------------------ 21 | 22 | sample :: Username 23 | sample = $$(Username.valid "tcard") 24 | 25 | ------------------------------------------------------------------------------ 26 | 27 | main :: IO () 28 | main = print sample 29 | -------------------------------------------------------------------------------- /ttc-examples/prompt/CreditCard.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : CreditCard 4 | -- Description : credit card data types 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines the data types for a credit card, with 'TTC.Render' and 9 | -- 'TTC.Parse' instances, as an example of TTC usage. 10 | -- 11 | -- The constructors and accessors of composite data types 'CreditCard' and 12 | -- 'ExpirationDate' are exported, but the constructors for 'Name', 'Number', 13 | -- 'Year', 'Month', and 'SecurityCode' are not. The 'TTC.Parse' instances 14 | -- serve as "smart constructors," ensuring that all values are valid. 15 | ------------------------------------------------------------------------------ 16 | 17 | {-# LANGUAGE RecordWildCards #-} 18 | 19 | module CreditCard 20 | ( -- * CreditCard 21 | CreditCard(..) 22 | -- ** Name 23 | , Name 24 | -- ** Number 25 | , Number 26 | -- ** ExpirationDate 27 | , ExpirationDate(..) 28 | , toDay 29 | , Year 30 | , Month 31 | -- ** SecurityCode 32 | , SecurityCode 33 | ) where 34 | 35 | -- http://hackage.haskell.org/package/base 36 | import Control.Monad (unless, when) 37 | import Data.Bifunctor (first) 38 | import Data.Char (digitToInt, isDigit, isSpace, toUpper) 39 | import Data.List (dropWhileEnd, intersperse) 40 | import Text.Read (readMaybe) 41 | 42 | -- http://hackage.haskell.org/package/time 43 | import qualified Data.Time.Calendar as Calendar 44 | 45 | -- https://hackage.haskell.org/package/ttc 46 | import qualified Data.TTC as TTC 47 | 48 | ------------------------------------------------------------------------------ 49 | 50 | -- | A credit card has a name, number, expiration date, and security code. 51 | data CreditCard 52 | = CreditCard 53 | { name :: !Name 54 | , number :: !Number 55 | , expirationDate :: !ExpirationDate 56 | , securityCode :: !SecurityCode 57 | } 58 | deriving Show 59 | 60 | ------------------------------------------------------------------------------ 61 | 62 | -- | After any leading and trailing whitespace is stripped and all lowercase 63 | -- characters are converted to uppercase, a name must meet the following 64 | -- constraints: 65 | -- 66 | -- * Only characters between `0x20` (space) and `0x5F` (underscore) are 67 | -- allowed. 68 | -- * The name must be between 1 and 26 characters in length. 69 | -- 70 | -- Reference: 71 | -- 72 | -- * https://stackoverflow.com/questions/2004532 73 | newtype Name = Name { nameString :: String } 74 | deriving (Eq, Ord, Show) 75 | 76 | instance TTC.Parse Name where 77 | parse = TTC.asS $ \s -> first TTC.fromS $ do 78 | let nameString = map toUpper $ strip s 79 | invChars = filter ((||) <$> (< ' ') <*> (> '_')) nameString 80 | unless (null invChars) . Left $ 81 | "name has invalid character(s): " ++ intersperse ',' invChars 82 | when (null nameString) $ Left "name is empty" 83 | when (length nameString > 26) $ Left "name has more than 26 characters" 84 | pure Name{..} 85 | 86 | instance TTC.Render Name where 87 | render = TTC.convert . nameString 88 | 89 | ------------------------------------------------------------------------------ 90 | 91 | -- | After any space and dash characters are removed, a number must meet the 92 | -- following constraints: 93 | -- 94 | -- * Only ASCII digits are allowed. 95 | -- * The number must be between 8 and 19 characters in length. 96 | -- * The number must have a valid checksum. 97 | -- 98 | -- Reference: 99 | -- 100 | -- * https://en.wikipedia.org/wiki/Payment_card_number 101 | -- * https://en.wikipedia.org/wiki/Luhn_algorithm 102 | -- * http://rosettacode.org/wiki/Luhn_test_of_credit_card_numbers#Haskell 103 | newtype Number = Number { numberString :: String } 104 | deriving (Eq, Ord, Show) 105 | 106 | instance TTC.Parse Number where 107 | parse = TTC.asS $ \s -> first TTC.fromS $ do 108 | let numberString = filter ((&&) <$> (/= ' ') <*> (/= '-')) s 109 | invChars = filter (not . isDigit) numberString 110 | len = length numberString 111 | unless (null invChars) . Left $ 112 | "number has invalid character(s): " ++ intersperse ',' invChars 113 | unless (len >= 8) $ Left "number has fewer than 8 characters" 114 | unless (len <= 19) $ Left "number has more than 19 characters" 115 | unless (luhn numberString) $ Left "number checksum is invalid" 116 | pure Number{..} 117 | 118 | instance TTC.Render Number where 119 | render = TTC.convert . numberString 120 | 121 | luhn :: String -> Bool 122 | luhn 123 | = (== 0) 124 | . (`mod` 10) 125 | . sum 126 | . map (uncurry (+) . (`divMod` 10)) 127 | . zipWith (*) (cycle [1, 2]) 128 | . map digitToInt 129 | . reverse 130 | 131 | ------------------------------------------------------------------------------ 132 | 133 | -- | When parsed from a single string, an expiration date must be in `YYYY-MM` 134 | -- format. 135 | data ExpirationDate 136 | = ExpirationDate 137 | { year :: !Year 138 | , month :: !Month 139 | } 140 | deriving (Eq, Ord, Show) 141 | 142 | instance TTC.Parse ExpirationDate where 143 | parse = TTC.asS $ \s -> case break (== '-') (strip s) of 144 | (year', '-':month') -> 145 | ExpirationDate <$> TTC.parse year' <*> TTC.parse month' 146 | _ -> Left $ TTC.fromS "expiration date not in YYYY-MM format" 147 | 148 | instance TTC.Render ExpirationDate where 149 | render (ExpirationDate year' month') = 150 | TTC.fromS $ TTC.render year' ++ "-" ++ TTC.render month' 151 | 152 | toDay 153 | :: ExpirationDate 154 | -> Calendar.Day 155 | toDay (ExpirationDate (Year year') (Month month')) = 156 | let yearZ = fromIntegral year' 157 | day = Calendar.gregorianMonthLength yearZ month' 158 | in Calendar.fromGregorian yearZ month' day 159 | 160 | ------------------------------------------------------------------------------ 161 | 162 | -- | A year must be in `YYYY` format, between 1900 and 9999. 163 | newtype Year = Year { yearInt :: Int } 164 | deriving (Eq, Ord, Show) 165 | 166 | instance TTC.Parse Year where 167 | parse = TTC.asS $ \s -> first TTC.fromS $ do 168 | yearInt <- maybe (Left "year is not in YYYY format") pure $ readMaybe s 169 | unless (yearInt >= 1900) $ Left "year is before 1900" 170 | unless (yearInt <= 9999) $ Left "year is after 9999" 171 | pure Year{..} 172 | 173 | instance TTC.Render Year where 174 | render = TTC.convert . show . yearInt 175 | 176 | ------------------------------------------------------------------------------ 177 | 178 | -- | A month must be in `MM` format, between 1 (January) and 12 (December). 179 | newtype Month = Month { monthInt :: Int } 180 | deriving (Eq, Ord, Show) 181 | 182 | instance TTC.Parse Month where 183 | parse = TTC.asS $ \s -> first TTC.fromS $ do 184 | monthInt <- maybe (Left "month is not in MM format") pure $ readMaybe s 185 | unless (monthInt >= 1 && monthInt <= 12) $ 186 | Left "month is not in 1-12 range" 187 | pure Month{..} 188 | 189 | instance TTC.Render Month where 190 | render Month{..} 191 | | monthInt < 10 = TTC.convert $ '0' : show monthInt 192 | | otherwise = TTC.convert $ show monthInt 193 | 194 | ------------------------------------------------------------------------------ 195 | 196 | -- | After any leading and trailing whitespace is stripped, a security code 197 | -- must meet the following constraints: 198 | -- 199 | -- * Only ASCII digits are allowed. 200 | -- * The number must be 3 or 4 characters in length. 201 | -- 202 | -- Reference: 203 | -- 204 | -- * https://en.wikipedia.org/wiki/Card_security_code 205 | newtype SecurityCode = SecurityCode { securityCodeString :: String } 206 | deriving (Eq, Ord, Show) 207 | 208 | instance TTC.Parse SecurityCode where 209 | parse = TTC.asS $ \s -> first TTC.fromS $ do 210 | let securityCodeString = strip s 211 | invChars = filter (not . isDigit) securityCodeString 212 | len = length securityCodeString 213 | unless (null invChars) . Left $ 214 | "security code has invalid character(s): " ++ intersperse ',' invChars 215 | unless (len >= 3) $ Left "security code has fewer than 3 characters" 216 | unless (len <= 4) $ Left "security code has more than 4 characters" 217 | pure SecurityCode{..} 218 | 219 | instance TTC.Render SecurityCode where 220 | render = TTC.convert . securityCodeString 221 | 222 | ------------------------------------------------------------------------------ 223 | 224 | strip :: String -> String 225 | strip = dropWhile isSpace . dropWhileEnd isSpace 226 | -------------------------------------------------------------------------------- /ttc-examples/prompt/prompt.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : example CLI prompt using TTC 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- 'TTC.Parse' and 'TTC.Render' instances are used to create a CLI prompt, 9 | -- demonstrating TTC usage. 10 | ------------------------------------------------------------------------------ 11 | 12 | module Main (main) where 13 | 14 | -- https://hackage.haskell.org/package/base 15 | import qualified System.IO as IO 16 | 17 | -- https://hackage.haskell.org/package/time 18 | import qualified Data.Time.Calendar as Calendar 19 | import qualified Data.Time.Clock as Clock 20 | 21 | -- https://hackage.haskell.org/package/ttc 22 | import qualified Data.TTC as TTC 23 | 24 | -- (ttc-examples:ttc-example-prompt) 25 | import qualified CreditCard as CC 26 | import CreditCard (CreditCard(CreditCard), ExpirationDate(ExpirationDate)) 27 | 28 | ------------------------------------------------------------------------------ 29 | 30 | -- | This function prompts for a value of the desired type. When input is 31 | -- invalid, it displays the error and tries again. 32 | prompt 33 | :: (String -> Either String a) -- ^ parse function 34 | -> String -- ^ prompt string 35 | -> IO a 36 | prompt parse promptString = loop 37 | where 38 | loop = do 39 | putStr promptString 40 | IO.hFlush IO.stdout 41 | s <- getLine 42 | case parse s of 43 | Right x -> return x 44 | Left err -> do 45 | putStrLn $ " " ++ err 46 | loop 47 | 48 | -- | This is a version of 'prompt' that uses 'TTC.Parse' instances. 49 | promptTTC 50 | :: TTC.Parse a 51 | => String -- ^ prompt string 52 | -> IO a 53 | promptTTC = prompt TTC.parse 54 | 55 | ------------------------------------------------------------------------------ 56 | 57 | -- | This function prompts for credit card details. 58 | promptCC :: IO CreditCard 59 | promptCC = CreditCard 60 | <$> promptTTC "Enter the name: " 61 | <*> promptTTC "Enter the number: " 62 | <*> 63 | ( ExpirationDate 64 | <$> promptTTC "Enter the expiration year (YYYY): " 65 | <*> promptTTC "Enter the expiration month (MM): " 66 | ) 67 | <*> promptTTC "Enter the security code: " 68 | 69 | ------------------------------------------------------------------------------ 70 | 71 | -- | The program prompts for credit card details, prints out the normalized 72 | -- values, and shows information about expiration. 73 | main :: IO () 74 | main = do 75 | putStrLn "Please enter some fake credit card details." 76 | cc <- promptCC 77 | putStrLn $ replicate 78 '-' 78 | putStrLn $ "Name: " ++ TTC.render (CC.name cc) 79 | putStrLn $ "Number: " ++ TTC.render (CC.number cc) 80 | putStrLn $ "Expiration date: " ++ TTC.render (CC.expirationDate cc) 81 | putStrLn $ "Security code: " ++ TTC.render (CC.securityCode cc) 82 | putStrLn $ replicate 78 '-' 83 | today <- Clock.utctDay <$> Clock.getCurrentTime 84 | putStrLn . ("This credit card " ++) . (++ "!") $ 85 | case CC.toDay (CC.expirationDate cc) of 86 | expiry 87 | | expiry > today -> "expires in " ++ diffDays expiry today 88 | | expiry < today -> "expired " ++ diffDays today expiry ++ " ago" 89 | | otherwise -> "expires today" 90 | where 91 | diffDays :: Calendar.Day -> Calendar.Day -> String 92 | diffDays day1 day2 = case Calendar.diffDays day1 day2 of 93 | 1 -> "1 day" 94 | n -> show n ++ " days" 95 | -------------------------------------------------------------------------------- /ttc-examples/ttc-examples.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 2.2 2 | name: ttc-examples 3 | version: 1.2.0.0 4 | synopsis: TTC Examples 5 | description: 6 | This package contains TTC examples. Please see the README on GitHub at 7 | . 8 | homepage: https://github.com/ExtremaIS/ttc-haskell/tree/main/ttc-examples#readme 9 | bug-reports: https://github.com/ExtremaIS/ttc-haskell/issues 10 | license: MIT 11 | license-file: LICENSE 12 | author: Travis Cardwell 13 | maintainer: Travis Cardwell 14 | copyright: Copyright (c) 2019-2025 Travis Cardwell 15 | category: Data, Text 16 | build-type: Simple 17 | extra-doc-files: README.md 18 | 19 | source-repository head 20 | type: git 21 | location: https://github.com/ExtremaIS/ttc-haskell.git 22 | 23 | flag optparse-applicative_ge_0_18 24 | description: Use optparse-applicative 0.18 or newer 25 | default: True 26 | manual: False 27 | 28 | flag ttc-example-invalid 29 | description: build ttc-example-invalid 30 | default: False 31 | manual: True 32 | 33 | executable ttc-example-enum 34 | hs-source-dirs: enum 35 | main-is: enum.hs 36 | build-depends: 37 | base 38 | , ttc 39 | if flag(optparse-applicative_ge_0_18) 40 | build-depends: 41 | optparse-applicative >=0.18 42 | , prettyprinter 43 | else 44 | build-depends: 45 | ansi-wl-pprint 46 | , optparse-applicative <0.18 47 | default-language: Haskell2010 48 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 49 | 50 | -- This example is not built by default because it provides an example of a 51 | -- compile-time error. 52 | executable ttc-example-invalid 53 | if flag(ttc-example-invalid) 54 | buildable: True 55 | else 56 | buildable: False 57 | hs-source-dirs: invalid 58 | main-is: invalid.hs 59 | other-modules: 60 | Username 61 | build-depends: 62 | base 63 | , template-haskell 64 | , ttc 65 | default-language: Haskell2010 66 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 67 | 68 | executable ttc-example-mkvalid 69 | hs-source-dirs: mkvalid 70 | main-is: mkvalid.hs 71 | other-modules: 72 | Username 73 | build-depends: 74 | base 75 | , text 76 | , ttc 77 | default-language: Haskell2010 78 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 79 | 80 | executable ttc-example-mkuvalid 81 | hs-source-dirs: mkuvalid 82 | main-is: mkuvalid.hs 83 | other-modules: 84 | Username 85 | build-depends: 86 | base 87 | , text 88 | , ttc 89 | default-language: Haskell2010 90 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 91 | 92 | executable ttc-example-prompt 93 | hs-source-dirs: prompt 94 | main-is: prompt.hs 95 | other-modules: 96 | CreditCard 97 | build-depends: 98 | base 99 | , time 100 | , ttc 101 | default-language: Haskell2010 102 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 103 | 104 | executable ttc-example-uname 105 | hs-source-dirs: uname 106 | main-is: uname.hs 107 | other-modules: 108 | Username 109 | build-depends: 110 | base 111 | , text 112 | , ttc 113 | default-language: Haskell2010 114 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 115 | 116 | executable ttc-example-uvalidof 117 | hs-source-dirs: uvalidof 118 | main-is: uvalidof.hs 119 | other-modules: 120 | Username 121 | build-depends: 122 | base 123 | , text 124 | , ttc 125 | default-language: Haskell2010 126 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 127 | 128 | executable ttc-example-mkuvalidqq 129 | hs-source-dirs: mkuvalidqq 130 | main-is: mkuvalidqq.hs 131 | other-modules: 132 | Username 133 | build-depends: 134 | base 135 | , text 136 | , ttc 137 | default-language: Haskell2010 138 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 139 | 140 | executable ttc-example-valid 141 | hs-source-dirs: valid 142 | main-is: valid.hs 143 | other-modules: 144 | Username 145 | build-depends: 146 | base 147 | , template-haskell 148 | , ttc 149 | default-language: Haskell2010 150 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 151 | 152 | executable ttc-example-validof 153 | hs-source-dirs: validof 154 | main-is: validof.hs 155 | other-modules: 156 | Username 157 | build-depends: 158 | base 159 | , text 160 | , ttc 161 | default-language: Haskell2010 162 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 163 | 164 | executable ttc-example-wrapper 165 | hs-source-dirs: wrapper 166 | main-is: wrapper.hs 167 | build-depends: 168 | base 169 | , text 170 | , ttc 171 | default-language: Haskell2010 172 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 173 | -------------------------------------------------------------------------------- /ttc-examples/uname/Username.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Username 4 | -- Description : username data type 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines a username data type, with 'TTC.Render' and 'TTC.Parse' 9 | -- instances, as an example of TTC usage. 10 | -- 11 | -- The constructor for 'Username' is not exported. The 'TTC.Parse' instance 12 | -- serves as a "smart constructor," ensuring that all values are valid. 13 | ------------------------------------------------------------------------------ 14 | 15 | module Username (Username) where 16 | 17 | -- https://hackage.haskell.org/package/base 18 | import Control.Monad (unless, when) 19 | import Data.Char (isAsciiLower) 20 | 21 | -- https://hackage.haskell.org/package/text 22 | import qualified Data.Text as T 23 | import Data.Text (Text) 24 | 25 | -- https://hackage.haskell.org/package/ttc 26 | import qualified Data.TTC as TTC 27 | 28 | ------------------------------------------------------------------------------ 29 | 30 | -- | A 'Username' must consist of 3 to 12 lowercase ASCII letters. 31 | newtype Username = Username { usernameText :: Text } 32 | deriving (Eq, Ord, Show) 33 | 34 | instance TTC.Parse Username where 35 | parse = TTC.asT $ \t -> TTC.prefixErrorS "invalid username: " $ do 36 | unless (T.all isAsciiLower t) $ Left "not only lowercase ASCII letters" 37 | let len = T.length t 38 | when (len < 3) $ Left "fewer than 3 characters" 39 | when (len > 12) $ Left "more than 12 characters" 40 | pure $ Username t 41 | 42 | instance TTC.Render Username where 43 | render = TTC.convert . usernameText 44 | -------------------------------------------------------------------------------- /ttc-examples/uname/uname.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : minimal example of using Render and Parse instances 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | ------------------------------------------------------------------------------ 8 | 9 | module Main (main) where 10 | 11 | -- https://hackage.haskell.org/package/ttc 12 | import qualified Data.TTC as TTC 13 | 14 | -- (ttc-examples:ttc-example-uname) 15 | import Username (Username) 16 | 17 | ------------------------------------------------------------------------------ 18 | 19 | testParse :: String -> IO () 20 | testParse s = do 21 | putStrLn $ "testParse " ++ show s 22 | putStrLn . (' ' :) $ case TTC.parse s :: Either String Username of 23 | Right uname -> "valid username: " ++ TTC.render uname 24 | Left err -> err 25 | 26 | ------------------------------------------------------------------------------ 27 | 28 | main :: IO () 29 | main = do 30 | testParse "tcard" 31 | testParse "Travis" 32 | -------------------------------------------------------------------------------- /ttc-examples/uvalidof/Username.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Username 4 | -- Description : username data type 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines a username data type, with 'TTC.Render' and 'TTC.Parse' 9 | -- instances, as an example of TTC usage. 10 | -- 11 | -- The constructor for 'Username' is not exported. The 'TTC.Parse' instance 12 | -- serves as a "smart constructor," ensuring that all values are valid. 13 | ------------------------------------------------------------------------------ 14 | 15 | module Username (Username) where 16 | 17 | -- https://hackage.haskell.org/package/base 18 | import Control.Monad (unless, when) 19 | import Data.Bifunctor (first) 20 | import Data.Char (isAsciiLower) 21 | 22 | -- https://hackage.haskell.org/package/text 23 | import qualified Data.Text as T 24 | import Data.Text (Text) 25 | 26 | -- https://hackage.haskell.org/package/ttc 27 | import qualified Data.TTC as TTC 28 | 29 | ------------------------------------------------------------------------------ 30 | 31 | -- | A 'Username' must consist of 3 to 12 lowercase ASCII letters. 32 | newtype Username = Username { usernameText :: Text } 33 | deriving (Eq, Ord, Show) 34 | 35 | instance TTC.Parse Username where 36 | parse = TTC.asT $ \t -> first TTC.fromS $ do 37 | unless (T.all isAsciiLower t) $ Left "username has invalid character(s)" 38 | let len = T.length t 39 | when (len < 3) $ Left "username has fewer than 3 characters" 40 | when (len > 12) $ Left "username has more than 12 characters" 41 | pure $ Username t 42 | 43 | instance TTC.Render Username where 44 | render = TTC.convert . usernameText 45 | -------------------------------------------------------------------------------- /ttc-examples/uvalidof/uvalidof.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : example of compile-time validation 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- 'TTC.untypedValidOf' is used to create a validated constant. The sample 9 | -- username is validated at compile-time. 10 | ------------------------------------------------------------------------------ 11 | 12 | {-# LANGUAGE TemplateHaskell #-} 13 | 14 | module Main (main) where 15 | 16 | -- https://hackage.haskell.org/package/base 17 | import Data.Proxy (Proxy(Proxy)) 18 | 19 | -- https://hackage.haskell.org/package/ttc 20 | import qualified Data.TTC as TTC 21 | 22 | -- (ttc-examples:ttc-example-uvalidof) 23 | import Username (Username) 24 | 25 | ------------------------------------------------------------------------------ 26 | 27 | sample :: Username 28 | sample = $(TTC.untypedValidOf (Proxy :: Proxy Username) "tcard") 29 | 30 | ------------------------------------------------------------------------------ 31 | 32 | main :: IO () 33 | main = print sample 34 | -------------------------------------------------------------------------------- /ttc-examples/valid/Username.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Username 4 | -- Description : username data type 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines a username data type, with 'TTC.Render' and 'TTC.Parse' 9 | -- instances, as an example of TTC usage. 10 | -- 11 | -- The constructor for 'Username' is not exported. The 'TTC.Parse' instance 12 | -- serves as a "smart constructor," ensuring that all values are valid. 13 | -- 14 | -- This version of the module provides a trivial example of deriving 15 | -- 'THS.Lift'. 16 | -- 17 | -- The 'Username' data type derives 'THS.Lift', using the @DeriveLift@ 18 | -- language extension. The underlying 'String' already has a 'THS.Lift' 19 | -- instance. 20 | ------------------------------------------------------------------------------ 21 | 22 | {-# LANGUAGE DeriveLift #-} 23 | 24 | module Username (Username) where 25 | 26 | -- https://hackage.haskell.org/package/base 27 | import Control.Monad (unless, when) 28 | import Data.Bifunctor (first) 29 | import Data.Char (isAsciiLower) 30 | 31 | -- https://hackage.haskell.org/package/template-haskell 32 | import qualified Language.Haskell.TH.Syntax as THS 33 | 34 | -- https://hackage.haskell.org/package/ttc 35 | import qualified Data.TTC as TTC 36 | 37 | ------------------------------------------------------------------------------ 38 | 39 | -- | A 'Username' must consist of 3 to 12 lowercase ASCII letters. 40 | newtype Username = Username { usernameString :: String } 41 | deriving (Eq, Ord, Show, THS.Lift) 42 | 43 | instance TTC.Parse Username where 44 | parse = TTC.asS $ \s -> first TTC.fromS $ do 45 | unless (all isAsciiLower s) $ Left "username has invalid character(s)" 46 | let len = length s 47 | when (len < 3) $ Left "username has fewer than 3 characters" 48 | when (len > 12) $ Left "username has more than 12 characters" 49 | pure $ Username s 50 | 51 | instance TTC.Render Username where 52 | render = TTC.convert . usernameString 53 | -------------------------------------------------------------------------------- /ttc-examples/valid/valid.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : example of compile-time validation 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- 'TTC.valid' is used to create validated constants. The type must have a 9 | -- `Lift` instance since this is implemented using typed Template Haskell. 10 | -- When using the `OverloadedStrings` extension, you do not even have to write 11 | -- the function name, at the expense of documentation. With GHC 9, you can 12 | -- even leave off the parenthesis. The sample usernames are validated at 13 | -- compile-time. 14 | ------------------------------------------------------------------------------ 15 | 16 | {-# LANGUAGE CPP #-} 17 | {-# LANGUAGE OverloadedStrings #-} 18 | {-# LANGUAGE TemplateHaskell #-} 19 | 20 | module Main (main) where 21 | 22 | -- https://hackage.haskell.org/package/ttc 23 | import qualified Data.TTC as TTC 24 | 25 | -- (ttc-examples:ttc-example-valid) 26 | import Username (Username) 27 | 28 | ------------------------------------------------------------------------------ 29 | 30 | sample1 :: Username 31 | -- This syntax works with all supported versions of GHC. 32 | sample1 = $$(TTC.valid "tcard") 33 | 34 | sample2 :: Username 35 | #if __GLASGOW_HASKELL__ >= 900 36 | -- This syntax only works with GHC 9. 37 | sample2 = $$"alice" 38 | #else 39 | -- This syntax works with all supported versions of GHC. 40 | sample2 = $$("alice") 41 | #endif 42 | 43 | ------------------------------------------------------------------------------ 44 | 45 | main :: IO () 46 | main = do 47 | print sample1 48 | print sample2 49 | -------------------------------------------------------------------------------- /ttc-examples/validof/Username.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Username 4 | -- Description : username data type 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines a username data type, with 'TTC.Render' and 'TTC.Parse' 9 | -- instances, as an example of TTC usage. 10 | -- 11 | -- The constructor for 'Username' is not exported. The 'TTC.Parse' instance 12 | -- serves as a "smart constructor," ensuring that all values are valid. 13 | ------------------------------------------------------------------------------ 14 | 15 | module Username (Username) where 16 | 17 | -- https://hackage.haskell.org/package/base 18 | import Control.Monad (unless, when) 19 | import Data.Bifunctor (first) 20 | import Data.Char (isAsciiLower) 21 | 22 | -- https://hackage.haskell.org/package/text 23 | import qualified Data.Text as T 24 | import Data.Text (Text) 25 | 26 | -- https://hackage.haskell.org/package/ttc 27 | import qualified Data.TTC as TTC 28 | 29 | ------------------------------------------------------------------------------ 30 | 31 | -- | A 'Username' must consist of 3 to 12 lowercase ASCII letters. 32 | newtype Username = Username { usernameText :: Text } 33 | deriving (Eq, Ord, Show) 34 | 35 | instance TTC.Parse Username where 36 | parse = TTC.asT $ \t -> first TTC.fromS $ do 37 | unless (T.all isAsciiLower t) $ Left "username has invalid character(s)" 38 | let len = T.length t 39 | when (len < 3) $ Left "username has fewer than 3 characters" 40 | when (len > 12) $ Left "username has more than 12 characters" 41 | pure $ Username t 42 | 43 | instance TTC.Render Username where 44 | render = TTC.convert . usernameText 45 | -------------------------------------------------------------------------------- /ttc-examples/validof/validof.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : example of compile-time validation 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- 'TTC.validOf' is used to create a validated constant. The sample username 9 | -- is validated at compile-time. 10 | ------------------------------------------------------------------------------ 11 | 12 | {-# LANGUAGE TemplateHaskell #-} 13 | 14 | module Main (main) where 15 | 16 | -- https://hackage.haskell.org/package/base 17 | import Data.Proxy (Proxy(Proxy)) 18 | 19 | -- https://hackage.haskell.org/package/ttc 20 | import qualified Data.TTC as TTC 21 | 22 | -- (ttc-examples:ttc-example-validof) 23 | import Username (Username) 24 | 25 | ------------------------------------------------------------------------------ 26 | 27 | sample :: Username 28 | sample = $$(TTC.validOf (Proxy :: Proxy Username) "tcard") 29 | 30 | ------------------------------------------------------------------------------ 31 | 32 | main :: IO () 33 | main = print sample 34 | -------------------------------------------------------------------------------- /ttc-examples/wrapper/wrapper.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Main 4 | -- Description : minimal example of using a wrapper type 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This example demonstrates use of @DerivingVia@ to define TTC instances via 9 | -- a wrapper type. 10 | ------------------------------------------------------------------------------ 11 | 12 | {-# LANGUAGE DerivingVia #-} 13 | 14 | module Main (main) where 15 | 16 | -- https://hackage.haskell.org/package/text 17 | import Data.Text (Text) 18 | 19 | -- https://hackage.haskell.org/package/ttc 20 | import qualified Data.TTC as TTC 21 | import qualified Data.TTC.Wrapper as TTCW 22 | 23 | ------------------------------------------------------------------------------ 24 | 25 | newtype Username = Username { usernameText :: Text } 26 | deriving (Eq, Ord, Show) 27 | deriving TTC.Parse via TTCW.WrapperT 28 | deriving TTC.Render via TTCW.WrapperT 29 | 30 | ------------------------------------------------------------------------------ 31 | 32 | testParse :: String -> IO () 33 | testParse s = do 34 | putStrLn $ "testParse " ++ show s 35 | putStrLn . (' ' :) $ case TTC.parse s :: Either String Username of 36 | Right uname -> "valid username: " ++ TTC.render uname 37 | Left err -> err 38 | 39 | ------------------------------------------------------------------------------ 40 | 41 | main :: IO () 42 | main = do 43 | testParse "tcard" 44 | testParse "Travis" 45 | -------------------------------------------------------------------------------- /ttc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `ttc` Changelog 2 | 3 | This project follows the [Haskell package versioning policy][PVP], with 4 | versions in `A.B.C.D` format. `A` may be incremented arbitrarily for 5 | non-technical reasons, but [semantic versioning][SemVer] is otherwise 6 | followed, where `A.B` is the major version, `C` is the minor version, and `D` 7 | is the patch version. Initial development uses versions `0.0.C.D`, for which 8 | every version is considered breaking. 9 | 10 | [PVP]: 11 | [SemVer]: 12 | 13 | The format of this changelog is based on [Keep a Changelog][KaC], with the 14 | following conventions: 15 | 16 | * Level-two heading `Unreleased` is used to track changes that have not been 17 | released. 18 | * Other level-two headings specify the release in `A.B.C.D (YYYY-MM-DD)` 19 | format, with newer versions above older versions. 20 | * Level-three headings are used to categorize changes as follows: 21 | 1. Breaking 22 | 2. Non-Breaking 23 | * Changes are listed in arbitrary order and present tense. 24 | 25 | [KaC]: 26 | 27 | ## 1.5.0.0 (2025-01-02) 28 | 29 | ### Breaking 30 | 31 | * Change type argument order for easier use with `TypeApplications` 32 | * Add `RenderDefault` `Bool` instance 33 | * Add `ParseDefault` `Bool` instance 34 | * Add missing `RenderDefault` and `ParseDefault` instances for `TLB.Builder`, 35 | `ST.ShortText`, `BSB.Builder`, and `SBS.ShortByteString` 36 | * Remove support for GHC 8.6, constraining lower bounds 37 | * Remove support for GHC 8.4, constraining lower bounds 38 | * Remove support for GHC 8.2, constraining lower bounds 39 | * Change minimal Cabal from 1.24 to 3.0 40 | 41 | ### Non-Breaking 42 | 43 | * Add `Data.TTC.Wrapper` module 44 | * Add Template Haskell functions for loading default instances 45 | * Bump `base` dependency version upper bound 46 | * Bump `template-haskell` dependency version upper bound 47 | 48 | ## 1.4.0.0 (2023-12-03) 49 | 50 | ### Breaking 51 | 52 | * Add support for `ShortText` 53 | 54 | ### Non-Breaking 55 | 56 | * Bump `base` dependency version upper bound 57 | * Bump `template-haskell` dependency version upper bound 58 | 59 | ## 1.3.0.0 (2023-09-17) 60 | 61 | ### Breaking 62 | 63 | * Add typed Template Haskell expression `IsString` orphan instance 64 | * Add `parseOrFail` functions 65 | 66 | ### Non-Breaking 67 | 68 | * Bump `bytestring` dependency version upper bound 69 | * Bump `tasty` dependency version upper bound 70 | * Bump `text` dependency version upper bound 71 | 72 | ## 1.2.1.0 (2023-03-21) 73 | 74 | ### Non-Breaking 75 | 76 | * Bump `template-haskell` dependency version upper bound 77 | * Adjust dependency constraints to match tested versions 78 | 79 | ## 1.2.0.0 (2022-03-18) 80 | 81 | ### Breaking 82 | 83 | * Add `withError` functions 84 | * Add `prefixError` functions 85 | 86 | ## 1.1.1.1 (2022-03-01) 87 | 88 | ### Non-Breaking 89 | 90 | * Refactor `Makefile` 91 | 92 | ## 1.1.1.0 (2021-12-25) 93 | 94 | ### Non-Breaking 95 | 96 | * Bump `text` dependency version upper bound 97 | 98 | ## 1.1.0.2 (2021-08-23) 99 | 100 | ### Non-Breaking 101 | 102 | * Bump `template-haskell` dependency version upper bound 103 | * Add CPP macro around `BSB.Builder` `Show` instance in test code 104 | 105 | ## 1.1.0.1 (2021-06-25) 106 | 107 | ### Non-Breaking 108 | 109 | * Refactor Nix configuration 110 | 111 | ## 1.1.0.0 (2021-06-10) 112 | 113 | ### Breaking 114 | 115 | * Add `Textual` `TLB.Builder` instance and related functions 116 | * Add `Textual` `BSB.Builder` instance and related functions 117 | * Add `Textual` `SBS.ShortByteString` instance and related functions 118 | * Add `RenderDefault` and `ParseDefault` type classes and instances 119 | * Remove `Data.TTC.Instances` 120 | 121 | ### Non-Breaking 122 | 123 | * Add `HasCallStack` to unsafe functions 124 | 125 | ## 1.0.0.0 (2021-06-03) 126 | 127 | ### Non-Breaking 128 | 129 | * Add Cabal support to `Makefile` 130 | 131 | ## 0.4.0.0 (2021-03-27) 132 | 133 | ### Breaking 134 | 135 | * Add support for GHC 9 136 | * Add `renderTLB`, `renderBSB`, and `renderSBS` functions 137 | * Use `Textual` error messages for `parseEnum'` 138 | 139 | ### Non-Breaking 140 | 141 | * Add `@since` annotations 142 | * Rename Git default branch to `main` 143 | * Use GitHub Actions instead of Travis CI 144 | * Add Cabal tests to GitHub Actions 145 | * Add [stan](https://hackage.haskell.org/package/stan) static analysis 146 | 147 | ## 0.3.0.0 (2020-11-03) 148 | 149 | ### Breaking 150 | 151 | * Use `Textual` error messages 152 | * Add `maybeParseWithRead` function 153 | 154 | ## 0.2.3.0 (2020-09-25) 155 | 156 | ### Non-Breaking 157 | 158 | * Bump `bytestring` dependency version upper bound 159 | 160 | ## 0.2.2.0 (2020-05-17) 161 | 162 | ### Non-Breaking 163 | 164 | * Bump `template-haskell` dependency version upper bound 165 | * Update `lift` example for compatibility with `template-haskell 2.16.0.0` 166 | 167 | ## 0.2.1.0 (2020-05-11) 168 | 169 | ### Non-Breaking 170 | 171 | * Update examples to support older libraries 172 | * Refactor `Makefile`, add `STACK_NIX_PATH` support 173 | * Add `test-all` command to `Makefile` 174 | * Bump `tasty` dependency version upper bound 175 | 176 | ## 0.2.0.0 (2019-12-15) 177 | 178 | ### Non-Breaking 179 | 180 | * Add untyped validation functions 181 | * Move examples to a separate package 182 | * Refactor examples and add more 183 | 184 | ## 0.1.0.1 (2019-12-02) 185 | 186 | ### Non-Breaking 187 | 188 | * Bump `time` dependency version upper bound 189 | 190 | ## 0.1.0.0 (2019-12-01) 191 | 192 | ### Non-Breaking 193 | 194 | * Update Cabal file in preparation for release to Hackage 195 | 196 | ## 0.0.0.4 (2019-11-30) 197 | 198 | ### Non-Breaking 199 | 200 | * Update Cabal file in preparation for release to Hackage 201 | * Update documentation 202 | * Add examples 203 | 204 | ## 0.0.0.3 (2019-11-28) 205 | 206 | ### Non-Breaking 207 | 208 | * Add continuous integration support 209 | 210 | ## 0.0.0.2 (2019-11-28) 211 | 212 | ### Non-Breaking 213 | 214 | * Update Cabal metadata 215 | * Update README 216 | 217 | ## 0.0.0.1 (2019-11-23) 218 | 219 | ### Breaking 220 | 221 | * Initial public release 222 | -------------------------------------------------------------------------------- /ttc/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019-2025 Travis Cardwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ttc/README.md: -------------------------------------------------------------------------------- 1 | # TTC: Textual Type Classes 2 | 3 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 4 | [![GitHub CI](https://github.com/ExtremaIS/ttc-haskell/workflows/CI/badge.svg?branch=main)](https://github.com/ExtremaIS/ttc-haskell/actions) 5 | [![Hackage](https://img.shields.io/hackage/v/ttc.svg)](https://hackage.haskell.org/package/ttc) 6 | [![Stackage LTS](https://stackage.org/package/ttc/badge/lts)](https://stackage.org/package/ttc) 7 | [![Stackage Nightly](https://stackage.org/package/ttc/badge/nightly)](https://stackage.org/nightly/package/ttc) 8 | 9 | ## Overview 10 | 11 | TTC, an initialism of _Textual Type Classes_, is a library that provides 12 | `Render` and `Parse` type classes for conversion between data types and 13 | textual data types (strings). Use the `Show` and `Read` type classes for 14 | debugging/development, and use the `Render` and `Parse` type classes for your 15 | own purposes. The library also provides various ways to validate constants at 16 | compile-time. 17 | 18 | This package (`ttc`) uses a `Textual` type class for conversion between 19 | textual data types. This package uses simple types and compiles quickly, but 20 | the supported textual data types are fixed. It is not possible for users to 21 | add support for additional textual data types. 22 | 23 | Since a type may have at most one instance of a given type class, special care 24 | must be taken when defining type class instances in a shared library. In 25 | particular, orphan instances should generally *not* be used in shared 26 | libraries since they prevent users of the libraries from writing their own 27 | instances. 28 | 29 | `Render` and `Parse` are best used with types that have canonical textual 30 | representations, such as textual identifiers. When there is more than one way 31 | to create a textual representation, such as configurable formatting, using a 32 | normal function is probably more appropriate. Such a function can make use of 33 | the `Textual` type class to support multiple textual data types. 34 | 35 | This overview includes a brief introduction of the library. The following 36 | resources are also available: 37 | 38 | * [API documentation][] is viewable on Hackage. 39 | * A [series of articles][] gives a guided tour of the library. 40 | 1. [Textual Type Class][] 41 | 2. [Render and Parse][] 42 | 3. [Validated Constants][] 43 | 4. [Best Practices][] 44 | * The [`ttc-examples`][] directory in the repository contains usage examples. 45 | 46 | [API documentation]: 47 | [series of articles]: 48 | [Textual Type Class]: 49 | [Render and Parse]: 50 | [Validated Constants]: 51 | [Best Practices]: 52 | [`ttc-examples`]: 53 | 54 | ### `Textual` 55 | 56 | The `Textual` type class is used to convert between the following textual data 57 | types: 58 | 59 | * `String` 60 | * Strict `Text` 61 | * Lazy `Text` 62 | * `Text` `Builder` 63 | * `ShortText` 64 | * Strict `ByteString` 65 | * Lazy `ByteString` 66 | * `ByteString` `Builder` (and `Data.Binary.Builder`) 67 | * `ShortByteString` 68 | 69 | This type class has two key features: 70 | 71 | * Type conversion is *not* done through a fixed type (such as `String` or 72 | `Text`). 73 | * It has a single type variable, making it easy to write functions that 74 | accept arguments and/or return values that may be any of the supported 75 | textual data types. 76 | 77 | For more details, see the [Textual Type Class][] article. 78 | 79 | ### `Render` 80 | 81 | The `Render` type class renders a data type as a `Textual` data type: 82 | 83 | ```haskell 84 | class Render a where 85 | render :: Textual t => a -> t 86 | ``` 87 | 88 | It is analogous to the `Show` type class, which can be reserved for 89 | debugging/development. 90 | 91 | The `render` function returns any of the supported textual data types. Use 92 | the textual data type that is most natural in the implementation of `render` 93 | instances, and return values are converted to other textual data types when 94 | necessary. The `Show` and `IsString` type classes are not used, so use of the 95 | `String` type is not required. 96 | 97 | As a simple example, consider a `Username` type that is implemented as a 98 | `newtype` over `Text`: 99 | 100 | ```haskell 101 | module Username (Username) where 102 | 103 | import Control.Monad (unless, when) 104 | import Data.Char (isAsciiLower) 105 | import qualified Data.Text as T 106 | import Data.Text (Text) 107 | import qualified Data.TTC as TTC 108 | 109 | newtype Username = Username Text 110 | deriving (Eq, Ord, Show) 111 | 112 | instance TTC.Render Username where 113 | render (Username t) = TTC.convert t 114 | ``` 115 | 116 | If a username needs to be included in a `String` error message, conversion is 117 | automatic: 118 | 119 | ```haskell 120 | putStrLn $ "user not found: " ++ TTC.render uname 121 | ``` 122 | 123 | For more details, see the [Render and Parse][] article. 124 | 125 | ### `Parse` 126 | 127 | The `Parse` type class parses a data type from a `Textual` data type: 128 | 129 | ```haskell 130 | class Parse a where 131 | parse :: (Textual t, Textual e) => t -> Either e a 132 | ``` 133 | 134 | It is analogous to the `Read` type class, which can be reserved for 135 | debugging/development. 136 | 137 | The `parse` function takes any of the supported textual data types as an 138 | argument. Use the textual data type that is most natural in the 139 | implementation of `parse` instances, and arguments are converted from other 140 | textual data types when necessary. The `IsString` type class is not used, so 141 | use of the `String` type is not required. 142 | 143 | Here is an example instance for `Username`, implementing some restrictions: 144 | 145 | ```haskell 146 | instance TTC.Parse Username where 147 | parse = TTC.asT $ \t -> TTC.prefixErrorS "invalid username: " $ do 148 | unless (T.all isAsciiLower t) $ Left "not only lowercase ASCII letters" 149 | let len = T.length t 150 | when (len < 3) $ Left "fewer than 3 characters" 151 | when (len > 12) $ Left "more than 12 characters" 152 | pure $ Username t 153 | ``` 154 | 155 | If a username needs to be parsed from a `String`, conversion is automatic: 156 | 157 | ```haskell 158 | case TTC.parse s :: Either String Username of 159 | Right uname -> "valid username: " ++ TTC.render uname 160 | Left err -> err 161 | ``` 162 | 163 | For more details, see the [Render and Parse][] article. 164 | 165 | ### Constant Validation 166 | 167 | TTC provides functions to validate constants at compile-time, using Template 168 | Haskell. For example, a `Username` constant can be defined as follows: 169 | 170 | ```haskell 171 | user :: Username 172 | user = $$(TTC.valid "tcard") 173 | ``` 174 | 175 | For more details, see the [Validated Constants][] article. 176 | 177 | ## Project 178 | 179 | See the [project README][] for general project information. 180 | 181 | [project README]: 182 | -------------------------------------------------------------------------------- /ttc/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /ttc/src/Data/TTC/Wrapper.hs: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------ 2 | -- | 3 | -- Module : Data.TTC.Wrapper 4 | -- Description : TTC wrapper types 5 | -- Copyright : Copyright (c) 2019-2025 Travis Cardwell 6 | -- License : MIT 7 | -- 8 | -- This module defines a @newtype@ wrapper for each 'TTC.Textual' data type. 9 | -- Each wrapper has a 'TTC.Render' instance that converts the wrapped type and 10 | -- a 'TTC.Parse' instance that wraps the argument (never failing). Data types 11 | -- that are coercible to a 'TTC.Textual' data type can use these wrappers to 12 | -- derive instances using @DerivingVia@. 13 | -- 14 | -- See the @wrapper@ example program in the @ttc-examples@ directory of the 15 | -- source repository. 16 | ------------------------------------------------------------------------------ 17 | 18 | module Data.TTC.Wrapper 19 | ( -- * Wrapper Types 20 | WrapperS(..) 21 | , WrapperT(..) 22 | , WrapperTL(..) 23 | , WrapperTLB(..) 24 | , WrapperST(..) 25 | , WrapperBS(..) 26 | , WrapperBSL(..) 27 | , WrapperBSB(..) 28 | , WrapperSBS(..) 29 | ) where 30 | 31 | -- https://hackage.haskell.org/package/bytestring 32 | import qualified Data.ByteString as BS 33 | import qualified Data.ByteString.Builder as BSB 34 | import qualified Data.ByteString.Lazy as BSL 35 | import qualified Data.ByteString.Short as SBS 36 | 37 | -- https://hackage.haskell.org/package/text 38 | import qualified Data.Text as T 39 | import qualified Data.Text.Lazy as TL 40 | import qualified Data.Text.Lazy.Builder as TLB 41 | 42 | -- https://hackage.haskell.org/package/text-short 43 | import qualified Data.Text.Short as ST 44 | 45 | -- (ttc) 46 | import qualified Data.TTC as TTC 47 | 48 | ------------------------------------------------------------------------------ 49 | 50 | -- | 'String' wrapper type 51 | -- 52 | -- @since 1.5.0.0 53 | newtype WrapperS = WrapperS { unWrapperS :: String } 54 | 55 | instance TTC.Parse WrapperS where 56 | parse = TTC.asS $ pure . WrapperS 57 | 58 | instance TTC.Render WrapperS where 59 | render = TTC.fromS . unWrapperS 60 | 61 | ------------------------------------------------------------------------------ 62 | 63 | -- | Strict 'T.Text' wrapper type 64 | -- 65 | -- @since 1.5.0.0 66 | newtype WrapperT = WrapperT { unWrapperT :: T.Text } 67 | 68 | instance TTC.Parse WrapperT where 69 | parse = TTC.asT $ pure . WrapperT 70 | 71 | instance TTC.Render WrapperT where 72 | render = TTC.fromT . unWrapperT 73 | 74 | ------------------------------------------------------------------------------ 75 | 76 | -- | Lazy 'TL.Text' wrapper type 77 | -- 78 | -- @since 1.5.0.0 79 | newtype WrapperTL = WrapperTL { unWrapperTL :: TL.Text } 80 | 81 | instance TTC.Parse WrapperTL where 82 | parse = TTC.asTL $ pure . WrapperTL 83 | 84 | instance TTC.Render WrapperTL where 85 | render = TTC.fromTL . unWrapperTL 86 | 87 | ------------------------------------------------------------------------------ 88 | 89 | -- | Text 'TLB.Builder' wrapper type 90 | -- 91 | -- @since 1.5.0.0 92 | newtype WrapperTLB = WrapperTLB { unWrapperTLB :: TLB.Builder } 93 | 94 | instance TTC.Parse WrapperTLB where 95 | parse = TTC.asTLB $ pure . WrapperTLB 96 | 97 | instance TTC.Render WrapperTLB where 98 | render = TTC.fromTLB . unWrapperTLB 99 | 100 | ------------------------------------------------------------------------------ 101 | 102 | -- | 'ST.ShortText' wrapper type 103 | -- 104 | -- @since 1.5.0.0 105 | newtype WrapperST = WrapperST { unWrapperST :: ST.ShortText } 106 | 107 | instance TTC.Parse WrapperST where 108 | parse = TTC.asST $ pure . WrapperST 109 | 110 | instance TTC.Render WrapperST where 111 | render = TTC.fromST . unWrapperST 112 | 113 | ------------------------------------------------------------------------------ 114 | 115 | -- | Strict 'BS.ByteString' wrapper type 116 | -- 117 | -- @since 1.5.0.0 118 | newtype WrapperBS = WrapperBS { unWrapperBS :: BS.ByteString } 119 | 120 | instance TTC.Parse WrapperBS where 121 | parse = TTC.asBS $ pure . WrapperBS 122 | 123 | instance TTC.Render WrapperBS where 124 | render = TTC.fromBS . unWrapperBS 125 | 126 | ------------------------------------------------------------------------------ 127 | 128 | -- | Lazy 'BSL.ByteString' wrapper type 129 | -- 130 | -- @since 1.5.0.0 131 | newtype WrapperBSL = WrapperBSL { unWrapperBSL :: BSL.ByteString } 132 | 133 | instance TTC.Parse WrapperBSL where 134 | parse = TTC.asBSL $ pure . WrapperBSL 135 | 136 | instance TTC.Render WrapperBSL where 137 | render = TTC.fromBSL . unWrapperBSL 138 | 139 | ------------------------------------------------------------------------------ 140 | 141 | -- | ByteString 'BSB.Builder' wrapper type 142 | -- 143 | -- @since 1.5.0.0 144 | newtype WrapperBSB = WrapperBSB { unWrapperBSB :: BSB.Builder } 145 | 146 | instance TTC.Parse WrapperBSB where 147 | parse = TTC.asBSB $ pure . WrapperBSB 148 | 149 | instance TTC.Render WrapperBSB where 150 | render = TTC.fromBSB . unWrapperBSB 151 | 152 | ------------------------------------------------------------------------------ 153 | 154 | -- | 'SBS.ShortByteString' wrapper type 155 | -- 156 | -- @since 1.5.0.0 157 | newtype WrapperSBS = WrapperSBS { unWrapperSBS :: SBS.ShortByteString } 158 | 159 | instance TTC.Parse WrapperSBS where 160 | parse = TTC.asSBS $ pure . WrapperSBS 161 | 162 | instance TTC.Render WrapperSBS where 163 | render = TTC.fromSBS . unWrapperSBS 164 | -------------------------------------------------------------------------------- /ttc/test/Data/TTC/Test.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE QuasiQuotes #-} 6 | {-# LANGUAGE RankNTypes #-} 7 | {-# LANGUAGE ScopedTypeVariables #-} 8 | {-# LANGUAGE TemplateHaskell #-} 9 | {-# LANGUAGE TypeApplications #-} 10 | 11 | {-# OPTIONS_GHC -fno-warn-orphans #-} 12 | 13 | module Data.TTC.Test (tests) where 14 | 15 | -- https://hackage.haskell.org/package/base 16 | import Control.Exception (ErrorCall, Exception, evaluate, handle) 17 | import Data.Int (Int8, Int16, Int32, Int64) 18 | import Data.Proxy (Proxy(Proxy), asProxyTypeOf) 19 | import Data.String (IsString) 20 | import Data.Word (Word8, Word16, Word32, Word64) 21 | import GHC.Stack (HasCallStack) 22 | import Text.Read (readMaybe) 23 | 24 | -- https://hackage.haskell.org/package/bytestring 25 | import qualified Data.ByteString as BS 26 | import qualified Data.ByteString.Builder as BSB 27 | import qualified Data.ByteString.Lazy as BSL 28 | import qualified Data.ByteString.Short as SBS 29 | 30 | -- https://hackage.haskell.org/package/tasty 31 | import Test.Tasty (TestName, TestTree, testGroup) 32 | 33 | -- https://hackage.haskell.org/package/tasty-hunit 34 | import Test.Tasty.HUnit ((@=?), Assertion, assertFailure, testCase) 35 | 36 | -- https://hackage.haskell.org/package/text 37 | import qualified Data.Text as T 38 | import qualified Data.Text.Lazy as TL 39 | import qualified Data.Text.Lazy.Builder as TLB 40 | 41 | -- https://hackage.haskell.org/package/text-short 42 | import qualified Data.Text.Short as ST 43 | 44 | -- (ttc) 45 | import qualified Data.TTC as TTC 46 | 47 | -- (ttc:test) 48 | import qualified TestTypes.Ex as Ex 49 | import TestTypes.Ex (Ex(Ex)) 50 | 51 | ------------------------------------------------------------------------------ 52 | -- $Instances 53 | 54 | TTC.defaultRenderAndParseInstances 55 | [ ''Bool, ''Char, ''Double, ''Float, ''Integer 56 | , ''Int, ''Int8, ''Int16, ''Int32, ''Int64 57 | , ''Word, ''Word8, ''Word16, ''Word32, ''Word64 58 | , ''String 59 | , ''T.Text, ''TL.Text, ''TLB.Builder, ''ST.ShortText 60 | , ''BS.ByteString, ''BSL.ByteString, ''BSB.Builder, ''SBS.ShortByteString 61 | ] 62 | 63 | ------------------------------------------------------------------------------ 64 | 65 | instance Eq BSB.Builder where 66 | x == y = BSB.toLazyByteString x == BSB.toLazyByteString y 67 | 68 | #if !MIN_VERSION_bytestring(0,11,1) 69 | instance Show BSB.Builder where 70 | show = show . BSB.toLazyByteString 71 | #endif 72 | 73 | ------------------------------------------------------------------------------ 74 | -- $HelperFunctions 75 | 76 | assertRaises 77 | :: (HasCallStack, Exception e, Show a) 78 | => Proxy e 79 | -> a 80 | -> Assertion 81 | assertRaises proxy x = 82 | handle 83 | (const (return ()) . (`asProxyTypeOf` proxy)) 84 | (assertFailure . ("expected exception; got: " ++) . show =<< evaluate x) 85 | 86 | ------------------------------------------------------------------------------ 87 | -- $TestData 88 | 89 | xS :: String 90 | xS = "test テスト" 91 | 92 | xT :: T.Text 93 | xT = "test テスト" 94 | 95 | xTL :: TL.Text 96 | xTL = "test テスト" 97 | 98 | xTLB :: TLB.Builder 99 | xTLB = "test テスト" 100 | 101 | xST :: ST.ShortText 102 | xST = "test テスト" 103 | 104 | xBS :: BS.ByteString 105 | xBS = "test \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" 106 | 107 | xBSL :: BSL.ByteString 108 | xBSL = "test \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" 109 | 110 | xBSB :: BSB.Builder 111 | xBSB = "test テスト" 112 | 113 | xSBS :: SBS.ShortByteString 114 | xSBS = "test \xe3\x83\x86\xe3\x82\xb9\xe3\x83\x88" 115 | 116 | -- U+FFFD is the Unicode replacement character 117 | xiS :: String 118 | xiS = "test \xfffd" 119 | 120 | xiT :: T.Text 121 | xiT = T.pack xiS 122 | 123 | xiTL :: TL.Text 124 | xiTL = TL.pack xiS 125 | 126 | xiTLB :: TLB.Builder 127 | xiTLB = TLB.fromString xiS 128 | 129 | xiST :: ST.ShortText 130 | xiST = ST.pack xiS 131 | 132 | -- Lone continuation byte is invalid 133 | xiBS :: BS.ByteString 134 | xiBS = "test \xe3" 135 | 136 | xiBSL :: BSL.ByteString 137 | xiBSL = BSL.fromStrict xiBS 138 | 139 | xiBSB :: BSB.Builder 140 | xiBSB = BSB.byteString xiBS 141 | 142 | xiSBS :: SBS.ShortByteString 143 | xiSBS = SBS.toShort xiBS 144 | 145 | ------------------------------------------------------------------------------ 146 | 147 | newtype PosInt = PosInt Int 148 | deriving (Eq, Show) 149 | 150 | instance TTC.Parse PosInt where 151 | parse = TTC.asS $ \ s -> case readMaybe s of 152 | Just i 153 | | i >= 0 -> Right $ PosInt i 154 | | otherwise -> Left $ TTC.fromS "not positive" 155 | Nothing -> Left $ TTC.fromS "not an integer" 156 | 157 | instance TTC.Render PosInt where 158 | render (PosInt i) = TTC.convert $ show i 159 | 160 | answer :: PosInt 161 | answer = PosInt 42 162 | 163 | answerS :: String 164 | answerS = "42" 165 | 166 | answerT :: T.Text 167 | answerT = "42" 168 | 169 | answerTL :: TL.Text 170 | answerTL = "42" 171 | 172 | answerTLB :: TLB.Builder 173 | answerTLB = "42" 174 | 175 | answerST :: ST.ShortText 176 | answerST = "42" 177 | 178 | answerBS :: BS.ByteString 179 | answerBS = "42" 180 | 181 | answerBSL :: BSL.ByteString 182 | answerBSL = "42" 183 | 184 | answerBSB :: BSB.Builder 185 | answerBSB = "42" 186 | 187 | answerSBS :: SBS.ShortByteString 188 | answerSBS = "42" 189 | 190 | answerZ :: Int 191 | answerZ = 42 192 | 193 | data IntError = IntInvalid 194 | deriving (Eq, Show) 195 | 196 | ------------------------------------------------------------------------------ 197 | 198 | data Color 199 | = Red 200 | | Green 201 | | Blue 202 | | White 203 | | Black 204 | deriving (Bounded, Enum, Eq, Show) 205 | 206 | instance Read Color where 207 | readsPrec _ = TTC.readsEnum True True 208 | 209 | instance TTC.Render Color where 210 | render = TTC.fromT . \case 211 | Red -> "red" 212 | Green -> "green" 213 | Blue -> "blue" 214 | White -> "white" 215 | Black -> "black" 216 | 217 | data ColorError 218 | = ColorInvalid 219 | | ColorAmbiguous 220 | deriving (Eq, Show) 221 | 222 | redS :: String 223 | redS = "red" 224 | 225 | redT :: T.Text 226 | redT = "red" 227 | 228 | redTL :: TL.Text 229 | redTL = "red" 230 | 231 | redTLB :: TLB.Builder 232 | redTLB = "red" 233 | 234 | redST :: ST.ShortText 235 | redST = "red" 236 | 237 | redBS :: BS.ByteString 238 | redBS = "red" 239 | 240 | redBSL :: BSL.ByteString 241 | redBSL = "red" 242 | 243 | redBSB :: BSB.Builder 244 | redBSB = "red" 245 | 246 | redSBS :: SBS.ShortByteString 247 | redSBS = "red" 248 | 249 | ------------------------------------------------------------------------------ 250 | 251 | newtype PartialParser = PartialParser String 252 | deriving (Eq, Show) 253 | 254 | instance TTC.Parse PartialParser where 255 | parse = TTC.asS $ \case 256 | "" -> Left $ TTC.fromT undefined 257 | s -> Right $ PartialParser s 258 | 259 | ------------------------------------------------------------------------------ 260 | -- $Textual 261 | 262 | testConvert :: TestTree 263 | testConvert = testGroup "convert" $ 264 | [ testCase "@@" $ 265 | "test テスト" @=? TTC.convert @String @String "test テスト" 266 | , testCase "@_" $ xT @=? TTC.convert @String "test テスト" 267 | , testCase "_@" $ "test テスト" @=? TTC.convert @_ @TL.Text xT 268 | ] 269 | ++ mkTests "S" xS 270 | ++ mkTests "T" xT 271 | ++ mkTests "TL" xTL 272 | ++ mkTests "TLB" xTLB 273 | ++ mkTests "ST" xST 274 | ++ mkTests "BS" xBS 275 | ++ mkTests "BSL" xBSL 276 | ++ mkTests "BSB" xBSB 277 | ++ mkTests "SBS" xSBS 278 | where 279 | mkTests :: TTC.Textual a => String -> a -> [TestTree] 280 | mkTests s x = 281 | [ testCase (s ++ "->S") $ xS @=? TTC.convert x 282 | , testCase (s ++ "->T") $ xT @=? TTC.convert x 283 | , testCase (s ++ "->TL") $ xTL @=? TTC.convert x 284 | , testCase (s ++ "->TLB") $ xTLB @=? TTC.convert x 285 | , testCase (s ++ "->ST") $ xST @=? TTC.convert x 286 | , testCase (s ++ "->BS") $ xBS @=? TTC.convert x 287 | , testCase (s ++ "->BSL") $ xBSL @=? TTC.convert x 288 | , testCase (s ++ "->BSB") $ xBSB @=? TTC.convert x 289 | , testCase (s ++ "->SBS") $ xSBS @=? TTC.convert x 290 | ] 291 | 292 | ------------------------------------------------------------------------------ 293 | 294 | testToX :: TestTree 295 | testToX = testGroup "to*" 296 | [ mkTests "toS" TTC.toS xS xiS 297 | , mkTests "toT" TTC.toT xT xiT 298 | , mkTests "toTL" TTC.toTL xTL xiTL 299 | , mkTests "toTLB" TTC.toTLB xTLB xiTLB 300 | , mkTests "toST" TTC.toST xST xiST 301 | , mkTests "toBS" TTC.toBS xBS xiBS 302 | , mkTests "toBSL" TTC.toBSL xBSL xiBSL 303 | , mkTests "toBSB" TTC.toBSB xBSB xiBSB 304 | , mkTests "toSBS" TTC.toSBS xSBS xiSBS 305 | ] 306 | where 307 | mkTests 308 | :: (Eq a, Show a) 309 | => TestName 310 | -> (forall t. TTC.Textual t => t -> a) 311 | -> a 312 | -> a 313 | -> TestTree 314 | mkTests testName f x xi = testGroup testName 315 | [ testCase "@" $ x @=? f @T.Text "test テスト" 316 | , testCase "S" $ x @=? f xS 317 | , testCase "T" $ x @=? f xT 318 | , testCase "TL" $ x @=? f xTL 319 | , testCase "TLB" $ x @=? f xTLB 320 | , testCase "ST" $ x @=? f xST 321 | , testCase "BS" $ x @=? f xBS 322 | , testCase "BSL" $ x @=? f xBSL 323 | , testCase "BSB" $ x @=? f xBSB 324 | , testCase "SBS" $ x @=? f xSBS 325 | , testCase "BS/invalid" $ xi @=? f xiBS 326 | , testCase "BSL/invalid" $ xi @=? f xiBSL 327 | , testCase "BSB/invalid" $ xi @=? f xiBSB 328 | , testCase "SBS/invalid" $ xi @=? f xiSBS 329 | ] 330 | 331 | ------------------------------------------------------------------------------ 332 | 333 | testFromX :: TestTree 334 | testFromX = testGroup "from*" 335 | [ mkTests "fromS" TTC.fromS xS Nothing 336 | , mkTests "fromT" TTC.fromT xT Nothing 337 | , mkTests "fromTL" TTC.fromTL xTL Nothing 338 | , mkTests "fromTLB" TTC.fromTLB xTLB Nothing 339 | , mkTests "fromST" TTC.fromST xST Nothing 340 | , mkTests "fromBS" TTC.fromBS xBS (Just xiBS) 341 | , mkTests "fromBSL" TTC.fromBSL xBSL (Just xiBSL) 342 | , mkTests "fromBSB" TTC.fromBSB xBSB (Just xiBSB) 343 | , mkTests "fromSBS" TTC.fromSBS xSBS (Just xiSBS) 344 | ] 345 | where 346 | mkTests 347 | :: (Eq a, Show a) 348 | => TestName 349 | -> (forall t. TTC.Textual t => a -> t) 350 | -> a 351 | -> Maybe a 352 | -> TestTree 353 | mkTests testName f x mXi = testGroup testName $ 354 | [ testCase "@" $ "test テスト" @=? f @String x 355 | , testCase "S" $ xS @=? f x 356 | , testCase "T" $ xT @=? f x 357 | , testCase "TL" $ xTL @=? f x 358 | , testCase "TLB" $ xTLB @=? f x 359 | , testCase "ST" $ xST @=? f x 360 | , testCase "BS" $ xBS @=? f x 361 | , testCase "BSL" $ xBSL @=? f x 362 | , testCase "BSB" $ xBSB @=? f x 363 | , testCase "SBS" $ xSBS @=? f x 364 | ] ++ 365 | case mXi of 366 | Nothing -> [] 367 | Just xi -> 368 | [ testCase "S/Invalid" $ xiS @=? f xi 369 | , testCase "T/Invalid" $ xiT @=? f xi 370 | , testCase "TL/Invalid" $ xiTL @=? f xi 371 | , testCase "TLB/Invalid" $ xiTLB @=? f xi 372 | , testCase "ST/Invalid" $ xiST @=? f xi 373 | , testCase "BS/Invalid" $ xiBS @=? f xi 374 | , testCase "BSL/Invalid" $ xiBSL @=? f xi 375 | , testCase "BSB/Invalid" $ xiBSB @=? f xi 376 | , testCase "SBS/Invalid" $ xiSBS @=? f xi 377 | ] 378 | 379 | ------------------------------------------------------------------------------ 380 | 381 | testAsX :: TestTree 382 | testAsX = testGroup "as*" 383 | [ mkTests "asS" TTC.asS xS xiS 384 | , mkTests "asT" TTC.asT xT xiT 385 | , mkTests "asTL" TTC.asTL xTL xiTL 386 | , mkTests "asTLB" TTC.asTLB xTLB xiTLB 387 | , mkTests "asST" TTC.asST xST xiST 388 | , mkTests "asBS" TTC.asBS xBS xiBS 389 | , mkTests "asBSL" TTC.asBSL xBSL xiBSL 390 | , mkTests "asBSB" TTC.asBSB xBSB xiBSB 391 | , mkTests "asSBS" TTC.asSBS xSBS xiSBS 392 | ] 393 | where 394 | mkTests 395 | :: (Eq a, Show a) 396 | => TestName 397 | -> (forall t. TTC.Textual t => (a -> a) -> t -> a) 398 | -> a 399 | -> a 400 | -> TestTree 401 | mkTests testName f x xi = testGroup testName 402 | [ testCase "@" $ x @=? f @T.Text id "test テスト" 403 | , testCase "S" $ x @=? f id xS 404 | , testCase "T" $ x @=? f id xT 405 | , testCase "TL" $ x @=? f id xTL 406 | , testCase "TLB" $ x @=? f id xTLB 407 | , testCase "ST" $ x @=? f id xST 408 | , testCase "BS" $ x @=? f id xBS 409 | , testCase "BSL" $ x @=? f id xBSL 410 | , testCase "BSB" $ x @=? f id xBSB 411 | , testCase "SBS" $ x @=? f id xSBS 412 | , testCase "BS/invalid" $ xi @=? f id xiBS 413 | , testCase "BSL/invalid" $ xi @=? f id xiBSL 414 | , testCase "BSB/invalid" $ xi @=? f id xiBSB 415 | , testCase "SBS/invalid" $ xi @=? f id xiSBS 416 | ] 417 | 418 | ------------------------------------------------------------------------------ 419 | -- $Render 420 | 421 | testRender :: TestTree 422 | testRender = testGroup "render" 423 | [ testCase "_@" $ "42" @=? TTC.render @_ @String answer 424 | , testCase "S" $ answerS @=? TTC.render answer 425 | , testCase "T" $ answerT @=? TTC.render answer 426 | , testCase "TL" $ answerTL @=? TTC.render answer 427 | , testCase "TLB" $ answerTLB @=? TTC.render answer 428 | , testCase "ST" $ answerST @=? TTC.render answer 429 | , testCase "BS" $ answerBS @=? TTC.render answer 430 | , testCase "BSL" $ answerBSL @=? TTC.render answer 431 | , testCase "BSB" $ answerBSB @=? TTC.render answer 432 | , testCase "SBS" $ answerSBS @=? TTC.render answer 433 | ] 434 | 435 | ------------------------------------------------------------------------------ 436 | 437 | testRenderDefault :: TestTree 438 | testRenderDefault = testGroup "RenderDefault" 439 | [ testCase "Bool" $ "True" @=? TTC.renderS True 440 | , testCase "Char" $ "*" @=? TTC.renderS '*' 441 | , mkTestShow @Double "Double" 3.14159 442 | , mkTestShow @Float "Float" 3.14159 443 | , mkTestShow @Integer "Integer" 42 444 | , mkTestShow @Int "Int" 42 445 | , mkTestShow @Int8 "Int8" 42 446 | , mkTestShow @Int16 "Int16" 42 447 | , mkTestShow @Int32 "Int32" 42 448 | , mkTestShow @Int64 "Int64" 42 449 | , mkTestShow @Word "Word" 42 450 | , mkTestShow @Word8 "Word8" 42 451 | , mkTestShow @Word16 "Word16" 42 452 | , mkTestShow @Word32 "Word32" 42 453 | , mkTestShow @Word64 "Word64" 42 454 | , testCase "String" $ xS @=? TTC.renderS xS 455 | , testCase "T.Text" $ xS @=? TTC.renderS xT 456 | , testCase "TL.Text" $ xS @=? TTC.renderS xTL 457 | , testCase "TLB.Builder" $ xS @=? TTC.renderS xTLB 458 | , testCase "ST.ShortText" $ xS @=? TTC.renderS xST 459 | , testCase "BS.ByteString" $ xS @=? TTC.renderS xBS 460 | , testCase "BSL.ByteString" $ xS @=? TTC.renderS xBSL 461 | , testCase "BSB.Builder" $ xS @=? TTC.renderS xBSB 462 | , testCase "SBS.ShortByteString" $ xS @=? TTC.renderS xSBS 463 | ] 464 | where 465 | mkTestShow :: (Show a, TTC.Render a) => TestName -> a -> TestTree 466 | mkTestShow testName x = testCase testName $ show x @=? TTC.renderS x 467 | 468 | ------------------------------------------------------------------------------ 469 | 470 | testRenderWithShow :: TestTree 471 | testRenderWithShow = testGroup "renderWithShow" 472 | [ testCase "@" $ "42" @=? TTC.renderWithShow @String answerZ 473 | , testCase "S" $ answerS @=? TTC.renderWithShow answerZ 474 | , testCase "T" $ answerT @=? TTC.renderWithShow answerZ 475 | , testCase "TL" $ answerTL @=? TTC.renderWithShow answerZ 476 | , testCase "TLB" $ answerTLB @=? TTC.renderWithShow answerZ 477 | , testCase "ST" $ answerST @=? TTC.renderWithShow answerZ 478 | , testCase "BS" $ answerBS @=? TTC.renderWithShow answerZ 479 | , testCase "BSL" $ answerBSL @=? TTC.renderWithShow answerZ 480 | , testCase "BSB" $ answerBSB @=? TTC.renderWithShow answerZ 481 | , testCase "SBS" $ answerSBS @=? TTC.renderWithShow answerZ 482 | ] 483 | 484 | ------------------------------------------------------------------------------ 485 | 486 | testRenderX :: TestTree 487 | testRenderX = testGroup "render*" 488 | [ testCase "renderS" $ answerS @=? TTC.renderS answer 489 | , testCase "renderT" $ answerT @=? TTC.renderT answer 490 | , testCase "renderTL" $ answerTL @=? TTC.renderTL answer 491 | , testCase "renderTLB" $ answerTLB @=? TTC.renderTLB answer 492 | , testCase "renderST" $ answerST @=? TTC.renderST answer 493 | , testCase "renderBS" $ answerBS @=? TTC.renderBS answer 494 | , testCase "renderBSL" $ answerBSL @=? TTC.renderBSL answer 495 | , testCase "renderBSB" $ answerBSB @=? TTC.renderBSB answer 496 | , testCase "renderSBS" $ answerSBS @=? TTC.renderSBS answer 497 | ] 498 | 499 | ------------------------------------------------------------------------------ 500 | -- $Parse 501 | 502 | testParse :: TestTree 503 | testParse = testGroup "parse" 504 | [ testCase "_@@" $ Right answer @=? TTC.parse @_ @String @String "42" 505 | , testCase "S" $ Right answer @=? parse answerS 506 | , testCase "T" $ Right answer @=? parse answerT 507 | , testCase "TL" $ Right answer @=? parse answerTL 508 | , testCase "TLB" $ Right answer @=? parse answerTLB 509 | , testCase "ST" $ Right answer @=? parse answerST 510 | , testCase "BS" $ Right answer @=? parse answerBS 511 | , testCase "BSL" $ Right answer @=? parse answerBSL 512 | , testCase "BSB" $ Right answer @=? parse answerBSB 513 | , testCase "SBS" $ Right answer @=? parse answerSBS 514 | , testCase "negative" $ Left "not positive" @=? parse @String "-42" 515 | , testCase "invalid" $ Left "not an integer" @=? parse @String "4a2" 516 | ] 517 | where 518 | parse :: TTC.Textual t => t -> Either String PosInt 519 | parse = TTC.parse 520 | 521 | ------------------------------------------------------------------------------ 522 | 523 | testParseDefault :: TestTree 524 | testParseDefault = testGroup "ParseDefault" 525 | [ testGroup "Bool" 526 | [ testCase "True" $ Right True @=? parse "True" 527 | , testCase "False" $ Right False @=? parse "False" 528 | , testCase "invalid" $ Left "invalid Bool" @=? parse @Bool "false" 529 | ] 530 | , testGroup "Char" 531 | [ testCase "valid" $ Right '*' @=? parse "*" 532 | , testCase "empty" $ Left "invalid Char" @=? parse @Char "" 533 | , testCase "multiple" $ Left "invalid Char" @=? parse @Char "**" 534 | ] 535 | , mkTestsShow @Double "Double" 3.14159 536 | , mkTestsShow @Float "Float" 3.14159 537 | , mkTestsShow @Integer "Integer" 42 538 | , mkTestsShow @Int "Int" 42 539 | , mkTestsShow @Int8 "Int8" 42 540 | , mkTestsShow @Int16 "Int16" 42 541 | , mkTestsShow @Int32 "Int32" 42 542 | , mkTestsShow @Int64 "Int64" 42 543 | , mkTestsShow @Word "Word" 42 544 | , mkTestsShow @Word8 "Word8" 42 545 | , mkTestsShow @Word16 "Word16" 42 546 | , mkTestsShow @Word32 "Word32" 42 547 | , mkTestsShow @Word64 "Word64" 42 548 | , mkTestsTextual @String "String" xS 549 | , mkTestsTextual @T.Text "T.Text" xT 550 | , mkTestsTextual @TL.Text "TL.Text" xTL 551 | , mkTestsTextual @TLB.Builder "TLB.Builder" xTLB 552 | , mkTestsTextual @ST.ShortText "ST.ShortText" xST 553 | , mkTestsTextual @BS.ByteString "BS.ByteString" xBS 554 | , mkTestsTextual @BSL.ByteString "BSL.ByteString" xBSL 555 | , mkTestsTextual @BSB.Builder "BSB.Builder" xBSB 556 | , mkTestsTextual @SBS.ShortByteString "SBS.ShortByteString" xSBS 557 | ] 558 | where 559 | mkTestsShow 560 | :: forall a. (Eq a, Show a, TTC.Parse a) 561 | => TestName 562 | -> a 563 | -> TestTree 564 | mkTestsShow testName x = testGroup testName 565 | [ testCase "valid" $ Right x @=? parse (show x) 566 | , testCase "invalid" $ 567 | Left ("invalid " ++ testName) @=? parse @a "invalid" 568 | ] 569 | 570 | mkTestsTextual 571 | :: forall a. (Eq a, IsString a, Show a, TTC.Parse a) 572 | => TestName 573 | -> a 574 | -> TestTree 575 | mkTestsTextual testName x = testGroup testName 576 | [ testCase "empty" $ Right "" @=? parse @a "" 577 | , testCase "nonempty" $ Right x @=? parse xS 578 | ] 579 | 580 | parse :: TTC.Parse a => String -> Either String a 581 | parse = TTC.parse 582 | 583 | ------------------------------------------------------------------------------ 584 | 585 | testWithError :: TestTree 586 | testWithError = testGroup "withError" 587 | [ testCase "valid" $ 588 | Right answer @=? TTC.withError @String @String undefined (Just answer) 589 | , testCase "invalid" $ 590 | Left "err" @=? TTC.withError @String @String @PosInt "err" Nothing 591 | ] 592 | 593 | testWithErrorX :: TestTree 594 | testWithErrorX = testGroup "withError*" 595 | [ mkTests "withErrorS" TTC.withErrorS 596 | , mkTests "withErrorT" TTC.withErrorT 597 | , mkTests "withErrorTL" TTC.withErrorTL 598 | , mkTests "withErrorTLB" TTC.withErrorTLB 599 | , mkTests "withErrorST" TTC.withErrorST 600 | , mkTests "withErrorBS" TTC.withErrorBS 601 | , mkTests "withErrorBSL" TTC.withErrorBSL 602 | , mkTests "withErrorBSB" TTC.withErrorBSB 603 | , mkTests "withErrorSBS" TTC.withErrorSBS 604 | ] 605 | where 606 | mkTests 607 | :: IsString e' 608 | => TestName 609 | -> (e' -> Maybe PosInt -> Either String PosInt) 610 | -> TestTree 611 | mkTests testName f = testGroup testName 612 | [ testCase "valid" $ Right answer @=? f undefined (Just answer) 613 | , testCase "invalid" $ Left "err" @=? f "err" Nothing 614 | ] 615 | 616 | ------------------------------------------------------------------------------ 617 | 618 | testPrefixError :: TestTree 619 | testPrefixError = testGroup "prefixError" 620 | [ testCase "valid" $ 621 | Right answer 622 | @=? TTC.prefixError @String @String "oops: " (Right answer) 623 | , testCase "invalid" $ 624 | Left "oops: err" 625 | @=? TTC.prefixError @String @String @PosInt "oops: " (Left "err") 626 | ] 627 | 628 | testPrefixErrorX :: TestTree 629 | testPrefixErrorX = testGroup "prefixError*" 630 | [ mkTests "prefixErrorS" TTC.prefixErrorS 631 | , mkTests "prefixErrorT" TTC.prefixErrorT 632 | , mkTests "prefixErrorTL" TTC.prefixErrorTL 633 | , mkTests "prefixErrorTLB" TTC.prefixErrorTLB 634 | , mkTests "prefixErrorST" TTC.prefixErrorST 635 | , mkTests "prefixErrorBS" TTC.prefixErrorBS 636 | , mkTests "prefixErrorBSL" TTC.prefixErrorBSL 637 | , mkTests "prefixErrorBSB" TTC.prefixErrorBSB 638 | , mkTests "prefixErrorSBS" TTC.prefixErrorSBS 639 | ] 640 | where 641 | mkTests 642 | :: IsString e' 643 | => TestName 644 | -> (e' -> Either e' PosInt -> Either String PosInt) 645 | -> TestTree 646 | mkTests testName f = testGroup testName 647 | [ testCase "valid" $ Right answer @=? f "oops: " (Right answer) 648 | , testCase "invalid" $ Left "oops: err" @=? f "oops: " (Left "err") 649 | ] 650 | 651 | ------------------------------------------------------------------------------ 652 | 653 | testParseWithRead :: TestTree 654 | testParseWithRead = testGroup "parseWithRead" 655 | [ testCase "@" $ 656 | Right answerZ @=? TTC.parseWithRead @String IntInvalid "42" 657 | , testCase "S" $ Right answerZ @=? parseWithRead answerS 658 | , testCase "T" $ Right answerZ @=? parseWithRead answerT 659 | , testCase "TL" $ Right answerZ @=? parseWithRead answerTL 660 | , testCase "TLB" $ Right answerZ @=? parseWithRead answerTLB 661 | , testCase "ST" $ Right answerZ @=? parseWithRead answerST 662 | , testCase "BS" $ Right answerZ @=? parseWithRead answerBS 663 | , testCase "BSL" $ Right answerZ @=? parseWithRead answerBSL 664 | , testCase "BSB" $ Right answerZ @=? parseWithRead answerBSB 665 | , testCase "SBS" $ Right answerZ @=? parseWithRead answerSBS 666 | , testCase "invalid" $ Left IntInvalid @=? parseWithRead @String "4a2" 667 | ] 668 | where 669 | parseWithRead :: TTC.Textual t => t -> Either IntError Int 670 | parseWithRead = TTC.parseWithRead IntInvalid 671 | 672 | testParseWithRead' :: TestTree 673 | testParseWithRead' = testGroup "parseWithRead'" 674 | [ testCase "@" $ 675 | Right answerZ @=? TTC.parseWithRead' @String @String "Int" "42" 676 | , testCase "S" $ Right answerZ @=? parseWithRead' answerS 677 | , testCase "T" $ Right answerZ @=? parseWithRead' answerT 678 | , testCase "TL" $ Right answerZ @=? parseWithRead' answerTL 679 | , testCase "TLB" $ Right answerZ @=? parseWithRead' answerTLB 680 | , testCase "ST" $ Right answerZ @=? parseWithRead' answerST 681 | , testCase "BS" $ Right answerZ @=? parseWithRead' answerBS 682 | , testCase "BSL" $ Right answerZ @=? parseWithRead' answerBSL 683 | , testCase "BSB" $ Right answerZ @=? parseWithRead' answerBSB 684 | , testCase "SBS" $ Right answerZ @=? parseWithRead' answerSBS 685 | , testCase "invalid" $ Left "invalid Int" @=? parseWithRead' @String "4a2" 686 | ] 687 | where 688 | parseWithRead' :: TTC.Textual t => t -> Either String Int 689 | parseWithRead' = TTC.parseWithRead' "Int" 690 | 691 | testMaybeParseWithRead :: TestTree 692 | testMaybeParseWithRead = testGroup "maybeParseWithRead" 693 | [ testCase "@" $ Just answerZ @=? TTC.maybeParseWithRead @String "42" 694 | , testCase "S" $ Just answerZ @=? TTC.maybeParseWithRead answerS 695 | , testCase "T" $ Just answerZ @=? TTC.maybeParseWithRead answerT 696 | , testCase "TL" $ Just answerZ @=? TTC.maybeParseWithRead answerTL 697 | , testCase "TLB" $ Just answerZ @=? TTC.maybeParseWithRead answerTLB 698 | , testCase "ST" $ Just answerZ @=? TTC.maybeParseWithRead answerST 699 | , testCase "BS" $ Just answerZ @=? TTC.maybeParseWithRead answerBS 700 | , testCase "BSL" $ Just answerZ @=? TTC.maybeParseWithRead answerBSL 701 | , testCase "BSB" $ Just answerZ @=? TTC.maybeParseWithRead answerBSB 702 | , testCase "SBS" $ Just answerZ @=? TTC.maybeParseWithRead answerSBS 703 | , testCase "invalid" $ 704 | Nothing @=? TTC.maybeParseWithRead @String @Int "4a2" 705 | ] 706 | 707 | ------------------------------------------------------------------------------ 708 | 709 | testParseEnum :: TestTree 710 | testParseEnum = testGroup "parseEnum" 711 | [ testCase "@" $ 712 | Right Red 713 | @=? TTC.parseEnum @String 714 | False False ColorInvalid ColorAmbiguous "red" 715 | , testCase "S" $ Right Red @=? parseEnum False False redS 716 | , testCase "T" $ Right Red @=? parseEnum False False redT 717 | , testCase "TL" $ Right Red @=? parseEnum False False redTL 718 | , testCase "TLB" $ Right Red @=? parseEnum False False redTLB 719 | , testCase "ST" $ Right Red @=? parseEnum False False redST 720 | , testCase "BS" $ Right Red @=? parseEnum False False redBS 721 | , testCase "BSL" $ Right Red @=? parseEnum False False redBSL 722 | , testCase "BSB" $ Right Red @=? parseEnum False False redBSB 723 | , testCase "SBS" $ Right Red @=? parseEnum False False redSBS 724 | , testCase "CI" $ Right Red @=? parseEnum @String True False "Red" 725 | , testCase "!CI" $ 726 | Left ColorInvalid @=? parseEnum @String False False "Red" 727 | , testCase "prefix" $ Right Red @=? parseEnum @String False True "r" 728 | , testCase "ambiguous" $ 729 | Left ColorAmbiguous @=? parseEnum @String False True "bl" 730 | ] 731 | where 732 | parseEnum :: TTC.Textual t => Bool -> Bool -> t -> Either ColorError Color 733 | parseEnum allowCI allowPrefix = 734 | TTC.parseEnum allowCI allowPrefix ColorInvalid ColorAmbiguous 735 | 736 | testParseEnum' :: TestTree 737 | testParseEnum' = testGroup "parseEnum'" 738 | [ testCase "@" $ 739 | Right Red 740 | @=? TTC.parseEnum' @String @String "Color" False False "red" 741 | , testCase "S" $ Right Red @=? parseEnum' False False redS 742 | , testCase "T" $ Right Red @=? parseEnum' False False redT 743 | , testCase "TL" $ Right Red @=? parseEnum' False False redTL 744 | , testCase "TLB" $ Right Red @=? parseEnum' False False redTLB 745 | , testCase "ST" $ Right Red @=? parseEnum' False False redST 746 | , testCase "BS" $ Right Red @=? parseEnum' False False redBS 747 | , testCase "BSL" $ Right Red @=? parseEnum' False False redBSL 748 | , testCase "BSB" $ Right Red @=? parseEnum' False False redBSB 749 | , testCase "SBS" $ Right Red @=? parseEnum' False False redSBS 750 | , testCase "CI" $ Right Red @=? parseEnum' @String True False "Red" 751 | , testCase "!CI" $ 752 | Left "invalid Color" @=? parseEnum' @String False False "Red" 753 | , testCase "prefix" $ Right Red @=? parseEnum' @String False True "r" 754 | , testCase "ambiguous" $ 755 | Left "ambiguous Color" @=? parseEnum' @String False True "bl" 756 | ] 757 | where 758 | parseEnum' :: TTC.Textual t => Bool -> Bool -> t -> Either String Color 759 | parseEnum' = TTC.parseEnum' "Color" 760 | 761 | ------------------------------------------------------------------------------ 762 | 763 | testParseX :: TestTree 764 | testParseX = testGroup "parse*" 765 | [ mkTests "parseS" TTC.parseS answerS 766 | , mkTests "parseT" TTC.parseT answerT 767 | , mkTests "parseTL" TTC.parseTL answerTL 768 | , mkTests "parseTLB" TTC.parseTLB answerTLB 769 | , mkTests "parseST" TTC.parseST answerST 770 | , mkTests "parseBS" TTC.parseBS answerBS 771 | , mkTests "parseBSL" TTC.parseBSL answerBSL 772 | , mkTests "parseBSB" TTC.parseBSB answerBSB 773 | , mkTests "parseSBS" TTC.parseSBS answerSBS 774 | ] 775 | where 776 | mkTests 777 | :: IsString a 778 | => TestName 779 | -> (a -> Either String PosInt) 780 | -> a 781 | -> TestTree 782 | mkTests testName f x = testGroup testName 783 | [ testCase "valid" $ Right answer @=? f x 784 | , testCase "invalid" $ Left "not an integer" @=? f "4a2" 785 | ] 786 | 787 | ------------------------------------------------------------------------------ 788 | 789 | testParseMaybe :: TestTree 790 | testParseMaybe = testGroup "parseMaybe" 791 | [ testCase "@" $ Just answer @=? TTC.parseMaybe @String "42" 792 | , testCase "S" $ Just answer @=? TTC.parseMaybe answerS 793 | , testCase "T" $ Just answer @=? TTC.parseMaybe answerT 794 | , testCase "TL" $ Just answer @=? TTC.parseMaybe answerTL 795 | , testCase "TLB" $ Just answer @=? TTC.parseMaybe answerTLB 796 | , testCase "ST" $ Just answer @=? TTC.parseMaybe answerST 797 | , testCase "BS" $ Just answer @=? TTC.parseMaybe answerBS 798 | , testCase "BSL" $ Just answer @=? TTC.parseMaybe answerBSL 799 | , testCase "BSB" $ Just answer @=? TTC.parseMaybe answerBSB 800 | , testCase "SBS" $ Just answer @=? TTC.parseMaybe answerSBS 801 | , testCase "noerror" $ 802 | Just (PartialParser "test") @=? TTC.parseMaybe @String "test" 803 | , testCase "negative" $ Nothing @=? TTC.parseMaybe @String @PosInt "-42" 804 | , testCase "invalid" $ Nothing @=? TTC.parseMaybe @String @PosInt "4a2" 805 | ] 806 | 807 | testParseMaybeX :: TestTree 808 | testParseMaybeX = testGroup "parseMaybe*" 809 | [ mkTestsParseMaybe "parseMaybeS" TTC.parseMaybeS answerS 810 | , mkTestsParseMaybe "parseMaybeT" TTC.parseMaybeT answerT 811 | , mkTestsParseMaybe "parseMaybeTL" TTC.parseMaybeTL answerTL 812 | , mkTestsParseMaybe "parseMaybeTLB" TTC.parseMaybeTLB answerTLB 813 | , mkTestsParseMaybe "parseMaybeST" TTC.parseMaybeST answerST 814 | , mkTestsParseMaybe "parseMaybeBS" TTC.parseMaybeBS answerBS 815 | , mkTestsParseMaybe "parseMaybeBSL" TTC.parseMaybeBSL answerBSL 816 | , mkTestsParseMaybe "parseMaybeBSB" TTC.parseMaybeBSB answerBSB 817 | , mkTestsParseMaybe "parseMaybeSBS" TTC.parseMaybeSBS answerSBS 818 | ] 819 | 820 | mkTestsParseMaybe 821 | :: IsString a 822 | => TestName 823 | -> (a -> Maybe PosInt) 824 | -> a 825 | -> TestTree 826 | mkTestsParseMaybe testName f x = testGroup testName 827 | [ testCase "valid" $ Just answer @=? f x 828 | , testCase "invalid" $ Nothing @=? f "4a2" 829 | ] 830 | 831 | ------------------------------------------------------------------------------ 832 | 833 | testParseOrFail :: TestTree 834 | testParseOrFail = testGroup "parseOrFail" 835 | [ testCase "@" $ Just answer @=? TTC.parseOrFail @String "42" 836 | , testCase "S" $ Just answer @=? TTC.parseOrFail answerS 837 | , testCase "T" $ Just answer @=? TTC.parseOrFail answerT 838 | , testCase "TL" $ Just answer @=? TTC.parseOrFail answerTL 839 | , testCase "TLB" $ Just answer @=? TTC.parseOrFail answerTLB 840 | , testCase "ST" $ Just answer @=? TTC.parseOrFail answerST 841 | , testCase "BS" $ Just answer @=? TTC.parseOrFail answerBS 842 | , testCase "BSL" $ Just answer @=? TTC.parseOrFail answerBSL 843 | , testCase "BSB" $ Just answer @=? TTC.parseOrFail answerBSB 844 | , testCase "SBS" $ Just answer @=? TTC.parseOrFail answerSBS 845 | , testCase "noerror" $ 846 | Just (PartialParser "test") @=? TTC.parseOrFail @String "test" 847 | , testCase "negative" $ Nothing @=? TTC.parseOrFail @String @PosInt "-42" 848 | , testCase "invalid" $ Nothing @=? TTC.parseOrFail @String @PosInt "4a2" 849 | ] 850 | 851 | testParseOrFailX :: TestTree 852 | testParseOrFailX = testGroup "parseOrFail*" 853 | [ mkTestsParseMaybe "parseOrFailS" TTC.parseOrFailS answerS 854 | , mkTestsParseMaybe "parseOrFailT" TTC.parseOrFailT answerT 855 | , mkTestsParseMaybe "parseOrFailTL" TTC.parseOrFailTL answerTL 856 | , mkTestsParseMaybe "parseOrFailTLB" TTC.parseOrFailTLB answerTLB 857 | , mkTestsParseMaybe "parseOrFailST" TTC.parseOrFailST answerST 858 | , mkTestsParseMaybe "parseOrFailBS" TTC.parseOrFailBS answerBS 859 | , mkTestsParseMaybe "parseOrFailBSL" TTC.parseOrFailBSL answerBSL 860 | , mkTestsParseMaybe "parseOrFailBSB" TTC.parseOrFailBSB answerBSB 861 | , mkTestsParseMaybe "parseOrFailSBS" TTC.parseOrFailSBS answerSBS 862 | ] 863 | 864 | ------------------------------------------------------------------------------ 865 | 866 | testParseUnsafe :: TestTree 867 | testParseUnsafe = testGroup "parseUnsafe" 868 | [ testCase "@" $ answer @=? TTC.parseUnsafe @String "42" 869 | , testCase "S" $ answer @=? TTC.parseUnsafe answerS 870 | , testCase "T" $ answer @=? TTC.parseUnsafe answerT 871 | , testCase "TL" $ answer @=? TTC.parseUnsafe answerTL 872 | , testCase "TLB" $ answer @=? TTC.parseUnsafe answerTLB 873 | , testCase "ST" $ answer @=? TTC.parseUnsafe answerST 874 | , testCase "BS" $ answer @=? TTC.parseUnsafe answerBS 875 | , testCase "BSL" $ answer @=? TTC.parseUnsafe answerBSL 876 | , testCase "BSB" $ answer @=? TTC.parseUnsafe answerBSB 877 | , testCase "SBS" $ answer @=? TTC.parseUnsafe answerSBS 878 | , testCase "negative" . assertRaises (Proxy :: Proxy ErrorCall) $ 879 | TTC.parseUnsafe @String @PosInt "-42" 880 | , testCase "invalid" . assertRaises (Proxy :: Proxy ErrorCall) $ 881 | TTC.parseUnsafe @String @PosInt "4a2" 882 | ] 883 | 884 | testParseUnsafeX :: TestTree 885 | testParseUnsafeX = testGroup "parseUnsafe*" 886 | [ mkTests "parseUnsafeS" TTC.parseUnsafeS answerS 887 | , mkTests "parseUnsafeT" TTC.parseUnsafeT answerT 888 | , mkTests "parseUnsafeTL" TTC.parseUnsafeTL answerTL 889 | , mkTests "parseUnsafeTLB" TTC.parseUnsafeTLB answerTLB 890 | , mkTests "parseUnsafeST" TTC.parseUnsafeST answerST 891 | , mkTests "parseUnsafeBS" TTC.parseUnsafeBS answerBS 892 | , mkTests "parseUnsafeBSL" TTC.parseUnsafeBSL answerBSL 893 | , mkTests "parseUnsafeBSB" TTC.parseUnsafeBSB answerBSB 894 | , mkTests "parseUnsafeSBS" TTC.parseUnsafeSBS answerSBS 895 | ] 896 | where 897 | mkTests 898 | :: IsString a 899 | => TestName 900 | -> (a -> PosInt) 901 | -> a 902 | -> TestTree 903 | mkTests testName f x = testGroup testName 904 | [ testCase "valid" $ answer @=? f x 905 | , testCase "invalid" . assertRaises (Proxy :: Proxy ErrorCall) $ f "4a2" 906 | ] 907 | 908 | ------------------------------------------------------------------------------ 909 | 910 | testReadsWithParse :: TestTree 911 | testReadsWithParse = testGroup "readsWithParse" 912 | [ testCase "valid" $ [(answer, "")] @=? TTC.readsWithParse answerS 913 | , testCase "invalid" $ [] @=? (TTC.readsWithParse :: ReadS PosInt) "-42" 914 | ] 915 | 916 | testReadsEnum :: TestTree 917 | testReadsEnum = testGroup "readsEnum" 918 | [ testCase "valid" $ Just Red @=? readMaybe "R" 919 | , testCase "invalid" $ Nothing @=? readMaybe @Color "bl" 920 | ] 921 | 922 | ------------------------------------------------------------------------------ 923 | -- $Valid 924 | 925 | testValid :: TestTree 926 | testValid = testCase "valid" $ Ex "test" @=? validConst 927 | where 928 | validConst :: Ex 929 | validConst = $$(TTC.valid "test") 930 | 931 | testValidIsString :: TestTree 932 | testValidIsString = 933 | testCase "validIsString" $ Ex "test" @=? validConst 934 | where 935 | validConst :: Ex 936 | validConst = $$("test") 937 | 938 | testValidOf :: TestTree 939 | testValidOf = testCase "validOf" $ 940 | Ex "test" @=? $$(TTC.validOf (Proxy :: Proxy Ex) "test") 941 | 942 | testMkValid :: TestTree 943 | testMkValid = testCase "mkValid" $ Ex "test" @=? $$(Ex.valid "test") 944 | 945 | testUntypedValidOf :: TestTree 946 | testUntypedValidOf = testCase "untypedValidOf" $ 947 | Ex "test" @=? $(TTC.untypedValidOf (Proxy :: Proxy Ex) "test") 948 | 949 | testMkUntypedValid :: TestTree 950 | testMkUntypedValid = testCase "mkUntypedValid" $ 951 | Ex "test" @=? $(Ex.untypedValid "test") 952 | 953 | testMkUntypedValidQQ :: TestTree 954 | testMkUntypedValidQQ = testCase "mkUntypedValidQQ" $ 955 | Ex "test" @=? [Ex.untypedValidQQ|test|] 956 | 957 | ------------------------------------------------------------------------------ 958 | 959 | tests :: TestTree 960 | tests = testGroup "Data.TTC" 961 | [ testGroup "Textual" 962 | [ testConvert 963 | , testToX 964 | , testFromX 965 | , testAsX 966 | ] 967 | , testGroup "Render" 968 | [ testRender 969 | , testRenderDefault 970 | , testRenderWithShow 971 | , testRenderX 972 | ] 973 | , testGroup "Parse" 974 | [ testParse 975 | , testParseDefault 976 | , testWithError 977 | , testWithErrorX 978 | , testPrefixError 979 | , testPrefixErrorX 980 | , testParseWithRead 981 | , testParseWithRead' 982 | , testMaybeParseWithRead 983 | , testParseEnum 984 | , testParseEnum' 985 | , testParseX 986 | , testParseMaybe 987 | , testParseMaybeX 988 | , testParseOrFail 989 | , testParseOrFailX 990 | , testParseUnsafe 991 | , testParseUnsafeX 992 | , testReadsWithParse 993 | , testReadsEnum 994 | ] 995 | , testGroup "Valid" 996 | [ testValid 997 | , testValidIsString 998 | , testValidOf 999 | , testMkValid 1000 | , testUntypedValidOf 1001 | , testMkUntypedValid 1002 | , testMkUntypedValidQQ 1003 | ] 1004 | ] 1005 | -------------------------------------------------------------------------------- /ttc/test/Data/TTC/Wrapper/Test.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE CPP #-} 2 | {-# LANGUAGE DerivingVia #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE TypeApplications #-} 5 | 6 | {-# OPTIONS_GHC -fno-warn-orphans #-} 7 | 8 | module Data.TTC.Wrapper.Test (tests) where 9 | 10 | -- https://hackage.haskell.org/package/bytestring 11 | import qualified Data.ByteString as BS 12 | import qualified Data.ByteString.Builder as BSB 13 | import qualified Data.ByteString.Lazy as BSL 14 | import qualified Data.ByteString.Short as SBS 15 | 16 | -- https://hackage.haskell.org/package/tasty 17 | import Test.Tasty (TestTree, testGroup) 18 | 19 | -- https://hackage.haskell.org/package/tasty-hunit 20 | import Test.Tasty.HUnit ((@=?), testCase) 21 | 22 | -- https://hackage.haskell.org/package/text 23 | import qualified Data.Text as T 24 | import qualified Data.Text.Lazy as TL 25 | import qualified Data.Text.Lazy.Builder as TLB 26 | 27 | -- https://hackage.haskell.org/package/text-short 28 | import qualified Data.Text.Short as ST 29 | 30 | -- (ttc) 31 | import qualified Data.TTC as TTC 32 | import qualified Data.TTC.Wrapper as TTCW 33 | 34 | ------------------------------------------------------------------------------ 35 | 36 | instance Eq BSB.Builder where 37 | x == y = BSB.toLazyByteString x == BSB.toLazyByteString y 38 | 39 | #if !MIN_VERSION_bytestring(0,11,1) 40 | instance Show BSB.Builder where 41 | show = show . BSB.toLazyByteString 42 | #endif 43 | 44 | ------------------------------------------------------------------------------ 45 | -- $TestData 46 | 47 | xS :: String 48 | xS = "test テスト" 49 | 50 | xT :: T.Text 51 | xT = "test テスト" 52 | 53 | ------------------------------------------------------------------------------ 54 | -- $WrapperS 55 | 56 | newtype ExS = ExS String 57 | deriving (Eq, Ord, Show) 58 | deriving TTC.Parse via TTCW.WrapperS 59 | deriving TTC.Render via TTCW.WrapperS 60 | 61 | testWrapperS :: TestTree 62 | testWrapperS = testCase "WrapperS" $ 63 | Right xT @=? (TTC.render <$> TTC.parse @ExS @_ @String xT) 64 | 65 | ------------------------------------------------------------------------------ 66 | -- $WrapperT 67 | 68 | newtype ExT = ExT T.Text 69 | deriving (Eq, Ord, Show) 70 | deriving TTC.Parse via TTCW.WrapperT 71 | deriving TTC.Render via TTCW.WrapperT 72 | 73 | testWrapperT :: TestTree 74 | testWrapperT = testCase "WrapperT" $ 75 | Right xS @=? (TTC.render <$> TTC.parse @ExT @_ @String xS) 76 | 77 | ------------------------------------------------------------------------------ 78 | -- $WrapperTL 79 | 80 | newtype ExTL = ExTL TL.Text 81 | deriving (Eq, Ord, Show) 82 | deriving TTC.Parse via TTCW.WrapperTL 83 | deriving TTC.Render via TTCW.WrapperTL 84 | 85 | testWrapperTL :: TestTree 86 | testWrapperTL = testCase "WrapperTL" $ 87 | Right xS @=? (TTC.render <$> TTC.parse @ExTL @_ @String xS) 88 | 89 | ------------------------------------------------------------------------------ 90 | -- $WrapperTLB 91 | 92 | newtype ExTLB = ExTLB TLB.Builder 93 | deriving (Eq, Ord, Show) 94 | deriving TTC.Parse via TTCW.WrapperTLB 95 | deriving TTC.Render via TTCW.WrapperTLB 96 | 97 | testWrapperTLB :: TestTree 98 | testWrapperTLB = testCase "WrapperTLB" $ 99 | Right xS @=? (TTC.render <$> TTC.parse @ExTLB @_ @String xS) 100 | 101 | ------------------------------------------------------------------------------ 102 | -- $WrapperST 103 | 104 | newtype ExST = ExST ST.ShortText 105 | deriving (Eq, Ord, Show) 106 | deriving TTC.Parse via TTCW.WrapperST 107 | deriving TTC.Render via TTCW.WrapperST 108 | 109 | testWrapperST :: TestTree 110 | testWrapperST = testCase "WrapperST" $ 111 | Right xS @=? (TTC.render <$> TTC.parse @ExST @_ @String xS) 112 | 113 | ------------------------------------------------------------------------------ 114 | -- $WrapperBS 115 | 116 | newtype ExBS = ExBS BS.ByteString 117 | deriving (Eq, Ord, Show) 118 | deriving TTC.Parse via TTCW.WrapperBS 119 | deriving TTC.Render via TTCW.WrapperBS 120 | 121 | testWrapperBS :: TestTree 122 | testWrapperBS = testCase "WrapperBS" $ 123 | Right xS @=? (TTC.render <$> TTC.parse @ExBS @_ @String xS) 124 | 125 | ------------------------------------------------------------------------------ 126 | -- $WrapperBSL 127 | 128 | newtype ExBSL = ExBSL BSL.ByteString 129 | deriving (Eq, Ord, Show) 130 | deriving TTC.Parse via TTCW.WrapperBSL 131 | deriving TTC.Render via TTCW.WrapperBSL 132 | 133 | testWrapperBSL :: TestTree 134 | testWrapperBSL = testCase "WrapperBSL" $ 135 | Right xS @=? (TTC.render <$> TTC.parse @ExBSL @_ @String xS) 136 | 137 | ------------------------------------------------------------------------------ 138 | -- $WrapperBSB 139 | 140 | newtype ExBSB = ExBSB BSB.Builder 141 | deriving (Eq, Show) 142 | deriving TTC.Parse via TTCW.WrapperBSB 143 | deriving TTC.Render via TTCW.WrapperBSB 144 | 145 | testWrapperBSB :: TestTree 146 | testWrapperBSB = testCase "WrapperBSB" $ 147 | Right xS @=? (TTC.render <$> TTC.parse @ExBSB @_ @String xS) 148 | 149 | ------------------------------------------------------------------------------ 150 | -- $WrapperSBS 151 | 152 | newtype ExSBS = ExSBS SBS.ShortByteString 153 | deriving (Eq, Ord, Show) 154 | deriving TTC.Parse via TTCW.WrapperSBS 155 | deriving TTC.Render via TTCW.WrapperSBS 156 | 157 | testWrapperSBS :: TestTree 158 | testWrapperSBS = testCase "WrapperSBS" $ 159 | Right xS @=? (TTC.render <$> TTC.parse @ExSBS @_ @String xS) 160 | 161 | ------------------------------------------------------------------------------ 162 | 163 | tests :: TestTree 164 | tests = testGroup "Data.TTC.Wrapper" 165 | [ testWrapperS 166 | , testWrapperT 167 | , testWrapperTL 168 | , testWrapperTLB 169 | , testWrapperST 170 | , testWrapperBS 171 | , testWrapperBSL 172 | , testWrapperBSB 173 | , testWrapperSBS 174 | ] 175 | -------------------------------------------------------------------------------- /ttc/test/Spec.hs: -------------------------------------------------------------------------------- 1 | module Main (main) where 2 | 3 | -- https://hackage.haskell.org/package/tasty 4 | import Test.Tasty (defaultMain, testGroup) 5 | 6 | -- (ttc:test) 7 | import qualified Data.TTC.Test 8 | import qualified Data.TTC.Wrapper.Test 9 | 10 | ------------------------------------------------------------------------------ 11 | 12 | main :: IO () 13 | main = defaultMain $ testGroup "test" 14 | [ Data.TTC.Test.tests 15 | , Data.TTC.Wrapper.Test.tests 16 | ] 17 | -------------------------------------------------------------------------------- /ttc/test/TestTypes/Ex.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveLift #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | 4 | module TestTypes.Ex where 5 | 6 | -- https://hackage.haskell.org/package/template-haskell 7 | import qualified Language.Haskell.TH.Syntax as THS 8 | 9 | -- (ttc) 10 | import qualified Data.TTC as TTC 11 | 12 | ------------------------------------------------------------------------------ 13 | -- $Type 14 | 15 | newtype Ex = Ex { exString :: String } 16 | deriving (Eq, Ord, Show, THS.Lift) 17 | 18 | instance TTC.Parse Ex where 19 | parse = TTC.asS $ pure . Ex 20 | 21 | instance TTC.Render Ex where 22 | render = TTC.convert . exString 23 | 24 | ------------------------------------------------------------------------------ 25 | -- $API 26 | 27 | $(TTC.mkValid "valid" ''Ex) 28 | 29 | $(TTC.mkUntypedValid "untypedValid" ''Ex) 30 | 31 | $(TTC.mkUntypedValidQQ "untypedValidQQ" ''Ex) 32 | -------------------------------------------------------------------------------- /ttc/ttc.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 3.0 2 | name: ttc 3 | version: 1.5.0.0 4 | synopsis: Textual Type Classes 5 | description: 6 | This library provides type classes for conversion between data types and 7 | textual data types (strings). Please see the README on GitHub at 8 | . 9 | homepage: https://github.com/ExtremaIS/ttc-haskell/tree/main/ttc#readme 10 | bug-reports: https://github.com/ExtremaIS/ttc-haskell/issues 11 | license: MIT 12 | license-file: LICENSE 13 | author: Travis Cardwell 14 | maintainer: Travis Cardwell 15 | copyright: Copyright (c) 2019-2025 Travis Cardwell 16 | category: Data, Text 17 | build-type: Simple 18 | 19 | extra-doc-files: 20 | CHANGELOG.md 21 | README.md 22 | 23 | tested-with: 24 | GHC ==8.8.4 25 | || ==8.10.7 26 | || ==9.0.2 27 | || ==9.2.8 28 | || ==9.4.8 29 | || ==9.6.6 30 | || ==9.8.4 31 | || ==9.10.1 32 | || ==9.12.1 33 | 34 | source-repository head 35 | type: git 36 | location: https://github.com/ExtremaIS/ttc-haskell.git 37 | 38 | library 39 | hs-source-dirs: src 40 | exposed-modules: 41 | Data.TTC 42 | , Data.TTC.Wrapper 43 | build-depends: 44 | base >=4.13.0.0 && <4.22 45 | , bytestring >=0.10.10.1 && <0.13 46 | , template-haskell >=2.15.0.0 && <2.24 47 | , text >=1.2.4.0 && <2.2 48 | , text-short >=0.1.3 && <0.2 49 | default-language: Haskell2010 50 | default-extensions: 51 | OverloadedStrings 52 | ghc-options: -Wall 53 | 54 | test-suite ttc-test 55 | type: exitcode-stdio-1.0 56 | hs-source-dirs: test 57 | main-is: Spec.hs 58 | other-modules: 59 | Data.TTC.Test 60 | , Data.TTC.Wrapper.Test 61 | , TestTypes.Ex 62 | build-depends: 63 | base 64 | , bytestring 65 | , tasty >=1.2.3 && <1.6 66 | , tasty-hunit >=0.10.0.3 && <0.11 67 | , template-haskell 68 | , text 69 | , text-short 70 | , ttc 71 | default-language: Haskell2010 72 | ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N 73 | --------------------------------------------------------------------------------