Initial push
This commit is contained in:
parent
9c54a79def
commit
6c48f2357f
30 changed files with 2924 additions and 0 deletions
233
.github/workflows/haskell-ci.yml
vendored
Normal file
233
.github/workflows/haskell-ci.yml
vendored
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
# This GitHub workflow config has been generated by a script via
|
||||||
|
#
|
||||||
|
# haskell-ci 'github' 'cabal.project'
|
||||||
|
#
|
||||||
|
# To regenerate the script (for example after adjusting tested-with) run
|
||||||
|
#
|
||||||
|
# haskell-ci regenerate
|
||||||
|
#
|
||||||
|
# For more information, see https://github.com/haskell-CI/haskell-ci
|
||||||
|
#
|
||||||
|
# version: 0.15.20230312
|
||||||
|
#
|
||||||
|
# REGENDATA ("0.15.20230312",["github","cabal.project"])
|
||||||
|
#
|
||||||
|
name: Haskell-CI
|
||||||
|
on:
|
||||||
|
- push
|
||||||
|
- pull_request
|
||||||
|
jobs:
|
||||||
|
linux:
|
||||||
|
name: Haskell-CI - Linux - ${{ matrix.compiler }}
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
timeout-minutes:
|
||||||
|
60
|
||||||
|
container:
|
||||||
|
image: buildpack-deps:bionic
|
||||||
|
continue-on-error: ${{ matrix.allow-failure }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- compiler: ghc-9.4.4
|
||||||
|
compilerKind: ghc
|
||||||
|
compilerVersion: 9.4.4
|
||||||
|
setup-method: ghcup
|
||||||
|
allow-failure: false
|
||||||
|
- compiler: ghc-9.2.7
|
||||||
|
compilerKind: ghc
|
||||||
|
compilerVersion: 9.2.7
|
||||||
|
setup-method: ghcup
|
||||||
|
allow-failure: false
|
||||||
|
- compiler: ghc-9.0.2
|
||||||
|
compilerKind: ghc
|
||||||
|
compilerVersion: 9.0.2
|
||||||
|
setup-method: ghcup
|
||||||
|
allow-failure: false
|
||||||
|
- compiler: ghc-8.10.7
|
||||||
|
compilerKind: ghc
|
||||||
|
compilerVersion: 8.10.7
|
||||||
|
setup-method: ghcup
|
||||||
|
allow-failure: false
|
||||||
|
- compiler: ghc-8.8.4
|
||||||
|
compilerKind: ghc
|
||||||
|
compilerVersion: 8.8.4
|
||||||
|
setup-method: hvr-ppa
|
||||||
|
allow-failure: false
|
||||||
|
- compiler: ghc-8.6.5
|
||||||
|
compilerKind: ghc
|
||||||
|
compilerVersion: 8.6.5
|
||||||
|
setup-method: hvr-ppa
|
||||||
|
allow-failure: false
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- name: apt
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5
|
||||||
|
if [ "${{ matrix.setup-method }}" = ghcup ]; then
|
||||||
|
mkdir -p "$HOME/.ghcup/bin"
|
||||||
|
curl -sL https://downloads.haskell.org/ghcup/0.1.18.0/x86_64-linux-ghcup-0.1.18.0 > "$HOME/.ghcup/bin/ghcup"
|
||||||
|
chmod a+x "$HOME/.ghcup/bin/ghcup"
|
||||||
|
"$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false)
|
||||||
|
"$HOME/.ghcup/bin/ghcup" install cabal 3.10.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
|
||||||
|
else
|
||||||
|
apt-add-repository -y 'ppa:hvr/ghc'
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y "$HCNAME"
|
||||||
|
mkdir -p "$HOME/.ghcup/bin"
|
||||||
|
curl -sL https://downloads.haskell.org/ghcup/0.1.18.0/x86_64-linux-ghcup-0.1.18.0 > "$HOME/.ghcup/bin/ghcup"
|
||||||
|
chmod a+x "$HOME/.ghcup/bin/ghcup"
|
||||||
|
"$HOME/.ghcup/bin/ghcup" install cabal 3.10.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false)
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
HCKIND: ${{ matrix.compilerKind }}
|
||||||
|
HCNAME: ${{ matrix.compiler }}
|
||||||
|
HCVER: ${{ matrix.compilerVersion }}
|
||||||
|
- name: Set PATH and environment variables
|
||||||
|
run: |
|
||||||
|
echo "$HOME/.cabal/bin" >> $GITHUB_PATH
|
||||||
|
echo "LANG=C.UTF-8" >> "$GITHUB_ENV"
|
||||||
|
echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV"
|
||||||
|
echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV"
|
||||||
|
HCDIR=/opt/$HCKIND/$HCVER
|
||||||
|
if [ "${{ matrix.setup-method }}" = ghcup ]; then
|
||||||
|
HC=$HOME/.ghcup/bin/$HCKIND-$HCVER
|
||||||
|
echo "HC=$HC" >> "$GITHUB_ENV"
|
||||||
|
echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV"
|
||||||
|
echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV"
|
||||||
|
echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.1.0 -vnormal+nowrap" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
HC=$HCDIR/bin/$HCKIND
|
||||||
|
echo "HC=$HC" >> "$GITHUB_ENV"
|
||||||
|
echo "HCPKG=$HCDIR/bin/$HCKIND-pkg" >> "$GITHUB_ENV"
|
||||||
|
echo "HADDOCK=$HCDIR/bin/haddock" >> "$GITHUB_ENV"
|
||||||
|
echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.1.0 -vnormal+nowrap" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))')
|
||||||
|
echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV"
|
||||||
|
echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV"
|
||||||
|
echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV"
|
||||||
|
echo "HEADHACKAGE=false" >> "$GITHUB_ENV"
|
||||||
|
echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV"
|
||||||
|
echo "GHCJSARITH=0" >> "$GITHUB_ENV"
|
||||||
|
env:
|
||||||
|
HCKIND: ${{ matrix.compilerKind }}
|
||||||
|
HCNAME: ${{ matrix.compiler }}
|
||||||
|
HCVER: ${{ matrix.compilerVersion }}
|
||||||
|
- name: env
|
||||||
|
run: |
|
||||||
|
env
|
||||||
|
- name: write cabal config
|
||||||
|
run: |
|
||||||
|
mkdir -p $CABAL_DIR
|
||||||
|
cat >> $CABAL_CONFIG <<EOF
|
||||||
|
remote-build-reporting: anonymous
|
||||||
|
write-ghc-environment-files: never
|
||||||
|
remote-repo-cache: $CABAL_DIR/packages
|
||||||
|
logs-dir: $CABAL_DIR/logs
|
||||||
|
world-file: $CABAL_DIR/world
|
||||||
|
extra-prog-path: $CABAL_DIR/bin
|
||||||
|
symlink-bindir: $CABAL_DIR/bin
|
||||||
|
installdir: $CABAL_DIR/bin
|
||||||
|
build-summary: $CABAL_DIR/logs/build.log
|
||||||
|
store-dir: $CABAL_DIR/store
|
||||||
|
install-dirs user
|
||||||
|
prefix: $CABAL_DIR
|
||||||
|
repository hackage.haskell.org
|
||||||
|
url: http://hackage.haskell.org/
|
||||||
|
EOF
|
||||||
|
cat >> $CABAL_CONFIG <<EOF
|
||||||
|
program-default-options
|
||||||
|
ghc-options: $GHCJOBS +RTS -M3G -RTS
|
||||||
|
EOF
|
||||||
|
cat $CABAL_CONFIG
|
||||||
|
- name: versions
|
||||||
|
run: |
|
||||||
|
$HC --version || true
|
||||||
|
$HC --print-project-git-commit-id || true
|
||||||
|
$CABAL --version || true
|
||||||
|
- name: update cabal index
|
||||||
|
run: |
|
||||||
|
$CABAL v2-update -v
|
||||||
|
- name: install cabal-plan
|
||||||
|
run: |
|
||||||
|
mkdir -p $HOME/.cabal/bin
|
||||||
|
curl -sL https://github.com/haskell-hvr/cabal-plan/releases/download/v0.6.2.0/cabal-plan-0.6.2.0-x86_64-linux.xz > cabal-plan.xz
|
||||||
|
echo 'de73600b1836d3f55e32d80385acc055fd97f60eaa0ab68a755302685f5d81bc cabal-plan.xz' | sha256sum -c -
|
||||||
|
xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan
|
||||||
|
rm -f cabal-plan.xz
|
||||||
|
chmod a+x $HOME/.cabal/bin/cabal-plan
|
||||||
|
cabal-plan --version
|
||||||
|
- name: checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
path: source
|
||||||
|
- name: initial cabal.project for sdist
|
||||||
|
run: |
|
||||||
|
touch cabal.project
|
||||||
|
echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project
|
||||||
|
cat cabal.project
|
||||||
|
- name: sdist
|
||||||
|
run: |
|
||||||
|
mkdir -p sdist
|
||||||
|
$CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist
|
||||||
|
- name: unpack
|
||||||
|
run: |
|
||||||
|
mkdir -p unpacked
|
||||||
|
find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \;
|
||||||
|
- name: generate cabal.project
|
||||||
|
run: |
|
||||||
|
PKGDIR_foreign_rust="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/foreign-rust-[0-9.]*')"
|
||||||
|
echo "PKGDIR_foreign_rust=${PKGDIR_foreign_rust}" >> "$GITHUB_ENV"
|
||||||
|
rm -f cabal.project cabal.project.local
|
||||||
|
touch cabal.project
|
||||||
|
touch cabal.project.local
|
||||||
|
echo "packages: ${PKGDIR_foreign_rust}" >> cabal.project
|
||||||
|
echo "package foreign-rust" >> cabal.project
|
||||||
|
echo " ghc-options: -Werror=missing-methods" >> cabal.project
|
||||||
|
cat >> cabal.project <<EOF
|
||||||
|
EOF
|
||||||
|
$HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(foreign-rust)$/; }' >> cabal.project.local
|
||||||
|
cat cabal.project
|
||||||
|
cat cabal.project.local
|
||||||
|
- name: dump install plan
|
||||||
|
run: |
|
||||||
|
$CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all
|
||||||
|
cabal-plan
|
||||||
|
- name: restore cache
|
||||||
|
uses: actions/cache/restore@v3
|
||||||
|
with:
|
||||||
|
key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
|
||||||
|
path: ~/.cabal/store
|
||||||
|
restore-keys: ${{ runner.os }}-${{ matrix.compiler }}-
|
||||||
|
- name: install dependencies
|
||||||
|
run: |
|
||||||
|
$CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all
|
||||||
|
$CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all
|
||||||
|
- name: build w/o tests
|
||||||
|
run: |
|
||||||
|
$CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
|
||||||
|
- name: build
|
||||||
|
run: |
|
||||||
|
$CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always
|
||||||
|
- name: tests
|
||||||
|
run: |
|
||||||
|
$CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --test-show-details=direct
|
||||||
|
- name: cabal check
|
||||||
|
run: |
|
||||||
|
cd ${PKGDIR_foreign_rust} || false
|
||||||
|
${CABAL} -vnormal check
|
||||||
|
- name: haddock
|
||||||
|
run: |
|
||||||
|
$CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all
|
||||||
|
- name: unconstrained build
|
||||||
|
run: |
|
||||||
|
rm -f cabal.project.local
|
||||||
|
$CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all
|
||||||
|
- name: save cache
|
||||||
|
uses: actions/cache/save@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }}
|
||||||
|
path: ~/.cabal/store
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.envrc
|
||||||
|
dist-newstyle
|
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Revision history for foreign-rust
|
||||||
|
|
||||||
|
## 0.1.0.0 -- YYYY-mm-dd
|
||||||
|
|
||||||
|
* First version. Released on an unsuspecting world.
|
4
cabal.project
Normal file
4
cabal.project
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
packages: .
|
||||||
|
|
||||||
|
package foreign-rust
|
||||||
|
tests: true
|
151
demo-annotated/Main.hs
Normal file
151
demo-annotated/Main.hs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
import Data.Annotated
|
||||||
|
import Data.Kind
|
||||||
|
import GHC.TypeLits
|
||||||
|
|
||||||
|
import qualified GHC.Generics as GHC
|
||||||
|
import qualified Generics.SOP as SOP
|
||||||
|
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
import qualified Data.Structured.TH as Structured
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Demonstration of annotations
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Suppose we have some data type that is opaque Haskell-side (just some bytes),
|
||||||
|
--
|
||||||
|
|
||||||
|
data Keypair = Keypair
|
||||||
|
deriving stock (Show, GHC.Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
|
||||||
|
deriving anyclass (Structured.Show)
|
||||||
|
deriving CanAnnotate via PairWithAnnotation Keypair
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Perhaps we can inspect this datatype using an FFI
|
||||||
|
--
|
||||||
|
|
||||||
|
data Pubkey = Pubkey
|
||||||
|
deriving stock (Show, GHC.Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
|
||||||
|
deriving anyclass (Structured.Show)
|
||||||
|
|
||||||
|
data Secret = Secret
|
||||||
|
deriving stock (Show, GHC.Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
|
||||||
|
deriving anyclass (Structured.Show)
|
||||||
|
|
||||||
|
keypairPubkey :: Keypair -> Pubkey
|
||||||
|
keypairPubkey Keypair = Pubkey
|
||||||
|
|
||||||
|
keypairSecret :: Keypair -> Secret
|
||||||
|
keypairSecret Keypair = Secret
|
||||||
|
|
||||||
|
--
|
||||||
|
-- When we show a Keypair, we'd like to annotate it with these derived values
|
||||||
|
--
|
||||||
|
|
||||||
|
data KeypairAnnotation = KeypairAnnotation {
|
||||||
|
pubkey :: Pubkey
|
||||||
|
, secret :: Secret
|
||||||
|
}
|
||||||
|
deriving stock (Show, GHC.Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
|
||||||
|
deriving anyclass (Structured.Show)
|
||||||
|
|
||||||
|
type instance Annotation Keypair = KeypairAnnotation
|
||||||
|
|
||||||
|
instance ComputeAnnotation Keypair where
|
||||||
|
computeAnnotation kp = KeypairAnnotation {
|
||||||
|
pubkey = keypairPubkey kp
|
||||||
|
, secret = keypairSecret kp
|
||||||
|
}
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Generics
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
data RecordA = RecordA {
|
||||||
|
recA_field1 :: Bool
|
||||||
|
, recA_field2 :: Int
|
||||||
|
}
|
||||||
|
deriving stock (GHC.Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
|
||||||
|
deriving anyclass (Structured.Show)
|
||||||
|
|
||||||
|
-- A type, perhaps externally defined, with only standard Show instance
|
||||||
|
data SomeOtherType = SomeOtherType String
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
data RecordB = RecordB {
|
||||||
|
recB :: SomeOtherType
|
||||||
|
}
|
||||||
|
deriving stock (GHC.Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
|
||||||
|
|
||||||
|
instance Structured.Show RecordB where
|
||||||
|
toValue = Structured.gtoValueAfter Structured.FromPreludeShow
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Demonstrate TH support (crucically, with support for GADTs)
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
data SimpleEnum = SimpleEnumA | SimpleEnumB
|
||||||
|
|
||||||
|
Structured.deriveInstance 'SimpleEnumA [t|
|
||||||
|
forall. Structured.Show SimpleEnum
|
||||||
|
|]
|
||||||
|
|
||||||
|
data SimpleStruct a = SimpleStruct a Int
|
||||||
|
|
||||||
|
Structured.deriveInstance 'SimpleStruct [t|
|
||||||
|
forall a. Structured.Show a => Structured.Show (SimpleStruct a)
|
||||||
|
|]
|
||||||
|
|
||||||
|
data SomeGADT :: Symbol -> Type where
|
||||||
|
Foo :: SomeGADT "foo"
|
||||||
|
Bar :: SomeGADT "bar"
|
||||||
|
|
||||||
|
Structured.deriveInstance 'Foo [t|
|
||||||
|
forall k. Structured.Show (SomeGADT k)
|
||||||
|
|]
|
||||||
|
|
||||||
|
class Foo a where
|
||||||
|
data SomeAssocType a :: Type
|
||||||
|
|
||||||
|
instance Foo Int where
|
||||||
|
data SomeAssocType Int = SomeInt Int
|
||||||
|
|
||||||
|
Structured.deriveInstance 'SomeInt [t|
|
||||||
|
forall. Structured.Show (SomeAssocType Int)
|
||||||
|
|]
|
||||||
|
|
||||||
|
data SomeRecord = SomeRecord {
|
||||||
|
field1 :: Int
|
||||||
|
, field2 :: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
Structured.deriveInstance 'SomeRecord [t|
|
||||||
|
forall. Structured.Show SomeRecord
|
||||||
|
|]
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Main
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
-- Annotations
|
||||||
|
Structured.print . annotate $ [(Just Keypair, True)]
|
||||||
|
-- Generics
|
||||||
|
Structured.print $ RecordA { recA_field1 = True, recA_field2 = 5 }
|
||||||
|
Structured.print $ RecordB { recB = SomeOtherType "hi" }
|
||||||
|
-- TH
|
||||||
|
Structured.print $ SimpleEnumA
|
||||||
|
Structured.print $ SimpleStruct True 5
|
||||||
|
Structured.print $ Foo
|
||||||
|
Structured.print $ SomeInt 5
|
||||||
|
Structured.print $ SomeRecord { field1 = 1, field2 = True }
|
131
foreign-rust.cabal
Normal file
131
foreign-rust.cabal
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
cabal-version: 3.0
|
||||||
|
name: foreign-rust
|
||||||
|
version: 0.1.0
|
||||||
|
license: BSD-3-Clause
|
||||||
|
license-file: LICENSE
|
||||||
|
author: Edsko de Vries
|
||||||
|
maintainer: edsko@well-typed.com
|
||||||
|
category: Development
|
||||||
|
build-type: Simple
|
||||||
|
extra-doc-files: CHANGELOG.md
|
||||||
|
tested-with: GHC==8.6.5
|
||||||
|
, GHC==8.8.4
|
||||||
|
, GHC==8.10.7
|
||||||
|
, GHC==9.0.2
|
||||||
|
, GHC==9.2.7
|
||||||
|
, GHC==9.4.4
|
||||||
|
|
||||||
|
common lang
|
||||||
|
build-depends:
|
||||||
|
base >= 4.12
|
||||||
|
default-language:
|
||||||
|
Haskell2010
|
||||||
|
ghc-options:
|
||||||
|
-Wall
|
||||||
|
-Wredundant-constraints
|
||||||
|
if impl(ghc >= 8.10)
|
||||||
|
ghc-options:
|
||||||
|
-Wunused-packages
|
||||||
|
default-extensions:
|
||||||
|
DataKinds
|
||||||
|
DefaultSignatures
|
||||||
|
DeriveAnyClass
|
||||||
|
DeriveGeneric
|
||||||
|
DerivingStrategies
|
||||||
|
DerivingVia
|
||||||
|
FlexibleContexts
|
||||||
|
FlexibleInstances
|
||||||
|
GADTs
|
||||||
|
GeneralizedNewtypeDeriving
|
||||||
|
LambdaCase
|
||||||
|
MultiParamTypeClasses
|
||||||
|
PolyKinds
|
||||||
|
RankNTypes
|
||||||
|
ScopedTypeVariables
|
||||||
|
StandaloneDeriving
|
||||||
|
TupleSections
|
||||||
|
TypeApplications
|
||||||
|
TypeFamilies
|
||||||
|
TypeOperators
|
||||||
|
UndecidableInstances
|
||||||
|
|
||||||
|
library
|
||||||
|
import:
|
||||||
|
lang
|
||||||
|
exposed-modules:
|
||||||
|
Foreign.Rust.External.JSON
|
||||||
|
Foreign.Rust.External.Bincode
|
||||||
|
Foreign.Rust.Failure
|
||||||
|
Foreign.Rust.Marshall.Fixed
|
||||||
|
Foreign.Rust.Marshall.Variable
|
||||||
|
Foreign.Rust.SafeConv
|
||||||
|
Foreign.Rust.Serialisation.JSON
|
||||||
|
Foreign.Rust.Serialisation.Raw
|
||||||
|
Foreign.Rust.Serialisation.Raw.Base16
|
||||||
|
Foreign.Rust.Serialisation.Raw.Base58
|
||||||
|
Foreign.Rust.Serialisation.Raw.Base64
|
||||||
|
Foreign.Rust.Serialisation.Raw.Decimal
|
||||||
|
|
||||||
|
Data.Annotated
|
||||||
|
Data.Structured
|
||||||
|
Data.Structured.TH
|
||||||
|
other-modules:
|
||||||
|
Foreign.Rust.Marshall.Util
|
||||||
|
hs-source-dirs:
|
||||||
|
src
|
||||||
|
build-depends:
|
||||||
|
, aeson
|
||||||
|
, base16-bytestring
|
||||||
|
, base58-bytestring
|
||||||
|
, base64-bytestring
|
||||||
|
, binary
|
||||||
|
, borsh >= 0.3
|
||||||
|
, bytestring
|
||||||
|
, containers
|
||||||
|
, data-default
|
||||||
|
, generics-sop
|
||||||
|
, OneTuple
|
||||||
|
, sop-core
|
||||||
|
, template-haskell
|
||||||
|
, text
|
||||||
|
, th-abstraction
|
||||||
|
, vector
|
||||||
|
, wide-word
|
||||||
|
|
||||||
|
test-suite test-foreign-rust
|
||||||
|
import:
|
||||||
|
lang
|
||||||
|
type:
|
||||||
|
exitcode-stdio-1.0
|
||||||
|
main-is:
|
||||||
|
Main.hs
|
||||||
|
other-modules:
|
||||||
|
Test.Serialisation.JSON
|
||||||
|
Test.Serialisation.Raw.Base16
|
||||||
|
Test.Serialisation.Raw.Base58
|
||||||
|
Test.Serialisation.Raw.Base64
|
||||||
|
Test.Serialisation.Raw.Decimal
|
||||||
|
Test.Serialisation.Types
|
||||||
|
Test.Util.TH
|
||||||
|
hs-source-dirs:
|
||||||
|
test
|
||||||
|
build-depends:
|
||||||
|
, aeson
|
||||||
|
, foreign-rust
|
||||||
|
, haskell-src-exts
|
||||||
|
, tasty
|
||||||
|
, tasty-hunit
|
||||||
|
, template-haskell
|
||||||
|
|
||||||
|
test-suite demo-annotated
|
||||||
|
import:
|
||||||
|
lang
|
||||||
|
type:
|
||||||
|
exitcode-stdio-1.0
|
||||||
|
main-is:
|
||||||
|
Main.hs
|
||||||
|
hs-source-dirs:
|
||||||
|
demo-annotated
|
||||||
|
build-depends:
|
||||||
|
, generics-sop
|
||||||
|
, foreign-rust
|
380
src/Data/Annotated.hs
Normal file
380
src/Data/Annotated.hs
Normal file
|
@ -0,0 +1,380 @@
|
||||||
|
module Data.Annotated (
|
||||||
|
-- * Definition
|
||||||
|
CanAnnotate(..)
|
||||||
|
-- * Deriving-via support
|
||||||
|
-- ** Computing annotations
|
||||||
|
, Annotation
|
||||||
|
, PairWithAnnotation(..)
|
||||||
|
, ComputeAnnotation(..)
|
||||||
|
, WithAnnotation(..)
|
||||||
|
-- ** Other combinators
|
||||||
|
, NoAnnotation(..)
|
||||||
|
, AnnotateFoldable(..)
|
||||||
|
, AnnotateGenericallyAs(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Functor.Identity
|
||||||
|
import Data.Int
|
||||||
|
import Data.Kind
|
||||||
|
import Data.Map (Map)
|
||||||
|
import Data.Proxy
|
||||||
|
import Data.WideWord
|
||||||
|
import Data.Word
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
import qualified Data.SOP.Constraint as SOP
|
||||||
|
import qualified Generics.SOP as SOP
|
||||||
|
import qualified GHC.Generics as GHC
|
||||||
|
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
import Data.Tuple.Solo
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Definition
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
class CanAnnotate a where
|
||||||
|
type Annotated a :: Type
|
||||||
|
annotate :: a -> Annotated a
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving via support: computing annotations
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Annotation of a value
|
||||||
|
--
|
||||||
|
-- Unlike 'Annotated', 'Annotation' is not always defined: not all types /have/
|
||||||
|
-- annotations (for example, @Annotated a@ might just be @a@).
|
||||||
|
type family Annotation a :: Type
|
||||||
|
|
||||||
|
-- | Deriving via support: computing annotations
|
||||||
|
--
|
||||||
|
-- If you need to compute an annotation and do not need to worry about
|
||||||
|
-- annotating any nested values, you define a 'CanAnnotate' instance for some
|
||||||
|
-- type @A@ with annotation @B@ as follows:
|
||||||
|
--
|
||||||
|
-- > data A = ..
|
||||||
|
-- > deriving CanAnnotate via PairWithAnnotation A
|
||||||
|
newtype PairWithAnnotation a = PairWithAnnotation a
|
||||||
|
|
||||||
|
class ComputeAnnotation a where
|
||||||
|
computeAnnotation :: a -> Annotation a
|
||||||
|
|
||||||
|
instance ComputeAnnotation a => CanAnnotate (PairWithAnnotation a) where
|
||||||
|
type Annotated (PairWithAnnotation a) = WithAnnotation a (Annotation a)
|
||||||
|
annotate (PairWithAnnotation x) = WithAnnotation {
|
||||||
|
value = x
|
||||||
|
, annotation = computeAnnotation x
|
||||||
|
}
|
||||||
|
|
||||||
|
data WithAnnotation a b = WithAnnotation {
|
||||||
|
value :: a
|
||||||
|
, annotation :: b
|
||||||
|
}
|
||||||
|
deriving stock (Show, GHC.Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
|
||||||
|
deriving anyclass (Structured.Show)
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving via: default instance for foldable containers
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Deriving via: default instance for foldable containers
|
||||||
|
--
|
||||||
|
-- We annotate the values in the containers, and give the container length as
|
||||||
|
-- its own annotation.
|
||||||
|
--
|
||||||
|
-- Example:
|
||||||
|
--
|
||||||
|
-- > deriving
|
||||||
|
-- > via AnnotateFoldable [] a
|
||||||
|
-- > instance CanAnnotate a => CanAnnotate [a]
|
||||||
|
newtype AnnotateFoldable f a = AnnotateFoldable (f a)
|
||||||
|
|
||||||
|
newtype Length = Length Int
|
||||||
|
deriving stock (Show, GHC.Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
|
||||||
|
deriving anyclass (Structured.Show)
|
||||||
|
|
||||||
|
type instance Annotation (AnnotateFoldable f a) = Length
|
||||||
|
|
||||||
|
instance ( Functor f
|
||||||
|
, Foldable f
|
||||||
|
, CanAnnotate a
|
||||||
|
) => CanAnnotate (AnnotateFoldable f a) where
|
||||||
|
type Annotated (AnnotateFoldable f a) = WithAnnotation (f (Annotated a)) Length
|
||||||
|
annotate (AnnotateFoldable xs) = WithAnnotation {
|
||||||
|
value = annotate <$> xs
|
||||||
|
, annotation = Length $ length xs
|
||||||
|
}
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving-via: no annotation
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Deriving via: no annotation
|
||||||
|
--
|
||||||
|
-- Example:
|
||||||
|
--
|
||||||
|
-- > data A = ..
|
||||||
|
-- > deriving CanAnnotate via NoAnnotation A
|
||||||
|
newtype NoAnnotation a = NoAnnotation a
|
||||||
|
|
||||||
|
type instance Annotation (NoAnnotation a) = ()
|
||||||
|
|
||||||
|
instance CanAnnotate (NoAnnotation a) where
|
||||||
|
type Annotated (NoAnnotation a) = a
|
||||||
|
annotate (NoAnnotation x) = x
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving via: generics
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Deriving via: annotate generically
|
||||||
|
--
|
||||||
|
-- This combinator can be used to define 'CanAnnotate' instance that just
|
||||||
|
-- walk over the structure of the argument, without adding any annotations
|
||||||
|
-- of their own.
|
||||||
|
--
|
||||||
|
-- Example:
|
||||||
|
--
|
||||||
|
-- > deriving
|
||||||
|
-- > via AnnotateGenericallyAs (Maybe (Annotated a)) (Maybe a)
|
||||||
|
-- > instance CanAnnotate a => CanAnnotate (Maybe a)
|
||||||
|
newtype AnnotateGenericallyAs b a = AnnotateGenericallyAs a
|
||||||
|
|
||||||
|
-- | Internal auxiliary: two-parameter wrapper around 'CanAnnotate'
|
||||||
|
class Annotate' a b where
|
||||||
|
annotate' :: a -> b
|
||||||
|
|
||||||
|
instance (CanAnnotate a, b ~ Annotated a) => Annotate' a b where
|
||||||
|
annotate' = annotate
|
||||||
|
|
||||||
|
type instance Annotation (AnnotateGenericallyAs b a) = ()
|
||||||
|
|
||||||
|
instance ( SOP.Generic a
|
||||||
|
, SOP.Generic b
|
||||||
|
, SOP.SameShapeAs (SOP.Code a) (SOP.Code b)
|
||||||
|
, SOP.SameShapeAs (SOP.Code b) (SOP.Code a)
|
||||||
|
, SOP.AllZip2 Annotate' (SOP.Code a) (SOP.Code b)
|
||||||
|
) => CanAnnotate (AnnotateGenericallyAs b a) where
|
||||||
|
type Annotated (AnnotateGenericallyAs b a) = b
|
||||||
|
annotate (AnnotateGenericallyAs x) =
|
||||||
|
SOP.to
|
||||||
|
. SOP.htrans (Proxy @Annotate') (SOP.mapII annotate')
|
||||||
|
. SOP.from
|
||||||
|
$ x
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Standard instances: no annotation
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
deriving via NoAnnotation Bool instance CanAnnotate Bool
|
||||||
|
deriving via NoAnnotation Aeson.Value instance CanAnnotate Aeson.Value
|
||||||
|
|
||||||
|
deriving via NoAnnotation Integer instance CanAnnotate Integer
|
||||||
|
|
||||||
|
deriving via NoAnnotation Int instance CanAnnotate Int
|
||||||
|
deriving via NoAnnotation Int8 instance CanAnnotate Int8
|
||||||
|
deriving via NoAnnotation Int16 instance CanAnnotate Int16
|
||||||
|
deriving via NoAnnotation Int32 instance CanAnnotate Int32
|
||||||
|
deriving via NoAnnotation Int64 instance CanAnnotate Int64
|
||||||
|
deriving via NoAnnotation Int128 instance CanAnnotate Int128
|
||||||
|
|
||||||
|
deriving via NoAnnotation Word instance CanAnnotate Word
|
||||||
|
deriving via NoAnnotation Word8 instance CanAnnotate Word8
|
||||||
|
deriving via NoAnnotation Word16 instance CanAnnotate Word16
|
||||||
|
deriving via NoAnnotation Word32 instance CanAnnotate Word32
|
||||||
|
deriving via NoAnnotation Word64 instance CanAnnotate Word64
|
||||||
|
deriving via NoAnnotation Word128 instance CanAnnotate Word128
|
||||||
|
|
||||||
|
deriving via NoAnnotation Float instance CanAnnotate Float
|
||||||
|
deriving via NoAnnotation Double instance CanAnnotate Double
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Standard instances: foldable
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
deriving
|
||||||
|
via AnnotateFoldable [] a
|
||||||
|
instance CanAnnotate a => CanAnnotate [a]
|
||||||
|
|
||||||
|
deriving
|
||||||
|
via AnnotateFoldable (Map k) a
|
||||||
|
instance CanAnnotate a => CanAnnotate (Map k a)
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Standard instances: generic
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs (Maybe (Annotated a)) (Maybe a)
|
||||||
|
instance CanAnnotate a => CanAnnotate (Maybe a)
|
||||||
|
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs (Either (Annotated a) (Annotated b)) (Either a b)
|
||||||
|
instance (CanAnnotate a, CanAnnotate b) => CanAnnotate (Either a b)
|
||||||
|
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs (Identity (Annotated a)) (Identity a)
|
||||||
|
instance CanAnnotate a => CanAnnotate (Identity a)
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Standard instances: tuples
|
||||||
|
|
||||||
|
These instances also use 'AnnotateGenericallyAs'.
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- 0
|
||||||
|
deriving
|
||||||
|
via NoAnnotation ()
|
||||||
|
instance CanAnnotate ()
|
||||||
|
|
||||||
|
-- 1 ('Solo' does not support SOP generics)
|
||||||
|
instance CanAnnotate a => CanAnnotate (Solo a) where
|
||||||
|
type Annotated (Solo a) = Solo (Annotated a)
|
||||||
|
annotate (Solo x) = Solo (annotate x)
|
||||||
|
|
||||||
|
-- 2
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs
|
||||||
|
( Annotated a
|
||||||
|
, Annotated b
|
||||||
|
)
|
||||||
|
(a, b)
|
||||||
|
instance ( CanAnnotate a
|
||||||
|
, CanAnnotate b
|
||||||
|
) => CanAnnotate (a, b)
|
||||||
|
|
||||||
|
-- 3
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs
|
||||||
|
( Annotated a
|
||||||
|
, Annotated b
|
||||||
|
, Annotated c
|
||||||
|
)
|
||||||
|
(a, b, c)
|
||||||
|
instance ( CanAnnotate a
|
||||||
|
, CanAnnotate b
|
||||||
|
, CanAnnotate c
|
||||||
|
) => CanAnnotate (a, b, c)
|
||||||
|
|
||||||
|
-- 4
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs
|
||||||
|
( Annotated a
|
||||||
|
, Annotated b
|
||||||
|
, Annotated c
|
||||||
|
, Annotated d
|
||||||
|
)
|
||||||
|
(a, b, c, d)
|
||||||
|
instance ( CanAnnotate a
|
||||||
|
, CanAnnotate b
|
||||||
|
, CanAnnotate c
|
||||||
|
, CanAnnotate d
|
||||||
|
) => CanAnnotate (a, b, c, d)
|
||||||
|
|
||||||
|
-- 5
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs
|
||||||
|
( Annotated a
|
||||||
|
, Annotated b
|
||||||
|
, Annotated c
|
||||||
|
, Annotated d
|
||||||
|
, Annotated e
|
||||||
|
)
|
||||||
|
(a, b, c, d, e)
|
||||||
|
instance ( CanAnnotate a
|
||||||
|
, CanAnnotate b
|
||||||
|
, CanAnnotate c
|
||||||
|
, CanAnnotate d
|
||||||
|
, CanAnnotate e
|
||||||
|
) => CanAnnotate (a, b, c, d, e)
|
||||||
|
|
||||||
|
-- 6
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs
|
||||||
|
( Annotated a
|
||||||
|
, Annotated b
|
||||||
|
, Annotated c
|
||||||
|
, Annotated d
|
||||||
|
, Annotated e
|
||||||
|
, Annotated f
|
||||||
|
)
|
||||||
|
(a, b, c, d, e, f)
|
||||||
|
instance ( CanAnnotate a
|
||||||
|
, CanAnnotate b
|
||||||
|
, CanAnnotate c
|
||||||
|
, CanAnnotate d
|
||||||
|
, CanAnnotate e
|
||||||
|
, CanAnnotate f
|
||||||
|
) => CanAnnotate (a, b, c, d, e, f)
|
||||||
|
|
||||||
|
-- 7
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs
|
||||||
|
( Annotated a
|
||||||
|
, Annotated b
|
||||||
|
, Annotated c
|
||||||
|
, Annotated d
|
||||||
|
, Annotated e
|
||||||
|
, Annotated f
|
||||||
|
, Annotated g
|
||||||
|
)
|
||||||
|
(a, b, c, d, e, f, g)
|
||||||
|
instance ( CanAnnotate a
|
||||||
|
, CanAnnotate b
|
||||||
|
, CanAnnotate c
|
||||||
|
, CanAnnotate d
|
||||||
|
, CanAnnotate e
|
||||||
|
, CanAnnotate f
|
||||||
|
, CanAnnotate g
|
||||||
|
) => CanAnnotate (a, b, c, d, e, f, g)
|
||||||
|
|
||||||
|
-- 8
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs
|
||||||
|
( Annotated a
|
||||||
|
, Annotated b
|
||||||
|
, Annotated c
|
||||||
|
, Annotated d
|
||||||
|
, Annotated e
|
||||||
|
, Annotated f
|
||||||
|
, Annotated g
|
||||||
|
, Annotated h
|
||||||
|
)
|
||||||
|
(a, b, c, d, e, f, g, h)
|
||||||
|
instance ( CanAnnotate a
|
||||||
|
, CanAnnotate b
|
||||||
|
, CanAnnotate c
|
||||||
|
, CanAnnotate d
|
||||||
|
, CanAnnotate e
|
||||||
|
, CanAnnotate f
|
||||||
|
, CanAnnotate g
|
||||||
|
, CanAnnotate h
|
||||||
|
) => CanAnnotate (a, b, c, d, e, f, g, h)
|
||||||
|
|
||||||
|
-- 9
|
||||||
|
deriving
|
||||||
|
via AnnotateGenericallyAs
|
||||||
|
( Annotated a
|
||||||
|
, Annotated b
|
||||||
|
, Annotated c
|
||||||
|
, Annotated d
|
||||||
|
, Annotated e
|
||||||
|
, Annotated f
|
||||||
|
, Annotated g
|
||||||
|
, Annotated h
|
||||||
|
, Annotated i
|
||||||
|
)
|
||||||
|
(a, b, c, d, e, f, g, h, i)
|
||||||
|
instance ( CanAnnotate a
|
||||||
|
, CanAnnotate b
|
||||||
|
, CanAnnotate c
|
||||||
|
, CanAnnotate d
|
||||||
|
, CanAnnotate e
|
||||||
|
, CanAnnotate f
|
||||||
|
, CanAnnotate g
|
||||||
|
, CanAnnotate h
|
||||||
|
, CanAnnotate i
|
||||||
|
) => CanAnnotate (a, b, c, d, e, f, g, h, i)
|
663
src/Data/Structured.hs
Normal file
663
src/Data/Structured.hs
Normal file
|
@ -0,0 +1,663 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
|
-- | Pretty-print value in a way that is valid Haskell
|
||||||
|
--
|
||||||
|
-- Intended for qualified import
|
||||||
|
--
|
||||||
|
-- > import qualified Data.Structured as Structured
|
||||||
|
module Data.Structured (
|
||||||
|
Show(..)
|
||||||
|
, show
|
||||||
|
, showsPrec
|
||||||
|
, print
|
||||||
|
-- * Structured values
|
||||||
|
, Value(..)
|
||||||
|
-- * Generics
|
||||||
|
, gtoValue
|
||||||
|
, gtoValueAfter
|
||||||
|
, sopToValue
|
||||||
|
, sopToValueAfter
|
||||||
|
-- * Deriving-via support
|
||||||
|
, ToPreludeShow(..)
|
||||||
|
, FromPreludeShow(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Prelude hiding (Show(..), print)
|
||||||
|
import qualified Prelude
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Data.Bifunctor
|
||||||
|
import Data.ByteString.Short (ShortByteString)
|
||||||
|
import Data.Default
|
||||||
|
import Data.Functor.Identity
|
||||||
|
import Data.Int
|
||||||
|
import Data.List (intersperse)
|
||||||
|
import Data.List.NonEmpty (NonEmpty(..))
|
||||||
|
import Data.SOP
|
||||||
|
import Data.SOP.Dict
|
||||||
|
import Data.String
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Text.Lazy.Builder (Builder)
|
||||||
|
import Data.Tuple.Solo
|
||||||
|
import Data.Typeable
|
||||||
|
import Data.WideWord
|
||||||
|
import Data.Word
|
||||||
|
import GHC.Show (appPrec)
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
import qualified Data.Aeson.KeyMap as Aeson.KeyMap
|
||||||
|
import qualified Data.Aeson.Text as Aeson
|
||||||
|
import qualified Data.ByteString as Strict
|
||||||
|
import qualified Data.ByteString.Lazy as Lazy
|
||||||
|
import qualified Data.List.NonEmpty as NE
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
import qualified Data.Text.Lazy as Lazy
|
||||||
|
import qualified Data.Text.Lazy as Text.Lazy
|
||||||
|
import qualified Data.Text.Lazy.Builder as B
|
||||||
|
import qualified Data.Vector as Vector
|
||||||
|
import qualified Generics.SOP as SOP
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Main API
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Pretty-print value in a way that is valid Haskell
|
||||||
|
--
|
||||||
|
-- This is similar to what @pretty-show@ offers, but @pretty-show@ does not
|
||||||
|
-- guarantee valid Haskell (for example, strings are shown without quotes).
|
||||||
|
class Show a where
|
||||||
|
-- | Generate structured value
|
||||||
|
--
|
||||||
|
-- Typically instances are derived using generics:
|
||||||
|
--
|
||||||
|
-- > data MyType = ..
|
||||||
|
-- > deriving stock (GHC.Generic)
|
||||||
|
-- > deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo)
|
||||||
|
-- > deriving anyclass (Structured.Show)
|
||||||
|
--
|
||||||
|
-- If you want to tweak the generic instance, see 'gtoValueAfter'.
|
||||||
|
toValue :: a -> Value
|
||||||
|
|
||||||
|
default toValue ::
|
||||||
|
(SOP.HasDatatypeInfo a, All2 Show (SOP.Code a))
|
||||||
|
=> a -> Value
|
||||||
|
toValue = gtoValue
|
||||||
|
|
||||||
|
show :: Show a => a -> String
|
||||||
|
show = render False . toValue
|
||||||
|
|
||||||
|
showsPrec :: Show a => Int -> a -> ShowS
|
||||||
|
showsPrec p = showString . render (p > appPrec) . toValue
|
||||||
|
|
||||||
|
print :: Show a => a -> IO ()
|
||||||
|
print = putStrLn . show
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Value
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Structured value
|
||||||
|
data Value where
|
||||||
|
-- | Constructor (or smart constructor) application
|
||||||
|
--
|
||||||
|
-- We allow for some type applications, too.
|
||||||
|
Constr :: Text -> [TypeRep] -> [Value] -> Value
|
||||||
|
|
||||||
|
-- | Record
|
||||||
|
Record :: Text -> [(String, Value)] -> Value
|
||||||
|
|
||||||
|
-- | List
|
||||||
|
List :: [Value] -> Value
|
||||||
|
|
||||||
|
-- | Tuple
|
||||||
|
Tuple :: [Value] -> Value
|
||||||
|
|
||||||
|
-- | Anything String-like
|
||||||
|
--
|
||||||
|
-- Precondition: the 'Show' and 'IsString' instances must be compatible.
|
||||||
|
String :: forall a. (Prelude.Show a, IsString a) => a -> Value
|
||||||
|
|
||||||
|
-- | Integral numbers
|
||||||
|
--
|
||||||
|
-- These are shown assuming @NumericUnderscores@.
|
||||||
|
Integral :: forall a. (Prelude.Show a, Integral a) => a -> Value
|
||||||
|
|
||||||
|
-- | Quasi-quote
|
||||||
|
--
|
||||||
|
-- We separate out the quasi-quoter from the quoted string proper.
|
||||||
|
-- The lines of the quasi-quoted string are listed separately.
|
||||||
|
QuasiQ :: Text -> NonEmpty Builder -> Value
|
||||||
|
|
||||||
|
-- | JSON value
|
||||||
|
--
|
||||||
|
-- We define this as an explicit constructor, in case we ever want to
|
||||||
|
-- generate structured JSON logs from these values.
|
||||||
|
--
|
||||||
|
-- The pretty-printer uses the @aesonQQ@ quasi-quoter.
|
||||||
|
JSON :: Aeson.Value -> Value
|
||||||
|
|
||||||
|
-- | Value shown using the Prelude's show
|
||||||
|
PreludeShow :: Prelude.Show a => a -> Value
|
||||||
|
|
||||||
|
deriving instance Prelude.Show Value
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving-via support
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Derive 'Prelude.Show' through 'Show'
|
||||||
|
--
|
||||||
|
-- You might not want to do always do this; in some circumstances it may be
|
||||||
|
-- useful to have a non-pretty-printed 'Show' instance alongside 'Show'.
|
||||||
|
newtype ToPreludeShow a = ToPreludeShow a
|
||||||
|
|
||||||
|
-- | Derive 'Show' through 'Prelude.Show'
|
||||||
|
--
|
||||||
|
-- NOTE: This should be used sparingly. When 'Show x' is derived using
|
||||||
|
-- 'Prelude.Show x', the result should still be a law-abiding instance (generate
|
||||||
|
-- valid Haskell code), assuming that the 'Prelude.Show' instance is
|
||||||
|
-- law-abiding; however, it will limit the ability to generate structured
|
||||||
|
-- values in different formats, such as JSON.
|
||||||
|
newtype FromPreludeShow a = FromPreludeShow a
|
||||||
|
|
||||||
|
instance Show a => Prelude.Show (ToPreludeShow a) where
|
||||||
|
showsPrec p (ToPreludeShow x) = showsPrec p x
|
||||||
|
|
||||||
|
instance Prelude.Show a => Show (FromPreludeShow a) where
|
||||||
|
toValue (FromPreludeShow x) = PreludeShow x
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Generics
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Newtype which is transparent for the purposes of 'Show'
|
||||||
|
--
|
||||||
|
-- This is only used internally in 'sopToValue'.
|
||||||
|
newtype Transparent a = Transparent a
|
||||||
|
deriving newtype Show
|
||||||
|
|
||||||
|
gtoValue :: forall a.
|
||||||
|
(SOP.HasDatatypeInfo a, All2 Show (SOP.Code a))
|
||||||
|
=> a -> Value
|
||||||
|
gtoValue x = sopToValue (SOP.from x) (SOP.datatypeInfo (Proxy @a))
|
||||||
|
|
||||||
|
-- | Generic derivation of 'toValue'
|
||||||
|
--
|
||||||
|
-- The standard generics instance will depend on 'Show' for all nested values.
|
||||||
|
-- This is usually the right choice, but occassionally you will want to show
|
||||||
|
-- nested values in a different manner; in this case, you can use
|
||||||
|
-- 'gtoValueAfter'. Example:
|
||||||
|
--
|
||||||
|
-- > instance Structured.Show RecordB where
|
||||||
|
-- > toValue = Structured.gtoValueAfter Structured.FromPreludeShow
|
||||||
|
--
|
||||||
|
-- (However, 'FromPreludeShow' should be used sparingly; see discussion there.)
|
||||||
|
gtoValueAfter :: forall f a.
|
||||||
|
(SOP.HasDatatypeInfo a, All2 (Compose Show f) (SOP.Code a))
|
||||||
|
=> (forall x. x -> f x)
|
||||||
|
-> a -> Value
|
||||||
|
gtoValueAfter f x = sopToValueAfter f (SOP.from x) (SOP.datatypeInfo (Proxy @a))
|
||||||
|
|
||||||
|
sopToValue :: forall xss.
|
||||||
|
All2 Show xss
|
||||||
|
=> SOP I xss -> SOP.DatatypeInfo xss -> Value
|
||||||
|
sopToValue =
|
||||||
|
case aux of Dict -> sopToValueAfter Transparent
|
||||||
|
where
|
||||||
|
aux :: Dict (All2 (Compose Show Transparent)) xss
|
||||||
|
aux = all_POP $ hcpure (Proxy @Show) Dict
|
||||||
|
|
||||||
|
sopToValueAfter :: forall f xss.
|
||||||
|
All2 (Compose Show f) xss
|
||||||
|
=> (forall x. x -> f x)
|
||||||
|
-> SOP I xss -> SOP.DatatypeInfo xss -> Value
|
||||||
|
sopToValueAfter f (SOP xss) info = hcollapse $
|
||||||
|
hczipWith (Proxy @(All (Compose Show f))) aux (SOP.constructorInfo info) xss
|
||||||
|
where
|
||||||
|
aux ::
|
||||||
|
All (Compose Show f) xs
|
||||||
|
=> SOP.ConstructorInfo xs -> NP I xs -> K Value xs
|
||||||
|
aux (SOP.Constructor name) xs = K $ auxSimple name xs
|
||||||
|
aux (SOP.Record name fields) xs = K $ auxRecord name fields xs
|
||||||
|
aux (SOP.Infix _ _ _) _ = error "sopToValue: TODO: infix"
|
||||||
|
|
||||||
|
auxSimple ::
|
||||||
|
All (Compose Show f) xs
|
||||||
|
=> String -> NP I xs -> Value
|
||||||
|
auxSimple constr xs = Constr (Text.pack constr) [] $ hcollapse $
|
||||||
|
hcmap (Proxy @(Compose Show f)) (mapIK (toValue . f)) xs
|
||||||
|
|
||||||
|
auxRecord ::
|
||||||
|
All (Compose Show f) xs
|
||||||
|
=> SOP.ConstructorName -> NP SOP.FieldInfo xs -> NP I xs -> Value
|
||||||
|
auxRecord constr fields xs = Record (Text.pack constr) $ hcollapse $
|
||||||
|
hczipWith (Proxy @(Compose Show f)) auxRecordField fields xs
|
||||||
|
|
||||||
|
auxRecordField ::
|
||||||
|
Show (f x)
|
||||||
|
=> SOP.FieldInfo x -> I x -> K (String, Value) x
|
||||||
|
auxRecordField field (I x) = K (SOP.fieldName field, toValue (f x))
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Standard instances
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance Show Word where toValue = Integral
|
||||||
|
instance Show Word8 where toValue = Integral
|
||||||
|
instance Show Word16 where toValue = Integral
|
||||||
|
instance Show Word32 where toValue = Integral
|
||||||
|
instance Show Word64 where toValue = Integral
|
||||||
|
instance Show Word128 where toValue = Integral
|
||||||
|
|
||||||
|
instance Show Int where toValue = Integral
|
||||||
|
instance Show Int8 where toValue = Integral
|
||||||
|
instance Show Int16 where toValue = Integral
|
||||||
|
instance Show Int32 where toValue = Integral
|
||||||
|
instance Show Int64 where toValue = Integral
|
||||||
|
instance Show Int128 where toValue = Integral
|
||||||
|
|
||||||
|
instance Show Integer where toValue = Integral
|
||||||
|
|
||||||
|
instance {-# OVERLAPPABLE #-} Show a => Show [a] where
|
||||||
|
toValue = List . map toValue
|
||||||
|
|
||||||
|
instance Show Aeson.Value where
|
||||||
|
toValue = JSON
|
||||||
|
|
||||||
|
instance Typeable a => Show (Proxy (a :: k)) where
|
||||||
|
toValue p = Constr "Proxy" [typeRep p] []
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
String-like types
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance {-# OVERLAPPING #-} Show String where
|
||||||
|
toValue = String
|
||||||
|
|
||||||
|
instance Show Strict.ByteString where toValue = String
|
||||||
|
instance Show Lazy.ByteString where toValue = String
|
||||||
|
instance Show ShortByteString where toValue = String
|
||||||
|
instance Show Text where toValue = String
|
||||||
|
instance Show Lazy.Text where toValue = String
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Tuples
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- 0
|
||||||
|
instance Show ()
|
||||||
|
|
||||||
|
-- 1 (Solo does not support SOP generics)
|
||||||
|
instance Show a => Show (Solo a) where
|
||||||
|
toValue (Solo x) = Constr "Solo" [] [toValue x]
|
||||||
|
|
||||||
|
-- 2
|
||||||
|
instance ( Show a
|
||||||
|
, Show b
|
||||||
|
) => Show (a, b) where
|
||||||
|
toValue (a, b) = Tuple [
|
||||||
|
toValue a
|
||||||
|
, toValue b
|
||||||
|
]
|
||||||
|
|
||||||
|
-- 3
|
||||||
|
instance ( Show a
|
||||||
|
, Show b
|
||||||
|
, Show c
|
||||||
|
) => Show (a, b, c) where
|
||||||
|
toValue (a, b, c) = Tuple [
|
||||||
|
toValue a
|
||||||
|
, toValue b
|
||||||
|
, toValue c
|
||||||
|
]
|
||||||
|
|
||||||
|
-- 4
|
||||||
|
instance ( Show a
|
||||||
|
, Show b
|
||||||
|
, Show c
|
||||||
|
, Show d
|
||||||
|
) => Show (a, b, c, d) where
|
||||||
|
toValue (a, b, c, d) = Tuple [
|
||||||
|
toValue a
|
||||||
|
, toValue b
|
||||||
|
, toValue c
|
||||||
|
, toValue d
|
||||||
|
]
|
||||||
|
|
||||||
|
-- 5
|
||||||
|
instance ( Show a
|
||||||
|
, Show b
|
||||||
|
, Show c
|
||||||
|
, Show d
|
||||||
|
, Show e
|
||||||
|
) => Show (a, b, c, d, e) where
|
||||||
|
toValue (a, b, c, d, e) = Tuple [
|
||||||
|
toValue a
|
||||||
|
, toValue b
|
||||||
|
, toValue c
|
||||||
|
, toValue d
|
||||||
|
, toValue e
|
||||||
|
]
|
||||||
|
|
||||||
|
-- 6
|
||||||
|
instance ( Show a
|
||||||
|
, Show b
|
||||||
|
, Show c
|
||||||
|
, Show d
|
||||||
|
, Show e
|
||||||
|
, Show f
|
||||||
|
) => Show (a, b, c, d, e, f) where
|
||||||
|
toValue (a, b, c, d, e, f) = Tuple [
|
||||||
|
toValue a
|
||||||
|
, toValue b
|
||||||
|
, toValue c
|
||||||
|
, toValue d
|
||||||
|
, toValue e
|
||||||
|
, toValue f
|
||||||
|
]
|
||||||
|
|
||||||
|
-- 7
|
||||||
|
instance ( Show a
|
||||||
|
, Show b
|
||||||
|
, Show c
|
||||||
|
, Show d
|
||||||
|
, Show e
|
||||||
|
, Show f
|
||||||
|
, Show g
|
||||||
|
) => Show (a, b, c, d, e, f, g) where
|
||||||
|
toValue (a, b, c, d, e, f, g) = Tuple [
|
||||||
|
toValue a
|
||||||
|
, toValue b
|
||||||
|
, toValue c
|
||||||
|
, toValue d
|
||||||
|
, toValue e
|
||||||
|
, toValue f
|
||||||
|
, toValue g
|
||||||
|
]
|
||||||
|
|
||||||
|
-- 8
|
||||||
|
instance ( Show a
|
||||||
|
, Show b
|
||||||
|
, Show c
|
||||||
|
, Show d
|
||||||
|
, Show e
|
||||||
|
, Show f
|
||||||
|
, Show g
|
||||||
|
, Show h
|
||||||
|
) => Show (a, b, c, d, e, f, g, h) where
|
||||||
|
toValue (a, b, c, d, e, f, g, h) = Tuple [
|
||||||
|
toValue a
|
||||||
|
, toValue b
|
||||||
|
, toValue c
|
||||||
|
, toValue d
|
||||||
|
, toValue e
|
||||||
|
, toValue f
|
||||||
|
, toValue g
|
||||||
|
, toValue h
|
||||||
|
]
|
||||||
|
|
||||||
|
-- 9
|
||||||
|
instance ( Show a
|
||||||
|
, Show b
|
||||||
|
, Show c
|
||||||
|
, Show d
|
||||||
|
, Show e
|
||||||
|
, Show f
|
||||||
|
, Show g
|
||||||
|
, Show h
|
||||||
|
, Show i
|
||||||
|
) => Show (a, b, c, d, e, f, g, h, i) where
|
||||||
|
toValue (a, b, c, d, e, f, g, h, i) = Tuple [
|
||||||
|
toValue a
|
||||||
|
, toValue b
|
||||||
|
, toValue c
|
||||||
|
, toValue d
|
||||||
|
, toValue e
|
||||||
|
, toValue f
|
||||||
|
, toValue g
|
||||||
|
, toValue h
|
||||||
|
, toValue i
|
||||||
|
]
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Instances that rely on generics
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance Show Bool
|
||||||
|
|
||||||
|
instance Show a => Show (Maybe a)
|
||||||
|
|
||||||
|
instance (Show a, Show b) => Show (Either a b)
|
||||||
|
|
||||||
|
instance Show a => Show (Identity a)
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Rendering proper
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
render ::
|
||||||
|
Bool -- ^ Are we in a context that may require brackets?
|
||||||
|
-> Value -> String
|
||||||
|
render = \contextNeedsBrackets ->
|
||||||
|
Text.Lazy.unpack
|
||||||
|
. B.toLazyText
|
||||||
|
. intercalate "\n"
|
||||||
|
. NE.toList
|
||||||
|
. go contextNeedsBrackets
|
||||||
|
where
|
||||||
|
go :: Bool -> Value -> NonEmpty Builder
|
||||||
|
go contextneedsBrackets val =
|
||||||
|
bracketIf (contextneedsBrackets && requiresBrackets val) $
|
||||||
|
case val of
|
||||||
|
Integral x -> simple $ addNumericUnderscores (Prelude.show x)
|
||||||
|
String x -> simple $ Prelude.show x
|
||||||
|
Constr c ts xs -> renderComposite (compositeConstr c ts) $
|
||||||
|
map (go True) xs
|
||||||
|
List xs -> renderComposite compositeList $
|
||||||
|
map (go False) xs
|
||||||
|
Tuple xs -> renderComposite compositeTuple $
|
||||||
|
map (go False) xs
|
||||||
|
Record r xs -> renderComposite (compositeHaskellRecord r) $
|
||||||
|
map (uncurry goField . second (go False)) xs
|
||||||
|
QuasiQ qq str -> renderComposite (compositeQuasiQ qq) [str]
|
||||||
|
JSON json -> go contextneedsBrackets $ QuasiQ "aesonQQ" $
|
||||||
|
renderJSON json
|
||||||
|
PreludeShow x -> NE.fromList . map B.fromString $
|
||||||
|
lines (Prelude.showsPrec appPrec x "")
|
||||||
|
|
||||||
|
simple :: String -> NonEmpty Builder
|
||||||
|
simple = pure . B.fromString
|
||||||
|
|
||||||
|
goField :: String -> NonEmpty Builder -> NonEmpty Builder
|
||||||
|
goField field (firstLine :| rest) =
|
||||||
|
(B.fromString field <> " = " <> firstLine)
|
||||||
|
:| indent rest
|
||||||
|
|
||||||
|
bracketIf :: Bool -> NonEmpty Builder -> NonEmpty Builder
|
||||||
|
bracketIf False = id
|
||||||
|
bracketIf True = \case
|
||||||
|
oneLine :| [] -> ("(" <> oneLine <> ")") :| []
|
||||||
|
firstLine :| rest -> ("( " <> firstLine) :| concat [
|
||||||
|
indent rest
|
||||||
|
, [")"]
|
||||||
|
]
|
||||||
|
|
||||||
|
renderJSON :: Aeson.Value -> NonEmpty Builder
|
||||||
|
renderJSON = go
|
||||||
|
where
|
||||||
|
go :: Aeson.Value -> NonEmpty Builder
|
||||||
|
go (Aeson.Object xs) = renderComposite compositeJsonRecord $
|
||||||
|
map (uncurry goField . second go) $
|
||||||
|
Aeson.KeyMap.toList xs
|
||||||
|
go (Aeson.Array xs) = renderComposite compositeList $
|
||||||
|
map go $
|
||||||
|
Vector.toList xs
|
||||||
|
go val = Aeson.encodeToTextBuilder val :| []
|
||||||
|
|
||||||
|
goField :: Aeson.KeyMap.Key -> NonEmpty Builder -> NonEmpty Builder
|
||||||
|
goField key (firstLine :| rest) =
|
||||||
|
B.fromString (Prelude.show key) <> ": " <> firstLine
|
||||||
|
:| map (" " <>) rest
|
||||||
|
|
||||||
|
-- | Does this value require brackets when shown?
|
||||||
|
--
|
||||||
|
-- Of course, these brackets will only be necessary if the context demands them.
|
||||||
|
requiresBrackets :: Value -> Bool
|
||||||
|
requiresBrackets = \case
|
||||||
|
Constr _ ts xs -> not (null ts) || not (null xs)
|
||||||
|
_otherwise -> False
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Internal: rendering composite values
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
data Composite = Composite {
|
||||||
|
-- | Header (e.g. record name, type applications, ..)
|
||||||
|
compositeHeader :: Maybe Text
|
||||||
|
|
||||||
|
-- | Prefix (e.g. @{@ or @(@)
|
||||||
|
, compositePrefix :: Maybe Text
|
||||||
|
|
||||||
|
-- | Suffix (e.g, @}@ or @)@)
|
||||||
|
, compositeSuffix :: Maybe Text
|
||||||
|
|
||||||
|
-- | Element separator (e.g. @,@)
|
||||||
|
, compositeSeparator :: Char
|
||||||
|
|
||||||
|
-- | Should elements be shown on one line?
|
||||||
|
--
|
||||||
|
-- By default, this is true only if there is only a single element,
|
||||||
|
-- and that element is itself only one line.
|
||||||
|
, compositeOneLine :: [NonEmpty Builder] -> Maybe [Builder]
|
||||||
|
}
|
||||||
|
|
||||||
|
instance Default Composite where
|
||||||
|
def = Composite {
|
||||||
|
compositeHeader = Nothing
|
||||||
|
, compositePrefix = Nothing
|
||||||
|
, compositeSuffix = Nothing
|
||||||
|
, compositeSeparator = ','
|
||||||
|
, compositeOneLine = \case
|
||||||
|
[firstLine :| []] -> Just [firstLine]
|
||||||
|
_otherwise -> Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
compositeList :: Composite
|
||||||
|
compositeList = def {
|
||||||
|
compositePrefix = Just "["
|
||||||
|
, compositeSuffix = Just "]"
|
||||||
|
, compositeOneLine = \rs -> do
|
||||||
|
xs <- mapM isOneLine rs
|
||||||
|
|
||||||
|
let argLengths :: [Int64]
|
||||||
|
argLengths = map (Text.Lazy.length . B.toLazyText) xs
|
||||||
|
guard $ or [
|
||||||
|
sum argLengths < 80
|
||||||
|
, all (<= 5) argLengths
|
||||||
|
]
|
||||||
|
|
||||||
|
return xs
|
||||||
|
}
|
||||||
|
where
|
||||||
|
isOneLine :: NonEmpty a -> Maybe a
|
||||||
|
isOneLine (firstLine :| []) = Just firstLine
|
||||||
|
isOneLine _otherwise = Nothing
|
||||||
|
|
||||||
|
compositeTuple :: Composite
|
||||||
|
compositeTuple = def {
|
||||||
|
compositePrefix = Just "("
|
||||||
|
, compositeSuffix = Just ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
compositeConstr :: Text -> [TypeRep] -> Composite
|
||||||
|
compositeConstr c ts = def {
|
||||||
|
compositeSeparator = ' '
|
||||||
|
, compositeHeader = Just $ intercalate " " $ c : map typeApp ts
|
||||||
|
}
|
||||||
|
where
|
||||||
|
-- We are careful to insert brackets around the typerep if needed
|
||||||
|
typeApp :: TypeRep -> Text
|
||||||
|
typeApp typ = "@" <> Text.pack (Prelude.showsPrec appPrec typ [])
|
||||||
|
|
||||||
|
compositeHaskellRecord :: Text -> Composite
|
||||||
|
compositeHaskellRecord r = def {
|
||||||
|
compositeHeader = Just $ r
|
||||||
|
, compositePrefix = Just $ "{"
|
||||||
|
, compositeSuffix = Just $ "}"
|
||||||
|
, compositeOneLine = const Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
compositeJsonRecord :: Composite
|
||||||
|
compositeJsonRecord = def {
|
||||||
|
compositePrefix = Just $ "{"
|
||||||
|
, compositeSuffix = Just $ "}"
|
||||||
|
, compositeOneLine = const Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
compositeQuasiQ :: Text -> Composite
|
||||||
|
compositeQuasiQ qq = def {
|
||||||
|
compositePrefix = Just $ "[" <> qq <> "|"
|
||||||
|
, compositeSuffix = Just $ "|]"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | Render composite value
|
||||||
|
renderComposite :: Composite -> [NonEmpty Builder] -> NonEmpty Builder
|
||||||
|
renderComposite Composite{..} =
|
||||||
|
go
|
||||||
|
where
|
||||||
|
go :: [NonEmpty Builder] -> NonEmpty Builder
|
||||||
|
go rs
|
||||||
|
| Just xs <- compositeOneLine rs
|
||||||
|
= pure $ mconcat [
|
||||||
|
prefix
|
||||||
|
, intercalate (B.singleton compositeSeparator) xs
|
||||||
|
, maybe mempty B.fromText compositeSuffix
|
||||||
|
]
|
||||||
|
|
||||||
|
| otherwise
|
||||||
|
= prefix
|
||||||
|
:| concat [
|
||||||
|
concatMap NE.toList $ sepElemsBy compositeSeparator rs
|
||||||
|
, [B.fromText suffix | Just suffix <- [compositeSuffix]]
|
||||||
|
]
|
||||||
|
|
||||||
|
prefix :: Builder
|
||||||
|
prefix = mconcat [
|
||||||
|
maybe mempty (\hdr -> B.fromText hdr <> " ") compositeHeader
|
||||||
|
, maybe mempty B.fromText compositePrefix
|
||||||
|
]
|
||||||
|
|
||||||
|
sepElemsBy :: Char -> [NonEmpty Builder] -> [NonEmpty Builder]
|
||||||
|
sepElemsBy sep = zipWith aux (True : repeat False)
|
||||||
|
where
|
||||||
|
aux :: Bool -> NonEmpty Builder -> NonEmpty Builder
|
||||||
|
aux firstEntry (firstLine :| rest) =
|
||||||
|
( if firstEntry
|
||||||
|
then (B.singleton ' ' <> B.singleton ' ' <> firstLine)
|
||||||
|
else (B.singleton sep <> B.singleton ' ' <> firstLine)
|
||||||
|
)
|
||||||
|
:| indent rest
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Rendering auxiliary
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
indent :: [Builder] -> [Builder]
|
||||||
|
indent = map (B.fromText " " <>)
|
||||||
|
|
||||||
|
addNumericUnderscores :: String -> String
|
||||||
|
addNumericUnderscores =
|
||||||
|
reverse
|
||||||
|
. aux
|
||||||
|
. reverse
|
||||||
|
where
|
||||||
|
aux :: String -> String
|
||||||
|
aux str =
|
||||||
|
case splitAt 3 str of
|
||||||
|
(_ , [] ) -> str
|
||||||
|
(firstThree , rest) -> firstThree ++ "_" ++ aux rest
|
||||||
|
|
||||||
|
-- | Generation of @intercalate@ from the Prelude
|
||||||
|
intercalate :: Monoid a => a -> [a] -> a
|
||||||
|
intercalate x = mconcat . intersperse x
|
96
src/Data/Structured/TH.hs
Normal file
96
src/Data/Structured/TH.hs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
module Data.Structured.TH (
|
||||||
|
deriveInstance
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
|
import Language.Haskell.TH
|
||||||
|
import Language.Haskell.TH.Datatype
|
||||||
|
|
||||||
|
-- | Derive 'Show' instance
|
||||||
|
--
|
||||||
|
-- Normally TH is not required, and you can rely on generics instead. However,
|
||||||
|
-- in some cases TH is the only option; for example, this is the case when
|
||||||
|
-- deriving a 'Show' instance for a GADT.
|
||||||
|
--
|
||||||
|
-- Example usage:
|
||||||
|
--
|
||||||
|
-- > Structured.deriveInstance 'ConstrOfMyType [t|
|
||||||
|
-- > forall a. Structured.Show a => Structured.Show (MyType a)
|
||||||
|
-- > |]
|
||||||
|
--
|
||||||
|
-- All type variables must be explicitly quantified (use an empty forall if
|
||||||
|
-- there are none), and any required constraints must be explicitly listed. In
|
||||||
|
-- addition, one of the constructors of @MyType@ must be listed (this is used to
|
||||||
|
-- resolve the datatype, ensuring that it works with regular datatypes as well
|
||||||
|
-- as associated datatypes).
|
||||||
|
deriveInstance :: Name -> Q Type -> Q [Dec]
|
||||||
|
deriveInstance constr header = do
|
||||||
|
info <- reifyDatatype constr
|
||||||
|
(ctxt, rhs) <- parseHeader =<< header
|
||||||
|
(:[]) <$>
|
||||||
|
instanceD
|
||||||
|
(return ctxt)
|
||||||
|
(return rhs)
|
||||||
|
[ funD 'Structured.toValue $ map caseFor (datatypeCons info)
|
||||||
|
]
|
||||||
|
|
||||||
|
-- | Parse instance header
|
||||||
|
parseHeader :: Type -> Q (Cxt, Type)
|
||||||
|
parseHeader = \case
|
||||||
|
ForallT _bndrs ctxt rhs@(AppT (ConT nameShow) _)
|
||||||
|
| nameShow == ''Structured.Show
|
||||||
|
-> return (ctxt, rhs)
|
||||||
|
_otherwise ->
|
||||||
|
fail $ "Invalid header"
|
||||||
|
|
||||||
|
-- | Case for one of the constructors
|
||||||
|
caseFor :: ConstructorInfo -> Q Clause
|
||||||
|
caseFor ConstructorInfo{
|
||||||
|
constructorName = con
|
||||||
|
, constructorFields = fields
|
||||||
|
, constructorVariant = variant
|
||||||
|
} = do
|
||||||
|
args <- mapM (const $ newName "x") fields
|
||||||
|
clause
|
||||||
|
[conP con (map varP args)]
|
||||||
|
( normalB $
|
||||||
|
case variant of
|
||||||
|
RecordConstructor fieldNames -> record fieldNames args
|
||||||
|
_otherwise -> constr args
|
||||||
|
)
|
||||||
|
[]
|
||||||
|
where
|
||||||
|
-- Regular (non-record) constructor
|
||||||
|
constr :: [Name] -> ExpQ
|
||||||
|
constr args = appsE [
|
||||||
|
conE 'Structured.Constr
|
||||||
|
, varE 'Text.pack
|
||||||
|
`appE`
|
||||||
|
litE (StringL (nameBase con))
|
||||||
|
, listE [] -- We do not support any type applications
|
||||||
|
, listE $
|
||||||
|
map
|
||||||
|
(\x -> varE 'Structured.toValue `appE` varE x)
|
||||||
|
args
|
||||||
|
]
|
||||||
|
|
||||||
|
record :: [Name] -> [Name] -> ExpQ
|
||||||
|
record fieldNames args = appsE [
|
||||||
|
conE 'Structured.Record
|
||||||
|
, varE 'Text.pack
|
||||||
|
`appE`
|
||||||
|
litE (StringL (nameBase con))
|
||||||
|
, listE $
|
||||||
|
zipWith
|
||||||
|
(\f x -> tupE [
|
||||||
|
litE (StringL (nameBase f))
|
||||||
|
, varE 'Structured.toValue `appE` varE x
|
||||||
|
]
|
||||||
|
)
|
||||||
|
fieldNames
|
||||||
|
args
|
||||||
|
]
|
34
src/Foreign/Rust/External/Bincode.hs
vendored
Normal file
34
src/Foreign/Rust/External/Bincode.hs
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
-- | External (Rust-side) Bincode serialisation/deserialisation
|
||||||
|
--
|
||||||
|
-- Intended for qualified import.
|
||||||
|
--
|
||||||
|
-- > import qualified Foreign.Rust.External.Bincode as External
|
||||||
|
module Foreign.Rust.External.Bincode (
|
||||||
|
-- * Serialisation
|
||||||
|
Bincode(..)
|
||||||
|
, ToBincode(..)
|
||||||
|
, FromBincode(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Codec.Borsh
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as Lazy
|
||||||
|
|
||||||
|
import Foreign.Rust.Serialisation.Raw
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Serialisation
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
newtype Bincode = Bincode Lazy.ByteString
|
||||||
|
deriving stock (Eq)
|
||||||
|
deriving newtype (BorshSize, ToBorsh, FromBorsh)
|
||||||
|
deriving newtype (IsRaw)
|
||||||
|
|
||||||
|
-- | Types with an external Bincode serialiser (typically, in Rust)
|
||||||
|
class ToBincode a where
|
||||||
|
toBincode :: a -> Bincode
|
||||||
|
|
||||||
|
-- | Types with an external Bincode deserialiser (typically, in Rust)
|
||||||
|
class FromBincode a where
|
||||||
|
fromBincode :: Bincode -> Either String a
|
72
src/Foreign/Rust/External/JSON.hs
vendored
Normal file
72
src/Foreign/Rust/External/JSON.hs
vendored
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
-- | External (Rust-side) JSON serialisation/deserialisation
|
||||||
|
--
|
||||||
|
-- Intended for qualified import.
|
||||||
|
--
|
||||||
|
-- > import Foreign.Rust.External.JSON (UseExternalJSON, ShowAsJSON)
|
||||||
|
-- > import qualified Foreign.Rust.External.JSON as External
|
||||||
|
module Foreign.Rust.External.JSON (
|
||||||
|
-- * Serialisation
|
||||||
|
JSON(..)
|
||||||
|
, ToJSON(..)
|
||||||
|
, FromJSON(..)
|
||||||
|
-- * Deriving-via: derive Aeson instances using external (de)serialiser
|
||||||
|
, UseExternalJSON(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Codec.Borsh
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
import qualified Data.Aeson.Encoding as Aeson (unsafeToEncoding)
|
||||||
|
import qualified Data.Aeson.Types as Aeson (parseFail)
|
||||||
|
import qualified Data.Binary.Builder as Binary
|
||||||
|
import qualified Data.ByteString.Lazy as Lazy (ByteString)
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Serialisation
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Serialised JSON
|
||||||
|
newtype JSON = JSON Lazy.ByteString
|
||||||
|
deriving stock (Eq)
|
||||||
|
deriving newtype (BorshSize, ToBorsh, FromBorsh)
|
||||||
|
|
||||||
|
-- | Types with an external JSON renderer (typically, in Rust)
|
||||||
|
class ToJSON a where
|
||||||
|
toJSON :: a -> JSON
|
||||||
|
|
||||||
|
-- | Types with an external JSON parser (typically, in Rust)
|
||||||
|
class FromJSON a where
|
||||||
|
fromJSON :: JSON -> Either String a
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving-via: derive Aeson instances using external (de)serialiser
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
newtype UseExternalJSON a = UseExternalJSON a
|
||||||
|
|
||||||
|
instance ToJSON a => Aeson.ToJSON (UseExternalJSON a) where
|
||||||
|
toJSON (UseExternalJSON x) =
|
||||||
|
reparse (toJSON x)
|
||||||
|
where
|
||||||
|
-- We get serialised JSON from the external renderer, and then need to
|
||||||
|
-- re-parse that to a 'Value'. If this fails, however, it would mean that
|
||||||
|
-- the Rust-side generated invalid JSON.
|
||||||
|
reparse :: JSON -> Aeson.Value
|
||||||
|
reparse (JSON bs) =
|
||||||
|
case Aeson.eitherDecode bs of
|
||||||
|
Left err -> error err
|
||||||
|
Right val -> val
|
||||||
|
|
||||||
|
-- This relies on 'toJSON' generating valid JSON
|
||||||
|
toEncoding (UseExternalJSON x) =
|
||||||
|
case toJSON x of
|
||||||
|
JSON bs -> Aeson.unsafeToEncoding $ Binary.fromLazyByteString bs
|
||||||
|
|
||||||
|
instance FromJSON a => Aeson.FromJSON (UseExternalJSON a) where
|
||||||
|
parseJSON val =
|
||||||
|
case fromJSON (JSON (Aeson.encode val)) of
|
||||||
|
Left failure -> Aeson.parseFail failure
|
||||||
|
Right tx -> return $ UseExternalJSON tx
|
||||||
|
|
27
src/Foreign/Rust/Failure.hs
Normal file
27
src/Foreign/Rust/Failure.hs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
module Foreign.Rust.Failure (
|
||||||
|
Failure -- Opaque
|
||||||
|
, failureMessage
|
||||||
|
, mkFailure
|
||||||
|
) where
|
||||||
|
|
||||||
|
import GHC.Stack
|
||||||
|
import Data.Text (Text)
|
||||||
|
|
||||||
|
-- | Failure reported by a Rust function
|
||||||
|
--
|
||||||
|
-- TODO: For some cases we might be able to attach a Rust callstack, too.
|
||||||
|
data Failure = Failure {
|
||||||
|
failureMessage :: Text
|
||||||
|
, failureCallstackHaskell :: PrettyCallStack
|
||||||
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
mkFailure :: HasCallStack => Text -> Failure
|
||||||
|
mkFailure e = Failure e (PrettyCallStack callStack)
|
||||||
|
|
||||||
|
newtype PrettyCallStack = PrettyCallStack CallStack
|
||||||
|
|
||||||
|
instance Show PrettyCallStack where
|
||||||
|
show (PrettyCallStack stack) = prettyCallStack stack
|
||||||
|
|
||||||
|
|
55
src/Foreign/Rust/Marshall/Fixed.hs
Normal file
55
src/Foreign/Rust/Marshall/Fixed.hs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
|
||||||
|
|
||||||
|
-- | Marshalling to and from Rust, using Borsh
|
||||||
|
--
|
||||||
|
-- This module deals with types with fixed sized encodings.
|
||||||
|
-- See also "Foreign.Rust.Marshall.Variable".
|
||||||
|
module Foreign.Rust.Marshall.Fixed (
|
||||||
|
-- * Haskell to Rust
|
||||||
|
toBorshFixed
|
||||||
|
-- * Rust to Haskell
|
||||||
|
, allocFixedBuffer
|
||||||
|
, fromBorshFixed
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Codec.Borsh
|
||||||
|
import Data.Proxy
|
||||||
|
import Data.Typeable (Typeable)
|
||||||
|
import Foreign
|
||||||
|
import Foreign.C.Types
|
||||||
|
|
||||||
|
import qualified Data.ByteString as Strict
|
||||||
|
|
||||||
|
import Foreign.Rust.Marshall.Util
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Haskell to Rust
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
toBorshFixed ::
|
||||||
|
(ToBorsh a, StaticBorshSize a ~ 'HasKnownSize)
|
||||||
|
=> a -> ((Ptr CUChar, CULong) -> IO r) -> IO r
|
||||||
|
toBorshFixed a k =
|
||||||
|
Strict.useAsCStringLen (serialiseStrict a) (k . castFromSignedLen)
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Rust to Haskell
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
allocFixedBuffer :: forall a.
|
||||||
|
(BorshSize a, StaticBorshSize a ~ 'HasKnownSize)
|
||||||
|
=> ((Ptr CUChar, CULong) -> IO a) -> IO a
|
||||||
|
allocFixedBuffer k =
|
||||||
|
case borshSize (Proxy @a) of
|
||||||
|
SizeKnown n ->
|
||||||
|
allocaBytes (cast n) $ \ptr -> k (ptr, fromIntegral n)
|
||||||
|
where
|
||||||
|
cast :: Word32 -> Int
|
||||||
|
cast = fromIntegral
|
||||||
|
|
||||||
|
fromBorshFixed ::
|
||||||
|
(FromBorsh a, StaticBorshSize a ~ 'HasKnownSize, Typeable a)
|
||||||
|
=> Ptr CUChar -> CULong -> IO a
|
||||||
|
fromBorshFixed ptr len =
|
||||||
|
deserialiseStrictOrPanic <$>
|
||||||
|
Strict.packCStringLen (castToSigned ptr, fromIntegral len)
|
58
src/Foreign/Rust/Marshall/Util.hs
Normal file
58
src/Foreign/Rust/Marshall/Util.hs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
module Foreign.Rust.Marshall.Util (
|
||||||
|
-- * Borsh
|
||||||
|
serialiseStrict
|
||||||
|
, deserialiseStrictOrPanic
|
||||||
|
, deserialiseLazyOrPanic
|
||||||
|
-- * Casting
|
||||||
|
, castFromSigned
|
||||||
|
, castToSigned
|
||||||
|
, castFromSignedLen
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Codec.Borsh
|
||||||
|
import Data.Typeable
|
||||||
|
import Foreign
|
||||||
|
import Foreign.C
|
||||||
|
import GHC.Stack
|
||||||
|
|
||||||
|
import qualified Data.ByteString as Strict
|
||||||
|
import qualified Data.ByteString.Lazy as Lazy
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Borsh
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
serialiseStrict :: ToBorsh a => a -> Strict.ByteString
|
||||||
|
serialiseStrict = Lazy.toStrict . serialiseBorsh
|
||||||
|
|
||||||
|
deserialiseStrictOrPanic ::
|
||||||
|
(HasCallStack, FromBorsh a, Typeable a)
|
||||||
|
=> Strict.ByteString -> a
|
||||||
|
deserialiseStrictOrPanic = deserialiseLazyOrPanic . Lazy.fromStrict
|
||||||
|
|
||||||
|
deserialiseLazyOrPanic :: forall a.
|
||||||
|
(HasCallStack, FromBorsh a, Typeable a)
|
||||||
|
=> Lazy.ByteString -> a
|
||||||
|
deserialiseLazyOrPanic bs =
|
||||||
|
case deserialiseBorsh bs of
|
||||||
|
Right a -> a
|
||||||
|
Left err -> error $ concat [
|
||||||
|
"deserialiseLazyOrPanic for " ++ show (typeOf (Proxy @a)) ++ ": "
|
||||||
|
, show err ++ "\n"
|
||||||
|
, "buffer: " ++ show (Lazy.unpack bs)
|
||||||
|
, " (" ++ show (Lazy.length bs) ++ ")"
|
||||||
|
]
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Casting
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
castFromSigned :: Ptr CChar -> Ptr CUChar
|
||||||
|
castFromSigned = castPtr
|
||||||
|
|
||||||
|
castToSigned :: Ptr CUChar -> Ptr CChar
|
||||||
|
castToSigned = castPtr
|
||||||
|
|
||||||
|
castFromSignedLen :: (Ptr CChar, Int) -> (Ptr CUChar, CULong)
|
||||||
|
castFromSignedLen (ptr, len) = (castFromSigned ptr, fromIntegral len)
|
||||||
|
|
141
src/Foreign/Rust/Marshall/Variable.hs
Normal file
141
src/Foreign/Rust/Marshall/Variable.hs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
|
||||||
|
|
||||||
|
-- | Marshalling to and from Rust, using Borsh
|
||||||
|
--
|
||||||
|
-- This module deals with types with variable sized encodings.
|
||||||
|
-- See also "Foreign.Rust.Marshall.Fixed".
|
||||||
|
module Foreign.Rust.Marshall.Variable (
|
||||||
|
-- * Haskell to Rust
|
||||||
|
toBorshVar
|
||||||
|
-- * Rust to Haskell
|
||||||
|
, Buffer -- opaque
|
||||||
|
, getVarBuffer
|
||||||
|
, withBorshVarBuffer
|
||||||
|
, withBorshMaxBuffer
|
||||||
|
, withBorshFailure
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Codec.Borsh
|
||||||
|
import Data.Bifunctor
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Typeable
|
||||||
|
import Foreign
|
||||||
|
import Foreign.C.Types
|
||||||
|
import GHC.Stack
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
|
|
||||||
|
import qualified Data.ByteString as Strict
|
||||||
|
|
||||||
|
import Foreign.Rust.Marshall.Util
|
||||||
|
import Foreign.Rust.Failure
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Haskell to Rust
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
toBorshVar ::
|
||||||
|
(ToBorsh a, StaticBorshSize a ~ 'HasVariableSize)
|
||||||
|
=> a -> ((Ptr CUChar, CULong) -> IO r) -> IO r
|
||||||
|
toBorshVar a k =
|
||||||
|
Strict.useAsCStringLen (serialiseStrict a) (k . castFromSignedLen)
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Rust to Haskell
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Buffer containing value of (phantom) type @a@
|
||||||
|
data Buffer a = Buffer (Ptr CUChar) (Ptr CULong)
|
||||||
|
|
||||||
|
getVarBuffer :: Buffer a -> (Ptr CUChar, Ptr CULong)
|
||||||
|
getVarBuffer (Buffer buf ptrSize) = (buf, ptrSize)
|
||||||
|
|
||||||
|
-- | Provide buffer for foreign call
|
||||||
|
--
|
||||||
|
-- We start with an initial buffer of 1 kB. If that suffices, we copy the
|
||||||
|
-- (appropriate part of) the buffer to a ByteString and we're done. If not,
|
||||||
|
-- the foreign call will tell us what the required buffer size is, so we
|
||||||
|
-- try again with a larger buffer.
|
||||||
|
--
|
||||||
|
-- We allocate these buffers on the Haskell heap ('allocaBytes'), not the C
|
||||||
|
-- heap ('malloc'). This ensures that these buffers are visible to Haskell
|
||||||
|
-- profiling tools, and also appears to be more reliable on OSX. A slight
|
||||||
|
-- downside is that it doesn't give us a way to /change/ the size of a buffer,
|
||||||
|
-- but that's not an essential feature.
|
||||||
|
withBorshVarBuffer :: forall a.
|
||||||
|
( FromBorsh a
|
||||||
|
, StaticBorshSize a ~ 'HasVariableSize
|
||||||
|
, Typeable a
|
||||||
|
)
|
||||||
|
=> (Buffer a -> IO ()) -> a
|
||||||
|
withBorshVarBuffer = withBorshBufferOfInitSize 1024
|
||||||
|
|
||||||
|
withBorshMaxBuffer :: forall a.
|
||||||
|
( FromBorsh a
|
||||||
|
, StaticBorshSize a ~ 'HasVariableSize
|
||||||
|
, BorshMaxSize a
|
||||||
|
, Typeable a
|
||||||
|
)
|
||||||
|
=> (Buffer a -> IO ()) -> a
|
||||||
|
withBorshMaxBuffer =
|
||||||
|
withBorshBufferOfInitSize initBufSize
|
||||||
|
where
|
||||||
|
initBufSize :: CULong
|
||||||
|
initBufSize = fromIntegral $ borshMaxSize (Proxy @a)
|
||||||
|
|
||||||
|
-- | Wrapper around 'withBorshVarBuffer' with explicit support for failures
|
||||||
|
withBorshFailure :: forall a.
|
||||||
|
( FromBorsh a
|
||||||
|
, StaticBorshSize a ~ 'HasVariableSize
|
||||||
|
, Typeable a
|
||||||
|
, HasCallStack
|
||||||
|
)
|
||||||
|
=> (Buffer (Either Text a) -> IO ()) -> Either Failure a
|
||||||
|
withBorshFailure = first mkFailure . withBorshVarBuffer
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Internal auxiliary
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Generalization of 'withBorshVarBuffer' and 'withMaxBorshBuffer'
|
||||||
|
withBorshBufferOfInitSize :: forall a.
|
||||||
|
( FromBorsh a
|
||||||
|
, StaticBorshSize a ~ 'HasVariableSize
|
||||||
|
, Typeable a
|
||||||
|
)
|
||||||
|
=> CULong -> (Buffer a -> IO ()) -> a
|
||||||
|
withBorshBufferOfInitSize initBufSize f = unsafePerformIO $ do
|
||||||
|
mFirstAttempt <- allocaBytes (culongToInt initBufSize) $ \buf -> do
|
||||||
|
(bigEnough, reqSz) <- callWithSize buf initBufSize
|
||||||
|
if bigEnough then
|
||||||
|
Right . deserialiseStrictOrPanic <$>
|
||||||
|
Strict.packCStringLen (castPtr buf, culongToInt reqSz)
|
||||||
|
else
|
||||||
|
return $ Left reqSz
|
||||||
|
case mFirstAttempt of
|
||||||
|
Right r ->
|
||||||
|
return r
|
||||||
|
Left reqSz -> do
|
||||||
|
allocaBytes (culongToInt reqSz) $ \buf -> do
|
||||||
|
(bigEnough, reqSz') <- callWithSize buf reqSz
|
||||||
|
if bigEnough && reqSz == reqSz' then
|
||||||
|
deserialiseStrictOrPanic <$>
|
||||||
|
Strict.packCStringLen (castPtr buf, culongToInt reqSz)
|
||||||
|
else
|
||||||
|
fail $ concat [
|
||||||
|
"withBorshVarBuffer: unexpected change in required buffer size. "
|
||||||
|
, "was " ++ show reqSz ++ ", "
|
||||||
|
, "now " ++ show reqSz' ++ "."
|
||||||
|
]
|
||||||
|
where
|
||||||
|
-- Call the function with the current buffer size
|
||||||
|
-- Returns whether or not the buffer was big enough, and the required size
|
||||||
|
callWithSize :: Ptr CUChar -> CULong -> IO (Bool, CULong)
|
||||||
|
callWithSize buf providedSize = alloca $ \ptrBufSize -> do
|
||||||
|
poke ptrBufSize providedSize
|
||||||
|
f $ Buffer buf ptrBufSize
|
||||||
|
requiredSize <- peek ptrBufSize
|
||||||
|
return (requiredSize <= providedSize, requiredSize)
|
||||||
|
|
||||||
|
-- Buffer allocations should not take a signed 'Int' as argument 🙄
|
||||||
|
culongToInt :: CULong -> Int
|
||||||
|
culongToInt = fromIntegral
|
18
src/Foreign/Rust/SafeConv.hs
Normal file
18
src/Foreign/Rust/SafeConv.hs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module Foreign.Rust.SafeConv (
|
||||||
|
SafeConv(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Word
|
||||||
|
import Foreign.C.Types
|
||||||
|
|
||||||
|
class SafeConv a b where
|
||||||
|
safeConvFrom :: a -> b
|
||||||
|
safeConvTo :: b -> a
|
||||||
|
|
||||||
|
instance SafeConv CULong Word64 where
|
||||||
|
safeConvFrom = fromIntegral
|
||||||
|
safeConvTo = fromIntegral
|
||||||
|
|
||||||
|
instance SafeConv CULLong Word64 where
|
||||||
|
safeConvFrom = fromIntegral
|
||||||
|
safeConvTo = fromIntegral
|
59
src/Foreign/Rust/Serialisation/JSON.hs
Normal file
59
src/Foreign/Rust/Serialisation/JSON.hs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
-- | Serialise opaque types using JSON
|
||||||
|
--
|
||||||
|
-- See "Foreign.Rust.Serialisation.Raw" for detailed discussion.
|
||||||
|
module Foreign.Rust.Serialisation.JSON (
|
||||||
|
-- * Deriving-via support
|
||||||
|
AsJSON(..)
|
||||||
|
-- * Show instance
|
||||||
|
, asJSON
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Annotated
|
||||||
|
import Data.Aeson (FromJSON(..))
|
||||||
|
import Data.Typeable
|
||||||
|
|
||||||
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving-via combinator
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Serialise using JSON
|
||||||
|
--
|
||||||
|
-- The 'Show' instance will produce something like
|
||||||
|
--
|
||||||
|
-- > asJSON @UsesJSON
|
||||||
|
-- > [aesonQQ|
|
||||||
|
-- > {
|
||||||
|
-- > "a": null
|
||||||
|
-- > , "b": [1,2,3]
|
||||||
|
-- > }
|
||||||
|
-- > |]
|
||||||
|
--
|
||||||
|
-- This depends on 'asJSON' (defined in this module), @QuasiQuotes@ and
|
||||||
|
-- "Data.Aeson.QQ.Simple".
|
||||||
|
--
|
||||||
|
-- NOTE: 'Annotated' instance is only useful when using 'AsBase64' directly
|
||||||
|
-- (rather than using deriving-via).
|
||||||
|
newtype AsJSON a = AsJSON { unwrapAsJSON :: a }
|
||||||
|
deriving newtype CanAnnotate
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Show
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
deriving
|
||||||
|
via Structured.ToPreludeShow (AsJSON a)
|
||||||
|
instance (Typeable a, Aeson.ToJSON a) => Show (AsJSON a)
|
||||||
|
|
||||||
|
instance (Typeable a, Aeson.ToJSON a) => Structured.Show (AsJSON a) where
|
||||||
|
toValue (AsJSON x) =
|
||||||
|
Structured.Constr "asJSON" [typeRep (Proxy @a)] [
|
||||||
|
Structured.JSON (Aeson.toJSON x)
|
||||||
|
]
|
||||||
|
|
||||||
|
asJSON :: forall a. FromJSON a => Aeson.Value -> a
|
||||||
|
asJSON = either error id . Aeson.parseEither Aeson.parseJSON
|
107
src/Foreign/Rust/Serialisation/Raw.hs
Normal file
107
src/Foreign/Rust/Serialisation/Raw.hs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
-- | Dealing with types that are represented in raw form Haskell side
|
||||||
|
module Foreign.Rust.Serialisation.Raw (
|
||||||
|
IsRaw(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.FixedSizeArray (FixedSizeArray)
|
||||||
|
import Data.Proxy
|
||||||
|
import Data.Word
|
||||||
|
import GHC.TypeLits
|
||||||
|
|
||||||
|
import qualified Data.ByteString as Strict
|
||||||
|
import qualified Data.ByteString.Lazy as Lazy
|
||||||
|
import qualified Data.FixedSizeArray as FSA
|
||||||
|
import qualified Data.Vector.Generic as Vector
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Abstract over raw representation
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Datatype that is represented as raw bytes Haskell-side
|
||||||
|
--
|
||||||
|
-- Sometimes when dealing with Rust-side values we represent them as opaque
|
||||||
|
-- values Haskell side: essentially just a list of bytes. However, we typically
|
||||||
|
-- still want to be able to display, serialise and deserialise such values.
|
||||||
|
--
|
||||||
|
-- 'IsRaw' abstracts over how exactly those raw bytes stored; in the following
|
||||||
|
-- modules we then provide deriving-via combinators for specific encodings:
|
||||||
|
--
|
||||||
|
-- * Foreign.Rust.Serialisation.Raw.Base16 (base-16, hexadecimal, hexdump)
|
||||||
|
-- * Foreign.Rust.Serialisation.Raw.Base58 (base-58, bitcoin format)
|
||||||
|
-- * Foreign.Rust.Serialisation.Raw.Base64 (base-64)
|
||||||
|
-- * Foreign.Rust.Serialisation.Raw.Decimal (list of decimal values)
|
||||||
|
--
|
||||||
|
-- All of these modules provide combinators to derive
|
||||||
|
--
|
||||||
|
-- * 'Prelude.Show' and 'Data.Structured.Show' (law-abiding in the sense of
|
||||||
|
-- generating valid Haskell)
|
||||||
|
-- * 'FromJSON' and 'ToJSON'
|
||||||
|
--
|
||||||
|
-- All of these modules show the raw bytes, they just differ in /how/ they show
|
||||||
|
-- those raw bytes. If you want a more human-readable format, consider using
|
||||||
|
-- "Foreign.Rust.Serialisation.JSON".
|
||||||
|
class IsRaw a where
|
||||||
|
{-# MINIMAL (toRaw | toBytes), (fromRaw | fromBytes) #-}
|
||||||
|
|
||||||
|
-- rawSize
|
||||||
|
|
||||||
|
rawSize :: a -> Word32
|
||||||
|
rawSize = fromIntegral . Lazy.length . toRaw
|
||||||
|
|
||||||
|
-- toRaw
|
||||||
|
|
||||||
|
toRaw :: a -> Lazy.ByteString
|
||||||
|
toRaw = Lazy.pack . toBytes
|
||||||
|
|
||||||
|
toBytes :: a -> [Word8]
|
||||||
|
toBytes = Lazy.unpack . toRaw
|
||||||
|
|
||||||
|
-- fromRaw
|
||||||
|
|
||||||
|
fromRaw :: Lazy.ByteString -> Either String a
|
||||||
|
fromRaw = fromBytes . Lazy.unpack
|
||||||
|
|
||||||
|
fromBytes :: [Word8] -> Either String a
|
||||||
|
fromBytes = fromRaw . Lazy.pack
|
||||||
|
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
ByteString
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance IsRaw Lazy.ByteString where
|
||||||
|
rawSize = fromIntegral . Lazy.length
|
||||||
|
toRaw = id
|
||||||
|
fromRaw = Right
|
||||||
|
|
||||||
|
instance IsRaw Strict.ByteString where
|
||||||
|
rawSize = fromIntegral . Strict.length
|
||||||
|
toRaw = Lazy.fromStrict
|
||||||
|
fromRaw = Right . Lazy.toStrict
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
[Word8]
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance IsRaw [Word8] where
|
||||||
|
rawSize = fromIntegral . length
|
||||||
|
toBytes = id
|
||||||
|
fromBytes = Right
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
FixedSizeArray
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance KnownNat n => IsRaw (FixedSizeArray n Word8) where
|
||||||
|
rawSize = const $ fromIntegral $ natVal (Proxy @n)
|
||||||
|
toBytes = Vector.toList
|
||||||
|
fromBytes = \xs ->
|
||||||
|
let expectedSize, actualSize :: Int
|
||||||
|
expectedSize = fromIntegral $ natVal (Proxy @n)
|
||||||
|
actualSize = length xs
|
||||||
|
in if actualSize == expectedSize
|
||||||
|
then Right $ FSA.fromList xs
|
||||||
|
else Left $ "Expected " ++ show expectedSize ++ "bytes, "
|
||||||
|
++ "but got " ++ show actualSize ++ ": "
|
||||||
|
++ show xs
|
||||||
|
|
83
src/Foreign/Rust/Serialisation/Raw/Base16.hs
Normal file
83
src/Foreign/Rust/Serialisation/Raw/Base16.hs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
-- | Base-16 encoding (hexdump)
|
||||||
|
--
|
||||||
|
-- See "Foreign.Rust.Serialisation.Raw" for discussion.
|
||||||
|
module Foreign.Rust.Serialisation.Raw.Base16 (
|
||||||
|
-- * Deriving-via support
|
||||||
|
AsBase16(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Data.Aeson (ToJSON(..), FromJSON(..), withText)
|
||||||
|
import Data.Annotated
|
||||||
|
import Data.String
|
||||||
|
|
||||||
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
import qualified Data.ByteString.Base16.Lazy as Base16
|
||||||
|
import qualified Data.ByteString.Lazy as Lazy
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as Char8
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
|
import Foreign.Rust.Serialisation.Raw
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving-via combinator
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Show values in base-16 (hexadecimal/hexdump)
|
||||||
|
--
|
||||||
|
-- The 'Show' instance will produce something like
|
||||||
|
--
|
||||||
|
-- > "01020304"
|
||||||
|
--
|
||||||
|
-- This depends on @OverloadedStrings@.
|
||||||
|
--
|
||||||
|
-- NOTE: 'Annotated' instance is only useful when using 'AsBase16' directly
|
||||||
|
-- (rather than using deriving-via).
|
||||||
|
newtype AsBase16 a = AsBase16 { unwrapAsBase16 ::a }
|
||||||
|
deriving newtype CanAnnotate
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
JSON
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance IsRaw a => ToJSON (AsBase16 a) where
|
||||||
|
toJSON = toJSON . encode . unwrapAsBase16
|
||||||
|
|
||||||
|
instance IsRaw a => FromJSON (AsBase16 a) where
|
||||||
|
parseJSON = either Aeson.parseFail (return . AsBase16) . decode <=< parseJSON
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Show
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance IsRaw a => Show (AsBase16 a) where
|
||||||
|
show = show . encode . unwrapAsBase16
|
||||||
|
|
||||||
|
instance IsRaw a => Structured.Show (AsBase16 a) where
|
||||||
|
toValue = Structured.toValue . encode . unwrapAsBase16
|
||||||
|
|
||||||
|
instance IsRaw a => IsString (AsBase16 a) where
|
||||||
|
fromString = either error AsBase16 . decode . fromString
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Auxiliary: base-16 encoded value
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
newtype Base16 = Base16 { getBase16 :: Lazy.ByteString }
|
||||||
|
deriving newtype (Show, IsString)
|
||||||
|
|
||||||
|
instance ToJSON Base16 where
|
||||||
|
toJSON = Aeson.String . Text.pack . Char8.unpack . getBase16
|
||||||
|
|
||||||
|
instance FromJSON Base16 where
|
||||||
|
parseJSON = withText "Base16" $ return . Base16 . Char8.pack . Text.unpack
|
||||||
|
|
||||||
|
instance Structured.Show Base16 where
|
||||||
|
toValue = Structured.String
|
||||||
|
|
||||||
|
encode :: IsRaw a => a -> Base16
|
||||||
|
encode = Base16 . Base16.encode . toRaw
|
||||||
|
|
||||||
|
decode :: IsRaw a => Base16 -> Either String a
|
||||||
|
decode = fromRaw <=< Base16.decode . getBase16
|
102
src/Foreign/Rust/Serialisation/Raw/Base58.hs
Normal file
102
src/Foreign/Rust/Serialisation/Raw/Base58.hs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
-- | Base-58 encoding
|
||||||
|
--
|
||||||
|
-- See "Foreign.Rust.Serialisation.Raw" for discussion.
|
||||||
|
module Foreign.Rust.Serialisation.Raw.Base58 (
|
||||||
|
-- * Deriving-via support
|
||||||
|
AsBase58(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Data.Aeson (ToJSON(..), FromJSON(..), withText)
|
||||||
|
import Data.Annotated
|
||||||
|
import Data.String
|
||||||
|
|
||||||
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
import qualified Data.ByteString as Strict
|
||||||
|
import qualified Data.ByteString.Base58 as Base58
|
||||||
|
import qualified Data.ByteString.Char8 as Char8
|
||||||
|
import qualified Data.ByteString.Lazy as Lazy
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
|
import Foreign.Rust.Serialisation.Raw
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving-via combinator
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Serialise using base-58
|
||||||
|
--
|
||||||
|
-- The 'Show' instance will produce something like
|
||||||
|
--
|
||||||
|
-- > "2VfUX"
|
||||||
|
--
|
||||||
|
-- This depends on @OverloadedStrings@.
|
||||||
|
--
|
||||||
|
-- NOTE: 'Annotated' instance is only useful when using 'AsBase58' directly
|
||||||
|
-- (rather than using deriving-via).
|
||||||
|
newtype AsBase58 a = AsBase58 { unwrapAsBase58 :: a }
|
||||||
|
deriving newtype CanAnnotate
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
JSON
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance IsRaw a => ToJSON (AsBase58 a) where
|
||||||
|
toJSON = toJSON . encode . unwrapAsBase58
|
||||||
|
|
||||||
|
instance IsRaw a => FromJSON (AsBase58 a) where
|
||||||
|
parseJSON = either Aeson.parseFail (return . AsBase58) . decode <=< parseJSON
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Show
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance IsRaw a => Show (AsBase58 a) where
|
||||||
|
show = show . encode . unwrapAsBase58
|
||||||
|
|
||||||
|
instance IsRaw a => Structured.Show (AsBase58 a) where
|
||||||
|
toValue = Structured.toValue . encode . unwrapAsBase58
|
||||||
|
|
||||||
|
instance IsRaw a => IsString (AsBase58 a) where
|
||||||
|
fromString = either error AsBase58 . decode . fromString
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Auxiliary: base-58 encoded value
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Base58-encoded value
|
||||||
|
--
|
||||||
|
-- NOTE: base-58 is a relatively expensive encoding; in particular, unlike
|
||||||
|
-- base-64, base-58 does not make it very easy to process chunks of data
|
||||||
|
-- separately. Ideally, it should therefore only be used for a small pieces of
|
||||||
|
-- data. For this reason, we use a strict bytestring here.
|
||||||
|
newtype Base58 = Base58 { getBase58 :: Strict.ByteString }
|
||||||
|
deriving newtype (Show, IsString)
|
||||||
|
|
||||||
|
instance ToJSON Base58 where
|
||||||
|
toJSON = Aeson.String . Text.pack . Char8.unpack . getBase58
|
||||||
|
|
||||||
|
instance FromJSON Base58 where
|
||||||
|
parseJSON = withText "Base58" $ return . Base58 . Char8.pack . Text.unpack
|
||||||
|
|
||||||
|
instance Structured.Show Base58 where
|
||||||
|
toValue = Structured.String
|
||||||
|
|
||||||
|
encode :: IsRaw a => a -> Base58
|
||||||
|
encode = Base58 . encodeBitcoin . Lazy.toStrict . toRaw
|
||||||
|
|
||||||
|
decode :: IsRaw a => Base58 -> Either String a
|
||||||
|
decode = fromRaw <=< fmap Lazy.fromStrict . decodeBitcoin . getBase58
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Internal auxiliary
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
encodeBitcoin :: Strict.ByteString -> Strict.ByteString
|
||||||
|
encodeBitcoin = Base58.encodeBase58 Base58.bitcoinAlphabet
|
||||||
|
|
||||||
|
decodeBitcoin :: Strict.ByteString -> Either String Strict.ByteString
|
||||||
|
decodeBitcoin =
|
||||||
|
maybe (Left "invalid Base58 encoding") Right
|
||||||
|
. Base58.decodeBase58 Base58.bitcoinAlphabet
|
83
src/Foreign/Rust/Serialisation/Raw/Base64.hs
Normal file
83
src/Foreign/Rust/Serialisation/Raw/Base64.hs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
-- | Base-64 encoding
|
||||||
|
--
|
||||||
|
-- See "Foreign.Rust.Serialisation.Raw" for discussion.
|
||||||
|
module Foreign.Rust.Serialisation.Raw.Base64 (
|
||||||
|
-- * Deriving-via support
|
||||||
|
AsBase64(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Data.Aeson (ToJSON(..), FromJSON(..), withText)
|
||||||
|
import Data.Annotated
|
||||||
|
import Data.String
|
||||||
|
|
||||||
|
import qualified Data.Aeson.Types as Aeson
|
||||||
|
import qualified Data.ByteString.Base64.Lazy as Base64
|
||||||
|
import qualified Data.ByteString.Lazy as Lazy
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as Char8
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
import qualified Data.Text as Text
|
||||||
|
|
||||||
|
import Foreign.Rust.Serialisation.Raw
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving-via combinator
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Serialise using base-64
|
||||||
|
--
|
||||||
|
-- The 'Show' instance will produce something like
|
||||||
|
--
|
||||||
|
-- > "AQIDBA=="
|
||||||
|
--
|
||||||
|
-- This depends on @OverloadedStrings@.
|
||||||
|
--
|
||||||
|
-- NOTE: 'Annotated' instance is only useful when using 'AsBase64' directly
|
||||||
|
-- (rather than using deriving-via).
|
||||||
|
newtype AsBase64 a = AsBase64 { unwrapAsBase64 :: a }
|
||||||
|
deriving newtype CanAnnotate
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
JSON
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance IsRaw a => ToJSON (AsBase64 a) where
|
||||||
|
toJSON = toJSON . encode . unwrapAsBase64
|
||||||
|
|
||||||
|
instance IsRaw a => FromJSON (AsBase64 a) where
|
||||||
|
parseJSON = either Aeson.parseFail (return . AsBase64) . decode <=< parseJSON
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Show
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance IsRaw a => Show (AsBase64 a) where
|
||||||
|
show = show . encode . unwrapAsBase64
|
||||||
|
|
||||||
|
instance IsRaw a => Structured.Show (AsBase64 a) where
|
||||||
|
toValue = Structured.toValue . encode . unwrapAsBase64
|
||||||
|
|
||||||
|
instance IsRaw a => IsString (AsBase64 a) where
|
||||||
|
fromString = either error AsBase64 . decode . fromString
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Auxiliary: base-64 encoded value
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
newtype Base64 = Base64 { getBase64 :: Lazy.ByteString }
|
||||||
|
deriving newtype (Show, IsString)
|
||||||
|
|
||||||
|
instance ToJSON Base64 where
|
||||||
|
toJSON = Aeson.String . Text.pack . Char8.unpack . getBase64
|
||||||
|
|
||||||
|
instance FromJSON Base64 where
|
||||||
|
parseJSON = withText "Base64" $ return . Base64 . Char8.pack . Text.unpack
|
||||||
|
|
||||||
|
instance Structured.Show Base64 where
|
||||||
|
toValue = Structured.String
|
||||||
|
|
||||||
|
encode :: IsRaw a => a -> Base64
|
||||||
|
encode = Base64 . Base64.encode . toRaw
|
||||||
|
|
||||||
|
decode :: IsRaw a => Base64 -> Either String a
|
||||||
|
decode = fromRaw <=< Base64.decode . getBase64
|
61
src/Foreign/Rust/Serialisation/Raw/Decimal.hs
Normal file
61
src/Foreign/Rust/Serialisation/Raw/Decimal.hs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
-- | List of decimal values
|
||||||
|
--
|
||||||
|
-- See "Foreign.Rust.Serialisation.Raw" for discussion.
|
||||||
|
module Foreign.Rust.Serialisation.Raw.Decimal (
|
||||||
|
-- * Deriving-via support
|
||||||
|
AsDecimal(..)
|
||||||
|
-- * Show instance
|
||||||
|
, asDecimal
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Data.Aeson
|
||||||
|
import Data.Aeson.Types
|
||||||
|
import Data.Typeable
|
||||||
|
import Data.Word
|
||||||
|
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
|
||||||
|
import Foreign.Rust.Serialisation.Raw
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Deriving-via combinator
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
-- | Serialise to list of decimal values
|
||||||
|
--
|
||||||
|
-- The 'Show' instance will produce something like
|
||||||
|
--
|
||||||
|
-- > asDecimal @MyType [1,2,3,4]
|
||||||
|
--
|
||||||
|
-- This depends on 'asDecimal' (defined in this module).
|
||||||
|
newtype AsDecimal a = AsDecimal { unwrapAsDecimal :: a }
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
JSON
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
instance IsRaw a => ToJSON (AsDecimal a) where
|
||||||
|
toJSON = toJSON . toBytes . unwrapAsDecimal
|
||||||
|
|
||||||
|
instance IsRaw a => FromJSON (AsDecimal a) where
|
||||||
|
parseJSON = either parseFail (return . AsDecimal) . fromBytes <=< parseJSON
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Show
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
deriving
|
||||||
|
via Structured.ToPreludeShow (AsDecimal a)
|
||||||
|
instance (Typeable a, IsRaw a) => Show (AsDecimal a)
|
||||||
|
|
||||||
|
instance (Typeable a, IsRaw a) => Structured.Show (AsDecimal a) where
|
||||||
|
toValue (AsDecimal x) =
|
||||||
|
Structured.Constr "asDecimal" [typeRep (Proxy @a)] [
|
||||||
|
Structured.toValue (toBytes x)
|
||||||
|
]
|
||||||
|
|
||||||
|
asDecimal :: IsRaw a => [Word8] -> a
|
||||||
|
asDecimal = either error id . fromBytes
|
18
test/Main.hs
Normal file
18
test/Main.hs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module Main (main) where
|
||||||
|
|
||||||
|
import Test.Tasty
|
||||||
|
|
||||||
|
import qualified Test.Serialisation.JSON
|
||||||
|
import qualified Test.Serialisation.Raw.Base16
|
||||||
|
import qualified Test.Serialisation.Raw.Base58
|
||||||
|
import qualified Test.Serialisation.Raw.Base64
|
||||||
|
import qualified Test.Serialisation.Raw.Decimal
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = defaultMain $ testGroup "foreign-rust" [
|
||||||
|
Test.Serialisation.JSON.tests
|
||||||
|
, Test.Serialisation.Raw.Base16.tests
|
||||||
|
, Test.Serialisation.Raw.Base58.tests
|
||||||
|
, Test.Serialisation.Raw.Base64.tests
|
||||||
|
, Test.Serialisation.Raw.Decimal.tests
|
||||||
|
]
|
27
test/Test/Serialisation/JSON.hs
Normal file
27
test/Test/Serialisation/JSON.hs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
module Test.Serialisation.JSON (tests) where
|
||||||
|
|
||||||
|
import Test.Tasty
|
||||||
|
import Test.Tasty.HUnit
|
||||||
|
|
||||||
|
import Test.Serialisation.Types
|
||||||
|
import Test.Util.TH
|
||||||
|
|
||||||
|
import Foreign.Rust.Serialisation.JSON (asJSON)
|
||||||
|
|
||||||
|
tests :: TestTree
|
||||||
|
tests = testGroup "Test.Serialisation.JSON" [
|
||||||
|
testCase "show" test_show
|
||||||
|
, testCase "structured" test_structured
|
||||||
|
]
|
||||||
|
|
||||||
|
test_show :: Assertion
|
||||||
|
test_show =
|
||||||
|
assertEqual "" exampleUsesJSON $
|
||||||
|
$(reparseShow exampleUsesJSON)
|
||||||
|
|
||||||
|
test_structured :: Assertion
|
||||||
|
test_structured =
|
||||||
|
assertEqual "" exampleUsesJSON $
|
||||||
|
$(reparseStructured exampleUsesJSON)
|
33
test/Test/Serialisation/Raw/Base16.hs
Normal file
33
test/Test/Serialisation/Raw/Base16.hs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
module Test.Serialisation.Raw.Base16 (tests) where
|
||||||
|
|
||||||
|
import Test.Tasty
|
||||||
|
import Test.Tasty.HUnit
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
|
||||||
|
import Test.Serialisation.Types
|
||||||
|
import Test.Util.TH
|
||||||
|
|
||||||
|
tests :: TestTree
|
||||||
|
tests = testGroup "Test.Serialisation.Raw.Base16" [
|
||||||
|
testCase "show" test_show
|
||||||
|
, testCase "structured" test_structured
|
||||||
|
, testCase "json" test_json
|
||||||
|
]
|
||||||
|
|
||||||
|
test_show :: Assertion
|
||||||
|
test_show =
|
||||||
|
assertEqual "" exampleUsesBase16 $
|
||||||
|
$(reparseShow exampleUsesBase16)
|
||||||
|
|
||||||
|
test_structured :: Assertion
|
||||||
|
test_structured =
|
||||||
|
assertEqual "" exampleUsesBase16 $
|
||||||
|
$(reparseStructured exampleUsesBase16)
|
||||||
|
|
||||||
|
test_json :: Assertion
|
||||||
|
test_json =
|
||||||
|
assertEqual "" (Right exampleUsesBase16) $
|
||||||
|
Aeson.eitherDecode $ Aeson.encode exampleUsesBase16
|
33
test/Test/Serialisation/Raw/Base58.hs
Normal file
33
test/Test/Serialisation/Raw/Base58.hs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
module Test.Serialisation.Raw.Base58 (tests) where
|
||||||
|
|
||||||
|
import Test.Tasty
|
||||||
|
import Test.Tasty.HUnit
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
|
||||||
|
import Test.Serialisation.Types
|
||||||
|
import Test.Util.TH
|
||||||
|
|
||||||
|
tests :: TestTree
|
||||||
|
tests = testGroup "Test.Serialisation.Raw.Base58" [
|
||||||
|
testCase "show" test_show
|
||||||
|
, testCase "structured" test_structured
|
||||||
|
, testCase "json" test_json
|
||||||
|
]
|
||||||
|
|
||||||
|
test_show :: Assertion
|
||||||
|
test_show =
|
||||||
|
assertEqual "" exampleUsesBase58 $
|
||||||
|
$(reparseShow exampleUsesBase58)
|
||||||
|
|
||||||
|
test_structured :: Assertion
|
||||||
|
test_structured =
|
||||||
|
assertEqual "" exampleUsesBase58 $
|
||||||
|
$(reparseStructured exampleUsesBase58)
|
||||||
|
|
||||||
|
test_json :: Assertion
|
||||||
|
test_json =
|
||||||
|
assertEqual "" (Right exampleUsesBase58) $
|
||||||
|
Aeson.eitherDecode $ Aeson.encode exampleUsesBase58
|
33
test/Test/Serialisation/Raw/Base64.hs
Normal file
33
test/Test/Serialisation/Raw/Base64.hs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
module Test.Serialisation.Raw.Base64 (tests) where
|
||||||
|
|
||||||
|
import Test.Tasty
|
||||||
|
import Test.Tasty.HUnit
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
|
||||||
|
import Test.Serialisation.Types
|
||||||
|
import Test.Util.TH
|
||||||
|
|
||||||
|
tests :: TestTree
|
||||||
|
tests = testGroup "Test.Serialisation.Raw.Base64" [
|
||||||
|
testCase "show" test_show
|
||||||
|
, testCase "structured" test_structured
|
||||||
|
, testCase "json" test_json
|
||||||
|
]
|
||||||
|
|
||||||
|
test_show :: Assertion
|
||||||
|
test_show =
|
||||||
|
assertEqual "" exampleUsesBase64 $
|
||||||
|
$(reparseShow exampleUsesBase64)
|
||||||
|
|
||||||
|
test_structured :: Assertion
|
||||||
|
test_structured =
|
||||||
|
assertEqual "" exampleUsesBase64 $
|
||||||
|
$(reparseStructured exampleUsesBase64)
|
||||||
|
|
||||||
|
test_json :: Assertion
|
||||||
|
test_json =
|
||||||
|
assertEqual "" (Right exampleUsesBase64) $
|
||||||
|
Aeson.eitherDecode $ Aeson.encode exampleUsesBase64
|
35
test/Test/Serialisation/Raw/Decimal.hs
Normal file
35
test/Test/Serialisation/Raw/Decimal.hs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
module Test.Serialisation.Raw.Decimal (tests) where
|
||||||
|
|
||||||
|
import Test.Tasty
|
||||||
|
import Test.Tasty.HUnit
|
||||||
|
|
||||||
|
import qualified Data.Aeson as Aeson
|
||||||
|
|
||||||
|
import Test.Serialisation.Types
|
||||||
|
import Test.Util.TH
|
||||||
|
|
||||||
|
import Foreign.Rust.Serialisation.Raw.Decimal (asDecimal)
|
||||||
|
|
||||||
|
tests :: TestTree
|
||||||
|
tests = testGroup "Test.Serialisation.Raw.Decimal" [
|
||||||
|
testCase "show" test_show
|
||||||
|
, testCase "structured" test_structured
|
||||||
|
, testCase "json" test_json
|
||||||
|
]
|
||||||
|
|
||||||
|
test_show :: Assertion
|
||||||
|
test_show =
|
||||||
|
assertEqual "" exampleUsesDecimal $
|
||||||
|
$(reparseShow exampleUsesDecimal)
|
||||||
|
|
||||||
|
test_structured :: Assertion
|
||||||
|
test_structured =
|
||||||
|
assertEqual "" exampleUsesDecimal $
|
||||||
|
$(reparseStructured exampleUsesDecimal)
|
||||||
|
|
||||||
|
test_json :: Assertion
|
||||||
|
test_json =
|
||||||
|
assertEqual "" (Right exampleUsesDecimal) $
|
||||||
|
Aeson.eitherDecode $ Aeson.encode exampleUsesDecimal
|
99
test/Test/Serialisation/Types.hs
Normal file
99
test/Test/Serialisation/Types.hs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
-- | Example types
|
||||||
|
--
|
||||||
|
-- Defined as separate module to avoid TH stage restrictions
|
||||||
|
module Test.Serialisation.Types (
|
||||||
|
UsesDecimal(..)
|
||||||
|
, exampleUsesDecimal
|
||||||
|
, UsesBase16(..)
|
||||||
|
, exampleUsesBase16
|
||||||
|
, UsesBase58(..)
|
||||||
|
, exampleUsesBase58
|
||||||
|
, UsesBase64(..)
|
||||||
|
, exampleUsesBase64
|
||||||
|
, UsesJSON(..)
|
||||||
|
, exampleUsesJSON
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Aeson
|
||||||
|
import Data.Aeson.QQ.Simple
|
||||||
|
import Data.String
|
||||||
|
import Data.Word
|
||||||
|
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
|
||||||
|
import Foreign.Rust.Serialisation.JSON
|
||||||
|
import Foreign.Rust.Serialisation.Raw
|
||||||
|
import Foreign.Rust.Serialisation.Raw.Base16
|
||||||
|
import Foreign.Rust.Serialisation.Raw.Base58
|
||||||
|
import Foreign.Rust.Serialisation.Raw.Base64
|
||||||
|
import Foreign.Rust.Serialisation.Raw.Decimal
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
AsDecimal
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
newtype UsesDecimal = UsesDecimal [Word8]
|
||||||
|
deriving stock (Eq)
|
||||||
|
deriving newtype (IsRaw)
|
||||||
|
deriving (Show, Structured.Show) via AsDecimal UsesDecimal
|
||||||
|
deriving (FromJSON, ToJSON) via AsDecimal UsesDecimal
|
||||||
|
|
||||||
|
exampleUsesDecimal :: UsesDecimal
|
||||||
|
exampleUsesDecimal = UsesDecimal [1, 2, 3, 4]
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
AsBase16
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
newtype UsesBase16 = UsesBase16 [Word8]
|
||||||
|
deriving stock (Eq)
|
||||||
|
deriving newtype (IsRaw)
|
||||||
|
deriving (Show, Structured.Show, IsString) via AsBase16 UsesBase16
|
||||||
|
deriving (FromJSON, ToJSON) via AsBase16 UsesBase16
|
||||||
|
|
||||||
|
exampleUsesBase16 :: UsesBase16
|
||||||
|
exampleUsesBase16 = UsesBase16 [1, 2, 3, 4]
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
AsBase58
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
newtype UsesBase58 = UsesBase58 [Word8]
|
||||||
|
deriving stock (Eq)
|
||||||
|
deriving newtype (IsRaw)
|
||||||
|
deriving (Show, Structured.Show, IsString) via AsBase58 UsesBase58
|
||||||
|
deriving (FromJSON, ToJSON) via AsBase58 UsesBase58
|
||||||
|
|
||||||
|
exampleUsesBase58 :: UsesBase58
|
||||||
|
exampleUsesBase58 = UsesBase58 [1, 2, 3, 4]
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
AsBase64
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
newtype UsesBase64 = UsesBase64 [Word8]
|
||||||
|
deriving stock (Eq)
|
||||||
|
deriving newtype (IsRaw)
|
||||||
|
deriving (Show, Structured.Show, IsString) via AsBase64 UsesBase64
|
||||||
|
deriving (FromJSON, ToJSON) via AsBase64 UsesBase64
|
||||||
|
|
||||||
|
exampleUsesBase64 :: UsesBase64
|
||||||
|
exampleUsesBase64 = UsesBase64 [1, 2, 3, 4]
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
AsJSON
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
newtype UsesJSON = UsesJSON Value
|
||||||
|
deriving stock (Eq)
|
||||||
|
deriving newtype (ToJSON, FromJSON)
|
||||||
|
deriving (Show, Structured.Show) via AsJSON UsesJSON
|
||||||
|
|
||||||
|
exampleUsesJSON :: UsesJSON
|
||||||
|
exampleUsesJSON = UsesJSON [aesonQQ|
|
||||||
|
{ "a": null
|
||||||
|
, "b": [1, 2, 3]
|
||||||
|
}
|
||||||
|
|]
|
81
test/Test/Util/TH.hs
Normal file
81
test/Test/Util/TH.hs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
module Test.Util.TH (
|
||||||
|
reparseShow
|
||||||
|
, reparseStructured
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.String (fromString)
|
||||||
|
|
||||||
|
import qualified Data.Structured as Structured
|
||||||
|
import qualified Language.Haskell.TH as TH
|
||||||
|
import qualified Language.Haskell.Exts as Exts
|
||||||
|
import Language.Haskell.TH (Q)
|
||||||
|
import Language.Haskell.TH.Quote
|
||||||
|
import Data.Aeson.QQ.Simple
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Parse expressions
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
parseExp :: String -> Q TH.Exp
|
||||||
|
parseExp =
|
||||||
|
toExp
|
||||||
|
. Exts.fromParseResult
|
||||||
|
. Exts.parseExpWithMode parseMode
|
||||||
|
where
|
||||||
|
parseMode :: Exts.ParseMode
|
||||||
|
parseMode = Exts.defaultParseMode {
|
||||||
|
Exts.extensions = [
|
||||||
|
Exts.EnableExtension Exts.OverloadedStrings
|
||||||
|
, Exts.EnableExtension Exts.TypeApplications
|
||||||
|
, Exts.EnableExtension Exts.QuasiQuotes
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
reparseShow :: Show a => a -> Q TH.Exp
|
||||||
|
reparseShow = parseExp . show
|
||||||
|
|
||||||
|
reparseStructured :: Structured.Show a => a -> Q TH.Exp
|
||||||
|
reparseStructured = parseExp . Structured.show
|
||||||
|
|
||||||
|
{-------------------------------------------------------------------------------
|
||||||
|
Translate haskell-src-exts @Exp@ to TH @Exp@
|
||||||
|
|
||||||
|
There is a package for this (@haskell-src-meta@), but it does not support
|
||||||
|
overloaded string nor quasi-quotes, which makes it rather useless for our
|
||||||
|
purposes. We only need to support a tiny handful of expressions, so we just
|
||||||
|
define it ourselves.
|
||||||
|
-------------------------------------------------------------------------------}
|
||||||
|
|
||||||
|
toExp :: Exts.Exp Exts.SrcSpanInfo -> Q TH.Exp
|
||||||
|
toExp = \case
|
||||||
|
|
||||||
|
-- Standard instances
|
||||||
|
-- (These would presumably be similar in haskell-src-meta)
|
||||||
|
|
||||||
|
Exts.Var _ (Exts.UnQual _ (Exts.Ident _ n)) ->
|
||||||
|
pure $ TH.VarE $ TH.mkName n
|
||||||
|
Exts.App _ e (Exts.TypeApp _ (Exts.TyCon _ (Exts.UnQual _ (Exts.Ident _ n)))) ->
|
||||||
|
TH.AppTypeE <$> toExp e <*> pure (TH.ConT (TH.mkName n))
|
||||||
|
Exts.App _ e1 e2 ->
|
||||||
|
TH.AppE <$> toExp e1 <*> toExp e2
|
||||||
|
Exts.List _ es ->
|
||||||
|
TH.ListE <$> mapM toExp es
|
||||||
|
Exts.Lit _ (Exts.Int _ x _) ->
|
||||||
|
pure $ TH.LitE (TH.IntegerL x)
|
||||||
|
|
||||||
|
-- Overloaded strings
|
||||||
|
|
||||||
|
Exts.Lit _ (Exts.String _ x _) ->
|
||||||
|
pure $ TH.AppE (TH.VarE 'fromString) (TH.LitE (TH.StringL x))
|
||||||
|
|
||||||
|
-- Quasi-quotes
|
||||||
|
|
||||||
|
Exts.QuasiQuote _ "aesonQQ" str ->
|
||||||
|
quoteExp aesonQQ str
|
||||||
|
|
||||||
|
-- Anything else is urecognized
|
||||||
|
|
||||||
|
e -> fail $ "toExp: unrecognized expression " ++ show e
|
||||||
|
|
Loading…
Reference in a new issue