Fix Grype CVEs: update logrus and prometheus/prometheus

- Update github.com/sirupsen/logrus v1.9.0 -> v1.9.3 in test/go.mod
  to fix GHSA-4f99-4q7p-p3gh (High)
- Update github.com/prometheus/prometheus v0.35.0 -> v0.311.3
  to fix GHSA-vffh-x6r8-xx99 (Medium)
- Run go mod tidy and go mod vendor to update vendor directory
This commit is contained in:
Bruno Chauvet
2026-05-04 10:45:24 +03:00
committed by Ciprian Hacman
parent 255c6e602c
commit 97bb2fbb44
279 changed files with 17945 additions and 29165 deletions
+90
View File
@@ -0,0 +1,90 @@
# Benchmarking name mangling utilities
```bash
go test -bench XXX -run XXX -benchtime 30s
```
## Benchmarks at `b3e7a5386f996177e4808f11acb2aa93a0f660df`
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/swag
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkToXXXName/ToGoName-4 862623 44101 ns/op 10450 B/op 732 allocs/op
BenchmarkToXXXName/ToVarName-4 853656 40728 ns/op 10468 B/op 734 allocs/op
BenchmarkToXXXName/ToFileName-4 1268312 27813 ns/op 9785 B/op 617 allocs/op
BenchmarkToXXXName/ToCommandName-4 1276322 27903 ns/op 9785 B/op 617 allocs/op
BenchmarkToXXXName/ToHumanNameLower-4 895334 40354 ns/op 10472 B/op 731 allocs/op
BenchmarkToXXXName/ToHumanNameTitle-4 882441 40678 ns/op 10566 B/op 749 allocs/op
```
## Benchmarks after PR #79
~ x10 performance improvement and ~ /100 memory allocations.
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/swag
cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
BenchmarkToXXXName/ToGoName-4 9595830 3991 ns/op 42 B/op 5 allocs/op
BenchmarkToXXXName/ToVarName-4 9194276 3984 ns/op 62 B/op 7 allocs/op
BenchmarkToXXXName/ToFileName-4 17002711 2123 ns/op 147 B/op 7 allocs/op
BenchmarkToXXXName/ToCommandName-4 16772926 2111 ns/op 147 B/op 7 allocs/op
BenchmarkToXXXName/ToHumanNameLower-4 9788331 3749 ns/op 92 B/op 6 allocs/op
BenchmarkToXXXName/ToHumanNameTitle-4 9188260 3941 ns/op 104 B/op 6 allocs/op
```
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/swag
cpu: AMD Ryzen 7 5800X 8-Core Processor
BenchmarkToXXXName/ToGoName-16 18527378 1972 ns/op 42 B/op 5 allocs/op
BenchmarkToXXXName/ToVarName-16 15552692 2093 ns/op 62 B/op 7 allocs/op
BenchmarkToXXXName/ToFileName-16 32161176 1117 ns/op 147 B/op 7 allocs/op
BenchmarkToXXXName/ToCommandName-16 32256634 1137 ns/op 147 B/op 7 allocs/op
BenchmarkToXXXName/ToHumanNameLower-16 18599661 1946 ns/op 92 B/op 6 allocs/op
BenchmarkToXXXName/ToHumanNameTitle-16 17581353 2054 ns/op 105 B/op 6 allocs/op
```
## Benchmarks at `d7d2d1b895f5b6747afaff312dd2a402e69e818b`
go1.24
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/swag
cpu: AMD Ryzen 7 5800X 8-Core Processor
BenchmarkToXXXName/ToGoName-16 19757858 1881 ns/op 42 B/op 5 allocs/op
BenchmarkToXXXName/ToVarName-16 17494111 2094 ns/op 74 B/op 7 allocs/op
BenchmarkToXXXName/ToFileName-16 28161226 1492 ns/op 158 B/op 7 allocs/op
BenchmarkToXXXName/ToCommandName-16 23787333 1489 ns/op 158 B/op 7 allocs/op
BenchmarkToXXXName/ToHumanNameLower-16 17537257 2030 ns/op 103 B/op 6 allocs/op
BenchmarkToXXXName/ToHumanNameTitle-16 16977453 2156 ns/op 105 B/op 6 allocs/op
```
## Benchmarks after PR #106
Moving the scope of everything down to a struct allowed to reduce a bit garbage and pooling.
On top of that, ToGoName (and thus ToVarName) have been subject to a minor optimization, removing a few allocations.
Overall timings improve by ~ -10%.
go1.24
```
goos: linux
goarch: amd64
pkg: github.com/go-openapi/swag/mangling
cpu: AMD Ryzen 7 5800X 8-Core Processor
BenchmarkToXXXName/ToGoName-16 22496130 1618 ns/op 31 B/op 3 allocs/op
BenchmarkToXXXName/ToVarName-16 22538068 1618 ns/op 33 B/op 3 allocs/op
BenchmarkToXXXName/ToFileName-16 27722977 1236 ns/op 105 B/op 6 allocs/op
BenchmarkToXXXName/ToCommandName-16 27967395 1258 ns/op 105 B/op 6 allocs/op
BenchmarkToXXXName/ToHumanNameLower-16 18587901 1917 ns/op 103 B/op 6 allocs/op
BenchmarkToXXXName/ToHumanNameTitle-16 17193208 2019 ns/op 108 B/op 7 allocs/op
```
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+25
View File
@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Package mangling provides name mangling capabilities.
//
// Name mangling is an important stage when generating code:
// it helps construct safe program identifiers that abide by the language rules
// and play along with linters.
//
// Examples:
//
// Suppose we get an object name taken from an API spec: "json_object",
//
// We may generate a legit go type name using [NameMangler.ToGoName]: "JsonObject".
//
// We may then locate this type in a source file named using [NameMangler.ToFileName]: "json_object.go".
//
// The methods exposed by the NameMangler are used to generate code in many different contexts, such as:
//
// - generating exported or unexported go identifiers from a JSON schema or an API spec
// - generating file names
// - generating human-readable comments for types and variables
// - generating JSON-like API identifiers from go code
// - ...
package mangling
+270
View File
@@ -0,0 +1,270 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mangling
import (
"sort"
"strings"
"unicode"
"unicode/utf8"
)
// DefaultInitialisms returns all the initialisms configured by default for this package.
//
// # Motivation
//
// Common initialisms are acronyms for which the ordinary camel-casing rules are altered and
// for which we retain the original case.
//
// This is largely specific to the go naming conventions enforced by golint (now revive).
//
// # Example
//
// In go, "id" is a good-looking identifier, but "Id" is not and "ID" is preferred
// (notice that this stems only from conventions: the go compiler accepts all of these).
//
// Similarly, we may use "http", but not "Http". In this case, "HTTP" is preferred.
//
// # Reference and customization
//
// The default list of these casing-style exceptions is taken from the [github.com/mgechev/revive] linter for go:
// https://github.com/mgechev/revive/blob/master/lint/name.go#L93
//
// There are a few additions to the original list, such as IPv4, IPv6 and OAI ("OpenAPI").
//
// For these additions, "IPv4" would be preferred to "Ipv4" or "IPV4", and "OAI" to "Oai"
//
// You may redefine this list entirely using the mangler option [WithInitialisms], or simply add extra definitions
// using [WithAdditionalInitialisms].
//
// # Mixed-case and plurals
//
// Notice that initialisms are not necessarily fully upper-cased: a mixed-case initialism indicates the preferred casing.
//
// Obviously, lower-case only initialisms do not make a lot of sense: if lower-case only initialisms are added,
// they will be considered fully capitalized.
//
// Plural forms use mixed case like "IDs". And so do values like "IPv4" or "IPv6".
//
// The [NameMangler] automatically detects simple plurals for words such as "IDs" or "APIs",
// so you don't need to configure these variants.
//
// At this moment, it doesn't support pluralization of terms that ends with an 's' (or 'S'), since there is
// no clear consensus on whether a word like DNS should be pluralized as DNSes or remain invariant.
// The [NameMangler] consider those invariant. Therefore DNSs or DNSes are not recognized as plurals for DNS.
//
// Besids, we don't want to support pluralization of terms which would otherwise conflict with another one,
// like "HTTPs" vs "HTTPS". All these should be considered invariant. Hence: "Https" matches "HTTPS" and
// "HTTPSS" is "HTTPS" followed by "S".
func DefaultInitialisms() []string {
return []string{
"ACL",
"API",
"ASCII",
"CPU",
"CSS",
"DNS",
"EOF",
"GUID",
"HTML",
"HTTPS",
"HTTP",
"ID",
"IP",
"IPv4", // prefer the mixed case outcome IPv4 over the capitalized IPV4
"IPv6", // prefer the mixed case outcome IPv6 over the capitalized IPV6
"JSON",
"LHS",
"OAI",
"QPS",
"RAM",
"RHS",
"RPC",
"SLA",
"SMTP",
"SQL",
"SSH",
"TCP",
"TLS",
"TTL",
"UDP",
"UI",
"UID",
"UUID",
"URI",
"URL",
"UTF8",
"VM",
"XML",
"XMPP",
"XSRF",
"XSS",
}
}
type indexOfInitialisms struct {
initialismsCache
index map[string]struct{}
}
func newIndexOfInitialisms() *indexOfInitialisms {
return &indexOfInitialisms{
index: make(map[string]struct{}),
}
}
func (m *indexOfInitialisms) add(words ...string) *indexOfInitialisms {
for _, word := range words {
// sanitization of injected words: trimmed from blanks, and must start with a letter
trimmed := strings.TrimSpace(word)
firstRune, _ := utf8.DecodeRuneInString(trimmed)
if !unicode.IsLetter(firstRune) {
continue
}
// Initialisms are case-sensitive. This means that we support mixed-case words.
// However, if specified as a lower-case string, the initialism should be fully capitalized.
if trimmed == strings.ToLower(trimmed) {
m.index[strings.ToUpper(trimmed)] = struct{}{}
continue
}
m.index[trimmed] = struct{}{}
}
return m
}
func (m *indexOfInitialisms) sorted() []string {
result := make([]string, 0, len(m.index))
for k := range m.index {
result = append(result, k)
}
sort.Sort(sort.Reverse(byInitialism(result)))
return result
}
func (m *indexOfInitialisms) buildCache() {
m.build(m.sorted(), m.pluralForm)
}
// initialismsCache caches all needed pre-computed and converted initialism entries,
// in the desired resolution order.
type initialismsCache struct {
initialisms []string
initialismsRunes [][]rune
initialismsUpperCased [][]rune // initialisms cached in their trimmed, upper-cased version
initialismsPluralForm []pluralForm
}
func (c *initialismsCache) build(in []string, pluralfunc func(string) pluralForm) {
c.initialisms = in
c.initialismsRunes = asRunes(c.initialisms)
c.initialismsUpperCased = asUpperCased(c.initialisms)
c.initialismsPluralForm = asPluralForms(c.initialisms, pluralfunc)
}
// pluralForm denotes the kind of pluralization to be used for initialisms.
//
// At this moment, initialisms are either invariant or follow a simple plural form with an
// extra (lower case) "s".
type pluralForm uint8
const (
notPlural pluralForm = iota
invariantPlural
simplePlural
)
func (f pluralForm) String() string {
switch f {
case notPlural:
return "notPlural"
case invariantPlural:
return "invariantPlural"
case simplePlural:
return "simplePlural"
default:
return "<unknown>"
}
}
// pluralForm indicates how we want to pluralize a given initialism.
//
// Besides configured invariant forms (like HTTP and HTTPS),
// an initialism is normally pluralized by adding a single 's', like in IDs.
//
// Initialisms ending with an 'S' or an 's' are configured as invariant (we don't
// support plural forms like CSSes or DNSes, however the mechanism could be extended to
// do just that).
func (m *indexOfInitialisms) pluralForm(key string) pluralForm {
if _, ok := m.index[key]; !ok {
return notPlural
}
if strings.HasSuffix(strings.ToUpper(key), "S") {
return invariantPlural
}
if _, ok := m.index[key+"s"]; ok {
return invariantPlural
}
if _, ok := m.index[key+"S"]; ok {
return invariantPlural
}
return simplePlural
}
type byInitialism []string
func (s byInitialism) Len() int {
return len(s)
}
func (s byInitialism) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Less specifies the order in which initialisms are prioritized:
// 1. match longest first
// 2. when equal length, match in reverse lexicographical order, lower case match comes first
func (s byInitialism) Less(i, j int) bool {
if len(s[i]) != len(s[j]) {
return len(s[i]) < len(s[j])
}
return s[i] < s[j]
}
func asRunes(in []string) [][]rune {
out := make([][]rune, len(in))
for i, initialism := range in {
out[i] = []rune(initialism)
}
return out
}
func asUpperCased(in []string) [][]rune {
out := make([][]rune, len(in))
for i, initialism := range in {
out[i] = []rune(upper(trim(initialism)))
}
return out
}
// asPluralForms bakes an index of pluralization support.
func asPluralForms(in []string, pluralFunc func(string) pluralForm) []pluralForm {
out := make([]pluralForm, len(in))
for i, initialism := range in {
out[i] = pluralFunc(initialism)
}
return out
}
+186
View File
@@ -0,0 +1,186 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mangling
import (
"bytes"
"strings"
"unicode"
"unicode/utf8"
)
type (
lexemKind uint8
nameLexem struct {
original string
matchedInitialism string
kind lexemKind
}
)
const (
lexemKindCasualName lexemKind = iota
lexemKindInitialismName
)
func newInitialismNameLexem(original, matchedInitialism string) nameLexem {
return nameLexem{
kind: lexemKindInitialismName,
original: original,
matchedInitialism: matchedInitialism,
}
}
func newCasualNameLexem(original string) nameLexem {
return nameLexem{
kind: lexemKindCasualName,
original: trim(original), // TODO: save on calls to trim
}
}
// WriteTitleized writes the titleized lexeme to a bytes.Buffer.
//
// If the first letter cannot be capitalized, it doesn't write anything and return false,
// so the caller may attempt some workaround strategy.
func (l nameLexem) WriteTitleized(w *bytes.Buffer, alwaysUpper bool) bool {
if l.kind == lexemKindInitialismName {
w.WriteString(l.matchedInitialism)
return true
}
if len(l.original) == 0 {
return true
}
if len(l.original) == 1 {
// identifier is too short: casing will depend on the context
firstByte := l.original[0]
switch {
case 'A' <= firstByte && firstByte <= 'Z':
// safe
w.WriteByte(firstByte)
return true
case alwaysUpper && 'a' <= firstByte && firstByte <= 'z':
w.WriteByte(firstByte - 'a' + 'A')
return true
default:
// not a letter: skip and let the caller decide
return false
}
}
if firstByte := l.original[0]; firstByte < utf8.RuneSelf {
// ASCII
switch {
case 'A' <= firstByte && firstByte <= 'Z':
// already an upper case letter
w.WriteString(l.original)
return true
case 'a' <= firstByte && firstByte <= 'z':
w.WriteByte(firstByte - 'a' + 'A')
w.WriteString(l.original[1:])
return true
default:
// not a good candidate: doesn't start with a letter
return false
}
}
// unicode
firstRune, idx := utf8.DecodeRuneInString(l.original)
if !unicode.IsLetter(firstRune) || !unicode.IsUpper(unicode.ToUpper(firstRune)) {
// not a good candidate: doesn't start with a letter
// or a rune for which case doesn't make sense (e.g. East-Asian runes etc)
return false
}
rest := l.original[idx:]
w.WriteRune(unicode.ToUpper(firstRune))
w.WriteString(strings.ToLower(rest))
return true
}
// WriteLower is like write titleized but it writes a lower-case version of the lexeme.
//
// Similarly, there is no writing if the casing of the first rune doesn't make sense.
func (l nameLexem) WriteLower(w *bytes.Buffer, alwaysLower bool) bool {
if l.kind == lexemKindInitialismName {
w.WriteString(lower(l.matchedInitialism))
return true
}
if len(l.original) == 0 {
return true
}
if len(l.original) == 1 {
// identifier is too short: casing will depend on the context
firstByte := l.original[0]
switch {
case 'a' <= firstByte && firstByte <= 'z':
// safe
w.WriteByte(firstByte)
return true
case alwaysLower && 'A' <= firstByte && firstByte <= 'Z':
w.WriteByte(firstByte - 'A' + 'a')
return true
default:
// not a letter: skip and let the caller decide
return false
}
}
if firstByte := l.original[0]; firstByte < utf8.RuneSelf {
// ASCII
switch {
case 'a' <= firstByte && firstByte <= 'z':
// already a lower case letter
w.WriteString(l.original)
return true
case 'A' <= firstByte && firstByte <= 'Z':
w.WriteByte(firstByte - 'A' + 'a')
w.WriteString(l.original[1:])
return true
default:
// not a good candidate: doesn't start with a letter
return false
}
}
// unicode
firstRune, idx := utf8.DecodeRuneInString(l.original)
if !unicode.IsLetter(firstRune) || !unicode.IsLower(unicode.ToLower(firstRune)) {
// not a good candidate: doesn't start with a letter
// or a rune for which case doesn't make sense (e.g. East-Asian runes etc)
return false
}
rest := l.original[idx:]
w.WriteRune(unicode.ToLower(firstRune))
w.WriteString(rest)
return true
}
func (l nameLexem) GetOriginal() string {
return l.original
}
func (l nameLexem) IsInitialism() bool {
return l.kind == lexemKindInitialismName
}
+370
View File
@@ -0,0 +1,370 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mangling
import (
"strings"
"unicode"
)
// NameMangler knows how to transform sentences or words into
// identifiers that are a better fit in contexts such as:
//
// - unexported or exported go variable identifiers
// - file names
// - camel cased identifiers
// - ...
//
// The [NameMangler] is safe for concurrent use, save for its [NameMangler.AddInitialisms] method,
// which is not.
//
// # Known limitations
//
// At this moment, the [NameMangler] doesn't play well with "all caps" text:
//
// unless every single upper-cased word is declared as an initialism, capitalized words would generally
// not be transformed with the expected result, e.g.
//
// ToFileName("THIS_IS_ALL_CAPS")
//
// yields the weird outcome
//
// "t_h_i_s_i_s_a_l_l_c_a_p_s"
type NameMangler struct {
options
index *indexOfInitialisms
splitter splitter
splitterWithPostSplit splitter
_ struct{}
}
// NewNameMangler builds a name mangler ready to convert strings.
//
// The default name mangler is configured with default common initialisms and all default options.
func NewNameMangler(opts ...Option) NameMangler {
m := NameMangler{
options: optionsWithDefaults(opts),
index: newIndexOfInitialisms(),
}
m.addInitialisms(m.commonInitialisms...)
// a splitter that returns matches lexemes as ready-to-assemble strings:
// details of the lexemes are redeemed.
m.splitter = newSplitter(
withInitialismsCache(&m.index.initialismsCache),
withReplaceFunc(m.replaceFunc),
)
// a splitter that returns matches lexemes ready for post-processing
m.splitterWithPostSplit = newSplitter(
withInitialismsCache(&m.index.initialismsCache),
withReplaceFunc(m.replaceFunc),
withPostSplitInitialismCheck,
)
return m
}
// AddInitialisms declares extra initialisms to the mangler.
//
// It declares extra words as "initialisms" (i.e. words that won't be camel cased or titled cased),
// on top of the existing list of common initialisms (such as ID, HTTP...).
//
// Added words must start with a (unicode) letter. If some don't, they are ignored.
// Added words are either fully capitalized or mixed-cased. Lower-case only words are considered capitalized.
//
// It is typically used just after initializing the [NameMangler].
//
// When all initialisms are known at the time the mangler is initialized, it is preferable to
// use [NewNameMangler] with the option [WithAdditionalInitialisms].
//
// Adding initialisms mutates the mangler and should not be carried out concurrently with other calls to the mangler.
func (m *NameMangler) AddInitialisms(words ...string) {
m.addInitialisms(words...)
}
// Initialisms renders the list of initialisms supported by this mangler.
func (m *NameMangler) Initialisms() []string {
return m.index.initialisms
}
// Camelize a single word.
//
// Example:
//
// - "HELLO" and "hello" become "Hello".
func (m NameMangler) Camelize(word string) string {
ru := []rune(word)
switch len(ru) {
case 0:
return ""
case 1:
return string(unicode.ToUpper(ru[0]))
default:
camelized := poolOfBuffers.BorrowBuffer(len(word))
camelized.Grow(len(word))
defer func() {
poolOfBuffers.RedeemBuffer(camelized)
}()
camelized.WriteRune(unicode.ToUpper(ru[0]))
for _, ru := range ru[1:] {
camelized.WriteRune(unicode.ToLower(ru))
}
return camelized.String()
}
}
// ToFileName generates a suitable snake-case file name from a sentence.
//
// It lower-cases everything with underscore (_) as a word separator.
//
// Examples:
//
// - "Hello, Swagger" becomes "hello_swagger"
// - "HelloSwagger" becomes "hello_swagger"
func (m NameMangler) ToFileName(name string) string {
inptr := m.split(name)
in := *inptr
out := make([]string, 0, len(in))
for _, w := range in {
out = append(out, lower(w))
}
poolOfStrings.RedeemStrings(inptr)
return strings.Join(out, "_")
}
// ToCommandName generates a suitable CLI command name from a sentence.
//
// It lower-cases everything with dash (-) as a word separator.
//
// Examples:
//
// - "Hello, Swagger" becomes "hello-swagger"
// - "HelloSwagger" becomes "hello-swagger"
func (m NameMangler) ToCommandName(name string) string {
inptr := m.split(name)
in := *inptr
out := make([]string, 0, len(in))
for _, w := range in {
out = append(out, lower(w))
}
poolOfStrings.RedeemStrings(inptr)
return strings.Join(out, "-")
}
// ToHumanNameLower represents a code name as a human-readable series of words.
//
// It lower-cases everything with blank space as a word separator.
//
// NOTE: parts recognized as initialisms just keep their original casing.
//
// Examples:
//
// - "Hello, Swagger" becomes "hello swagger"
// - "HelloSwagger" or "Hello-Swagger" become "hello swagger"
func (m NameMangler) ToHumanNameLower(name string) string {
s := m.splitterWithPostSplit
in := s.split(name)
out := make([]string, 0, len(*in))
for _, w := range *in {
if !w.IsInitialism() {
out = append(out, lower(w.GetOriginal()))
} else {
out = append(out, trim(w.GetOriginal()))
}
}
poolOfLexems.RedeemLexems(in)
return strings.Join(out, " ")
}
// ToHumanNameTitle represents a code name as a human-readable series of titleized words.
//
// It titleizes every word with blank space as a word separator.
//
// Examples:
//
// - "hello, Swagger" becomes "Hello Swagger"
// - "helloSwagger" becomes "Hello Swagger"
func (m NameMangler) ToHumanNameTitle(name string) string {
s := m.splitterWithPostSplit
in := s.split(name)
out := make([]string, 0, len(*in))
for _, w := range *in {
original := trim(w.GetOriginal())
if !w.IsInitialism() {
out = append(out, m.Camelize(original))
} else {
out = append(out, original)
}
}
poolOfLexems.RedeemLexems(in)
return strings.Join(out, " ")
}
// ToJSONName generates a camelized single-word version of a sentence.
//
// The output assembles every camelized word, but for the first word, which
// is lower-cased.
//
// Example:
//
// - "Hello_swagger" becomes "helloSwagger"
func (m NameMangler) ToJSONName(name string) string {
inptr := m.split(name)
in := *inptr
out := make([]string, 0, len(in))
for i, w := range in {
if i == 0 {
out = append(out, lower(w))
continue
}
out = append(out, m.Camelize(trim(w)))
}
poolOfStrings.RedeemStrings(inptr)
return strings.Join(out, "")
}
// ToVarName generates a legit unexported go variable name from a sentence.
//
// The generated name plays well with linters (see also [NameMangler.ToGoName]).
//
// Examples:
//
// - "Hello_swagger" becomes "helloSwagger"
// - "Http_server" becomes "httpServer"
//
// This name applies the same rules as [NameMangler.ToGoName] (legit exported variable), save the
// capitalization of the initial rune.
//
// Special case: when the initial part is a recognized as an initialism (like in the example above),
// the full part is lower-cased.
func (m NameMangler) ToVarName(name string) string {
return m.goIdentifier(name, false)
}
// ToGoName generates a legit exported go variable name from a sentence.
//
// The generated name plays well with most linters.
//
// ToGoName abides by the go "exported" symbol rule starting with an upper-case letter.
//
// Examples:
//
// - "hello_swagger" becomes "HelloSwagger"
// - "Http_server" becomes "HTTPServer"
//
// # Edge cases
//
// Whenever the first rune is not eligible to upper case, a special prefix is prepended to the resulting name.
// By default this is simply "X" and you may customize this behavior using the [WithGoNamePrefixFunc] option.
//
// This happens when the first rune is not a letter, e.g. a digit, or a symbol that has no word transliteration
// (see also [WithReplaceFunc] about symbol transliterations),
// as well as for most East Asian or Devanagari runes, for which there is no such concept as upper-case.
//
// # Linting
//
// [revive], the successor of golint is the reference linter.
//
// This means that [NameMangler.ToGoName] supports the initialisms that revive checks (see also [DefaultInitialisms]).
//
// At this moment, there is no attempt to transliterate unicode into ascii, meaning that some linters
// (e.g. asciicheck, gosmopolitan) may croak on go identifiers generated from unicode input.
//
// [revive]: https://github.com/mgechev/revive
func (m NameMangler) ToGoName(name string) string {
return m.goIdentifier(name, true)
}
func (m NameMangler) goIdentifier(name string, exported bool) string {
s := m.splitterWithPostSplit
lexems := s.split(name)
defer func() {
poolOfLexems.RedeemLexems(lexems)
}()
lexemes := *lexems
if len(lexemes) == 0 {
return ""
}
result := poolOfBuffers.BorrowBuffer(len(name))
defer func() {
poolOfBuffers.RedeemBuffer(result)
}()
firstPart := lexemes[0]
if !exported {
if ok := firstPart.WriteLower(result, true); !ok {
// NOTE: an initialism as the first part is lower-cased: no longer generates stuff like hTTPxyz.
//
// same prefixing rule applied to unexported variable as to an exported one, so that we have consistent
// names, whether the generated identifier is exported or not.
result.WriteString(strings.ToLower(m.prefixFunc()(name)))
result.WriteString(lexemes[0].GetOriginal())
}
} else {
if ok := firstPart.WriteTitleized(result, true); !ok {
// "repairs" a lexeme that doesn't start with a letter to become
// the start a legit go name. The current strategy is very crude and simply adds a fixed prefix,
// e.g. "X".
// For instance "1_sesame_street" would be split into lexemes ["1", "sesame", "street"] and
// the first one ("1") would result in something like "X1" (with the default prefix function).
//
// NOTE: no longer forcing the first part to be fully upper-cased
result.WriteString(m.prefixFunc()(name))
result.WriteString(lexemes[0].GetOriginal())
}
}
for _, lexem := range lexemes[1:] {
// NOTE: no longer forcing initialism parts to be fully upper-cased:
// * pluralized initialism preserve their trailing "s"
// * mixed-cased initialisms, such as IPv4, are preserved
if ok := lexem.WriteTitleized(result, false); !ok {
// it's not titleized: perhaps it's too short, perhaps the first rune is not a letter.
// write anyway
result.WriteString(lexem.GetOriginal())
}
}
return result.String()
}
func (m *NameMangler) addInitialisms(words ...string) {
m.index.add(words...)
m.index.buildCache()
}
// split calls the inner splitter.
func (m NameMangler) split(str string) *[]string {
s := m.splitter
lexems := s.split(str)
result := poolOfStrings.BorrowStrings()
for _, lexem := range *lexems {
*result = append(*result, lexem.GetOriginal())
}
poolOfLexems.RedeemLexems(lexems)
return result
}
+150
View File
@@ -0,0 +1,150 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mangling
type (
// PrefixFunc defines a safeguard rule (that may depend on the input string), to prefix
// a generated go name (in [NameMangler.ToGoName] and [NameMangler.ToVarName]).
//
// See [NameMangler.ToGoName] for more about which edge cases the prefix function covers.
PrefixFunc func(string) string
// ReplaceFunc is a transliteration function to replace special runes by a word.
ReplaceFunc func(r rune) (string, bool)
// Option to configure a [NameMangler].
Option func(*options)
options struct {
commonInitialisms []string
goNamePrefixFunc PrefixFunc
goNamePrefixFuncPtr *PrefixFunc
replaceFunc func(r rune) (string, bool)
}
)
func (o *options) prefixFunc() PrefixFunc {
if o.goNamePrefixFuncPtr != nil && *o.goNamePrefixFuncPtr != nil {
return *o.goNamePrefixFuncPtr
}
return o.goNamePrefixFunc
}
// WithGoNamePrefixFunc overrides the default prefix rule to safeguard generated go names.
//
// Example:
//
// This helps convert "123" into "{prefix}123" (a very crude strategy indeed, but it works).
//
// See [github.com/go-swagger/go-swagger/generator.DefaultFuncMap] for an example.
//
// The prefix function is assumed to return a string that starts with an upper case letter.
//
// The default is to prefix with "X".
//
// See [NameMangler.ToGoName] for more about which edge cases the prefix function covers.
func WithGoNamePrefixFunc(fn PrefixFunc) Option {
return func(o *options) {
o.goNamePrefixFunc = fn
}
}
// WithGoNamePrefixFuncPtr is like [WithGoNamePrefixFunc] but it specifies a pointer to a function.
//
// [WithGoNamePrefixFunc] should be preferred in most situations. This option should only serve the
// purpose of handling special situations where the prefix function is not an internal variable
// (e.g. an exported package global).
//
// [WithGoNamePrefixFuncPtr] supersedes [WithGoNamePrefixFunc] if it also specified.
//
// If the provided pointer is nil or points to a nil value, this option has no effect.
//
// The caller should ensure that no undesirable concurrent changes are applied to the function pointed to.
func WithGoNamePrefixFuncPtr(ptr *PrefixFunc) Option {
return func(o *options) {
o.goNamePrefixFuncPtr = ptr
}
}
// WithInitialisms declares the initialisms this mangler supports.
//
// This supersedes any pre-loaded defaults (see [DefaultInitialisms] for more about what initialisms are).
//
// It declares words to be recognized as "initialisms" (i.e. words that won't be camel cased or titled cased).
//
// Words must start with a (unicode) letter. If some don't, they are ignored.
// Words are either fully capitalized or mixed-cased. Lower-case only words are considered capitalized.
func WithInitialisms(words ...string) Option {
return func(o *options) {
o.commonInitialisms = words
}
}
// WithAdditionalInitialisms adds new initialisms to the currently supported list (see [DefaultInitialisms]).
//
// The same sanitization rules apply as those described for [WithInitialisms].
func WithAdditionalInitialisms(words ...string) Option {
return func(o *options) {
o.commonInitialisms = append(o.commonInitialisms, words...)
}
}
// WithReplaceFunc specifies a custom transliteration function instead of the default.
//
// The default translates the following characters into words as follows:
//
// - '@' -> 'At'
// - '&' -> 'And'
// - '|' -> 'Pipe'
// - '$' -> 'Dollar'
// - '!' -> 'Bang'
//
// Notice that the outcome of a transliteration should always be titleized.
func WithReplaceFunc(fn ReplaceFunc) Option {
return func(o *options) {
o.replaceFunc = fn
}
}
func defaultPrefixFunc(_ string) string {
return "X"
}
// defaultReplaceTable finds a word representation for special characters.
func defaultReplaceTable(r rune) (string, bool) {
switch r {
case '@':
return "At ", true
case '&':
return "And ", true
case '|':
return "Pipe ", true
case '$':
return "Dollar ", true
case '!':
return "Bang ", true
case '-':
return "", true
case '_':
return "", true
default:
return "", false
}
}
func optionsWithDefaults(opts []Option) options {
o := options{
commonInitialisms: DefaultInitialisms(),
goNamePrefixFunc: defaultPrefixFunc,
replaceFunc: defaultReplaceTable,
}
for _, apply := range opts {
apply(&o)
}
return o
}
+123
View File
@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mangling
import (
"bytes"
"sync"
)
const maxAllocMatches = 8
type (
// memory pools of temporary objects.
//
// These are used to recycle temporarily allocated objects
// and relieve the GC from undue pressure.
matchesPool struct {
*sync.Pool
}
buffersPool struct {
*sync.Pool
}
lexemsPool struct {
*sync.Pool
}
stringsPool struct {
*sync.Pool
}
)
var (
// poolOfMatches holds temporary slices for recycling during the initialism match process
poolOfMatches = matchesPool{
Pool: &sync.Pool{
New: func() any {
s := make(initialismMatches, 0, maxAllocMatches)
return &s
},
},
}
poolOfBuffers = buffersPool{
Pool: &sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
},
}
poolOfLexems = lexemsPool{
Pool: &sync.Pool{
New: func() any {
s := make([]nameLexem, 0, maxAllocMatches)
return &s
},
},
}
poolOfStrings = stringsPool{
Pool: &sync.Pool{
New: func() any {
s := make([]string, 0, maxAllocMatches)
return &s
},
},
}
)
func (p matchesPool) BorrowMatches() *initialismMatches {
s := p.Get().(*initialismMatches)
*s = (*s)[:0] // reset slice, keep allocated capacity
return s
}
func (p buffersPool) BorrowBuffer(size int) *bytes.Buffer {
s := p.Get().(*bytes.Buffer)
s.Reset()
if s.Cap() < size {
s.Grow(size)
}
return s
}
func (p lexemsPool) BorrowLexems() *[]nameLexem {
s := p.Get().(*[]nameLexem)
*s = (*s)[:0] // reset slice, keep allocated capacity
return s
}
func (p stringsPool) BorrowStrings() *[]string {
s := p.Get().(*[]string)
*s = (*s)[:0] // reset slice, keep allocated capacity
return s
}
func (p matchesPool) RedeemMatches(s *initialismMatches) {
p.Put(s)
}
func (p buffersPool) RedeemBuffer(s *bytes.Buffer) {
p.Put(s)
}
func (p lexemsPool) RedeemLexems(s *[]nameLexem) {
p.Put(s)
}
func (p stringsPool) RedeemStrings(s *[]string) {
p.Put(s)
}
+341
View File
@@ -0,0 +1,341 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mangling
import (
"fmt"
"unicode"
)
type splitterOption func(*splitter)
// withPostSplitInitialismCheck allows to catch initialisms after main split process
func withPostSplitInitialismCheck(s *splitter) {
s.postSplitInitialismCheck = true
}
func withReplaceFunc(fn ReplaceFunc) func(*splitter) {
return func(s *splitter) {
s.replaceFunc = fn
}
}
func withInitialismsCache(c *initialismsCache) splitterOption {
return func(s *splitter) {
s.initialismsCache = c
}
}
type (
initialismMatch struct {
body []rune
start, end int
complete bool
hasPlural pluralForm
}
initialismMatches []initialismMatch
)
// String representation of a match, e.g. for debugging.
func (m initialismMatch) String() string {
return fmt.Sprintf("{body: %s (%d), start: %d, end; %d, complete: %t, hasPlural: %v}",
string(m.body), len(m.body), m.start, m.end, m.complete, m.hasPlural,
)
}
func (m initialismMatch) isZero() bool {
return m.start == 0 && m.end == 0
}
type splitter struct {
*initialismsCache
postSplitInitialismCheck bool
replaceFunc ReplaceFunc
}
func newSplitter(options ...splitterOption) splitter {
var s splitter
for _, option := range options {
option(&s)
}
if s.replaceFunc == nil {
s.replaceFunc = defaultReplaceTable
}
return s
}
func (s splitter) split(name string) *[]nameLexem {
nameRunes := []rune(name)
matches := s.gatherInitialismMatches(nameRunes)
if matches == nil {
return poolOfLexems.BorrowLexems()
}
return s.mapMatchesToNameLexems(nameRunes, matches)
}
func (s splitter) gatherInitialismMatches(nameRunes []rune) *initialismMatches {
matches := poolOfMatches.BorrowMatches()
const minLenInitialism = 1
if len(nameRunes) < minLenInitialism+1 {
// can't match initialism with 0 or 1 rune
return matches
}
// first iteration
s.findMatches(matches, nameRunes, nameRunes[0], 0)
for i, currentRune := range nameRunes[1:] {
currentRunePosition := i + 1
// recycle allocations as we loop over runes
// with such recycling, only 2 slices should be allocated per call
// instead of o(n).
//
// BorrowMatches always yields slices with zero length (with some capacity)
newMatches := poolOfMatches.BorrowMatches()
// check current initialism matches
for _, match := range *matches {
if keepCompleteMatch := match.complete; keepCompleteMatch {
// the match is already complete: keep it then move on to the next match
*newMatches = append(*newMatches, match)
continue
}
if currentRunePosition-match.start == len(match.body) {
// unmatched: skip
continue
}
// 1. by construction of the matches, we can't have currentRunePosition - match.start < 0
// because matches have been computed with their start <= currentRunePosition in the previous
// iterations.
// 2. by construction of the matches, we can't have currentRunePosition - match.start >= len(match.body)
currentMatchRune := match.body[currentRunePosition-match.start]
if currentMatchRune != currentRune {
// failed match, discard it then move on to the next match
continue
}
// try to complete the current match
if currentRunePosition-match.start == len(match.body)-1 {
// we are close: the next step is to check the symbol ahead
// if it is a lowercase letter, then it is not the end of match
// but the beginning of the next word.
//
// NOTE(fredbi): this heuristic sometimes leads to counterintuitive splits and
// perhaps (not sure yet) we should check against case _alternance_.
//
// Example:
//
// In the current version, in the sentence "IDS initialism", "ID" is recognized as an initialism,
// leading to a split like "id_s_initialism" (or IDSInitialism),
// whereas in the sentence "IDx initialism", it is not and produces something like
// "i_d_x_initialism" (or IDxInitialism). The generated file name is not great.
//
// Both go identifiers are tolerated by linters.
//
// Notice that the slightly different input "IDs initialism" is correctly detected
// as a pluralized initialism and produces something like "ids_initialism" (or IDsInitialism).
if currentRunePosition < len(nameRunes)-1 { // when before the last rune
nextRune := nameRunes[currentRunePosition+1]
// recognize a plural form for this initialism (only simple english pluralization is supported).
if nextRune == 's' && match.hasPlural == simplePlural {
// detected a pluralized initialism
match.body = append(match.body, nextRune)
lookAhead := currentRunePosition + 1
if lookAhead < len(nameRunes)-1 {
nextRune = nameRunes[lookAhead+1]
if newWord := unicode.IsLower(nextRune); newWord {
// it is the start of a new word.
// Match is only partial and the initialism is not recognized:
// move on to the next match, but do not advance the rune position
continue
}
}
// this is a pluralized match: keep it
currentRunePosition++
match.complete = true
match.hasPlural = simplePlural
match.end = currentRunePosition
*newMatches = append(*newMatches, match)
// match is complete: keep it then move on to the next match
continue
}
// other cases
// example: invariant plural such as "TLS"
if newWord := unicode.IsLower(nextRune); newWord {
// it is the start of a new word
// Match is only partial and the initialism is not recognized : move on
continue
}
}
match.complete = true
match.end = currentRunePosition
}
// append the ongoing matching attempt: it is not necessarily complete, but was successful so far.
// Let's see if it still matches on the next rune.
*newMatches = append(*newMatches, match)
}
s.findMatches(newMatches, nameRunes, currentRune, currentRunePosition)
poolOfMatches.RedeemMatches(matches)
matches = newMatches
}
// it is up to the caller to redeem this last slice
return matches
}
func (s splitter) findMatches(newMatches *initialismMatches, nameRunes []rune, currentRune rune, currentRunePosition int) {
// check for new initialism matches, based on the first character
for i, r := range s.initialismsRunes {
if r[0] != currentRune {
continue
}
if currentRunePosition+len(r) > len(nameRunes) {
continue // not eligible: would spilll over the initial string
}
// possible matches: all initialisms starting with the current rune and that can fit the given string (nameRunes)
*newMatches = append(*newMatches, initialismMatch{
start: currentRunePosition,
body: r,
complete: false,
hasPlural: s.initialismsPluralForm[i],
})
}
}
func (s splitter) mapMatchesToNameLexems(nameRunes []rune, matches *initialismMatches) *[]nameLexem {
nameLexems := poolOfLexems.BorrowLexems()
var lastAcceptedMatch initialismMatch
for _, match := range *matches {
if !match.complete {
continue
}
if firstMatch := lastAcceptedMatch.isZero(); firstMatch {
s.appendBrokenDownCasualString(nameLexems, nameRunes[:match.start])
*nameLexems = append(*nameLexems, s.breakInitialism(string(match.body)))
lastAcceptedMatch = match
continue
}
if overlappedMatch := match.start <= lastAcceptedMatch.end; overlappedMatch {
continue
}
middle := nameRunes[lastAcceptedMatch.end+1 : match.start]
s.appendBrokenDownCasualString(nameLexems, middle)
*nameLexems = append(*nameLexems, s.breakInitialism(string(match.body)))
lastAcceptedMatch = match
}
// we have not found any accepted matches
if lastAcceptedMatch.isZero() {
*nameLexems = (*nameLexems)[:0]
s.appendBrokenDownCasualString(nameLexems, nameRunes)
} else if lastAcceptedMatch.end+1 != len(nameRunes) {
rest := nameRunes[lastAcceptedMatch.end+1:]
s.appendBrokenDownCasualString(nameLexems, rest)
}
poolOfMatches.RedeemMatches(matches)
return nameLexems
}
func (s splitter) breakInitialism(original string) nameLexem {
return newInitialismNameLexem(original, original)
}
func (s splitter) appendBrokenDownCasualString(segments *[]nameLexem, str []rune) {
currentSegment := poolOfBuffers.BorrowBuffer(len(str)) // unlike strings.Builder, bytes.Buffer initial storage can reused
defer func() {
poolOfBuffers.RedeemBuffer(currentSegment)
}()
addCasualNameLexem := func(original string) {
*segments = append(*segments, newCasualNameLexem(original))
}
addInitialismNameLexem := func(original, match string) {
*segments = append(*segments, newInitialismNameLexem(original, match))
}
var addNameLexem func(string)
if s.postSplitInitialismCheck {
addNameLexem = func(original string) {
for i := range s.initialisms {
if isEqualFoldIgnoreSpace(s.initialismsUpperCased[i], original) {
addInitialismNameLexem(original, s.initialisms[i])
return
}
}
addCasualNameLexem(original)
}
} else {
addNameLexem = addCasualNameLexem
}
// NOTE: (performance). The few remaining non-amortized allocations
// lay in the code below: using String() forces
for _, rn := range str {
if replace, found := s.replaceFunc(rn); found {
if currentSegment.Len() > 0 {
addNameLexem(currentSegment.String())
currentSegment.Reset()
}
if replace != "" {
addNameLexem(replace)
}
continue
}
if !unicode.In(rn, unicode.L, unicode.M, unicode.N, unicode.Pc) {
if currentSegment.Len() > 0 {
addNameLexem(currentSegment.String())
currentSegment.Reset()
}
continue
}
if unicode.IsUpper(rn) {
if currentSegment.Len() > 0 {
addNameLexem(currentSegment.String())
}
currentSegment.Reset()
}
currentSegment.WriteRune(rn)
}
if currentSegment.Len() > 0 {
addNameLexem(currentSegment.String())
}
}
+11
View File
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mangling
import "unsafe"
// hackStringBytes returns the (unsafe) underlying bytes slice of a string.
func hackStringBytes(str string) []byte {
return unsafe.Slice(unsafe.StringData(str), len(str))
}
+118
View File
@@ -0,0 +1,118 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
package mangling
import (
"strings"
"unicode"
"unicode/utf8"
)
// Removes leading whitespaces
func trim(str string) string { return strings.TrimSpace(str) }
// upper is strings.ToUpper() combined with trim
func upper(str string) string {
return strings.ToUpper(trim(str))
}
// lower is strings.ToLower() combined with trim
func lower(str string) string {
return strings.ToLower(trim(str))
}
// isEqualFoldIgnoreSpace is the same as strings.EqualFold, but
// it ignores leading and trailing blank spaces in the compared
// string.
//
// base is assumed to be composed of upper-cased runes, and be already
// trimmed.
//
// This code is heavily inspired from strings.EqualFold.
func isEqualFoldIgnoreSpace(base []rune, str string) bool {
var i, baseIndex int
// equivalent to b := []byte(str), but without data copy
b := hackStringBytes(str)
for i < len(b) {
if c := b[i]; c < utf8.RuneSelf {
// fast path for ASCII
if c != ' ' && c != '\t' {
break
}
i++
continue
}
// unicode case
r, size := utf8.DecodeRune(b[i:])
if !unicode.IsSpace(r) {
break
}
i += size
}
if i >= len(b) {
return len(base) == 0
}
for _, baseRune := range base {
if i >= len(b) {
break
}
if c := b[i]; c < utf8.RuneSelf {
// single byte rune case (ASCII)
if baseRune >= utf8.RuneSelf {
return false
}
baseChar := byte(baseRune)
if c != baseChar && ((c < 'a') || (c > 'z') || (c-'a'+'A' != baseChar)) {
return false
}
baseIndex++
i++
continue
}
// unicode case
r, size := utf8.DecodeRune(b[i:])
if unicode.ToUpper(r) != baseRune {
return false
}
baseIndex++
i += size
}
if baseIndex != len(base) {
return false
}
// all passed: now we should only have blanks
for i < len(b) {
if c := b[i]; c < utf8.RuneSelf {
// fast path for ASCII
if c != ' ' && c != '\t' {
return false
}
i++
continue
}
// unicode case
r, size := utf8.DecodeRune(b[i:])
if !unicode.IsSpace(r) {
return false
}
i += size
}
return true
}