Skip to content
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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,27 @@ jobs:
- name: Run benchmarks
run: go test -bench=. -benchmem ./...

# Static analysis
lint:
name: Lint
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26'
cache: true

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: latest
args: --timeout=5m

# Code formatting check
formatting:
name: Formatting
Expand Down
165 changes: 165 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# GolangCI-Lint v2 Configuration for coregx/opt
# Documentation: https://golangci-lint.run/docs/configuration/
# Based on: coregx/fursy, coregx/stream (enterprise reference configs)

version: "2"

run:
timeout: 5m
tests: true

linters:
enable:
# Code quality and complexity
- gocyclo
- gocognit
- funlen
- maintidx
- cyclop
- nestif

# Bug detection
- govet
- staticcheck
- errcheck
- errorlint
- gosec
- nilnil
- nilerr
- nilnesserr
- ineffassign

# Code style and consistency
- misspell
- whitespace
- unconvert
- unparam

# Naming conventions
- errname
- revive

# Performance
- prealloc
- makezero

# Code practices
- goconst
- gocritic
- goprintffuncname
- nolintlint
- nakedret

# Comments and documentation
- godot

# Additional quality checkers
- dupl
- dogsled
- durationcheck

settings:
govet:
enable:
- copylocks
disable:
- fieldalignment

gocyclo:
min-complexity: 15

cyclop:
max-complexity: 15

funlen:
lines: 120
statements: 60

gocognit:
min-complexity: 20

misspell:
locale: US

nestif:
min-complexity: 4

revive:
rules:
- name: var-naming
- name: exported
- name: error-return
- name: error-naming
- name: if-return
- name: increment-decrement
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
- name: unreachable-code
- name: redefines-builtin-id

gocritic:
enabled-tags:
- diagnostic
- style
- performance
disabled-checks:
- commentFormatting
- whyNoLint
- unnamedResult
- commentedOutCode
settings:
hugeParam:
sizeThreshold: 256

exclusions:
rules:
- path: _test\.go
linters:
- gocyclo
- cyclop
- funlen
- maintidx
- errcheck
- gosec
- goconst
- dupl

- path: ".*\\.pb\\.go"
linters:
- revive
- gocritic

- path: internal/.*\.go
linters:
- godot

# int16/int32/byte types are structurally identical by design (one type per file).
# Each type wraps a different Go primitive — code duplication is intentional, not refactorable.
- path: (int16|int32|int16_test|int32_test|byte)\.go
linters:
- dupl

- path: zero/(int16|int32|byte)\.go
linters:
- dupl

# G115: int64→int16/int32 conversions are safe — internal.UnmarshalIntJSON
# validates range before returning. Overflow is caught at the parsing layer.
- linters:
- gosec
text: "G115"


issues:
max-issues-per-linter: 0
max-same-issues: 0
new: false
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.1] - 2026-07-05

### Added

- **golangci-lint v2 configuration** — enterprise-grade static analysis with 30+ linters matching coregx ecosystem standard (fursy, stream, coregex, ahocorasick)
- **Lint CI job** — `golangci/golangci-lint-action@v8` in GitHub Actions workflow

### Changed

- **JSON null marshal: zero-allocation** — `[]byte("null")` extracted to package-level `var jsonNull` across all types. Eliminates 1 alloc/op on every null MarshalJSON call (15x faster: 13ns → 0.86ns)
- JSON string literals (`"null"`, `"true"`, `"false"`) extracted to constants for DRY

### Fixed

- `time.ExactEqual` uses struct comparison (`==`) for timezone-aware equality; `time.Equal` uses `time.Time.Equal()` for instant comparison (staticcheck QF1009)
- `NewBool` parameter style: `func(b, valid bool)` (gocritic paramTypeCombine)
- Test helper simplified: `strconv.Itoa` passed directly instead of wrapping lambda (gocritic unlambda)

## [0.3.0] - 2026-07-05

