Merge pull request #1 from jimeh/initial-implementation

feat(undent): initial implementation
This commit is contained in:
2020-11-26 04:04:55 +00:00
committed by GitHub
11 changed files with 951 additions and 0 deletions

139
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,139 @@
---
name: CI
on: [push]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.31
env:
VERBOSE: "true"
tidy:
name: Tidy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.15
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Check if mods are tidy
run: make check-tidy
benchmark:
name: Benchmarks
runs-on: ubuntu-latest
if: github.ref != 'refs/heads/master'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.15
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run benchmarks
run: make bench | tee output.raw
- name: Fix benchmark names
run: >-
perl -pe 's/^(Benchmark.+?)\/(\S+)(-\d+)(\s+)/\1__\2\4/' output.raw |
tr '-' '_' | tee output.txt
- name: Announce benchmark result
uses: rhysd/github-action-benchmark@v1
with:
tool: "go"
output-file-path: output.txt
fail-on-alert: true
comment-on-alert: true
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: false
cov:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.15
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Publish coverage
uses: paambaati/codeclimate-action@v2.7.4
env:
VERBOSE: "true"
GOMAXPROCS: 4
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
with:
coverageCommand: make cov
prefix: github.com/${{ github.repository }}
coverageLocations: |
${{ github.workspace }}/coverage.out:gocov
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.15
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run tests
run: make test
env:
VERBOSE: "true"
benchmark-store:
name: Store benchmarks
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.15
- uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Run benchmarks
run: make bench | tee output.raw
- name: Fix benchmark names
run: >-
perl -pe 's/^(Benchmark.+?)\/(\S+)(-\d+)(\s+)/\1__\2\4/' output.raw |
tr '-' '_' | tee output.txt
- name: Store benchmark result
uses: rhysd/github-action-benchmark@v1
with:
tool: "go"
output-file-path: output.txt
github-token: ${{ secrets.GH_PUSH_TOKEN }}
comment-on-alert: true
auto-push: true

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/*.tidy-check
/bin/*
/coverage.out
/output.txt

78
.golangci.yml Normal file
View File

@@ -0,0 +1,78 @@
linters-settings:
funlen:
lines: 100
statements: 150
gocyclo:
min-complexity: 20
golint:
min-confidence: 0
govet:
check-shadowing: true
enable-all: true
lll:
line-length: 80
tab-width: 4
maligned:
suggest-new: true
misspell:
locale: US
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dupl
- errcheck
- funlen
- gochecknoinits
- goconst
- gocritic
- gocyclo
- goerr113
- goimports
- golint
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- lll
- misspell
- nakedret
- nlreturn
- noctx
- nolintlint
- scopelint
- sqlclosecheck
- staticcheck
- structcheck
- typecheck
- unconvert
- unused
- varcheck
- whitespace
issues:
include:
# - EXC0002 # disable excluding of issues about comments from golint
exclude:
- Using the variable on range scope `tt` in function literal
- Using the variable on range scope `tc` in function literal
exclude-rules:
- path: "_test\\.go"
linters:
- funlen
- dupl
- source: "^//go:generate "
linters:
- lll
- source: "`json:"
linters:
- lll
run:
timeout: 2m
allow-parallel-runners: true
modules-download-mode: readonly

20
LICENSE Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2020 Jim Myhrberg
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

165
Makefile Normal file
View File

@@ -0,0 +1,165 @@
SOURCES := $(shell find . -name "*.go" -or -name "go.mod" -or -name "go.sum" \
-or -name "Makefile")
# Verbose output
ifdef VERBOSE
V = -v
endif
#
# Environment
#
BINDIR := bin
TOOLDIR := $(BINDIR)/tools
# Global environment variables for all targets
SHELL ?= /bin/bash
SHELL := env \
GO111MODULE=on \
GOBIN=$(CURDIR)/$(TOOLDIR) \
CGO_ENABLED=1 \
PATH='$(CURDIR)/$(BINDIR):$(CURDIR)/$(TOOLDIR):$(PATH)' \
$(SHELL)
#
# Defaults
#
# Default target
.DEFAULT_GOAL := test
#
# Tools
#
TOOLS += $(TOOLDIR)/gobin
gobin: $(TOOLDIR)/gobin
$(TOOLDIR)/gobin:
GO111MODULE=off go get -u github.com/myitcv/gobin
# external tool
define tool # 1: binary-name, 2: go-import-path
TOOLS += $(TOOLDIR)/$(1)
.PHONY: $(1)
$(1): $(TOOLDIR)/$(1)
$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile
gobin $(V) "$(2)"
endef
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc))
$(eval $(call tool,gofumports,mvdan.cc/gofumpt/gofumports))
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31))
.PHONY: tools
tools: $(TOOLS)
#
# Development
#
TEST ?= $$(go list ./... | grep -v 'vendor')
BENCH ?= .
.PHONY: clean
clean:
rm -f $(TOOLS)
rm -f ./coverage.out ./go.mod.tidy-check ./go.sum.tidy-check
.PHONY: clean-golden
clean-golden:
rm -f $(shell find * -path '*/testdata/*' -name "*.golden" \
-exec echo "'{}'" \;)
.PHONY: test
test:
go test $(V) -count=1 -race $(TESTARGS) $(TEST)
.PHONY: test-update-golden
test-update-golden:
@$(MAKE) test UPDATE_GOLDEN=1
.PHONY: regen-golden
regen-golden: clean-golden test-update-golden
.PHONY: test-deps
test-deps:
go test all
.PHONY: lint
lint: golangci-lint
GOGC=off golangci-lint $(V) run
.PHONY: format
format: gofumports
gofumports -w .
.SILENT: bench
.PHONY: bench
bench:
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) $(TEST)
#
# Coverage
#
.PHONY: cov
cov: coverage.out
.PHONY: cov-html
cov-html: coverage.out
go tool cover -html=./coverage.out
.PHONY: cov-func
cov-func: coverage.out
go tool cover -func=./coverage.out
coverage.out: $(SOURCES)
go test $(V) -covermode=count -coverprofile=./coverage.out ./...
#
# Dependencies
#
.PHONY: deps
deps:
$(info Downloading dependencies)
go mod download
.PHONY: tidy
tidy:
go mod tidy $(V)
.PHONY: verify
verify:
go mod verify
.SILENT: check-tidy
.PHONY: check-tidy
check-tidy:
cp go.mod go.mod.tidy-check
cp go.sum go.sum.tidy-check
go mod tidy
( \
diff go.mod go.mod.tidy-check && \
diff go.sum go.sum.tidy-check && \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum \
) || ( \
rm -f go.mod go.sum && \
mv go.mod.tidy-check go.mod && \
mv go.sum.tidy-check go.sum; \
exit 1 \
)
#
# Documentation
#
# Serve docs
.PHONY: docs
docs: godoc
@godoc -http=127.0.0.1:6060

View File

@@ -1 +1,20 @@
# undent
Go package which removes leading indentation/white-space from multi-line strings
and byte slices.
```go
s := undent.String(`
{
"hello": "world"
}`,
)
fmt.Println(s)
```
```
{
"hello": "world"
}
```

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/jimeh/undent
go 1.15
require github.com/stretchr/testify v1.6.1

11
go.sum Normal file
View File

@@ -0,0 +1,11 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

73
undent.go Normal file
View File

@@ -0,0 +1,73 @@
// Package undent removes leading indentation/white-space from strings and byte
// slices.
package undent
import (
"fmt"
"regexp"
)
var matcher = regexp.MustCompile(`(?m)^([ \t]*)(?:\S)`)
// Bytes removes leading indentation/white-space from given byte slice.
func Bytes(b []byte) []byte {
matches := matcher.FindAll(b, -1)
if len(matches) == 0 {
return b
}
index := 0
length := len(matches[0])
for i, s := range matches[1:] {
l := len(s)
if l < length {
index = i + 1
length = l
}
}
if length <= 1 {
return b
}
indent := matches[index][0 : length-1]
return regexp.MustCompile(
`(?m)^`+regexp.QuoteMeta(string(indent)),
).ReplaceAllLiteral(b, []byte{})
}
// String removes leading indentation/white-space from given string.
func String(s string) string {
matches := matcher.FindAllString(s, -1)
if len(matches) == 0 {
return s
}
index := 0
length := len(matches[0])
for i, s := range matches[1:] {
l := len(s)
if l < length {
index = i + 1
length = l
}
}
if length <= 1 {
return s
}
indent := matches[index][0 : length-1]
return regexp.MustCompile(
`(?m)^`+regexp.QuoteMeta(indent),
).ReplaceAllLiteralString(s, "")
}
// Stringf removes leading indentation/white-space from given format string
// before passing format and all additional arguments to fmt.Sprintf, returning
// the result.
func Stringf(format string, a ...interface{}) string {
return fmt.Sprintf(String(format), a...)
}

51
undent_example_test.go Normal file
View File

@@ -0,0 +1,51 @@
package undent_test
import (
"fmt"
"github.com/jimeh/undent"
)
func ExampleBytes() {
b := undent.Bytes([]byte(`
{
"hello": "world"
}`,
))
fmt.Println(string(b))
// Output:
//
// {
// "hello": "world"
// }
}
func ExampleString() {
s := undent.String(`
{
"hello": "world"
}`,
)
fmt.Println(s)
// Output:
//
// {
// "hello": "world"
// }
}
func ExampleStringf() {
s := undent.Stringf(`
{
"hello": "%s"
}`,
"world",
)
fmt.Println(s)
// Output:
//
// {
// "hello": "world"
// }
}

386
undent_test.go Normal file
View File

@@ -0,0 +1,386 @@
package undent
import (
"testing"
"github.com/stretchr/testify/assert"
)
var stringTestCases = []struct {
name string
s string
want string
}{
{
name: "empty",
s: "",
want: "",
},
{
name: "single-line",
s: "hello world",
want: "hello world",
},
{
name: "single-line indented",
s: " hello world",
want: "hello world",
},
{
name: "multi-line",
s: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
want: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
},
{
name: "multi-line space indented",
s: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
want: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
},
{
name: "multi-line tab indented",
s: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
want: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
},
{
name: "multi-line tab indented with tabs and spaces after indent",
s: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
want: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
},
{
name: "multi-line space indented with blank lines",
s: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
want: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
},
{
name: "multi-line tab indented with blank lines",
s: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
want: `
{
"hello": "world",
"foo": [
"bar"
]
}`,
},
{
name: "multi-line space indented with random indentation",
s: `
hello
world
foo
bar`,
want: `
hello
world
foo
bar`,
},
{
name: "multi-line tab indented with random indentation",
s: `
hello
world
foo
bar`,
want: `
hello
world
foo
bar`,
},
}
var stringfTestCases = []struct {
name string
s string
a []interface{}
want string
}{
{
name: "empty",
s: "",
want: "",
},
{
name: "single-line",
s: "hello %s, %d",
a: []interface{}{"world", 42},
want: "hello world, 42",
},
{
name: "single-line indented",
s: " hello %s, %d",
a: []interface{}{"world", 42},
want: "hello world, 42",
},
{
name: "multi-line",
s: `
{
"hello": "%s",
"foo": [
%d
]
}`,
a: []interface{}{"world", 42},
want: `
{
"hello": "world",
"foo": [
42
]
}`,
},
{
name: "multi-line space indented",
s: `
{
"hello": "%s",
"foo": [
%d
]
}`,
a: []interface{}{"world", 42},
want: `
{
"hello": "world",
"foo": [
42
]
}`,
},
{
name: "multi-line tab indented",
s: `
{
"hello": "%s",
"foo": [
%d
]
}`,
a: []interface{}{"world", 42},
want: `
{
"hello": "world",
"foo": [
42
]
}`,
},
{
name: "multi-line tab indented with tabs and spaces after indent",
s: `
{
"hello": "%s",
"foo": [
%d
]
}`,
a: []interface{}{"world", 42},
want: `
{
"hello": "world",
"foo": [
42
]
}`,
},
{
name: "multi-line space indented with blank lines",
s: `
{
"hello": "%s",
"foo": [
%d
]
}`,
a: []interface{}{"world", 42},
want: `
{
"hello": "world",
"foo": [
42
]
}`,
},
{
name: "multi-line tab indented with blank lines",
s: `
{
"hello": "%s",
"foo": [
%d
]
}`,
a: []interface{}{"world", 42},
want: `
{
"hello": "world",
"foo": [
42
]
}`,
},
{
name: "multi-line space indented with random indentation",
s: `
hello
%s
foo
%d`,
a: []interface{}{"world", 42},
want: `
hello
world
foo
42`,
},
{
name: "multi-line tab indented with random indentation",
s: `
hello
%s
foo
%d`,
a: []interface{}{"world", 42},
want: `
hello
world
foo
42`,
},
}
func TestBytes(t *testing.T) {
for _, tt := range stringTestCases {
t.Run(tt.name, func(t *testing.T) {
got := Bytes([]byte(tt.s))
assert.Equal(t, []byte(tt.want), got)
})
}
}
func TestString(t *testing.T) {
for _, tt := range stringTestCases {
t.Run(tt.name, func(t *testing.T) {
got := String(tt.s)
assert.Equal(t, tt.want, got)
})
}
}
func TestStringf(t *testing.T) {
for _, tt := range stringfTestCases {
t.Run(tt.name, func(t *testing.T) {
got := Stringf(tt.s, tt.a...)
assert.Equal(t, tt.want, got)
})
}
}
func BenchmarkBytes(b *testing.B) {
for _, tt := range stringTestCases {
b.Run(tt.name, func(b *testing.B) {
input := []byte(tt.s)
for i := 0; i < b.N; i++ {
Bytes(input)
}
})
}
}
func BenchmarkString(b *testing.B) {
for _, tt := range stringTestCases {
b.Run(tt.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
String(tt.s)
}
})
}
}