|
| 1 | +package migrations |
| 2 | + |
| 3 | +import ( |
| 4 | + "strings" |
| 5 | + "time" |
| 6 | + "unicode" |
| 7 | + "unicode/utf8" |
| 8 | + |
| 9 | + "github.com/pkg/errors" |
| 10 | +) |
| 11 | + |
| 12 | +var ( |
| 13 | + // ErrUnknownNamingConvention indicates that an attempt was made to |
| 14 | + // create a migration, without specifying a name. |
| 15 | + ErrUnknownNamingConvention = errors.New("unknown naming convention") |
| 16 | +) |
| 17 | + |
| 18 | +// MigrationNameConvention represents a naming convention in terms of |
| 19 | +// casing and underscores for a Migrator. |
| 20 | +type MigrationNameConvention string |
| 21 | + |
| 22 | +const ( |
| 23 | + // CamelCase represents a camelCase naming convention. |
| 24 | + CamelCase MigrationNameConvention = "camelCase" |
| 25 | + |
| 26 | + // SnakeCase represents a snake_case naming convention. |
| 27 | + SnakeCase MigrationNameConvention = "snakeCase" |
| 28 | +) |
| 29 | + |
| 30 | +// Caser is intended to convert a description to a particular naming |
| 31 | +// convention. It should take spaces into account. |
| 32 | +type Caser interface { |
| 33 | + // ToFileCase converts a description to the casing required for |
| 34 | + // a filename. There should not be any spaces in the output. |
| 35 | + ToFileCase(time.Time, string) string |
| 36 | + |
| 37 | + // ToFileCase converts a description to the casing required for |
| 38 | + // a function name. There should not be any spaces in the output. |
| 39 | + ToFuncCase(time.Time, string) string |
| 40 | +} |
| 41 | + |
| 42 | +// GetCaser returns the appropriate caser for the given naming convention. |
| 43 | +func GetCaser(convention MigrationNameConvention) (Caser, error) { |
| 44 | + switch convention { |
| 45 | + case SnakeCase: |
| 46 | + return SnakeCaser{}, nil |
| 47 | + case CamelCase: |
| 48 | + return CamelCaser{}, nil |
| 49 | + default: |
| 50 | + err := errors.Wrapf( |
| 51 | + ErrUnknownNamingConvention, |
| 52 | + "unknown convention %s", |
| 53 | + convention, |
| 54 | + ) |
| 55 | + return nil, err |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +var _ Caser = (*SnakeCaser)(nil) |
| 60 | + |
| 61 | +// SnakeCaser will attempt to use snake_case for filenames. |
| 62 | +type SnakeCaser struct{} |
| 63 | + |
| 64 | +func (cc SnakeCaser) ToFileCase(date time.Time, input string) string { |
| 65 | + // Panicking here is acceptable, because builder.WriteString |
| 66 | + // should only ever return an error when out of memory. |
| 67 | + builder := strings.Builder{} |
| 68 | + _, err := builder.WriteString(date.Format("20060102150405")) |
| 69 | + if err != nil { |
| 70 | + panic(err) |
| 71 | + } |
| 72 | + |
| 73 | + _, err = builder.WriteRune('_') |
| 74 | + if err != nil { |
| 75 | + panic(err) |
| 76 | + } |
| 77 | + |
| 78 | + description := ConvertCamelCaseToSnakeCase(input) |
| 79 | + _, err = builder.WriteString(description) |
| 80 | + if err != nil { |
| 81 | + panic(err) |
| 82 | + } |
| 83 | + return builder.String() |
| 84 | +} |
| 85 | + |
| 86 | +func (cc SnakeCaser) ToFuncCase(date time.Time, input string) string { |
| 87 | + // Panicking here is acceptable, because builder.WriteString |
| 88 | + // should only ever return an error when out of memory. |
| 89 | + builder := strings.Builder{} |
| 90 | + _, err := builder.WriteString(date.Format("20060102150405")) |
| 91 | + if err != nil { |
| 92 | + panic(err) |
| 93 | + } |
| 94 | + description := ConvertSnakeCaseToCamelCase(input) |
| 95 | + _, err = builder.WriteString(description) |
| 96 | + if err != nil { |
| 97 | + panic(err) |
| 98 | + } |
| 99 | + return builder.String() |
| 100 | +} |
| 101 | + |
| 102 | +var _ Caser = (*CamelCaser)(nil) |
| 103 | + |
| 104 | +// SnakeCaser will attempt to use camelCase for filenames. |
| 105 | +type CamelCaser struct{} |
| 106 | + |
| 107 | +func (cc CamelCaser) ToFileCase(date time.Time, input string) string { |
| 108 | + // Panicking here is acceptable, because builder.WriteString |
| 109 | + // should only ever return an error when out of memory. |
| 110 | + builder := strings.Builder{} |
| 111 | + _, err := builder.WriteString(date.Format("20060102150405")) |
| 112 | + if err != nil { |
| 113 | + panic(err) |
| 114 | + } |
| 115 | + description := ConvertSnakeCaseToCamelCase(input) |
| 116 | + _, err = builder.WriteString(description) |
| 117 | + if err != nil { |
| 118 | + panic(err) |
| 119 | + } |
| 120 | + return builder.String() |
| 121 | +} |
| 122 | + |
| 123 | +func (cc CamelCaser) ToFuncCase(date time.Time, input string) string { |
| 124 | + // Panicking here is acceptable, because builder.WriteString |
| 125 | + // should only ever return an error when out of memory. |
| 126 | + builder := strings.Builder{} |
| 127 | + _, err := builder.WriteString(date.Format("20060102150405")) |
| 128 | + if err != nil { |
| 129 | + panic(err) |
| 130 | + } |
| 131 | + description := ConvertSnakeCaseToCamelCase(input) |
| 132 | + _, err = builder.WriteString(description) |
| 133 | + if err != nil { |
| 134 | + panic(err) |
| 135 | + } |
| 136 | + return builder.String() |
| 137 | +} |
| 138 | + |
| 139 | +// ConvertCamelCaseToSnakeCase converts a potentially camel-case |
| 140 | +// string to snake-case. Should be Unicode-safe. |
| 141 | +// |
| 142 | +// Spaces are converted to underscores and any uppercase letters |
| 143 | +// are replaced with an underscore and the lowercase version of |
| 144 | +// the same letter. |
| 145 | +func ConvertCamelCaseToSnakeCase(word string) (result string) { |
| 146 | + if len(word) == 0 { |
| 147 | + return "" |
| 148 | + } |
| 149 | + |
| 150 | + var err error |
| 151 | + builder := &strings.Builder{} |
| 152 | + char, _ := utf8.DecodeRuneInString(word) |
| 153 | + _, err = builder.WriteRune(unicode.ToLower(char)) |
| 154 | + if err != nil { |
| 155 | + panic(err) |
| 156 | + } |
| 157 | + |
| 158 | + var prevWordBoundary bool |
| 159 | + for _, char := range word[1:] { |
| 160 | + if char == '_' || unicode.IsSpace(char) { |
| 161 | + prevWordBoundary = true |
| 162 | + continue |
| 163 | + } |
| 164 | + if prevWordBoundary || unicode.IsUpper(char) { |
| 165 | + prevWordBoundary = false |
| 166 | + _, err = builder.WriteRune('_') |
| 167 | + if err != nil { |
| 168 | + panic(err) |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + _, err = builder.WriteRune(unicode.ToLower(char)) |
| 173 | + if err != nil { |
| 174 | + panic(err) |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + return builder.String() |
| 179 | +} |
| 180 | + |
| 181 | +// ConvertSnakeCaseToCamelCase converts a potentially snake-case |
| 182 | +// string to camel-case. Should be Unicode-safe. |
| 183 | +// |
| 184 | +// Spaces and underscores are removed and any letter following |
| 185 | +// immediately after these removed characters will be converted |
| 186 | +// to uppercase. |
| 187 | +func ConvertSnakeCaseToCamelCase(word string) (result string) { |
| 188 | + builder := &strings.Builder{} |
| 189 | + var prevWordBoundary bool |
| 190 | + for _, char := range word { |
| 191 | + if char == '_' || unicode.IsSpace(char) { |
| 192 | + prevWordBoundary = true |
| 193 | + continue |
| 194 | + } |
| 195 | + |
| 196 | + var err error |
| 197 | + if prevWordBoundary { |
| 198 | + prevWordBoundary = false |
| 199 | + _, err = builder.WriteRune(unicode.ToUpper(char)) |
| 200 | + } else { |
| 201 | + _, err = builder.WriteRune(unicode.ToLower(char)) |
| 202 | + } |
| 203 | + if err != nil { |
| 204 | + panic(err) |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + return builder.String() |
| 209 | +} |
0 commit comments