Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lang/funcs: Add camelCase, kebab-case and snake_case functions #27357

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
77 changes: 77 additions & 0 deletions lang/funcs/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,85 @@ var ReplaceFunc = function.New(&function.Spec{
},
})

var SnakeCaseFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0].AsString()
// Remove non alpha-numerics
out := regexp.MustCompile(`(?m)[^a-zA-Z0-9]`).ReplaceAllString(in, "_")
// Split on uppercase characters followed by lower case (e.g. camel case)
out = regexp.MustCompile(`(?m)[A-Z][a-z]`).ReplaceAllString(out, "_$0")
// Remove any consecutive underscores
out = regexp.MustCompile(`(?m)_+`).ReplaceAllString(out, "_")
// Remove leading/trailing underscore
out = regexp.MustCompile(`^_|_$`).ReplaceAllString(out, "")
return cty.StringVal(strings.ToLower(out)), nil
},
})

var KebabCaseFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0]
out, _ := SnakeCase(in)
return cty.StringVal(strings.ReplaceAll(out.AsString(), "_", "-")), nil
},
})

var CamelCaseFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowDynamicType: true,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
in := args[0]
snake, _ := SnakeCase(in)
words := strings.ReplaceAll(snake.AsString(), "_", " ")
pascal := strings.ReplaceAll(strings.Title(words), " ", "")

if pascal != "" {
camel := string(strings.ToLower(pascal)[0]) + pascal[1:]
return cty.StringVal(camel), nil
}
return cty.StringVal(""), nil
},
})

// Replace searches a given string for another given substring,
// and replaces all occurences with a given replacement string.
func Replace(str, substr, replace cty.Value) (cty.Value, error) {
return ReplaceFunc.Call([]cty.Value{str, substr, replace})
}

// SnakeCase is a Function that converts a given string to snake_case.
func SnakeCase(str cty.Value) (cty.Value, error) {
return SnakeCaseFunc.Call([]cty.Value{str})
}

// KebabCase is a Function that converts a given string to kebab-case.
func KebabCase(str cty.Value) (cty.Value, error) {
return KebabCaseFunc.Call([]cty.Value{str})
}

// KebabCase is a Function that converts a given string to kebab-case.
Copy link

@sikachu sikachu Nov 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for jumping in, but I just noticed this typo in the comment

Suggested change
// KebabCase is a Function that converts a given string to kebab-case.
// CamelCase is a Function that converts a given string to camelCase.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// CamelCase is a Function that converts a given string to camelCase.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thank you!

func CamelCase(str cty.Value) (cty.Value, error) {
return CamelCaseFunc.Call([]cty.Value{str})
}
198 changes: 198 additions & 0 deletions lang/funcs/string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,201 @@ func TestReplace(t *testing.T) {
})
}
}

