diff --git a/libs/tags/aws.go b/libs/tags/aws.go index 44d69c683e8..3272ec99c67 100644 --- a/libs/tags/aws.go +++ b/libs/tags/aws.go @@ -4,6 +4,8 @@ import ( "regexp" "unicode" + "github.com/databricks/cli/libs/textutil" + "golang.org/x/text/unicode/rangetable" ) @@ -20,17 +22,17 @@ var awsChars = rangetable.Merge( var awsTag = &tag{ keyLength: 127, keyPattern: regexp.MustCompile(`^[\d \w\+\-=\.:\/@]*$`), - keyNormalize: chain( - normalizeMarks(), - replaceNotIn(latin1, '_'), - replaceNotIn(awsChars, '_'), + keyNormalize: textutil.Chain( + textutil.NormalizeMarks(), + textutil.ReplaceNotIn(textutil.Latin1, '_'), + textutil.ReplaceNotIn(awsChars, '_'), ), valueLength: 255, valuePattern: regexp.MustCompile(`^[\d \w\+\-=\.:/@]*$`), - valueNormalize: chain( - normalizeMarks(), - replaceNotIn(latin1, '_'), - replaceNotIn(awsChars, '_'), + valueNormalize: textutil.Chain( + textutil.NormalizeMarks(), + textutil.ReplaceNotIn(textutil.Latin1, '_'), + textutil.ReplaceNotIn(awsChars, '_'), ), } diff --git a/libs/tags/azure.go b/libs/tags/azure.go index e98a5eb2d41..4b58a5b9e65 100644 --- a/libs/tags/azure.go +++ b/libs/tags/azure.go @@ -3,6 +3,8 @@ package tags import ( "regexp" + "github.com/databricks/cli/libs/textutil" + "golang.org/x/text/unicode/rangetable" ) @@ -12,14 +14,14 @@ var azureForbiddenChars = rangetable.New('<', '>', '*', '&', '%', ';', '\\', '/' var azureTag = &tag{ keyLength: 512, keyPattern: regexp.MustCompile(`^[^<>\*&%;\\\/\+\?]*$`), - keyNormalize: chain( - replaceNotIn(latin1, '_'), - replaceIn(azureForbiddenChars, '_'), + keyNormalize: textutil.Chain( + textutil.ReplaceNotIn(textutil.Latin1, '_'), + textutil.ReplaceIn(azureForbiddenChars, '_'), ), valueLength: 256, valuePattern: regexp.MustCompile(`^.*$`), - valueNormalize: chain( - replaceNotIn(latin1, '_'), + valueNormalize: textutil.Chain( + textutil.ReplaceNotIn(textutil.Latin1, '_'), ), } diff --git a/libs/tags/gcp.go b/libs/tags/gcp.go index f30ca4cae00..9fed16d4bd1 100644 --- a/libs/tags/gcp.go +++ b/libs/tags/gcp.go @@ -3,6 +3,8 @@ package tags import ( "regexp" "unicode" + + "github.com/databricks/cli/libs/textutil" ) // Tag keys and values on GCP are limited to 63 characters and must match the @@ -45,19 +47,19 @@ var gcpInner = &unicode.RangeTable{ var gcpTag = &tag{ keyLength: 63, keyPattern: regexp.MustCompile(`^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$`), - keyNormalize: chain( - normalizeMarks(), - replaceNotIn(latin1, '_'), - replaceNotIn(gcpInner, '_'), - trimIfNotIn(gcpOuter), + keyNormalize: textutil.Chain( + textutil.NormalizeMarks(), + textutil.ReplaceNotIn(textutil.Latin1, '_'), + textutil.ReplaceNotIn(gcpInner, '_'), + textutil.TrimIfNotIn(gcpOuter), ), valueLength: 63, valuePattern: regexp.MustCompile(`^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$`), - valueNormalize: chain( - normalizeMarks(), - replaceNotIn(latin1, '_'), - replaceNotIn(gcpInner, '_'), - trimIfNotIn(gcpOuter), + valueNormalize: textutil.Chain( + textutil.NormalizeMarks(), + textutil.ReplaceNotIn(textutil.Latin1, '_'), + textutil.ReplaceNotIn(gcpInner, '_'), + textutil.TrimIfNotIn(gcpOuter), ), } diff --git a/libs/tags/latin_test.go b/libs/tags/latin_test.go deleted file mode 100644 index c3234a44357..00000000000 --- a/libs/tags/latin_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package tags - -import ( - "testing" - "unicode" - - "github.com/stretchr/testify/assert" -) - -func TestLatinTable(t *testing.T) { - assert.True(t, unicode.In('\u0000', latin1)) - assert.True(t, unicode.In('A', latin1)) - assert.True(t, unicode.In('Z', latin1)) - assert.True(t, unicode.In('\u00ff', latin1)) - assert.False(t, unicode.In('\u0100', latin1)) -} diff --git a/libs/tags/tag.go b/libs/tags/tag.go index 64eab947e2c..dc2c479d2db 100644 --- a/libs/tags/tag.go +++ b/libs/tags/tag.go @@ -6,6 +6,8 @@ import ( "regexp" "strings" "unicode" + + "github.com/databricks/cli/libs/textutil" ) // The tag type holds the validation and normalization rules for @@ -13,11 +15,11 @@ import ( type tag struct { keyLength int keyPattern *regexp.Regexp - keyNormalize transformer + keyNormalize textutil.Transformer valueLength int valuePattern *regexp.Regexp - valueNormalize transformer + valueNormalize textutil.Transformer } func (t *tag) ValidateKey(s string) error { @@ -27,7 +29,7 @@ func (t *tag) ValidateKey(s string) error { if len(s) > t.keyLength { return fmt.Errorf("key length %d exceeds maximum of %d", len(s), t.keyLength) } - if strings.ContainsFunc(s, func(r rune) bool { return !unicode.Is(latin1, r) }) { + if strings.ContainsFunc(s, func(r rune) bool { return !unicode.Is(textutil.Latin1, r) }) { return errors.New("key contains non-latin1 characters") } if !t.keyPattern.MatchString(s) { @@ -40,7 +42,7 @@ func (t *tag) ValidateValue(s string) error { if len(s) > t.valueLength { return fmt.Errorf("value length %d exceeds maximum of %d", len(s), t.valueLength) } - if strings.ContainsFunc(s, func(r rune) bool { return !unicode.Is(latin1, r) }) { + if strings.ContainsFunc(s, func(r rune) bool { return !unicode.Is(textutil.Latin1, r) }) { return errors.New("value contains non-latin1 characters") } if !t.valuePattern.MatchString(s) { @@ -50,9 +52,9 @@ func (t *tag) ValidateValue(s string) error { } func (t *tag) NormalizeKey(s string) string { - return t.keyNormalize.transform(s) + return t.keyNormalize.TransformString(s) } func (t *tag) NormalizeValue(s string) string { - return t.valueNormalize.transform(s) + return t.valueNormalize.TransformString(s) } diff --git a/libs/tags/transform.go b/libs/tags/transform.go deleted file mode 100644 index 71d01b35633..00000000000 --- a/libs/tags/transform.go +++ /dev/null @@ -1,87 +0,0 @@ -package tags - -import ( - "strings" - "unicode" - - "golang.org/x/text/runes" - "golang.org/x/text/transform" - "golang.org/x/text/unicode/norm" -) - -type transformer interface { - transform(string) string -} - -type chainTransformer []transformer - -func (c chainTransformer) transform(s string) string { - for _, t := range c { - s = t.transform(s) - } - return s -} - -func chain(t ...transformer) transformer { - return chainTransformer(t) -} - -// Implement [transformer] interface with text/transform package. -type textTransformer struct { - transform.Transformer -} - -func (t textTransformer) transform(s string) string { - s, _, _ = transform.String(t, s) - return s -} - -func normalizeMarks() transformer { - // Decompose unicode characters, then remove all non-spacing marks, then recompose. - // This turns 'é' into 'e' and 'ü' into 'u'. - return textTransformer{ - transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC), - } -} - -// Replaces characters in the given set with replacement. -type replaceTransformer struct { - set runes.Set - replacement rune -} - -func (t replaceTransformer) transform(s string) string { - return strings.Map(func(r rune) rune { - if t.set.Contains(r) { - return t.replacement - } - return r - }, s) -} - -func replaceIn(table *unicode.RangeTable, replacement rune) transformer { - return replaceTransformer{runes.In(table), replacement} -} - -func replaceNotIn(table *unicode.RangeTable, replacement rune) transformer { - return replaceTransformer{runes.NotIn(table), replacement} -} - -// Trims the given string of all characters in the given set. -type trimTransformer struct { - set runes.Set -} - -func (t trimTransformer) transform(s string) string { - return strings.TrimFunc(s, func(r rune) bool { - return t.set.Contains(r) - }) -} - -func trimIfIn(table *unicode.RangeTable) transformer { - return trimTransformer{runes.In(table)} -} - -func trimIfNotIn(table *unicode.RangeTable) transformer { - return trimTransformer{runes.NotIn(table)} -} diff --git a/libs/tags/transform_test.go b/libs/tags/transform_test.go deleted file mode 100644 index 6481b6d9bcb..00000000000 --- a/libs/tags/transform_test.go +++ /dev/null @@ -1,25 +0,0 @@ -package tags - -import ( - "testing" - "unicode" - - "github.com/stretchr/testify/assert" -) - -func TestNormalizeMarks(t *testing.T) { - x := normalizeMarks() - assert.Equal(t, "cafe", x.transform("café")) - assert.Equal(t, "cafe 🍎", x.transform("café 🍎")) - assert.Equal(t, "Foo Bar", x.transform("Foo Bar")) -} - -func TestReplace(t *testing.T) { - assert.Equal(t, "___abc___", replaceIn(unicode.Digit, '_').transform("000abc999")) - assert.Equal(t, "___000___", replaceNotIn(unicode.Digit, '_').transform("abc000abc")) -} - -func TestTrim(t *testing.T) { - assert.Equal(t, "abc", trimIfIn(unicode.Digit).transform("000abc999")) - assert.Equal(t, "000", trimIfNotIn(unicode.Digit).transform("abc000abc")) -} diff --git a/libs/tags/latin.go b/libs/textutil/latin.go similarity index 75% rename from libs/tags/latin.go rename to libs/textutil/latin.go index df9ad403e7e..87a111793c3 100644 --- a/libs/tags/latin.go +++ b/libs/textutil/latin.go @@ -1,9 +1,9 @@ -package tags +package textutil import "unicode" // Range table for all characters in the Latin1 character set. -var latin1 = &unicode.RangeTable{ +var Latin1 = &unicode.RangeTable{ R16: []unicode.Range16{ {0x0000, 0x00ff, 1}, }, diff --git a/libs/textutil/latin_test.go b/libs/textutil/latin_test.go new file mode 100644 index 00000000000..3d5734ddde1 --- /dev/null +++ b/libs/textutil/latin_test.go @@ -0,0 +1,16 @@ +package textutil + +import ( + "testing" + "unicode" + + "github.com/stretchr/testify/assert" +) + +func TestLatinTable(t *testing.T) { + assert.True(t, unicode.In('\u0000', Latin1)) + assert.True(t, unicode.In('A', Latin1)) + assert.True(t, unicode.In('Z', Latin1)) + assert.True(t, unicode.In('\u00ff', Latin1)) + assert.False(t, unicode.In('\u0100', Latin1)) +} diff --git a/libs/textutil/transform.go b/libs/textutil/transform.go new file mode 100644 index 00000000000..a37906b9dd8 --- /dev/null +++ b/libs/textutil/transform.go @@ -0,0 +1,94 @@ +package textutil + +import ( + "strings" + "unicode" + + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +// Transformer represents a text transformation operation. +type Transformer interface { + TransformString(string) string +} + +type chainTransformer []Transformer + +func (c chainTransformer) TransformString(s string) string { + for _, t := range c { + s = t.TransformString(s) + } + return s +} + +// Chain creates a transformer that applies multiple transformers in sequence. +func Chain(t ...Transformer) Transformer { + return chainTransformer(t) +} + +// Implement [Transformer] interface with text/transform package. +type textTransformer struct { + transform.Transformer +} + +func (t textTransformer) TransformString(s string) string { + s, _, _ = transform.String(t, s) + return s +} + +// NormalizeMarks creates a transformer that removes diacritical marks from characters. +// This turns 'é' into 'e' and 'ü' into 'u'. +func NormalizeMarks() Transformer { + // Decompose unicode characters, then remove all non-spacing marks, then recompose + return textTransformer{ + transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC), + } +} + +// Replaces characters in the given set with replacement. +type replaceTransformer struct { + set runes.Set + replacement rune +} + +func (t replaceTransformer) TransformString(s string) string { + return strings.Map(func(r rune) rune { + if t.set.Contains(r) { + return t.replacement + } + return r + }, s) +} + +// ReplaceIn creates a transformer that replaces characters within the given Unicode range table with the replacement rune. +func ReplaceIn(table *unicode.RangeTable, replacement rune) Transformer { + return replaceTransformer{runes.In(table), replacement} +} + +// ReplaceNotIn creates a transformer that replaces characters NOT within the given Unicode range table with the replacement rune. +func ReplaceNotIn(table *unicode.RangeTable, replacement rune) Transformer { + return replaceTransformer{runes.NotIn(table), replacement} +} + +// Trims the given string of all characters in the given set. +type trimTransformer struct { + set runes.Set +} + +func (t trimTransformer) TransformString(s string) string { + return strings.TrimFunc(s, func(r rune) bool { + return t.set.Contains(r) + }) +} + +// TrimIfIn creates a transformer that trims characters from the beginning and end of strings if they are within the given Unicode range table. +func TrimIfIn(table *unicode.RangeTable) Transformer { + return trimTransformer{runes.In(table)} +} + +// TrimIfNotIn creates a transformer that trims characters from the beginning and end of strings if they are NOT within the given Unicode range table. +func TrimIfNotIn(table *unicode.RangeTable) Transformer { + return trimTransformer{runes.NotIn(table)} +} diff --git a/libs/textutil/transform_test.go b/libs/textutil/transform_test.go new file mode 100644 index 00000000000..9f39139c0ae --- /dev/null +++ b/libs/textutil/transform_test.go @@ -0,0 +1,25 @@ +package textutil + +import ( + "testing" + "unicode" + + "github.com/stretchr/testify/assert" +) + +func TestNormalizeMarks(t *testing.T) { + x := NormalizeMarks() + assert.Equal(t, "cafe", x.TransformString("café")) + assert.Equal(t, "cafe 🍎", x.TransformString("café 🍎")) + assert.Equal(t, "Foo Bar", x.TransformString("Foo Bar")) +} + +func TestReplace(t *testing.T) { + assert.Equal(t, "___abc___", ReplaceIn(unicode.Digit, '_').TransformString("000abc999")) + assert.Equal(t, "___000___", ReplaceNotIn(unicode.Digit, '_').TransformString("abc000abc")) +} + +func TestTrim(t *testing.T) { + assert.Equal(t, "abc", TrimIfIn(unicode.Digit).TransformString("000abc999")) + assert.Equal(t, "000", TrimIfNotIn(unicode.Digit).TransformString("abc000abc")) +}