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
20 changes: 20 additions & 0 deletions .github/workflows/fmt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Format

on:
push:
branches: [ "main", "master" ]
pull_request:

jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Format
run: |
go fmt ./...
git diff --exit-code
19 changes: 19 additions & 0 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: golangci-lint

on:
push:
branches: [ "main", "master" ]
pull_request:

jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
10 changes: 5 additions & 5 deletions .github/workflows/go.yml → .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
name: Go
name: Test

on:
push:
branches: [ "main", "master" ]
pull_request:

jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Vet
run: go vet ./...
- name: Test
run: go test ./...
18 changes: 18 additions & 0 deletions .github/workflows/vet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Vet

on:
push:
branches: [ "main", "master" ]
pull_request:

jobs:
vet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Vet
run: go vet ./...
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 arran4

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.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# strings2

[![Test Status](https://github.com/arran4/strings2/actions/workflows/test.yml/badge.svg)](https://github.com/arran4/strings2/actions/workflows/test.yml)
[![Vet Status](https://github.com/arran4/strings2/actions/workflows/vet.yml/badge.svg)](https://github.com/arran4/strings2/actions/workflows/vet.yml)
[![Lint Status](https://github.com/arran4/strings2/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/arran4/strings2/actions/workflows/golangci-lint.yml)
[![Fmt Status](https://github.com/arran4/strings2/actions/workflows/fmt.yml/badge.svg)](https://github.com/arran4/strings2/actions/workflows/fmt.yml)
[![Go Reference](https://pkg.go.dev/badge/github.com/arran4/strings2.svg)](https://pkg.go.dev/github.com/arran4/strings2)

strings2 provides utilities for converting slices of words into various casing conventions. It is intended to supplement Go's standard library `strings` package with helpers for creating formats such as `camelCase`, `PascalCase`, `snake_case` and `kebab-case`.

## Installation
Expand Down Expand Up @@ -60,3 +66,7 @@ fmt.Println(strings2.ToSnakeCase(words, strings2.OptionCaseMode(strings2.CMScrea
```

Options are composable so multiple behaviours can be applied at once. See the documentation in `types.go` for details on further options.

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
53 changes: 53 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package strings2_test

import (
"fmt"

"github.com/arran4/strings2"
)

func ExampleToCamelCase() {
words := []strings2.Word{
strings2.SingleCaseWord("hello"),
strings2.SingleCaseWord("world"),
}
fmt.Println(strings2.ToCamelCase(words))
// Output: helloWorld
}

func ExampleToPascalCase() {
words := []strings2.Word{
strings2.SingleCaseWord("hello"),
strings2.SingleCaseWord("world"),
}
fmt.Println(strings2.ToPascalCase(words))
// Output: HelloWorld
}

func ExampleToKebabCase() {
words := []strings2.Word{
strings2.SingleCaseWord("hello"),
strings2.SingleCaseWord("world"),
}
fmt.Println(strings2.ToKebabCase(words))
// Output: hello-world
}

func ExampleToSnakeCase() {
words := []strings2.Word{
strings2.SingleCaseWord("hello"),
strings2.SingleCaseWord("world"),
}
fmt.Println(strings2.ToSnakeCase(words))
// Output: hello_world
}

func ExampleToFormattedCase() {
words := []strings2.Word{
strings2.SingleCaseWord("hello"),
strings2.SingleCaseWord("world"),
}
// Screaming snake case
fmt.Println(strings2.ToFormattedCase(words, strings2.OptionCaseMode(strings2.CMScreaming), strings2.OptionDelimiter("_")))
// Output: HELLO_WORLD
}
22 changes: 21 additions & 1 deletion types.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@ import (
"strings"
)

// Word interface representing a stringer type
// Word interface representing a stringer type that can be used in casing conversions.
type Word fmt.Stringer

// Word Types

// SingleCaseWord is a word that will be lowercased when stringified.
type SingleCaseWord string

// FirstUpperCaseWord is a word that will have its first letter uppercased and the rest lowercased when stringified.
type FirstUpperCaseWord string

// ExactCaseWord is a word that preserves its case when stringified.
type ExactCaseWord string

// String implementations
func (w SingleCaseWord) String() string { return strings.ToLower(string(w)) }
func (w FirstUpperCaseWord) String() string { return UpperCaseFirst(strings.ToLower(string(w))) }

// UpperCaseFirst uppercases the first character of the string.
func UpperCaseFirst(s string) string {
if s == "" {
return s
Expand All @@ -28,14 +35,21 @@ func (w ExactCaseWord) String() string { return string(w) }
// Options
type Option func(*caseConfig)

// CaseMode defines the casing transformation mode.
type CaseMode int

const (
// CMVerbatim leaves the case as is.
CMVerbatim CaseMode = iota
// CMFirstTitle uppercases the first character of the first word.
CMFirstTitle
// CMAllTitle uppercases the first character of every word.
CMAllTitle
// CMFirstLower lowercases the first character of the first word.
CMFirstLower
// CMWhispering lowercases all characters (like snake_case or kebab-case usually).
CMWhispering
// CMScreaming uppercases all characters (like SCREAMING_SNAKE_CASE).
CMScreaming
)

Expand All @@ -52,26 +66,32 @@ type caseConfig struct {
firstLower bool
}

// OptionDelimiter sets the delimiter between words.
func OptionDelimiter(d string) Option {
return func(cfg *caseConfig) { cfg.delimiter = d }
}

// OptionCaseMode sets the case mode.
func OptionCaseMode(caseMode CaseMode) Option {
return func(cfg *caseConfig) { cfg.caseMode = caseMode }
}

// OptionFirstUpper ensures the very first character of the result is uppercase.
func OptionFirstUpper() Option {
return func(cfg *caseConfig) { cfg.firstUpper = true }
}

// OptionFirstLower ensures the very first character of the result is lowercase.
func OptionFirstLower() Option {
return func(cfg *caseConfig) { cfg.firstLower = true }
}

// OptionMixCaseSupport enables splitting of mixed case words (e.g. CamelCase) into separate words based on uppercase letters.
func OptionMixCaseSupport() Option {
return func(cfg *caseConfig) { cfg.mixCaseSupport = true }
}

// OptionUpperIndicator sets a specific indicator for upper case (often used for double delimiters).
func OptionUpperIndicator(d string) Option {
return func(cfg *caseConfig) { cfg.upperIndicator = d }
}
Expand Down
16 changes: 8 additions & 8 deletions types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ func TestToCamelCase(t *testing.T) {
expected: "hello",
},
{
name: "Empty",
words: []Word{},
name: "Empty",
words: []Word{},
expected: "",
},
}
Expand Down Expand Up @@ -107,7 +107,7 @@ func TestToKebabCase(t *testing.T) {
SingleCaseWord("hello"),
SingleCaseWord("world"),
},
options: []Option{OptionCaseMode(CMScreaming)},
options: []Option{OptionCaseMode(CMScreaming)},
expected: "HELLO-WORLD",
},
{
Expand All @@ -116,7 +116,7 @@ func TestToKebabCase(t *testing.T) {
SingleCaseWord("hello"),
SingleCaseWord("world"),
},
options: []Option{OptionUpperIndicator("-")},
options: []Option{OptionUpperIndicator("-")},
expected: "hello--world",
},
}
Expand Down Expand Up @@ -152,7 +152,7 @@ func TestToSnakeCase(t *testing.T) {
SingleCaseWord("hello"),
SingleCaseWord("world"),
},
options: []Option{OptionCaseMode(CMScreaming)},
options: []Option{OptionCaseMode(CMScreaming)},
expected: "HELLO_WORLD",
},
{
Expand All @@ -161,7 +161,7 @@ func TestToSnakeCase(t *testing.T) {
SingleCaseWord("hello"),
SingleCaseWord("world"),
},
options: []Option{OptionFirstUpper()},
options: []Option{OptionFirstUpper()},
expected: "Hello_world",
},
}
Expand Down Expand Up @@ -237,15 +237,15 @@ func TestMixCaseSupport(t *testing.T) {
ExactCaseWord("camelCase"),
SingleCaseWord("test"),
},
options: []Option{OptionMixCaseSupport(), OptionDelimiter("-")},
options: []Option{OptionMixCaseSupport(), OptionDelimiter("-")},
expected: "camel-Case-test",
},
{
name: "FirstUpperCase MixCase",
words: []Word{
ExactCaseWord("camelCase"),
},
options: []Option{OptionMixCaseSupport(), OptionDelimiter("_"), OptionFirstUpper()},
options: []Option{OptionMixCaseSupport(), OptionDelimiter("_"), OptionFirstUpper()},
expected: "Camel_Case",
},
}
Expand Down
Loading