├── 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 | --------------------------------------------------------------------------------