func TestSnakeCase(t *testing.T) {
tests := []struct {
Input cty.Value
Want cty.Value
}{
{
cty.StringVal("hello_world"),
cty.StringVal("hello_world"),
},
{
cty.StringVal("HelloWorld"),
cty.StringVal("hello_world"),
},
{
cty.StringVal("ABC"),
cty.StringVal("abc"),
},
{
cty.StringVal("ABCd"),
cty.StringVal("ab_cd"),
},
{
cty.StringVal("_hello_world_"),
cty.StringVal("hello_world"),
},
{
cty.StringVal("snake_case"),
cty.StringVal("snake_case"),
},
{
cty.StringVal("PascalCase"),
cty.StringVal("pascal_case"),
},
{
cty.StringVal("camelCase"),
cty.StringVal("camel_case"),
},
{
cty.StringVal("kebab-case"),
cty.StringVal("kebab_case"),
},
{
cty.StringVal(""),
cty.StringVal(""),
},
{
cty.StringVal("1"),
cty.StringVal("1"),
},
}

for _, test := range tests {
t.Run(test.Input.GoString(), func(t *testing.T) {
got, err := SnakeCase(test.Input)

if err != nil {
t.Fatalf("unexpected error: %s", err)
}

if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

func TestKebabCase(t *testing.T) {
tests := []struct {
Input cty.Value
Want cty.Value
}{
{
cty.StringVal("hello_world"),
cty.StringVal("hello-world"),
},
{
cty.StringVal("HelloWorld"),
cty.StringVal("hello-world"),
},
{
cty.StringVal("ABC"),
cty.StringVal("abc"),
},
{
cty.StringVal("ABCd"),
cty.StringVal("ab-cd"),
},
{
cty.StringVal("_hello_world_"),
cty.StringVal("hello-world"),
},
{
cty.StringVal("snake_case"),
cty.StringVal("snake-case"),
},
{
cty.StringVal("PascalCase"),
cty.StringVal("pascal-case"),
},
{
cty.StringVal("camelCase"),
cty.StringVal("camel-case"),
},
{
cty.StringVal("kebab-case"),
cty.StringVal("kebab-case"),
},
{
cty.StringVal(""),
cty.StringVal(""),
},
{
cty.StringVal("1"),
cty.StringVal("1"),
},
}

for _, test := range tests {
t.Run(test.Input.GoString(), func(t *testing.T) {
got, err := KebabCase(test.Input)

if err != nil {
t.Fatalf("unexpected error: %s", err)
}

if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

func TestCamelCase(t *testing.T) {
tests := []struct {
Input cty.Value
Want cty.Value
}{
{
cty.StringVal("hello_world"),
cty.StringVal("helloWorld"),
},
{
cty.StringVal("HelloWorld"),
cty.StringVal("helloWorld"),
},
{
cty.StringVal("ABC"),
cty.StringVal("abc"),
},
{
cty.StringVal("ABCd"),
cty.StringVal("abCd"),
},
{
cty.StringVal("_hello_world_"),
cty.StringVal("helloWorld"),
},
{
cty.StringVal("snake_case"),
cty.StringVal("snakeCase"),
},
{
cty.StringVal("PascalCase"),
cty.StringVal("pascalCase"),
},
{
cty.StringVal("camelCase"),
cty.StringVal("camelCase"),
},
{
cty.StringVal("kebab-case"),
cty.StringVal("kebabCase"),
},
{
cty.StringVal(""),
cty.StringVal(""),
},
{
cty.StringVal("1"),
cty.StringVal("1"),
},
}

for _, test := range tests {
t.Run(test.Input.GoString(), func(t *testing.T) {
got, err := CamelCase(test.Input)

if err != nil {
t.Fatalf("unexpected error: %s", err)
}

if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}
3 changes: 3 additions & 0 deletions lang/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (s *Scope) Functions() map[string]function.Function {
"base64sha256": funcs.Base64Sha256Func,
"base64sha512": funcs.Base64Sha512Func,
"bcrypt": funcs.BcryptFunc,
"camelcase": funcs.CamelCaseFunc,
"can": tryfunc.CanFunc,
"ceil": stdlib.CeilFunc,
"chomp": stdlib.ChompFunc,
Expand Down Expand Up @@ -81,6 +82,7 @@ func (s *Scope) Functions() map[string]function.Function {
"join": stdlib.JoinFunc,
"jsondecode": stdlib.JSONDecodeFunc,
"jsonencode": stdlib.JSONEncodeFunc,
"kebabcase": funcs.KebabCaseFunc,
"keys": stdlib.KeysFunc,
"length": funcs.LengthFunc,
"list": funcs.ListFunc,
Expand Down Expand Up @@ -111,6 +113,7 @@ func (s *Scope) Functions() map[string]function.Function {
"sha512": funcs.Sha512Func,
"signum": stdlib.SignumFunc,
"slice": stdlib.SliceFunc,
"snakecase": funcs.SnakeCaseFunc,
"sort": stdlib.SortFunc,
"split": stdlib.SplitFunc,
"strrev": stdlib.ReverseFunc,
Expand Down
21 changes: 21 additions & 0 deletions lang/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ func TestFunctions(t *testing.T) {
},
},

"camelcase": {
{
`camelcase("foo-bar_baz1 qux quuxCorge")`,
cty.StringVal("fooBarBaz1QuxQuuxCorge"),
},
},

"can": {
{
`can(true)`,
Expand Down Expand Up @@ -507,6 +514,13 @@ func TestFunctions(t *testing.T) {
},
},

"kebabcase": {
{
`kebabcase("foo-bar_baz1 qux quuxCorge")`,
cty.StringVal("foo-bar-baz1-qux-quux-corge"),
},
},

"keys": {
{
`keys({"hello"=1, "goodbye"=42})`,
Expand Down Expand Up @@ -777,6 +791,13 @@ func TestFunctions(t *testing.T) {
},
},

"snakecase": {
{
`snakecase("foo-bar_baz1 qux quuxCorge")`,
cty.StringVal("foo_bar_baz1_qux_quux_corge"),
},
},

"sort": {
{
`sort(["banana", "apple"])`,
Expand Down
32 changes: 32 additions & 0 deletions website/docs/configuration/functions/camelcase.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
layout: "language"
page_title: "camelcase - Functions - Configuration Language"
sidebar_current: "docs-funcs-string-camelcase"
description: |-
The camelcase function converts a string into camelCase.
---

# `camelcase` Function

`camelcase` converts a string in to camelCase: non-alphanumeric characters are removed
and the words joined by capitalising the first letter, and lower-casing the rest.

The very first letter will be lowercased. Additionally substrings which already camelCased
will be unchanged.


## Examples

```
> camelcase("hello world")
helloWorld
> camelcase("hello-world")
helloWorld
> camelcase("helloWorld")
helloWorld
```

## Related Functions

* [`kebabcase`](./kebabcase.html) converts a string into kebab-case.
* [`snakecase`](./snakecase.html) converts a string into snake_case.
Loading