Skip to content
New issue

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

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

Already on GitHub? # to your account

Refactor exported API #12

Merged
merged 1 commit into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 32 additions & 31 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,14 @@ jobs:
only-new-issues: true
args: --verbose

diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- run: go mod tidy -diff
- run: go mod download
- run: go mod verify
- run: go generate ./...
- name: Detect uncommitted changes
run: |
changes=$(git status --porcelain)
if [[ -n "$changes" ]]; then
{
echo "## :construction: Uncommitted changes"
echo "\`\`\`console"
echo "\$ git status --porcelain"
echo "$changes"
echo "\`\`\`"
} >> "$GITHUB_STEP_SUMMARY"

echo "::group::Uncommitted changes"
echo "$changes"
echo "::endgroup::"

exit 1
fi

test:
needs: diff
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- run: go mod tidy -diff
- run: go mod download
- run: go mod verify
- run: go test -v -count=1 -race -shuffle=on -coverprofile=coverage.out -covermode=atomic ./...
Expand All @@ -83,3 +53,34 @@ jobs:
- uses: codecov/codecov-action@v4
with:
use_oidc: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) }}

generate-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- run: go mod tidy -diff
- run: go mod download
- run: go mod verify
- run: go generate ./...
- name: Detect uncommitted changes
run: |
changes=$(git status --porcelain)
if [[ -n "$changes" ]]; then
{
echo '## :construction: Uncommitted changes'
echo 'Run `$ go generate ./...` to re-generate the files.'
echo '```console'
echo '$ git status --porcelain'
echo "$changes"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

echo "::group::Uncommitted changes"
echo "$changes"
echo "::endgroup::"

exit 1
fi
9 changes: 5 additions & 4 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@ linters:

disable:
- dupl
- depguard
- testpackage
- varnamelen
- wsl
- nlreturn
- revive

issues:
exclude-rules:
- path: '(.+)_test\.go'
linters:
- dupl
- lll
- depguard
- funlen
- maintidx
- path: 'version_test\.go'
linters:
- lll
139 changes: 8 additions & 131 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

<div align="center">

[![Go Reference](https://pkg.go.dev/badge/github.com/typisttech/comver.svg)](https://pkg.go.dev/github.com/typisttech/comver)
[![GitHub Release](https://img.shields.io/github/v/release/typisttech/comver?style=flat-square&)](https://github.com/typisttech/comver/releases/latest)
[![Go](https://github.com/typisttech/comver/actions/workflows/go.yml/badge.svg)](https://github.com/typisttech/comver/actions/workflows/go.yml)
[![codecov](https://codecov.io/gh/typisttech/comver/graph/badge.svg?token=GVO7RV80TJ)](https://codecov.io/gh/typisttech/comver)
[![Go Report Card](https://goreportcard.com/badge/github.com/typisttech/comver)](https://goreportcard.com/report/github.com/typisttech/comver)
[![GitHub Release](https://img.shields.io/github/v/release/typisttech/comver?style=flat-square&)](https://github.com/typisttech/comver/releases/latest)
[![Go Reference](https://pkg.go.dev/badge/github.com/typisttech/comver.svg)](https://pkg.go.dev/github.com/typisttech/comver)
[![license](https://img.shields.io/github/license/typisttech/comver.svg?style=flat-square)](https://github.com/typisttech/comver/blob/master/LICENSE)
[![X Follow @TangRufus](https://img.shields.io/badge/Follow-%40TangRufus-black?style=flat-square&logo=x&logoColor=white)](https://x.com/tangrufus)
[![Hire Typist Tech](https://img.shields.io/badge/Hire-Typist%20Tech-ff69b4.svg?style=flat-square)](https://typist.tech/contact/)
Expand All @@ -29,138 +29,15 @@
## Usage

> [!NOTE]
> See full API documentation at [pkg.go.dev](https://pkg.go.dev/github.com/typisttech/comver).

### `Version`

[`NewVersion`](https://pkg.go.dev/github.com/typisttech/comver#NewVersion) parses a given version string, attempts to coerce a version string into a [`Version`](https://pkg.go.dev/github.com/typisttech/comver#Version) object or return an error if unable to parse the version string.

If there is a leading **v** or a version listed without all parts (e.g. **v1.2.p5+foo**) it will attempt to coerce it into a valid composer version (e.g. **1.2.0.0-patch5**). In both cases a [`Version`](https://pkg.go.dev/github.com/typisttech/comver#Version) object is returned that can be sorted, compared, and used in constraints.
>
> See full API documentation on [pkg.go.dev](https://pkg.go.dev/github.com/typisttech/comver).

## Known Issues

> [!WARNING]
> Due to implementation complexity, it only supports a subset of [composer versioning](https://github.com/composer/semver/).
> [!CAUTION]
>
> Refer to the [`version_test.go`](version_test.go) for examples.


```go
ss := []string{
"1.2.3",
"v1.2.p5+foo",
"v1.2.3.4.p5+foo",
"2010-01-02",
"2010-01-02.5",
"not a version",
"1.0.0-meh",
"20100102.0.3.4",
"1.0.0-alpha.beta",
}

for _, s := range ss {
v, err := comver.NewVersion(s)
if err != nil {
fmt.Println(s, " => ", err)
continue
}
fmt.Println(s, " => ", v)
}

// Output:
// 1.2.3 => 1.2.3.0
// v1.2.p5+foo => 1.2.0.0-patch5
// v1.2.3.4.p5+foo => 1.2.3.4-patch5
// 2010-01-02 => 2010.1.2.0
// 2010-01-02.5 => 2010.1.2.5
// not a version => error parsing version string "not a version"
// 1.0.0-meh => error parsing version string "1.0.0-meh"
// 20100102.0.3.4 => error parsing version string "20100102.0.3.4"
// 1.0.0-alpha.beta => error parsing version string "1.0.0-alpha.beta"
```

### `constraint`

```go
v1, _ := comver.NewVersion("1")
v2, _ := comver.NewVersion("2")
v3, _ := comver.NewVersion("3")
v4, _ := comver.NewVersion("4")

cs := []any{
comver.NewGreaterThanConstraint(v1),
comver.NewGreaterThanOrEqualToConstraint(v2),
comver.NewLessThanOrEqualToConstraint(v3),
comver.NewLessThanConstraint(v4),
}

for _, c := range cs {
fmt.Println(c)
}

// Output:
// >1
// >=2
// <=3
// <4
```

### `interval`

`interval` represents the intersection (logical AND) of two constraints.

```go
v1, _ := comver.NewVersion("1")
v2, _ := comver.NewVersion("2")
v3, _ := comver.NewVersion("3")

g1l3, _ := comver.NewInterval(
comver.NewGreaterThanConstraint(v1),
comver.NewLessThanConstraint(v3),
)

if g1l3.Check(v2) {
fmt.Println(v2.Short(), "satisfies", g1l3)
}

if !g1l3.Check(v3) {
fmt.Println(v2.Short(), "doesn't satisfy", g1l3)
}

// Output:
// 2 satisfies >1 <3
// 2 doesn't satisfy >1 <3
```

### `Intervals`

[`Intervals`](https://pkg.go.dev/github.com/typisttech/comver#Intervals) represent the union (logical OR) of multiple intervals.

```go
v1, _ := comver.NewVersion("1")
v2, _ := comver.NewVersion("2")
v3, _ := comver.NewVersion("3")
v4, _ := comver.NewVersion("4")

g1l3, _ := comver.NewInterval(
comver.NewGreaterThanConstraint(v1),
comver.NewLessThanConstraint(v3),
)

ge2le4, _ := comver.NewInterval(
comver.NewGreaterThanOrEqualToConstraint(v2),
comver.NewLessThanOrEqualToConstraint(v4),
)

is := comver.Intervals{g1l3, ge2le4}
fmt.Println(is)

is = comver.Compact(is)
fmt.Println(is)

// Output:
// >1 <3 || >=2 <=4
// >1 <=4
```
> Due to implementation complexity, it only supports a subset of [composer versioning](https://github.com/composer/semver/).
> Refer to the [version_test.go](https://github.com/typisttech/comver/blob/main/version_test.go) for examples.

## Credits

Expand Down
109 changes: 109 additions & 0 deletions and.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package comver

import "slices"

const (
errNoEndlessGiven stringError = "no endless given"
errUnexpectedAndLogic stringError = "unexpected and logic"
errImpossibleInterval stringError = "impossible interval"
)

// And returns a [CeilingFloorConstrainter] instance representing the logical AND of
// the given [Endless] instances; or return an error if the given [Endless] instances
// could never be satisfied at the same time.
func And(es ...Endless) (CeilingFloorConstrainter, error) { //nolint:cyclop,ireturn
var nilC CeilingFloorConstrainter

if len(es) == 0 {
return nilC, errNoEndlessGiven
}

es = slices.Clone(es)
es = slices.DeleteFunc(es, Endless.wildcard)

if len(es) == 0 {
return NewWildcard(), nil
}
if len(es) == 1 {
return es[0], nil
}

ceiling, ceilingOk := minBoundedCeiling(es...)
floor, floorOk := maxBoundedFloor(es...)

if !ceilingOk && !floorOk {
// logic error! This should never happen
return nilC, errUnexpectedAndLogic
}
if ceilingOk && !floorOk {
return ceiling, nil
}
if !ceilingOk { // floorOk is always true here
return floor, nil
}

vCmp := floor.floor().versionCompare(ceiling.ceiling().version)

if vCmp > 0 {
return nilC, errImpossibleInterval
}

if vCmp == 0 {
if !floor.floor().inclusive() || !ceiling.ceiling().inclusive() {
return nilC, errImpossibleInterval
}

return NewExactConstraint(*floor.floor().version), nil
}

return interval{
upper: ceiling,
lower: floor,
}, nil
}

// MustAnd is like [And] but panics if an error occurs.
func MustAnd(es ...Endless) CeilingFloorConstrainter { //nolint:ireturn
c, err := And(es...)
if err != nil {
panic(err)
}

return c
}

func minBoundedCeiling(es ...Endless) (Endless, bool) {
es = slices.Clone(es)

bcs := slices.DeleteFunc(es, func(b Endless) bool {
return b.ceiling().version == nil
})

if len(bcs) == 0 {
var nilF Endless

return nilF, false
}

return slices.MinFunc(bcs, func(a, b Endless) int {
return a.ceiling().compare(b.ceiling())
}), true
}

func maxBoundedFloor(es ...Endless) (Endless, bool) {
es = slices.Clone(es)

bfs := slices.DeleteFunc(es, func(c Endless) bool {
return c.floor().wildcard()
})

if len(bfs) == 0 {
var nilF Endless

return nilF, false
}

return slices.MaxFunc(bfs, func(a, b Endless) int {
return a.floor().compare(b.floor())
}), true
}
Loading
Loading