scs

package module
v2.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 1, 2026 License: MIT Imports: 4 Imported by: 0

README

Go Report Card License License Stay with Ukraine

scs — String Case Style for Go

scs converts identifiers between naming conventions: camelCase, PascalCase, snake_case, kebab-case, SCREAMING_SNAKE_CASE, dot.case, Title Case and Sentence case.

Every conversion is built on one universal tokenizer. Split breaks any input into normalized words; each style is then a different rendering of those words. Because a single tokenizer feeds every renderer, the converters are total — they never return an error and never need to know the input's original style.

scs.ToSnake("HTTPServerID") // "http_server_id"
scs.ToCamel("user_id")      // "userId"
scs.ToKebab("HelloWorld")   // "hello-world"

Features

  • Eight case styles from a single word model.
  • Total functions — any string maps to a well-defined result, no errors.
  • Predictable, documented rules for acronyms, digits and Unicode.
  • Opt-in Go-style initialisms (ID, URL, HTTP) via a reusable, concurrency-safe Caser.
  • Detect with an honest contract — it commits to a style only when the answer is unambiguous.
  • Public tokenizer: Split (slice) and Words (iter.Seq).
  • Zero dependencies.

Installation

go get github.com/goloop/scs/v2
import "github.com/goloop/scs/v2"

Requires Go 1.24 or newer. The package has no third-party dependencies.

Quick start

package main

import (
    "fmt"

    "github.com/goloop/scs/v2"
)

func main() {
    fmt.Println(scs.ToCamel("hello_world"))     // helloWorld
    fmt.Println(scs.ToPascal("hello-world"))    // HelloWorld
    fmt.Println(scs.ToSnake("HelloWorld"))      // hello_world
    fmt.Println(scs.ToKebab("helloWorld"))      // hello-world
    fmt.Println(scs.ToScreamingSnake("userID")) // USER_ID
    fmt.Println(scs.ToSentence("hello_world"))  // Hello world

    // Style chosen at runtime (config, CLI flag, ...).
    style, _ := scs.ParseStyle("kebab")
    fmt.Println(scs.Convert(style, "HTTPServerID")) // http-server-id

    // Go-style all-caps initialisms are opt-in via a reusable Caser.
    c := scs.New(scs.WithAcronyms("ID", "URL", "HTTP"))
    fmt.Println(c.ToPascal("user_id")) // UserID
}

Documentation

Contributing

Contributions are welcome. Please run go test ./..., go vet ./... and gofmt -l . before submitting a pull request.

License

scs is released under the MIT License. See LICENSE.

Documentation

Overview

Package scs (String Case Style) converts identifiers between naming conventions: camelCase, PascalCase, snake_case, kebab-case, SCREAMING_SNAKE_CASE, dot.case and Title Case.

Model

Every conversion is built on one universal tokenizer. Split breaks any input into normalized words by honoring separators, case transitions and acronym boundaries; each style is then a different rendering of those words:

scs.ToSnake("HTTPServerID")  // "http_server_id"
scs.ToCamel("user_id")       // "userId"
scs.ToKebab("HelloWorld")    // "hello-world"

Because a single tokenizer feeds every renderer, the converters are total: they never return an error and never need to know the input's original style. Any string maps to a well-defined result.

Initialisms

By default words are Title-cased ("Id", "Url", "Http"), which always round-trips. To follow the Go convention of all-caps initialisms, build a Caser with preserved acronyms:

c := scs.New(scs.WithAcronyms("ID", "URL", "HTTP"))
c.ToPascal("user_id") // "UserID"

Numbers

Digits attach to neighboring letters and never split a word on their own, so identifier fragments stay intact ("utf8", "sha256", "oauth2"). Only an explicit separator turns a number into its own word ("web 2 print" -> "web_2_print").

Thread safety

All package-level functions and all *Caser methods are safe for concurrent use; a Caser is immutable once built.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Convert

func Convert(to Style, s string) string