### Breaking Changes
Expand Down Expand Up @@ -65,7 +83,8 @@ Concrete types (opt.String, opt.Int, etc.) and constructors (New, From, FromPtr,
- `encoding/json/v2` compatible (no changes needed)
- CI/CD: GitHub Actions (3 OS × 3 Go versions), Codecov OIDC, branch protection

[Unreleased]: https://github.com/coregx/opt/compare/v0.3.0...HEAD
[Unreleased]: https://github.com/coregx/opt/compare/v0.3.1...HEAD
[0.3.1]: https://github.com/coregx/opt/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/coregx/opt/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/coregx/opt/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/coregx/opt/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

---

## Current State: v0.3.0
## Current State: v0.3.1

### Core
- **`Option[T]`** — generic foundation on `sql.Null[T]` with JSON/SQL/Text support
Expand Down
10 changes: 5 additions & 5 deletions bool.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Bool struct {
}

// NewBool creates a Bool with the given value and validity.
func NewBool(b bool, valid bool) Bool {
func NewBool(b, valid bool) Bool {
return Bool{New(b, valid)}
}

Expand Down Expand Up @@ -39,7 +39,7 @@ func (b Bool) Equal(other Bool) bool {
// MarshalJSON implements json.Marshaler.
func (b Bool) MarshalJSON() ([]byte, error) {
if !b.Valid {
return []byte("null"), nil
return jsonNull, nil
}
if b.V {
return []byte("true"), nil
Expand Down Expand Up @@ -84,14 +84,14 @@ func (b Bool) MarshalText() ([]byte, error) {
func (b *Bool) UnmarshalText(text []byte) error {
str := string(text)
switch str {
case "", "null":
case "", strNull:
b.Valid = false
return nil
case "true":
case strTrue:
b.V = true
b.Valid = true
return nil
case "false":
case strFalse:
b.V = false
b.Valid = true
return nil
Expand Down
4 changes: 2 additions & 2 deletions byte.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (b Byte) Equal(other Byte) bool {
// MarshalJSON implements json.Marshaler.
func (b Byte) MarshalJSON() ([]byte, error) {
if !b.Valid {
return []byte("null"), nil
return jsonNull, nil
}
return json.Marshal(b.V)
}
Expand Down Expand Up @@ -71,7 +71,7 @@ func (b Byte) MarshalText() ([]byte, error) {

// UnmarshalText implements encoding.TextUnmarshaler.
func (b *Byte) UnmarshalText(text []byte) error {
if len(text) == 0 || string(text) == "null" {
if len(text) == 0 || string(text) == strNull {
b.Valid = false
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion field.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (f Field[T]) IsZero() bool {
// If marshaled explicitly: null when invalid, value when valid.
func (f Field[T]) MarshalJSON() ([]byte, error) {
if !f.Valid {
return []byte("null"), nil
return jsonNull, nil
}
return json.Marshal(f.V)
}
Expand Down
2 changes: 1 addition & 1 deletion float.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (f Float) Equal(other Float) bool {
// MarshalJSON implements json.Marshaler. Rejects Inf and NaN.
func (f Float) MarshalJSON() ([]byte, error) {
if !f.Valid {
return []byte("null"), nil
return jsonNull, nil
}
if math.IsInf(f.V, 0) || math.IsNaN(f.V) {
return nil, &json.UnsupportedValueError{
Expand Down
4 changes: 2 additions & 2 deletions funcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import (

func TestMap(t *testing.T) {
v := From(42)
result := Map(v, func(i int) string { return strconv.Itoa(i) })
result := Map(v, strconv.Itoa)
if result.V != "42" || !result.Valid {
t.Errorf("Map valid: got {%v, %v}, want {\"42\", true}", result.V, result.Valid)
}

null := New(0, false)
nullResult := Map(null, func(i int) string { return strconv.Itoa(i) })
nullResult := Map(null, strconv.Itoa)
if nullResult.Valid {
t.Error("Map null: should be invalid")
}
Expand Down
2 changes: 1 addition & 1 deletion int.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (i Int) Equal(other Int) bool {
// MarshalJSON implements json.Marshaler.
func (i Int) MarshalJSON() ([]byte, error) {
if !i.Valid {
return []byte("null"), nil
return jsonNull, nil
}
return []byte(strconv.FormatInt(i.V, 10)), nil
}
Expand Down
2 changes: 1 addition & 1 deletion int16.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (i Int16) Equal(other Int16) bool {
// MarshalJSON implements json.Marshaler.
func (i Int16) MarshalJSON() ([]byte, error) {
if !i.Valid {
return []byte("null"), nil
return jsonNull, nil
}
return []byte(strconv.FormatInt(int64(i.V), 10)), nil
}
Expand Down
2 changes: 1 addition & 1 deletion int32.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (i Int32) Equal(other Int32) bool {
// MarshalJSON implements json.Marshaler.
func (i Int32) MarshalJSON() ([]byte, error) {
if !i.Valid {
return []byte("null"), nil
return jsonNull, nil
}
return []byte(strconv.FormatInt(int64(i.V), 10)), nil
}
Expand Down
10 changes: 9 additions & 1 deletion option.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import (
"encoding/json"
)

var jsonNull = []byte(strNull)

const (
strNull = "null"
strTrue = "true"
strFalse = "false"
)

// Option is a generic nullable type backed by sql.Null[T].
// It marshals to JSON null when invalid and to the value when valid.
// For SQL, it inherits Scanner and Valuer from sql.Null[T].
Expand Down Expand Up @@ -80,7 +88,7 @@ func (v Option[T]) IsZero() bool {
// MarshalJSON implements json.Marshaler. Encodes null when invalid.
func (v Option[T]) MarshalJSON() ([]byte, error) {
if !v.Valid {
return []byte("null"), nil
return jsonNull, nil
}
return json.Marshal(v.V)
}
Expand Down
2 changes: 1 addition & 1 deletion string.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (s String) Equal(other String) bool {
// MarshalJSON implements json.Marshaler.
func (s String) MarshalJSON() ([]byte, error) {
if !s.Valid {
return []byte("null"), nil
return jsonNull, nil
}
return json.Marshal(s.V)
}
Expand Down
Loading
Loading