Skip to content

Commit

Permalink
Merge pull request #3 from brandonchinn178/docs
Browse files Browse the repository at this point in the history
Update docs, add release workflow
  • Loading branch information
brandonchinn178 committed Jun 7, 2022
2 parents 4a2e62a + 3d09ec9 commit baf6c0b
Show file tree
Hide file tree
Showing 12 changed files with 463 additions and 14 deletions.
22 changes: 21 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
name: CI
on: push
on:
pull_request:
push:
branches:
- main
workflow_call:

jobs:
build_and_test:
Expand Down Expand Up @@ -67,3 +72,18 @@ jobs:
chmod +x /usr/local/bin/fourmolu
- name: Run fourmolu
run: fourmolu -m check $(git ls-files '*.hs')

check_sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/cache@v3
with:
path: ~/.stack
key: ${{ runner.os }}-check_sdist-${{ hashFiles('stack.yaml') }}
- name: Create sdist bundle
run: stack sdist --test-tarball --tar-dir .
- uses: actions/upload-artifact@v3
with:
name: toml-reader-sdist
path: toml-reader-*.tar.gz
42 changes: 42 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Release
on: workflow_dispatch

jobs:
ci:
uses: ./.github/workflows/ci.yml

release:
runs-on: ubuntu-latest
needs:
- ci

steps:
- uses: actions/checkout@v2
with:
ref: main

- uses: actions/download-artifact@v3
with:
name: toml-reader-sdist
path: ./sdist/

- name: Load package version
run: scripts/GetVersion.hs
id: version_info

- name: Load Hackage token secret name
run: |
import re
username = "${{ github.actor }}"
secret_name = "HACKAGE_TOKEN_" + re.sub(r"\W+", "_", username).upper()
print(f"::set-output name=secret_name::{secret_name}")
shell: python
id: hackage_token_secret

- name: Make release
run: scripts/make-release.sh
env:
gh_token: ${{ secrets.GITHUB_TOKEN }}
hackage_token: ${{ secrets[steps.hackage_token_secret.outputs.secret_name] }}
version: ${{ steps.version_info.outputs.version }}
sdistdir: ./sdist/
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## v0.1.0.0

Initial release
80 changes: 78 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
# toml-reader

