Skip to content

Commit

Permalink
v0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
chanced committed Aug 29, 2022
1 parent 622da74 commit 6c3a524
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 78 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# caps

caps is a case conversion library for Go. It was built with the following
priorites: configurability, consistency, correctness, ergonomic, and performant
in mind, in that order.

Out of the box, the following case conversion are supported:

- Camel Case (e.g. CamelCase)
- Lower Camel Case (e.g. lowerCamelCase)
- Snake Case (e.g. snake_case)
- Screaming Snake Case (e.g. SCREAMING_SNAKE_CASE)
- Kebab Case (e.g. kebab-case)
- Screaming Kebab Case(e.g. SCREAMING-KEBAB-CASE)
- Dot Notation Case (e.g. dot.notation.case)
- Title Case (e.g. Title Case)
- Other deliminations

Word boundaries are determined by the `caps.Formatter`. The provided implementation, `caps.FormatterImpl`,
delegates the boundary detection to `caps.Tokenizer`. The provided implementation, `caps.TokenizerImpl`,
uses the following tokens as delimiters: `" _.!?:;$-(){}[]#@&+~"`.

`caps.StdFormatter` also allows users to register `caps.Replacement`s for acronym replacement. The default list is:

```go
{"Http", "HTTP"}
{"Https", "HTTPS"}
{"Id", "ID"}
{"Ip", "IP"}
{"Html", "HTML"}
{"Xml", "XML"}
{"Json", "JSON"}
{"Csv", "CSV"}
{"Aws", "AWS"}
{"Gcp", "GCP"}
{"Sql", "SQL"}
```

If you would like to add or remove entries from that list, you have a few
options.

You can pass a new instance of `caps.StdFormatter` with a new set of
`caps.Replacement` (likely preferred).

You can create your own `caps.Formatter`. This could be as simple as
implementing the single `Format` method, calling `caps.DefaultFormatter.Format`,
and then modifying the result.

Finally, if you are so inclined, you can update `caps.DefaultFormatter`. Just be aware that the
module was not built with thread-safety in mind so you should set it once.
Otherwise, you'll need guard your usage of the library accordingly.

## License