Convert renders s in the requested style using the default configuration. An Unknown or out-of-range style returns s unchanged, so Convert is total and never panics.

scs.Convert(scs.Kebab, "userID") // "user-id"
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	for _, style := range []scs.Style{scs.Snake, scs.Kebab, scs.Camel} {
		fmt.Printf("%-6s %s\n", style, scs.Convert(style, "HTTPServerID"))
	}
}
Output:
snake  http_server_id
kebab  http-server-id
camel  httpServerId

func Is

func Is(style Style, s string) bool

Is reports whether s is already canonical for the given style. An invalid style always reports false.

func IsCamel

func IsCamel(s string) bool

IsCamel reports whether s is already in canonical camelCase, i.e. it is non-empty and ToCamel leaves it unchanged.

Note that a single lowercase word such as "api" is canonical in several styles at once (camel, snake, kebab, dot); use Detect when you need a single unambiguous answer.

func IsDot

func IsDot(s string) bool

IsDot reports whether s is already in canonical dot.case.

func IsKebab

func IsKebab(s string) bool

IsKebab reports whether s is already in canonical kebab-case.

func IsPascal

func IsPascal(s string) bool

IsPascal reports whether s is already in canonical PascalCase.

func IsScreamingSnake

func IsScreamingSnake(s string) bool

IsScreamingSnake reports whether s is already in canonical SCREAMING_SNAKE_CASE.

func IsSentence

func IsSentence(s string) bool

IsSentence reports whether s is already in canonical Sentence case.

func IsSnake

func IsSnake(s string) bool

IsSnake reports whether s is already in canonical snake_case.

func IsTitle

func IsTitle(s string) bool

IsTitle reports whether s is already in canonical Title Case.

func Split

func Split(s string) []string

Split returns the normalized (lowercased) words of s, as detected by the package's universal tokenizer. It is the building block every converter shares: ToSnake(s) is the words joined by "_", ToCamel(s) is the words rendered camelCase, and so on.

The result is empty for an empty string or a string of separators only.

scs.Split("HTTPServerID")  // ["http", "server", "id"]
scs.Split("user_id")       // ["user", "id"]
scs.Split("web2print")     // ["web2print"]
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	fmt.Println(scs.Split("HTTPServerID"))
	fmt.Println(scs.Split("web2print"))
}
Output:
[http server id]
[web2print]

func ToCamel

func ToCamel(s string) string

ToCamel converts s to camelCase using the default configuration.

scs.ToCamel("hello_world") // "helloWorld"
scs.ToCamel("HelloWorld")  // "helloWorld"
scs.ToCamel("HTTP server")  // "httpServer"
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	fmt.Println(scs.ToCamel("hello_world"))
	fmt.Println(scs.ToCamel("HelloWorld"))
	fmt.Println(scs.ToCamel("HTTP server"))
}
Output:
helloWorld
helloWorld
httpServer

func ToDot

func ToDot(s string) string

ToDot converts s to dot.case.

scs.ToDot("helloWorld") // "hello.world"
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	fmt.Println(scs.ToDot("HelloWorld"))
}
Output:
hello.world

func ToKebab

func ToKebab(s string) string

ToKebab converts s to kebab-case.

scs.ToKebab("helloWorld") // "hello-world"
scs.ToKebab("HelloWorld") // "hello-world"
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	fmt.Println(scs.ToKebab("helloWorld"))
}
Output:
hello-world

func ToPascal

func ToPascal(s string) string

ToPascal converts s to PascalCase using the default configuration.

scs.ToPascal("hello_world") // "HelloWorld"
scs.ToPascal("helloWorld")  // "HelloWorld"
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	fmt.Println(scs.ToPascal("hello-world"))
	fmt.Println(scs.ToPascal("userID"))
}
Output:
HelloWorld
UserId

func ToScreamingSnake

func ToScreamingSnake(s string) string

ToScreamingSnake converts s to SCREAMING_SNAKE_CASE, the usual style for constants and environment variables.