TOML format parser compliant with v1.0.0.
[![](https://img.shields.io/github/workflow/status/brandonchinn178/toml-reader/CI/main)](https://github.com/brandonchinn178/toml-reader/actions)
[![](https://img.shields.io/codecov/c/gh/brandonchinn178/toml-reader)](https://app.codecov.io/gh/brandonchinn178/toml-reader)
[![](https://img.shields.io/hackage/v/toml-reader)](https://hackage.haskell.org/package/toml-reader)

TOML format parser compliant with [v1.0.0](https://toml.io/en/v1.0.0) (verified with the [`toml-test`](https://github.com/BurntSushi/toml-test) tool).

## Usage

TODO
```hs
data MyConfig = MyConfig
{ field1 :: Int
, field2 :: Bool
}

instance DecodeTOML MyConfig where
tomlDecoder =
MyConfig
<$> getField "field1"
<*> getField "field2"

main :: IO ()
main = do
result <- decodeFile "config.toml"
case result of
Right cfg -> print (cfg :: MyConfig)
Left e -> print e
```

## Design decisions

Expand All @@ -26,3 +48,57 @@ TODO
Since reading/writing isn't an idempotent operation, this library won't even pretend to provide `DecodeTOML`/`EncodeTOML` typeclasses that imply that they're inverses of each other.

Hopefully some other `toml-writer` library may come along to make it easy to specify how to format your data in TOML (e.g. a combinator for `table` vs `inlineTable`), or you could use [`tomland`](https://github.com/kowainik/tomland).

* This library defines `DecodeTOML` with an opaque `Decoder a` as opposed to a `Value -> DecodeM a` function, like `aeson` does. In my opinion, this makes the common case of decoding config files much more straightforward, especially around nested fields, which are much more common in TOML than JSON. e.g.

```hs
-- aeson-like
instance DecodeTOML MyConfig where
decodeTOML :: Value -> DecodeM MyConfig
decodeTOML = withObject "MyConfig" $ \o ->
MyConfig
<$> o .: "field1"
<*> (o .: "field2" >>= (.: "field3"))
```

```hs
-- with toml-parser
instance DecodeTOML MyConfig where
tomlDecoder :: Decoder MyConfig
tomlDecoder =
MyConfig
<$> getField "field1"
<*> getFields ["field2", "field3"]
```

It also makes it easy to define ad-hoc decoders:

```hs
instance DecodeTOML MyConfig where
tomlDecoder = ...

alternativeDecoder :: Decoder MyConfig
alternativeDecoder = ...

-- uses tomlDecoder
decode "a = 1"

-- uses explicit decoder
decodeWith alternativeDecoder "a = 1"
```

As a bonus, it also makes for a less point-free interface when defining a decoder based on another decoder, which is kinda cool:

```hs
-- aeson-like
instance DecodeTOML MyString where
decodeTOML = fmap toMyString . decodeTOML
```

```hs
-- with toml-parser
instance DecodeTOML MyString where
tomlDecoder = toMyString <$> tomlDecoder
```

Ultimately, `Decoder` is just a newtype around `Value -> DecodeM a`, so we could always go back to it. Originally, I wanted to do something like [`jordan`](https://hackage.haskell.org/package/jordan), where this interface is required due to the way it parses and deserializes at the same time, but this isn't possible with TOML due to the way TOML needs to be normalized.
1 change: 1 addition & 0 deletions scripts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv
21 changes: 21 additions & 0 deletions scripts/GetVersion.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env stack
{- stack runghc --package Cabal -}

import Data.List (intercalate)
import Distribution.Package (packageVersion)
import Distribution.PackageDescription.Parsec (readGenericPackageDescription)
import qualified Distribution.Verbosity as Verbosity
import Distribution.Version (versionNumbers)

main :: IO ()
main = do
packageDesc <- readGenericPackageDescription Verbosity.silent "toml-reader.cabal"
let version = intercalate "." . map show . versionNumbers . packageVersion $ packageDesc
setOutput "version" version

{- |
Set output for a GitHub action.
https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter
-}
setOutput :: String -> String -> IO ()
setOutput name value = putStrLn $ "::set-output name=" ++ name ++ "::" ++ value
11 changes: 11 additions & 0 deletions scripts/make-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env bash

set -euxo pipefail
HERE="$(builtin cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

if [[ ! -d "${HERE}/.venv" ]]; then
python3 -m venv "${HERE}/.venv"
"${HERE}/.venv/bin/pip" install requests
fi

exec "${HERE}/.venv/bin/python3" "${HERE}/make_release.py" "$@"
133 changes: 133 additions & 0 deletions scripts/make_release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# pyright: strict, reportUnknownMemberType=false

from __future__ import annotations

import itertools
import json
import logging
import os
import requests
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)


def main():
gh_token = os.environ["gh_token"]
hackage_token = os.environ["hackage_token"]
version = os.environ["version"]
sdistdir = os.environ["sdistdir"]
repo = os.environ["GITHUB_REPOSITORY"]
sha = os.environ["GITHUB_SHA"]

version_name = f"v{version}"

# check inputs
if not hackage_token:
raise Exception(
"Hackage token is not provided (did you add a Secret of the form HACKAGE_TOKEN_<github username>?)"
)

# ensure release files exist
sdist_archive = Path(sdistdir) / f"toml-reader-{version}.tar.gz"
if not sdist_archive.exists():
raise Exception(f"File does not exist: {sdist_archive}")

logger.info(f"Creating release {version_name}")

# check + parse CHANGELOG
changelog = Path("CHANGELOG.md").read_text()
if not changelog.startswith(f"## {version_name}"):
raise Exception("CHANGELOG doesn't look updated")
version_changes = get_version_changes(changelog)

create_github_release(
repo=repo,
token=gh_token,
sha=sha,
version_name=version_name,
version_changes=version_changes,
)

# uploading as candidate because uploads are irreversible, unlike
# GitHub releases, so just to be extra sure, we'll upload this as
# a candidate and manually confirm uploading the package on Hackage
upload_hackage_candidate(
token=hackage_token,
archive=sdist_archive,
)

logger.info(f"Released toml-reader {version_name}!")


def get_version_changes(changelog: str) -> str:
lines = changelog.split("\n")

# skip initial '## vX.Y.Z' line
lines = lines[1:]

# take lines until the next '## vX.Y.Z' line
lines = itertools.takewhile(lambda line: not line.startswith("## v"), lines)

return "\n".join(lines)


def create_github_release(
*,
repo: str,
token: str,
sha: str,
version_name: str,
version_changes: str,
):
session = init_session()
session.headers["Accept"] = "application/vnd.github.v3+json"
session.headers["Authorization"] = f"token {token}"
session.headers["User-Agent"] = repo

payload = {
"tag_name": version_name,
"target_commitish": sha,
"name": version_name,
"body": version_changes,
}
logger.debug(f"Creating release with: {json.dumps(payload)}")

session.post(
f"https://api.github.com/repos/{repo}/releases",
json=payload,
)


def upload_hackage_candidate(
*,
token: str,
archive: Path,
):
session = init_session()
with archive.open("rb") as f:
session.post(
"https://hackage.haskell.org/packages/candidates",
headers={"Authorization": f"X-ApiKey {token}"},
files={"package": f},
)


def init_session() -> requests.Session:
session = requests.Session()

def _check_status(r: requests.Response, *args: Any, **kwargs: Any):
r.raise_for_status()

# https://github.com/python/typeshed/issues/7776
session.hooks["response"].append( # pyright: ignore[reportFunctionMemberAccess]
_check_status,
)

return session


if __name__ == "__main__":
main()
1 change: 0 additions & 1 deletion src/TOML.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ module TOML (
invalidValue,
typeMismatch,
decodeFail,
decodeError,

-- * TOML types
Value (..),
Expand Down

0 comments on commit baf6c0b

Please sign in to comment.