├── .codecov.yml ├── .devcontainer └── devcontainer.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .gomarkdoc.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DOC.md ├── LICENSE ├── README.md ├── SECURITY.md ├── alphanumeric.go ├── alphanumeric_test.go ├── ascii.go ├── ascii_test.go ├── check_error.go ├── check_error_test.go ├── check_func.go ├── checker.go ├── checker_test.go ├── cidr.go ├── cidr_test.go ├── credit_card.go ├── credit_card_test.go ├── digits.go ├── digits_test.go ├── email.go ├── email_test.go ├── fqdn.go ├── fqdn_test.go ├── go.mod ├── gte.go ├── gte_test.go ├── header.txt ├── helper_test.go ├── hex.go ├── hex_test.go ├── html_escape.go ├── html_escape_test.go ├── html_unescape.go ├── html_unescape_test.go ├── ip.go ├── ip_test.go ├── ipv4.go ├── ipv4_test.go ├── ipv6.go ├── ipv6_test.go ├── isbn.go ├── isbn_test.go ├── locales ├── DOC.md ├── en_us.go └── locales.go ├── lower.go ├── lower_test.go ├── lte.go ├── lte_test.go ├── luhn.go ├── luhn_test.go ├── mac.go ├── mac_test.go ├── maker.go ├── maker_test.go ├── max_len.go ├── max_len_test.go ├── min_len.go ├── min_len_test.go ├── regexp.go ├── regexp_test.go ├── required.go ├── required_test.go ├── revive.toml ├── taskfile.yml ├── time.go ├── time_test.go ├── title.go ├── title_test.go ├── trim_left.go ├── trim_left_test.go ├── trim_right.go ├── trim_right_test.go ├── trim_space.go ├── trim_space_test.go ├── upper.go ├── upper_test.go ├── url.go ├── url_escape.go ├── url_escape_test.go ├── url_test.go ├── url_unescape.go └── url_unescape_test.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 100% 6 | threshold: 0% 7 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go 3 | { 4 | "name": "Go", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/go", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | // "features": {}, 10 | 11 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 12 | // "forwardPorts": [], 13 | 14 | // Use 'postCreateCommand' to run commands after the container is created. 15 | // "postCreateCommand": "go version", 16 | 17 | // Configure tool-specific properties. 18 | "customizations": { 19 | "vscode": { 20 | "settings": { 21 | "go.lintTool": "revive", 22 | "go.lintOnSave": "package" 23 | }, 24 | "extensions": [ 25 | "github.vscode-pull-request-github" 26 | ] 27 | } 28 | } 29 | 30 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 31 | // "remoteUser": "root" 32 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Please provide a running example code snippet that clearly demonstrates the issue. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Describe Request 2 | 3 | Please describe the request in detail. Please refer to any sources that you have used. 4 | 5 | If this request fixes an existing issue, please refer to that issue. 6 | 7 | Fixes #(issue) 8 | 9 | # Change Type 10 | 11 | Is this a bug fix, a new checker, a new normalizer, or maintanance. 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version-file: ./go.mod 19 | 20 | - name: Setup cache 21 | uses: actions/cache@v4 22 | with: 23 | path: | 24 | ~/.cache/go-build 25 | ~/go/pkg/mod 26 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 27 | restore-keys: | 28 | ${{ runner.os }}-go- 29 | 30 | - name: Run task 31 | run: go run github.com/go-task/task/v3/cmd/task@v3.38.0 32 | 33 | - name: Upload coverage reports to Codecov 34 | uses: codecov/codecov-action@v4.5.0 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /.gomarkdoc.yml: -------------------------------------------------------------------------------- 1 | output: "{{.Dir}}/DOC.md" 2 | repository: 3 | url: https://github.com/cinar/checker 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our errors, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | email address. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute to Checker 2 | 3 | This guide will help you contribute to the Checkers library. 4 | 5 | ## Who can contribute? 6 | 7 | Anyone can contribute to Checkers library. No prior experience is required, but it is helpful to have some knowledge of coding. 8 | 9 | ## How to contribute? 10 | 11 | Before you start contributing, please make sure to read our [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md) guide first. 12 | 13 | ### Did you find a bug? 14 | 15 | Ensure the bug was not already reported by searching on GitHub under [Project Issues](https://github.com/cinar/checker/issues). If you're unable to find an open issue addressing the problem, open a new one. If possible, use the relevant bug report templates to create the issue. Be sure to include a title and clear description, as much relevant information as possible, and a code sample or an executable test case demonstrating the expected behavior that is not occurring. 16 | 17 | ### Did you write a fix? 18 | 19 | Open a new GitHub pull request with the patch. Pull requests will be reviewed by the project maintainers. Changes will be merged if they meet the project's coding standards and they are approved by the maintainers. 20 | 21 | ## Coding Standards 22 | 23 | The project follows the Go coding standards. 24 | 25 | ### Code Quality 26 | 27 | User input validation is a critical task that must be performed correctly in order to ensure that user data is handled correctly. This is why it is important to have extensive unit testing in place for any user input validation library. 28 | 29 | The Checker library has a code coverage threshold of 100%, which means that all of the code in the library has been tested. This ensures that the library is extremely reliable and that it will not fail to validate user input correctly. 30 | 31 | The test cases for the library can be found in the _test.go files. These files contain a comprehensive set of tests that cover all of the possible scenarios for user input validation. 32 | 33 | If you are planning to make a pull request to this project, please make sure to add enough test cases to ensure that the code coverage remains at 100%. This will help to ensure that the library remains reliable and that user data is handled correctly. 34 | 35 | Thank you for contributing! We appreciate your help in making Checkers library better. 36 | -------------------------------------------------------------------------------- /DOC.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # v2 6 | 7 | ```go 8 | import "github.com/cinar/checker/v2" 9 | ``` 10 | 11 | Package v2 Checker is a Go library for validating user input through checker rules provided in struct tags. 12 | 13 | ## Index 14 | 15 | - [Constants](<#constants>) 16 | - [Variables](<#variables>) 17 | - [func Check\[T any\]\(value T, checks ...CheckFunc\[T\]\) \(T, error\)](<#Check>) 18 | - [func CheckStruct\(st any\) \(map\[string\]error, bool\)](<#CheckStruct>) 19 | - [func CheckWithConfig\[T any\]\(value T, config string\) \(T, error\)](<#CheckWithConfig>) 20 | - [func HTMLEscape\(value string\) \(string, error\)](<#HTMLEscape>) 21 | - [func HTMLUnescape\(value string\) \(string, error\)](<#HTMLUnescape>) 22 | - [func IsASCII\(value string\) \(string, error\)](<#IsASCII>) 23 | - [func IsAlphanumeric\(value string\) \(string, error\)](<#IsAlphanumeric>) 24 | - [func IsAmexCreditCard\(number string\) \(string, error\)](<#IsAmexCreditCard>) 25 | - [func IsAnyCreditCard\(number string\) \(string, error\)](<#IsAnyCreditCard>) 26 | - [func IsCIDR\(value string\) \(string, error\)](<#IsCIDR>) 27 | - [func IsDigits\(value string\) \(string, error\)](<#IsDigits>) 28 | - [func IsDinersCreditCard\(number string\) \(string, error\)](<#IsDinersCreditCard>) 29 | - [func IsDiscoverCreditCard\(number string\) \(string, error\)](<#IsDiscoverCreditCard>) 30 | - [func IsEmail\(value string\) \(string, error\)](<#IsEmail>) 31 | - [func IsFQDN\(value string\) \(string, error\)](<#IsFQDN>) 32 | - [func IsGte\[T cmp.Ordered\]\(value, n T\) \(T, error\)](<#IsGte>) 33 | - [func IsHex\(value string\) \(string, error\)](<#IsHex>) 34 | - [func IsIP\(value string\) \(string, error\)](<#IsIP>) 35 | - [func IsIPv4\(value string\) \(string, error\)](<#IsIPv4>) 36 | - [func IsIPv6\(value string\) \(string, error\)](<#IsIPv6>) 37 | - [func IsISBN\(value string\) \(string, error\)](<#IsISBN>) 38 | - [func IsJcbCreditCard\(number string\) \(string, error\)](<#IsJcbCreditCard>) 39 | - [func IsLUHN\(value string\) \(string, error\)](<#IsLUHN>) 40 | - [func IsLte\[T cmp.Ordered\]\(value, n T\) \(T, error\)](<#IsLte>) 41 | - [func IsMAC\(value string\) \(string, error\)](<#IsMAC>) 42 | - [func IsMasterCardCreditCard\(number string\) \(string, error\)](<#IsMasterCardCreditCard>) 43 | - [func IsRegexp\(expression, value string\) \(string, error\)](<#IsRegexp>) 44 | - [func IsTime\(params, value string\) \(string, error\)](<#IsTime>) 45 | - [func IsURL\(value string\) \(string, error\)](<#IsURL>) 46 | - [func IsUnionPayCreditCard\(number string\) \(string, error\)](<#IsUnionPayCreditCard>) 47 | - [func IsVisaCreditCard\(number string\) \(string, error\)](<#IsVisaCreditCard>) 48 | - [func Lower\(value string\) \(string, error\)](<#Lower>) 49 | - [func ReflectCheckWithConfig\(value reflect.Value, config string\) \(reflect.Value, error\)](<#ReflectCheckWithConfig>) 50 | - [func RegisterLocale\(locale string, messages map\[string\]string\)](<#RegisterLocale>) 51 | - [func RegisterMaker\(name string, maker MakeCheckFunc\)](<#RegisterMaker>) 52 | - [func Required\[T any\]\(value T\) \(T, error\)](<#Required>) 53 | - [func Title\(value string\) \(string, error\)](<#Title>) 54 | - [func TrimLeft\(value string\) \(string, error\)](<#TrimLeft>) 55 | - [func TrimRight\(value string\) \(string, error\)](<#TrimRight>) 56 | - [func TrimSpace\(value string\) \(string, error\)](<#TrimSpace>) 57 | - [func URLEscape\(value string\) \(string, error\)](<#URLEscape>) 58 | - [func URLUnescape\(value string\) \(string, error\)](<#URLUnescape>) 59 | - [func Upper\(value string\) \(string, error\)](<#Upper>) 60 | - [type CheckError](<#CheckError>) 61 | - [func NewCheckError\(code string\) \*CheckError](<#NewCheckError>) 62 | - [func NewCheckErrorWithData\(code string, data map\[string\]interface\{\}\) \*CheckError](<#NewCheckErrorWithData>) 63 | - [func \(c \*CheckError\) Error\(\) string](<#CheckError.Error>) 64 | - [func \(c \*CheckError\) ErrorWithLocale\(locale string\) string](<#CheckError.ErrorWithLocale>) 65 | - [func \(c \*CheckError\) Is\(target error\) bool](<#CheckError.Is>) 66 | - [type CheckFunc](<#CheckFunc>) 67 | - [func MakeRegexpChecker\(expression string, invalidError error\) CheckFunc\[reflect.Value\]](<#MakeRegexpChecker>) 68 | - [func MaxLen\[T any\]\(n int\) CheckFunc\[T\]](<#MaxLen>) 69 | - [func MinLen\[T any\]\(n int\) CheckFunc\[T\]](<#MinLen>) 70 | - [type MakeCheckFunc](<#MakeCheckFunc>) 71 | 72 | 73 | ## Constants 74 | 75 | 76 | 77 | ```go 78 | const ( 79 | // DefaultLocale is the default locale. 80 | DefaultLocale = locales.EnUS 81 | ) 82 | ``` 83 | 84 | ## Variables 85 | 86 | 87 | 88 | ```go 89 | var ( 90 | // ErrGte indicates that the value is not greater than or equal to the given value. 91 | ErrGte = NewCheckError("NOT_GTE") 92 | ) 93 | ``` 94 | 95 | 96 | 97 | ```go 98 | var ( 99 | // ErrLte indicates that the value is not less than or equal to the given value. 100 | ErrLte = NewCheckError("NOT_LTE") 101 | ) 102 | ``` 103 | 104 | 105 | 106 | ```go 107 | var ( 108 | // ErrMaxLen indicates that the value's length is greater than the specified maximum. 109 | ErrMaxLen = NewCheckError("NOT_MAX_LEN") 110 | ) 111 | ``` 112 | 113 | 114 | 115 | ```go 116 | var ( 117 | // ErrMinLen indicates that the value's length is less than the specified minimum. 118 | ErrMinLen = NewCheckError("NOT_MIN_LEN") 119 | ) 120 | ``` 121 | 122 | 123 | 124 | ```go 125 | var ( 126 | // ErrNotASCII indicates that the given string contains non-ASCII characters. 127 | ErrNotASCII = NewCheckError("NOT_ASCII") 128 | ) 129 | ``` 130 | 131 | 132 | 133 | ```go 134 | var ( 135 | // ErrNotAlphanumeric indicates that the given string contains non-alphanumeric characters. 136 | ErrNotAlphanumeric = NewCheckError("NOT_ALPHANUMERIC") 137 | ) 138 | ``` 139 | 140 | 141 | 142 | ```go 143 | var ( 144 | // ErrNotCIDR indicates that the given value is not a valid CIDR. 145 | ErrNotCIDR = NewCheckError("NOT_CIDR") 146 | ) 147 | ``` 148 | 149 | 150 | 151 | ```go 152 | var ( 153 | // ErrNotCreditCard indicates that the given value is not a valid credit card number. 154 | ErrNotCreditCard = NewCheckError("NOT_CREDIT_CARD") 155 | ) 156 | ``` 157 | 158 | 159 | 160 | ```go 161 | var ( 162 | // ErrNotDigits indicates that the given value is not a valid digits string. 163 | ErrNotDigits = NewCheckError("NOT_DIGITS") 164 | ) 165 | ``` 166 | 167 | 168 | 169 | ```go 170 | var ( 171 | // ErrNotEmail indicates that the given value is not a valid email address. 172 | ErrNotEmail = NewCheckError("NOT_EMAIL") 173 | ) 174 | ``` 175 | 176 | 177 | 178 | ```go 179 | var ( 180 | // ErrNotFQDN indicates that the given value is not a valid FQDN. 181 | ErrNotFQDN = NewCheckError("FQDN") 182 | ) 183 | ``` 184 | 185 | 186 | 187 | ```go 188 | var ( 189 | // ErrNotHex indicates that the given string contains hex characters. 190 | ErrNotHex = NewCheckError("NOT_HEX") 191 | ) 192 | ``` 193 | 194 | 195 | 196 | ```go 197 | var ( 198 | // ErrNotIP indicates that the given value is not a valid IP address. 199 | ErrNotIP = NewCheckError("NOT_IP") 200 | ) 201 | ``` 202 | 203 | 204 | 205 | ```go 206 | var ( 207 | // ErrNotIPv4 indicates that the given value is not a valid IPv4 address. 208 | ErrNotIPv4 = NewCheckError("NOT_IPV4") 209 | ) 210 | ``` 211 | 212 | 213 | 214 | ```go 215 | var ( 216 | // ErrNotIPv6 indicates that the given value is not a valid IPv6 address. 217 | ErrNotIPv6 = NewCheckError("NOT_IPV6") 218 | ) 219 | ``` 220 | 221 | 222 | 223 | ```go 224 | var ( 225 | // ErrNotISBN indicates that the given value is not a valid ISBN. 226 | ErrNotISBN = NewCheckError("NOT_ISBN") 227 | ) 228 | ``` 229 | 230 | 231 | 232 | ```go 233 | var ( 234 | // ErrNotLUHN indicates that the given value is not a valid LUHN number. 235 | ErrNotLUHN = NewCheckError("NOT_LUHN") 236 | ) 237 | ``` 238 | 239 | 240 | 241 | ```go 242 | var ( 243 | // ErrNotMAC indicates that the given value is not a valid MAC address. 244 | ErrNotMAC = NewCheckError("NOT_MAC") 245 | ) 246 | ``` 247 | 248 | ErrNotMatch indicates that the given string does not match the regexp pattern. 249 | 250 | ```go 251 | var ErrNotMatch = NewCheckError("REGEXP") 252 | ``` 253 | 254 | 255 | 256 | ```go 257 | var ( 258 | // ErrNotURL indicates that the given value is not a valid URL. 259 | ErrNotURL = NewCheckError("NOT_URL") 260 | ) 261 | ``` 262 | 263 | 264 | 265 | ```go 266 | var ( 267 | // ErrRequired indicates that a required value was missing. 268 | ErrRequired = NewCheckError("REQUIRED") 269 | ) 270 | ``` 271 | 272 | 273 | 274 | ```go 275 | var ( 276 | // ErrTime indicates that the value is not a valid based on the given time format. 277 | ErrTime = NewCheckError("NOT_TIME") 278 | ) 279 | ``` 280 | 281 | 282 | ## func [Check]() 283 | 284 | ```go 285 | func Check[T any](value T, checks ...CheckFunc[T]) (T, error) 286 | ``` 287 | 288 | Check applies the given check functions to a value sequentially. It returns the final value and the first encountered error, if any. 289 | 290 |
Example 291 |

292 | 293 | 294 | 295 | ```go 296 | package main 297 | 298 | import ( 299 | "fmt" 300 | 301 | v2 "github.com/cinar/checker/v2" 302 | ) 303 | 304 | func main() { 305 | name := " Onur Cinar " 306 | 307 | name, err := v2.Check(name, v2.TrimSpace, v2.Required) 308 | if err != nil { 309 | fmt.Println(err) 310 | return 311 | } 312 | 313 | fmt.Println(name) 314 | } 315 | ``` 316 | 317 | #### Output 318 | 319 | ``` 320 | Onur Cinar 321 | ``` 322 | 323 |

324 |
325 | 326 | 327 | ## func [CheckStruct]() 328 | 329 | ```go 330 | func CheckStruct(st any) (map[string]error, bool) 331 | ``` 332 | 333 | CheckStruct checks the given struct based on the validation rules specified in the "checker" tag of each struct field. It returns a map of field names to their corresponding errors, and a boolean indicating if all checks passed. 334 | 335 |
Example 336 |

337 | 338 | 339 | 340 | ```go 341 | package main 342 | 343 | import ( 344 | "fmt" 345 | 346 | v2 "github.com/cinar/checker/v2" 347 | ) 348 | 349 | func main() { 350 | type Person struct { 351 | Name string `checkers:"trim required"` 352 | } 353 | 354 | person := &Person{ 355 | Name: " Onur Cinar ", 356 | } 357 | 358 | errs, ok := v2.CheckStruct(person) 359 | if !ok { 360 | fmt.Println(errs) 361 | return 362 | } 363 | 364 | fmt.Println(person.Name) 365 | } 366 | ``` 367 | 368 | #### Output 369 | 370 | ``` 371 | Onur Cinar 372 | ``` 373 | 374 |

375 |
376 | 377 | 378 | ## func [CheckWithConfig]() 379 | 380 | ```go 381 | func CheckWithConfig[T any](value T, config string) (T, error) 382 | ``` 383 | 384 | CheckWithConfig applies the check functions specified by the config string to the given value. It returns the modified value and the first encountered error, if any. 385 | 386 | 387 | ## func [HTMLEscape]() 388 | 389 | ```go 390 | func HTMLEscape(value string) (string, error) 391 | ``` 392 | 393 | HTMLEscape applies HTML escaping to special characters. 394 | 395 | 396 | ## func [HTMLUnescape]() 397 | 398 | ```go 399 | func HTMLUnescape(value string) (string, error) 400 | ``` 401 | 402 | HTMLUnescape applies HTML unescaping to special characters. 403 | 404 | 405 | ## func [IsASCII]() 406 | 407 | ```go 408 | func IsASCII(value string) (string, error) 409 | ``` 410 | 411 | IsASCII checks if the given string consists of only ASCII characters. 412 | 413 |
Example 414 |

415 | 416 | 417 | 418 | ```go 419 | package main 420 | 421 | import ( 422 | "fmt" 423 | 424 | v2 "github.com/cinar/checker/v2" 425 | ) 426 | 427 | func main() { 428 | _, err := v2.IsASCII("Checker") 429 | if err != nil { 430 | fmt.Println(err) 431 | } 432 | } 433 | ``` 434 | 435 |

436 |
437 | 438 | 439 | ## func [IsAlphanumeric]() 440 | 441 | ```go 442 | func IsAlphanumeric(value string) (string, error) 443 | ``` 444 | 445 | IsAlphanumeric checks if the given string consists of only alphanumeric characters. 446 | 447 |
Example 448 |

449 | 450 | 451 | 452 | ```go 453 | package main 454 | 455 | import ( 456 | "fmt" 457 | 458 | v2 "github.com/cinar/checker/v2" 459 | ) 460 | 461 | func main() { 462 | _, err := v2.IsAlphanumeric("ABcd1234") 463 | if err != nil { 464 | fmt.Println(err) 465 | } 466 | } 467 | ``` 468 | 469 |

470 |
471 | 472 | 473 | ## func [IsAmexCreditCard]() 474 | 475 | ```go 476 | func IsAmexCreditCard(number string) (string, error) 477 | ``` 478 | 479 | IsAmexCreditCard checks if the given valie is a valid AMEX credit card. 480 | 481 |
Example 482 |

483 | 484 | 485 | 486 | ```go 487 | package main 488 | 489 | import ( 490 | v2 "github.com/cinar/checker/v2" 491 | ) 492 | 493 | func main() { 494 | _, err := v2.IsAmexCreditCard("378282246310005") 495 | 496 | if err != nil { 497 | // Send the errors back to the user 498 | } 499 | } 500 | ``` 501 | 502 |

503 |
504 | 505 | 506 | ## func [IsAnyCreditCard]() 507 | 508 | ```go 509 | func IsAnyCreditCard(number string) (string, error) 510 | ``` 511 | 512 | IsAnyCreditCard checks if the given value is a valid credit card number. 513 | 514 |
Example 515 |

516 | 517 | 518 | 519 | ```go 520 | package main 521 | 522 | import ( 523 | v2 "github.com/cinar/checker/v2" 524 | ) 525 | 526 | func main() { 527 | _, err := v2.IsAnyCreditCard("6011111111111117") 528 | 529 | if err != nil { 530 | // Send the errors back to the user 531 | } 532 | } 533 | ``` 534 | 535 |

536 |
537 | 538 | 539 | ## func [IsCIDR]() 540 | 541 | ```go 542 | func IsCIDR(value string) (string, error) 543 | ``` 544 | 545 | IsCIDR checks if the value is a valid CIDR notation IP address and prefix length. 546 | 547 |
Example 548 |

549 | 550 | 551 | 552 | ```go 553 | package main 554 | 555 | import ( 556 | "fmt" 557 | 558 | v2 "github.com/cinar/checker/v2" 559 | ) 560 | 561 | func main() { 562 | _, err := v2.IsCIDR("2001:db8::/32") 563 | if err != nil { 564 | fmt.Println(err) 565 | } 566 | } 567 | ``` 568 | 569 |

570 |
571 | 572 | 573 | ## func [IsDigits]() 574 | 575 | ```go 576 | func IsDigits(value string) (string, error) 577 | ``` 578 | 579 | IsDigits checks if the value contains only digit characters. 580 | 581 |
Example 582 |

583 | 584 | 585 | 586 | ```go 587 | package main 588 | 589 | import ( 590 | "fmt" 591 | 592 | v2 "github.com/cinar/checker/v2" 593 | ) 594 | 595 | func main() { 596 | _, err := v2.IsDigits("123456") 597 | if err != nil { 598 | fmt.Println(err) 599 | } 600 | } 601 | ``` 602 | 603 |

604 |
605 | 606 | 607 | ## func [IsDinersCreditCard]() 608 | 609 | ```go 610 | func IsDinersCreditCard(number string) (string, error) 611 | ``` 612 | 613 | IsDinersCreditCard checks if the given valie is a valid Diners credit card. 614 | 615 |
Example 616 |

617 | 618 | 619 | 620 | ```go 621 | package main 622 | 623 | import ( 624 | v2 "github.com/cinar/checker/v2" 625 | ) 626 | 627 | func main() { 628 | _, err := v2.IsDinersCreditCard("36227206271667") 629 | 630 | if err != nil { 631 | // Send the errors back to the user 632 | } 633 | } 634 | ``` 635 | 636 |

637 |
638 | 639 | 640 | ## func [IsDiscoverCreditCard]() 641 | 642 | ```go 643 | func IsDiscoverCreditCard(number string) (string, error) 644 | ``` 645 | 646 | IsDiscoverCreditCard checks if the given valie is a valid Discover credit card. 647 | 648 |
Example 649 |

650 | 651 | 652 | 653 | ```go 654 | package main 655 | 656 | import ( 657 | v2 "github.com/cinar/checker/v2" 658 | ) 659 | 660 | func main() { 661 | _, err := v2.IsDiscoverCreditCard("6011111111111117") 662 | 663 | if err != nil { 664 | // Send the errors back to the user 665 | } 666 | } 667 | ``` 668 | 669 |

670 |
671 | 672 | 673 | ## func [IsEmail]() 674 | 675 | ```go 676 | func IsEmail(value string) (string, error) 677 | ``` 678 | 679 | IsEmail checks if the value is a valid email address. 680 | 681 |
Example 682 |

683 | 684 | 685 | 686 | ```go 687 | package main 688 | 689 | import ( 690 | "fmt" 691 | 692 | v2 "github.com/cinar/checker/v2" 693 | ) 694 | 695 | func main() { 696 | _, err := v2.IsEmail("test@example.com") 697 | if err != nil { 698 | fmt.Println(err) 699 | } 700 | } 701 | ``` 702 | 703 |

704 |
705 | 706 | 707 | ## func [IsFQDN]() 708 | 709 | ```go 710 | func IsFQDN(value string) (string, error) 711 | ``` 712 | 713 | IsFQDN checks if the value is a valid fully qualified domain name \(FQDN\). 714 | 715 |
Example 716 |

717 | 718 | 719 | 720 | ```go 721 | package main 722 | 723 | import ( 724 | "fmt" 725 | 726 | v2 "github.com/cinar/checker/v2" 727 | ) 728 | 729 | func main() { 730 | _, err := v2.IsFQDN("example.com") 731 | if err != nil { 732 | fmt.Println(err) 733 | } 734 | } 735 | ``` 736 | 737 |

738 |
739 | 740 | 741 | ## func [IsGte]() 742 | 743 | ```go 744 | func IsGte[T cmp.Ordered](value, n T) (T, error) 745 | ``` 746 | 747 | IsGte checks if the value is greater than or equal to the given value. 748 | 749 | 750 | ## func [IsHex]() 751 | 752 | ```go 753 | func IsHex(value string) (string, error) 754 | ``` 755 | 756 | IsHex checks if the given string consists of only hex characters. 757 | 758 |
Example 759 |

760 | 761 | 762 | 763 | ```go 764 | package main 765 | 766 | import ( 767 | "fmt" 768 | 769 | v2 "github.com/cinar/checker/v2" 770 | ) 771 | 772 | func main() { 773 | _, err := v2.IsHex("0123456789abcdefABCDEF") 774 | if err != nil { 775 | fmt.Println(err) 776 | } 777 | } 778 | ``` 779 | 780 |

781 |
782 | 783 | 784 | ## func [IsIP]() 785 | 786 | ```go 787 | func IsIP(value string) (string, error) 788 | ``` 789 | 790 | IsIP checks if the value is a valid IP address. 791 | 792 |
Example 793 |

794 | 795 | 796 | 797 | ```go 798 | package main 799 | 800 | import ( 801 | "fmt" 802 | 803 | v2 "github.com/cinar/checker/v2" 804 | ) 805 | 806 | func main() { 807 | _, err := v2.IsIP("192.168.1.1") 808 | if err != nil { 809 | fmt.Println(err) 810 | } 811 | } 812 | ``` 813 | 814 |

815 |
816 | 817 | 818 | ## func [IsIPv4]() 819 | 820 | ```go 821 | func IsIPv4(value string) (string, error) 822 | ``` 823 | 824 | IsIPv4 checks if the value is a valid IPv4 address. 825 | 826 |
Example 827 |

828 | 829 | 830 | 831 | ```go 832 | package main 833 | 834 | import ( 835 | "fmt" 836 | 837 | v2 "github.com/cinar/checker/v2" 838 | ) 839 | 840 | func main() { 841 | _, err := v2.IsIPv4("192.168.1.1") 842 | if err != nil { 843 | fmt.Println(err) 844 | } 845 | } 846 | ``` 847 | 848 |

849 |
850 | 851 | 852 | ## func [IsIPv6]() 853 | 854 | ```go 855 | func IsIPv6(value string) (string, error) 856 | ``` 857 | 858 | IsIPv6 checks if the value is a valid IPv6 address. 859 | 860 |
Example 861 |

862 | 863 | 864 | 865 | ```go 866 | package main 867 | 868 | import ( 869 | "fmt" 870 | 871 | v2 "github.com/cinar/checker/v2" 872 | ) 873 | 874 | func main() { 875 | _, err := v2.IsIPv6("2001:db8::1") 876 | if err != nil { 877 | fmt.Println(err) 878 | } 879 | } 880 | ``` 881 | 882 |

883 |
884 | 885 | 886 | ## func [IsISBN]() 887 | 888 | ```go 889 | func IsISBN(value string) (string, error) 890 | ``` 891 | 892 | IsISBN checks if the value is a valid ISBN\-10 or ISBN\-13. 893 | 894 |
Example 895 |

896 | 897 | 898 | 899 | ```go 900 | package main 901 | 902 | import ( 903 | "fmt" 904 | 905 | v2 "github.com/cinar/checker/v2" 906 | ) 907 | 908 | func main() { 909 | _, err := v2.IsISBN("1430248270") 910 | if err != nil { 911 | fmt.Println(err) 912 | } 913 | } 914 | ``` 915 | 916 |

917 |
918 | 919 | 920 | ## func [IsJcbCreditCard]() 921 | 922 | ```go 923 | func IsJcbCreditCard(number string) (string, error) 924 | ``` 925 | 926 | IsJcbCreditCard checks if the given valie is a valid JCB 15 credit card. 927 | 928 |
Example 929 |

930 | 931 | 932 | 933 | ```go 934 | package main 935 | 936 | import ( 937 | v2 "github.com/cinar/checker/v2" 938 | ) 939 | 940 | func main() { 941 | _, err := v2.IsJcbCreditCard("3530111333300000") 942 | 943 | if err != nil { 944 | // Send the errors back to the user 945 | } 946 | } 947 | ``` 948 | 949 |

950 |
951 | 952 | 953 | ## func [IsLUHN]() 954 | 955 | ```go 956 | func IsLUHN(value string) (string, error) 957 | ``` 958 | 959 | IsLUHN checks if the value is a valid LUHN number. 960 | 961 |
Example 962 |

963 | 964 | 965 | 966 | ```go 967 | package main 968 | 969 | import ( 970 | "fmt" 971 | 972 | v2 "github.com/cinar/checker/v2" 973 | ) 974 | 975 | func main() { 976 | _, err := v2.IsLUHN("4012888888881881") 977 | if err != nil { 978 | fmt.Println(err) 979 | } 980 | } 981 | ``` 982 | 983 |

984 |
985 | 986 | 987 | ## func [IsLte]() 988 | 989 | ```go 990 | func IsLte[T cmp.Ordered](value, n T) (T, error) 991 | ``` 992 | 993 | IsLte checks if the value is less than or equal to the given value. 994 | 995 | 996 | ## func [IsMAC]() 997 | 998 | ```go 999 | func IsMAC(value string) (string, error) 1000 | ``` 1001 | 1002 | IsMAC checks if the value is a valid MAC address. 1003 | 1004 |
Example 1005 |

1006 | 1007 | 1008 | 1009 | ```go 1010 | package main 1011 | 1012 | import ( 1013 | "fmt" 1014 | 1015 | v2 "github.com/cinar/checker/v2" 1016 | ) 1017 | 1018 | func main() { 1019 | _, err := v2.IsMAC("00:1A:2B:3C:4D:5E") 1020 | if err != nil { 1021 | fmt.Println(err) 1022 | } 1023 | } 1024 | ``` 1025 | 1026 |

1027 |
1028 | 1029 | 1030 | ## func [IsMasterCardCreditCard]() 1031 | 1032 | ```go 1033 | func IsMasterCardCreditCard(number string) (string, error) 1034 | ``` 1035 | 1036 | IsMasterCardCreditCard checks if the given valie is a valid MasterCard credit card. 1037 | 1038 |
Example 1039 |

1040 | 1041 | 1042 | 1043 | ```go 1044 | package main 1045 | 1046 | import ( 1047 | v2 "github.com/cinar/checker/v2" 1048 | ) 1049 | 1050 | func main() { 1051 | _, err := v2.IsMasterCardCreditCard("5555555555554444") 1052 | 1053 | if err != nil { 1054 | // Send the errors back to the user 1055 | } 1056 | } 1057 | ``` 1058 | 1059 |

1060 |
1061 | 1062 | 1063 | ## func [IsRegexp]() 1064 | 1065 | ```go 1066 | func IsRegexp(expression, value string) (string, error) 1067 | ``` 1068 | 1069 | IsRegexp checks if the given string matches the given regexp expression. 1070 | 1071 |
Example 1072 |

1073 | 1074 | 1075 | 1076 | ```go 1077 | package main 1078 | 1079 | import ( 1080 | "fmt" 1081 | 1082 | v2 "github.com/cinar/checker/v2" 1083 | ) 1084 | 1085 | func main() { 1086 | _, err := v2.IsRegexp("^[0-9a-fA-F]+$", "ABcd1234") 1087 | if err != nil { 1088 | fmt.Println(err) 1089 | } 1090 | } 1091 | ``` 1092 | 1093 |

1094 |
1095 | 1096 | 1097 | ## func [IsTime]() 1098 | 1099 | ```go 1100 | func IsTime(params, value string) (string, error) 1101 | ``` 1102 | 1103 | IsTime checks if the given value is a valid time based on the given layout. 1104 | 1105 |
Example 1106 |

1107 | 1108 | 1109 | 1110 | ```go 1111 | package main 1112 | 1113 | import ( 1114 | v2 "github.com/cinar/checker/v2" 1115 | ) 1116 | 1117 | func main() { 1118 | value := "2024-12-31" 1119 | 1120 | _, err := v2.IsTime("DateOnly", value) 1121 | if err != nil { 1122 | panic(err) 1123 | } 1124 | } 1125 | ``` 1126 | 1127 |

1128 |
1129 | 1130 |
Example (Custom) 1131 |

1132 | 1133 | 1134 | 1135 | ```go 1136 | package main 1137 | 1138 | import ( 1139 | v2 "github.com/cinar/checker/v2" 1140 | ) 1141 | 1142 | func main() { 1143 | rfc3339Layout := "2006-01-02T15:04:05Z07:00" 1144 | 1145 | value := "2024-12-31T10:20:00Z07:00" 1146 | 1147 | _, err := v2.IsTime(rfc3339Layout, value) 1148 | if err != nil { 1149 | panic(err) 1150 | } 1151 | } 1152 | ``` 1153 | 1154 |

1155 |
1156 | 1157 | 1158 | ## func [IsURL]() 1159 | 1160 | ```go 1161 | func IsURL(value string) (string, error) 1162 | ``` 1163 | 1164 | IsURL checks if the value is a valid URL. 1165 | 1166 |
Example 1167 |

1168 | 1169 | 1170 | 1171 | ```go 1172 | package main 1173 | 1174 | import ( 1175 | "fmt" 1176 | 1177 | v2 "github.com/cinar/checker/v2" 1178 | ) 1179 | 1180 | func main() { 1181 | _, err := v2.IsURL("https://example.com") 1182 | if err != nil { 1183 | fmt.Println(err) 1184 | } 1185 | } 1186 | ``` 1187 | 1188 |

1189 |
1190 | 1191 | 1192 | ## func [IsUnionPayCreditCard]() 1193 | 1194 | ```go 1195 | func IsUnionPayCreditCard(number string) (string, error) 1196 | ``` 1197 | 1198 | IsUnionPayCreditCard checks if the given valie is a valid UnionPay credit card. 1199 | 1200 |
Example 1201 |

1202 | 1203 | 1204 | 1205 | ```go 1206 | package main 1207 | 1208 | import ( 1209 | v2 "github.com/cinar/checker/v2" 1210 | ) 1211 | 1212 | func main() { 1213 | _, err := v2.IsUnionPayCreditCard("6200000000000005") 1214 | 1215 | if err != nil { 1216 | // Send the errors back to the user 1217 | } 1218 | } 1219 | ``` 1220 | 1221 |

1222 |
1223 | 1224 | 1225 | ## func [IsVisaCreditCard]() 1226 | 1227 | ```go 1228 | func IsVisaCreditCard(number string) (string, error) 1229 | ``` 1230 | 1231 | IsVisaCreditCard checks if the given valie is a valid Visa credit card. 1232 | 1233 |
Example 1234 |

1235 | 1236 | 1237 | 1238 | ```go 1239 | package main 1240 | 1241 | import ( 1242 | v2 "github.com/cinar/checker/v2" 1243 | ) 1244 | 1245 | func main() { 1246 | _, err := v2.IsVisaCreditCard("4111111111111111") 1247 | 1248 | if err != nil { 1249 | // Send the errors back to the user 1250 | } 1251 | } 1252 | ``` 1253 | 1254 |

1255 |
1256 | 1257 | 1258 | ## func [Lower]() 1259 | 1260 | ```go 1261 | func Lower(value string) (string, error) 1262 | ``` 1263 | 1264 | Lower maps all Unicode letters in the given value to their lower case. 1265 | 1266 | 1267 | ## func [ReflectCheckWithConfig]() 1268 | 1269 | ```go 1270 | func ReflectCheckWithConfig(value reflect.Value, config string) (reflect.Value, error) 1271 | ``` 1272 | 1273 | ReflectCheckWithConfig applies the check functions specified by the config string to the given reflect.Value. It returns the modified reflect.Value and the first encountered error, if any. 1274 | 1275 | 1276 | ## func [RegisterLocale]() 1277 | 1278 | ```go 1279 | func RegisterLocale(locale string, messages map[string]string) 1280 | ``` 1281 | 1282 | RegisterLocale registers the localized error messages for the given locale. 1283 | 1284 | 1285 | ## func [RegisterMaker]() 1286 | 1287 | ```go 1288 | func RegisterMaker(name string, maker MakeCheckFunc) 1289 | ``` 1290 | 1291 | RegisterMaker registers a new maker function with the given name. 1292 | 1293 |
Example 1294 |

1295 | 1296 | 1297 | 1298 | ```go 1299 | package main 1300 | 1301 | import ( 1302 | "fmt" 1303 | "reflect" 1304 | 1305 | "github.com/cinar/checker/v2/locales" 1306 | 1307 | v2 "github.com/cinar/checker/v2" 1308 | ) 1309 | 1310 | func main() { 1311 | locales.EnUSMessages["NOT_FRUIT"] = "Not a fruit name." 1312 | 1313 | v2.RegisterMaker("is-fruit", func(params string) v2.CheckFunc[reflect.Value] { 1314 | return func(value reflect.Value) (reflect.Value, error) { 1315 | stringValue := value.Interface().(string) 1316 | 1317 | if stringValue == "apple" || stringValue == "banana" { 1318 | return value, nil 1319 | } 1320 | 1321 | return value, v2.NewCheckError("NOT_FRUIT") 1322 | } 1323 | }) 1324 | 1325 | type Item struct { 1326 | Name string `checkers:"is-fruit"` 1327 | } 1328 | 1329 | person := &Item{ 1330 | Name: "banana", 1331 | } 1332 | 1333 | err, ok := v2.CheckStruct(person) 1334 | if !ok { 1335 | fmt.Println(err) 1336 | } 1337 | } 1338 | ``` 1339 | 1340 |

1341 |
1342 | 1343 | 1344 | ## func [Required]() 1345 | 1346 | ```go 1347 | func Required[T any](value T) (T, error) 1348 | ``` 1349 | 1350 | Required checks if the given value of type T is its zero value. It returns an error if the value is zero. 1351 | 1352 | 1353 | ## func [Title]() 1354 | 1355 | ```go 1356 | func Title(value string) (string, error) 1357 | ``` 1358 | 1359 | Title returns the value of the string with the first letter of each word in upper case. 1360 | 1361 | 1362 | ## func [TrimLeft]() 1363 | 1364 | ```go 1365 | func TrimLeft(value string) (string, error) 1366 | ``` 1367 | 1368 | TrimLeft returns the value of the string with whitespace removed from the beginning. 1369 | 1370 | 1371 | ## func [TrimRight]() 1372 | 1373 | ```go 1374 | func TrimRight(value string) (string, error) 1375 | ``` 1376 | 1377 | TrimRight returns the value of the string with whitespace removed from the end. 1378 | 1379 | 1380 | ## func [TrimSpace]() 1381 | 1382 | ```go 1383 | func TrimSpace(value string) (string, error) 1384 | ``` 1385 | 1386 | TrimSpace returns the value of the string with whitespace removed from both ends. 1387 | 1388 | 1389 | ## func [URLEscape]() 1390 | 1391 | ```go 1392 | func URLEscape(value string) (string, error) 1393 | ``` 1394 | 1395 | URLEscape applies URL escaping to special characters. 1396 | 1397 | 1398 | ## func [URLUnescape]() 1399 | 1400 | ```go 1401 | func URLUnescape(value string) (string, error) 1402 | ``` 1403 | 1404 | URLUnescape applies URL unescaping to special characters. 1405 | 1406 | 1407 | ## func [Upper]() 1408 | 1409 | ```go 1410 | func Upper(value string) (string, error) 1411 | ``` 1412 | 1413 | Upper maps all Unicode letters in the given value to their upper case. 1414 | 1415 | 1416 | ## type [CheckError]() 1417 | 1418 | CheckError defines the check error. 1419 | 1420 | ```go 1421 | type CheckError struct { 1422 | // Code is the error code. 1423 | Code string 1424 | 1425 | // data is the error data. 1426 | Data map[string]interface{} 1427 | } 1428 | ``` 1429 | 1430 | 1431 | ### func [NewCheckError]() 1432 | 1433 | ```go 1434 | func NewCheckError(code string) *CheckError 1435 | ``` 1436 | 1437 | NewCheckError creates a new check error with the given code. 1438 | 1439 | 1440 | ### func [NewCheckErrorWithData]() 1441 | 1442 | ```go 1443 | func NewCheckErrorWithData(code string, data map[string]interface{}) *CheckError 1444 | ``` 1445 | 1446 | NewCheckErrorWithData creates a new check error with the given code and data. 1447 | 1448 | 1449 | ### func \(\*CheckError\) [Error]() 1450 | 1451 | ```go 1452 | func (c *CheckError) Error() string 1453 | ``` 1454 | 1455 | Error returns the error message for the check. 1456 | 1457 | 1458 | ### func \(\*CheckError\) [ErrorWithLocale]() 1459 | 1460 | ```go 1461 | func (c *CheckError) ErrorWithLocale(locale string) string 1462 | ``` 1463 | 1464 | ErrorWithLocale returns the localized error message for the check with the given locale. 1465 | 1466 | 1467 | ### func \(\*CheckError\) [Is]() 1468 | 1469 | ```go 1470 | func (c *CheckError) Is(target error) bool 1471 | ``` 1472 | 1473 | Is reports whether the check error is the same as the target error. 1474 | 1475 | 1476 | ## type [CheckFunc]() 1477 | 1478 | CheckFunc is a function that takes a value of type T and performs a check on it. It returns the resulting value and any error that occurred during the check. 1479 | 1480 | ```go 1481 | type CheckFunc[T any] func(value T) (T, error) 1482 | ``` 1483 | 1484 | 1485 | ### func [MakeRegexpChecker]() 1486 | 1487 | ```go 1488 | func MakeRegexpChecker(expression string, invalidError error) CheckFunc[reflect.Value] 1489 | ``` 1490 | 1491 | MakeRegexpChecker makes a regexp checker for the given regexp expression with the given invalid result. 1492 | 1493 | 1494 | ### func [MaxLen]() 1495 | 1496 | ```go 1497 | func MaxLen[T any](n int) CheckFunc[T] 1498 | ``` 1499 | 1500 | MaxLen checks if the length of the given value \(string, slice, or map\) is at most n. Returns an error if the length is greater than n. 1501 | 1502 | 1503 | ### func [MinLen]() 1504 | 1505 | ```go 1506 | func MinLen[T any](n int) CheckFunc[T] 1507 | ``` 1508 | 1509 | MinLen checks if the length of the given value \(string, slice, or map\) is at least n. Returns an error if the length is less than n. 1510 | 1511 | 1512 | ## type [MakeCheckFunc]() 1513 | 1514 | MakeCheckFunc is a function that returns a check function using the given params. 1515 | 1516 | ```go 1517 | type MakeCheckFunc func(params string) CheckFunc[reflect.Value] 1518 | ``` 1519 | 1520 | Generated by [gomarkdoc]() 1521 | 1522 | 1523 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Onur Cinar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/cinar/checker?status.svg)](https://godoc.org/github.com/cinar/checker) 2 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/cinar/checker)](https://goreportcard.com/report/github.com/cinar/checker) 4 | ![Go CI](https://github.com/cinar/checker/actions/workflows/ci.yml/badge.svg) 5 | [![codecov](https://codecov.io/gh/cinar/checker/branch/main/graph/badge.svg?token=VO9BYBHJHE)](https://codecov.io/gh/cinar/checker) 6 | 7 | # Checker 8 | 9 | Checker is a lightweight Go library designed to validate user input efficiently. It supports validation of both struct fields and individual input values. 10 | 11 | While there are numerous validation libraries available, Checker stands out due to its simplicity and lack of external dependencies. This makes it an ideal choice for developers who prefer to minimize dependencies and maintain control over their tools. Checker is straightforward to use and effectively meets your validation needs. 12 | 13 | ## Usage 14 | 15 | To begin using the Checker library, install it with the following command: 16 | 17 | ```bash 18 | go get github.com/cinar/checker/v2 19 | ``` 20 | 21 | Then, import the library into your source file as shown below: 22 | 23 | ```golang 24 | import ( 25 | checker "github.com/cinar/checker/v2" 26 | ) 27 | ``` 28 | 29 | ### Validating User Input Stored in a Struct 30 | 31 | Checker can validate user input stored in a struct by listing the checkers in the struct tags for each field. Here is an example: 32 | 33 | ```golang 34 | type Person struct { 35 | Name string `checkers:"trim required"` 36 | } 37 | 38 | person := &Person{ 39 | Name: " Onur Cinar ", 40 | } 41 | 42 | errors, valid := checker.CheckStruct(person) 43 | if !valid { 44 | // Handle validation errors 45 | } 46 | ``` 47 | 48 | ### Validating Individual User Input with Multiple Checkers 49 | 50 | You can also validate individual user input by calling checker functions directly. Here is an example: 51 | 52 | ```golang 53 | name := " Onur Cinar " 54 | 55 | name, err := checker.Check(name, checker.Trim, checker.Required) 56 | if err != nil { 57 | // Handle validation error 58 | } 59 | ``` 60 | 61 | The checkers and normalizers can also be provided through a config string. Here is an example: 62 | 63 | ```golang 64 | name := " Onur Cinar " 65 | 66 | name, err := checker.CheckWithConfig(name, "trim requied") 67 | if err != nil { 68 | // Handle validation error 69 | } 70 | 71 | ``` 72 | 73 | ### Validating Individual User Input 74 | 75 | For simpler validation, you can call individual checker functions. Here is an example: 76 | 77 | ```golang 78 | name := "Onur Cinar" 79 | 80 | err := checker.IsRequired(name) 81 | if err != nil { 82 | // Handle validation error 83 | } 84 | ``` 85 | 86 | ## Normalizers and Checkers 87 | 88 | Checkers validate user input, while normalizers transform it into a preferred format. For example, a normalizer can trim spaces from a string or convert it to title case. 89 | 90 | Although combining checkers and normalizers into a single library might seem unconventional, using them together can be beneficial. They can be mixed in any order when defining validation steps. For instance, you can use the `trim` normalizer with the `required` checker to first trim the input and then ensure it is provided. Here is an example: 91 | 92 | ```golang 93 | type Person struct { 94 | Name string `checkers:"trim required"` 95 | } 96 | ``` 97 | 98 | # Checkers Provided 99 | 100 | - [`ascii`](DOC.md#IsASCII): Ensures the string contains only ASCII characters. 101 | - [`alphanumeric`](DOC.md#IsAlphanumeric): Ensures the string contains only letters and numbers. 102 | - [`credit-card`](DOC.md#IsAnyCreditCard): Ensures the string is a valid credit card number. 103 | - [`cidr`](DOC.md#IsCIDR): Ensures the string is a valid CIDR notation. 104 | - [`digits`](DOC.md#IsDigits): Ensures the string contains only digits. 105 | - [`email`](DOC.md#IsEmail): Ensures the string is a valid email address. 106 | - [`fqdn`](DOC.md#IsFQDN): Ensures the string is a valid fully qualified domain name. 107 | - [`gte`](DOC.md#IsGte): Ensures the value is greater than or equal to the specified number. 108 | - [`hex`](DOC.md#IsHex): Ensures the string contains only hexadecimal digits. 109 | - [`ip`](DOC.md#IsIP): Ensures the string is a valid IP address. 110 | - [`ipv4`](DOC.md#IsIPv4): Ensures the string is a valid IPv4 address. 111 | - [`ipv6`](DOC.md#IsIPv6): Ensures the string is a valid IPv6 address. 112 | - [`isbn`](DOC.md#IsISBN): Ensures the string is a valid ISBN. 113 | - [`lte`](DOC.md#ISLte): Ensures the value is less than or equal to the specified number. 114 | - [`luhn`](DOC.md#IsLUHN): Ensures the string is a valid LUHN number. 115 | - [`mac`](DOC.md#IsMAC): Ensures the string is a valid MAC address. 116 | - [`max-len`](DOC.md#func-maxlen): Ensures the length of the given value (string, slice, or map) is at most n. 117 | - [`min-len`](DOC.md#func-minlen): Ensures the length of the given value (string, slice, or map) is at least n. 118 | - [`required`](DOC.md#func-required) Ensures the value is provided. 119 | - [`regexp`](DOC.md#func-makeregexpchecker) Ensured the string matches the pattern. 120 | - [`time`](DOC.md#func-istime) Ensured the string matches the provided time layout. 121 | - [`url`](DOC.md#IsURL): Ensures the string is a valid URL. 122 | 123 | # Normalizers Provided 124 | 125 | - [`lower`](DOC.md#Lower): Converts the string to lowercase. 126 | - [`title`](DOC.md#Title): Converts the string to title case. 127 | - [`trim-left`](DOC.md#TrimLeft): Trims whitespace from the left side of the string. 128 | - [`trim-right`](DOC.md#TrimRight): Trims whitespace from the right side of the string. 129 | - [`trim`](DOC.md#TrimSpace): Trims whitespace from both sides of the string. 130 | - [`upper`](DOC.md#Upper): Converts the string to uppercase. 131 | - [`html-escape`](DOC.md#HTMLEscape): Escapes special characters in the string for HTML. 132 | - [`html-unescape`](DOC.md#HTMLUnescape): Unescapes special characters in the string for HTML. 133 | - [`url-escape`](DOC.md#URLEscape): Escapes special characters in the string for URLs. 134 | - [`url-unescape`](DOC.md#URLUnescape): Unescapes special characters in the string for URLs. 135 | 136 | # Custom Checkers and Normalizers 137 | 138 | You can define custom checkers or normalizers and register them for use in your validation logic. Here is an example of how to create and register a custom checker: 139 | 140 | ```golang 141 | checker.RegisterMaker("is-fruit", func(params string) v2.CheckFunc[reflect.Value] { 142 | return func(value reflect.Value) (reflect.Value, error) { 143 | stringValue := value.Interface().(string) 144 | 145 | if stringValue == "apple" || stringValue == "banana" { 146 | return value, nil 147 | } 148 | 149 | return value, v2.NewCheckError("NOT_FRUIT") 150 | } 151 | }) 152 | ``` 153 | 154 | In this example, the custom checker `is-fruit` checks if the input value is either "apple" or "banana". If the value is not one of these, it returns an error. 155 | 156 | Once registered, you can use your custom checker in struct tags just like the built-in checkers: 157 | 158 | ```golang 159 | type Item struct { 160 | Name string `checkers:"is-fruit"` 161 | } 162 | 163 | item := &Item{ 164 | Name: "banana", 165 | } 166 | 167 | errors, valid := v2.CheckStruct(item) 168 | if !valid { 169 | fmt.Println(errors) 170 | } 171 | ``` 172 | 173 | In this example, the `is-fruit` checker is used to validate that the `Name` field of the `Item` struct is either "apple" or "banana". 174 | 175 | # Slice and Item Level Checkers 176 | 177 | When adding checker struct tags to a slice, you can use the `@` prefix to indicate that the checker should be applied to the slice itself. Checkers without the `@` prefix will be applied to the individual items within the slice. Here is an example: 178 | 179 | ```golang 180 | type Person struct { 181 | Name string `checkers:"required"` 182 | Emails []string `checkers:"@max-len:2 max-len:64"` 183 | } 184 | ``` 185 | 186 | In this example: 187 | - `@max-len:2` ensures that the `Emails` slice itself has at most two items. 188 | - `max-len:64` ensures that each email string within the `Emails` slice has a maximum length of 64 characters. 189 | 190 | # Localized Error Messages 191 | 192 | When validation fails, Checker returns an error. By default, the [Error()](DOC.md#CheckError.Error) function provides a human-readable error message in `en-US` locale. 193 | 194 | ```golang 195 | _, err := checker.IsEmail("abcd") 196 | if err != nil { 197 | fmt.Println(err) 198 | // Output: Not a valid email address. 199 | } 200 | ``` 201 | 202 | To get error messages in other languages, use the [ErrorWithLocale()](DOC.md#CheckError.ErrorWithLocale) function. By default, only `en-US` is registered. You can register additional languages by calling [RegisterLocale](DOC.md#RegisterLocale). 203 | 204 | ```golang 205 | // Register de-DE localized error messages. 206 | checker.RegisterLocale(locales.DeDE, locales.DeDEMessages) 207 | 208 | _, err := checker.IsEmail("abcd") 209 | if err != nil { 210 | fmt.Println(err.ErrorWithLocale(locales.DeDE)) 211 | // Output: Keine gültige E-Mail-Adresse. 212 | } 213 | ``` 214 | 215 | You can also customize existing error messages or add new ones to `locales.EnUSMessages` and other locale maps. 216 | 217 | ```golang 218 | // Register the en-US localized error message for the custom NOT_FRUIT error code. 219 | locales.EnUSMessages["NOT_FRUIT"] = "Not a fruit name." 220 | 221 | errors, valid := v2.CheckStruct(item) 222 | if !valid { 223 | fmt.Println(errors) 224 | // Output: map[Name:Not a fruit name.] 225 | } 226 | ``` 227 | 228 | Error messages are generated using Golang template functions, allowing them to include variables. 229 | 230 | ```golang 231 | // Custrom checker error containing the item name. 232 | err := checker.NewCheckErrorWithData( 233 | "NOT_FRUIT", 234 | map[string]interface{}{ 235 | "name": "abcd", 236 | }, 237 | ) 238 | 239 | // Register the en-US localized error message for the custom NOT_FRUIT error code. 240 | locales.EnUSMessages["NOT_FRUIT"] = "Name {{ .name }} is not a fruit name." 241 | 242 | errors, valid := v2.CheckStruct(item) 243 | if !valid { 244 | fmt.Println(errors) 245 | // Output: map[Name:Name abcd is not a fruit name.] 246 | } 247 | ``` 248 | 249 | # Contributing to the Project 250 | 251 | Anyone can contribute to Checkers library. Please make sure to read our [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md) guide first. Follow the [How to Contribute to Checker](./CONTRIBUTING.md) to contribute. 252 | 253 | # License 254 | 255 | This library is free to use, modify, and distribute under the terms of the MIT license. The full license text can be found in the [LICENSE](./LICENSE) file. 256 | 257 | The MIT license is a permissive license that allows you to do almost anything with the library, as long as you retain the copyright notice and the license text. This means that you can use the library in commercial products, modify it, and redistribute it without having to ask for permission from the authors. 258 | 259 | The [LICENSE](./LICENSE) file is located in the root directory of the library. You can open it in a text editor to read the full license text. 260 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0.x | :white_check_mark: | 8 | | < 1.0 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | If you found a vulnerability with this library, please file a new issue. 13 | -------------------------------------------------------------------------------- /alphanumeric.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "unicode" 11 | ) 12 | 13 | const ( 14 | // nameAlphanumeric is the name of the alphanumeric check. 15 | nameAlphanumeric = "alphanumeric" 16 | ) 17 | 18 | var ( 19 | // ErrNotAlphanumeric indicates that the given string contains non-alphanumeric characters. 20 | ErrNotAlphanumeric = NewCheckError("NOT_ALPHANUMERIC") 21 | ) 22 | 23 | // IsAlphanumeric checks if the given string consists of only alphanumeric characters. 24 | func IsAlphanumeric(value string) (string, error) { 25 | for _, c := range value { 26 | if !unicode.IsDigit(c) && !unicode.IsLetter(c) { 27 | return value, ErrNotAlphanumeric 28 | } 29 | } 30 | 31 | return value, nil 32 | } 33 | 34 | // checkAlphanumeric checks if the given string consists of only alphanumeric characters. 35 | func isAlphanumeric(value reflect.Value) (reflect.Value, error) { 36 | _, err := IsAlphanumeric(value.Interface().(string)) 37 | return value, err 38 | } 39 | 40 | // makeAlphanumeric makes a checker function for the alphanumeric checker. 41 | func makeAlphanumeric(_ string) CheckFunc[reflect.Value] { 42 | return isAlphanumeric 43 | } 44 | -------------------------------------------------------------------------------- /alphanumeric_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsAlphanumeric() { 16 | _, err := v2.IsAlphanumeric("ABcd1234") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsAlphanumericInvalid(t *testing.T) { 23 | _, err := v2.IsAlphanumeric("-/") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsAlphanumericValid(t *testing.T) { 30 | _, err := v2.IsAlphanumeric("ABcd1234") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckAlphanumericNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Person struct { 40 | Name int `checkers:"alphanumeric"` 41 | } 42 | 43 | person := &Person{} 44 | 45 | v2.CheckStruct(person) 46 | } 47 | 48 | func TestCheckAlphanumericInvalid(t *testing.T) { 49 | type Person struct { 50 | Name string `checkers:"alphanumeric"` 51 | } 52 | 53 | person := &Person{ 54 | Name: "name-/", 55 | } 56 | 57 | _, ok := v2.CheckStruct(person) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckAlphanumericValid(t *testing.T) { 64 | type Person struct { 65 | Name string `checkers:"alphanumeric"` 66 | } 67 | 68 | person := &Person{ 69 | Name: "ABcd1234", 70 | } 71 | 72 | errs, ok := v2.CheckStruct(person) 73 | if !ok { 74 | t.Fatal(errs) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ascii.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "unicode" 11 | ) 12 | 13 | const ( 14 | // nameASCII is the name of the ASCII check. 15 | nameASCII = "ascii" 16 | ) 17 | 18 | var ( 19 | // ErrNotASCII indicates that the given string contains non-ASCII characters. 20 | ErrNotASCII = NewCheckError("NOT_ASCII") 21 | ) 22 | 23 | // IsASCII checks if the given string consists of only ASCII characters. 24 | func IsASCII(value string) (string, error) { 25 | for _, c := range value { 26 | if c > unicode.MaxASCII { 27 | return value, ErrNotASCII 28 | } 29 | } 30 | 31 | return value, nil 32 | } 33 | 34 | // checkASCII checks if the given string consists of only ASCII characters. 35 | func isASCII(value reflect.Value) (reflect.Value, error) { 36 | _, err := IsASCII(value.Interface().(string)) 37 | return value, err 38 | } 39 | 40 | // makeASCII makes a checker function for the ASCII checker. 41 | func makeASCII(_ string) CheckFunc[reflect.Value] { 42 | return isASCII 43 | } 44 | -------------------------------------------------------------------------------- /ascii_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsASCII() { 16 | _, err := v2.IsASCII("Checker") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsASCIIInvalid(t *testing.T) { 23 | _, err := v2.IsASCII("𝄞 Music!") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsASCIIValid(t *testing.T) { 30 | _, err := v2.IsASCII("Checker") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckASCIINonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type User struct { 40 | Username int `checkers:"ascii"` 41 | } 42 | 43 | user := &User{} 44 | 45 | v2.CheckStruct(user) 46 | } 47 | 48 | func TestCheckASCIIInvalid(t *testing.T) { 49 | type User struct { 50 | Username string `checkers:"ascii"` 51 | } 52 | 53 | user := &User{ 54 | Username: "𝄞 Music!", 55 | } 56 | 57 | _, ok := v2.CheckStruct(user) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckASCIIValid(t *testing.T) { 64 | type User struct { 65 | Username string `checkers:"ascii"` 66 | } 67 | 68 | user := &User{ 69 | Username: "checker", 70 | } 71 | 72 | _, ok := v2.CheckStruct(user) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /check_error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "html/template" 10 | "strings" 11 | 12 | "github.com/cinar/checker/v2/locales" 13 | ) 14 | 15 | // CheckError defines the check error. 16 | type CheckError struct { 17 | // Code is the error code. 18 | Code string 19 | 20 | // data is the error data. 21 | Data map[string]interface{} 22 | } 23 | 24 | const ( 25 | // DefaultLocale is the default locale. 26 | DefaultLocale = locales.EnUS 27 | ) 28 | 29 | // errorMessages is the map of localized error messages. 30 | var errorMessages = map[string]map[string]string{ 31 | locales.EnUS: locales.EnUSMessages, 32 | } 33 | 34 | // NewCheckError creates a new check error with the given code. 35 | func NewCheckError(code string) *CheckError { 36 | return NewCheckErrorWithData( 37 | code, 38 | make(map[string]interface{}), 39 | ) 40 | } 41 | 42 | // NewCheckErrorWithData creates a new check error with the given code and data. 43 | func NewCheckErrorWithData(code string, data map[string]interface{}) *CheckError { 44 | return &CheckError{ 45 | Code: code, 46 | Data: data, 47 | } 48 | } 49 | 50 | // Error returns the error message for the check. 51 | func (c *CheckError) Error() string { 52 | return c.ErrorWithLocale(DefaultLocale) 53 | } 54 | 55 | // Is reports whether the check error is the same as the target error. 56 | func (c *CheckError) Is(target error) bool { 57 | if other, ok := target.(*CheckError); ok { 58 | return c.Code == other.Code 59 | } 60 | 61 | return false 62 | } 63 | 64 | // ErrorWithLocale returns the localized error message for the check with the given locale. 65 | func (c *CheckError) ErrorWithLocale(locale string) string { 66 | tmpl, err := template.New("error").Parse(getLocalizedErrorMessage(locale, c.Code)) 67 | if err != nil { 68 | return c.Code 69 | } 70 | 71 | var message strings.Builder 72 | if err := tmpl.Execute(&message, c.Data); err != nil { 73 | return c.Code 74 | } 75 | 76 | return message.String() 77 | } 78 | 79 | // RegisterLocale registers the localized error messages for the given locale. 80 | func RegisterLocale(locale string, messages map[string]string) { 81 | errorMessages[locale] = messages 82 | } 83 | 84 | // getLocalizedErrorMessage returns the localized error message for the given locale and code. 85 | func getLocalizedErrorMessage(locale, code string) string { 86 | if messages, found := errorMessages[locale]; found { 87 | if message, exists := messages[code]; exists { 88 | return message 89 | } 90 | } 91 | 92 | if messages, found := errorMessages[DefaultLocale]; found { 93 | if message, exists := messages[code]; exists { 94 | return message 95 | } 96 | } 97 | 98 | return code 99 | } 100 | -------------------------------------------------------------------------------- /check_error_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "io/fs" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | "github.com/cinar/checker/v2/locales" 14 | ) 15 | 16 | func TestCheckErrorWithNotLocalizedCode(t *testing.T) { 17 | code := "TEST" 18 | 19 | err := v2.NewCheckError(code) 20 | 21 | if err.Error() != code { 22 | t.Fatalf("actual %s expected %s", err.Error(), code) 23 | } 24 | } 25 | 26 | func TestCheckErrorWithLocalizedCode(t *testing.T) { 27 | code := "TEST" 28 | message := "Test message" 29 | 30 | locales.EnUSMessages[code] = message 31 | 32 | err := v2.NewCheckError(code) 33 | 34 | if err.ErrorWithLocale("fr-FR") != message { 35 | t.Fatalf("actual %s expected %s", err.Error(), message) 36 | } 37 | } 38 | 39 | func TestCheckErrorWithDefaultLocalizedCode(t *testing.T) { 40 | code := "TEST" 41 | message := "Test message" 42 | 43 | locales.EnUSMessages[code] = message 44 | 45 | err := v2.NewCheckError(code) 46 | 47 | if err.Error() != message { 48 | t.Fatalf("actual %s expected %s", err.Error(), message) 49 | } 50 | } 51 | 52 | func TestCheckErrorWithDataAndLocalizedCode(t *testing.T) { 53 | code := "TEST" 54 | message := "Test message {{.Name}}" 55 | 56 | locales.EnUSMessages[code] = message 57 | 58 | err := v2.NewCheckErrorWithData(code, map[string]interface{}{ 59 | "Name": "Onur", 60 | }) 61 | 62 | expected := "Test message Onur" 63 | 64 | if err.Error() != expected { 65 | t.Fatalf("actual %s expected %s", err.Error(), expected) 66 | } 67 | } 68 | 69 | func TestCheckErrorWithLocalizedCodeInvalidTemplate(t *testing.T) { 70 | code := "TEST" 71 | message := "Test message {{}" 72 | 73 | locales.EnUSMessages[code] = message 74 | 75 | err := v2.NewCheckError(code) 76 | 77 | if err.Error() != code { 78 | t.Fatalf("actual %s expected %s", err.Error(), code) 79 | } 80 | } 81 | 82 | func TestCheckErrorWithLocalizedCodeInvalidExecute(t *testing.T) { 83 | code := "TEST" 84 | message := "{{ len .Name}}" 85 | 86 | locales.EnUSMessages[code] = message 87 | 88 | err := v2.NewCheckError(code) 89 | 90 | if err.Error() != code { 91 | t.Fatalf("actual %s expected %s", err.Error(), code) 92 | } 93 | } 94 | 95 | func TestCheckErrorIsSuccess(t *testing.T) { 96 | code := "TEST" 97 | 98 | err1 := v2.NewCheckError(code) 99 | err2 := v2.NewCheckError(code) 100 | 101 | if !err1.Is(err2) { 102 | t.Fatalf("actual %t expected %t", err1.Is(err2), true) 103 | } 104 | } 105 | 106 | func TestCheckErrorIsFailure(t *testing.T) { 107 | code1 := "TEST1" 108 | code2 := "TEST2" 109 | 110 | err1 := v2.NewCheckError(code1) 111 | err2 := v2.NewCheckError(code2) 112 | 113 | if err1.Is(err2) { 114 | t.Fatalf("actual %t expected %t", err1.Is(err2), false) 115 | } 116 | } 117 | 118 | func TestCheckErrorIsFailureWithDifferentType(t *testing.T) { 119 | code := "TEST" 120 | 121 | err1 := v2.NewCheckError(code) 122 | err2 := fs.ErrExist 123 | 124 | if err1.Is(err2) { 125 | t.Fatalf("actual %t expected %t", err1.Is(err2), false) 126 | } 127 | } 128 | 129 | func TestRegisterLocale(t *testing.T) { 130 | locale := "de-DE" 131 | code := "TEST" 132 | message := "Testmeldung" 133 | 134 | v2.RegisterLocale(locale, map[string]string{ 135 | code: message, 136 | }) 137 | 138 | err := v2.NewCheckError(code) 139 | 140 | if err.ErrorWithLocale("de-DE") != message { 141 | t.Fatalf("actual %s expected %s", err.Error(), message) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /check_func.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | // CheckFunc is a function that takes a value of type T and performs 9 | // a check on it. It returns the resulting value and any error that 10 | // occurred during the check. 11 | type CheckFunc[T any] func(value T) (T, error) 12 | -------------------------------------------------------------------------------- /checker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | // Package v2 Checker is a Go library for validating user input through checker rules provided in struct tags. 7 | package v2 8 | 9 | import ( 10 | "fmt" 11 | "reflect" 12 | "strings" 13 | ) 14 | 15 | const ( 16 | // checkerTag is the name of the field tag used for checker. 17 | checkerTag = "checkers" 18 | 19 | // sliceConfigPrefix is the prefix used to distinguish slice-level checks from item-level checks. 20 | sliceConfigPrefix = "@" 21 | ) 22 | 23 | // checkStructJob defines a check strcut job. 24 | type checkStructJob struct { 25 | Name string 26 | Value reflect.Value 27 | Config string 28 | } 29 | 30 | // Check applies the given check functions to a value sequentially. 31 | // It returns the final value and the first encountered error, if any. 32 | func Check[T any](value T, checks ...CheckFunc[T]) (T, error) { 33 | var err error 34 | 35 | for _, check := range checks { 36 | value, err = check(value) 37 | if err != nil { 38 | break 39 | } 40 | } 41 | 42 | return value, err 43 | } 44 | 45 | // CheckWithConfig applies the check functions specified by the config string to the given value. 46 | // It returns the modified value and the first encountered error, if any. 47 | func CheckWithConfig[T any](value T, config string) (T, error) { 48 | newValue, err := ReflectCheckWithConfig(reflect.Indirect(reflect.ValueOf(value)), config) 49 | return newValue.Interface().(T), err 50 | } 51 | 52 | // ReflectCheckWithConfig applies the check functions specified by the config string 53 | // to the given reflect.Value. It returns the modified reflect.Value and the first 54 | // encountered error, if any. 55 | func ReflectCheckWithConfig(value reflect.Value, config string) (reflect.Value, error) { 56 | return Check(value, makeChecks(config)...) 57 | } 58 | 59 | // CheckStruct checks the given struct based on the validation rules specified in the 60 | // "checker" tag of each struct field. It returns a map of field names to their 61 | // corresponding errors, and a boolean indicating if all checks passed. 62 | func CheckStruct(st any) (map[string]error, bool) { 63 | errs := make(map[string]error) 64 | 65 | jobs := []*checkStructJob{ 66 | { 67 | Name: "", 68 | Value: reflect.Indirect(reflect.ValueOf(st)), 69 | }, 70 | } 71 | 72 | for len(jobs) > 0 { 73 | job := jobs[0] 74 | jobs = jobs[1:] 75 | 76 | switch job.Value.Kind() { 77 | case reflect.Struct: 78 | for i := 0; i < job.Value.NumField(); i++ { 79 | field := job.Value.Type().Field(i) 80 | 81 | name := fieldName(job.Name, field) 82 | value := reflect.Indirect(job.Value.FieldByIndex(field.Index)) 83 | 84 | jobs = append(jobs, &checkStructJob{ 85 | Name: name, 86 | Value: value, 87 | Config: field.Tag.Get(checkerTag), 88 | }) 89 | } 90 | 91 | case reflect.Slice: 92 | sliceConfig, itemConfig := splitSliceConfig(job.Config) 93 | job.Config = sliceConfig 94 | 95 | for i := 0; i < job.Value.Len(); i++ { 96 | name := fmt.Sprintf("%s[%d]", job.Name, i) 97 | value := reflect.Indirect(job.Value.Index(i)) 98 | 99 | jobs = append(jobs, &checkStructJob{ 100 | Name: name, 101 | Value: value, 102 | Config: itemConfig, 103 | }) 104 | } 105 | } 106 | 107 | if job.Config != "" { 108 | newValue, err := ReflectCheckWithConfig(job.Value, job.Config) 109 | if err != nil { 110 | errs[job.Name] = err 111 | } 112 | 113 | job.Value.Set(newValue) 114 | } 115 | } 116 | 117 | return errs, len(errs) == 0 118 | } 119 | 120 | // fieldName returns the field name. If a "json" tag is present, it uses the 121 | // tag value instead. It also prepends the parent struct's name (if any) to 122 | // create a fully qualified field name. 123 | func fieldName(prefix string, field reflect.StructField) string { 124 | // Default to field name 125 | name := field.Name 126 | 127 | // Use json tag if present 128 | if jsonTag, ok := field.Tag.Lookup("json"); ok { 129 | name = jsonTag 130 | } 131 | 132 | // Prepend parent name 133 | if prefix != "" { 134 | name = prefix + "." + name 135 | } 136 | 137 | return name 138 | } 139 | 140 | // splitSliceConfig splits config string into slice and item-level configurations. 141 | func splitSliceConfig(config string) (string, string) { 142 | sliceFileds := make([]string, 0) 143 | itemFields := make([]string, 0) 144 | 145 | for _, configField := range strings.Fields(config) { 146 | if strings.HasPrefix(configField, sliceConfigPrefix) { 147 | sliceFileds = append(sliceFileds, strings.TrimPrefix(configField, sliceConfigPrefix)) 148 | } else { 149 | itemFields = append(itemFields, configField) 150 | } 151 | } 152 | 153 | return strings.Join(sliceFileds, " "), strings.Join(itemFields, " ") 154 | } 155 | -------------------------------------------------------------------------------- /checker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "testing" 12 | 13 | v2 "github.com/cinar/checker/v2" 14 | ) 15 | 16 | func ExampleCheck() { 17 | name := " Onur Cinar " 18 | 19 | name, err := v2.Check(name, v2.TrimSpace, v2.Required) 20 | if err != nil { 21 | fmt.Println(err) 22 | return 23 | } 24 | 25 | fmt.Println(name) 26 | // Output: Onur Cinar 27 | } 28 | 29 | func ExampleCheckStruct() { 30 | type Person struct { 31 | Name string `checkers:"trim required"` 32 | } 33 | 34 | person := &Person{ 35 | Name: " Onur Cinar ", 36 | } 37 | 38 | errs, ok := v2.CheckStruct(person) 39 | if !ok { 40 | fmt.Println(errs) 41 | return 42 | } 43 | 44 | fmt.Println(person.Name) 45 | // Output: Onur Cinar 46 | } 47 | 48 | func TestCheckTrimSpaceRequiredSuccess(t *testing.T) { 49 | input := " test " 50 | expected := "test" 51 | 52 | actual, err := v2.Check(input, v2.TrimSpace, v2.Required) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | if actual != expected { 58 | t.Fatalf("actual %s expected %s", actual, expected) 59 | } 60 | } 61 | 62 | func TestCheckTrimSpaceRequiredMissing(t *testing.T) { 63 | input := " " 64 | expected := "" 65 | 66 | actual, err := v2.Check(input, v2.TrimSpace, v2.Required) 67 | if !errors.Is(err, v2.ErrRequired) { 68 | t.Fatalf("got unexpected error %v", err) 69 | } 70 | 71 | if actual != expected { 72 | t.Fatalf("actual %s expected %s", actual, expected) 73 | } 74 | } 75 | 76 | func TestCheckWithConfigSuccess(t *testing.T) { 77 | input := " test " 78 | expected := "test" 79 | 80 | actual, err := v2.CheckWithConfig(input, "trim required") 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | if actual != expected { 86 | t.Fatalf("actual %s expected %s", actual, expected) 87 | } 88 | } 89 | 90 | func TestCheckWithConfigRequiredMissing(t *testing.T) { 91 | input := " " 92 | expected := "" 93 | 94 | actual, err := v2.CheckWithConfig(input, "trim required") 95 | if !errors.Is(err, v2.ErrRequired) { 96 | t.Fatalf("got unexpected error %v", err) 97 | } 98 | 99 | if actual != expected { 100 | t.Fatalf("actual %s expected %s", actual, expected) 101 | } 102 | } 103 | 104 | func TestCheckStructSuccess(t *testing.T) { 105 | type Address struct { 106 | Street string `checkers:"required"` 107 | } 108 | 109 | type Person struct { 110 | Name string `checkers:"required"` 111 | Address *Address 112 | } 113 | 114 | person := &Person{ 115 | Name: "Onur Cinar", 116 | Address: &Address{ 117 | Street: "1234 Main", 118 | }, 119 | } 120 | 121 | errors, ok := v2.CheckStruct(person) 122 | if !ok { 123 | t.Fatalf("got unexpected errors %v", errors) 124 | } 125 | } 126 | 127 | func TestCheckStructRequiredMissing(t *testing.T) { 128 | type Address struct { 129 | Street string `checkers:"required"` 130 | } 131 | 132 | type Person struct { 133 | Name string `checkers:"required"` 134 | Address *Address 135 | } 136 | 137 | person := &Person{ 138 | Name: "", 139 | Address: &Address{ 140 | Street: "", 141 | }, 142 | } 143 | 144 | errs, ok := v2.CheckStruct(person) 145 | if ok { 146 | t.Fatal("expected errors") 147 | } 148 | 149 | if !errors.Is(errs["Name"], v2.ErrRequired) { 150 | t.Fatalf("expected name required %v", errs) 151 | } 152 | 153 | if !errors.Is(errs["Address.Street"], v2.ErrRequired) { 154 | t.Fatalf("expected streed required %v", errs) 155 | } 156 | } 157 | 158 | func TestCheckStructCustomName(t *testing.T) { 159 | type Person struct { 160 | Name string `json:"name" checkers:"required"` 161 | } 162 | 163 | person := &Person{ 164 | Name: "", 165 | } 166 | 167 | errs, ok := v2.CheckStruct(person) 168 | if ok { 169 | t.Fatal("expected errors") 170 | } 171 | 172 | if !errors.Is(errs["name"], v2.ErrRequired) { 173 | t.Fatalf("expected name required %v", errs) 174 | } 175 | } 176 | 177 | func TestCheckStructSlice(t *testing.T) { 178 | type Person struct { 179 | Name string `checkers:"required"` 180 | Emails []string `checkers:"@max-len:1 max-len:4"` 181 | } 182 | 183 | person := &Person{ 184 | Name: "Onur Cinar", 185 | Emails: []string{ 186 | "onur.cinar", 187 | }, 188 | } 189 | 190 | errs, ok := v2.CheckStruct(person) 191 | if ok { 192 | t.Fatal("expected errors") 193 | } 194 | 195 | if !errors.Is(errs["Emails[0]"], v2.ErrMaxLen) { 196 | t.Fatalf("expected email max len") 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /cidr.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "net" 10 | "reflect" 11 | ) 12 | 13 | const ( 14 | // nameCIDR is the name of the CIDR check. 15 | nameCIDR = "cidr" 16 | ) 17 | 18 | var ( 19 | // ErrNotCIDR indicates that the given value is not a valid CIDR. 20 | ErrNotCIDR = NewCheckError("NOT_CIDR") 21 | ) 22 | 23 | // IsCIDR checks if the value is a valid CIDR notation IP address and prefix length. 24 | func IsCIDR(value string) (string, error) { 25 | _, _, err := net.ParseCIDR(value) 26 | if err != nil { 27 | return value, ErrNotCIDR 28 | } 29 | 30 | return value, nil 31 | } 32 | 33 | // checkCIDR checks if the value is a valid CIDR notation IP address and prefix length. 34 | func checkCIDR(value reflect.Value) (reflect.Value, error) { 35 | _, err := IsCIDR(value.Interface().(string)) 36 | return value, err 37 | } 38 | 39 | // makeCIDR makes a checker function for the CIDR checker. 40 | func makeCIDR(_ string) CheckFunc[reflect.Value] { 41 | return checkCIDR 42 | } 43 | -------------------------------------------------------------------------------- /cidr_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsCIDR() { 16 | _, err := v2.IsCIDR("2001:db8::/32") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsCIDRInvalid(t *testing.T) { 23 | _, err := v2.IsCIDR("900.800.200.100//24") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsCIDRValid(t *testing.T) { 30 | _, err := v2.IsCIDR("2001:db8::/32") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckCIDRNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Network struct { 40 | Subnet int `checkers:"cidr"` 41 | } 42 | 43 | network := &Network{} 44 | 45 | v2.CheckStruct(network) 46 | } 47 | 48 | func TestCheckCIDRInvalid(t *testing.T) { 49 | type Network struct { 50 | Subnet string `checkers:"cidr"` 51 | } 52 | 53 | network := &Network{ 54 | Subnet: "900.800.200.100//24", 55 | } 56 | 57 | _, ok := v2.CheckStruct(network) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckCIDRValid(t *testing.T) { 64 | type Network struct { 65 | Subnet string `checkers:"cidr"` 66 | } 67 | 68 | network := &Network{ 69 | Subnet: "192.0.2.0/24", 70 | } 71 | 72 | _, ok := v2.CheckStruct(network) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /credit_card.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | // nameCreditCard is the name of the credit card check. 16 | nameCreditCard = "credit-card" 17 | ) 18 | 19 | var ( 20 | // ErrNotCreditCard indicates that the given value is not a valid credit card number. 21 | ErrNotCreditCard = NewCheckError("NOT_CREDIT_CARD") 22 | 23 | // amexExpression is the regexp for the AMEX cards. They start with 34 or 37, and has 15 digits. 24 | amexExpression = "(?:^(?:3[47])[0-9]{13}$)" 25 | amexPattern = regexp.MustCompile(amexExpression) 26 | 27 | // dinersExpression is the regexp for the Diners cards. They start with 305, 36, 38, and has 14 digits. 28 | dinersExpression = "(?:^3(?:(?:05[0-9]{11})|(?:[68][0-9]{12}))$)" 29 | dinersPattern = regexp.MustCompile(dinersExpression) 30 | 31 | // discoverExpression is the regexp for the Discover cards. They start with 6011 and has 16 digits. 32 | discoverExpression = "(?:^6011[0-9]{12}$)" 33 | discoverPattern = regexp.MustCompile(discoverExpression) 34 | 35 | // jcbExpression is the regexp for the JCB 15 cards. They start with 2131, 1800, and has 15 digits, or start with 35 and has 16 digits. 36 | jcbExpression = "(?:^(?:(?:2131)|(?:1800)|(?:35[0-9]{3}))[0-9]{11}$)" 37 | jcbPattern = regexp.MustCompile(jcbExpression) 38 | 39 | // masterCardExpression is the regexp for the MasterCard cards. They start with 51, 52, 53, 54, or 55, and has 15 digits. 40 | masterCardExpression = "(?:^5[12345][0-9]{14}$)" 41 | masterCardPattern = regexp.MustCompile(masterCardExpression) 42 | 43 | // unionPayExpression is the regexp for the UnionPay cards. They start either with 62 or 67, and has 16 digits, or they start with 81 and has 16 to 19 digits. 44 | unionPayExpression = "(?:(?:6[27][0-9]{14})|(?:81[0-9]{14,17})^$)" 45 | unionPayPattern = regexp.MustCompile(unionPayExpression) 46 | 47 | // visaExpression is the regexp for the Visa cards. They start with 4 and has 13 or 16 digits. 48 | visaExpression = "(?:^4[0-9]{12}(?:[0-9]{3})?$)" 49 | visaPattern = regexp.MustCompile(visaExpression) 50 | 51 | // anyCreditCardPattern is the regexp for any credit card. 52 | anyCreditCardPattern = regexp.MustCompile(strings.Join([]string{ 53 | amexExpression, 54 | dinersExpression, 55 | discoverExpression, 56 | jcbExpression, 57 | masterCardExpression, 58 | unionPayExpression, 59 | visaExpression, 60 | }, "|")) 61 | 62 | // creditCardPatterns is the mapping for credit card names to patterns. 63 | creditCardPatterns = map[string]*regexp.Regexp{ 64 | "amex": amexPattern, 65 | "diners": dinersPattern, 66 | "discover": discoverPattern, 67 | "jcb": jcbPattern, 68 | "mastercard": masterCardPattern, 69 | "unionpay": unionPayPattern, 70 | "visa": visaPattern, 71 | } 72 | ) 73 | 74 | // IsAnyCreditCard checks if the given value is a valid credit card number. 75 | func IsAnyCreditCard(number string) (string, error) { 76 | return isCreditCard(number, anyCreditCardPattern) 77 | } 78 | 79 | // IsAmexCreditCard checks if the given valie is a valid AMEX credit card. 80 | func IsAmexCreditCard(number string) (string, error) { 81 | return isCreditCard(number, amexPattern) 82 | } 83 | 84 | // IsDinersCreditCard checks if the given valie is a valid Diners credit card. 85 | func IsDinersCreditCard(number string) (string, error) { 86 | return isCreditCard(number, dinersPattern) 87 | } 88 | 89 | // IsDiscoverCreditCard checks if the given valie is a valid Discover credit card. 90 | func IsDiscoverCreditCard(number string) (string, error) { 91 | return isCreditCard(number, discoverPattern) 92 | } 93 | 94 | // IsJcbCreditCard checks if the given valie is a valid JCB 15 credit card. 95 | func IsJcbCreditCard(number string) (string, error) { 96 | return isCreditCard(number, jcbPattern) 97 | } 98 | 99 | // IsMasterCardCreditCard checks if the given valie is a valid MasterCard credit card. 100 | func IsMasterCardCreditCard(number string) (string, error) { 101 | return isCreditCard(number, masterCardPattern) 102 | } 103 | 104 | // IsUnionPayCreditCard checks if the given valie is a valid UnionPay credit card. 105 | func IsUnionPayCreditCard(number string) (string, error) { 106 | return isCreditCard(number, unionPayPattern) 107 | } 108 | 109 | // IsVisaCreditCard checks if the given valie is a valid Visa credit card. 110 | func IsVisaCreditCard(number string) (string, error) { 111 | return isCreditCard(number, visaPattern) 112 | } 113 | 114 | // makeCreditCard makes a checker function for the credit card checker. 115 | func makeCreditCard(config string) CheckFunc[reflect.Value] { 116 | patterns := []*regexp.Regexp{} 117 | 118 | if config != "" { 119 | for _, card := range strings.Split(config, ",") { 120 | pattern, ok := creditCardPatterns[card] 121 | if !ok { 122 | panic("unknown credit card name") 123 | } 124 | 125 | patterns = append(patterns, pattern) 126 | } 127 | } else { 128 | patterns = append(patterns, anyCreditCardPattern) 129 | } 130 | 131 | return func(value reflect.Value) (reflect.Value, error) { 132 | if value.Kind() != reflect.String { 133 | panic("string expected") 134 | } 135 | 136 | number := value.String() 137 | 138 | for _, pattern := range patterns { 139 | _, err := isCreditCard(number, pattern) 140 | if err == nil { 141 | return value, nil 142 | } 143 | } 144 | 145 | return value, ErrNotCreditCard 146 | } 147 | } 148 | 149 | // isCreditCard checks if the given number based on the given credit card pattern and the Luhn algorithm check digit. 150 | func isCreditCard(number string, pattern *regexp.Regexp) (string, error) { 151 | if !pattern.MatchString(number) { 152 | return number, ErrNotCreditCard 153 | } 154 | 155 | return IsLUHN(number) 156 | } 157 | -------------------------------------------------------------------------------- /credit_card_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "strconv" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | // Test numbers from https://stripe.com/docs/testing 16 | var invalidCard = "1234123412341234" 17 | var amexCard = "378282246310005" 18 | var dinersCard = "36227206271667" 19 | var discoverCard = "6011111111111117" 20 | var jcbCard = "3530111333300000" 21 | var masterCard = "5555555555554444" 22 | var unionPayCard = "6200000000000005" 23 | var visaCard = "4111111111111111" 24 | 25 | // changeToInvalidLuhn increments the luhn digit to make the number invalid. It assumes that the given number is valid. 26 | func changeToInvalidLuhn(number string) string { 27 | luhn, err := strconv.Atoi(number[len(number)-1:]) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | luhn = (luhn + 1) % 10 33 | 34 | return number[:len(number)-1] + strconv.Itoa(luhn) 35 | } 36 | 37 | func ExampleIsAnyCreditCard() { 38 | _, err := v2.IsAnyCreditCard("6011111111111117") 39 | 40 | if err != nil { 41 | // Send the errors back to the user 42 | } 43 | } 44 | 45 | func TestIsAnyCreditCardValid(t *testing.T) { 46 | _, err := v2.IsAnyCreditCard(amexCard) 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | } 51 | 52 | func TestIsAnyCreditCardInvalidPattern(t *testing.T) { 53 | if _, err := v2.IsAnyCreditCard(invalidCard); err == nil { 54 | t.Error("expected error for invalid card pattern") 55 | } 56 | } 57 | 58 | func TestIsAnyCreditCardInvalidLuhn(t *testing.T) { 59 | if _, err := v2.IsAnyCreditCard(changeToInvalidLuhn(amexCard)); err == nil { 60 | t.Error("expected error for invalid Luhn") 61 | } 62 | } 63 | 64 | func ExampleIsAmexCreditCard() { 65 | _, err := v2.IsAmexCreditCard("378282246310005") 66 | 67 | if err != nil { 68 | // Send the errors back to the user 69 | } 70 | } 71 | 72 | func TestIsAmexCreditCardValid(t *testing.T) { 73 | if _, err := v2.IsAmexCreditCard(amexCard); err != nil { 74 | t.Error(err) 75 | } 76 | } 77 | 78 | func TestIsAmexCreditCardInvalidPattern(t *testing.T) { 79 | if _, err := v2.IsAmexCreditCard(invalidCard); err == nil { 80 | t.Error("expected error for invalid card pattern") 81 | } 82 | } 83 | 84 | func TestIsAmexCreditCardInvalidLuhn(t *testing.T) { 85 | if _, err := v2.IsAmexCreditCard(changeToInvalidLuhn(amexCard)); err == nil { 86 | t.Error("expected error for invalid Luhn") 87 | } 88 | } 89 | 90 | func ExampleIsDinersCreditCard() { 91 | _, err := v2.IsDinersCreditCard("36227206271667") 92 | 93 | if err != nil { 94 | // Send the errors back to the user 95 | } 96 | } 97 | func TestIsDinersCreditCardValid(t *testing.T) { 98 | if _, err := v2.IsDinersCreditCard(dinersCard); err != nil { 99 | t.Error(err) 100 | } 101 | } 102 | 103 | func TestIsDinersCreditCardInvalidPattern(t *testing.T) { 104 | if _, err := v2.IsDinersCreditCard(invalidCard); err == nil { 105 | t.Error("expected error for invalid card pattern") 106 | } 107 | } 108 | 109 | func TestIsDinersCreditCardInvalidLuhn(t *testing.T) { 110 | if _, err := v2.IsDinersCreditCard(changeToInvalidLuhn(dinersCard)); err == nil { 111 | t.Error("expected error for invalid Luhn") 112 | } 113 | } 114 | 115 | func ExampleIsDiscoverCreditCard() { 116 | _, err := v2.IsDiscoverCreditCard("6011111111111117") 117 | 118 | if err != nil { 119 | // Send the errors back to the user 120 | } 121 | } 122 | func TestIsDiscoverCreditCardValid(t *testing.T) { 123 | if _, err := v2.IsDiscoverCreditCard(discoverCard); err != nil { 124 | t.Error(err) 125 | } 126 | } 127 | 128 | func TestIsDiscoverCreditCardInvalidPattern(t *testing.T) { 129 | if _, err := v2.IsDiscoverCreditCard(invalidCard); err == nil { 130 | t.Error("expected error for invalid card pattern") 131 | } 132 | } 133 | 134 | func TestIsDiscoverCreditCardInvalidLuhn(t *testing.T) { 135 | if _, err := v2.IsDiscoverCreditCard(changeToInvalidLuhn(discoverCard)); err == nil { 136 | t.Error("expected error for invalid Luhn") 137 | } 138 | } 139 | 140 | func ExampleIsJcbCreditCard() { 141 | _, err := v2.IsJcbCreditCard("3530111333300000") 142 | 143 | if err != nil { 144 | // Send the errors back to the user 145 | } 146 | } 147 | 148 | func TestIsJcbCreditCardValid(t *testing.T) { 149 | if _, err := v2.IsJcbCreditCard(jcbCard); err != nil { 150 | t.Error(err) 151 | } 152 | } 153 | 154 | func TestIsJcbCreditCardInvalidPattern(t *testing.T) { 155 | if _, err := v2.IsJcbCreditCard(invalidCard); err == nil { 156 | t.Error("expected error for invalid card pattern") 157 | } 158 | } 159 | 160 | func TestIsJcbCreditCardInvalidLuhn(t *testing.T) { 161 | if _, err := v2.IsJcbCreditCard(changeToInvalidLuhn(jcbCard)); err == nil { 162 | t.Error("expected error for invalid Luhn") 163 | } 164 | } 165 | 166 | func ExampleIsMasterCardCreditCard() { 167 | _, err := v2.IsMasterCardCreditCard("5555555555554444") 168 | 169 | if err != nil { 170 | // Send the errors back to the user 171 | } 172 | } 173 | 174 | func TestIsMasterCardCreditCardValid(t *testing.T) { 175 | if _, err := v2.IsMasterCardCreditCard(masterCard); err != nil { 176 | t.Error(err) 177 | } 178 | } 179 | 180 | func TestIsMasterCardCreditCardInvalidPattern(t *testing.T) { 181 | if _, err := v2.IsMasterCardCreditCard(invalidCard); err == nil { 182 | t.Error("expected error for invalid card pattern") 183 | } 184 | } 185 | 186 | func TestIsMasterCardCreditCardInvalidLuhn(t *testing.T) { 187 | if _, err := v2.IsMasterCardCreditCard(changeToInvalidLuhn(masterCard)); err == nil { 188 | t.Error("expected error for invalid Luhn") 189 | } 190 | } 191 | 192 | func ExampleIsUnionPayCreditCard() { 193 | _, err := v2.IsUnionPayCreditCard("6200000000000005") 194 | 195 | if err != nil { 196 | // Send the errors back to the user 197 | } 198 | } 199 | 200 | func TestIsUnionPayCreditCardValid(t *testing.T) { 201 | if _, err := v2.IsUnionPayCreditCard(unionPayCard); err != nil { 202 | t.Error(err) 203 | } 204 | } 205 | 206 | func TestIsUnionPayCreditCardInvalidPattern(t *testing.T) { 207 | if _, err := v2.IsUnionPayCreditCard(invalidCard); err == nil { 208 | t.Error("expected error for invalid card pattern") 209 | } 210 | } 211 | 212 | func TestIsUnionPayCreditCardInvalidLuhn(t *testing.T) { 213 | if _, err := v2.IsUnionPayCreditCard(changeToInvalidLuhn(unionPayCard)); err == nil { 214 | t.Error("expected error for invalid Luhn") 215 | } 216 | } 217 | 218 | func ExampleIsVisaCreditCard() { 219 | _, err := v2.IsVisaCreditCard("4111111111111111") 220 | 221 | if err != nil { 222 | // Send the errors back to the user 223 | } 224 | } 225 | func TestIsVisaCreditCardValid(t *testing.T) { 226 | if _, err := v2.IsVisaCreditCard(visaCard); err != nil { 227 | t.Error(err) 228 | } 229 | } 230 | 231 | func TestIsVisaCreditCardInvalidPattern(t *testing.T) { 232 | if _, err := v2.IsVisaCreditCard(invalidCard); err == nil { 233 | t.Error("expected error for invalid card pattern") 234 | } 235 | } 236 | 237 | func TestIsVisaCreditCardInvalidLuhn(t *testing.T) { 238 | if _, err := v2.IsVisaCreditCard(changeToInvalidLuhn(visaCard)); err == nil { 239 | t.Error("expected error for invalid Luhn") 240 | } 241 | } 242 | 243 | func TestCheckCreditCardNonString(t *testing.T) { 244 | defer FailIfNoPanic(t, "expected panic for non-string credit card") 245 | 246 | type Order struct { 247 | CreditCard int `checkers:"credit-card"` 248 | } 249 | 250 | order := &Order{} 251 | 252 | v2.CheckStruct(order) 253 | } 254 | 255 | func TestCheckCreditCardValid(t *testing.T) { 256 | type Order struct { 257 | CreditCard string `checkers:"credit-card"` 258 | } 259 | 260 | order := &Order{ 261 | CreditCard: amexCard, 262 | } 263 | 264 | _, valid := v2.CheckStruct(order) 265 | if !valid { 266 | t.Fail() 267 | } 268 | } 269 | 270 | func TestCheckCreditCardInvalid(t *testing.T) { 271 | type Order struct { 272 | CreditCard string `checkers:"credit-card"` 273 | } 274 | 275 | order := &Order{ 276 | CreditCard: invalidCard, 277 | } 278 | 279 | _, valid := v2.CheckStruct(order) 280 | if valid { 281 | t.Fail() 282 | } 283 | } 284 | 285 | func TestCheckCreditCardMultipleUnknown(t *testing.T) { 286 | defer FailIfNoPanic(t, "expected panic for unknown credit card") 287 | 288 | type Order struct { 289 | CreditCard string `checkers:"credit-card:amex,unknown"` 290 | } 291 | 292 | order := &Order{ 293 | CreditCard: amexCard, 294 | } 295 | 296 | v2.CheckStruct(order) 297 | } 298 | 299 | func TestCheckCreditCardMultipleInvalid(t *testing.T) { 300 | type Order struct { 301 | CreditCard string `checkers:"credit-card:amex,visa"` 302 | } 303 | 304 | order := &Order{ 305 | CreditCard: discoverCard, 306 | } 307 | 308 | _, valid := v2.CheckStruct(order) 309 | if valid { 310 | t.Fail() 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /digits.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "unicode" 11 | ) 12 | 13 | const ( 14 | // nameDigits is the name of the digits check. 15 | nameDigits = "digits" 16 | ) 17 | 18 | var ( 19 | // ErrNotDigits indicates that the given value is not a valid digits string. 20 | ErrNotDigits = NewCheckError("NOT_DIGITS") 21 | ) 22 | 23 | // IsDigits checks if the value contains only digit characters. 24 | func IsDigits(value string) (string, error) { 25 | for _, r := range value { 26 | if !unicode.IsDigit(r) { 27 | return value, ErrNotDigits 28 | } 29 | } 30 | return value, nil 31 | } 32 | 33 | // checkDigits checks if the value contains only digit characters. 34 | func checkDigits(value reflect.Value) (reflect.Value, error) { 35 | _, err := IsDigits(value.Interface().(string)) 36 | return value, err 37 | } 38 | 39 | // makeDigits makes a checker function for the digits checker. 40 | func makeDigits(_ string) CheckFunc[reflect.Value] { 41 | return checkDigits 42 | } 43 | -------------------------------------------------------------------------------- /digits_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsDigits() { 16 | _, err := v2.IsDigits("123456") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsDigitsInvalid(t *testing.T) { 23 | _, err := v2.IsDigits("123a456") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsDigitsValid(t *testing.T) { 30 | _, err := v2.IsDigits("123456") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckDigitsNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Code struct { 40 | Value int `checkers:"digits"` 41 | } 42 | 43 | code := &Code{} 44 | 45 | v2.CheckStruct(code) 46 | } 47 | 48 | func TestCheckDigitsInvalid(t *testing.T) { 49 | type Code struct { 50 | Value string `checkers:"digits"` 51 | } 52 | 53 | code := &Code{ 54 | Value: "123a456", 55 | } 56 | 57 | _, ok := v2.CheckStruct(code) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckDigitsValid(t *testing.T) { 64 | type Code struct { 65 | Value string `checkers:"digits"` 66 | } 67 | 68 | code := &Code{ 69 | Value: "123456", 70 | } 71 | 72 | _, ok := v2.CheckStruct(code) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /email.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "net/mail" 10 | "reflect" 11 | ) 12 | 13 | const ( 14 | // nameEmail is the name of the email check. 15 | nameEmail = "email" 16 | ) 17 | 18 | var ( 19 | // ErrNotEmail indicates that the given value is not a valid email address. 20 | ErrNotEmail = NewCheckError("NOT_EMAIL") 21 | ) 22 | 23 | // IsEmail checks if the value is a valid email address. 24 | func IsEmail(value string) (string, error) { 25 | _, err := mail.ParseAddress(value) 26 | if err != nil { 27 | return value, ErrNotEmail 28 | } 29 | return value, nil 30 | } 31 | 32 | // checkEmail checks if the value is a valid email address. 33 | func checkEmail(value reflect.Value) (reflect.Value, error) { 34 | _, err := IsEmail(value.Interface().(string)) 35 | return value, err 36 | } 37 | 38 | // makeEmail makes a checker function for the email checker. 39 | func makeEmail(_ string) CheckFunc[reflect.Value] { 40 | return checkEmail 41 | } 42 | -------------------------------------------------------------------------------- /email_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsEmail() { 16 | _, err := v2.IsEmail("test@example.com") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsEmailInvalid(t *testing.T) { 23 | _, err := v2.IsEmail("invalid-email") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsEmailValid(t *testing.T) { 30 | _, err := v2.IsEmail("test@example.com") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckEmailNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type User struct { 40 | Email int `checkers:"email"` 41 | } 42 | 43 | user := &User{} 44 | 45 | v2.CheckStruct(user) 46 | } 47 | 48 | func TestCheckEmailInvalid(t *testing.T) { 49 | type User struct { 50 | Email string `checkers:"email"` 51 | } 52 | 53 | user := &User{ 54 | Email: "invalid-email", 55 | } 56 | 57 | _, ok := v2.CheckStruct(user) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckEmailValid(t *testing.T) { 64 | type User struct { 65 | Email string `checkers:"email"` 66 | } 67 | 68 | user := &User{ 69 | Email: "test@example.com", 70 | } 71 | 72 | _, ok := v2.CheckStruct(user) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /fqdn.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "regexp" 11 | ) 12 | 13 | const ( 14 | // nameFQDN is the name of the FQDN check. 15 | nameFQDN = "fqdn" 16 | ) 17 | 18 | var ( 19 | // ErrNotFQDN indicates that the given value is not a valid FQDN. 20 | ErrNotFQDN = NewCheckError("FQDN") 21 | 22 | // fqdnRegex is the regular expression for validating FQDN. 23 | fqdnRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`) 24 | ) 25 | 26 | // IsFQDN checks if the value is a valid fully qualified domain name (FQDN). 27 | func IsFQDN(value string) (string, error) { 28 | if !fqdnRegex.MatchString(value) { 29 | return value, ErrNotFQDN 30 | } 31 | return value, nil 32 | } 33 | 34 | // checkFQDN checks if the value is a valid fully qualified domain name (FQDN). 35 | func checkFQDN(value reflect.Value) (reflect.Value, error) { 36 | _, err := IsFQDN(value.Interface().(string)) 37 | return value, err 38 | } 39 | 40 | // makeFQDN makes a checker function for the FQDN checker. 41 | func makeFQDN(_ string) CheckFunc[reflect.Value] { 42 | return checkFQDN 43 | } -------------------------------------------------------------------------------- /fqdn_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsFQDN() { 16 | _, err := v2.IsFQDN("example.com") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsFQDNInvalid(t *testing.T) { 23 | _, err := v2.IsFQDN("invalid_fqdn") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsFQDNValid(t *testing.T) { 30 | _, err := v2.IsFQDN("example.com") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckFQDNNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Domain struct { 40 | Name int `checkers:"fqdn"` 41 | } 42 | 43 | domain := &Domain{} 44 | 45 | v2.CheckStruct(domain) 46 | } 47 | 48 | func TestCheckFQDNInvalid(t *testing.T) { 49 | type Domain struct { 50 | Name string `checkers:"fqdn"` 51 | } 52 | 53 | domain := &Domain{ 54 | Name: "invalid_fqdn", 55 | } 56 | 57 | _, ok := v2.CheckStruct(domain) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckFQDNValid(t *testing.T) { 64 | type Domain struct { 65 | Name string `checkers:"fqdn"` 66 | } 67 | 68 | domain := &Domain{ 69 | Name: "example.com", 70 | } 71 | 72 | _, ok := v2.CheckStruct(domain) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cinar/checker/v2 2 | 3 | go 1.23.2 4 | -------------------------------------------------------------------------------- /gte.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "cmp" 10 | "reflect" 11 | "strconv" 12 | ) 13 | 14 | const ( 15 | // nameGte is the name of the greater than or equal to check. 16 | nameGte = "gte" 17 | ) 18 | 19 | var ( 20 | // ErrGte indicates that the value is not greater than or equal to the given value. 21 | ErrGte = NewCheckError("NOT_GTE") 22 | ) 23 | 24 | // IsGte checks if the value is greater than or equal to the given value. 25 | func IsGte[T cmp.Ordered](value, n T) (T, error) { 26 | if cmp.Compare(value, n) < 0 { 27 | return value, newGteError(n) 28 | } 29 | 30 | return value, nil 31 | } 32 | 33 | // makeGte creates a greater than or equal to check function from a string parameter. 34 | // Panics if the parameter cannot be parsed as a number. 35 | func makeGte(params string) CheckFunc[reflect.Value] { 36 | n, err := strconv.ParseFloat(params, 64) 37 | if err != nil { 38 | panic("unable to parse params as float") 39 | } 40 | 41 | return func(value reflect.Value) (reflect.Value, error) { 42 | v := reflect.Indirect(value) 43 | 44 | switch { 45 | case v.CanInt(): 46 | _, err := IsGte(float64(v.Int()), n) 47 | return v, err 48 | 49 | case v.CanFloat(): 50 | _, err := IsGte(v.Float(), n) 51 | return v, err 52 | 53 | default: 54 | panic("value is not numeric") 55 | } 56 | } 57 | } 58 | 59 | // newGteError creates a new greater than or equal to error with the given value. 60 | func newGteError[T cmp.Ordered](n T) error { 61 | return NewCheckErrorWithData( 62 | ErrGte.Code, 63 | map[string]interface{}{ 64 | "n": n, 65 | }, 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /gte_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "errors" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func TestGteIntSuccess(t *testing.T) { 16 | value := 4 17 | 18 | result, err := v2.IsGte(value, 4) 19 | if result != value { 20 | t.Fatalf("result (%d) is not the original value (%d)", result, value) 21 | } 22 | 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | } 27 | 28 | func TestGteIntError(t *testing.T) { 29 | value := 4 30 | 31 | result, err := v2.IsGte(value, 5) 32 | if result != value { 33 | t.Fatalf("result (%d) is not the original value (%d)", result, value) 34 | } 35 | 36 | if err == nil { 37 | t.Fatal("expected error") 38 | } 39 | 40 | message := "Value cannot be less than 5." 41 | 42 | if err.Error() != message { 43 | t.Fatalf("expected %s actual %s", message, err.Error()) 44 | } 45 | } 46 | 47 | func TestReflectGteIntError(t *testing.T) { 48 | type Person struct { 49 | Age int `checkers:"gte:18"` 50 | } 51 | 52 | person := &Person{ 53 | Age: 16, 54 | } 55 | 56 | errs, ok := v2.CheckStruct(person) 57 | if ok { 58 | t.Fatalf("expected errors") 59 | } 60 | 61 | if !errors.Is(errs["Age"], v2.ErrGte) { 62 | t.Fatalf("expected ErrGte") 63 | } 64 | } 65 | 66 | func TestReflectGteIntInvalidGte(t *testing.T) { 67 | defer FailIfNoPanic(t, "expected panic") 68 | 69 | type Person struct { 70 | Age int `checkers:"gte:abcd"` 71 | } 72 | 73 | person := &Person{ 74 | Age: 16, 75 | } 76 | 77 | v2.CheckStruct(person) 78 | } 79 | 80 | func TestReflectGteIntInvalidType(t *testing.T) { 81 | defer FailIfNoPanic(t, "expected panic") 82 | 83 | type Person struct { 84 | Age string `checkers:"gte:18"` 85 | } 86 | 87 | person := &Person{ 88 | Age: "18", 89 | } 90 | 91 | v2.CheckStruct(person) 92 | } 93 | 94 | func TestReflectGteFloatError(t *testing.T) { 95 | type Person struct { 96 | Weight float64 `checkers:"gte:165.0"` 97 | } 98 | 99 | person := &Person{ 100 | Weight: 150, 101 | } 102 | 103 | errs, ok := v2.CheckStruct(person) 104 | if ok { 105 | t.Fatalf("expected errors") 106 | } 107 | 108 | if !errors.Is(errs["Weight"], v2.ErrGte) { 109 | t.Fatalf("expected ErrGte") 110 | } 111 | } 112 | 113 | func TestReflectGteFloatInvalidGte(t *testing.T) { 114 | defer FailIfNoPanic(t, "expected panic") 115 | 116 | type Person struct { 117 | Weight float64 `checkers:"gte:abcd"` 118 | } 119 | 120 | person := &Person{ 121 | Weight: 170, 122 | } 123 | 124 | v2.CheckStruct(person) 125 | } 126 | 127 | func TestReflectGteFloatInvalidType(t *testing.T) { 128 | defer FailIfNoPanic(t, "expected panic") 129 | 130 | type Person struct { 131 | Weight string `checkers:"gte:165.0"` 132 | } 133 | 134 | person := &Person{ 135 | Weight: "170", 136 | } 137 | 138 | v2.CheckStruct(person) 139 | } 140 | -------------------------------------------------------------------------------- /header.txt: -------------------------------------------------------------------------------- 1 | // Package checker is a Go library for validating user input through struct tags. 2 | // 3 | // https://github.com/cinar/checker 4 | // 5 | // Copyright 2023 Onur Cinar. All rights reserved. 6 | // Use of this source code is governed by a MIT-style 7 | // license that can be found in the LICENSE file. 8 | // 9 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import "testing" 9 | 10 | // FailIfNoPanic fails the test if there were no panic. 11 | func FailIfNoPanic(t *testing.T, message string) { 12 | t.Helper() 13 | 14 | if r := recover(); r == nil { 15 | t.Fatal(message) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /hex.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | ) 11 | 12 | const ( 13 | // nameHex is the name of the hex check. 14 | nameHex = "hex" 15 | ) 16 | 17 | var ( 18 | // ErrNotHex indicates that the given string contains hex characters. 19 | ErrNotHex = NewCheckError("NOT_HEX") 20 | ) 21 | 22 | // IsHex checks if the given string consists of only hex characters. 23 | func IsHex(value string) (string, error) { 24 | return IsRegexp("^[0-9a-fA-F]+$", value) 25 | } 26 | 27 | // isHex checks if the given string consists of only hex characters. 28 | func isHex(value reflect.Value) (reflect.Value, error) { 29 | _, err := IsHex(value.Interface().(string)) 30 | return value, err 31 | } 32 | 33 | // makeAlphanumeric makes a checker function for the alphanumeric checker. 34 | func makeHex(_ string) CheckFunc[reflect.Value] { 35 | return isHex 36 | } 37 | -------------------------------------------------------------------------------- /hex_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsHex() { 16 | _, err := v2.IsHex("0123456789abcdefABCDEF") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsHexInvalid(t *testing.T) { 23 | _, err := v2.IsHex("ONUR") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsHexValid(t *testing.T) { 30 | _, err := v2.IsHex("0123456789abcdefABCDEF") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckHexNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Car struct { 40 | Color int `checkers:"hex"` 41 | } 42 | 43 | car := &Car{} 44 | 45 | v2.CheckStruct(car) 46 | } 47 | 48 | func TestCheckHexInvalid(t *testing.T) { 49 | type Car struct { 50 | Color string `checkers:"hex"` 51 | } 52 | 53 | car := &Car{ 54 | Color: "red", 55 | } 56 | 57 | _, ok := v2.CheckStruct(car) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckHexValid(t *testing.T) { 64 | type Car struct { 65 | Color string `checkers:"hex"` 66 | } 67 | 68 | car := &Car{ 69 | Color: "ABcd1234", 70 | } 71 | 72 | errs, ok := v2.CheckStruct(car) 73 | if !ok { 74 | t.Fatal(errs) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /html_escape.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "html" 10 | "reflect" 11 | ) 12 | 13 | const ( 14 | // nameHTMLEscape is the name of the HTML escape normalizer. 15 | nameHTMLEscape = "html-escape" 16 | ) 17 | 18 | // HTMLEscape applies HTML escaping to special characters. 19 | func HTMLEscape(value string) (string, error) { 20 | return html.EscapeString(value), nil 21 | } 22 | 23 | // reflectHTMLEscape applies HTML escaping to special characters. 24 | func reflectHTMLEscape(value reflect.Value) (reflect.Value, error) { 25 | newValue, err := HTMLEscape(value.Interface().(string)) 26 | return reflect.ValueOf(newValue), err 27 | } 28 | 29 | // makeHTMLEscape returns the HTML escape normalizer function. 30 | func makeHTMLEscape(_ string) CheckFunc[reflect.Value] { 31 | return reflectHTMLEscape 32 | } 33 | -------------------------------------------------------------------------------- /html_escape_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestHTMLEscape(t *testing.T) { 15 | input := " \"Checker\" & 'Library' " 16 | expected := "<tag> "Checker" & 'Library' </tag>" 17 | 18 | actual, err := v2.HTMLEscape(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectHTMLEscape(t *testing.T) { 29 | type Comment struct { 30 | Body string `checkers:"html-escape"` 31 | } 32 | 33 | comment := &Comment{ 34 | Body: " \"Checker\" & 'Library' ", 35 | } 36 | 37 | expected := "<tag> "Checker" & 'Library' </tag>" 38 | 39 | errs, ok := v2.CheckStruct(comment) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if comment.Body != expected { 45 | t.Fatalf("actual %s expected %s", comment.Body, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /html_unescape.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "html" 10 | "reflect" 11 | ) 12 | 13 | // nameHTMLUnescape is the name of the HTML unescape normalizer. 14 | const nameHTMLUnescape = "html-unescape" 15 | 16 | // HTMLUnescape applies HTML unescaping to special characters. 17 | func HTMLUnescape(value string) (string, error) { 18 | return html.UnescapeString(value), nil 19 | } 20 | 21 | // reflectHTMLUnescape applies HTML unescaping to special characters. 22 | func reflectHTMLUnescape(value reflect.Value) (reflect.Value, error) { 23 | newValue, err := HTMLUnescape(value.Interface().(string)) 24 | return reflect.ValueOf(newValue), err 25 | } 26 | 27 | // makeHTMLUnescape returns the HTML unescape normalizer function. 28 | func makeHTMLUnescape(_ string) CheckFunc[reflect.Value] { 29 | return reflectHTMLUnescape 30 | } 31 | -------------------------------------------------------------------------------- /html_unescape_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestHTMLUnescape(t *testing.T) { 15 | input := "<tag> "Checker" & 'Library' </tag>" 16 | expected := " \"Checker\" & 'Library' " 17 | 18 | actual, err := v2.HTMLUnescape(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectHTMLUnescape(t *testing.T) { 29 | type Comment struct { 30 | Body string `checkers:"html-unescape"` 31 | } 32 | 33 | comment := &Comment{ 34 | Body: "<tag> "Checker" & 'Library' </tag>", 35 | } 36 | 37 | expected := " \"Checker\" & 'Library' " 38 | 39 | errs, ok := v2.CheckStruct(comment) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if comment.Body != expected { 45 | t.Fatalf("actual %s expected %s", comment.Body, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ip.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "net" 10 | "reflect" 11 | ) 12 | 13 | const ( 14 | // nameIP is the name of the IP check. 15 | nameIP = "ip" 16 | ) 17 | 18 | var ( 19 | // ErrNotIP indicates that the given value is not a valid IP address. 20 | ErrNotIP = NewCheckError("NOT_IP") 21 | ) 22 | 23 | // IsIP checks if the value is a valid IP address. 24 | func IsIP(value string) (string, error) { 25 | if net.ParseIP(value) == nil { 26 | return value, ErrNotIP 27 | } 28 | return value, nil 29 | } 30 | 31 | // checkIP checks if the value is a valid IP address. 32 | func checkIP(value reflect.Value) (reflect.Value, error) { 33 | _, err := IsIP(value.Interface().(string)) 34 | return value, err 35 | } 36 | 37 | // makeIP makes a checker function for the IP checker. 38 | func makeIP(_ string) CheckFunc[reflect.Value] { 39 | return checkIP 40 | } 41 | -------------------------------------------------------------------------------- /ip_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsIP() { 16 | _, err := v2.IsIP("192.168.1.1") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsIPInvalid(t *testing.T) { 23 | _, err := v2.IsIP("invalid-ip") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsIPValid(t *testing.T) { 30 | _, err := v2.IsIP("192.168.1.1") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckIPNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Network struct { 40 | Address int `checkers:"ip"` 41 | } 42 | 43 | network := &Network{} 44 | 45 | v2.CheckStruct(network) 46 | } 47 | 48 | func TestCheckIPInvalid(t *testing.T) { 49 | type Network struct { 50 | Address string `checkers:"ip"` 51 | } 52 | 53 | network := &Network{ 54 | Address: "invalid-ip", 55 | } 56 | 57 | _, ok := v2.CheckStruct(network) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckIPValid(t *testing.T) { 64 | type Network struct { 65 | Address string `checkers:"ip"` 66 | } 67 | 68 | network := &Network{ 69 | Address: "192.168.1.1", 70 | } 71 | 72 | _, ok := v2.CheckStruct(network) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ipv4.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "net" 10 | "reflect" 11 | ) 12 | 13 | const ( 14 | // nameIPv4 is the name of the IPv4 check. 15 | nameIPv4 = "ipv4" 16 | ) 17 | 18 | var ( 19 | // ErrNotIPv4 indicates that the given value is not a valid IPv4 address. 20 | ErrNotIPv4 = NewCheckError("NOT_IPV4") 21 | ) 22 | 23 | // IsIPv4 checks if the value is a valid IPv4 address. 24 | func IsIPv4(value string) (string, error) { 25 | ip := net.ParseIP(value) 26 | if ip == nil || ip.To4() == nil { 27 | return value, ErrNotIPv4 28 | } 29 | return value, nil 30 | } 31 | 32 | // checkIPv4 checks if the value is a valid IPv4 address. 33 | func checkIPv4(value reflect.Value) (reflect.Value, error) { 34 | _, err := IsIPv4(value.Interface().(string)) 35 | return value, err 36 | } 37 | 38 | // makeIPv4 makes a checker function for the IPv4 checker. 39 | func makeIPv4(_ string) CheckFunc[reflect.Value] { 40 | return checkIPv4 41 | } 42 | -------------------------------------------------------------------------------- /ipv4_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsIPv4() { 16 | _, err := v2.IsIPv4("192.168.1.1") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsIPv4Invalid(t *testing.T) { 23 | _, err := v2.IsIPv4("2001:db8::1") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsIPv4Valid(t *testing.T) { 30 | _, err := v2.IsIPv4("192.168.1.1") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckIPv4NonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Network struct { 40 | Address int `checkers:"ipv4"` 41 | } 42 | 43 | network := &Network{} 44 | 45 | v2.CheckStruct(network) 46 | } 47 | 48 | func TestCheckIPv4Invalid(t *testing.T) { 49 | type Network struct { 50 | Address string `checkers:"ipv4"` 51 | } 52 | 53 | network := &Network{ 54 | Address: "2001:db8::1", 55 | } 56 | 57 | _, ok := v2.CheckStruct(network) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckIPv4Valid(t *testing.T) { 64 | type Network struct { 65 | Address string `checkers:"ipv4"` 66 | } 67 | 68 | network := &Network{ 69 | Address: "192.168.1.1", 70 | } 71 | 72 | _, ok := v2.CheckStruct(network) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ipv6.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "net" 10 | "reflect" 11 | ) 12 | 13 | const ( 14 | // nameIPv6 is the name of the IPv6 check. 15 | nameIPv6 = "ipv6" 16 | ) 17 | 18 | var ( 19 | // ErrNotIPv6 indicates that the given value is not a valid IPv6 address. 20 | ErrNotIPv6 = NewCheckError("NOT_IPV6") 21 | ) 22 | 23 | // IsIPv6 checks if the value is a valid IPv6 address. 24 | func IsIPv6(value string) (string, error) { 25 | if net.ParseIP(value) == nil || net.ParseIP(value).To4() != nil { 26 | return value, ErrNotIPv6 27 | } 28 | return value, nil 29 | } 30 | 31 | // checkIPv6 checks if the value is a valid IPv6 address. 32 | func checkIPv6(value reflect.Value) (reflect.Value, error) { 33 | _, err := IsIPv6(value.Interface().(string)) 34 | return value, err 35 | } 36 | 37 | // makeIPv6 makes a checker function for the IPv6 checker. 38 | func makeIPv6(_ string) CheckFunc[reflect.Value] { 39 | return checkIPv6 40 | } 41 | -------------------------------------------------------------------------------- /ipv6_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsIPv6() { 16 | _, err := v2.IsIPv6("2001:db8::1") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsIPv6Invalid(t *testing.T) { 23 | _, err := v2.IsIPv6("192.168.1.1") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsIPv6Valid(t *testing.T) { 30 | _, err := v2.IsIPv6("2001:db8::1") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckIPv6NonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Network struct { 40 | Address int `checkers:"ipv6"` 41 | } 42 | 43 | network := &Network{} 44 | 45 | v2.CheckStruct(network) 46 | } 47 | 48 | func TestCheckIPv6Invalid(t *testing.T) { 49 | type Network struct { 50 | Address string `checkers:"ipv6"` 51 | } 52 | 53 | network := &Network{ 54 | Address: "192.168.1.1", 55 | } 56 | 57 | _, ok := v2.CheckStruct(network) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckIPv6Valid(t *testing.T) { 64 | type Network struct { 65 | Address string `checkers:"ipv6"` 66 | } 67 | 68 | network := &Network{ 69 | Address: "2001:db8::1", 70 | } 71 | 72 | _, ok := v2.CheckStruct(network) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /isbn.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "regexp" 11 | ) 12 | 13 | const ( 14 | // nameISBN is the name of the ISBN check. 15 | nameISBN = "isbn" 16 | ) 17 | 18 | var ( 19 | // ErrNotISBN indicates that the given value is not a valid ISBN. 20 | ErrNotISBN = NewCheckError("NOT_ISBN") 21 | 22 | // isbnRegex is the regular expression for validating ISBN-10 and ISBN-13. 23 | isbnRegex = regexp.MustCompile(`^(97(8|9))?\d{9}(\d|X)$`) 24 | ) 25 | 26 | // IsISBN checks if the value is a valid ISBN-10 or ISBN-13. 27 | func IsISBN(value string) (string, error) { 28 | if !isbnRegex.MatchString(value) { 29 | return value, ErrNotISBN 30 | } 31 | return value, nil 32 | } 33 | 34 | // checkISBN checks if the value is a valid ISBN-10 or ISBN-13. 35 | func checkISBN(value reflect.Value) (reflect.Value, error) { 36 | _, err := IsISBN(value.Interface().(string)) 37 | return value, err 38 | } 39 | 40 | // makeISBN makes a checker function for the ISBN checker. 41 | func makeISBN(_ string) CheckFunc[reflect.Value] { 42 | return checkISBN 43 | } 44 | -------------------------------------------------------------------------------- /isbn_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsISBN() { 16 | _, err := v2.IsISBN("1430248270") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsISBNInvalid(t *testing.T) { 23 | _, err := v2.IsISBN("invalid-isbn") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsISBNValid(t *testing.T) { 30 | _, err := v2.IsISBN("1430248270") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckISBNNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Book struct { 40 | ISBN int `checkers:"isbn"` 41 | } 42 | 43 | book := &Book{} 44 | 45 | v2.CheckStruct(book) 46 | } 47 | 48 | func TestCheckISBNInvalid(t *testing.T) { 49 | type Book struct { 50 | ISBN string `checkers:"isbn"` 51 | } 52 | 53 | book := &Book{ 54 | ISBN: "invalid-isbn", 55 | } 56 | 57 | _, ok := v2.CheckStruct(book) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckISBNValid(t *testing.T) { 64 | type Book struct { 65 | ISBN string `checkers:"isbn"` 66 | } 67 | 68 | book := &Book{ 69 | ISBN: "9783161484100", 70 | } 71 | 72 | _, ok := v2.CheckStruct(book) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /locales/DOC.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # locales 6 | 7 | ```go 8 | import "github.com/cinar/checker/v2/locales" 9 | ``` 10 | 11 | Package locales provides the localized error messages for the check errors. 12 | 13 | ## Index 14 | 15 | - [Constants](<#constants>) 16 | - [Variables](<#variables>) 17 | 18 | 19 | ## Constants 20 | 21 | 22 | 23 | ```go 24 | const ( 25 | // EnUS is the en_us locale. 26 | EnUS = "en-US" 27 | ) 28 | ``` 29 | 30 | ## Variables 31 | 32 | EnUSMessages is the map of en\-US messages. 33 | 34 | ```go 35 | var EnUSMessages = map[string]string{ 36 | "NOT_ALPHANUMERIC": "Not an alphanumeric string.", 37 | "NOT_ASCII": "Can only contain ASCII characters.", 38 | "NOT_CIDR": "Not a valid CIDR notation.", 39 | "NOT_CREDIT_CARD": "Not a valid credit card number.", 40 | "NOT_DIGITS": "Can only contain digits.", 41 | "NOT_EMAIL": "Not a valid email address.", 42 | "NOT_FQDN": "Not a fully qualified domain name (FQDN).", 43 | "NOT_GTE": "Value cannot be less than {{ .n }}.", 44 | "NOT_HEX": "Can only contain hexadecimal characters.", 45 | "NOT_IP": "Not a valid IP address.", 46 | "NOT_IPV4": "Not a valid IPv4 address.", 47 | "NOT_IPV6": "Not a valid IPv6 address.", 48 | "NOT_ISBN": "Not a valid ISBN number.", 49 | "NOT_LTE": "Value cannot be less than {{ .n }}.", 50 | "NOT_LUHN": "Not a valid LUHN number.", 51 | "NOT_MAC": "Not a valid MAC address.", 52 | "NOT_MAX_LEN": "Value cannot be greater than {{ .max }}.", 53 | "NOT_MIN_LEN": "Value cannot be less than {{ .min }}.", 54 | "NOT_TIME": "Not a valid time.", 55 | "REQUIRED": "Required value is missing.", 56 | "NOT_URL": "Not a valid URL.", 57 | } 58 | ``` 59 | 60 | Generated by [gomarkdoc]() 61 | 62 | 63 | -------------------------------------------------------------------------------- /locales/en_us.go: -------------------------------------------------------------------------------- 1 | package locales 2 | 3 | const ( 4 | // EnUS is the en_us locale. 5 | EnUS = "en-US" 6 | ) 7 | 8 | // EnUSMessages is the map of en-US messages. 9 | var EnUSMessages = map[string]string{ 10 | "NOT_ALPHANUMERIC": "Not an alphanumeric string.", 11 | "NOT_ASCII": "Can only contain ASCII characters.", 12 | "NOT_CIDR": "Not a valid CIDR notation.", 13 | "NOT_CREDIT_CARD": "Not a valid credit card number.", 14 | "NOT_DIGITS": "Can only contain digits.", 15 | "NOT_EMAIL": "Not a valid email address.", 16 | "NOT_FQDN": "Not a fully qualified domain name (FQDN).", 17 | "NOT_GTE": "Value cannot be less than {{ .n }}.", 18 | "NOT_HEX": "Can only contain hexadecimal characters.", 19 | "NOT_IP": "Not a valid IP address.", 20 | "NOT_IPV4": "Not a valid IPv4 address.", 21 | "NOT_IPV6": "Not a valid IPv6 address.", 22 | "NOT_ISBN": "Not a valid ISBN number.", 23 | "NOT_LTE": "Value cannot be less than {{ .n }}.", 24 | "NOT_LUHN": "Not a valid LUHN number.", 25 | "NOT_MAC": "Not a valid MAC address.", 26 | "NOT_MAX_LEN": "Value cannot be greater than {{ .max }}.", 27 | "NOT_MIN_LEN": "Value cannot be less than {{ .min }}.", 28 | "NOT_TIME": "Not a valid time.", 29 | "REQUIRED": "Required value is missing.", 30 | "NOT_URL": "Not a valid URL.", 31 | } 32 | -------------------------------------------------------------------------------- /locales/locales.go: -------------------------------------------------------------------------------- 1 | // Package locales provides the localized error messages for the check errors. 2 | package locales 3 | -------------------------------------------------------------------------------- /lower.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | // nameLower is the name of the lower normalizer. 15 | nameLower = "lower" 16 | ) 17 | 18 | // Lower maps all Unicode letters in the given value to their lower case. 19 | func Lower(value string) (string, error) { 20 | return strings.ToLower(value), nil 21 | } 22 | 23 | // reflectLower maps all Unicode letters in the given value to their lower case. 24 | func reflectLower(value reflect.Value) (reflect.Value, error) { 25 | newValue, err := Lower(value.Interface().(string)) 26 | return reflect.ValueOf(newValue), err 27 | } 28 | 29 | // makeLower returns the lower normalizer function. 30 | func makeLower(_ string) CheckFunc[reflect.Value] { 31 | return reflectLower 32 | } 33 | -------------------------------------------------------------------------------- /lower_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestLower(t *testing.T) { 15 | input := "CHECKER" 16 | expected := "checker" 17 | 18 | actual, err := v2.Lower(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectLower(t *testing.T) { 29 | type Person struct { 30 | Name string `checkers:"lower"` 31 | } 32 | 33 | person := &Person{ 34 | Name: "CHECKER", 35 | } 36 | 37 | expected := "checker" 38 | 39 | errs, ok := v2.CheckStruct(person) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if person.Name != expected { 45 | t.Fatalf("actual %s expected %s", person.Name, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lte.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "cmp" 10 | "reflect" 11 | "strconv" 12 | ) 13 | 14 | const ( 15 | // nameLte is the name of the less than or equal to check. 16 | nameLte = "lte" 17 | ) 18 | 19 | var ( 20 | // ErrLte indicates that the value is not less than or equal to the given value. 21 | ErrLte = NewCheckError("NOT_LTE") 22 | ) 23 | 24 | // IsLte checks if the value is less than or equal to the given value. 25 | func IsLte[T cmp.Ordered](value, n T) (T, error) { 26 | if cmp.Compare(value, n) > 0 { 27 | return value, newLteError(n) 28 | } 29 | 30 | return value, nil 31 | } 32 | 33 | // makeLte creates a less than or equal to check function from a string parameter. 34 | // Panics if the parameter cannot be parsed as a number. 35 | func makeLte(params string) CheckFunc[reflect.Value] { 36 | n, err := strconv.ParseFloat(params, 64) 37 | if err != nil { 38 | panic("unable to parse params as float") 39 | } 40 | 41 | return func(value reflect.Value) (reflect.Value, error) { 42 | v := reflect.Indirect(value) 43 | 44 | switch { 45 | case v.CanInt(): 46 | _, err := IsLte(float64(v.Int()), n) 47 | return v, err 48 | 49 | case v.CanFloat(): 50 | _, err := IsLte(v.Float(), n) 51 | return v, err 52 | 53 | default: 54 | panic("value is not numeric") 55 | } 56 | } 57 | } 58 | 59 | // newLteError creates a new less than or equal to error with the given value. 60 | func newLteError[T cmp.Ordered](n T) error { 61 | return NewCheckErrorWithData( 62 | ErrLte.Code, 63 | map[string]interface{}{ 64 | "n": n, 65 | }, 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /lte_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "errors" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func TestLteIntSuccess(t *testing.T) { 16 | value := 4 17 | 18 | result, err := v2.IsLte(value, 4) 19 | if result != value { 20 | t.Fatalf("result (%d) is not the original value (%d)", result, value) 21 | } 22 | 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | } 27 | 28 | func TestLteIntError(t *testing.T) { 29 | value := 6 30 | 31 | result, err := v2.IsLte(value, 5) 32 | if result != value { 33 | t.Fatalf("result (%d) is not the original value (%d)", result, value) 34 | } 35 | 36 | if err == nil { 37 | t.Fatal("expected error") 38 | } 39 | 40 | message := "Value cannot be less than 5." 41 | 42 | if err.Error() != message { 43 | t.Fatalf("expected %s actual %s", message, err.Error()) 44 | } 45 | } 46 | 47 | func TestReflectLteIntError(t *testing.T) { 48 | type Person struct { 49 | Age int `checkers:"lte:18"` 50 | } 51 | 52 | person := &Person{ 53 | Age: 21, 54 | } 55 | 56 | errs, ok := v2.CheckStruct(person) 57 | if ok { 58 | t.Fatalf("expected errors") 59 | } 60 | 61 | if !errors.Is(errs["Age"], v2.ErrLte) { 62 | t.Fatalf("expected ErrLte") 63 | } 64 | } 65 | 66 | func TestReflectLteIntInvalidLte(t *testing.T) { 67 | defer FailIfNoPanic(t, "expected panic") 68 | 69 | type Person struct { 70 | Age int `checkers:"lte:abcd"` 71 | } 72 | 73 | person := &Person{ 74 | Age: 16, 75 | } 76 | 77 | v2.CheckStruct(person) 78 | } 79 | 80 | func TestReflectLteIntInvalidType(t *testing.T) { 81 | defer FailIfNoPanic(t, "expected panic") 82 | 83 | type Person struct { 84 | Age string `checkers:"lte:18"` 85 | } 86 | 87 | person := &Person{ 88 | Age: "18", 89 | } 90 | 91 | v2.CheckStruct(person) 92 | } 93 | 94 | func TestReflectLteFloatError(t *testing.T) { 95 | type Person struct { 96 | Weight float64 `checkers:"lte:165.0"` 97 | } 98 | 99 | person := &Person{ 100 | Weight: 170, 101 | } 102 | 103 | errs, ok := v2.CheckStruct(person) 104 | if ok { 105 | t.Fatalf("expected errors") 106 | } 107 | 108 | if !errors.Is(errs["Weight"], v2.ErrLte) { 109 | t.Fatalf("expected ErrLte") 110 | } 111 | } 112 | 113 | func TestReflectLteFloatInvalidLte(t *testing.T) { 114 | defer FailIfNoPanic(t, "expected panic") 115 | 116 | type Person struct { 117 | Weight float64 `checkers:"lte:abcd"` 118 | } 119 | 120 | person := &Person{ 121 | Weight: 170, 122 | } 123 | 124 | v2.CheckStruct(person) 125 | } 126 | 127 | func TestReflectLteFloatInvalidType(t *testing.T) { 128 | defer FailIfNoPanic(t, "expected panic") 129 | 130 | type Person struct { 131 | Weight string `checkers:"lte:165.0"` 132 | } 133 | 134 | person := &Person{ 135 | Weight: "170", 136 | } 137 | 138 | v2.CheckStruct(person) 139 | } 140 | -------------------------------------------------------------------------------- /luhn.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "unicode" 11 | ) 12 | 13 | const ( 14 | // nameLUHN is the name of the LUHN check. 15 | nameLUHN = "luhn" 16 | ) 17 | 18 | var ( 19 | // ErrNotLUHN indicates that the given value is not a valid LUHN number. 20 | ErrNotLUHN = NewCheckError("NOT_LUHN") 21 | ) 22 | 23 | // IsLUHN checks if the value is a valid LUHN number. 24 | func IsLUHN(value string) (string, error) { 25 | var sum int 26 | var alt bool 27 | 28 | for i := len(value) - 1; i >= 0; i-- { 29 | r := rune(value[i]) 30 | if !unicode.IsDigit(r) { 31 | return value, ErrNotLUHN 32 | } 33 | 34 | n := int(r - '0') 35 | if alt { 36 | n *= 2 37 | if n > 9 { 38 | n -= 9 39 | } 40 | } 41 | sum += n 42 | alt = !alt 43 | } 44 | 45 | if sum%10 != 0 { 46 | return value, ErrNotLUHN 47 | } 48 | 49 | return value, nil 50 | } 51 | 52 | // checkLUHN checks if the value is a valid LUHN number. 53 | func checkLUHN(value reflect.Value) (reflect.Value, error) { 54 | _, err := IsLUHN(value.Interface().(string)) 55 | return value, err 56 | } 57 | 58 | // makeLUHN makes a checker function for the LUHN checker. 59 | func makeLUHN(_ string) CheckFunc[reflect.Value] { 60 | return checkLUHN 61 | } 62 | -------------------------------------------------------------------------------- /luhn_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsLUHN() { 16 | _, err := v2.IsLUHN("4012888888881881") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsLUHNInvalid(t *testing.T) { 23 | _, err := v2.IsLUHN("123456789") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsLUHNInvalidDigits(t *testing.T) { 30 | _, err := v2.IsLUHN("ABCD") 31 | if err == nil { 32 | t.Fatal("expected error") 33 | } 34 | } 35 | 36 | func TestIsLUHNValid(t *testing.T) { 37 | _, err := v2.IsLUHN("4012888888881881") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | 43 | func TestCheckLUHNNonString(t *testing.T) { 44 | defer FailIfNoPanic(t, "expected panic") 45 | 46 | type Card struct { 47 | Number int `checkers:"luhn"` 48 | } 49 | 50 | card := &Card{} 51 | 52 | v2.CheckStruct(card) 53 | } 54 | 55 | func TestCheckLUHNInvalid(t *testing.T) { 56 | type Card struct { 57 | Number string `checkers:"luhn"` 58 | } 59 | 60 | card := &Card{ 61 | Number: "123456789", 62 | } 63 | 64 | _, ok := v2.CheckStruct(card) 65 | if ok { 66 | t.Fatal("expected error") 67 | } 68 | } 69 | 70 | func TestCheckLUHNValid(t *testing.T) { 71 | type Card struct { 72 | Number string `checkers:"luhn"` 73 | } 74 | 75 | card := &Card{ 76 | Number: "79927398713", 77 | } 78 | 79 | _, ok := v2.CheckStruct(card) 80 | if !ok { 81 | t.Fatal("expected valid") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /mac.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "net" 10 | "reflect" 11 | ) 12 | 13 | const ( 14 | // nameMAC is the name of the MAC check. 15 | nameMAC = "mac" 16 | ) 17 | 18 | var ( 19 | // ErrNotMAC indicates that the given value is not a valid MAC address. 20 | ErrNotMAC = NewCheckError("NOT_MAC") 21 | ) 22 | 23 | // IsMAC checks if the value is a valid MAC address. 24 | func IsMAC(value string) (string, error) { 25 | _, err := net.ParseMAC(value) 26 | if err != nil { 27 | return value, ErrNotMAC 28 | } 29 | return value, nil 30 | } 31 | 32 | // checkMAC checks if the value is a valid MAC address. 33 | func checkMAC(value reflect.Value) (reflect.Value, error) { 34 | _, err := IsMAC(value.Interface().(string)) 35 | return value, err 36 | } 37 | 38 | // makeMAC makes a checker function for the MAC checker. 39 | func makeMAC(_ string) CheckFunc[reflect.Value] { 40 | return checkMAC 41 | } 42 | -------------------------------------------------------------------------------- /mac_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsMAC() { 16 | _, err := v2.IsMAC("00:1A:2B:3C:4D:5E") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsMACInvalid(t *testing.T) { 23 | _, err := v2.IsMAC("invalid-mac") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsMACValid(t *testing.T) { 30 | _, err := v2.IsMAC("00:1A:2B:3C:4D:5E") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckMACNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Device struct { 40 | MAC int `checkers:"mac"` 41 | } 42 | 43 | device := &Device{} 44 | 45 | v2.CheckStruct(device) 46 | } 47 | 48 | func TestCheckMACInvalid(t *testing.T) { 49 | type Device struct { 50 | MAC string `checkers:"mac"` 51 | } 52 | 53 | device := &Device{ 54 | MAC: "invalid-mac", 55 | } 56 | 57 | _, ok := v2.CheckStruct(device) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckMACValid(t *testing.T) { 64 | type Device struct { 65 | MAC string `checkers:"mac"` 66 | } 67 | 68 | device := &Device{ 69 | MAC: "00:1A:2B:3C:4D:5E", 70 | } 71 | 72 | _, ok := v2.CheckStruct(device) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /maker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "fmt" 10 | "reflect" 11 | "strings" 12 | ) 13 | 14 | // MakeCheckFunc is a function that returns a check function using the given params. 15 | type MakeCheckFunc func(params string) CheckFunc[reflect.Value] 16 | 17 | // makers provides a mapping of maker functions keyed by the check name. 18 | var makers = map[string]MakeCheckFunc{ 19 | nameAlphanumeric: makeAlphanumeric, 20 | nameASCII: makeASCII, 21 | nameCIDR: makeCIDR, 22 | nameCreditCard: makeCreditCard, 23 | nameDigits: makeDigits, 24 | nameEmail: makeEmail, 25 | nameFQDN: makeFQDN, 26 | nameGte: makeGte, 27 | nameHex: makeHex, 28 | nameHTMLEscape: makeHTMLEscape, 29 | nameHTMLUnescape: makeHTMLUnescape, 30 | nameIP: makeIP, 31 | nameIPv4: makeIPv4, 32 | nameIPv6: makeIPv6, 33 | nameISBN: makeISBN, 34 | nameLower: makeLower, 35 | nameLte: makeLte, 36 | nameLUHN: makeLUHN, 37 | nameMAC: makeMAC, 38 | nameMaxLen: makeMaxLen, 39 | nameMinLen: makeMinLen, 40 | nameRegexp: makeRegexp, 41 | nameRequired: makeRequired, 42 | nameTime: makeTime, 43 | nameTitle: makeTitle, 44 | nameTrimLeft: makeTrimLeft, 45 | nameTrimRight: makeTrimRight, 46 | nameTrimSpace: makeTrimSpace, 47 | nameUpper: makeUpper, 48 | nameURL: makeURL, 49 | nameURLEscape: makeURLEscape, 50 | nameURLUnescape: makeURLUnescape, 51 | } 52 | 53 | // RegisterMaker registers a new maker function with the given name. 54 | func RegisterMaker(name string, maker MakeCheckFunc) { 55 | makers[name] = maker 56 | } 57 | 58 | // makeChecks take a checker config and returns the check functions. 59 | func makeChecks(config string) []CheckFunc[reflect.Value] { 60 | fields := strings.Fields(config) 61 | 62 | checks := make([]CheckFunc[reflect.Value], len(fields)) 63 | 64 | for i, field := range fields { 65 | name, params, _ := strings.Cut(field, ":") 66 | 67 | maker, ok := makers[name] 68 | if !ok { 69 | panic(fmt.Sprintf("check %s not found", name)) 70 | } 71 | 72 | checks[i] = maker(params) 73 | } 74 | 75 | return checks 76 | } 77 | -------------------------------------------------------------------------------- /maker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "reflect" 11 | "testing" 12 | 13 | "github.com/cinar/checker/v2/locales" 14 | 15 | v2 "github.com/cinar/checker/v2" 16 | ) 17 | 18 | func TestMakeCheckersUnknown(t *testing.T) { 19 | defer FailIfNoPanic(t, "expected panic") 20 | 21 | type Person struct { 22 | Name string `checkers:"unknown"` 23 | } 24 | 25 | person := &Person{ 26 | Name: "Onur", 27 | } 28 | 29 | v2.CheckStruct(person) 30 | } 31 | 32 | func ExampleRegisterMaker() { 33 | locales.EnUSMessages["NOT_FRUIT"] = "Not a fruit name." 34 | 35 | v2.RegisterMaker("is-fruit", func(params string) v2.CheckFunc[reflect.Value] { 36 | return func(value reflect.Value) (reflect.Value, error) { 37 | stringValue := value.Interface().(string) 38 | 39 | if stringValue == "apple" || stringValue == "banana" { 40 | return value, nil 41 | } 42 | 43 | return value, v2.NewCheckError("NOT_FRUIT") 44 | } 45 | }) 46 | 47 | type Item struct { 48 | Name string `checkers:"is-fruit"` 49 | } 50 | 51 | person := &Item{ 52 | Name: "banana", 53 | } 54 | 55 | err, ok := v2.CheckStruct(person) 56 | if !ok { 57 | fmt.Println(err) 58 | } 59 | } 60 | 61 | func TestRegisterMaker(t *testing.T) { 62 | v2.RegisterMaker("unknown", func(params string) v2.CheckFunc[reflect.Value] { 63 | return func(value reflect.Value) (reflect.Value, error) { 64 | return value, nil 65 | } 66 | }) 67 | 68 | type Person struct { 69 | Name string `checkers:"unknown"` 70 | } 71 | 72 | person := &Person{ 73 | Name: "Onur", 74 | } 75 | 76 | _, ok := v2.CheckStruct(person) 77 | if !ok { 78 | t.Fatal("expected valid") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /max_len.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "strconv" 11 | ) 12 | 13 | const ( 14 | // nameMaxLen is the name of the maximum length check. 15 | nameMaxLen = "max-len" 16 | ) 17 | 18 | var ( 19 | // ErrMaxLen indicates that the value's length is greater than the specified maximum. 20 | ErrMaxLen = NewCheckError("NOT_MAX_LEN") 21 | ) 22 | 23 | // MaxLen checks if the length of the given value (string, slice, or map) is at most n. 24 | // Returns an error if the length is greater than n. 25 | func MaxLen[T any](n int) CheckFunc[T] { 26 | return func(value T) (T, error) { 27 | v, ok := any(value).(reflect.Value) 28 | if !ok { 29 | v = reflect.ValueOf(value) 30 | } 31 | 32 | v = reflect.Indirect(v) 33 | 34 | if v.Len() > n { 35 | return value, newMaxLenError(n) 36 | } 37 | 38 | return value, nil 39 | } 40 | } 41 | 42 | // makeMaxLen creates a maximum length check function from a string parameter. 43 | // Panics if the parameter cannot be parsed as an integer. 44 | func makeMaxLen(params string) CheckFunc[reflect.Value] { 45 | n, err := strconv.Atoi(params) 46 | if err != nil { 47 | panic("unable to parse max length") 48 | } 49 | 50 | return MaxLen[reflect.Value](n) 51 | } 52 | 53 | // newMaxLenError creates a new maximum length error with the given maximum length. 54 | func newMaxLenError(n int) error { 55 | return NewCheckErrorWithData( 56 | ErrMaxLen.Code, 57 | map[string]interface{}{ 58 | "max": n, 59 | }, 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /max_len_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestMaxLenSuccess(t *testing.T) { 15 | value := "test" 16 | 17 | check := v2.MaxLen[string](4) 18 | 19 | result, err := check(value) 20 | if result != value { 21 | t.Fatalf("result (%s) is not the original value (%s)", result, value) 22 | } 23 | 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | 29 | func TestMaxLenError(t *testing.T) { 30 | value := "test test" 31 | 32 | check := v2.MaxLen[string](5) 33 | 34 | result, err := check(value) 35 | if result != value { 36 | t.Fatalf("result (%s) is not the original value (%s)", result, value) 37 | } 38 | 39 | message := "Value cannot be greater than 5." 40 | 41 | if err.Error() != message { 42 | t.Fatalf("expected %s actual %s", message, err.Error()) 43 | } 44 | } 45 | 46 | func TestReflectMaxLenError(t *testing.T) { 47 | type Person struct { 48 | Name string `checkers:"max-len:2"` 49 | } 50 | 51 | person := &Person{ 52 | Name: "Onur", 53 | } 54 | 55 | errs, ok := v2.CheckStruct(person) 56 | if ok { 57 | t.Fatalf("expected errors") 58 | } 59 | 60 | if errs["Name"] == nil { 61 | t.Fatalf("expected maximum length error") 62 | } 63 | } 64 | 65 | func TestReflectMaxLenInvalidMaxLen(t *testing.T) { 66 | defer FailIfNoPanic(t, "expected panic") 67 | 68 | type Person struct { 69 | Name string `checkers:"max-len:abcd"` 70 | } 71 | 72 | person := &Person{ 73 | Name: "Onur", 74 | } 75 | 76 | v2.CheckStruct(person) 77 | } 78 | 79 | func TestReflectMaxLenInvalidType(t *testing.T) { 80 | defer FailIfNoPanic(t, "expected panic") 81 | 82 | type Person struct { 83 | Name int `checkers:"max-len:8"` 84 | } 85 | 86 | person := &Person{ 87 | Name: 1, 88 | } 89 | 90 | v2.CheckStruct(person) 91 | } 92 | -------------------------------------------------------------------------------- /min_len.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "strconv" 11 | ) 12 | 13 | const ( 14 | // nameMinLen is the name of the minimum length check. 15 | nameMinLen = "min-len" 16 | ) 17 | 18 | var ( 19 | // ErrMinLen indicates that the value's length is less than the specified minimum. 20 | ErrMinLen = NewCheckError("NOT_MIN_LEN") 21 | ) 22 | 23 | // MinLen checks if the length of the given value (string, slice, or map) is at least n. 24 | // Returns an error if the length is less than n. 25 | func MinLen[T any](n int) CheckFunc[T] { 26 | return func(value T) (T, error) { 27 | v, ok := any(value).(reflect.Value) 28 | if !ok { 29 | v = reflect.ValueOf(value) 30 | } 31 | 32 | v = reflect.Indirect(v) 33 | 34 | if v.Len() < n { 35 | return value, newMinLenError(n) 36 | } 37 | 38 | return value, nil 39 | } 40 | } 41 | 42 | // makeMinLen creates a minimum length check function from a string parameter. 43 | // Panics if the parameter cannot be parsed as an integer. 44 | func makeMinLen(params string) CheckFunc[reflect.Value] { 45 | n, err := strconv.Atoi(params) 46 | if err != nil { 47 | panic("unable to parse min length") 48 | } 49 | 50 | return MinLen[reflect.Value](n) 51 | } 52 | 53 | // newMinLenError creates a new minimum length error with the given minimum value. 54 | func newMinLenError(n int) error { 55 | return NewCheckErrorWithData( 56 | ErrMinLen.Code, 57 | map[string]interface{}{ 58 | "min": n, 59 | }, 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /min_len_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestMinLenSuccess(t *testing.T) { 15 | value := "test" 16 | 17 | check := v2.MinLen[string](4) 18 | 19 | result, err := check(value) 20 | if result != value { 21 | t.Fatalf("result (%s) is not the original value (%s)", result, value) 22 | } 23 | 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | 29 | func TestMinLenError(t *testing.T) { 30 | value := "test" 31 | 32 | check := v2.MinLen[string](5) 33 | 34 | result, err := check(value) 35 | if result != value { 36 | t.Fatalf("result (%s) is not the original value (%s)", result, value) 37 | } 38 | 39 | message := "Value cannot be less than 5." 40 | 41 | if err.Error() != message { 42 | t.Fatalf("expected %s actual %s", message, err.Error()) 43 | } 44 | } 45 | 46 | func TestReflectMinLenError(t *testing.T) { 47 | type Person struct { 48 | Name string `checkers:"trim min-len:8"` 49 | } 50 | 51 | person := &Person{ 52 | Name: " Onur ", 53 | } 54 | 55 | errs, ok := v2.CheckStruct(person) 56 | if ok { 57 | t.Fatalf("expected errors") 58 | } 59 | 60 | if errs["Name"] == nil { 61 | t.Fatalf("expected minimum length error") 62 | } 63 | } 64 | 65 | func TestReflectMinLenInvalidMinLen(t *testing.T) { 66 | defer FailIfNoPanic(t, "expected panic") 67 | 68 | type Person struct { 69 | Name string `checkers:"min-len:abcd"` 70 | } 71 | 72 | person := &Person{ 73 | Name: "Onur", 74 | } 75 | 76 | v2.CheckStruct(person) 77 | } 78 | 79 | func TestReflectMinLenInvalidType(t *testing.T) { 80 | defer FailIfNoPanic(t, "expected panic") 81 | 82 | type Person struct { 83 | Name int `checkers:"min-len:8"` 84 | } 85 | 86 | person := &Person{ 87 | Name: 1, 88 | } 89 | 90 | v2.CheckStruct(person) 91 | } 92 | -------------------------------------------------------------------------------- /regexp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "regexp" 11 | ) 12 | 13 | // nameRegexp is the name of the regexp check. 14 | const nameRegexp = "regexp" 15 | 16 | // ErrNotMatch indicates that the given string does not match the regexp pattern. 17 | var ErrNotMatch = NewCheckError("REGEXP") 18 | 19 | // IsRegexp checks if the given string matches the given regexp expression. 20 | func IsRegexp(expression, value string) (string, error) { 21 | if !regexp.MustCompile(expression).MatchString(value) { 22 | return value, ErrNotMatch 23 | } 24 | 25 | return value, nil 26 | } 27 | 28 | // MakeRegexpChecker makes a regexp checker for the given regexp expression with the given invalid result. 29 | func MakeRegexpChecker(expression string, invalidError error) CheckFunc[reflect.Value] { 30 | return func(value reflect.Value) (reflect.Value, error) { 31 | if value.Kind() != reflect.String { 32 | panic("string expected") 33 | } 34 | 35 | _, err := IsRegexp(expression, value.String()) 36 | if err != nil { 37 | return value, invalidError 38 | } 39 | 40 | return value, nil 41 | } 42 | } 43 | 44 | // makeRegexp makes a checker function for the regexp. 45 | func makeRegexp(config string) CheckFunc[reflect.Value] { 46 | return MakeRegexpChecker(config, ErrNotMatch) 47 | } 48 | -------------------------------------------------------------------------------- /regexp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsRegexp() { 16 | _, err := v2.IsRegexp("^[0-9a-fA-F]+$", "ABcd1234") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsRegexpInvalid(t *testing.T) { 23 | _, err := v2.IsRegexp("^[0-9a-fA-F]+$", "Onur") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsRegexpValid(t *testing.T) { 30 | _, err := v2.IsRegexp("^[0-9a-fA-F]+$", "ABcd1234") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckRegexpNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type User struct { 40 | Username int `checkers:"regexp:^[A-Za-z]$"` 41 | } 42 | 43 | user := &User{} 44 | 45 | v2.CheckStruct(user) 46 | } 47 | 48 | func TestCheckRegexpInvalid(t *testing.T) { 49 | type User struct { 50 | Username string `checkers:"regexp:^[A-Za-z]+$"` 51 | } 52 | 53 | user := &User{ 54 | Username: "abcd1234", 55 | } 56 | 57 | _, ok := v2.CheckStruct(user) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckRegexpValid(t *testing.T) { 64 | type User struct { 65 | Username string `checkers:"regexp:^[A-Za-z]+$"` 66 | } 67 | 68 | user := &User{ 69 | Username: "abcd", 70 | } 71 | 72 | _, ok := v2.CheckStruct(user) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /required.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import "reflect" 9 | 10 | const ( 11 | // nameRequired is the name of the required check. 12 | nameRequired = "required" 13 | ) 14 | 15 | var ( 16 | // ErrRequired indicates that a required value was missing. 17 | ErrRequired = NewCheckError("REQUIRED") 18 | ) 19 | 20 | // Required checks if the given value of type T is its zero value. It 21 | // returns an error if the value is zero. 22 | func Required[T any](value T) (T, error) { 23 | _, err := reflectRequired(reflect.ValueOf(value)) 24 | return value, err 25 | } 26 | 27 | // reflectRequired checks if the given value is its zero value. It 28 | // returns an error if the value is zero. 29 | func reflectRequired(value reflect.Value) (reflect.Value, error) { 30 | var err error 31 | 32 | if value.IsZero() { 33 | err = ErrRequired 34 | } 35 | 36 | return value, err 37 | } 38 | 39 | // makeRequired returns the required check function. 40 | func makeRequired(_ string) CheckFunc[reflect.Value] { 41 | return reflectRequired 42 | } 43 | -------------------------------------------------------------------------------- /required_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "errors" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func TestRequiredSuccess(t *testing.T) { 16 | value := "test" 17 | 18 | result, err := v2.Required(value) 19 | 20 | if result != value { 21 | t.Fatalf("result (%s) is not the original value (%s)", result, value) 22 | } 23 | 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | 29 | func TestRequiredMissing(t *testing.T) { 30 | var value string 31 | 32 | result, err := v2.Required(value) 33 | 34 | if result != value { 35 | t.Fatalf("result (%s) is not the original value (%s)", result, value) 36 | } 37 | 38 | if !errors.Is(err, v2.ErrRequired) { 39 | t.Fatalf("got unexpected error %v", err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.receiver-naming] 22 | [rule.time-naming] 23 | [rule.unexported-return] 24 | [rule.indent-error-flow] 25 | [rule.errorf] 26 | [rule.empty-block] 27 | [rule.superfluous-else] 28 | [rule.unused-parameter] 29 | [rule.unreachable-code] 30 | [rule.redefines-builtin-id] 31 | -------------------------------------------------------------------------------- /taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | output: 'prefixed' 3 | 4 | tasks: 5 | default: 6 | cmds: 7 | - task: fmt 8 | - task: lint 9 | - task: test 10 | - task: docs 11 | 12 | action: 13 | deps: [lint, test] 14 | 15 | fmt: 16 | cmds: 17 | - go fix ./... 18 | 19 | lint: 20 | cmds: 21 | - go vet ./... 22 | - go run github.com/securego/gosec/v2/cmd/gosec@v2.20.0 ./... 23 | - go run honnef.co/go/tools/cmd/staticcheck@v0.5.1 ./... 24 | - go run github.com/mgechev/revive@v1.3.4 -config=revive.toml ./... 25 | 26 | test: 27 | cmds: 28 | - go test -cover -coverprofile=coverage.out ./... 29 | - go tool cover -func=coverage.out 30 | 31 | docs: 32 | cmds: 33 | - go run github.com/princjef/gomarkdoc/cmd/gomarkdoc@v1.1.0 -e ./... 34 | 35 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "time" 11 | ) 12 | 13 | const ( 14 | // time is the name of the time format check. 15 | nameTime = "time" 16 | ) 17 | 18 | var ( 19 | // ErrTime indicates that the value is not a valid based on the given time format. 20 | ErrTime = NewCheckError("NOT_TIME") 21 | ) 22 | 23 | // timeLayouts is a map of time layouts that can be used in the time check. 24 | var timeLayouts = map[string]string{ 25 | "Layout": time.Layout, 26 | "ANSIC": time.ANSIC, 27 | "UnixDate": time.UnixDate, 28 | "RubyDate": time.RubyDate, 29 | "RFC822": time.RFC822, 30 | "RFC822Z": time.RFC822Z, 31 | "RFC850": time.RFC850, 32 | "RFC1123": time.RFC1123, 33 | "RFC1123Z": time.RFC1123Z, 34 | "RFC3339": time.RFC3339, 35 | "RFC3339Nano": time.RFC3339Nano, 36 | "Kitchen": time.Kitchen, 37 | "Stamp": time.Stamp, 38 | "StampMilli": time.StampMilli, 39 | "StampMicro": time.StampMicro, 40 | "StampNano": time.StampNano, 41 | "DateTime": time.DateTime, 42 | "DateOnly": time.DateOnly, 43 | "TimeOnly": time.TimeOnly, 44 | } 45 | 46 | // IsTime checks if the given value is a valid time based on the given layout. 47 | func IsTime(params, value string) (string, error) { 48 | layout, ok := timeLayouts[params] 49 | if !ok { 50 | layout = params 51 | } 52 | 53 | _, err := time.Parse(layout, value) 54 | if err != nil { 55 | return value, ErrTime 56 | } 57 | 58 | return value, nil 59 | } 60 | 61 | // makeTime makes a time format check based on the given time layout. 62 | func makeTime(params string) CheckFunc[reflect.Value] { 63 | return func(value reflect.Value) (reflect.Value, error) { 64 | _, err := IsTime(params, value.String()) 65 | return value, err 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "errors" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsTime() { 16 | value := "2024-12-31" 17 | 18 | _, err := v2.IsTime("DateOnly", value) 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | 24 | func ExampleIsTime_custom() { 25 | rfc3339Layout := "2006-01-02T15:04:05Z07:00" 26 | 27 | value := "2024-12-31T10:20:00Z07:00" 28 | 29 | _, err := v2.IsTime(rfc3339Layout, value) 30 | if err != nil { 31 | panic(err) 32 | } 33 | } 34 | 35 | func TestIsTimeSuccess(t *testing.T) { 36 | value := "2024-12-31" 37 | 38 | result, err := v2.IsTime("DateOnly", value) 39 | if result != value { 40 | t.Fatalf("result (%s) is not the original value (%s)", result, value) 41 | } 42 | 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | } 47 | 48 | func TestIsTimeError(t *testing.T) { 49 | value := "2024-12-31" 50 | 51 | result, err := v2.IsTime("2006-02-01", value) 52 | if result != value { 53 | t.Fatalf("result (%s) is not the original value (%s)", result, value) 54 | } 55 | 56 | if !errors.Is(err, v2.ErrTime) { 57 | t.Fatalf("expected error %s actual %s", v2.ErrTime, err) 58 | } 59 | 60 | message := "Not a valid time." 61 | 62 | if err.Error() != message { 63 | t.Fatalf("expected %s actual %s", message, err.Error()) 64 | } 65 | } 66 | 67 | func TestStructTimeError(t *testing.T) { 68 | type Person struct { 69 | Birthday string `checkers:"time:DateOnly"` 70 | } 71 | 72 | person := &Person{ 73 | Birthday: "2024-31-12", 74 | } 75 | 76 | errs, ok := v2.CheckStruct(person) 77 | if ok { 78 | t.Fatalf("expected errors") 79 | } 80 | 81 | if !errors.Is(errs["Birthday"], v2.ErrTime) { 82 | t.Fatalf("expected time error") 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /title.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "strings" 11 | "unicode" 12 | ) 13 | 14 | const ( 15 | // nameTitle is the name of the title normalizer. 16 | nameTitle = "title" 17 | ) 18 | 19 | // Title returns the value of the string with the first letter of each word in upper case. 20 | func Title(value string) (string, error) { 21 | var sb strings.Builder 22 | begin := true 23 | 24 | for _, c := range value { 25 | if unicode.IsLetter(c) { 26 | if begin { 27 | c = unicode.ToUpper(c) 28 | begin = false 29 | } else { 30 | c = unicode.ToLower(c) 31 | } 32 | } else { 33 | begin = true 34 | } 35 | 36 | sb.WriteRune(c) 37 | } 38 | 39 | return sb.String(), nil 40 | } 41 | 42 | // reflectTitle returns the value of the string with the first letter of each word in upper case. 43 | func reflectTitle(value reflect.Value) (reflect.Value, error) { 44 | newValue, err := Title(value.Interface().(string)) 45 | return reflect.ValueOf(newValue), err 46 | } 47 | 48 | // makeTitle returns the title normalizer function. 49 | func makeTitle(_ string) CheckFunc[reflect.Value] { 50 | return reflectTitle 51 | } 52 | -------------------------------------------------------------------------------- /title_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestTitle(t *testing.T) { 15 | input := "the checker" 16 | expected := "The Checker" 17 | 18 | actual, err := v2.Title(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectTitle(t *testing.T) { 29 | type Book struct { 30 | Chapter string `checkers:"title"` 31 | } 32 | 33 | book := &Book{ 34 | Chapter: "the checker", 35 | } 36 | 37 | expected := "The Checker" 38 | 39 | errs, ok := v2.CheckStruct(book) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if book.Chapter != expected { 45 | t.Fatalf("actual %s expected %s", book.Chapter, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /trim_left.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | // nameTrimLeft is the name of the trim left normalizer. 15 | nameTrimLeft = "trim-left" 16 | ) 17 | 18 | // TrimLeft returns the value of the string with whitespace removed from the beginning. 19 | func TrimLeft(value string) (string, error) { 20 | return strings.TrimLeft(value, " \t"), nil 21 | } 22 | 23 | // reflectTrimLeft returns the value of the string with whitespace removed from the beginning. 24 | func reflectTrimLeft(value reflect.Value) (reflect.Value, error) { 25 | newValue, err := TrimLeft(value.Interface().(string)) 26 | return reflect.ValueOf(newValue), err 27 | } 28 | 29 | // makeTrimLeft returns the trim left normalizer function. 30 | func makeTrimLeft(_ string) CheckFunc[reflect.Value] { 31 | return reflectTrimLeft 32 | } 33 | -------------------------------------------------------------------------------- /trim_left_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestTrimLeft(t *testing.T) { 15 | input := " test " 16 | expected := "test " 17 | 18 | actual, err := v2.TrimLeft(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectTrimLeft(t *testing.T) { 29 | type Person struct { 30 | Name string `checkers:"trim-left"` 31 | } 32 | 33 | person := &Person{ 34 | Name: " test ", 35 | } 36 | 37 | expected := "test " 38 | 39 | errs, ok := v2.CheckStruct(person) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if person.Name != expected { 45 | t.Fatalf("actual %s expected %s", person.Name, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /trim_right.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | // nameTrimRight is the name of the trim right normalizer. 15 | nameTrimRight = "trim-right" 16 | ) 17 | 18 | // TrimRight returns the value of the string with whitespace removed from the end. 19 | func TrimRight(value string) (string, error) { 20 | return strings.TrimRight(value, " \t"), nil 21 | } 22 | 23 | // reflectTrimRight returns the value of the string with whitespace removed from the end. 24 | func reflectTrimRight(value reflect.Value) (reflect.Value, error) { 25 | newValue, err := TrimRight(value.Interface().(string)) 26 | return reflect.ValueOf(newValue), err 27 | } 28 | 29 | // makeTrimRight returns the trim right normalizer function. 30 | func makeTrimRight(_ string) CheckFunc[reflect.Value] { 31 | return reflectTrimRight 32 | } 33 | -------------------------------------------------------------------------------- /trim_right_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestTrimRight(t *testing.T) { 15 | input := " test " 16 | expected := " test" 17 | 18 | actual, err := v2.TrimRight(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectTrimRight(t *testing.T) { 29 | type Person struct { 30 | Name string `checkers:"trim-right"` 31 | } 32 | 33 | person := &Person{ 34 | Name: " test ", 35 | } 36 | 37 | expected := " test" 38 | 39 | errs, ok := v2.CheckStruct(person) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if person.Name != expected { 45 | t.Fatalf("actual %s expected %s", person.Name, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /trim_space.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | // nameTrimSpace is the name of the trim normalizer. 15 | nameTrimSpace = "trim" 16 | ) 17 | 18 | // TrimSpace returns the value of the string with whitespace removed from both ends. 19 | func TrimSpace(value string) (string, error) { 20 | return strings.TrimSpace(value), nil 21 | } 22 | 23 | // reflectTrimSpace returns the value of the string with whitespace removed from both ends. 24 | func reflectTrimSpace(value reflect.Value) (reflect.Value, error) { 25 | newValue, err := TrimSpace(value.Interface().(string)) 26 | return reflect.ValueOf(newValue), err 27 | } 28 | 29 | // makeTrimSpace returns the trim space normalizer function. 30 | func makeTrimSpace(_ string) CheckFunc[reflect.Value] { 31 | return reflectTrimSpace 32 | } 33 | -------------------------------------------------------------------------------- /trim_space_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestTrimSpace(t *testing.T) { 15 | input := " test " 16 | expected := "test" 17 | 18 | actual, err := v2.TrimSpace(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectTrimSpace(t *testing.T) { 29 | type Person struct { 30 | Name string `checkers:"trim"` 31 | } 32 | 33 | person := &Person{ 34 | Name: " test ", 35 | } 36 | 37 | expected := "test" 38 | 39 | errs, ok := v2.CheckStruct(person) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if person.Name != expected { 45 | t.Fatalf("actual %s expected %s", person.Name, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /upper.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | // nameUpper is the name of the upper normalizer. 15 | nameUpper = "upper" 16 | ) 17 | 18 | // Upper maps all Unicode letters in the given value to their upper case. 19 | func Upper(value string) (string, error) { 20 | return strings.ToUpper(value), nil 21 | } 22 | 23 | // reflectUpper maps all Unicode letters in the given value to their upper case. 24 | func reflectUpper(value reflect.Value) (reflect.Value, error) { 25 | newValue, err := Upper(value.Interface().(string)) 26 | return reflect.ValueOf(newValue), err 27 | } 28 | 29 | // makeUpper returns the upper normalizer function. 30 | func makeUpper(_ string) CheckFunc[reflect.Value] { 31 | return reflectUpper 32 | } 33 | -------------------------------------------------------------------------------- /upper_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestUpper(t *testing.T) { 15 | input := "checker" 16 | expected := "CHECKER" 17 | 18 | actual, err := v2.Upper(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectUpper(t *testing.T) { 29 | type Person struct { 30 | Name string `checkers:"upper"` 31 | } 32 | 33 | person := &Person{ 34 | Name: "checker", 35 | } 36 | 37 | expected := "CHECKER" 38 | 39 | errs, ok := v2.CheckStruct(person) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if person.Name != expected { 45 | t.Fatalf("actual %s expected %s", person.Name, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "net/url" 10 | "reflect" 11 | ) 12 | 13 | const ( 14 | // nameURL is the name of the URL check. 15 | nameURL = "url" 16 | ) 17 | 18 | var ( 19 | // ErrNotURL indicates that the given value is not a valid URL. 20 | ErrNotURL = NewCheckError("NOT_URL") 21 | ) 22 | 23 | // IsURL checks if the value is a valid URL. 24 | func IsURL(value string) (string, error) { 25 | _, err := url.ParseRequestURI(value) 26 | if err != nil { 27 | return value, ErrNotURL 28 | } 29 | return value, nil 30 | } 31 | 32 | // checkURL checks if the value is a valid URL. 33 | func checkURL(value reflect.Value) (reflect.Value, error) { 34 | _, err := IsURL(value.Interface().(string)) 35 | return value, err 36 | } 37 | 38 | // makeURL makes a checker function for the URL checker. 39 | func makeURL(_ string) CheckFunc[reflect.Value] { 40 | return checkURL 41 | } 42 | -------------------------------------------------------------------------------- /url_escape.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "net/url" 10 | "reflect" 11 | ) 12 | 13 | // nameURLEscape is the name of the URL escape normalizer. 14 | const nameURLEscape = "url-escape" 15 | 16 | // URLEscape applies URL escaping to special characters. 17 | func URLEscape(value string) (string, error) { 18 | return url.QueryEscape(value), nil 19 | } 20 | 21 | // reflectURLEscape applies URL escaping to special characters. 22 | func reflectURLEscape(value reflect.Value) (reflect.Value, error) { 23 | newValue, err := URLEscape(value.Interface().(string)) 24 | return reflect.ValueOf(newValue), err 25 | } 26 | 27 | // makeURLEscape returns the URL escape normalizer function. 28 | func makeURLEscape(_ string) CheckFunc[reflect.Value] { 29 | return reflectURLEscape 30 | } 31 | -------------------------------------------------------------------------------- /url_escape_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestURLEscape(t *testing.T) { 15 | input := "param1/param2 = 1 + 2 & 3 + 4" 16 | expected := "param1%2Fparam2+%3D+1+%2B+2+%26+3+%2B+4" 17 | 18 | actual, err := v2.URLEscape(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectURLEscape(t *testing.T) { 29 | type Request struct { 30 | Query string `checkers:"url-escape"` 31 | } 32 | 33 | request := &Request{ 34 | Query: "param1/param2 = 1 + 2 & 3 + 4", 35 | } 36 | 37 | expected := "param1%2Fparam2+%3D+1+%2B+2+%26+3+%2B+4" 38 | 39 | errs, ok := v2.CheckStruct(request) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if request.Query != expected { 45 | t.Fatalf("actual %s expected %s", request.Query, expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /url_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | 12 | v2 "github.com/cinar/checker/v2" 13 | ) 14 | 15 | func ExampleIsURL() { 16 | _, err := v2.IsURL("https://example.com") 17 | if err != nil { 18 | fmt.Println(err) 19 | } 20 | } 21 | 22 | func TestIsURLInvalid(t *testing.T) { 23 | _, err := v2.IsURL("invalid-url") 24 | if err == nil { 25 | t.Fatal("expected error") 26 | } 27 | } 28 | 29 | func TestIsURLValid(t *testing.T) { 30 | _, err := v2.IsURL("https://example.com") 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func TestCheckURLNonString(t *testing.T) { 37 | defer FailIfNoPanic(t, "expected panic") 38 | 39 | type Website struct { 40 | Link int `checkers:"url"` 41 | } 42 | 43 | website := &Website{} 44 | 45 | v2.CheckStruct(website) 46 | } 47 | 48 | func TestCheckURLInvalid(t *testing.T) { 49 | type Website struct { 50 | Link string `checkers:"url"` 51 | } 52 | 53 | website := &Website{ 54 | Link: "invalid-url", 55 | } 56 | 57 | _, ok := v2.CheckStruct(website) 58 | if ok { 59 | t.Fatal("expected error") 60 | } 61 | } 62 | 63 | func TestCheckURLValid(t *testing.T) { 64 | type Website struct { 65 | Link string `checkers:"url"` 66 | } 67 | 68 | website := &Website{ 69 | Link: "https://example.com", 70 | } 71 | 72 | _, ok := v2.CheckStruct(website) 73 | if !ok { 74 | t.Fatal("expected valid") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /url_unescape.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2 7 | 8 | import ( 9 | "net/url" 10 | "reflect" 11 | ) 12 | 13 | // nameURLUnescape is the name of the URL unescape normalizer. 14 | const nameURLUnescape = "url-unescape" 15 | 16 | // URLUnescape applies URL unescaping to special characters. 17 | func URLUnescape(value string) (string, error) { 18 | unescaped, err := url.QueryUnescape(value) 19 | return unescaped, err 20 | } 21 | 22 | // reflectURLUnescape applies URL unescaping to special characters. 23 | func reflectURLUnescape(value reflect.Value) (reflect.Value, error) { 24 | newValue, err := URLUnescape(value.Interface().(string)) 25 | return reflect.ValueOf(newValue), err 26 | } 27 | 28 | // makeURLUnescape returns the URL unescape normalizer function. 29 | func makeURLUnescape(_ string) CheckFunc[reflect.Value] { 30 | return reflectURLUnescape 31 | } 32 | -------------------------------------------------------------------------------- /url_unescape_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023-2024 Onur Cinar. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | // https://github.com/cinar/checker 5 | 6 | package v2_test 7 | 8 | import ( 9 | "testing" 10 | 11 | v2 "github.com/cinar/checker/v2" 12 | ) 13 | 14 | func TestURLUnescape(t *testing.T) { 15 | input := "param1%2Fparam2+%3D+1+%2B+2+%26+3+%2B+4" 16 | expected := "param1/param2 = 1 + 2 & 3 + 4" 17 | 18 | actual, err := v2.URLUnescape(input) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if actual != expected { 24 | t.Fatalf("actual %s expected %s", actual, expected) 25 | } 26 | } 27 | 28 | func TestReflectURLUnescape(t *testing.T) { 29 | type Request struct { 30 | Query string `checkers:"url-unescape"` 31 | } 32 | 33 | request := &Request{ 34 | Query: "param1%2Fparam2+%3D+1+%2B+2+%26+3+%2B+4", 35 | } 36 | 37 | expected := "param1/param2 = 1 + 2 & 3 + 4" 38 | 39 | errs, ok := v2.CheckStruct(request) 40 | if !ok { 41 | t.Fatalf("got unexpected errors %v", errs) 42 | } 43 | 44 | if request.Query != expected { 45 | t.Fatalf("actual %s expected %s", request.Query, expected) 46 | } 47 | } 48 | --------------------------------------------------------------------------------