scs.ToScreamingSnake("helloWorld") // "HELLO_WORLD"
scs.ToScreamingSnake("userID")     // "USER_ID"
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	fmt.Println(scs.ToScreamingSnake("userID"))
}
Output:
USER_ID

func ToSentence

func ToSentence(s string) string

ToSentence converts s to Sentence case (only the first word capitalized).

scs.ToSentence("hello_world") // "Hello world"
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	fmt.Println(scs.ToSentence("hello_world"))
	fmt.Println(scs.ToSentence("parseHTTPResponse"))
}
Output:
Hello world
Parse http response

func ToSnake

func ToSnake(s string) string

ToSnake converts s to snake_case.

scs.ToSnake("helloWorld") // "hello_world"
scs.ToSnake("HTTPServer")  // "http_server"
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	fmt.Println(scs.ToSnake("helloWorld"))
	fmt.Println(scs.ToSnake("HTTPServer"))
}
Output:
hello_world
http_server

func ToTitle

func ToTitle(s string) string

ToTitle converts s to Title Case.

scs.ToTitle("hello_world") // "Hello World"
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	fmt.Println(scs.ToTitle("hello_world"))
}
Output:
Hello World

func Words

func Words(s string) iter.Seq[string]

Words returns an iterator over the normalized words of s. It yields exactly the same tokens as Split without materializing a slice, which lets callers build custom renderings cheaply.

for w := range scs.Words("HTTPServerID") {
	fmt.Println(w) // http, server, id
}
Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	for w := range scs.Words("parseJSONResponse") {
		fmt.Println(w)
	}
}
Output:
parse
json
response

Types

type Caser

type Caser struct {
	// contains filtered or unexported fields
}

Caser is a reusable, immutable converter configuration. The package-level functions (ToCamel, ToSnake, ...) use a zero-configuration Caser that always renders Title-cased words; build a custom one with New when you need extra behavior such as preserved initialisms.

A Caser is safe for concurrent use by multiple goroutines: it is never mutated after New returns.

func New

func New(opts ...Option) *Caser

New returns a Caser configured by the given options. With no options it is equivalent to the package-level functions.

Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	// Opt in to Go-style initialisms.
	c := scs.New(scs.WithAcronyms("ID", "URL", "HTTP"))
	fmt.Println(c.ToPascal("user_id"))
	fmt.Println(c.ToCamel("http_url_builder"))
}
Output:
UserID
httpURLBuilder

func (*Caser) Convert

func (c *Caser) Convert(to Style, s string) string

Convert renders s in the requested style. An Unknown or out-of-range style returns s unchanged, so Convert is always total.

func (*Caser) ToCamel

func (c *Caser) ToCamel(s string) string

ToCamel renders s as camelCase.

func (*Caser) ToDot

func (c *Caser) ToDot(s string) string

ToDot renders s as dot.case.

func (*Caser) ToKebab

func (c *Caser) ToKebab(s string) string

ToKebab renders s as kebab-case.

func (*Caser) ToPascal

func (c *Caser) ToPascal(s string) string

ToPascal renders s as PascalCase.

func (*Caser) ToScreamingSnake

func (c *Caser) ToScreamingSnake(s string) string

ToScreamingSnake renders s as SCREAMING_SNAKE_CASE.

func (*Caser) ToSentence

func (c *Caser) ToSentence(s string) string

ToSentence renders s as Sentence case: the first word is capitalized, the rest stay lowercase (initialisms excepted), joined by single spaces.

func (*Caser) ToSnake

func (c *Caser) ToSnake(s string) string

ToSnake renders s as snake_case.

func (*Caser) ToTitle

func (c *Caser) ToTitle(s string) string

ToTitle renders s as Title Case (capitalized words joined by spaces).

type Option

type Option func(*Caser)

Option configures a Caser. Options are applied by New in order.

func WithAcronyms

func WithAcronyms(words ...string) Option

