├── go.mod
├── .gitignore
├── .github
├── workflows
│ ├── go-test.yml
│ ├── auto-merge.yml
│ ├── release.yml
│ ├── contributors.yml
│ ├── tag-release.yml
│ ├── scanner.yml
│ ├── codeql.yml
│ └── bump-release.yml
├── DCO.md
├── dependabot.yaml
└── CONTRIBUTING.md
├── .editorconfig
├── CONTRIBUTORS.md
├── SECURITY.md
├── .golangci.yml
├── NOTICE
├── CODE_OF_CONDUCT.md
├── docs
├── STYLE.md
└── MAINTAINERS.md
├── README.md
├── .cliff.toml
├── LICENSE
├── inflect_test.go
└── inflect.go
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-openapi/inflect
2 |
3 | go 1.24
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | secrets.yml
2 | coverage.out
3 | coverage.txt
4 | *.cov
5 | .idea
6 |
--------------------------------------------------------------------------------
/.github/workflows/go-test.yml:
--------------------------------------------------------------------------------
1 | name: go test
2 |
3 | permissions:
4 | pull-requests: read
5 | contents: read
6 |
7 | on:
8 | push:
9 | branches:
10 | - master
11 |
12 | pull_request:
13 |
14 | jobs:
15 | test:
16 | uses: go-openapi/ci-workflows/.github/workflows/go-test.yml@d0b50195d14745b9a9a8a41cf3bb7ecd874af37a # v0.1.1
17 | secrets: inherit
18 |
--------------------------------------------------------------------------------
/.github/workflows/auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot auto-merge
2 |
3 | permissions:
4 | contents: read
5 |
6 | on:
7 | pull_request:
8 |
9 | jobs:
10 | dependabot:
11 | permissions:
12 | contents: write
13 | pull-requests: write
14 | uses: go-openapi/ci-workflows/.github/workflows/auto-merge.yml@d0b50195d14745b9a9a8a41cf3bb7ecd874af37a # v0.1.1
15 | secrets: inherit
16 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v[0-9]+*
7 |
8 | permissions:
9 | contents: read
10 |
11 | jobs:
12 | release:
13 | permissions:
14 | contents: write
15 | uses: go-openapi/ci-workflows/.github/workflows/release.yml@d0b50195d14745b9a9a8a41cf3bb7ecd874af37a # v0.1.1
16 | with:
17 | tag: ${{ github.ref_name }}
18 | secrets: inherit
19 |
--------------------------------------------------------------------------------
/.github/workflows/contributors.yml:
--------------------------------------------------------------------------------
1 | name: Contributors
2 |
3 | on:
4 | schedule:
5 | - cron: '18 4 * * 6'
6 |
7 | workflow_dispatch:
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | contributors:
14 | permissions:
15 | pull-requests: write
16 | contents: write
17 | uses: go-openapi/ci-workflows/.github/workflows/contributors.yml@d0b50195d14745b9a9a8a41cf3bb7ecd874af37a # v0.1.1
18 | secrets: inherit
19 |
--------------------------------------------------------------------------------
/.github/workflows/tag-release.yml:
--------------------------------------------------------------------------------
1 | name: Release on tag
2 |
3 | permissions:
4 | contents: read
5 |
6 | on:
7 | push:
8 | tags:
9 | - v[0-9]+*
10 |
11 | jobs:
12 | gh-release:
13 | name: Create release
14 | permissions:
15 | contents: write
16 | uses: go-openapi/ci-workflows/.github/workflows/release.yml@d0b50195d14745b9a9a8a41cf3bb7ecd874af37a # v0.1.1
17 | with:
18 | tag: ${{ github.ref_name }}
19 | secrets: inherit
20 |
--------------------------------------------------------------------------------
/.github/workflows/scanner.yml:
--------------------------------------------------------------------------------
1 | name: Vulnerability scans
2 |
3 | on:
4 | branch_protection_rule:
5 | push:
6 | branches: [ "master" ]
7 | schedule:
8 | - cron: '18 4 * * 3'
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | scanners:
15 | permissions:
16 | contents: read
17 | security-events: write
18 | uses: go-openapi/ci-workflows/.github/workflows/scanner.yml@d0b50195d14745b9a9a8a41cf3bb7ecd874af37a # V0.1.1
19 | secrets: inherit
20 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 | paths-ignore: # remove this clause if CodeQL is a required check
9 | - '**/*.md'
10 | schedule:
11 | - cron: '39 19 * * 5'
12 |
13 | permissions:
14 | contents: read
15 |
16 | jobs:
17 | codeql:
18 | permissions:
19 | contents: read
20 | security-events: write
21 | uses: go-openapi/ci-workflows/.github/workflows/codeql.yml@d0b50195d14745b9a9a8a41cf3bb7ecd874af37a # v0.1.1
22 | secrets: inherit
23 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | # Unix-style newlines with a newline ending every file
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 | trim_trailing_whitespace = true
11 |
12 | # Set default charset
13 | [*.{js,py,go,scala,rb,java,html,css,less,sass,md}]
14 | charset = utf-8
15 |
16 | # Tab indentation (no size specified)
17 | [*.go]
18 | indent_style = tab
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
23 | # Matches the exact files either package.json or .travis.yml
24 | [{package.json,.travis.yml}]
25 | indent_style = space
26 | indent_size = 2
27 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | # Contributors
2 |
3 | - Repository: ['go-openapi/inflect']
4 |
5 | | Total Contributors | Total Contributions |
6 | | --- | --- |
7 | | 5 | 42 |
8 |
9 | | Username | All Time Contribution Count | All Commits |
10 | | --- | --- | --- |
11 | | @fredbi | 21 | https://github.com/go-openapi/inflect/commits?author=fredbi |
12 | | @chrisfarms | 15 | https://github.com/go-openapi/inflect/commits?author=chrisfarms |
13 | | @hirochachacha | 4 | https://github.com/go-openapi/inflect/commits?author=hirochachacha |
14 | | @casualjim | 1 | https://github.com/go-openapi/inflect/commits?author=casualjim |
15 | | @roktas | 1 | https://github.com/go-openapi/inflect/commits?author=roktas |
16 |
17 | _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_
18 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | This policy outlines the commitment and practices of the go-openapi maintainers regarding security.
4 |
5 | ## Supported Versions
6 |
7 | | Version | Supported |
8 | | ------- | ------------------ |
9 | | 0.22.x | :white_check_mark: |
10 |
11 | ## Reporting a vulnerability
12 |
13 | If you become aware of a security vulnerability that affects the current repository,
14 | please report it privately to the maintainers.
15 |
16 | Please follow the instructions provided by github to
17 | [Privately report a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability).
18 |
19 | TL;DR: on Github, navigate to the project's "Security" tab then click on "Report a vulnerability".
20 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | linters:
3 | default: all
4 | disable:
5 | - depguard
6 | - funlen
7 | - godox
8 | - exhaustruct
9 | - nlreturn
10 | - nonamedreturns
11 | - noinlineerr
12 | - paralleltest
13 | - recvcheck
14 | - testpackage
15 | - tparallel
16 | - varnamelen
17 | - whitespace
18 | - wrapcheck
19 | - wsl
20 | - wsl_v5
21 | settings:
22 | dupl:
23 | threshold: 200
24 | goconst:
25 | min-len: 2
26 | min-occurrences: 3
27 | cyclop:
28 | max-complexity: 20
29 | gocyclo:
30 | min-complexity: 20
31 | exhaustive:
32 | default-signifies-exhaustive: true
33 | default-case-required: true
34 | lll:
35 | line-length: 180
36 | exclusions:
37 | generated: lax
38 | presets:
39 | - comments
40 | - common-false-positives
41 | - legacy
42 | - std-error-handling
43 | paths:
44 | - third_party$
45 | - builtin$
46 | - examples$
47 | formatters:
48 | enable:
49 | - gofmt
50 | - goimports
51 | - gofumpt
52 | exclusions:
53 | generated: lax
54 | paths:
55 | - third_party$
56 | - builtin$
57 | - examples$
58 | issues:
59 | # Maximum issues count per one linter.
60 | # Set to 0 to disable.
61 | # Default: 50
62 | max-issues-per-linter: 0
63 | # Maximum count of issues with the same text.
64 | # Set to 0 to disable.
65 | # Default: 3
66 | max-same-issues: 0
67 |
--------------------------------------------------------------------------------
/.github/workflows/bump-release.yml:
--------------------------------------------------------------------------------
1 | name: Bump Release
2 |
3 | permissions:
4 | contents: read
5 |
6 | on:
7 | workflow_dispatch:
8 | inputs:
9 | bump-patch:
10 | description: Bump a patch version release
11 | type: boolean
12 | required: false
13 | default: true
14 | bump-minor:
15 | description: Bump a minor version release
16 | type: boolean
17 | required: false
18 | default: false
19 | bump-major:
20 | description: Bump a major version release
21 | type: boolean
22 | required: false
23 | default: false
24 | tag-message-title:
25 | description: Tag message title to prepend to the release notes
26 | required: false
27 | type: string
28 | tag-message-body:
29 | description: |
30 | Tag message body to prepend to the release notes.
31 | (use "|" to replace end of line).
32 | required: false
33 | type: string
34 |
35 | jobs:
36 | bump-release:
37 | permissions:
38 | contents: write
39 | uses: go-openapi/ci-workflows/.github/workflows/bump-release.yml@d0b50195d14745b9a9a8a41cf3bb7ecd874af37a # v0.1.1
40 | with:
41 | bump-patch: ${{ inputs.bump-patch }}
42 | bump-minor: ${{ inputs.bump-minor }}
43 | bump-major: ${{ inputs.bump-major }}
44 | tag-message-title: ${{ inputs.tag-message-title }}
45 | tag-message-body: ${{ inputs.tag-message-body }}
46 | secrets: inherit
47 |
--------------------------------------------------------------------------------
/.github/DCO.md:
--------------------------------------------------------------------------------
1 | # Developer's Certificate of Origin
2 |
3 | ```
4 | Developer Certificate of Origin
5 | Version 1.1
6 |
7 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
8 | 660 York Street, Suite 102,
9 | San Francisco, CA 94110 USA
10 |
11 | Everyone is permitted to copy and distribute verbatim copies of this
12 | license document, but changing it is not allowed.
13 |
14 |
15 | Developer's Certificate of Origin 1.1
16 |
17 | By making a contribution to this project, I certify that:
18 |
19 | (a) The contribution was created in whole or in part by me and I
20 | have the right to submit it under the open source license
21 | indicated in the file; or
22 |
23 | (b) The contribution is based upon previous work that, to the best
24 | of my knowledge, is covered under an appropriate open source
25 | license and I have the right under that license to submit that
26 | work with modifications, whether created in whole or in part
27 | by me, under the same open source license (unless I am
28 | permitted to submit under a different license), as indicated
29 | in the file; or
30 |
31 | (c) The contribution was provided directly to me by some other
32 | person who certified (a), (b) or (c) and I have not modified
33 | it.
34 |
35 | (d) I understand and agree that this project and the contribution
36 | are public and that a record of the contribution (including all
37 | personal information I submit with it, including my sign-off) is
38 | maintained indefinitely and may be redistributed consistent with
39 | this project or the open source license(s) involved.
40 | ```
41 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | day: "friday"
8 | open-pull-requests-limit: 2 # <- default is 5
9 | groups: # <- group all github actions updates in a single PR
10 | # 1. development-dependencies are auto-merged
11 | development-dependencies:
12 | patterns:
13 | - '*'
14 |
15 | - package-ecosystem: "gomod"
16 | # We define 4 groups of dependencies to regroup update pull requests:
17 | # - development (e.g. test dependencies)
18 | # - go-openapi updates
19 | # - golang.org (e.g. golang.org/x/... packages)
20 | # - other dependencies (direct or indirect)
21 | #
22 | # * All groups are checked once a week and each produce at most 1 PR.
23 | # * All dependabot PRs are auto-approved
24 | #
25 | # Auto-merging policy, when requirements are met:
26 | # 1. development-dependencies are auto-merged
27 | # 2. golang.org-dependencies are auto-merged
28 | # 3. go-openapi patch updates are auto-merged. Minor/major version updates require a manual merge.
29 | # 4. other dependencies require a manual merge
30 | directory: "/"
31 | schedule:
32 | interval: "weekly"
33 | day: "friday"
34 | open-pull-requests-limit: 4
35 | groups:
36 | development-dependencies:
37 | patterns:
38 | - "github.com/stretchr/testify"
39 |
40 | golang-org-dependencies:
41 | patterns:
42 | - "golang.org/*"
43 |
44 | go-openapi-dependencies:
45 | patterns:
46 | - "github.com/go-openapi/*"
47 |
48 | other-dependencies:
49 | exclude-patterns:
50 | - "github.com/go-openapi/*"
51 | - "github.com/stretchr/testify"
52 | - "golang.org/*"
53 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright 2015-2025 go-swagger maintainers
2 |
3 | // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
4 | // SPDX-License-Identifier: Apache-2.0
5 |
6 | This software library, github.com/go-openapi/jsonpointer, includes software developed
7 | by the go-swagger and go-openapi maintainers ("go-swagger maintainers").
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License");
10 | you may not use this software except in compliance with the License.
11 |
12 | You may obtain a copy of the License at
13 |
14 | http://www.apache.org/licenses/LICENSE-2.0.
15 |
16 | This software is copied from, derived from, and inspired by other original software products.
17 | It ships with copies of other software which license terms are recalled below.
18 |
19 | The original software was authored by Chris Farmiloe at https://bitbucket.org/pkg/inflect under a MIT License.
20 |
21 | ghttps://bitbucket.org/pkg/inflect
22 | ===========================
23 |
24 | Copyright (c) 2011 Chris Farmiloe
25 |
26 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
27 |
28 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
29 |
30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at ivan+abuse@flanders.co.nz. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/docs/STYLE.md:
--------------------------------------------------------------------------------
1 | # Coding style at `go-openapi`
2 |
3 | > **TL;DR**
4 | >
5 | > Let's be honest: at `go-openapi` and `go-swagger` we've never been super-strict on code style etc.
6 | >
7 | > But perhaps now (2025) is the time to adopt a different stance.
8 |
9 | Even though our repos have been early adopters of `golangci-lint` years ago
10 | (we used some other metalinter before), our decade-old codebase is only realigned to new rules from time to time.
11 |
12 | Now go-openapi and go-swagger make up a really large codebase, which is taxing to maintain and keep afloat.
13 |
14 | Code quality and the harmonization of rules have thus become things that we need now.
15 |
16 | ## Meta-linter
17 |
18 | Universally formatted go code promotes ease of writing, reading, and maintenance.
19 |
20 | You should run `golangci-lint run` before committing your changes.
21 |
22 | Many editors have plugins that do that automatically.
23 |
24 | > We use the `golangci-lint` meta-linter. The configuration lies in [`.golangci.yml`](../.golangci.yml).
25 | > You may read for additional reference.
26 |
27 | ## Linting rules posture
28 |
29 | Thanks to go's original design, we developers don't have to waste much time arguing about code figures of style.
30 |
31 | However, the number of available linters has been growing to the point that we need to pick a choice.
32 |
33 | We enable all linters published by `golangci-lint` by default, then disable a few ones.
34 |
35 | Here are the reasons why they are disabled (update: Nov. 2025, `golangci-lint v2.6.1`):
36 |
37 | ```yaml
38 | disable:
39 | - depguard # we don't want to configure rules to constrain import. That's the reviewer's job
40 | - exhaustruct # we don't want to configure regexp's to check type name. That's the reviewer's job
41 | - funlen # we accept cognitive complexity as a meaningful metric, but function length is relevant
42 | - godox # we don't see any value in forbidding TODO's etc in code
43 | - nlreturn # we usually apply this "blank line" rule to make code less compact. We just don't want to enforce it
44 | - nonamedreturns # we don't see any valid reason why we couldn't used named returns
45 | - noinlineerr # there is no value added forbidding inlined err
46 | - paralleltest # we like parallel tests. We just don't want them to be enforced everywhere
47 | - recvcheck # we like the idea of having pointer and non-pointer receivers
48 | - testpackage # we like test packages. We just don't want them to be enforced everywhere
49 | - tparallel # see paralleltest
50 | - varnamelen # sometimes, we like short variables. The linter doesn't catch cases when a short name is good
51 | - whitespace # no added value
52 | - wrapcheck # although there is some sense with this linter's general idea, it produces too much noise
53 | - wsl # no added value. Noise
54 | - wsl_v5 # no added value. Noise
55 | ```
56 |
57 | As you may see, we agree with the objectives of most linters, at least the principle they are supposed to enforce.
58 | But all linters do not support fine-grained tuning to tolerate some cases and not some others.
59 |
60 | When this is possible, we enable linters with relaxed constraints:
61 |
62 | ```yaml
63 | settings:
64 | dupl:
65 | threshold: 200 # in a older code base such as ours, we have to be tolerant with a little redundancy
66 | # Hopefully, we'll be able to gradually get rid of those.
67 | goconst:
68 | min-len: 2
69 | min-occurrences: 3
70 | cyclop:
71 | max-complexity: 20 # the default is too low for most of our functions. 20 is a nicer trade-off
72 | gocyclo:
73 | min-complexity: 20
74 | exhaustive: # when using default in switch, this should be good enough
75 | default-signifies-exhaustive: true
76 | default-case-required: true
77 | lll:
78 | line-length: 180 # we just want to avoid extremely long lines.
79 | # It is no big deal if a line or two don't fit on your terminal.
80 | ```
81 |
82 | Final note: since we have switched to a forked version of `stretchr/testify`,
83 | we no longer benefit from the great `testifylint` linter for tests.
84 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # inflect
2 |
3 |
4 | [![Tests][test-badge]][test-url] [![Coverage][cov-badge]][cov-url] [![CI vuln scan][vuln-scan-badge]][vuln-scan-url] [![CodeQL][codeql-badge]][codeql-url]
5 |
6 |
7 |
8 | [![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url]
9 |
10 |
11 | [![GoDoc][godoc-badge]][godoc-url] [![Slack Channel][slack-logo]![slack-badge]][slack-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge]
12 |
13 | ---
14 |
15 | A package to pluralize words.
16 |
17 | Originally forked from https://bitbucket.org/pkg/inflect under a MIT License.
18 |
19 | ## Status
20 |
21 | API is stable.
22 |
23 | This library is not used at all by other go-openapi packages and is somewhat redundant with
24 | go-openapi/swag/mangling (for camelcase etc).
25 |
26 | Currently we have one single dependency in one place in a go-swagger template (used as a funcmap).
27 |
28 | ## Import this library in your project
29 |
30 | ```cmd
31 | go get github.com/go-openapi/inflect
32 | ```
33 |
34 | ## Basic usage
35 |
36 | A golang library applying grammar rules to English words.
37 |
38 | > This package provides a basic set of functions applying
39 | > grammar rules to inflect English words, modify case style
40 | > (Capitalize, camelCase, snake_case, etc.).
41 | >
42 | > Acronyms are properly handled. A common use case is word pluralization.
43 |
44 | ## Change log
45 |
46 | See
47 |
48 |
51 |
52 | ## Licensing
53 |
54 | This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE).
55 |
56 | See the license [NOTICE](./NOTICE), which recalls the licensing terms of all the pieces of software
57 | on top of which it has been built.
58 |
59 |
62 |
63 | ## Other documentation
64 |
65 | * [All-time contributors](./CONTRIBUTORS.md)
66 | * [Contributing guidelines](.github/CONTRIBUTING.md)
67 |
71 |
72 | ## Cutting a new release
73 |
74 | Maintainers can cut a new release by either:
75 |
76 | * running [this workflow](https://github.com/go-openapi/inflect/actions/workflows/bump-release.yml)
77 | * or pushing a semver tag
78 | * signed tags are preferred
79 | * The tag message is prepended to release notes
80 |
81 |
82 | [test-badge]: https://github.com/go-openapi/inflect/actions/workflows/go-test.yml/badge.svg
83 | [test-url]: https://github.com/go-openapi/inflect/actions/workflows/go-test.yml
84 | [cov-badge]: https://codecov.io/gh/go-openapi/inflect/branch/master/graph/badge.svg
85 | [cov-url]: https://codecov.io/gh/go-openapi/inflect
86 | [vuln-scan-badge]: https://github.com/go-openapi/inflect/actions/workflows/scanner.yml/badge.svg
87 | [vuln-scan-url]: https://github.com/go-openapi/inflect/actions/workflows/scanner.yml
88 | [codeql-badge]: https://github.com/go-openapi/inflect/actions/workflows/codeql.yml/badge.svg
89 | [codeql-url]: https://github.com/go-openapi/inflect/actions/workflows/codeql.yml
90 |
91 | [release-badge]: https://badge.fury.io/gh/go-openapi%2Finflect.svg
92 | [release-url]: https://badge.fury.io/gh/go-openapi%2Finflect
93 | [gomod-badge]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Finflect.svg
94 | [gomod-url]: https://badge.fury.io/go/github.com%2Fgo-openapi%2Finflect
95 |
96 | [gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/inflect
97 | [gocard-url]: https://goreportcard.com/report/github.com/go-openapi/inflect
98 | [codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/inflect
99 | [codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/inflect
100 |
101 | [doc-badge]: https://img.shields.io/badge/doc-site-blue?link=https%3A%2F%2Fgoswagger.io%2Fgo-openapi%2F
102 | [doc-url]: https://goswagger.io/go-openapi
103 | [godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/inflect
104 | [godoc-url]: http://pkg.go.dev/github.com/go-openapi/inflect
105 | [slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
106 | [slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
107 | [slack-url]: https://goswagger.slack.com/archives/C04R30YMU
108 |
109 | [license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
110 | [license-url]: https://github.com/go-openapi/inflect/?tab=Apache-2.0-1-ov-file#readme
111 |
112 | [goversion-badge]: https://img.shields.io/github/go-mod/go-version/go-openapi/inflect
113 | [goversion-url]: https://github.com/go-openapi/inflect/blob/master/go.mod
114 | [top-badge]: https://img.shields.io/github/languages/top/go-openapi/inflect
115 | [commits-badge]: https://img.shields.io/github/commits-since/go-openapi/inflect/latest
116 |
--------------------------------------------------------------------------------
/docs/MAINTAINERS.md:
--------------------------------------------------------------------------------
1 | # Maintainer's guide
2 |
3 | ## Repo structure
4 |
5 | Single go module.
6 |
7 | > **NOTE**
8 | >
9 | > Some `go-openapi` repos are mono-repos with multiple modules,
10 | > with adapted CI workflows.
11 |
12 | ## Repo configuration
13 |
14 | * default branch: master
15 | * protected branches: master
16 | * branch protection rules:
17 | * require pull requests and approval
18 | * required status checks:
19 | - DCO (simple email sign-off)
20 | - Lint
21 | - tests completed
22 | * auto-merge enabled (used for dependabot updates)
23 |
24 | ## Continuous Integration
25 |
26 | ### Code Quality checks
27 |
28 | * meta-linter: golangci-lint
29 | * linter config: [`.golangci.yml`](../.golangci.yml) (see our [posture](./STYLE.md) on linters)
30 |
31 | * Code quality assessment: [CodeFactor](https://www.codefactor.io/dashboard)
32 | * Code quality badges
33 | * go report card:
34 | * CodeFactor:
35 |
36 | > **NOTES**
37 | >
38 | > codefactor inherits roles from github. There is no need to create a dedicated account.
39 | >
40 | > The codefactor app is installed at the organization level (`github.com/go-openapi`).
41 | >
42 | > There is no special token to setup in github for CI usage.
43 |
44 | ### Testing
45 |
46 | * Test reports
47 | * Uploaded to codecov:
48 | * Test coverage reports
49 | * Uploaded to codecov:
50 |
51 | * Fuzz testing
52 | * Fuzz tests are handled separately by CI and may reuse a cached version of the fuzzing corpus.
53 | At this moment, cache may not be shared between feature branches or feature branch and master.
54 | The minimized corpus produced on failure is uploaded as an artifact and should be added manually
55 | to `testdata/fuzz/...`.
56 |
57 | Coverage threshold status is informative and not blocking.
58 | This is because the thresholds are difficult to tune and codecov oftentimes reports false negatives
59 | or may fail to upload coverage.
60 |
61 | All tests use our fork of `stretchr/testify`: `github.com/go-openapi/testify`.
62 | This allows for minimal test dependencies.
63 |
64 | > **NOTES**
65 | >
66 | > codecov inherits roles from github. There is no need to create a dedicated account.
67 | > However, there is only 1 maintainer allowed to be the admin of the organization on codecov
68 | > with their free plan.
69 | >
70 | > The codecov app is installed at the organization level (`github.com/go-openapi`).
71 | >
72 | > There is no special token to setup in github for CI usage.
73 | > A organization-level token used to upload coverage and test reports is managed at codecov:
74 | > no setup is required on github.
75 |
76 | ### Automated updates
77 |
78 | * dependabot
79 | * configuration: [`dependabot.yaml`](../.github/dependabot.yaml)
80 |
81 | Principle:
82 |
83 | * codecov applies updates and security patches to the github-actions and golang ecosystems.
84 | * all updates from "trusted" dependencies (github actions, golang.org packages, go-openapi packages
85 | are auto-merged if they successfully pass CI.
86 |
87 | * go version udpates
88 |
89 | Principle:
90 |
91 | * we support the 2 latest minor versions of the go compiler (`stable`, `oldstable`)
92 | * `go.mod` should be updated (manually) whenever there is a new go minor release
93 | (e.g. every 6 months).
94 |
95 | * contributors
96 | * a [`CONTRIBUTORS.md`](../CONTRIBUTORS.md) file is updated weekly, with all-time contributors to the repository
97 | * the `github-actions[bot]` posts a pull request to do that automatically
98 | * at this moment, this pull request is not auto-approved/auto-merged (bot cannot approve its own PRs)
99 |
100 | ### Vulnerability scanners
101 |
102 | There are 3 complementary scanners - obviously, there is some overlap, but each has a different focus.
103 |
104 | * github `CodeQL`
105 | * `trivy`
106 | * `govulnscan`
107 |
108 | None of these tools require an additional account or token.
109 |
110 | Github CodeQL configuration is set to "Advanced", so we may collect a CI status for this check (e.g. for badges).
111 |
112 | Scanners run on every commit to master and at least once a week.
113 |
114 | Reports are centralized in github security reports for code scanning tools.
115 |
116 | ## Releases
117 |
118 | The release process is minimalist:
119 |
120 | * push a semver tag (i.e v{major}.{minor}.{patch}) to the master branch.
121 | * the CI handles this to generate a github release with release notes
122 |
123 | * release notes generator: git-cliff
124 | * configuration: [`cliff.toml`](../.cliff.toml)
125 |
126 | Tags are preferably PGP-signed.
127 |
128 | The tag message introduces the release notes (e.g. a summary of this release).
129 |
130 | The release notes generator does not assume that commits are necessarily "conventional commits".
131 |
132 | ## Other files
133 |
134 | Standard documentation:
135 |
136 | * [`CONTRIBUTING.md`](../.github/CONTRIBUTING.md) guidelines
137 | * [`DCO.md`](../.github/DCO.md) terms for first-time contributors to read
138 | * [`CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md)
139 | * [`SECURIY.md`](../SECURITY.md) policy: how to report vulnerabilities privately
140 | * [`LICENSE`](../LICENSE) terms
141 | * [`NOTICE`](../NOTICE) on supplementary license terms (original authors, copied code etc)
142 |
143 | Reference documentation (released):
144 |
145 | * [godoc](https://pkg.go/dev/go-openapi/inflect)
146 |
147 | ## TODOs & other ideas
148 |
149 | A few things remain ahead to ease a bit a maintainer's job:
150 |
151 | * [x] reuse CI workflows (e.g. in `github.com/go-openapi/workflows`)
152 | * [x] reusable actions with custom tools pinned (e.g. in `github.com/go-openapi/gh-actions`)
153 | * [ ] open-source license checks
154 | * [x] auto-merge for CONTRIBUTORS.md (requires a github app to produce tokens)
155 | * [ ] more automated code renovation / relinting work (possibly built with CLAUDE)
156 | * [ ] organization-level documentation web site
157 | * ...
158 |
--------------------------------------------------------------------------------
/.cliff.toml:
--------------------------------------------------------------------------------
1 | # git-cliff ~ configuration file
2 | # https://git-cliff.org/docs/configuration
3 |
4 | [changelog]
5 | header = """
6 | """
7 |
8 | footer = """
9 |
10 | -----
11 |
12 | **[{{ remote.github.repo }}]({{ self::remote_url() }}) license terms**
13 |
14 | [![License][license-badge]][license-url]
15 |
16 | [license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
17 | [license-url]: {{ self::remote_url() }}/?tab=Apache-2.0-1-ov-file#readme
18 |
19 | {%- macro remote_url() -%}
20 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
21 | {%- endmacro -%}
22 | """
23 |
24 | body = """
25 | {%- if version %}
26 | ## [{{ version | trim_start_matches(pat="v") }}]({{ self::remote_url() }}/tree/{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
27 | {%- else %}
28 | ## [unreleased]
29 | {%- endif %}
30 | {%- if message %}
31 | {%- raw %}\n{% endraw %}
32 | {{ message }}
33 | {%- raw %}\n{% endraw %}
34 | {%- endif %}
35 | {%- if version %}
36 | {%- if previous.version %}
37 |
38 | **Full Changelog**: <{{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}>
39 | {%- endif %}
40 | {%- else %}
41 | {%- raw %}\n{% endraw %}
42 | {%- endif %}
43 |
44 | {%- if statistics %}{% if statistics.commit_count %}
45 | {%- raw %}\n{% endraw %}
46 | {{ statistics.commit_count }} commits in this release.
47 | {%- raw %}\n{% endraw %}
48 | {%- endif %}{% endif %}
49 | -----
50 |
51 | {%- for group, commits in commits | group_by(attribute="group") %}
52 | {%- raw %}\n{% endraw %}
53 | ### {{ group | upper_first }}
54 | {%- raw %}\n{% endraw %}
55 | {%- for commit in commits %}
56 | {%- if commit.remote.pr_title %}
57 | {%- set commit_message = commit.remote.pr_title %}
58 | {%- else %}
59 | {%- set commit_message = commit.message %}
60 | {%- endif %}
61 | * {{ commit_message | split(pat="\n") | first | trim }}
62 | {%- if commit.remote.username %}
63 | {%- raw %} {% endraw %}by [@{{ commit.remote.username }}](https://github.com/{{ commit.remote.username }})
64 | {%- endif %}
65 | {%- if commit.remote.pr_number %}
66 | {%- raw %} {% endraw %}in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }})
67 | {%- endif %}
68 | {%- raw %} {% endraw %}[...]({{ self::remote_url() }}/commit/{{ commit.id }})
69 | {%- endfor %}
70 | {%- endfor %}
71 |
72 | {%- if github %}
73 | {%- raw %}\n{% endraw -%}
74 | {%- set all_contributors = github.contributors | length %}
75 | {%- if github.contributors | filter(attribute="username", value="dependabot[bot]") | length < all_contributors %}
76 | -----
77 |
78 | ### People who contributed to this release
79 | {% endif %}
80 | {%- for contributor in github.contributors | filter(attribute="username") | sort(attribute="username") %}
81 | {%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %}
82 | * [@{{ contributor.username }}](https://github.com/{{ contributor.username }})
83 | {%- endif %}
84 | {%- endfor %}
85 |
86 | {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
87 | -----
88 | {%- raw %}\n{% endraw %}
89 |
90 | ### New Contributors
91 | {%- endif %}
92 |
93 | {%- for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
94 | {%- if contributor.username != "dependabot[bot]" and contributor.username != "github-actions[bot]" %}
95 | * @{{ contributor.username }} made their first contribution
96 | {%- if contributor.pr_number %}
97 | in [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
98 | {%- endif %}
99 | {%- endif %}
100 | {%- endfor %}
101 | {%- endif %}
102 |
103 | {%- raw %}\n{% endraw %}
104 |
105 | {%- macro remote_url() -%}
106 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
107 | {%- endmacro -%}
108 | """
109 | # Remove leading and trailing whitespaces from the changelog's body.
110 | trim = true
111 | # Render body even when there are no releases to process.
112 | render_always = true
113 | # An array of regex based postprocessors to modify the changelog.
114 | postprocessors = [
115 | # Replace the placeholder with a URL.
116 | #{ pattern = '', replace = "https://github.com/orhun/git-cliff" },
117 | ]
118 | # output file path
119 | # output = "test.md"
120 |
121 | [git]
122 | # Parse commits according to the conventional commits specification.
123 | # See https://www.conventionalcommits.org
124 | conventional_commits = false
125 | # Exclude commits that do not match the conventional commits specification.
126 | filter_unconventional = false
127 | # Require all commits to be conventional.
128 | # Takes precedence over filter_unconventional.
129 | require_conventional = false
130 | # Split commits on newlines, treating each line as an individual commit.
131 | split_commits = false
132 | # An array of regex based parsers to modify commit messages prior to further processing.
133 | commit_preprocessors = [
134 | # Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
135 | #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"},
136 | # Check spelling of the commit message using https://github.com/crate-ci/typos.
137 | # If the spelling is incorrect, it will be fixed automatically.
138 | #{ pattern = '.*', replace_command = 'typos --write-changes -' }
139 | ]
140 | # Prevent commits that are breaking from being excluded by commit parsers.
141 | protect_breaking_commits = false
142 | # An array of regex based parsers for extracting data from the commit message.
143 | # Assigns commits to groups.
144 | # Optionally sets the commit's scope and can decide to exclude commits from further processing.
145 | commit_parsers = [
146 | { message = "^[Cc]hore\\([Rr]elease\\): prepare for", skip = true },
147 | { message = "(^[Mm]erge)|([Mm]erge conflict)", skip = true },
148 | { field = "author.name", pattern = "dependabot*", group = "Updates" },
149 | { message = "([Ss]ecurity)|([Vv]uln)", group = "Security" },
150 | { body = "(.*[Ss]ecurity)|([Vv]uln)", group = "Security" },
151 | { message = "([Cc]hore\\(lint\\))|(style)|(lint)|(codeql)|(golangci)", group = "Code quality" },
152 | { message = "(^[Dd]oc)|((?i)readme)|(badge)|(typo)|(documentation)", group = "Documentation" },
153 | { message = "(^[Ff]eat)|(^[Ee]nhancement)", group = "Implemented enhancements" },
154 | { message = "(^ci)|(\\(ci\\))|(fixup\\s+ci)|(fix\\s+ci)|(license)|(example)", group = "Miscellaneous tasks" },
155 | { message = "^test", group = "Testing" },
156 | { message = "(^fix)|(panic)", group = "Fixed bugs" },
157 | { message = "(^refact)|(rework)", group = "Refactor" },
158 | { message = "(^[Pp]erf)|(performance)", group = "Performance" },
159 | { message = "(^[Cc]hore)", group = "Miscellaneous tasks" },
160 | { message = "^[Rr]evert", group = "Reverted changes" },
161 | { message = "(upgrade.*?go)|(go\\s+version)", group = "Updates" },
162 | { message = ".*", group = "Other" },
163 | ]
164 | # Exclude commits that are not matched by any commit parser.
165 | filter_commits = false
166 | # An array of link parsers for extracting external references, and turning them into URLs, using regex.
167 | link_parsers = []
168 | # Include only the tags that belong to the current branch.
169 | use_branch_tags = false
170 | # Order releases topologically instead of chronologically.
171 | topo_order = false
172 | # Order releases topologically instead of chronologically.
173 | topo_order_commits = true
174 | # Order of commits in each group/release within the changelog.
175 | # Allowed values: newest, oldest
176 | sort_commits = "newest"
177 | # Process submodules commits
178 | recurse_submodules = false
179 |
180 | #[remote.github]
181 | #owner = "go-openapi"
182 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contribution Guidelines
2 |
3 | You'll find below general guidelines, which mostly correspond to standard practices for open sourced repositories.
4 |
5 | >**TL;DR**
6 | >
7 | > If you're already an experienced go developer on github, then you should just feel at home with us
8 | > and you may well skip the rest of this document.
9 | >
10 | > You'll essentially find the usual guideline for a go library project on github.
11 |
12 | These guidelines are general to all libraries published on github by the `go-openapi` organization.
13 |
14 | You'll find more detailed (or repo-specific) instructions in the [maintainer's docs](../docs).
15 |
16 | ## How can I contribute?
17 |
18 | There are many ways in which you can contribute. Here are a few ideas:
19 |
20 | * Reporting Issues / Bugs
21 | * Suggesting Improvements
22 | * Code
23 | * bug fixes and new features that are within the main project scope
24 | * improving test coverage
25 | * addressing code quality issues
26 | * Documentation
27 | * Art work that makes the project look great
28 |
29 | ## Questions & issues
30 |
31 | ### Asking questions
32 |
33 | You may inquire about anything about this library by reporting a "Question" issue on github.
34 |
35 | ### Reporting issues
36 |
37 | Reporting a problem with our libraries _is_ a valuable contribution.
38 |
39 | You can do this on the github issues page of this repository.
40 |
41 | Please be as specific as possible when describing your issue.
42 |
43 | Whenever relevant, please provide information about your environment (go version, OS).
44 |
45 | Adding a code snippet to reproduce the issue is great, and as a big time saver for maintainers.
46 |
47 | ### Triaging issues
48 |
49 | You can help triage issues which may include:
50 |
51 | * reproducing bug reports
52 | * asking for important information, such as version numbers or reproduction instructions
53 | * answering questions and sharing your insight in issue comments
54 |
55 | ## Code contributions
56 |
57 | ### Pull requests are always welcome
58 |
59 | We are always thrilled to receive pull requests, and we do our best to
60 | process them as fast as possible.
61 |
62 | Not sure if that typo is worth a pull request? Do it! We will appreciate it.
63 |
64 | If your pull request is not accepted on the first try, don't be discouraged!
65 | If there's a problem with the implementation, hopefully you received feedback on what to improve.
66 |
67 | If you have a lot of ideas or a lot of issues to solve, try to refrain a bit and post focused
68 | pull requests.
69 | Think that they must be reviewed by a maintainer and it is easy to lost track of things on big PRs.
70 |
71 | We're trying very hard to keep the go-openapi packages lean and focused.
72 | These packages constitute a toolkit: it won't do everything for everybody out of the box,
73 | but everybody can use it to do just about everything related to OpenAPI.
74 |
75 | This means that we might decide against incorporating a new feature.
76 |
77 | However, there might be a way to implement that feature *on top of* our libraries.
78 |
79 | ### Environment
80 |
81 | You just need a `go` compiler to be installed. No special tools are needed to work with our libraries.
82 |
83 | The go compiler version required is always the old stable (latest minor go version - 1).
84 |
85 | If you're already used to work with `go` you should already have everything in place.
86 |
87 | Although not required, you'll be certainly more productive with a local installation of `golangci-lint`,
88 | the meta-linter our CI uses.
89 |
90 | If you don't have it, you may install it like so:
91 |
92 | ```sh
93 | go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
94 | ```
95 |
96 | ### Conventions
97 |
98 | #### Git flow
99 |
100 | Fork the repo and make changes to your fork in a feature branch.
101 |
102 | To submit a pull request, push your branch to your fork (e.g. `upstream` remote):
103 | github will propose to open a pull request on the original repository.
104 |
105 | Typically you'd follow some common naming conventions:
106 |
107 | - if it's a bugfix branch, name it `fix/XXX-something`where XXX is the number of the
108 | issue on github
109 | - if it's a feature branch, create an enhancement issue to announce your
110 | intentions, and name it `feature/XXX-something` where XXX is the number of the issue.
111 |
112 | > NOTE: we don't enforce naming conventions on branches: it's your fork after all.
113 |
114 | #### Tests
115 |
116 | Submit unit tests for your changes.
117 |
118 | Go has a great built-in test framework ; use it!
119 |
120 | Take a look at existing tests for inspiration, and run the full test suite on your branch
121 | before submitting a pull request.
122 |
123 | Our CI measures test coverage and the test coverage of every patch.
124 | Although not a blocking step - because there are so many special cases -
125 | this is an indicator that maintainers consider when approving a PR.
126 |
127 | Please try your best to cover about 80% of your patch.
128 |
129 | #### Code style
130 |
131 | You may read our stance on code style [there](../docs/STYLE.md).
132 |
133 | #### Documentation
134 |
135 | Don't forget to update the documentation when creating or modifying features.
136 |
137 | Most documentation for this library is directly found in code as comments for godoc.
138 |
139 | The documentation for the go-openapi packages is published on the public go docs site:
140 |
141 |
142 |
143 | Check your documentation changes for clarity, concision, and correctness.
144 |
145 | If you want to assess the rendering of your changes when published to `pkg.go.dev`, you may
146 | want to install the `pkgsite` tool proposed by `golang.org`.
147 |
148 | ```sh
149 | go install golang.org/x/pkgsite/cmd/pkgsite@latest
150 | ```
151 |
152 | Then run on the repository folder:
153 | ```sh
154 | pkgsite .
155 | ```
156 |
157 | This wil run a godoc server locally where you may see the documentation generated from your local repository.
158 |
159 | #### Commit messages
160 |
161 | Pull requests descriptions should be as clear as possible and include a
162 | reference to all the issues that they address.
163 |
164 | Pull requests must not contain commits from other users or branches.
165 |
166 | Commit messages are not required to follow the "conventional commit" rule, but it's certainly a good
167 | thing to follow this guidelinea (e.g. "fix: blah blah", "ci: did this", "feat: did that" ...).
168 |
169 | The title in your commit message is used directly to produce our release notes: try to keep them neat.
170 |
171 | The commit message body should detail your changes.
172 |
173 | If an issue should be closed by a commit, please add this reference in the commit body:
174 |
175 | ```
176 | * fixes #{issue number}
177 | ```
178 |
179 | #### Code review
180 |
181 | Code review comments may be added to your pull request.
182 |
183 | Discuss, then make the suggested modifications and push additional commits to your feature branch.
184 |
185 | Be sure to post a comment after pushing. The new commits will show up in the pull
186 | request automatically, but the reviewers will not be notified unless you comment.
187 |
188 | Before the pull request is merged,
189 | **make sure that you squash your commits into logical units of work**
190 | using `git rebase -i` and `git push -f`.
191 |
192 | After every commit the test suite should be passing.
193 |
194 | Include documentation changes in the same commit so that a revert would remove all traces of the feature or fix.
195 |
196 | #### Sign your work
197 |
198 | The sign-off is a simple line at the end of your commit message,
199 | which certifies that you wrote it or otherwise have the right to
200 | pass it on as an open-source patch.
201 |
202 | We require the simple DCO below with an email signing your commit.
203 | PGP-signed commit are greatly appreciated but not required.
204 |
205 | The rules are pretty simple:
206 |
207 | * read our [DCO](./DCO.md) (from [developercertificate.org](http://developercertificate.org/))
208 | * if you agree with these terms, then you just add a line to every git commit message
209 |
210 | Signed-off-by: Joe Smith
211 |
212 | using your real name (sorry, no pseudonyms or anonymous contributions.)
213 |
214 | You can add the sign off when creating the git commit via `git commit -s`.
215 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/inflect_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package inflect
5 |
6 | import (
7 | "testing"
8 | )
9 |
10 | func TestPluralizePlurals(t *testing.T) {
11 | assertEqual(t, "plurals", Pluralize("plurals"))
12 | assertEqual(t, "Plurals", Pluralize("Plurals"))
13 | }
14 |
15 | func TestPluralizeEmptyString(t *testing.T) {
16 | assertEqual(t, "", Pluralize(""))
17 | }
18 |
19 | func TestUncountables(t *testing.T) {
20 | for word := range Uncountables() {
21 | assertEqual(t, word, Singularize(word))
22 | assertEqual(t, word, Pluralize(word))
23 | assertEqual(t, Pluralize(word), Singularize(word))
24 | }
25 | }
26 |
27 | func TestUncountableWordIsNotGreedy(t *testing.T) {
28 | uncountableWord := "ors"
29 | countableWord := "sponsor"
30 |
31 | AddUncountable(uncountableWord)
32 |
33 | assertEqual(t, uncountableWord, Singularize(uncountableWord))
34 | assertEqual(t, uncountableWord, Pluralize(uncountableWord))
35 | assertEqual(t, Pluralize(uncountableWord), Singularize(uncountableWord))
36 | assertEqual(t, "sponsor", Singularize(countableWord))
37 | assertEqual(t, "sponsors", Pluralize(countableWord))
38 | assertEqual(t, "sponsor", Singularize(Pluralize(countableWord)))
39 | }
40 |
41 | func TestPluralizeSingular(t *testing.T) {
42 | for singular, plural := range SingularToPlural {
43 | assertEqual(t, plural, Pluralize(singular))
44 | assertEqual(t, Capitalize(plural), Capitalize(Pluralize(singular)))
45 | }
46 | }
47 |
48 | func TestSingularizePlural(t *testing.T) {
49 | assertEqual(t, "", Singularize(""))
50 |
51 | for singular, plural := range SingularToPlural {
52 | assertEqual(t, singular, Singularize(plural))
53 | assertEqual(t, Capitalize(singular), Capitalize(Singularize(plural)))
54 | }
55 | }
56 |
57 | func TestPluralizePlural(t *testing.T) {
58 | for _, plural := range SingularToPlural {
59 | assertEqual(t, plural, Pluralize(plural))
60 | assertEqual(t, Capitalize(plural), Capitalize(Pluralize(plural)))
61 | }
62 | }
63 |
64 | func TestOverwritePreviousInflectors(t *testing.T) {
65 | assertEqual(t, "series", Singularize("series"))
66 | AddSingular("series", "serie")
67 | assertEqual(t, "serie", Singularize("series"))
68 | AddUncountable("series") // reset
69 | AddPlural("wolf", "wolves")
70 | assertEqual(t, "wolves", Pluralize("wolf"))
71 | }
72 |
73 | func TestTitleize(t *testing.T) {
74 | for before, titleized := range MixtureToTitleCase {
75 | assertEqual(t, titleized, Titleize(before))
76 | }
77 | }
78 |
79 | func TestCapitalize(t *testing.T) {
80 | for lower, capitalized := range CapitalizeMixture {
81 | assertEqual(t, capitalized, Capitalize(lower))
82 | }
83 | }
84 |
85 | func TestCamelize(t *testing.T) {
86 | for camel, underscore := range CamelToUnderscore {
87 | assertEqual(t, camel, Camelize(underscore))
88 | }
89 | }
90 |
91 | func TestCamelizeOtherSeparators(t *testing.T) {
92 | for camel, other := range CamelOthers {
93 | assertEqual(t, camel, Camelize(other))
94 | }
95 | }
96 |
97 | func TestCamelizeWithLowerDowncasesTheFirstLetter(t *testing.T) {
98 | assertEqual(t, "capital", CamelizeDownFirst("Capital"))
99 | }
100 |
101 | func TestCamelizeWithUnderscores(t *testing.T) {
102 | assertEqual(t, "CamelCase", Camelize("Camel_Case"))
103 | }
104 |
105 | // func TestAcronyms(t *testing.T) {
106 | // AddAcronym("API")
107 | // AddAcronym("HTML")
108 | // AddAcronym("HTTP")
109 | // AddAcronym("RESTful")
110 | // AddAcronym("W3C")
111 | // AddAcronym("PhD")
112 | // AddAcronym("RoR")
113 | // AddAcronym("SSL")
114 | // // each in table
115 | // for _,x := range AcronymCases {
116 | // assertEqual(t, x.camel, Camelize(x.under))
117 | // assertEqual(t, x.camel, Camelize(x.camel))
118 | // assertEqual(t, x.under, Underscore(x.under))
119 | // assertEqual(t, x.under, Underscore(x.camel))
120 | // assertEqual(t, x.title, Titleize(x.under))
121 | // assertEqual(t, x.title, Titleize(x.camel))
122 | // assertEqual(t, x.human, Humanize(x.under))
123 | // }
124 | // }
125 |
126 | // func TestAcronymOverride(t *testing.T) {
127 | // AddAcronym("API")
128 | // AddAcronym("LegacyApi")
129 | // assertEqual(t, "LegacyApi", Camelize("legacyapi"))
130 | // assertEqual(t, "LegacyAPI", Camelize("legacy_api"))
131 | // assertEqual(t, "SomeLegacyApi", Camelize("some_legacyapi"))
132 | // assertEqual(t, "Nonlegacyapi", Camelize("nonlegacyapi"))
133 | // }
134 |
135 | // func TestAcronymsCamelizeLower(t *testing.T) {
136 | // AddAcronym("API")
137 | // AddAcronym("HTML")
138 | // assertEqual(t, "htmlAPI", CamelizeDownFirst("html_api"))
139 | // assertEqual(t, "htmlAPI", CamelizeDownFirst("htmlAPI"))
140 | // assertEqual(t, "htmlAPI", CamelizeDownFirst("HTMLAPI"))
141 | // }
142 |
143 | func TestUnderscoreAcronymSequence(t *testing.T) {
144 | AddAcronym("API")
145 | AddAcronym("HTML5")
146 | AddAcronym("HTML")
147 | assertEqual(t, "html5_html_api", Underscore("HTML5HTMLAPI"))
148 | }
149 |
150 | func TestUnderscore(t *testing.T) {
151 | for camel, underscore := range CamelToUnderscore {
152 | assertEqual(t, underscore, Underscore(camel))
153 | }
154 | for camel, underscore := range CamelToUnderscoreWithoutReverse {
155 | assertEqual(t, underscore, Underscore(camel))
156 | }
157 | }
158 |
159 | func TestForeignKey(t *testing.T) {
160 | for klass, foreignKey := range ClassNameToForeignKeyWithUnderscore {
161 | assertEqual(t, foreignKey, ForeignKey(klass))
162 | }
163 | for word, foreignKey := range PluralToForeignKeyWithUnderscore {
164 | assertEqual(t, foreignKey, ForeignKey(word))
165 | }
166 | for klass, foreignKey := range ClassNameToForeignKeyWithoutUnderscore {
167 | assertEqual(t, foreignKey, ForeignKeyCondensed(klass))
168 | }
169 | }
170 |
171 | func TestTableize(t *testing.T) {
172 | for klass, table := range ClassNameToTableName {
173 | assertEqual(t, table, Tableize(klass))
174 | }
175 | }
176 |
177 | func TestParameterize(t *testing.T) {
178 | for str, parameterized := range StringToParameterized {
179 | assertEqual(t, parameterized, Parameterize(str))
180 | }
181 | }
182 |
183 | func TestParameterizeAndNormalize(t *testing.T) {
184 | for str, parameterized := range StringToParameterizedAndNormalized {
185 | assertEqual(t, parameterized, Parameterize(str))
186 | }
187 | }
188 |
189 | func TestParameterizeWithCustomSeparator(t *testing.T) {
190 | for str, parameterized := range StringToParameterizeWithUnderscore {
191 | assertEqual(t, parameterized, ParameterizeJoin(str, "_"))
192 | }
193 | }
194 |
195 | func TestTypeify(t *testing.T) {
196 | for klass, table := range ClassNameToTableName {
197 | assertEqual(t, klass, Typeify(table))
198 | assertEqual(t, klass, Typeify("table_prefix."+table))
199 | }
200 | }
201 |
202 | func TestTypeifyWithLeadingSchemaName(t *testing.T) {
203 | assertEqual(t, "FooBar", Typeify("schema.foo_bar"))
204 | }
205 |
206 | func TestHumanize(t *testing.T) {
207 | for underscore, human := range UnderscoreToHuman {
208 | assertEqual(t, human, Humanize(underscore))
209 | }
210 | }
211 |
212 | func TestHumanizeByString(t *testing.T) {
213 | AddHuman("col_rpted_bugs", "reported bugs")
214 | assertEqual(t, "90 reported bugs recently", Humanize("90 col_rpted_bugs recently"))
215 | }
216 |
217 | func TestOrdinal(t *testing.T) {
218 | for number, ordinalized := range OrdinalNumbers {
219 | assertEqual(t, ordinalized, Ordinalize(number))
220 | }
221 |
222 | t.Run("should not ordinalize when not a number", func(t *testing.T) {
223 | const s = "not_a_number"
224 | assertEqual(t, s, Ordinalize(s))
225 | })
226 | }
227 |
228 | func TestAsciify(t *testing.T) {
229 | const s, expected = "àçéä", "acea"
230 | assertEqual(t, expected, Asciify(s))
231 | }
232 |
233 | func TestDasherize(t *testing.T) {
234 | for underscored, dasherized := range UnderscoresToDashes {
235 | assertEqual(t, dasherized, Dasherize(underscored))
236 | }
237 | }
238 |
239 | func TestUnderscoreAsReverseOfDasherize(t *testing.T) {
240 | for underscored := range UnderscoresToDashes {
241 | assertEqual(t, underscored, Underscore(Dasherize(underscored)))
242 | }
243 | }
244 |
245 | func TestUnderscoreToLowerCamel(t *testing.T) {
246 | for underscored, lower := range UnderscoreToLowerCamel {
247 | assertEqual(t, lower, CamelizeDownFirst(underscored))
248 | }
249 | }
250 |
251 | /*
252 | func Test_clear_all(t *testing.T) {
253 | // test a way of resetting inflexions
254 | }
255 | */
256 |
257 | func TestIrregularityBetweenSingularAndPlural(t *testing.T) {
258 | for singular, plural := range Irregularities {
259 | AddIrregular(singular, plural)
260 | assertEqual(t, singular, Singularize(plural))
261 | assertEqual(t, plural, Pluralize(singular))
262 | }
263 | }
264 |
265 | func TestPluralizeOfIrregularity(t *testing.T) {
266 | for singular, plural := range Irregularities {
267 | AddIrregular(singular, plural)
268 | assertEqual(t, plural, Pluralize(plural))
269 | }
270 | }
271 |
272 | // assert helper.
273 | func assertEqual(t *testing.T, a, b string) {
274 | t.Helper()
275 | if a != b {
276 | t.Errorf("inflect: expected %v got %v", a, b)
277 | }
278 | }
279 |
280 | // test data
281 |
282 | var SingularToPlural = map[string]string{
283 | "search": "searches",
284 | "switch": "switches",
285 | "fix": "fixes",
286 | "box": "boxes",
287 | "process": "processes",
288 | "address": "addresses",
289 | "case": "cases",
290 | "stack": "stacks",
291 | "wish": "wishes",
292 | "fish": "fish",
293 | "jeans": "jeans",
294 | "funky jeans": "funky jeans",
295 | "category": "categories",
296 | "query": "queries",
297 | "ability": "abilities",
298 | "agency": "agencies",
299 | "movie": "movies",
300 | "archive": "archives",
301 | "index": "indices",
302 | "wife": "wives",
303 | "safe": "saves",
304 | "half": "halves",
305 | "move": "moves",
306 | "salesperson": "salespeople",
307 | "person": "people",
308 | "spokesman": "spokesmen",
309 | "man": "men",
310 | "woman": "women",
311 | "basis": "bases",
312 | "diagnosis": "diagnoses",
313 | "diagnosis_a": "diagnosis_as",
314 | "datum": "data",
315 | "medium": "media",
316 | "stadium": "stadia",
317 | "analysis": "analyses",
318 | "node_child": "node_children",
319 | "child": "children",
320 | "experience": "experiences",
321 | "day": "days",
322 | "comment": "comments",
323 | "foobar": "foobars",
324 | "newsletter": "newsletters",
325 | "old_news": "old_news",
326 | "news": "news",
327 | "series": "series",
328 | "species": "species",
329 | "quiz": "quizzes",
330 | "perspective": "perspectives",
331 | "ox": "oxen",
332 | "photo": "photos",
333 | "buffalo": "buffaloes",
334 | "tomato": "tomatoes",
335 | "dwarf": "dwarves",
336 | "elf": "elves",
337 | "information": "information",
338 | "equipment": "equipment",
339 | "bus": "buses",
340 | "status": "statuses",
341 | "status_code": "status_codes",
342 | "mouse": "mice",
343 | "louse": "lice",
344 | "house": "houses",
345 | "octopus": "octopi",
346 | "virus": "viri",
347 | "alias": "aliases",
348 | "portfolio": "portfolios",
349 | "vertex": "vertices",
350 | "matrix": "matrices",
351 | "matrix_fu": "matrix_fus",
352 | "axis": "axes",
353 | "testis": "testes",
354 | "crisis": "crises",
355 | "rice": "rice",
356 | "shoe": "shoes",
357 | "horse": "horses",
358 | "prize": "prizes",
359 | "edge": "edges",
360 | "database": "databases",
361 | }
362 |
363 | var CapitalizeMixture = map[string]string{
364 | // expected: test case
365 | "product": "Product",
366 | "special_guest": "Special_guest",
367 | "applicationController": "ApplicationController",
368 | "Area51Controller": "Area51Controller",
369 | }
370 |
371 | var CamelToUnderscore = map[string]string{
372 | "Product": "product",
373 | "SpecialGuest": "special_guest",
374 | "ApplicationController": "application_controller",
375 | "Area51Controller": "area51_controller",
376 | }
377 |
378 | var CamelOthers = map[string]string{
379 | // other separators
380 | "BlankController": "blank controller",
381 | "SpaceController": "space\tcontroller",
382 | "SeparatorController": "separator:controller",
383 | }
384 |
385 | var UnderscoreToLowerCamel = map[string]string{
386 | "product": "product",
387 | "special_guest": "specialGuest",
388 | "application_controller": "applicationController",
389 | "area51_controller": "area51Controller",
390 | }
391 |
392 | var CamelToUnderscoreWithoutReverse = map[string]string{
393 | "HTMLTidy": "html_tidy",
394 | "HTMLTidyGenerator": "html_tidy_generator",
395 | "FreeBsd": "free_bsd",
396 | "HTML": "html",
397 | }
398 |
399 | var ClassNameToForeignKeyWithUnderscore = map[string]string{
400 | "Person": "person_id",
401 | "Account": "account_id",
402 | }
403 |
404 | var PluralToForeignKeyWithUnderscore = map[string]string{
405 | "people": "person_id",
406 | "accounts": "account_id",
407 | }
408 |
409 | var ClassNameToForeignKeyWithoutUnderscore = map[string]string{
410 | "Person": "personid",
411 | "Account": "accountid",
412 | }
413 |
414 | var ClassNameToTableName = map[string]string{
415 | "PrimarySpokesman": "primary_spokesmen",
416 | "NodeChild": "node_children",
417 | }
418 |
419 | var StringToParameterized = map[string]string{
420 | "Donald E. Knuth": "donald-e-knuth",
421 | "Random text with *(bad)* characters": "random-text-with-bad-characters",
422 | "Allow_Under_Scores": "allow_under_scores",
423 | "Trailing bad characters!@#": "trailing-bad-characters",
424 | "!@#Leading bad characters": "leading-bad-characters",
425 | "Squeeze separators": "squeeze-separators",
426 | "Test with + sign": "test-with-sign",
427 | "Test with malformed utf8 \251": "test-with-malformed-utf8",
428 | }
429 |
430 | var StringToParameterizeWithNoSeparator = map[string]string{
431 | "Donald E. Knuth": "donaldeknuth",
432 | "With-some-dashes": "with-some-dashes",
433 | "Random text with *(bad)* characters": "randomtextwithbadcharacters",
434 | "Trailing bad characters!@#": "trailingbadcharacters",
435 | "!@#Leading bad characters": "leadingbadcharacters",
436 | "Squeeze separators": "squeezeseparators",
437 | "Test with + sign": "testwithsign",
438 | "Test with malformed utf8 \251": "testwithmalformedutf8",
439 | }
440 |
441 | var StringToParameterizeWithUnderscore = map[string]string{
442 | "Donald E. Knuth": "donald_e_knuth",
443 | "Random text with *(bad)* characters": "random_text_with_bad_characters",
444 | "With-some-dashes": "with-some-dashes",
445 | "Retain_underscore": "retain_underscore",
446 | "Trailing bad characters!@#": "trailing_bad_characters",
447 | "!@#Leading bad characters": "leading_bad_characters",
448 | "Squeeze separators": "squeeze_separators",
449 | "Test with + sign": "test_with_sign",
450 | "Test with malformed utf8 \251": "test_with_malformed_utf8",
451 | }
452 |
453 | var StringToParameterizedAndNormalized = map[string]string{
454 | "Malmö": "malmo",
455 | "Garçons": "garcons",
456 | "Opsů": "opsu",
457 | "Ærøskøbing": "aeroskobing",
458 | "Aßlar": "asslar",
459 | "Japanese: 日本語": "japanese", //nolint:gosmopolitan // fals positive: this is expected for testing
460 | }
461 |
462 | var UnderscoreToHuman = map[string]string{
463 | "employee_salary": "Employee salary",
464 | "employee_id": "Employee",
465 | "underground": "Underground",
466 | }
467 |
468 | var MixtureToTitleCase = map[string]string{
469 | "active_record": "Active Record",
470 | "ActiveRecord": "Active Record",
471 | "action web service": "Action Web Service",
472 | "Action Web Service": "Action Web Service",
473 | "Action web service": "Action Web Service",
474 | "actionwebservice": "Actionwebservice",
475 | "Actionwebservice": "Actionwebservice",
476 | "david's code": "David's Code",
477 | "David's code": "David's Code",
478 | "david's Code": "David's Code",
479 | }
480 |
481 | var OrdinalNumbers = map[string]string{
482 | "-1": "-1st",
483 | "-2": "-2nd",
484 | "-3": "-3rd",
485 | "-4": "-4th",
486 | "-5": "-5th",
487 | "-6": "-6th",
488 | "-7": "-7th",
489 | "-8": "-8th",
490 | "-9": "-9th",
491 | "-10": "-10th",
492 | "-11": "-11th",
493 | "-12": "-12th",
494 | "-13": "-13th",
495 | "-14": "-14th",
496 | "-20": "-20th",
497 | "-21": "-21st",
498 | "-22": "-22nd",
499 | "-23": "-23rd",
500 | "-24": "-24th",
501 | "-100": "-100th",
502 | "-101": "-101st",
503 | "-102": "-102nd",
504 | "-103": "-103rd",
505 | "-104": "-104th",
506 | "-110": "-110th",
507 | "-111": "-111th",
508 | "-112": "-112th",
509 | "-113": "-113th",
510 | "-1000": "-1000th",
511 | "-1001": "-1001st",
512 | "0": "0th",
513 | "1": "1st",
514 | "2": "2nd",
515 | "3": "3rd",
516 | "4": "4th",
517 | "5": "5th",
518 | "6": "6th",
519 | "7": "7th",
520 | "8": "8th",
521 | "9": "9th",
522 | "10": "10th",
523 | "11": "11th",
524 | "12": "12th",
525 | "13": "13th",
526 | "14": "14th",
527 | "20": "20th",
528 | "21": "21st",
529 | "22": "22nd",
530 | "23": "23rd",
531 | "24": "24th",
532 | "100": "100th",
533 | "101": "101st",
534 | "102": "102nd",
535 | "103": "103rd",
536 | "104": "104th",
537 | "110": "110th",
538 | "111": "111th",
539 | "112": "112th",
540 | "113": "113th",
541 | "1000": "1000th",
542 | "1001": "1001st",
543 | }
544 |
545 | var UnderscoresToDashes = map[string]string{
546 | "street": "street",
547 | "street_address": "street-address",
548 | "person_street_address": "person-street-address",
549 | }
550 |
551 | var Irregularities = map[string]string{
552 | "person": "people",
553 | "man": "men",
554 | "child": "children",
555 | "sex": "sexes",
556 | "move": "moves",
557 | }
558 |
559 | type AcronymCase struct {
560 | camel string
561 | under string
562 | human string
563 | title string
564 | }
565 |
566 | var AcronymCases = []*AcronymCase{
567 | // camelize underscore humanize titleize
568 | {camel: "API", under: "api", human: "API", title: "API"},
569 | {"APIController", "api_controller", "API controller", "API Controller"},
570 | {"Nokogiri::HTML", "nokogiri/html", "Nokogiri/HTML", "Nokogiri/HTML"},
571 | {"HTTPAPI", "http_api", "HTTP API", "HTTP API"},
572 | {"HTTP::Get", "http/get", "HTTP/get", "HTTP/Get"},
573 | {"SSLError", "ssl_error", "SSL error", "SSL Error"},
574 | {"RESTful", "restful", "RESTful", "RESTful"},
575 | {"RESTfulController", "restful_controller", "RESTful controller", "RESTful Controller"},
576 | {"IHeartW3C", "i_heart_w3c", "I heart W3C", "I Heart W3C"},
577 | {"PhDRequired", "phd_required", "PhD required", "PhD Required"},
578 | {"IRoRU", "i_ror_u", "I RoR u", "I RoR U"},
579 | {"RESTfulHTTPAPI", "restful_http_api", "RESTful HTTP API", "RESTful HTTP API"},
580 | // misdirection
581 | {"Capistrano", "capistrano", "Capistrano", "Capistrano"},
582 | {"CapiController", "capi_controller", "Capi controller", "Capi Controller"},
583 | {"HttpsApis", "https_apis", "Https apis", "Https Apis"},
584 | {"Html5", "html5", "Html5", "Html5"},
585 | {"Restfully", "restfully", "Restfully", "Restfully"},
586 | {"RoRails", "ro_rails", "Ro rails", "Ro Rails"},
587 | }
588 |
--------------------------------------------------------------------------------
/inflect.go:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package inflect
5 |
6 | import (
7 | "fmt"
8 | "regexp"
9 | "strconv"
10 | "strings"
11 | "unicode"
12 | "unicode/utf8"
13 | )
14 |
15 | // Rule is used by rulesets.
16 | type Rule struct {
17 | suffix string
18 | replacement string
19 | exact bool
20 | }
21 |
22 | // Ruleset is the config of pluralization rules
23 | // you can extend the rules with the Add* methods.
24 | type Ruleset struct {
25 | uncountables map[string]bool
26 | plurals []*Rule
27 | singulars []*Rule
28 | humans []*Rule
29 | acronyms []*Rule
30 | }
31 |
32 | // NewRuleset creates a blank ruleset. Unless you are going to
33 | // build your own rules from scratch you probably
34 | // won't need this and can just use the defaultRuleset
35 | // via the global inflect.* methods.
36 | func NewRuleset() *Ruleset {
37 | rs := new(Ruleset)
38 | rs.uncountables = make(map[string]bool)
39 | rs.plurals = make([]*Rule, 0)
40 | rs.singulars = make([]*Rule, 0)
41 | rs.humans = make([]*Rule, 0)
42 | rs.acronyms = make([]*Rule, 0)
43 | return rs
44 | }
45 |
46 | // NewDefaultRuleset create a new ruleset and load it with the default
47 | // set of common English pluralization rules.
48 | func NewDefaultRuleset() *Ruleset {
49 | rs := NewRuleset()
50 | rs.AddPlural("s", "s")
51 | rs.AddPlural("testis", "testes")
52 | rs.AddPlural("axis", "axes")
53 | rs.AddPlural("octopus", "octopi")
54 | rs.AddPlural("virus", "viri")
55 | rs.AddPlural("octopi", "octopi")
56 | rs.AddPlural("viri", "viri")
57 | rs.AddPlural("alias", "aliases")
58 | rs.AddPlural("status", "statuses")
59 | rs.AddPlural("bus", "buses")
60 | rs.AddPlural("buffalo", "buffaloes")
61 | rs.AddPlural("tomato", "tomatoes")
62 | rs.AddPlural("tum", "ta")
63 | rs.AddPlural("ium", "ia")
64 | rs.AddPlural("ta", "ta")
65 | rs.AddPlural("ia", "ia")
66 | rs.AddPlural("sis", "ses")
67 | rs.AddPlural("lf", "lves")
68 | rs.AddPlural("rf", "rves")
69 | rs.AddPlural("afe", "aves")
70 | rs.AddPlural("bfe", "bves")
71 | rs.AddPlural("cfe", "cves")
72 | rs.AddPlural("dfe", "dves")
73 | rs.AddPlural("efe", "eves")
74 | rs.AddPlural("gfe", "gves")
75 | rs.AddPlural("hfe", "hves")
76 | rs.AddPlural("ife", "ives")
77 | rs.AddPlural("jfe", "jves")
78 | rs.AddPlural("kfe", "kves")
79 | rs.AddPlural("lfe", "lves")
80 | rs.AddPlural("mfe", "mves")
81 | rs.AddPlural("nfe", "nves")
82 | rs.AddPlural("ofe", "oves")
83 | rs.AddPlural("pfe", "pves")
84 | rs.AddPlural("qfe", "qves")
85 | rs.AddPlural("rfe", "rves")
86 | rs.AddPlural("sfe", "sves")
87 | rs.AddPlural("tfe", "tves")
88 | rs.AddPlural("ufe", "uves")
89 | rs.AddPlural("vfe", "vves")
90 | rs.AddPlural("wfe", "wves")
91 | rs.AddPlural("xfe", "xves")
92 | rs.AddPlural("yfe", "yves")
93 | rs.AddPlural("zfe", "zves")
94 | rs.AddPlural("hive", "hives")
95 | rs.AddPlural("quy", "quies")
96 | rs.AddPlural("by", "bies")
97 | rs.AddPlural("cy", "cies")
98 | rs.AddPlural("dy", "dies")
99 | rs.AddPlural("fy", "fies")
100 | rs.AddPlural("gy", "gies")
101 | rs.AddPlural("hy", "hies")
102 | rs.AddPlural("jy", "jies")
103 | rs.AddPlural("ky", "kies")
104 | rs.AddPlural("ly", "lies")
105 | rs.AddPlural("my", "mies")
106 | rs.AddPlural("ny", "nies")
107 | rs.AddPlural("py", "pies")
108 | rs.AddPlural("qy", "qies")
109 | rs.AddPlural("ry", "ries")
110 | rs.AddPlural("sy", "sies")
111 | rs.AddPlural("ty", "ties")
112 | rs.AddPlural("vy", "vies")
113 | rs.AddPlural("wy", "wies")
114 | rs.AddPlural("xy", "xies")
115 | rs.AddPlural("zy", "zies")
116 | rs.AddPlural("x", "xes")
117 | rs.AddPlural("ch", "ches")
118 | rs.AddPlural("ss", "sses")
119 | rs.AddPlural("sh", "shes")
120 | rs.AddPlural("matrix", "matrices")
121 | rs.AddPlural("vertix", "vertices")
122 | rs.AddPlural("indix", "indices")
123 | rs.AddPlural("matrex", "matrices")
124 | rs.AddPlural("vertex", "vertices")
125 | rs.AddPlural("index", "indices")
126 | rs.AddPlural("mouse", "mice")
127 | rs.AddPlural("louse", "lice")
128 | rs.AddPlural("mice", "mice")
129 | rs.AddPlural("lice", "lice")
130 | rs.AddPluralExact("ox", "oxen", true)
131 | rs.AddPluralExact("oxen", "oxen", true)
132 | rs.AddPluralExact("quiz", "quizzes", true)
133 | rs.AddSingular("s", "")
134 | rs.AddSingular("news", "news")
135 | rs.AddSingular("ta", "tum")
136 | rs.AddSingular("ia", "ium")
137 | rs.AddSingular("analyses", "analysis")
138 | rs.AddSingular("bases", "basis")
139 | rs.AddSingular("diagnoses", "diagnosis")
140 | rs.AddSingular("parentheses", "parenthesis")
141 | rs.AddSingular("prognoses", "prognosis")
142 | rs.AddSingular("synopses", "synopsis")
143 | rs.AddSingular("theses", "thesis")
144 | rs.AddSingular("analyses", "analysis")
145 | rs.AddSingular("aves", "afe")
146 | rs.AddSingular("bves", "bfe")
147 | rs.AddSingular("cves", "cfe")
148 | rs.AddSingular("dves", "dfe")
149 | rs.AddSingular("eves", "efe")
150 | rs.AddSingular("gves", "gfe")
151 | rs.AddSingular("hves", "hfe")
152 | rs.AddSingular("ives", "ife")
153 | rs.AddSingular("jves", "jfe")
154 | rs.AddSingular("kves", "kfe")
155 | rs.AddSingular("lves", "lfe")
156 | rs.AddSingular("mves", "mfe")
157 | rs.AddSingular("nves", "nfe")
158 | rs.AddSingular("oves", "ofe")
159 | rs.AddSingular("pves", "pfe")
160 | rs.AddSingular("qves", "qfe")
161 | rs.AddSingular("rves", "rfe")
162 | rs.AddSingular("sves", "sfe")
163 | rs.AddSingular("tves", "tfe")
164 | rs.AddSingular("uves", "ufe")
165 | rs.AddSingular("vves", "vfe")
166 | rs.AddSingular("wves", "wfe")
167 | rs.AddSingular("xves", "xfe")
168 | rs.AddSingular("yves", "yfe")
169 | rs.AddSingular("zves", "zfe")
170 | rs.AddSingular("hives", "hive")
171 | rs.AddSingular("tives", "tive")
172 | rs.AddSingular("lves", "lf")
173 | rs.AddSingular("rves", "rf")
174 | rs.AddSingular("quies", "quy")
175 | rs.AddSingular("bies", "by")
176 | rs.AddSingular("cies", "cy")
177 | rs.AddSingular("dies", "dy")
178 | rs.AddSingular("fies", "fy")
179 | rs.AddSingular("gies", "gy")
180 | rs.AddSingular("hies", "hy")
181 | rs.AddSingular("jies", "jy")
182 | rs.AddSingular("kies", "ky")
183 | rs.AddSingular("lies", "ly")
184 | rs.AddSingular("mies", "my")
185 | rs.AddSingular("nies", "ny")
186 | rs.AddSingular("pies", "py")
187 | rs.AddSingular("qies", "qy")
188 | rs.AddSingular("ries", "ry")
189 | rs.AddSingular("sies", "sy")
190 | rs.AddSingular("ties", "ty")
191 | rs.AddSingular("vies", "vy")
192 | rs.AddSingular("wies", "wy")
193 | rs.AddSingular("xies", "xy")
194 | rs.AddSingular("zies", "zy")
195 | rs.AddSingular("series", "series")
196 | rs.AddSingular("movies", "movie")
197 | rs.AddSingular("xes", "x")
198 | rs.AddSingular("ches", "ch")
199 | rs.AddSingular("sses", "ss")
200 | rs.AddSingular("shes", "sh")
201 | rs.AddSingular("mice", "mouse")
202 | rs.AddSingular("lice", "louse")
203 | rs.AddSingular("buses", "bus")
204 | rs.AddSingular("oes", "o")
205 | rs.AddSingular("shoes", "shoe")
206 | rs.AddSingular("crises", "crisis")
207 | rs.AddSingular("axes", "axis")
208 | rs.AddSingular("testes", "testis")
209 | rs.AddSingular("octopi", "octopus")
210 | rs.AddSingular("viri", "virus")
211 | rs.AddSingular("statuses", "status")
212 | rs.AddSingular("aliases", "alias")
213 | rs.AddSingularExact("oxen", "ox", true)
214 | rs.AddSingular("vertices", "vertex")
215 | rs.AddSingular("indices", "index")
216 | rs.AddSingular("matrices", "matrix")
217 | rs.AddSingularExact("quizzes", "quiz", true)
218 | rs.AddSingular("databases", "database")
219 | rs.AddIrregular("person", "people")
220 | rs.AddIrregular("man", "men")
221 | rs.AddIrregular("child", "children")
222 | rs.AddIrregular("sex", "sexes")
223 | rs.AddIrregular("move", "moves")
224 | rs.AddIrregular("zombie", "zombies")
225 | rs.AddUncountable("equipment")
226 | rs.AddUncountable("information")
227 | rs.AddUncountable("rice")
228 | rs.AddUncountable("money")
229 | rs.AddUncountable("species")
230 | rs.AddUncountable("series")
231 | rs.AddUncountable("fish")
232 | rs.AddUncountable("sheep")
233 | rs.AddUncountable("jeans")
234 | rs.AddUncountable("police")
235 | return rs
236 | }
237 |
238 | func (rs *Ruleset) Uncountables() map[string]bool {
239 | return rs.uncountables
240 | }
241 |
242 | // AddPlural adds a pluralization rule.
243 | func (rs *Ruleset) AddPlural(suffix, replacement string) {
244 | rs.AddPluralExact(suffix, replacement, false)
245 | }
246 |
247 | // AddPluralExact adds a pluralization rule with full string match.
248 | func (rs *Ruleset) AddPluralExact(suffix, replacement string, exact bool) {
249 | // remove uncountable
250 | delete(rs.uncountables, suffix)
251 | // create rule
252 | r := new(Rule)
253 | r.suffix = suffix
254 | r.replacement = replacement
255 | r.exact = exact
256 | // prepend
257 | rs.plurals = append([]*Rule{r}, rs.plurals...)
258 | }
259 |
260 | // AddSingular add a singular rule.
261 | func (rs *Ruleset) AddSingular(suffix, replacement string) {
262 | rs.AddSingularExact(suffix, replacement, false)
263 | }
264 |
265 | // AddSingularExact same as AddSingular but you can set `exact` to force
266 | // a full string match.
267 | func (rs *Ruleset) AddSingularExact(suffix, replacement string, exact bool) {
268 | // remove from uncountable
269 | delete(rs.uncountables, suffix)
270 | // create rule
271 | r := new(Rule)
272 | r.suffix = suffix
273 | r.replacement = replacement
274 | r.exact = exact
275 | rs.singulars = append([]*Rule{r}, rs.singulars...)
276 | }
277 |
278 | // AddHuman rules are applied by humanize to show more friendly
279 | // versions of words.
280 | func (rs *Ruleset) AddHuman(suffix, replacement string) {
281 | r := new(Rule)
282 | r.suffix = suffix
283 | r.replacement = replacement
284 | rs.humans = append([]*Rule{r}, rs.humans...)
285 | }
286 |
287 | // AddIrregular adds any inconsistent pluralizing/singularizing rules
288 | // to the set here.
289 | func (rs *Ruleset) AddIrregular(singular, plural string) {
290 | delete(rs.uncountables, singular)
291 | delete(rs.uncountables, plural)
292 | rs.AddPlural(singular, plural)
293 | rs.AddPlural(plural, plural)
294 | rs.AddSingular(plural, singular)
295 | }
296 |
297 | // AddAcronym is used if you use acronym you may need to add them to the ruleset
298 | // to prevent Underscored words of things like "HTML" coming out
299 | // as "h_t_m_l".
300 | func (rs *Ruleset) AddAcronym(word string) {
301 | r := new(Rule)
302 | r.suffix = word
303 | r.replacement = rs.Titleize(strings.ToLower(word))
304 | rs.acronyms = append(rs.acronyms, r)
305 | }
306 |
307 | // AddUncountable adds a word to this ruleset that has the same singular and plural form
308 | // for example: "rice".
309 | func (rs *Ruleset) AddUncountable(word string) {
310 | rs.uncountables[strings.ToLower(word)] = true
311 | }
312 |
313 | // Pluralize returns the plural form of a singular word.
314 | func (rs *Ruleset) Pluralize(word string) string {
315 | if len(word) == 0 {
316 | return word
317 | }
318 | if rs.isUncountable(word) {
319 | return word
320 | }
321 | for _, rule := range rs.plurals {
322 | if rule.exact {
323 | if word == rule.suffix {
324 | return rule.replacement
325 | }
326 | } else {
327 | if strings.HasSuffix(word, rule.suffix) {
328 | return replaceLast(word, rule.suffix, rule.replacement)
329 | }
330 | }
331 | }
332 | return word + "s"
333 | }
334 |
335 | // Singularize returns the singular form of a plural word.
336 | func (rs *Ruleset) Singularize(word string) string {
337 | if len(word) == 0 {
338 | return word
339 | }
340 | if rs.isUncountable(word) {
341 | return word
342 | }
343 | for _, rule := range rs.singulars {
344 | if rule.exact {
345 | if word == rule.suffix {
346 | return rule.replacement
347 | }
348 | } else {
349 | if strings.HasSuffix(word, rule.suffix) {
350 | return replaceLast(word, rule.suffix, rule.replacement)
351 | }
352 | }
353 | }
354 | return word
355 | }
356 |
357 | // Capitalize uppercase first character.
358 | func (rs *Ruleset) Capitalize(word string) string {
359 | return strings.ToUpper(word[:1]) + word[1:]
360 | }
361 |
362 | // Camelize makes a word camel-case: "dino_party" -> "DinoParty".
363 | func (rs *Ruleset) Camelize(word string) string {
364 | words := splitAtCaseChangeWithTitlecase(word)
365 | return strings.Join(words, "")
366 | }
367 |
368 | // CamelizeDownFirst is the same as Camelcase but with first letter downcased.
369 | func (rs *Ruleset) CamelizeDownFirst(word string) string {
370 | word = Camelize(word)
371 | return strings.ToLower(word[:1]) + word[1:]
372 | }
373 |
374 | // Titleize capitalizes every word in sentence: "hello there" -> "Hello There".
375 | func (rs *Ruleset) Titleize(word string) string {
376 | words := splitAtCaseChangeWithTitlecase(word)
377 | return strings.Join(words, " ")
378 | }
379 |
380 | // Underscore makes a lowercase underscore version: "BigBen" -> "big_ben".
381 | func (rs *Ruleset) Underscore(word string) string {
382 | return rs.seperatedWords(word, "_")
383 | }
384 |
385 | // Humanize makes the first letter of a sentence capitalized.
386 | //
387 | // Uses custom friendly replacements via AddHuman().
388 | func (rs *Ruleset) Humanize(word string) string {
389 | word = replaceLast(word, "_id", "") // strip foreign key kinds
390 | // replace and strings in humans list
391 | for _, rule := range rs.humans {
392 | word = strings.ReplaceAll(word, rule.suffix, rule.replacement)
393 | }
394 | sentance := rs.seperatedWords(word, " ")
395 | return strings.ToUpper(sentance[:1]) + sentance[1:]
396 | }
397 |
398 | // ForeignKey makes an underscored foreign key name: "Person" -> "person_id".
399 | func (rs *Ruleset) ForeignKey(word string) string {
400 | return rs.Underscore(rs.Singularize(word)) + "_id"
401 | }
402 |
403 | // ForeignKeyCondensed makes a foreign key (without an underscore) "Person" -> "personid".
404 | func (rs *Ruleset) ForeignKeyCondensed(word string) string {
405 | return rs.Underscore(word) + "id"
406 | }
407 |
408 | // Tableize makes a rails style pluralized table name: "SuperPerson" -> "super_people".
409 | func (rs *Ruleset) Tableize(word string) string {
410 | return rs.Pluralize(rs.Underscore(rs.Typeify(word)))
411 | }
412 |
413 | var notURLSafe = regexp.MustCompile(`[^\w\d\-_ ]`)
414 |
415 | // Parameterize makes param safe dasherized names like "my-param".
416 | func (rs *Ruleset) Parameterize(word string) string {
417 | return ParameterizeJoin(word, "-")
418 | }
419 |
420 | // ParameterizeJoin makes param safe dasherized names with custom separator.
421 | func (rs *Ruleset) ParameterizeJoin(word, sep string) string {
422 | word = strings.ToLower(word)
423 | word = rs.Asciify(word)
424 | word = notURLSafe.ReplaceAllString(word, "")
425 | word = strings.ReplaceAll(word, " ", sep)
426 | if len(sep) > 0 {
427 | squash, err := regexp.Compile(sep + "+")
428 | if err == nil {
429 | word = squash.ReplaceAllString(word, sep)
430 | }
431 | }
432 | word = strings.Trim(word, sep+" ")
433 | return word
434 | }
435 |
436 | // Asciify transforms latin characters like é -> e.
437 | func (rs *Ruleset) Asciify(word string) string {
438 | for repl, regex := range lookalikes {
439 | word = regex.ReplaceAllString(word, repl)
440 | }
441 | return word
442 | }
443 |
444 | var tablePrefix = regexp.MustCompile(`^[^.]*\.`)
445 |
446 | // Typeify makes "something_like_this" -> "SomethingLikeThis".
447 | func (rs *Ruleset) Typeify(word string) string {
448 | word = tablePrefix.ReplaceAllString(word, "")
449 | return rs.Camelize(rs.Singularize(word))
450 | }
451 |
452 | // Dasherize uses dashes: "SomeText" -> "some-text".
453 | func (rs *Ruleset) Dasherize(word string) string {
454 | return rs.seperatedWords(word, "-")
455 | }
456 |
457 | // Ordinalize returns an ordinal: "1031" -> "1031st"
458 | //
459 | //nolint:mnd
460 | func (rs *Ruleset) Ordinalize(str string) string {
461 | number, err := strconv.Atoi(str)
462 | if err != nil {
463 | return str
464 | }
465 | switch abs(number) % 100 {
466 | case 11, 12, 13:
467 | return fmt.Sprintf("%dth", number)
468 | default:
469 | switch abs(number) % 10 {
470 | case 1:
471 | return fmt.Sprintf("%dst", number)
472 | case 2:
473 | return fmt.Sprintf("%dnd", number)
474 | case 3:
475 | return fmt.Sprintf("%drd", number)
476 | }
477 | }
478 | return fmt.Sprintf("%dth", number)
479 | }
480 |
481 | func (rs *Ruleset) isUncountable(word string) bool {
482 | // handle multiple words by using the last one
483 | words := strings.Split(word, " ")
484 | if _, exists := rs.uncountables[strings.ToLower(words[len(words)-1])]; exists {
485 | return true
486 | }
487 | return false
488 | }
489 |
490 | func (rs *Ruleset) safeCaseAcronyms(word string) string {
491 | // convert an acroymn like HTML into Html
492 | for _, rule := range rs.acronyms {
493 | word = strings.ReplaceAll(word, rule.suffix, rule.replacement)
494 | }
495 | return word
496 | }
497 |
498 | func (rs *Ruleset) seperatedWords(word, sep string) string {
499 | word = rs.safeCaseAcronyms(word)
500 | words := splitAtCaseChange(word)
501 | return strings.Join(words, sep)
502 | }
503 |
504 | var lookalikes = map[string]*regexp.Regexp{
505 | "A": regexp.MustCompile(`À|Á|Â|Ã|Ä|Å`),
506 | "AE": regexp.MustCompile(`Æ`),
507 | "C": regexp.MustCompile(`Ç`),
508 | "E": regexp.MustCompile(`È|É|Ê|Ë`),
509 | "G": regexp.MustCompile(`Ğ`),
510 | "I": regexp.MustCompile(`Ì|Í|Î|Ï|İ`),
511 | "N": regexp.MustCompile(`Ñ`),
512 | "O": regexp.MustCompile(`Ò|Ó|Ô|Õ|Ö|Ø`),
513 | "S": regexp.MustCompile(`Ş`),
514 | "U": regexp.MustCompile(`Ù|Ú|Û|Ü`),
515 | "Y": regexp.MustCompile(`Ý`),
516 | "ss": regexp.MustCompile(`ß`),
517 | "a": regexp.MustCompile(`à|á|â|ã|ä|å`),
518 | "ae": regexp.MustCompile(`æ`),
519 | "c": regexp.MustCompile(`ç`),
520 | "e": regexp.MustCompile(`è|é|ê|ë`),
521 | "g": regexp.MustCompile(`ğ`),
522 | "i": regexp.MustCompile(`ì|í|î|ï|ı`),
523 | "n": regexp.MustCompile(`ñ`),
524 | "o": regexp.MustCompile(`ò|ó|ô|õ|ö|ø`),
525 | "s": regexp.MustCompile(`ş`),
526 | "u": regexp.MustCompile(`ù|ú|û|ü|ũ|ū|ŭ|ů|ű|ų`),
527 | "y": regexp.MustCompile(`ý|ÿ`),
528 | }
529 |
530 | /////////////////////////////////////////
531 | // the default global ruleset
532 | //////////////////////////////////////////
533 |
534 | var defaultRuleset *Ruleset
535 |
536 | func init() {
537 | defaultRuleset = NewDefaultRuleset()
538 | }
539 |
540 | func Uncountables() map[string]bool {
541 | return defaultRuleset.Uncountables()
542 | }
543 |
544 | func AddPlural(suffix, replacement string) {
545 | defaultRuleset.AddPlural(suffix, replacement)
546 | }
547 |
548 | func AddSingular(suffix, replacement string) {
549 | defaultRuleset.AddSingular(suffix, replacement)
550 | }
551 |
552 | func AddHuman(suffix, replacement string) {
553 | defaultRuleset.AddHuman(suffix, replacement)
554 | }
555 |
556 | func AddIrregular(singular, plural string) {
557 | defaultRuleset.AddIrregular(singular, plural)
558 | }
559 |
560 | func AddAcronym(word string) {
561 | defaultRuleset.AddAcronym(word)
562 | }
563 |
564 | func AddUncountable(word string) {
565 | defaultRuleset.AddUncountable(word)
566 | }
567 |
568 | func Pluralize(word string) string {
569 | return defaultRuleset.Pluralize(word)
570 | }
571 |
572 | func Singularize(word string) string {
573 | return defaultRuleset.Singularize(word)
574 | }
575 |
576 | func Capitalize(word string) string {
577 | return defaultRuleset.Capitalize(word)
578 | }
579 |
580 | func Camelize(word string) string {
581 | return defaultRuleset.Camelize(word)
582 | }
583 |
584 | func CamelizeDownFirst(word string) string {
585 | return defaultRuleset.CamelizeDownFirst(word)
586 | }
587 |
588 | func Titleize(word string) string {
589 | return defaultRuleset.Titleize(word)
590 | }
591 |
592 | func Underscore(word string) string {
593 | return defaultRuleset.Underscore(word)
594 | }
595 |
596 | func Humanize(word string) string {
597 | return defaultRuleset.Humanize(word)
598 | }
599 |
600 | func ForeignKey(word string) string {
601 | return defaultRuleset.ForeignKey(word)
602 | }
603 |
604 | func ForeignKeyCondensed(word string) string {
605 | return defaultRuleset.ForeignKeyCondensed(word)
606 | }
607 |
608 | func Tableize(word string) string {
609 | return defaultRuleset.Tableize(word)
610 | }
611 |
612 | func Parameterize(word string) string {
613 | return defaultRuleset.Parameterize(word)
614 | }
615 |
616 | func ParameterizeJoin(word, sep string) string {
617 | return defaultRuleset.ParameterizeJoin(word, sep)
618 | }
619 |
620 | func Typeify(word string) string {
621 | return defaultRuleset.Typeify(word)
622 | }
623 |
624 | func Dasherize(word string) string {
625 | return defaultRuleset.Dasherize(word)
626 | }
627 |
628 | func Ordinalize(word string) string {
629 | return defaultRuleset.Ordinalize(word)
630 | }
631 |
632 | func Asciify(word string) string {
633 | return defaultRuleset.Asciify(word)
634 | }
635 |
636 | // helper funcs
637 |
638 | func reverse(s string) string {
639 | o := make([]rune, utf8.RuneCountInString(s))
640 | i := len(o)
641 | for _, c := range s {
642 | i--
643 | o[i] = c
644 | }
645 | return string(o)
646 | }
647 |
648 | func isSpacerChar(c rune) bool {
649 | switch {
650 | case c == '_':
651 | return true
652 | case c == ':':
653 | return true
654 | case c == '-':
655 | return true
656 | case unicode.IsSpace(c):
657 | return true
658 | }
659 | return false
660 | }
661 |
662 | func splitAtCaseChange(s string) []string {
663 | words := make([]string, 0)
664 | word := make([]rune, 0)
665 | for _, c := range s {
666 | spacer := isSpacerChar(c)
667 | if len(word) > 0 {
668 | if unicode.IsUpper(c) || spacer {
669 | words = append(words, string(word))
670 | word = make([]rune, 0)
671 | }
672 | }
673 | if !spacer {
674 | word = append(word, unicode.ToLower(c))
675 | }
676 | }
677 | words = append(words, string(word))
678 | return words
679 | }
680 |
681 | func splitAtCaseChangeWithTitlecase(s string) []string {
682 | words := make([]string, 0)
683 | word := make([]rune, 0)
684 | for _, c := range s {
685 | spacer := isSpacerChar(c)
686 | if len(word) > 0 {
687 | if unicode.IsUpper(c) || spacer {
688 | words = append(words, string(word))
689 | word = make([]rune, 0)
690 | }
691 | }
692 | if !spacer {
693 | if len(word) > 0 {
694 | word = append(word, unicode.ToLower(c))
695 | } else {
696 | word = append(word, unicode.ToUpper(c))
697 | }
698 | }
699 | }
700 | words = append(words, string(word))
701 | return words
702 | }
703 |
704 | func replaceLast(s, match, repl string) string {
705 | // reverse strings
706 | srev := reverse(s)
707 | mrev := reverse(match)
708 | rrev := reverse(repl)
709 | // match first and reverse back
710 | return reverse(strings.Replace(srev, mrev, rrev, 1))
711 | }
712 |
713 | func abs(x int) int {
714 | if x < 0 {
715 | return -x
716 | }
717 | return x
718 | }
719 |
--------------------------------------------------------------------------------