sqlboiler/strmangle/strmangle.go

448 lines
9.4 KiB
Go
Raw Normal View History

// Package strmangle is a collection of string manipulation functions.
// Primarily used by boil and templates for code generation.
// Because it is focused on pipelining inside templates
// you will see some odd parameter ordering.
package strmangle
import (
"fmt"
"math"
2016-08-02 04:55:40 +02:00
"regexp"
"strings"
)
var (
2016-08-22 05:50:54 +02:00
idAlphabet = []byte("abcdefghijklmnopqrstuvwxyz")
smartQuoteRgx = regexp.MustCompile(`^(?i)"?[a-z_][_a-z0-9]*"?(\."?[_a-z][_a-z0-9]*"?)*(\.\*)?$`)
)
2016-08-22 05:50:54 +02:00
var uppercaseWords = map[string]struct{}{
"guid": struct{}{},
"id": struct{}{},
"uid": struct{}{},
"uuid": struct{}{},
}
2016-08-17 07:19:23 +02:00
func init() {
2016-08-21 05:40:20 +02:00
// Our Boil inflection Ruleset does not include uncountable inflections.
2016-08-17 07:19:23 +02:00
// This way, people using words like Sheep will not have
// collisions with their model name (Sheep) and their
// function name (Sheep()). Instead, it will
// use the regular inflection rules: Sheep, Sheeps().
2016-08-21 05:40:20 +02:00
boilRuleset = newBoilRuleset()
2016-08-17 07:19:23 +02:00
}
// IdentQuote attempts to quote simple identifiers in SQL tatements
func IdentQuote(s string) string {
2016-08-04 06:46:58 +02:00
if strings.ToLower(s) == "null" {
2016-08-02 04:55:40 +02:00
return s
}
2016-08-02 04:55:40 +02:00
if m := smartQuoteRgx.MatchString(s); m != true {
return s
}
2016-08-13 10:07:45 +02:00
buf := GetBuffer()
defer PutBuffer(buf)
2016-08-02 04:55:40 +02:00
splits := strings.Split(s, ".")
for i, split := range splits {
2016-08-13 10:07:45 +02:00
if i != 0 {
buf.WriteByte('.')
}
2016-08-04 04:17:29 +02:00
if strings.HasPrefix(split, `"`) || strings.HasSuffix(split, `"`) || split == "*" {
2016-08-13 10:07:45 +02:00
buf.WriteString(split)
2016-08-02 04:55:40 +02:00
continue
}
2016-08-13 10:07:45 +02:00
buf.WriteByte('"')
buf.WriteString(split)
buf.WriteByte('"')
}
2016-08-13 10:07:45 +02:00
return buf.String()
}
2016-08-04 06:46:58 +02:00
// IdentQuoteSlice applies IdentQuote to a slice.
func IdentQuoteSlice(s []string) []string {
if len(s) == 0 {
return s
}
strs := make([]string, len(s))
for i, str := range s {
strs[i] = IdentQuote(str)
}
return strs
}
2016-07-17 05:33:16 +02:00
// Identifier is a base conversion from Base 10 integers to Base 26
// integers that are represented by an alphabet from a-z
// See tests for example outputs.
func Identifier(in int) string {
ln := len(idAlphabet)
var n int
if in == 0 {
n = 1
} else {
n = 1 + int(math.Log(float64(in))/math.Log(float64(ln)))
}
2016-08-13 15:30:53 +02:00
cols := GetBuffer()
defer PutBuffer(cols)
for i := 0; i < n; i++ {
divisor := int(math.Pow(float64(ln), float64(n-i-1)))
rem := in / divisor
2016-08-13 15:30:53 +02:00
cols.WriteByte(idAlphabet[rem])
in -= rem * divisor
}
2016-08-13 15:30:53 +02:00
return cols.String()
}
// Plural converts singular words to plural words (eg: person to people)
func Plural(name string) string {
2016-08-13 15:39:13 +02:00
buf := GetBuffer()
defer PutBuffer(buf)
splits := strings.Split(name, "_")
2016-08-13 15:39:13 +02:00
for i := 0; i < len(splits); i++ {
if i != 0 {
buf.WriteByte('_')
}
if i == len(splits)-1 {
2016-08-21 05:40:20 +02:00
buf.WriteString(boilRuleset.Pluralize(splits[len(splits)-1]))
2016-08-13 15:39:13 +02:00
break
}
buf.WriteString(splits[i])
}
return buf.String()
}
// Singular converts plural words to singular words (eg: people to person)
func Singular(name string) string {
2016-08-13 15:39:13 +02:00
buf := GetBuffer()
defer PutBuffer(buf)
splits := strings.Split(name, "_")
2016-08-13 15:39:13 +02:00
for i := 0; i < len(splits); i++ {
if i != 0 {
buf.WriteByte('_')
}
if i == len(splits)-1 {
2016-08-21 05:40:20 +02:00
buf.WriteString(boilRuleset.Singularize(splits[len(splits)-1]))
2016-08-13 15:39:13 +02:00
break
}
buf.WriteString(splits[i])
}
return buf.String()
}
// TitleCase changes a snake-case variable name
2016-03-02 04:11:47 +01:00
// into a go styled object variable name of "ColumnName".
// titleCase also fully uppercases "ID" components of names, for example
// "column_name_id" to "ColumnNameID".
2016-08-22 05:50:54 +02:00
//
// Note: This method is ugly because it has been highly optimized,
// we found that it was a fairly large bottleneck when we were using regexp.
func TitleCase(n string) string {
ln := len(n)
name := []byte(n)
2016-08-13 15:48:31 +02:00
buf := GetBuffer()
2016-08-22 05:50:54 +02:00
start := 0
end := 0
for start < ln {
// Find the start and end of the underscores to account
// for the possibility of being multiple underscores in a row.
2016-08-22 07:10:15 +02:00
if end < ln {
if name[start] == '_' {
start++
end++
continue
// Once we have found the end of the underscores, we can
// find the end of the first full word.
} else if name[end] != '_' {
end++
continue
}
}
2016-08-22 05:50:54 +02:00
word := name[start:end]
var vowels bool
numStart := -1
numBroken := false
for i, c := range word {
// If the word starts with digits, it does not
// fit the criteria for "uppercasedWords"
if i == 0 && c > 47 && c < 58 {
numBroken = true
break
}
if c > 47 && c < 58 && numStart == -1 {
numStart = i
continue
} else if c > 47 && c < 58 {
continue
}
// A letter inbetween numbers will set numBroken to true, eg: 9e9
if numStart != -1 {
numBroken = true
break
}
// If on the last loop and we have not found any digits, update
// numStart to len of word, so we can get the full word below
if i == len(word)-1 && numStart == -1 && !numBroken {
numStart = len(word)
}
}
var match bool
if !numBroken {
_, match = uppercaseWords[string(word[:numStart])]
}
if !match {
for _, c := range word {
if found := c == 97 || c == 101 || c == 105 || c == 111 || c == 117 || c == 121; found {
vowels = true
break
}
}
}
if match || !vowels {
// Uppercase all a-z characters
for _, c := range word {
if c > 96 && c < 123 {
buf.WriteByte(c - 32)
} else {
buf.WriteByte(c)
}
}
} else {
// If the first char is a-z, then uppercase it
// by subtracting 32, and append the rest of the word.
// Otherwise, it's probably a digit, so just append
// the whole word as is.
if word[0] > 96 && word[0] < 123 {
buf.WriteByte(word[0] - 32)
buf.Write(word[1:])
} else {
buf.Write(word)
}
}
start = end + 1
end = start
}
2016-08-22 05:50:54 +02:00
ret := buf.String()
PutBuffer(buf)
return ret
}
// CamelCase takes a variable name in the format of "var_name" and converts
// it into a go styled variable name of "varName".
// camelCase also fully uppercases "ID" components of names, for example
// "var_name_id" to "varNameID".
func CamelCase(name string) string {
2016-08-13 15:48:31 +02:00
buf := GetBuffer()
defer PutBuffer(buf)
2016-08-22 05:50:54 +02:00
index := -1
for i := 0; i < len(name); i++ {
if name[i] != '_' {
index = i
break
}
2016-08-22 05:50:54 +02:00
}
2016-08-22 05:50:54 +02:00
if index != -1 {
name = name[index:]
} else {
return ""
}
index = -1
for i := 0; i < len(name); i++ {
if name[i] == '_' {
index = i
break
2016-07-17 05:33:16 +02:00
}
2016-08-22 05:50:54 +02:00
}
2016-07-17 05:33:16 +02:00
2016-08-22 05:50:54 +02:00
if index == -1 {
buf.WriteString(name)
} else {
buf.WriteString(name[:index])
buf.WriteString(TitleCase(name[index+1:]))
}
2016-08-13 15:48:31 +02:00
return buf.String()
}
// MakeStringMap converts a map[string]string into the format:
// "key": "value", "key": "value"
func MakeStringMap(types map[string]string) string {
2016-08-13 15:54:57 +02:00
buf := GetBuffer()
defer PutBuffer(buf)
c := 0
for k, v := range types {
2016-08-13 15:54:57 +02:00
buf.WriteString(fmt.Sprintf(`"%s": "%s"`, k, v))
if c < len(types)-1 {
buf.WriteString(", ")
}
c++
}
2016-08-13 15:54:57 +02:00
return buf.String()
}
// StringMap maps a function over a slice of strings.
func StringMap(modifier func(string) string, strs []string) []string {
ret := make([]string, len(strs))
for i, str := range strs {
ret[i] = modifier(str)
}
return ret
}
// PrefixStringSlice with the given str.
func PrefixStringSlice(str string, strs []string) []string {
ret := make([]string, len(strs))
for i, s := range strs {
ret[i] = fmt.Sprintf("%s%s", str, s)
}
return ret
2016-04-04 12:28:58 +02:00
}
// Placeholders generates the SQL statement placeholders for in queries.
2016-08-21 04:34:18 +02:00
// For example, ($1,$2,$3),($4,$5,$6) etc.
// It will start counting placeholders at "start".
func Placeholders(count int, start int, group int) string {
2016-08-13 15:55:52 +02:00
buf := GetBuffer()
defer PutBuffer(buf)
if start == 0 || group == 0 {
panic("Invalid start or group numbers supplied.")
}
if group > 1 {
buf.WriteByte('(')
}
for i := 0; i < count; i++ {
if i != 0 {
if group > 1 && i%group == 0 {
2016-08-13 09:57:13 +02:00
buf.WriteString("),(")
} else {
2016-08-13 09:57:13 +02:00
buf.WriteByte(',')
}
}
buf.WriteString(fmt.Sprintf("$%d", start+i))
}
if group > 1 {
buf.WriteByte(')')
}
return buf.String()
}
// SetParamNames takes a slice of columns and returns a comma separated
// list of parameter names for a template statement SET clause.
// eg: "col1"=$1, "col2"=$2, "col3"=$3
func SetParamNames(columns []string) string {
2016-08-13 15:59:02 +02:00
buf := GetBuffer()
defer PutBuffer(buf)
for i, c := range columns {
buf.WriteString(fmt.Sprintf(`"%s"=$%d`, c, i+1))
if i < len(columns)-1 {
buf.WriteString(", ")
}
}
2016-08-13 15:59:02 +02:00
return buf.String()
}
// WhereClause returns the where clause using start as the $ flag index
// For example, if start was 2 output would be: "colthing=$2 AND colstuff=$3"
func WhereClause(start int, cols []string) string {
2016-08-08 18:52:34 +02:00
if start == 0 {
panic("0 is not a valid start number for whereClause")
2016-08-08 18:52:34 +02:00
}
2016-08-13 16:01:06 +02:00
buf := GetBuffer()
defer PutBuffer(buf)
for i, c := range cols {
2016-08-13 16:01:06 +02:00
buf.WriteString(fmt.Sprintf(`"%s"=$%d`, c, start+i))
if i < len(cols)-1 {
buf.WriteString(" AND ")
}
2016-08-08 18:52:34 +02:00
}
2016-08-13 16:01:06 +02:00
return buf.String()
2016-08-08 18:52:34 +02:00
}
// JoinSlices merges two string slices of equal length
func JoinSlices(sep string, a, b []string) []string {
lna, lnb := len(a), len(b)
if lna != lnb {
panic("joinSlices: can only merge slices of same length")
} else if lna == 0 {
return nil
}
ret := make([]string, len(a))
for i, elem := range a {
ret[i] = fmt.Sprintf("%s%s%s", elem, sep, b[i])
}
return ret
}
// StringSliceMatch returns true if the length of both
// slices is the same, and the elements of both slices are the same.
// The elements can be in any order.
func StringSliceMatch(a []string, b []string) bool {
if len(a) != len(b) {
return false
}
for _, aval := range a {
found := false
for _, bval := range b {
if bval == aval {
found = true
break
}
}
if !found {
return false
}
}
return true
}