WithAcronyms makes the Caser render the given words as all-caps initialisms in the camelCase, PascalCase and Title styles, matching the Go convention of writing "ID", "URL" or "HTTP" instead of "Id", "Url" or "Http". Matching is case-insensitive on word boundaries detected by the tokenizer.

c := scs.New(scs.WithAcronyms("ID", "URL", "HTTP"))
c.ToPascal("user_id_url") // "UserIDURL"
c.ToCamel("http_server")  // "httpServer"  (first word stays lowercase)

Initialisms are an opt-in convenience, not the default, because adjacent all-caps acronyms cannot always be split back apart: "HTTPAPI" is ambiguous between [HTTP, API] and other partitions. The zero-configuration Caser keeps Title casing, which always round-trips.

type Style

type Style uint8

Style enumerates the naming conventions the package can render to.

The zero value is Unknown and is never produced by a converter; it is returned by Detect when the input does not map unambiguously to a single style. Use Valid to test whether a Style names a real convention.

const (
	// Unknown is the zero value: not a real style. Detect returns it for
	// inputs that match no style or match several at once.
	Unknown Style = iota

	// Camel is camelCase: words joined, first word lowercase, every
	// subsequent word capitalized (e.g. "helloWorld").
	Camel

	// Pascal is PascalCase: words joined, every word capitalized including
	// the first (e.g. "HelloWorld").
	Pascal

	// Snake is snake_case: lowercase words joined by underscores
	// (e.g. "hello_world").
	Snake

	// Kebab is kebab-case: lowercase words joined by hyphens
	// (e.g. "hello-world").
	Kebab

	// ScreamingSnake is SCREAMING_SNAKE_CASE: uppercase words joined by
	// underscores (e.g. "HELLO_WORLD"). Common for constants and env vars.
	ScreamingSnake

	// Dot is dot.case: lowercase words joined by dots (e.g. "hello.world").
	Dot

	// Title is Title Case: capitalized words joined by single spaces
	// (e.g. "Hello World").
	Title

	// Sentence is Sentence case: only the first word is capitalized, the rest
	// stay lowercase, joined by single spaces (e.g. "Hello world").
	Sentence
)

func Detect

func Detect(s string) (Style, bool)

Detect returns the single style for which s is already canonical. If s is empty, matches no style, or matches more than one style (for example the bare word "api", which is valid camel, snake, kebab and dot at once), it returns Unknown and false.

scs.Detect("user_id")  // Snake, true
scs.Detect("userId")   // Camel, true
scs.Detect("USER_ID")  // ScreamingSnake, true
scs.Detect("api")      // Unknown, false (ambiguous)
scs.Detect("Hello_World") // Unknown, false (no canonical style)

The input is scanned once for its separator so that only the styles which could possibly match are probed; a string mixing two separators, or carrying any other punctuation, is rejected without conversion.

Example
package main

import (
	"fmt"

	"github.com/goloop/scs/v2"
)

func main() {
	for _, s := range []string{"user_id", "userId", "API", "api"} {
		style, ok := scs.Detect(s)
		fmt.Printf("%q -> %v (%v)\n", s, style, ok)
	}
}
Output:
"user_id" -> snake (true)
"userId" -> camel (true)
"API" -> screaming_snake (true)
"api" -> unknown (false)

func ParseStyle

func ParseStyle(name string) (Style, bool)

ParseStyle maps a style identifier (as produced by Style.String) to its Style. The match is exact and case-sensitive. The second result is false for unrecognized names, in which case the Style is Unknown.

A few common aliases are accepted: "constant" and "screaming" for ScreamingSnake.

func (Style) String

func (s Style) String() string

String returns the lowercase identifier of the style, suitable for flags, configuration files and diagnostics. Unknown and out-of-range values return "unknown".

func (Style) Valid

func (s Style) Valid() bool

Valid reports whether s names a real naming convention (i.e. is not Unknown and is within the known range).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL