github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/amino/genproto/stringutil/snakecase.go (about)

     1  // Copyright 2020-2023 Buf Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // This file has ToLowerSnakeCase which the Protobuf linter uses to make
    16  // the expected spelling of a message field name. It is a copy of:
    17  // https://github.com/bufbuild/buf/blob/main/private/pkg/stringutil/stringutil.go
    18  
    19  package stringutil
    20  
    21  import (
    22  	"sort"
    23  	"strings"
    24  	"unicode"
    25  )
    26  
    27  // TrimLines splits the output into individual lines and trims the spaces from each line.
    28  //
    29  // This also trims the start and end spaces from the original output.
    30  func TrimLines(output string) string {
    31  	return strings.TrimSpace(strings.Join(SplitTrimLines(output), "\n"))
    32  }
    33  
    34  // SplitTrimLines splits the output into individual lines and trims the spaces from each line.
    35  func SplitTrimLines(output string) []string {
    36  	// this should work for windows as well as \r will be trimmed
    37  	split := strings.Split(output, "\n")
    38  	lines := make([]string, len(split))
    39  	for i, line := range split {
    40  		lines[i] = strings.TrimSpace(line)
    41  	}
    42  	return lines
    43  }
    44  
    45  // SplitTrimLinesNoEmpty splits the output into individual lines and trims the spaces from each line.
    46  //
    47  // This removes any empty lines.
    48  func SplitTrimLinesNoEmpty(output string) []string {
    49  	// this should work for windows as well as \r will be trimmed
    50  	split := strings.Split(output, "\n")
    51  	lines := make([]string, 0, len(split))
    52  	for _, line := range split {
    53  		line = strings.TrimSpace(line)
    54  		if line != "" {
    55  			lines = append(lines, line)
    56  		}
    57  	}
    58  	return lines
    59  }
    60  
    61  // MapToSortedSlice transforms m to a sorted slice.
    62  func MapToSortedSlice(m map[string]struct{}) []string {
    63  	s := MapToSlice(m)
    64  	sort.Strings(s)
    65  	return s
    66  }
    67  
    68  // MapToSlice transforms m to a slice.
    69  func MapToSlice(m map[string]struct{}) []string {
    70  	s := make([]string, 0, len(m))
    71  	for e := range m {
    72  		s = append(s, e)
    73  	}
    74  	return s
    75  }
    76  
    77  // SliceToMap transforms s to a map.
    78  func SliceToMap(s []string) map[string]struct{} {
    79  	m := make(map[string]struct{}, len(s))
    80  	for _, e := range s {
    81  		m[e] = struct{}{}
    82  	}
    83  	return m
    84  }
    85  
    86  // SliceToUniqueSortedSlice returns a sorted copy of s with no duplicates.
    87  func SliceToUniqueSortedSlice(s []string) []string {
    88  	return MapToSortedSlice(SliceToMap(s))
    89  }
    90  
    91  // SliceToUniqueSortedSliceFilterEmptyStrings returns a sorted copy of s with no duplicates and no empty strings.
    92  //
    93  // Strings with only spaces are considered empty.
    94  func SliceToUniqueSortedSliceFilterEmptyStrings(s []string) []string {
    95  	m := SliceToMap(s)
    96  	for key := range m {
    97  		if strings.TrimSpace(key) == "" {
    98  			delete(m, key)
    99  		}
   100  	}
   101  	return MapToSortedSlice(m)
   102  }
   103  
   104  // SliceToChunks splits s into chunks of the given chunk size.
   105  //
   106  // If s is nil or empty, returns empty.
   107  // If chunkSize is <=0, returns [][]string{s}.
   108  func SliceToChunks(s []string, chunkSize int) [][]string {
   109  	var chunks [][]string
   110  	if len(s) == 0 {
   111  		return chunks
   112  	}
   113  	if chunkSize <= 0 {
   114  		return [][]string{s}
   115  	}
   116  	c := make([]string, len(s))
   117  	copy(c, s)
   118  	// https://github.com/golang/go/wiki/SliceTricks#batching-with-minimal-allocation
   119  	for chunkSize < len(c) {
   120  		c, chunks = c[chunkSize:], append(chunks, c[0:chunkSize:chunkSize])
   121  	}
   122  	return append(chunks, c)
   123  }
   124  
   125  // SliceElementsEqual returns true if the two slices have equal elements.
   126  //
   127  // Nil and empty slices are treated as equals.
   128  func SliceElementsEqual(one []string, two []string) bool {
   129  	if len(one) != len(two) {
   130  		return false
   131  	}
   132  	for i, elem := range one {
   133  		if two[i] != elem {
   134  			return false
   135  		}
   136  	}
   137  	return true
   138  }
   139  
   140  // SliceElementsContained returns true if superset contains subset.
   141  //
   142  // Nil and empty slices are treated as equals.
   143  func SliceElementsContained(superset []string, subset []string) bool {
   144  	m := SliceToMap(superset)
   145  	for _, elem := range subset {
   146  		if _, ok := m[elem]; !ok {
   147  			return false
   148  		}
   149  	}
   150  	return true
   151  }
   152  
   153  // JoinSliceQuoted joins the slice with quotes.
   154  func JoinSliceQuoted(s []string, sep string) string {
   155  	if len(s) == 0 {
   156  		return ""
   157  	}
   158  	return `"` + strings.Join(s, `"`+sep+`"`) + `"`
   159  }
   160  
   161  // SliceToString prints the slice as [e1,e2].
   162  func SliceToString(s []string) string {
   163  	if len(s) == 0 {
   164  		return ""
   165  	}
   166  	return "[" + strings.Join(s, ",") + "]"
   167  }
   168  
   169  // SliceToHumanString prints the slice as "e1, e2, and e3".
   170  func SliceToHumanString(s []string) string {
   171  	switch len(s) {
   172  	case 0:
   173  		return ""
   174  	case 1:
   175  		return s[0]
   176  	case 2:
   177  		return s[0] + " and " + s[1]
   178  	default:
   179  		return strings.Join(s[:len(s)-1], ", ") + ", and " + s[len(s)-1]
   180  	}
   181  }
   182  
   183  // SliceToHumanStringQuoted prints the slice as `"e1", "e2", and "e3"`.
   184  func SliceToHumanStringQuoted(s []string) string {
   185  	switch len(s) {
   186  	case 0:
   187  		return ""
   188  	case 1:
   189  		return `"` + s[0] + `"`
   190  	case 2:
   191  		return `"` + s[0] + `" and "` + s[1] + `"`
   192  	default:
   193  		return `"` + strings.Join(s[:len(s)-1], `", "`) + `", and "` + s[len(s)-1] + `"`
   194  	}
   195  }
   196  
   197  // SliceToHumanStringOr prints the slice as "e1, e2, or e3".
   198  func SliceToHumanStringOr(s []string) string {
   199  	switch len(s) {
   200  	case 0:
   201  		return ""
   202  	case 1:
   203  		return s[0]
   204  	case 2:
   205  		return s[0] + " or " + s[1]
   206  	default:
   207  		return strings.Join(s[:len(s)-1], ", ") + ", or " + s[len(s)-1]
   208  	}
   209  }
   210  
   211  // SliceToHumanStringOrQuoted prints the slice as `"e1", "e2", or "e3"`.
   212  func SliceToHumanStringOrQuoted(s []string) string {
   213  	switch len(s) {
   214  	case 0:
   215  		return ""
   216  	case 1:
   217  		return `"` + s[0] + `"`
   218  	case 2:
   219  		return `"` + s[0] + `" or "` + s[1] + `"`
   220  	default:
   221  		return `"` + strings.Join(s[:len(s)-1], `", "`) + `", or "` + s[len(s)-1] + `"`
   222  	}
   223  }
   224  
   225  // SnakeCaseOption is an option for snake_case conversions.
   226  type SnakeCaseOption func(*snakeCaseOptions)
   227  
   228  // SnakeCaseWithNewWordOnDigits is a SnakeCaseOption that signifies
   229  // to split on digits, ie foo_bar_1 instead of foo_bar1.
   230  func SnakeCaseWithNewWordOnDigits() SnakeCaseOption {
   231  	return func(snakeCaseOptions *snakeCaseOptions) {
   232  		snakeCaseOptions.newWordOnDigits = true
   233  	}
   234  }
   235  
   236  // ToLowerSnakeCase transforms s to lower_snake_case.
   237  func ToLowerSnakeCase(s string, options ...SnakeCaseOption) string {
   238  	return strings.ToLower(toSnakeCase(s, options...))
   239  }
   240  
   241  // ToUpperSnakeCase transforms s to UPPER_SNAKE_CASE.
   242  func ToUpperSnakeCase(s string, options ...SnakeCaseOption) string {
   243  	return strings.ToUpper(toSnakeCase(s, options...))
   244  }
   245  
   246  // ToPascalCase converts s to PascalCase.
   247  //
   248  // Splits on '-', '_', ' ', '\t', '\n', '\r'.
   249  // Uppercase letters will stay uppercase,
   250  func ToPascalCase(s string) string {
   251  	output := ""
   252  	var previous rune
   253  	for i, c := range strings.TrimSpace(s) {
   254  		if !isDelimiter(c) {
   255  			if i == 0 || isDelimiter(previous) || unicode.IsUpper(c) {
   256  				output += string(unicode.ToUpper(c))
   257  			} else {
   258  				output += string(unicode.ToLower(c))
   259  			}
   260  		}
   261  		previous = c
   262  	}
   263  	return output
   264  }
   265  
   266  // IsAlphanumeric returns true for [0-9a-zA-Z].
   267  func IsAlphanumeric(r rune) bool {
   268  	return IsNumeric(r) || IsAlpha(r)
   269  }
   270  
   271  // IsAlpha returns true for [a-zA-Z].
   272  func IsAlpha(r rune) bool {
   273  	return IsLowerAlpha(r) || IsUpperAlpha(r)
   274  }
   275  
   276  // IsLowerAlpha returns true for [a-z].
   277  func IsLowerAlpha(r rune) bool {
   278  	return 'a' <= r && r <= 'z'
   279  }
   280  
   281  // IsUpperAlpha returns true for [A-Z].
   282  func IsUpperAlpha(r rune) bool {
   283  	return 'A' <= r && r <= 'Z'
   284  }
   285  
   286  // IsNumeric returns true for [0-9].
   287  func IsNumeric(r rune) bool {
   288  	return '0' <= r && r <= '9'
   289  }
   290  
   291  // IsLowerAlphanumeric returns true for [0-9a-z].
   292  func IsLowerAlphanumeric(r rune) bool {
   293  	return IsNumeric(r) || IsLowerAlpha(r)
   294  }
   295  
   296  func toSnakeCase(s string, options ...SnakeCaseOption) string {
   297  	snakeCaseOptions := &snakeCaseOptions{}
   298  	for _, option := range options {
   299  		option(snakeCaseOptions)
   300  	}
   301  	output := ""
   302  	s = strings.TrimFunc(s, isDelimiter)
   303  	for i, c := range s {
   304  		if isDelimiter(c) {
   305  			c = '_'
   306  		}
   307  		if i == 0 {
   308  			output += string(c)
   309  		} else if isSnakeCaseNewWord(c, snakeCaseOptions.newWordOnDigits) &&
   310  			output[len(output)-1] != '_' &&
   311  			((i < len(s)-1 && !isSnakeCaseNewWord(rune(s[i+1]), true) && !isDelimiter(rune(s[i+1]))) ||
   312  				(snakeCaseOptions.newWordOnDigits && unicode.IsDigit(c)) ||
   313  				(unicode.IsLower(rune(s[i-1])))) {
   314  			output += "_" + string(c)
   315  		} else if !(isDelimiter(c) && output[len(output)-1] == '_') {
   316  			output += string(c)
   317  		}
   318  	}
   319  	return output
   320  }
   321  
   322  func isSnakeCaseNewWord(r rune, newWordOnDigits bool) bool {
   323  	if newWordOnDigits {
   324  		return unicode.IsUpper(r) || unicode.IsDigit(r)
   325  	}
   326  	return unicode.IsUpper(r)
   327  }
   328  
   329  func isDelimiter(r rune) bool {
   330  	return r == '.' || r == '-' || r == '_' || r == ' ' || r == '\t' || r == '\n' || r == '\r'
   331  }
   332  
   333  type snakeCaseOptions struct {
   334  	newWordOnDigits bool
   335  }