MIT
57 changes: 31 additions & 26 deletions caps.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ type Tokenizer interface {
//
// style: Expected output caps.Style of the string.
// repStyle: The caps.ReplaceStyle to use if a word needs to be replaced.
// join: The delimiter to use when joining the words. For CamelCase, this is an empty string.
// join: The delimiter to use when joining the words. For Camel, this is an empty string.
// allowedSymbols: The set of allowed symbols. If set, these should take precedence over any delimiters
// numberRules: Any custom rules dictating how to handle special characters in numbers.
type Formatter interface {
Expand Down Expand Up @@ -153,8 +153,6 @@ func (t TokenizerImpl) Tokenize(str string, allowedSymbols []rune, numberRules m
allowed := newRunes(allowedSymbols)

for i, r := range str {
rStr := string(r)
_ = rStr
switch {
case unicode.IsUpper(r):
if foundLower && current.Len() > 0 {
Expand Down Expand Up @@ -304,10 +302,10 @@ type ReplaceStyle uint8

type (
Replacement struct {
// Camelcase variant of the word which should be replaced.
// Camel case variant of the word which should be replaced.
// e.g. "Http"
Camel string
// Screaming (all uppercase) representation of the word to replace.
// Screaming (all upper case) representation of the word to replace.
// e.g. "HTTP"
Screaming string
}
Expand All @@ -318,8 +316,8 @@ type Style uint8
const (
StyleLower Style = iota // The output should be lowercase (e.g. "an_example")
StyleScreaming // The output should be screaming (e.g. "AN_EXAMPLE")
StyleCamel // The output should be camelcase (e.g. "AnExample")
StyleLowerCamel // The output should be lower camelcase (e.g. "anExample")
StyleCamel // The output should be camel case (e.g. "AnExample")
StyleLowerCamel // The output should be lower camel case (e.g. "anExample")
)

const (
Expand Down Expand Up @@ -616,7 +614,7 @@ func loadOpts(opts []Opts) Opts {
result := Opts{
AllowedSymbols: "",
Formatter: DefaultFormatter,
ReplaceStyle: ReplaceStyleNotSpecified,
ReplaceStyle: ReplaceStyleScreaming,
}
if len(opts) == 0 {
return result
Expand All @@ -628,6 +626,13 @@ func loadOpts(opts []Opts) Opts {
if opts[0].Formatter != nil {
result.Formatter = opts[0].Formatter
}
if opts[0].ReplaceStyle != ReplaceStyleNotSpecified {
result.ReplaceStyle = opts[0].ReplaceStyle
}
if opts[0].NumberRules != nil {
result.NumberRules = opts[0].NumberRules
}

return result
}

Expand All @@ -653,22 +658,22 @@ func WithoutNumbers[T ~string](s T) T {
}, string(s)))
}

// ToCamel transforms the case of str into Camelcase (e.g. AnExampleString) using
// ToCamel transforms the case of str into Camel Case (e.g. AnExampleString) using
// either the provided Formatter or the DefaultFormatter otherwise.
//
// The default Formatter detects case so that "AN_EXAMPLE_STRING" becomes "AnExampleString".
// It also has a configurable set of replacements, such that "some_json" becomes "SomeJSON"
// so long as opts.ReplacementStyle is set to ReplaceStyleScreaming. A ReplaceStyle of
// ReplaceStyleCamel would result in "SomeJson".
//
// caps.ToCamel("This is [an] {example}${id32}.") // thisIsAnExampleID32
// caps.ToCamel("AN_EXAMPLE_STRING", ) // anExampleString
// caps.ToCamel("This is [an] {example}${id32}.") // ThisIsAnExampleID32
// caps.ToCamel("AN_EXAMPLE_STRING", ) // AnExampleString
func ToCamel[T ~string](str T, options ...Opts) T {
opts := loadOpts(options)
return T(opts.Formatter.Format(StyleCamel, opts.ReplaceStyle, string(str), "", []rune(opts.AllowedSymbols), opts.NumberRules))
}

// ToLowerCamel transforms the case of str into Lower Camelcase (e.g. anExampleString) using
// ToLowerCamel transforms the case of str into Lower Camel Case (e.g. anExampleString) using
// either the provided Formatter or the DefaultFormatter otherwise.
//
// The default Formatter detects case so that "AN_EXAMPLE_STRING" becomes "anExampleString".
Expand All @@ -682,15 +687,15 @@ func ToLowerCamel[T ~string](str T, options ...Opts) T {
return T(opts.Formatter.Format(StyleLowerCamel, opts.ReplaceStyle, string(str), "", []rune(opts.AllowedSymbols), opts.NumberRules))
}

// ToSnake transforms the case of str into Lower Snakecase (e.g. an_example_string) using
// ToSnake transforms the case of str into Lower Snake Case (e.g. an_example_string) using
// either the provided Formatter or the DefaultFormatter otherwise.
//
// caps.ToSnake("This is [an] {example}${id32}.") // this_is_an_example_id_32
func ToSnake[T ~string](str T, options ...Opts) T {
return ToDelimited(str, '_', true, options...)
}

// ToScreamingSnake transforms the case of str into Screaming Snakecase (e.g.
// ToScreamingSnake transforms the case of str into Screaming Snake Case (e.g.
// AN_EXAMPLE_STRING) using either the provided Formatter or the
// DefaultFormatter otherwise.
//
Expand All @@ -699,41 +704,41 @@ func ToScreamingSnake[T ~string](str T, options ...Opts) T {
return ToDelimited(str, '_', false, options...)
}

// ToKebab transforms the case of str into Lower Kebabcase (e.g. an-example-string) using
// ToKebab transforms the case of str into Lower Kebab Case (e.g. an-example-string) using
// either the provided Formatter or the DefaultFormatter otherwise.
//
// caps.ToKebab("This is [an] {example}${id32}.") // this-is-an-example-id-32
func ToKebab[T ~string](str T, options ...Opts) T {
return ToDelimited(str, '_', true, options...)
return ToDelimited(str, '-', true, options...)
}

// ToScreamingKebab transforms the case of str into Screaming Kebab (e.g.
// ToScreamingKebab transforms the case of str into Screaming Kebab Snake (e.g.
// AN-EXAMPLE-STRING) using either the provided Formatter or the
// DefaultFormatter otherwise.
//
// caps.ToScreamingSnake("This is [an] {example}${id32}.") // THIS-IS-AN-EXAMPLE-ID-32
// caps.ToScreamingKebab("This is [an] {example}${id32}.") // THIS-IS-AN-EXAMPLE-ID-32
func ToScreamingKebab[T ~string](str T, options ...Opts) T {
return ToDelimited(str, '_', false, options...)
return ToDelimited(str, '-', false, options...)
}

// ToDot transforms the case of str into Lower Dot notation case (e.g. an.example.string) using
// ToDot transforms the case of str into Lower Dot Notation Case (e.g. an.example.string) using
// either the provided Formatter or the DefaultFormatter otherwise.
//
// caps.ToDot("This is [an] {example}${id32}.") // this.is.an.example.id.32
func ToDot[T ~string](str T, options ...Opts) T {
return ToDelimited(str, '_', true, options...)
return ToDelimited(str, '.', true, options...)
}

// ToScreamingKebab transforms the case of str into Screaming Kebab (e.g.
// ToScreamingKebab transforms the case of str into Screaming Kebab Case (e.g.
// AN-EXAMPLE-STRING) using either the provided Formatter or the
// DefaultFormatter otherwise.
//
// caps.ToScreamingDot("This is [an] {example}${id32}.") // THIS.IS.AN.EXAMPLE.ID.32
func ToScreamingDot[T ~string](str T, options ...Opts) T {
return ToDelimited(str, '_', false, options...)
return ToDelimited(str, '.', false, options...)
}

// ToTitle transforms the case of str into Lower Dot notation case (e.g. An Example String) using
// ToTitle transforms the case of str into Title Case (e.g. An Example String) using
// either the provided Formatter or the DefaultFormatter otherwise.
//
// caps.ToTitle("This is [an] {example}${id32}.") // This Is An Example ID 32
Expand All @@ -752,8 +757,8 @@ func ToTitle[T ~string](str T, options ...Opts) T {
// # Example
//
// caps.ToDelimited("This is [an] {example}${id}.#32", '.', true) // this.is.an.example.id.32
// caps.ToDelimited("This is [an] {example}${id32}.break32", '.', false) // THIS.IS.AN.EXAMPLE.ID.BREAK.32
// caps.ToDelimited("This is [an] {example}${id32}.v32", '.', true, caps.Opts{AllowedSymbols: "$" }) // this.is.an.example.id.$.v32
// caps.ToDelimited("This is [an] {example}${id}.break32", '.', false) // THIS.IS.AN.EXAMPLE.ID.BREAK.32
// caps.ToDelimited("This is [an] {example}${id}.v32", '.', true, caps.Opts{AllowedSymbols: "$"}) // this.is.an.example.$.id.v32
func ToDelimited[T ~string](str T, delimiter rune, lowercase bool, options ...Opts) T {
opts := loadOpts(options)
var style Style
Expand Down
92 changes: 65 additions & 27 deletions examples_test.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,76 @@
package caps_test

func ExampleToCamel() {
// fmt.Println(caps.ToCamel("This is [an] {example}${id32}."))
// fmt.Println(caps.ToCamel("This is [an] {example}${id32}.break32"))
// fmt.Println(caps.ToCamel("This example allows for $ symbols", caps.Opts{AllowedSymbols: "$"}))
import (
"fmt"

// customReplacer := caps.NewFormatter([]caps.R{{"Http", "HTTP"}, {"Https", "HTTPS"}})
// fmt.Println(caps.ToCamel("No Id just http And Https", caps.Opts{Formatter: customReplacer}))
"github.com/chanced/caps"
)

// Outputx:
func ExampleToCamel() {
fmt.Println(caps.ToCamel("This is [an] {example}${id32}."))
fmt.Println(caps.ToCamel("AN_EXAMPLE_STRING"))
// Output:
// ThisIsAnExampleID32
// AnExampleString
}

func ExampleToLowerCamel() {
fmt.Println(caps.ToLowerCamel("This is [an] {example}${id32}."))
// Output:
// thisIsAnExampleID32
// thisIsAnExampleID32Break32
// thisExampleAllowsFor$symbols
// noIdJustHTTPAndHTTPS
}

func ExampleToSnake() {
fmt.Println(caps.ToSnake("This is [an] {example}${id32}."))
fmt.Println(caps.ToSnake("v3.2.2"))
// Output:
// this_is_an_example_id_32
// v3_2_2
}

func ExampleToScreamingSnake() {
fmt.Println(caps.ToScreamingSnake("This is [an] {example}${id32}."))
// Output:
// THIS_IS_AN_EXAMPLE_ID_32
}

func ExampleToKebab() {
fmt.Println(caps.ToKebab("This is [an] {example}${id32}."))
// Output:
// this-is-an-example-id-32
}

func ExampleToScreamingKebab() {
fmt.Println(caps.ToScreamingKebab("This is [an] {example}${id32}."))
// Output:
// THIS-IS-AN-EXAMPLE-ID-32
}

func ExampleToDot() {
fmt.Println(caps.ToDot("This is [an] {example}${id32}."))
// Output:
// this.is.an.example.id.32
}

func ExampleToScreamingDot() {
fmt.Println(caps.ToScreamingDot("This is [an] {example}${id32}."))
// Output:
// THIS.IS.AN.EXAMPLE.ID.32
}

func ExampleToDelimited() {
// fmt.Println(caps.ToDelimited("A # B _ C", '.', true))
// fmt.Println(caps.ToDelimited("$id", '.', false))
// fmt.Println(caps.ToDelimited("$id", '.', true, caps.Opts{AllowedSymbols: "$"}))
// fmt.Println(caps.ToDelimited("fromCamelcaseString", '.', true))
// Outputx:
// a.b.c
// ID
// $id
// from.camelcase.string
fmt.Println(caps.ToDelimited("This is [an] {example}${id}.#32", '.', true))
fmt.Println(caps.ToDelimited("This is [an] {example}${id}.break32", '.', false))
fmt.Println(caps.ToDelimited("This is [an] {example}${id}.v32", '.', true, caps.Opts{AllowedSymbols: "$"}))

// Output:
// this.is.an.example.id.32
// THIS.IS.AN.EXAMPLE.ID.BREAK.32
// this.is.an.example.$.id.v32
}

func ExampleToSnake() {
// fmt.Println(caps.ToSnake("A long string with spaces"))
// fmt.Println(caps.ToSnake(strings.ToLower("A_SCREAMING_SNAKE_MUST_BE_LOWERED_FIRST")))
// fmt.Println(caps.ToSnake("$word", caps.Opts{AllowedSymbols: "$"}))
// OutputX:
// a_long_string_with_spaces
// a_screaming_snake_must_be_lowered_first
// $word
func ExampleToTitle() {
fmt.Println(caps.ToTitle("This is [an] {example}${id32}."))
// Output:
// This Is An Example ID 32
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/chanced/caps

go 1.19
go 1.18
48 changes: 24 additions & 24 deletions text/text.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
package text

import "strings"
// import "strings"

type Text string
// type Text string

func (t Text) String() string {
return string(t)
}
// func (t Text) String() string {
// return string(t)
// }

func (t Text) ToLower() Text {
return Text(strings.ToLower(t.String()))
}
// func (t Text) ToLower() Text {
// return Text(strings.ToLower(t.String()))
// }

func (t Text) ToUpper() Text {
return Text(strings.ToUpper(t.String()))
}
// func (t Text) ToUpper() Text {
// return Text(strings.ToUpper(t.String()))
// }

func (t Text) ToSnake() Text {
panic("not implemented")
}
// func (t Text) ToSnake() Text {
// panic("not implemented")
// }

func (t Text) ToScreamingSnake() Text {
// return Text(strcase.ToScreamingSnake(t.String()))
panic("not implemented")
}
// func (t Text) ToScreamingSnake() Text {
// // return Text(strcase.ToScreamingSnake(t.String()))
// panic("not implemented")
// }

func (t Text) ReplaceAll(old string, new string) Text {
return Text(strings.Replace(t.String(), old, new, -1))
}
// func (t Text) ReplaceAll(old string, new string) Text {
// return Text(strings.Replace(t.String(), old, new, -1))
// }

func (t Text) Replace(old, new string, n int) Text {
return Text(strings.Replace(t.String(), old, new, n))
}
// func (t Text) Replace(old, new string, n int) Text {
// return Text(strings.Replace(t.String(), old, new, n))
// }

0 comments on commit 6c3a524

Please sign in to comment.