Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kafka streaming, projection state recovery improvements, documentation, code coverage #110

Merged
merged 136 commits into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
136 commits
Select commit Hold shift + click to select a range
1009277
x/projection: fiddling with prototype
widmogrod Jan 10, 2024
f51fa8d
x/projection: introduce DoLoad, DoMap, DoJoin, DoSink functions
widmogrod Jan 10, 2024
4612b59
x/stream: new package draft
widmogrod Jan 10, 2024
f7eace2
x/projection: use x/stream
widmogrod Jan 10, 2024
7b3e9b7
x/stream: make API more robust
widmogrod Jan 10, 2024
bd1b992
x/stream: nil PullCMD then assume from beginning
widmogrod Jan 10, 2024
fc6015d
x/stream: error naming is better
widmogrod Jan 10, 2024
452fb49
x/projection: use better stream API
widmogrod Jan 10, 2024
3f5afd1
x/stream: rename files
widmogrod Jan 10, 2024
49877fd
x/stream: empty command is an error
widmogrod Jan 10, 2024
d14a803
x/search: simple implementation of search algorithms
widmogrod Jan 10, 2024
8dbe640
cmd/mkunion: mkunion:",noserde" option is supported
widmogrod Jan 12, 2024
6e0d62e
x/shape: supports TypeParameters in UnionLike
widmogrod Jan 12, 2024
e154409
x/generators: generate type parameters in UnionLike
widmogrod Jan 12, 2024
a85c177
x/generators: generate visitor and match function for Union with gene…
widmogrod Jan 12, 2024
8668343
x/generators: example test that generate union for generics
widmogrod Jan 12, 2024
ee5fa5e
x/shape: regenerate and update test
widmogrod Jan 12, 2024
a329219
example: ast remove -skip-extension
widmogrod Jan 12, 2024
0ab3673
x/projection: refactor using Record[A] Either[A,B] types
widmogrod Jan 13, 2024
bbcd865
x/shape: AliasLike now support TypeParams
widmogrod Jan 13, 2024
fdd0103
x/generators: shape and visitor support type aliases
widmogrod Jan 13, 2024
23bf774
x/generators: shape renders type params
widmogrod Jan 13, 2024
4742b3d
x/shape: regenerate + update test
widmogrod Jan 13, 2024
5ef56c4
x/projection: use generic support in mkunion
widmogrod Jan 13, 2024
dd388a7
x/stream: now supports event time
widmogrod Jan 13, 2024
b68c908
x/projection: assert order of events and details of specific events
widmogrod Jan 13, 2024
e3cdd96
x/stream: regenerate model
widmogrod Jan 13, 2024
4e0557f
x/projection: introduce windowing primitive
widmogrod Jan 13, 2024
68a094e
x/projection: simplify WindowByKey object and DoWindow function
widmogrod Jan 13, 2024
359ba1a
x/projection: simplify join context
widmogrod Jan 13, 2024
e12165b
x/projection: sketch state snapshots
widmogrod Jan 14, 2024
c7a4966
x/projection: refactor DoLoad to work on Record
widmogrod Jan 14, 2024
35126a7
x/projection: refactor DoMap to work on Record
widmogrod Jan 14, 2024
07f7761
x/projection: decouple x/stream form Record definition
widmogrod Jan 14, 2024
5d90d48
x/stream: probability of failure
widmogrod Jan 14, 2024
35aa4f7
x/projection: test recovery in case of random failure
widmogrod Jan 14, 2024
24f3baa
x/stream: support for topics and key validation
widmogrod Jan 14, 2024
2e55f59
x/projection: ugly changes to support topic names
widmogrod Jan 14, 2024
ec60d7a
x/shape: introduce NameToPrimitiveShape
widmogrod Jan 16, 2024
b8841a8
x/shape: FromAST use NameToPrimitiveShape
widmogrod Jan 16, 2024
608ee86
x/shape: LookupShape use NameToPrimitiveShape
widmogrod Jan 16, 2024
e51d352
x/shape: IndexWith index in addition Alias and Union
widmogrod Jan 16, 2024
6ffb90c
x/schema: {To,From}GoReflect use shape.IndexWith
widmogrod Jan 16, 2024
6d97c11
x/projection: DoWindow now use repository to store windows
widmogrod Jan 16, 2024
28178e8
x/projection: Introduce WindowFlushMode
widmogrod Jan 20, 2024
1229214
x/schema: introduce pointer support
widmogrod Jan 20, 2024
562e0d9
x/projection: introduce TriggerDescription support
widmogrod Jan 20, 2024
f3c382c
x/projection: refactor recovery function
widmogrod Jan 20, 2024
70e562f
x/schema: make FromGoReflect more strict
widmogrod Jan 20, 2024
efaa49a
x/projection: refactoring of SnapshotState as union
widmogrod Jan 20, 2024
07ae14e
x/projection: NewJoinInMemoryContext use *JoinContextState
widmogrod Jan 22, 2024
f8fe28b
x/storage: UpdateRecords return now modified records
widmogrod Jan 26, 2024
9305de1
x/workflow: now use new x/storage API
widmogrod Jan 26, 2024
5e685cb
x/taskqueue: now use new x/storage API
widmogrod Jan 26, 2024
f829fbb
x/projection: improved snapshotting even in load function
widmogrod Jan 26, 2024
ca5a06b
my-app: now use new x/storage API
widmogrod Jan 26, 2024
e94c7cc
x/storage: update generated files
widmogrod Jan 26, 2024
d16d084
x/storage: window recovery draft
widmogrod Jan 26, 2024
da78876
x/stream: introduce typed streams
widmogrod Jan 26, 2024
fc6e5c0
x/stream: TypeStreamTopic return TopicName()
widmogrod Jan 26, 2024
4ddf5ac
x/projection: refactor HappyPath test to use typed topics
widmogrod Jan 26, 2024
9c62ee9
x/projection: refactor Recovery path test to use typed topics
widmogrod Jan 26, 2024
20f72fa
x/projection: make window tests deterministic
widmogrod Jan 27, 2024
33f876f
x/stream: better coverage of tests
widmogrod Jan 28, 2024
e504da4
x/stream: fix usage of ErrorAs
widmogrod Jan 28, 2024
43e15d6
x/*: fix ussage of ErrorAs in tests
widmogrod Jan 28, 2024
2e3474f
x/stream: change name of ErrEndOfSteam to ErrNoMoreNewDataInStream to…
widmogrod Jan 28, 2024
14e634f
x/projection: Window recovery
widmogrod Jan 28, 2024
035ed59
go1.21
widmogrod Jan 29, 2024
cdc0fcb
x/shape: introduce well tested implementation of MkRefNameFromString
widmogrod Jan 29, 2024
422d820
x/shape: InstantiateTypeThatAreOvershadowByTypeParam supports UnionLi…
widmogrod Jan 29, 2024
1a50a14
x/shape: improve ToGoFullTypeNameFromReflect
widmogrod Jan 29, 2024
421ba68
x/schema: hacky but working implementation for finding union types li…
widmogrod Jan 29, 2024
f2396cd
x/schema: clean implementation of FromGoReflect
widmogrod Jan 29, 2024
1f54b95
x/projection: DoWindow implementation that pass Recovery tests
widmogrod Jan 29, 2024
24321f7
x/projection: Recovery more logging
widmogrod Jan 29, 2024
473bdb2
x/projection: refactor of joining
widmogrod Jan 29, 2024
af6543f
x/projection: tests refactor and passing every time
widmogrod Jan 29, 2024
ed99b5e
x/projection: serde generation as workaround missing better type regi…
widmogrod Jan 29, 2024
10655d2
x/generators: serde for union refactor from template to programmatic …
widmogrod Feb 3, 2024
36793fb
x/generators: fix missing comma for multiple types
widmogrod Feb 3, 2024
12c9661
x/*: regenerate
widmogrod Feb 3, 2024
ea9e5f3
x/shape: FindInstantiationsOf & tests
widmogrod Feb 3, 2024
06e609c
x/shape: FindInstantiationsOf now returns []*RefName
widmogrod Feb 3, 2024
9e7f712
x/shape: Extract indexed types walking AST
widmogrod Feb 4, 2024
ce424be
x/shape: remove FindInstantiationsOf is not needed
widmogrod Feb 4, 2024
0a6720b
x/shape: extract indexed types from function bodies
widmogrod Feb 4, 2024
6ce7380
x/shape: FromAST less panics
widmogrod Feb 4, 2024
54f1843
x/shape: RetrieveShapeNamedAs search structs and unions
widmogrod Feb 5, 2024
33d1c41
x/shape: InferredInfo can return FileName()
widmogrod Feb 5, 2024
b5b96c0
x/shape: ToGoTypeName tests
widmogrod Feb 5, 2024
ffa03f9
x/shape: IndexedTypeWalker and remove old FindInstantiationOf
widmogrod Feb 6, 2024
7ea36d1
x/shape: ToGoTypeName now prints nicer type parameters and indexed ty…
widmogrod Feb 6, 2024
72fffb0
x/shape: new type for testing
widmogrod Feb 6, 2024
6dd5b1c
x/projection: now leverages generated types
widmogrod Feb 6, 2024
641a8a9
x/generators: refactor TemplateHelperShapeVariantToName
widmogrod Feb 6, 2024
9f014ea
x/generators: serde fix for type params rendering in constructionf
widmogrod Feb 6, 2024
bff0008
cmd/mkunion: now generates registry of types that are instantiated in…
widmogrod Feb 6, 2024
44d824d
x/schema: To/FromGoReflection use just type name (without type params…
widmogrod Feb 6, 2024
627b857
x/shared: introduce TypeRegistryStore to complement type generation
widmogrod Feb 6, 2024
8651267
cmd/mkunion: -type-registry flag
widmogrod Feb 6, 2024
b865eba
x/shape: go back from shape.Name to shape.ToGoTypeName for variant name
widmogrod Feb 6, 2024
cc34e8d
x/projection: use mkunion -type-registry
widmogrod Feb 6, 2024
80b69c5
dev: introduce kafka and kafka-ui
widmogrod Feb 11, 2024
528d7a7
x/stream: refactor OffsetCompare and introduce specification for comp…
widmogrod Feb 11, 2024
6a18dd8
x/stream: introduce specification for happy path stream behaviour
widmogrod Feb 11, 2024
e9aa293
x/stream: introduce Kafka implementation
widmogrod Feb 11, 2024
4fb878a
go.mod add kafka
widmogrod Feb 11, 2024
dc5a9b8
3 partitions in kafka
widmogrod Feb 11, 2024
77a35f9
update function doc
widmogrod Feb 11, 2024
42a6dd2
x/projection: use stream.OffsetCompare
widmogrod Feb 11, 2024
b2cb784
x/projection: Recovery load, change offset
widmogrod Feb 11, 2024
49f816f
x/shape: ExpandedShapes and other changes to make sure that indexed t…
widmogrod Feb 16, 2024
20b0039
x/generators: tags variant types
widmogrod Feb 16, 2024
05bb7f0
cmd/mkunion: leverages expanded shapes
widmogrod Feb 16, 2024
ac5557e
x/projection: fix issue with explicit type declaration (for variants …
widmogrod Feb 16, 2024
c5a3917
x/*: regenerate everything from scratch
widmogrod Feb 16, 2024
446bc86
x/shape: simplify how directories are walked
widmogrod Feb 16, 2024
a758ab2
x/shape: refactor pkgImportName extraction
widmogrod Feb 16, 2024
1caaf79
x/shape: new test case that eliminates extraction of type parameters
widmogrod Feb 17, 2024
6e8e925
x/.../typedful: remove dot import alias
widmogrod Feb 17, 2024
8b02715
f: introduce Some, Either, Result data types
widmogrod Feb 24, 2024
31cb5c6
my-app: remove generated.go
widmogrod Feb 24, 2024
13c9684
x/projection: refactor state managment add AckOffset and AckWatermark
widmogrod Feb 25, 2024
d937445
x/projection: refactor snapshots, refactor stream to use shape.Shape,…
widmogrod Feb 26, 2024
c53aaab
x/projection: remove todo section
widmogrod Feb 26, 2024
fb09340
introduce codecov
widmogrod Mar 1, 2024
73be91c
docs/index and getting started
widmogrod Mar 3, 2024
7f5730c
introduce mkdocs
widmogrod Mar 3, 2024
20656c5
cmd/mkunion: update descriptions of actions
widmogrod Mar 3, 2024
49c4044
example/vehicle: cleanup
widmogrod Mar 3, 2024
be82a54
x/shared: JSONMarshal now has type inference
widmogrod Mar 3, 2024
7685113
.github: race and code coverage profile can fail
widmogrod Mar 3, 2024
37a49db
introduce documentation for development and example of generic_union
widmogrod Mar 3, 2024
4e25aa8
update generic_union.md
widmogrod Mar 3, 2024
ac3378c
introduce roadmap
widmogrod Mar 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ^1.19
go-version: ^1.21
id: go

- run: cd cmd/mkunion; go get -v -t -d ./...
Expand Down Expand Up @@ -39,3 +39,12 @@ jobs:
echo "Go tests failed after all retries."
exit 1
fi

- run: |
go test -race -coverprofile=coverage.out -covermode=atomic ./... || true

- name: Upload coverage reports to Codecov
uses: codecov/[email protected]
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: widmogrod/mkunion
29 changes: 29 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: docs
on:
push:
branches:
- master
- main
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure Git Credentials
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
- uses: actions/setup-python@v5
with:
python-version: 3.x
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
key: mkdocs-material-${{ env.cache_id }}
path: .cache
restore-keys: |
mkdocs-material-
- run: pip install mkdocs-material
- run: mkdocs gh-deploy --force
117 changes: 101 additions & 16 deletions cmd/mkunion/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"os/signal"
"path"
"sort"
"strings"
"syscall"
)
Expand All @@ -26,7 +27,7 @@ func main() {
var app *cli.App
app = &cli.App{
Name: shared.Program,
Description: "VisitorGenerator union type and visitor pattern gor golang",
Description: "Strongly typed union type in golang.",
EnableBashCompletion: true,
UseShortOptionHandling: true,
Flags: []cli.Flag{
Expand All @@ -35,17 +36,6 @@ func main() {
Aliases: []string{"n"},
Required: false,
},
&cli.StringFlag{
Name: "skip-extension",
Aliases: []string{"skip-ext"},
Value: "",
Required: false,
},
&cli.StringFlag{
Name: "include-extension",
Aliases: []string{"inc-ext"},
Required: false,
},
&cli.StringSliceFlag{
Name: "input-go-file",
Aliases: []string{"i", "input"},
Expand All @@ -59,8 +49,8 @@ func main() {
Value: false,
},
&cli.BoolFlag{
Name: "no-compact",
Required: false,
Name: "type-registry",
Value: false,
},
},
Action: func(c *cli.Context) error {
Expand All @@ -82,13 +72,19 @@ func main() {
cli.ShowAppHelpAndExit(c, 1)
}

packages := make(map[string]*shape.InferredInfo)

for _, sourcePath := range sourcePaths {
// file name without extension
inferred, err := shape.InferFromFile(sourcePath)
if err != nil {
return err
}

if _, ok := packages[inferred.PackageImportName()]; !ok {
packages[inferred.PackageImportName()] = inferred
}

contents, err := GenerateUnions(inferred)
if err != nil {
return fmt.Errorf("failed generating union in %s: %w", sourcePath, err)
Expand Down Expand Up @@ -118,14 +114,42 @@ func main() {
if err != nil {
return fmt.Errorf("failed saving shape in %s: %w", sourcePath, err)
}
}

if c.Bool("type-registry") {
for _, inferred := range packages {
dir := path.Dir(inferred.FileName())

// walk through all *.go files in the same directory
// and generate type registry for all inferred packages
// in the same directory

indexed, err := shape.NewIndexTypeInDir(dir)
if err != nil {
return fmt.Errorf("mkunion: failed indexing types in directory %s: %w", dir, err)
}

if len(indexed.IndexedShapes()) > 0 {
contents, err := GenerateTypeRegistry(indexed)
if err != nil {
return fmt.Errorf("mkunion: failed walking through directory %s: %w", dir, err)
}

regPath := path.Join(dir, "types.go")
err = SaveFile(contents, regPath, "reg_gen")
if err != nil {
return fmt.Errorf("mkunion: failed saving type registry in %s: %w", regPath, err)
}
}
}
}

return nil
},
Commands: []*cli.Command{
{
Name: "match",
Name: "match",
Description: "Generate custom pattern matching function",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Expand Down Expand Up @@ -172,7 +196,8 @@ func main() {
},
},
{
Name: "shape-export",
Name: "shape-export",
Description: "Generate typescript types from golang types, and enable end-to-end type safety.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "language",
Expand Down Expand Up @@ -270,6 +295,10 @@ func GenerateUnions(inferred *shape.InferredInfo) (bytes.Buffer, error) {
}
shapesContents.Write(contents)

if shape.TagHasOption(union.Tags, "mkunion", "noserde") {
continue
}

genSerde := generators.NewSerdeJSONUnion(union)
genSerde.SkipImportsAndPackage(true)

Expand Down Expand Up @@ -485,3 +514,59 @@ func SaveFile(contents bytes.Buffer, sourcePath string, infix string) error {
}
return nil
}

func GenerateTypeRegistry(inferred *shape.IndexedTypeWalker) (bytes.Buffer, error) {
packageName := inferred.PackageName()

contents := bytes.Buffer{}
contents.WriteString("// Code generated by mkunion. DO NOT EDIT.\n")
contents.WriteString(fmt.Sprintf("package %s\n\n", packageName))

found := inferred.ExpandedShapes()
if len(found) == 0 {
return contents, nil
}

sortedKeys := make([]string, 0, len(found))
for k := range found {
sortedKeys = append(sortedKeys, k)
}

sort.Strings(sortedKeys)

maps := []generators.PkgMap{
{
"shared": "github.com/widmogrod/mkunion/x/shared",
},
}
for _, key := range sortedKeys {
inst := found[key]
next := shape.ExtractPkgImportNamesForTypeInitialisation(inst)
maps = append(maps, next)
}
pkgMap := generators.MergePkgMaps(maps...)
delete(pkgMap, packageName)

contents.WriteString(generators.GenerateImports(pkgMap))

contents.WriteString("func init() {\n")
// generate type registry

for _, key := range sortedKeys {
inst := found[key]
instantiatedTypeName := shape.ToGoTypeName(inst,
shape.WithInstantiation(),
shape.WithRootPackage(packageName),
)
fullTypeName := shape.ToGoTypeName(inst,
shape.WithInstantiation(),
shape.WithPkgImportName(),
)

contents.WriteString(fmt.Sprintf("\tshared.TypeRegistryStore[%s](%q)\n", instantiatedTypeName, fullTypeName))
}

contents.WriteString("}\n")

return contents, nil
}
4 changes: 4 additions & 0 deletions dev/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ echo "export AWS_ENDPOINT_URL=http://localhost:4566" >> $envrc_file
echo "export OPENSEARCH_ADDRESS=http://localhost:9200" >> $envrc_file
echo "export OPENSEARCH_USERNAME=admin" >> $envrc_file
echo "export AWS_SQS_QUEUE_URL=$queue_url" >> $envrc_file
echo "export KAFKA_SERVERS=localhost:9092" >> $envrc_file

echo "Localstack is UI is at port"
echo "http://localhost:8080"

echo "Kafka UI is at port"
echo "http://localhost:9088"

## check if it should stream logs, or just end
## don't trigger trap on exit
if [ "$1" == "-nologs" ]; then
Expand Down
49 changes: 49 additions & 0 deletions dev/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,56 @@ services:
volumes:
- data01:/usr/share/opensearch/data

zookeeper:
image: confluentinc/cp-zookeeper:7.5.3
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000

kafka:
image: confluentinc/cp-kafka:7.5.3
depends_on:
- zookeeper
ports:
- 9092:9092

environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_NUM_PARTITIONS: 3
KAFKA_LISTENERS: 'PLAINTEXT://kafka:29092,PLAINTEXT_HOST://0.0.0.0:9092'

healthcheck:
test: [ "CMD", "kafka-topics", "--bootstrap-server", "kafka:9092", "--list" ]
interval: 5s
timeout: 10s
retries: 30
start_period: 10s

kafka-ui:
container_name: kafka-ui
image: provectuslabs/kafka-ui:latest
ports:
- 9088:8080
depends_on:
- kafka
# - schema-registry0
# - kafka-connect0
environment:
KAFKA_CLUSTERS_0_NAME: local
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
KAFKA_CLUSTERS_0_METRICS_PORT: 9997
# KAFKA_CLUSTERS_0_SCHEMAREGISTRY: http://schema-registry0:8085
# KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: first
# KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083
DYNAMIC_CONFIG_ENABLED: 'true' # not necessary, added for tests
KAFKA_CLUSTERS_0_AUDIT_TOPICAUDITENABLED: 'true'
KAFKA_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED: 'true'
volumes:
data01:
name: tmp
Expand Down
13 changes: 13 additions & 0 deletions dev/docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash
set -e

cwd=$(dirname "$0")
project_root=$(dirname "$cwd")

if [ "$1" == "run" ]; then
docker run --rm -it -p 8000:8000 -v ${project_root}:/docs squidfunk/mkdocs-material
elif [ "$1" == "build" ]; then
docker run --rm -it -v ${project_root}:/docs squidfunk/mkdocs-material build
else
echo "Usage: $0 [run|build]"
fi
35 changes: 35 additions & 0 deletions docs/development/development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: Contributing and development
---

# Contributing and development
## Development

Checkout repo and run:
```
./dev/bootstrap.sh
```

This command starts docker container with all necessary tools to develop and test `mkunion` project.

In separate terminal run:
```
go generate ./...
go test ./...
```

This will generate code and run tests.

Note: Some tests are flaky (yes I know, I'm working on it), so if you see some test failing, please run it again.

## Documentation

To preview documentation run:
```
./dev/docs.sh run
```

## Contributing

If you want to contribute to `mkunion` project, please open issue first to discuss your idea.
I have opinions about how `mkunion` should work, how I want to evolve it, and I want to make sure that your idea fits into the project.
Loading
Loading