├── .githooks ├── commit-msg ├── install.py ├── post-commit └── pre-push ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_issue.md ├── pull_request_template.md └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── generator.go ├── go.mod ├── go.sum ├── requires_rules.go ├── rule_set.go ├── rules.go ├── tests ├── choices_test.go ├── complex_validator_test.go ├── custom_validators_from_generators_test.go ├── decrypt_test.go ├── email_test.go ├── float_test.go ├── from_slice_test.go ├── from_struct_test.go ├── int_test.go ├── len_range_test.go ├── len_test.go ├── map_test.go ├── max_test.go ├── min_test.go ├── non_empty_test.go ├── non_nil_test.go ├── non_zero_test.go ├── or_test.go ├── password_test.go ├── phone_test.go ├── regex_test.go ├── required_test.go ├── slice_test.go ├── string_test.go ├── struct_test.go ├── super_complex_test.go ├── tags │ ├── choices_test.go │ ├── tags_when_not_exist_all_test.go │ ├── tags_when_not_exist_one_test.go │ └── utils.go ├── translator_test.go ├── type_test.go ├── utils.go ├── when_exist_all_test.go ├── when_exist_one_test.go ├── when_not_exist_all_test.go ├── when_not_exist_one_test.go └── xor_test.go ├── utils.go └── validator.go /.githooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!env/bin/python 2 | # Conventional Commit Check and sticker set 3 | 4 | import sys, re 5 | 6 | f = open(sys.argv[1], 'r', encoding="utf-8") 7 | messages = f.readlines() 8 | message = "" 9 | for m in messages: 10 | if m[0] != "#" and m[0] != "\n": 11 | message += m 12 | message = message.strip() 13 | f.close() 14 | 15 | if len(message) == 0: 16 | print('no commit message provided') 17 | sys.exit(1) 18 | 19 | # Check if it is formatted before or not 20 | if message[0] in ['🎉', '🐛', '🔧', '📖', '🚀', '✨', '✅', '😬']: 21 | sys.exit(0) 22 | 23 | did_pass = re.fullmatch(r"^((feat|fix|refactor|docs|perf|style|test|¯\\_\(ツ\)_/¯)(\(\w+(\-\w+)?\))?(!)?(: (.*\s*)*))|(: merge (.*\s*)*)|(: initial commit$)", message) 24 | # https://stackoverflow.com/a/62293234/14967240 25 | 26 | if not did_pass: 27 | print("""your commit message does not follow this repositories `Conventional Commit` check: 28 | 29 | feat|fix|refactor|docs|perf|style|test(optional): |merge |initial commit""") 30 | sys.exit(1) 31 | 32 | first = messages[0].split(':')[0].split('(')[0] 33 | sticker = "" 34 | if first == "feat": 35 | sticker = "🎉 " 36 | elif first == "fix": 37 | sticker = "🐛 " 38 | elif first == "refactor": 39 | sticker = "🔧 " 40 | elif first == "docs": 41 | sticker = "📖 " 42 | elif first == "perf": 43 | sticker = "🚀 " 44 | elif first == "style": 45 | sticker = "✨ " 46 | elif first == "test": 47 | sticker = "✅ " 48 | elif "¯\_(ツ)_/¯" in messages[0]: 49 | sticker = "😬 " 50 | else: 51 | print('how are we here? please don\'t change the script') 52 | sys.exit(1) 53 | message = message.replace(first, sticker + first, 1) 54 | 55 | if first == "feat" or first == "fix" or first == "perf" or first == "test": 56 | 57 | try: 58 | f = open('CHANGELOG.rst', 'r', encoding="utf-8") 59 | contents = f.readlines() 60 | content = "" 61 | for c in contents: 62 | content += c 63 | f.close() 64 | 65 | if message in content: 66 | print("formatted commit message:\n\n" + message + "\n") 67 | 68 | else: 69 | content = content.replace("UNRELEASED\n----------\n\n", f"UNRELEASED\n----------\n\n* {message}\n") 70 | 71 | f = open('CHANGELOG.rst', 'w', encoding="utf-8") 72 | f.write(content) 73 | f.close() 74 | 75 | f = open('.commit', 'w') 76 | f.write("") 77 | f.close() 78 | except: 79 | print("There was a problem when I tried to update CHANGELOG.rst file.\nPlease restore every changes you made to the file with `git restore CHANGELOG.rst`.") 80 | sys.exit(1) 81 | 82 | f = open(sys.argv[1], 'w', encoding="utf-8") 83 | f.write(message) 84 | f.close() 85 | 86 | sys.exit(0) 87 | -------------------------------------------------------------------------------- /.githooks/install.py: -------------------------------------------------------------------------------- 1 | #!env/bin/python 2 | # Install hooks 3 | 4 | import os 5 | import sys 6 | 7 | # env folder creation 8 | print("setting up virtual env") 9 | os.system("python3 -m venv env") 10 | 11 | # configure git config 12 | text = "\thooksPath = .githooks" 13 | husky = "hooksPath = .husky" 14 | 15 | if not os.path.exists(".git"): 16 | print("no .git folder found") 17 | sys.exit(0) 18 | 19 | f = open(".git/config", "r") 20 | lines = f.readlines() 21 | lines_string = "" 22 | for line in lines: 23 | # adding config in core 24 | if line.strip() == "[core]": 25 | lines_string = line + text + "\n" 26 | continue 27 | # removing previous using husky tool 28 | if line.strip() != husky: 29 | lines_string += line 30 | # if configuration happened before, quit 31 | if line.strip().find(text.strip()) != -1: 32 | print("configurations happened before") 33 | sys.exit(0) 34 | f.close() 35 | 36 | f = open(".git/config", "w") 37 | f.write(lines_string + "\n") 38 | f.close() 39 | 40 | print("git hooks activated") -------------------------------------------------------------------------------- /.githooks/post-commit: -------------------------------------------------------------------------------- 1 | #!env/bin/python 2 | # Save CHANGELOG 3 | 4 | import os 5 | 6 | if os.path.isfile('.commit'): 7 | 8 | os.system('rm .commit') 9 | 10 | print("\nRecommit to add CHANGELOG.rst changes to the same commit:\n\n") 11 | 12 | os.system("git add CHANGELOG.rst") 13 | os.system("git commit --amend --no-edit") 14 | 15 | 16 | exit(0) -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!env/bin/python 2 | # Run test cases before push 3 | 4 | import os, sys 5 | 6 | packages = ["tests", "tests/tags"] 7 | 8 | for i in range(len(packages)): 9 | packages[i] = "./" + packages[i] 10 | 11 | f = open("go.mod") 12 | mod = f.readline() 13 | f.close() 14 | if mod == "": 15 | print( 16 | "go.mod file is unreadable, please restore the changes to the file. `git restore go.mod`" 17 | ) 18 | sys.exit(1) 19 | mod = mod.replace("module ", "").strip() 20 | 21 | try: 22 | for package in packages: 23 | if os.path.isdir(package): 24 | files = os.listdir(package) 25 | hast_test_files = False 26 | for file in files: 27 | if "_test.go" in file: 28 | hast_test_files = True 29 | if hast_test_files: 30 | print(f"Testing {mod}/{package.replace('./', '')} package:\n") 31 | if ( 32 | os.system( 33 | f"go clean -testcache && go test {mod}/{package.replace('./', '')}" 34 | ) 35 | != 0 36 | ): 37 | print( 38 | f"❌ {package.replace('./', '')} package test cases didn't succeed." 39 | ) 40 | sys.exit(1) 41 | except: 42 | print("❌ All test cases didn't succeed.") 43 | sys.exit(1) 44 | 45 | print("\n✅ All test cases succeed.") 46 | 47 | print("""\n✨ Thanks for your Contribution ✨\n""") 48 | 49 | sys.exit(0) 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: When I did this, that happened. 4 | title: "A sentence describing what causes which problem" 5 | labels: 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 | Steps to reproduce the behavior: 15 | 1. Run the function like... 16 | 2. Function panics because... 17 | 3. ... 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Environment:** 26 | - Go version: 1.17 27 | - Operation System: Ubuntu 20 28 | - ... 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Issue 3 | about: I want to add [this] new feature. 4 | title: "[this], does that" 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Feature Description** 11 | A clear and concise description of input and output of your new function or describe your new feature. 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Why: 2 | 3 | Added new feature, function, package or ... 4 | 5 | Closes # 6 | 7 | 8 | ### What's being changed: 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: GoTests-Main 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@v2 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.17 19 | 20 | - name: Install Dependencies 21 | run: go mod download -x 22 | 23 | - name: Test 24 | run: go test ./... 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # go 2 | 3 | main.go 4 | 5 | # vscode 6 | 7 | .vscode 8 | 9 | # env 10 | 11 | env 12 | .env 13 | 14 | # git 15 | .commit 16 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | UNRELEASED 5 | ---------- 6 | 7 | 2.1.2 (2025-07-13) 8 | 9 | * 🐛 fix: required rule doesn't check for zero values 10 | 11 | 2.1.1 (2025-07-11) 12 | 13 | * 🐛 fix: password validation was a little bit limited 14 | 15 | 2.1.0 (2025-06-07) 16 | 17 | * 🎉 feat: added GetRule, GetStructRule in ruleSet and GetValidator with GetChildrenValidator in Validator 18 | 19 | 2.0.0 (2024-05-07) 20 | ------------------ 21 | 22 | * 🎉 feat: a context goes through the validators now 23 | 24 | 1.4.4 (2024-05-07) 25 | ------------------ 26 | 27 | * 🐛 fix: fixed #11 reported bug 28 | 29 | 1.4.3 (2023-10-10) 30 | ------------------ 31 | 32 | * 🐛 fix: add custom messages for rules with params is now possible 33 | 34 | 1.4.2 (2023-08-21) 35 | ------------------ 36 | 37 | * 🐛 fix: unexported fields will get ignored to prevent unwanted behavior 38 | 39 | 1.4.1 (2023-08-20) 40 | ------------------ 41 | 42 | * 🐛 fix: WhenNotExistAll and WhenNotExistOne were not usable in tags + Validator does not add a type check validator anymore + bug fixed on after passing required 43 | 44 | 1.4.0 (2023-08-20) 45 | ------------------ 46 | 47 | * ✅ test: test cases updated for new features 48 | * 🎉 feat: field names will get translated 49 | * 🎉 feat: ability to pass custom validators from generators to ruleSet is now possible 50 | * 🎉 feat: added AlwaysCheckRules method in ruleSet 51 | * 🎉 feat: added WhenNotExistOne + WhenNotExistAll 52 | 53 | 1.3.5 (2023-08-19) 54 | ------------------ 55 | 56 | * 🐛 fix: bug fixed on setting error messages for conflicted keys in tags 57 | 58 | 1.3.4 (2023-07-11) 59 | ------------------ 60 | 61 | * 🐛 fix: children rules can now have no value (they had before for some reason) 62 | 63 | 1.3.3 (2023-07-10) 64 | ------------------ 65 | 66 | * 🐛 fix: fixed problem on Phone number rule + fixed bug on setDefaultOn function 67 | 68 | 1.3.2 (2023-05-02) 69 | ------------------ 70 | 71 | * 🐛 fix: phone number rule will be just valid with international phone numbers formats 72 | 73 | 1.3.1 (2023-04-30) 74 | ------------------ 75 | 76 | * 🐛 fix: mixed a bug in phonenumber verification rule and made it better 77 | 78 | 1.3.0 (2023-01-28) 79 | ------------------ 80 | 81 | * 🎉 feat: added new functions to use in patch method or other scenarios: 82 | 1. `SetDefaultOnNil` is added which is used in PATCH method. 83 | 2. `SetDefault` is added which is used in scenarios that you want to have default passed values in nil, zero or empty of element positions. 84 | 85 | 1.2.0 (2022-12-20) 86 | ------------------ 87 | 88 | * 🎉 feat: translator feature added 89 | 90 | 1.1.2 (2022-12-16) 91 | ------------------ 92 | 93 | * 🐛 fix: some odd behaviors of creating a validator from a generator fixed 94 | * 📖 docs: documentation updated 95 | 96 | 1.1.1 (2022-12-11) 97 | ------------------ 98 | 99 | * ✅ test: added test cases for ComplexValidator method 100 | * 🎉 feat: ComplexValidator added to Generator for creating a complex(struct or map) from zero level 101 | 102 | 1.1.0 (2022-11-29) 103 | ------------------ 104 | 105 | * ✅ test: test cases for new feature added 106 | * 🎉 feat: support for errors returned from [gin](https://github.com/gin-gonic/gin)'s Bind actions added 107 | 108 | 1.0.0 (2022-11-09) 109 | ------------------ 110 | 111 | * 🎉 feat: added CustomMessages method for generator 112 | * 🎉 feat: #1 feature added, added `c.` operator in tags to use when need to define rules for children of a slice element. 113 | * 🐛 fix: #4 114 | 115 | 0.0.2 (2022-11-05) 116 | ------------------ 117 | 118 | * 🐛 fix: #1, Pointers of different variables when used inside `g.Validator` or `Validator.Validate` methods, didn't work properly. 119 | 120 | 0.0.1 (2022-10-30) 121 | ------------------ 122 | 123 | * 🐛 fix: two bugs fixed 124 | 1. Capital letters in tags didn't register right in validator system for error SpecialMessages 125 | 2. More than one special error message in tags, just kept the last one in memory 126 | 127 | 0.0.0 (2022-09-29) 128 | ------------------ 129 | 130 | * ✅ test: test cases for many scenarios added 131 | * 🐛 fix: Huge amount of bug fixes happened 132 | * 🎉 feat: validator creation from a slice is possible 133 | * 🎉 feat: OR and XOR is possible inside tags 134 | * 🎉 feat: Choices, WhenExistOne and WhenExistAll are possible by tags 135 | * 🎉 feat: adding specificMessages inside a ruleSet is possible 136 | * 🎉 feat: custom functions can be defined by elements tags 137 | * 🎉 feat: added slice validation + more complex validations like complex inside them is possible 138 | * 🎉 feat: all fields are optional, until developer uses Required, NonNil, NonEmpty or NonZero rules or WhenExistAll or WhenExistOne 139 | * 🎉 feat: String rule added + struct validation added 140 | * 🎉 feat: added a name for ruleSets to customize output of fields 141 | * 🎉 feat: added WhenExistAll rule function + added Optional rule function 142 | * 🎉 feat: requires functionality added to core structure of library 143 | * 🎉 feat: choices function added 144 | * 🎉 feat: XOR rule added 145 | * 🎉 feat: OR rule added 146 | * 🎉 feat: added R as an alias for RuleSet 147 | * 🎉 feat: if pass as reference happens, Validate function now can handle it 148 | * 🎉 feat: Password validator added 149 | * 🎉 feat: struct and map can be used as input 150 | * 🎉 feat: custom function added 151 | * 🎉 feat: phone validator added 152 | * 🎉 feat: ability to add custom error messages for specific rules in specific fields added 153 | * 🎉 feat: Regex function added 154 | * 🎉 feat: Email validator added 155 | * 🎉 feat: NonEmpty function added 156 | * 🎉 feat: added NonNil and NonZero 157 | * 🎉 feat: added Len and LenRange functions in different meanings 158 | * 🎉 feat: Required function added 159 | * 🎉 feat: users can now pass rules' keys as PascalCase or snake_case 160 | * 🎉 feat: Min and Max function and options parameter added to use in error prints 161 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at mahmoodh1378@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to GoLodash contributing guide 2 | 3 | Thank you for investing your time in contributing to our project! :sparkles:. 4 | 5 | Read our [Code of Conduct](/CODE_OF_CONDUCT.md) to keep our community approachable and respectable. 6 | 7 | In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. 8 | 9 | Use the table of contents icon on the top left corner of this document to get to a specific section of this guide quickly. 10 | 11 | ## New contributor guide 12 | 13 | To get an overview of the project, read the [README](/README.md). 14 | 15 | ## Contributor setup 16 | 17 | Install requirements for contributing: 18 | 1. Please install python3 on your system. (It is needed for scripts to format your commit messages and handle [CHANGELOG.rst](/CHANGELOG.rst) file) 19 | 2. Install venv module for python (linux users only): 20 | - `sudo apt-get install python3-venv` 21 | - If you have python3.10, install it like: `sudo apt-get install python3.10-venv` 22 | 3. Run `install.py` script like: 23 | - `python3 ./.githooks/install.py` 24 | 25 | ### Issues 26 | 27 | #### Create a new issue 28 | 29 | 1. **Bug**: If you spot a problem with a certain release or a problem, create a bug issue. 30 | 2. **Feature**: If you want to add a new functionality to the project, create a feature issue. 31 | 32 | ### Make changes 33 | 34 | **Note**: This workflow is designed based on git flow but please read till the end before doing anything. 35 | 1. Pick or create a feature or bug issue. 36 | 2. Fork the repository. 37 | 3. Create a new branch. 38 | 4. Do your changes on that branch. 39 | 5. Debug and be sure about the changes. 40 | 6. Add documentation for new functions and variables and any other new things you provided. 41 | 7. Commit and push your changes. (see [commit message guidelines](#commit-message-guidelines)) 42 | 8. Create a pull request and **mention** your issue number inside the pull request. 43 | 44 | ### Changelog 45 | 46 | Mostly do not touch or change the file and let the script handle it. 47 | 48 | ### Commit message guidelines 49 | 50 | There is a template for how to commit, this template is based on [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/): 51 | 52 | - **\(\): \** 53 | 54 | Samples: 55 | 56 | ``` 57 | docs(changelog): update changelog to beta.5 58 | ``` 59 | 60 | #### Type (Essential) 61 | 62 | * docs: Documentation only changes 63 | * feat: A new feature 64 | * fix: A bug fix 65 | * perf: A code change that improves performance 66 | * refactor: A code change that neither fixes a bug nor adds a feature 67 | * style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 68 | * test: Adding missing tests or correcting existing tests 69 | 70 | #### Scope (Optional) 71 | 72 | This section can be written in two formats: 73 | 1. (\-\) 74 | 2. (\) 75 | 76 | **Note**: If you don't specify this part, remove parenthesis too. 77 | 78 | #### Subject (Essential) 79 | 80 | A brief description about what just happened. 81 | 82 | ## Versioning (Extra Section) 83 | 84 | This is just a reminder for everyone to know what versioning system we are using. 85 | 86 | Versioning in this project is based on semantic versioning: 87 | 88 | v**Major**.**Minor**.**Patch**-**PreReleaseIdentifier** 89 | 90 | Example: 91 | - v1.4.0-beta.1 92 | 93 | ### Major Version 94 | 95 | Signals backward-incompatible changes in a module’s public API. This release carries no guarantee that it will be backward compatible with preceding major versions. 96 | 97 | ### Minor Version 98 | 99 | Signals backward-compatible changes to the module’s public API. This release guarantees backward compatibility and stability. 100 | 101 | ### Patch Version 102 | 103 | Signals changes that don’t affect the module’s public API or its dependencies. This release guarantees backward compatibility and stability. 104 | 105 | ### Pre-release Version 106 | 107 | Signals that this is a pre-release milestone, such as an alpha or beta. This release carries no stability guarantees. 108 | 109 | ### More information 110 | 111 | For more information about versioning, read [this](https://go.dev/doc/modules/version-numbers). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 GoLodash Founders and other contributors 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Galidator 2 | 3 | Galidator provides general use case for validation purpose.\ 4 | Just simply create a validator and validate your data with it.\ 5 | Either it returns `nil` which means that data is valid and your rules all passed or not which means that there 6 | is/are problem/problems with passed data and validation has failed. 7 | 8 | ## Installation 9 | 10 | Just use `go get` for installation: 11 | 12 | ``` 13 | go get github.com/golodash/galidator/v2 14 | ``` 15 | 16 | And then just import the package into your own code. 17 | 18 | ```go 19 | import "github.com/golodash/galidator/v2" 20 | ``` 21 | 22 | ## Generator 23 | 24 | ### What is a Generator? 25 | 26 | Generator is a validator generator which creates them under some common circumstances.\ 27 | For example, with `generator.CustomMessages` method you can change default error messages of rules for every other validator that is gonna get created with this generator. 28 | 29 | With this mechanism, you can change some default behavior even before attempting to create your validator. 30 | 31 | ### How to Create A Generator? 32 | 33 | ```go 34 | var g = galidator.NewGenerator() 35 | or 36 | var g = galidator.New() 37 | or 38 | var g = galidator.G() 39 | ``` 40 | 41 | All three does the same. 42 | 43 | ## Validator 44 | 45 | ### What is a Validator? 46 | 47 | Validator is a `galidator.Validator` interface that gets created under some circumstances that its generator defines + common environmental variables of galidator defines + your specified rules. 48 | 49 | ### How to Create a Validator? 50 | 51 | To create a validator, first you need to create a unique generator instance and then use generator to call a method to create your validator. 52 | 53 | 1. `Validator(input interface{}, messages ...Messages) Validator`: 54 | 1. `input` can be a `ruleSet`. (which gets created by `generator.R() or generator.RuleSet()`)\ 55 | Example down here accepts just an email string: 56 | ```go 57 | var g = galidator.G() 58 | var validator = g.Validator(g.R().Email()) 59 | ``` 60 | 2. `input` can be a `struct instance` with tags that define rules on every field.\ 61 | Example down here accepts a map or a struct which has a `Email` key and a value which is a valid email: 62 | ```go 63 | type user struct { 64 | Email string `galidator:"email"` // instead of `galidator:"email"`, `g:"email"` can be used 65 | } 66 | var g = galidator.G() 67 | var validator = g.Validator(user{}) 68 | ``` 69 | 3. `messages` input type is obvious and is used to replace common error messages on rules for just this validator. 70 | 71 | 2. `ComplexValidator(rules Rules, messages ...Messages) Validator`: 72 | - Generates a struct/map validator.\ 73 | Mostly used for complex scenarios that start with a struct or map view. 74 | ```go 75 | var g = galidator.G() 76 | var validator = g.ComplexValidator(galidator.Rules{ 77 | "Name": g.RuleSet("name").Required(), 78 | "Description": g.RuleSet("description").Required(), 79 | }) 80 | ``` 81 | 82 | ### How to Validate? 83 | 84 | Simply just create your validator with one of the top discussed methods and then: 85 | 86 | ```go 87 | var g = galidator.G() 88 | var emailValidator = g.Validator(g.R().Email()) 89 | 90 | func main() { 91 | input1 := "valid@email.com" 92 | input2 := "invalidEmail.com" 93 | 94 | output1 := emailValidator.Validate(context.TODO(), input1) 95 | output2 := emailValidator.Validate(context.TODO(), input2) 96 | 97 | fmt.Println(output1) 98 | fmt.Println(output2) 99 | } 100 | ``` 101 | 102 | output: 103 | ``` 104 | 105 | [not a valid email address] 106 | ``` 107 | 108 | And that's it, just to get better, see more examples down here. 109 | 110 | ## What is the Usecase of Sending a context to Validate function? 111 | 112 | In web development, sometimes we need to share some data with our custom validators and this is the way we do it, we record it in the context and send it to the validator which can read the sent data in out custom validators by calling `ctx.Value(key)` method. 113 | 114 | # Just For [Gin](https://github.com/gin-gonic/gin) Users 115 | 116 | ## 1. Use Galidator Just to Customize [Gin](https://github.com/gin-gonic/gin)'s Bind Method Error Outputs 117 | 118 | You can choose not to use galidator and it's validation process but instead use `Bind` method of [Gin](https://github.com/gin-gonic/gin) or other acronym's for `Bind` like: `BindJson` for validation process and just use galidator to change output error messages of it. 119 | 120 | Example of using galidator inside a gin project: 121 | 122 | ```go 123 | type login struct { 124 | Username string `json:"username" binding:"required" required:"$field is required"` 125 | Password string `json:"password"` 126 | } 127 | 128 | var ( 129 | g = galidator.G() 130 | validator = g.Validator(login{}) 131 | ) 132 | 133 | func test(c *gin.Context) { 134 | req := &login{} 135 | 136 | // Parse json 137 | if err := c.BindJSON(req); err != nil { 138 | c.JSON(400, gin.H{ 139 | // This is the part which generates that output 140 | "message": validator.DecryptErrors(err), 141 | }) 142 | return 143 | } 144 | 145 | c.JSON(200, gin.H{ 146 | "good": 200, 147 | }) 148 | } 149 | 150 | func main() { 151 | r := gin.Default() 152 | r.POST("/", test) 153 | r.Run("127.0.0.1:3000") 154 | } 155 | ``` 156 | 157 | If you don't send `username` or send it empty in json request body, this message returns: 158 | ``` 159 | {"message":{"username":"username is required"}} 160 | ``` 161 | 162 | **Note**: In cases which there is a conflict of names for rule messages(like `json`, `json` tag is used for output in json and if you want to add error message of json for it, you can't use `json` tag, so the solution is to use `_json` tag) 163 | 164 | Example: 165 | 166 | ```go 167 | type login struct { 168 | Field string `json:"field" binding:"required,json" _json:"$field is not json"` 169 | } 170 | ``` 171 | 172 | ## 2. Translate Error Output to Different Languages in [Gin]((https://github.com/gin-gonic/gin)) 173 | 174 | If you need to translate output error messages for different languages in a gin project, use this template: 175 | 176 | ```go 177 | type login struct { 178 | Username string `json:"username" g:"required" required:"$field is required"` 179 | Password string `json:"password"` 180 | } 181 | 182 | var ( 183 | g = galidator.New() 184 | validator = g.Validator(login{}) 185 | // Persian Language Dictionary 186 | faDictionary = map[string]string{ 187 | "$field is required": "$field نمیتواند خالی باشد", 188 | } 189 | ) 190 | 191 | // Persian Language Translator 192 | func PersianTranslator(input string) string { 193 | if translated, ok := faDictionary[input]; ok { 194 | return translated 195 | } 196 | return input 197 | } 198 | 199 | // Middleware that assigns a translator requested by user 200 | func customizeTranslator(c *gin.Context) { 201 | languageCode := c.GetHeader("Accept-Language") 202 | if languageCode == "fa" { 203 | c.Set("translator", PersianTranslator) 204 | } else { 205 | c.Set("translator", func(input string) string { return input }) 206 | } 207 | c.Next() 208 | } 209 | 210 | // Main Handler 211 | func loginHandler(c *gin.Context) { 212 | req := &login{} 213 | translator := c.MustGet("translator").(func(string) string) 214 | 215 | // Parse json 216 | if err := c.BindJSON(req); err != nil { 217 | c.JSON(400, gin.H{ 218 | "message": "bad json", 219 | }) 220 | return 221 | } 222 | 223 | // Validation 224 | if errors := validator.Validate(context.TODO(), req, translator); errors != nil { 225 | c.JSON(400, gin.H{ 226 | "errors": errors, 227 | "message": "bad inputs", 228 | }) 229 | return 230 | } 231 | 232 | c.JSON(200, gin.H{ 233 | "good": 200, 234 | }) 235 | } 236 | 237 | func main() { 238 | r := gin.Default() 239 | groupWithMiddleware := r.Group("/", customizeTranslator) 240 | groupWithMiddleware.POST("/", loginHandler) 241 | r.Run("127.0.0.1:3000") 242 | } 243 | ``` 244 | 245 | Now if you make a post request to http://127.0.0.1:3000 url when having `Accept-Language` header with `fa` value assigned to it, and in request body do not specify username field or specify it's value as an empty string, this will be the output: 246 | 247 | ``` 248 | {"errors":{"username":["username نمیتواند خالی باشد"]},"message": "bad inputs"} 249 | ``` 250 | 251 | ## 3. Making Ease of PATCH method with Galidator in [Gin]((https://github.com/gin-gonic/gin)) 252 | 253 | 1. Make all your field types pointer. (string -> *string) 254 | 2. Use `SetDefaultOnNil` method which is accessible from a `Validator` instance. 255 | - Have in mind to pass pointer to a struct variable into `SetDefaultOnNil` method. 256 | 3. Done... items are nil if user did not send them to api and will be filled with default values which programmer passed them. 257 | 258 | ```go 259 | type article struct { 260 | Title *string `json:"title"` 261 | Content *string `json:"content"` 262 | } 263 | 264 | var ( 265 | title = "This is first" 266 | content = "This is the content" 267 | articles = []article{ 268 | {Title: &title, Content: &content}, 269 | } 270 | g = galidator.G() 271 | validator = g.Validator(article{}) 272 | ) 273 | 274 | func patchArticle(c *gin.Context) { 275 | req := &article{} 276 | id, _ := strconv.Atoi(c.Param("id")) 277 | defaults := articles[id] 278 | 279 | c.BindJSON(req) 280 | if err := validator.Validate(context.TODO(), req); err == nil { 281 | // This is the part to set default value for nil fields 282 | validator.SetDefaultOnNil(req, defaults) 283 | // This is update action 284 | articles[id] = *req 285 | } else { 286 | c.JSON(400, gin.H{ 287 | "message": "error in validation", 288 | }) 289 | } 290 | 291 | c.JSON(200, gin.H{ 292 | "good": 200, 293 | "data": req, 294 | }) 295 | } 296 | 297 | func main() { 298 | r := gin.Default() 299 | r.PATCH("/comments/:id", patchArticle) 300 | r.Run("127.0.0.1:3000") 301 | } 302 | ``` 303 | 304 | # Examples 305 | 306 | ## Simple Usage(Register a User) 307 | 308 | Lets validate a register form: 309 | 310 | ```go 311 | package main 312 | 313 | import ( 314 | "fmt" 315 | "context" 316 | 317 | "github.com/golodash/galidator/v2" 318 | ) 319 | 320 | func main() { 321 | g := galidator.New() 322 | validator := g.ComplexValidator(galidator.Rules{ 323 | "Username": g.R("username").Required().Min(3).Max(32), 324 | "Password": g.R("password").Required().Password(), 325 | "Email": g.R("email").Required().Email(), 326 | }) 327 | 328 | userInput := map[string]string{ 329 | "username": "DoctorMK", 330 | "password": "123456789", 331 | "email": "DoctorMK@gmail.com", 332 | } 333 | 334 | errors := validator.Validate(context.TODO(), userInput) 335 | 336 | fmt.Println(errors) 337 | fmt.Println(errors == nil) 338 | } 339 | ``` 340 | 341 | Output: 342 | ``` 343 | map[password:[password must be at least 8 characters long and contain one lowercase, one uppercase, one special and one number character]] 344 | false 345 | ``` 346 | 347 | We can even validate a struct by the same validator and get the same result: 348 | 349 | ```go 350 | package main 351 | 352 | import ( 353 | "fmt" 354 | "context" 355 | 356 | "github.com/golodash/galidator/v2" 357 | ) 358 | 359 | type Person struct { 360 | Username string 361 | Password string 362 | Email string 363 | } 364 | 365 | func main() { 366 | g := galidator.New() 367 | validator := g.Validator(galidator.Rules{ 368 | "Username": g.R("username").Required().Min(3).Max(32), 369 | "Password": g.R("password").Required().Min(5).Password(), 370 | "Email": g.R("email").Required().Email(), 371 | }) 372 | 373 | userInput := Person{ 374 | Username: "DoctorMK", 375 | Password: "123456789", 376 | Email: "DoctorMK@gmail.com", 377 | } 378 | 379 | errors := validator.Validate(context.TODO(), userInput) 380 | 381 | fmt.Println(errors) 382 | fmt.Println(errors == nil) 383 | } 384 | ``` 385 | 386 | Or we can even create the same validator by defining some struct tags. 387 | 388 | ```go 389 | package main 390 | 391 | import ( 392 | "fmt" 393 | "context" 394 | 395 | "github.com/golodash/galidator/v2" 396 | ) 397 | 398 | type Person struct { 399 | Username string `json:"username" g:"required,min=3,max=32"` 400 | Password string `json:"password" g:"required,min=5,password"` 401 | Email string `json:"email" g:"required,email"` 402 | } 403 | 404 | func main() { 405 | g := galidator.New() 406 | validator := g.Validator(Person{}) 407 | 408 | userInput := Person{ 409 | Username: "DoctorMK", 410 | Password: "123456789", 411 | Email: "DoctorMK@gmail.com", 412 | } 413 | 414 | errors := validator.Validate(context.TODO(), userInput) 415 | 416 | fmt.Println(errors) 417 | fmt.Println(errors == nil) 418 | } 419 | ``` 420 | 421 | ## Receive a list of users 422 | 423 | ```go 424 | package main 425 | 426 | import ( 427 | "fmt" 428 | "context" 429 | 430 | "github.com/golodash/galidator/v2" 431 | ) 432 | 433 | type Person struct { 434 | Username string `json:"username" g:"required,min=3,max=32"` 435 | Password string `json:"password" g:"required,min=5,password" password:"$field failed"` 436 | Email string `json:"email" g:"required,email"` 437 | } 438 | 439 | func main() { 440 | g := galidator.New() 441 | validator := g.Validator([]Person{}) 442 | 443 | userInput := []Person{ 444 | { 445 | Username: "DoctorMK", 446 | Password: "123456789", 447 | Email: "DoctorMK@gmail.com", 448 | }, 449 | { 450 | Username: "Asghar", 451 | Password: "123456789mH!@", 452 | Email: "Doctors@gmail.com", 453 | }, 454 | } 455 | 456 | errors := validator.Validate(context.TODO(), userInput) 457 | 458 | fmt.Println(errors) 459 | fmt.Println(errors == nil) 460 | } 461 | ``` 462 | 463 | Output: 464 | ``` 465 | map[0:map[password:[password failed]]] 466 | false 467 | ``` 468 | 469 | We can create the same validator without tags too: 470 | 471 | ```go 472 | package main 473 | 474 | import ( 475 | "fmt" 476 | "context" 477 | 478 | "github.com/golodash/galidator/v2" 479 | ) 480 | 481 | func main() { 482 | g := galidator.New() 483 | validator := g.Validator(g.R().Children( 484 | g.R().Complex(galidator.Rules{ 485 | "Username": g.R("username").Required().Min(3).Max(32), 486 | "Password": g.R("password").Required().Password().SpecificMessages(galidator.Messages{"password": "$field failed"}), 487 | "Email": g.R("email").Required().Email(), 488 | }))) 489 | 490 | userInput := []map[string]string{ 491 | { 492 | "username": "DoctorMK", 493 | "password": "123456789", 494 | "email": "DoctorMK@gmail.com", 495 | }, 496 | { 497 | "username": "Asghar", 498 | "password": "123456789mH!@", 499 | "email": "Doctors@gmail.com", 500 | }, 501 | } 502 | 503 | errors := validator.Validate(context.TODO(), userInput) 504 | 505 | fmt.Println(errors) 506 | fmt.Println(errors == nil) 507 | } 508 | ``` 509 | 510 | ## OR 511 | 512 | In this example, input has to be either an email address or just a string longer equal to 5 characters or both. 513 | 514 | This example OR operator in struct tags can be used like: `g:"required,or=email|string+min=5"` 515 | 516 | ```go 517 | package main 518 | 519 | import ( 520 | "fmt" 521 | "context" 522 | 523 | "github.com/golodash/galidator/v2" 524 | ) 525 | 526 | func main() { 527 | g := galidator.New() 528 | validator := g.Validator(g.R().Required().OR(g.R().Email(), g.R().String().Min(5))) 529 | 530 | input := "m@g.com" 531 | errors := validator.Validate(context.TODO(), input) 532 | 533 | fmt.Println(errors) 534 | fmt.Println(errors == nil) 535 | } 536 | ``` 537 | 538 | Output: 539 | ``` 540 | 541 | true 542 | ``` 543 | 544 | ## XOR 545 | 546 | In this example, input has to be either an email address or phone number. 547 | 548 | This example XOR operator in struct tags can be used like: `g:"required,xor=email|phone"` 549 | 550 | ```go 551 | package main 552 | 553 | import ( 554 | "fmt" 555 | "context" 556 | 557 | "github.com/golodash/galidator/v2" 558 | ) 559 | 560 | func main() { 561 | g := galidator.New() 562 | validator := g.Validator(g.R().Required().XOR(g.R().Email(), g.R().Phone())) 563 | 564 | input := "m@g.com" 565 | errors := validator.Validate(context.TODO(), input) 566 | 567 | fmt.Println(errors) 568 | fmt.Println(errors == nil) 569 | } 570 | ``` 571 | 572 | Output: 573 | ``` 574 | 575 | true 576 | ``` 577 | 578 | ## WhenExistAll - WhenExistOne 579 | 580 | In this example, if two other struct fields(`Username` and `Password`) are not empty, 581 | nil or zero, field will act as a required field and all of its rules will get checked.\ 582 | Otherwise, if empty, nil or zero value get passed, because by default fields are 583 | optional, it does not check other defined rules and assume it passed.\ 584 | You can use this in tags like: `g:"when_exist_all=Username&Password,string" when_exist_all:"when_exist_all failed"` 585 | 586 | ```go 587 | package main 588 | 589 | import ( 590 | "fmt" 591 | "context" 592 | 593 | "github.com/golodash/galidator/v2" 594 | ) 595 | 596 | func main() { 597 | g := galidator.New() 598 | v := g.ComplexValidator(galidator.Rules{ 599 | "Username": g.R("username").String(), 600 | "Password": g.R("password").String(), 601 | "Data": g.R("data").WhenExistAll("Username", "Password").String().SpecificMessages(galidator.Messages{"when_exist_all": "when_exist_all failed"}), 602 | }) 603 | 604 | errors := v.Validate(context.TODO(), map[string]string{ 605 | "Username": "username", 606 | "Password": "password", 607 | "Data": "", 608 | }) 609 | 610 | fmt.Println(errors) 611 | fmt.Println(errors == nil) 612 | } 613 | ``` 614 | 615 | Output: 616 | ``` 617 | map[data:[when_exist_all failed]] 618 | false 619 | ``` 620 | 621 | ## Custom Validator 622 | 623 | ```go 624 | package main 625 | 626 | import ( 627 | "fmt" 628 | "context" 629 | 630 | "github.com/golodash/galidator/v2" 631 | "github.com/golodash/godash/slices" 632 | ) 633 | 634 | type Person struct { 635 | Username string `json:"username" g:"required,min=3,max=32,duplicate_check" duplicate_check:"$field is duplicate"` 636 | Password string `json:"password" g:"required,min=5,password"` 637 | Email string `json:"email" g:"required,email"` 638 | } 639 | 640 | var users = []string{ 641 | "ali", 642 | "james", 643 | "john", 644 | } 645 | 646 | func duplicate_check(ctx context.Context, input interface{}) bool { 647 | return slices.FindIndex(users, input) == -1 648 | } 649 | 650 | func main() { 651 | g := galidator.New().CustomValidators(galidator.Validators{"duplicate_check": duplicate_check}) 652 | validator := g.Validator(Person{}) 653 | 654 | userInput := Person{ 655 | Username: "ali", 656 | Password: "123456789mH!", 657 | Email: "DoctorMK@gmail.com", 658 | } 659 | 660 | errors := validator.Validate(context.TODO(), userInput) 661 | 662 | fmt.Println(errors) 663 | fmt.Println(errors == nil) 664 | } 665 | ``` 666 | 667 | Output: 668 | ``` 669 | map[username:[username is duplicate]] 670 | false 671 | ``` 672 | 673 | ## LenRange 674 | 675 | LenRange can be used in a struct tag like: `g:"len_range=3&5" len_range="len_range failed"` 676 | 677 | ```go 678 | package main 679 | 680 | import ( 681 | "fmt" 682 | "context" 683 | 684 | "github.com/golodash/galidator/v2" 685 | ) 686 | 687 | func main() { 688 | g := galidator.New() 689 | validator := g.Validator(g.R().LenRange(3, 5).SpecificMessages(galidator.Messages{"len_range": "len_range failed"})) 690 | 691 | userInput := 3 692 | 693 | errors := validator.Validate(context.TODO(), userInput) 694 | 695 | fmt.Println(errors) 696 | fmt.Println(errors == nil) 697 | } 698 | ``` 699 | 700 | Output: 701 | ``` 702 | [len_range failed] 703 | false 704 | ``` 705 | 706 | ## Changing Default Error Messages 707 | 708 | 1. Changing default error messages in generator layer: 709 | 710 | ```go 711 | package main 712 | 713 | import ( 714 | "fmt" 715 | "context" 716 | 717 | "github.com/golodash/galidator/v2" 718 | ) 719 | 720 | func main() { 721 | g := galidator.New().CustomMessages(galidator.Messages{ 722 | "string": "$value is not string", 723 | }) 724 | validator := g.Validator(g.R().String()) 725 | 726 | errors := validator.Validate(context.TODO(), 1) 727 | 728 | fmt.Println(errors) 729 | fmt.Println(errors == nil) 730 | } 731 | ``` 732 | 733 | output: 734 | ``` 735 | [1 is not string] 736 | ``` 737 | 738 | 2. Changing default error messages in validator layer (This layer overrides generator error messages-if same value that is defined in other layers gets defined in this layer too): 739 | 740 | ```go 741 | g := galidator.New().CustomMessages(galidator.Messages{ 742 | "string": "$value is not string", 743 | }) 744 | validator := g.Validator(g.R().String(), galidator.Messages{ 745 | "string": "not", 746 | }) 747 | ``` 748 | 749 | output: 750 | ``` 751 | [not] 752 | ``` 753 | 754 | 3. Changing default error messages in ruleSet layer (This layer overrides generator and validator error messages-if same value that is defined in other layers gets defined in this layer too): 755 | 756 | ```go 757 | g := galidator.New().CustomMessages(galidator.Messages{ 758 | "string": "$value is not string", 759 | }) 760 | validator := g.Validator(g.R().String().SpecificMessages(galidator.Messages{ 761 | "string": "not valid", 762 | }), galidator.Messages{ 763 | "string": "not", 764 | }) 765 | ``` 766 | 767 | output: 768 | ``` 769 | [not valid] 770 | ``` 771 | 772 | ## Defining `ruleSet` for Children of a Slice in Struct Tags 773 | 774 | If you need to define a rule for children of a slice in struct tags, you should use 775 | some proper prefix for those rules like: `c.` or `child.`\ 776 | And have in mind that with adding two or more of these prefixes, you keep digging in 777 | deeper layers. like: `child.child.child.min` or `c.c.c.min` or `c.child.c.min` or... means go deep three slices and add `min` rule to children of the last slice. 778 | 779 | example: 780 | 781 | ```go 782 | package main 783 | 784 | import ( 785 | "fmt" 786 | "context" 787 | 788 | "github.com/golodash/galidator/v2" 789 | ) 790 | 791 | type numbers struct { 792 | Numbers []int `g:"c.min=1,c.max=5" c.max:"$value is not <= $max" c.min:"$value is not >= $min"` 793 | } 794 | 795 | func main() { 796 | g := galidator.New() 797 | validator := g.Validator(numbers{}) 798 | 799 | fmt.Println(validator.Validate(context.TODO(), numbers{ 800 | Numbers: []int{ 801 | 1, 802 | 0, 803 | 5, 804 | 35, 805 | }, 806 | })) 807 | } 808 | ``` 809 | 810 | output: 811 | ``` 812 | map[Numbers:map[1:[0 is not >= 1] 3:[35 is not <= 5]]] 813 | ``` 814 | 815 | ## Translator 816 | 817 | When calling `Validator.Validate` method with your data, you can pass a translator function to translate output of error messages to your desired language. 818 | 819 | For example: 820 | 821 | ```go 822 | var ( 823 | g = galidator.G() 824 | validator = g.Validator(g.R().Required()) 825 | translates = map[string]string{ 826 | "required": "this is required and it is translated", 827 | } 828 | ) 829 | 830 | func translator(input string) string { 831 | if out, ok := translates[input]; ok { 832 | return out 833 | } 834 | return input 835 | } 836 | 837 | func main() { 838 | fmt.Println(validator.Validate(context.TODO(), translator)) 839 | } 840 | ``` 841 | 842 | output: 843 | ```go 844 | [this is required and it is translated] 845 | ``` 846 | 847 | # Star History 848 | 849 | [![Star History Chart](https://api.star-history.com/svg?repos=golodash/galidator&type=Date)](https://star-history.com/#golodash/galidator&Date) 850 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package galidator 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | type ( 9 | // A struct to implement generator interface 10 | generatorS struct { 11 | // Custom validators 12 | customValidators Validators 13 | // Custom error messages 14 | messages Messages 15 | } 16 | 17 | // An interface to generate a validator or ruleSet 18 | generator interface { 19 | // Overrides current validators(if there is one) with passed validators 20 | // 21 | // Call this method before calling `generator.Validator` method to have effect 22 | CustomValidators(validators Validators) generator 23 | // Overrides current messages(if there is one) with passed messages 24 | // 25 | // Call this method before calling `generator.Validator` method to have effect 26 | CustomMessages(messages Messages) generator 27 | // Generates a validator interface which can be used to validate struct or map by some rules. 28 | // 29 | // `input` can be a ruleSet or a struct instance. 30 | // 31 | // Please use CapitalCase for rules' keys (Important for getting data out of struct types) 32 | Validator(input interface{}, messages ...Messages) Validator 33 | // Generates a validator interface which can be used to validate struct or map or slice by passing an instance of them 34 | validator(input interface{}) Validator 35 | // Generates a ruleSet to validate passed information 36 | // 37 | // Variable name will be used in output of error keys 38 | RuleSet(name ...string) ruleSet 39 | // An alias for RuleSet function 40 | R(name ...string) ruleSet 41 | // Generates a complex validator to validate maps and structs 42 | ComplexValidator(rules Rules, messages ...Messages) Validator 43 | } 44 | ) 45 | 46 | func (o *generatorS) CustomValidators(validators Validators) generator { 47 | o.customValidators = validators 48 | return o 49 | } 50 | 51 | func (o *generatorS) CustomMessages(messages Messages) generator { 52 | o.messages = messages 53 | return o 54 | } 55 | 56 | func (o *generatorS) Validator(rule interface{}, errorMessages ...Messages) Validator { 57 | var messages Messages = o.messages 58 | if len(errorMessages) != 0 { 59 | for key, value := range errorMessages[0] { 60 | messages[key] = value 61 | } 62 | } 63 | switch rule := rule.(type) { 64 | case ruleSet: 65 | break 66 | default: 67 | for reflect.TypeOf(rule).Kind() == reflect.Ptr { 68 | rule = reflect.ValueOf(rule).Elem().Interface() 69 | } 70 | } 71 | 72 | var output Validator = nil 73 | switch v := rule.(type) { 74 | case ruleSet: 75 | output = &validatorS{rule: v, rules: nil, messages: nil} 76 | default: 77 | if reflect.TypeOf(v).Kind() == reflect.Struct || reflect.TypeOf(v).Kind() == reflect.Slice { 78 | output = o.validator(v) 79 | } else { 80 | panic("'rule' has to be a ruleSet or a struct instance") 81 | } 82 | } 83 | deepPassMessages(output, &messages) 84 | 85 | return output 86 | } 87 | 88 | func (o *generatorS) validator(input interface{}) Validator { 89 | inputValue := reflect.ValueOf(input) 90 | inputType := reflect.TypeOf(input) 91 | r := o.RuleSet() 92 | if inputType.Kind() == reflect.Struct { 93 | rules := Rules{} 94 | for i := 0; i < inputType.NumField(); i++ { 95 | elementT := inputType.Field(i) 96 | element := inputValue.Field(i) 97 | for element.Type().Kind() == reflect.Ptr { 98 | elementType := element.Type().Elem() 99 | element = reflect.New(elementType).Elem() 100 | elementT.Type = element.Type() 101 | } 102 | tags := []string{elementT.Tag.Get("g"), elementT.Tag.Get("galidator")} 103 | r = o.RuleSet(elementT.Tag.Get("json")) 104 | 105 | if elementT.Type.Kind() == reflect.Struct || elementT.Type.Kind() == reflect.Map { 106 | validator := o.validator(element.Interface()) 107 | r.setDeepValidator(validator) 108 | } else if elementT.Type.Kind() == reflect.Slice { 109 | child := elementT.Type.Elem() 110 | if child.Kind() != reflect.Slice && child.Kind() != reflect.Struct && child.Kind() != reflect.Map { 111 | r.Children(o.R().Type(child)) 112 | } else { 113 | validator := o.validator(reflect.Zero(elementT.Type.Elem()).Interface()) 114 | r.setChildrenValidator(validator) 115 | } 116 | } 117 | 118 | // Add messages of rules 119 | for _, fullTag := range tags { 120 | filters := strings.Split(fullTag, ",") 121 | for j := 0; j < len(filters); j++ { 122 | tag := strings.SplitN(filters[j], "=", 2) 123 | 124 | normalFuncName := applyRules(r, tag, o, true) 125 | value := elementT.Tag.Get("_" + normalFuncName) 126 | if value == "" { 127 | value = elementT.Tag.Get(normalFuncName) 128 | } 129 | 130 | addSpecificMessage(r, normalFuncName, value) 131 | } 132 | } 133 | 134 | // Support for binding tag that is used for Bind actions 135 | if bindingTags := elementT.Tag.Get("binding"); bindingTags != "" { 136 | splits := strings.Split(bindingTags, ",") 137 | for j := 0; j < len(splits); j++ { 138 | rule := strings.Split(splits[j], "=") 139 | value := elementT.Tag.Get("_" + rule[0]) 140 | if value == "" { 141 | value = elementT.Tag.Get(rule[0]) 142 | } 143 | addSpecificMessage(r, rule[0], value) 144 | } 145 | } 146 | rules[elementT.Name] = r 147 | } 148 | 149 | return &validatorS{rules: rules} 150 | } else if inputType.Kind() == reflect.Slice { 151 | child := inputType.Elem() 152 | if child.Kind() != reflect.Slice && child.Kind() != reflect.Struct && child.Kind() != reflect.Map { 153 | r.Children(o.R().Type(child)) 154 | } else { 155 | validator := o.validator(reflect.Zero(child).Interface()) 156 | r.setChildrenValidator(validator) 157 | } 158 | 159 | return &validatorS{rule: r} 160 | } else if inputType.Kind() == reflect.Map { 161 | panic("do not use map in Validator creation based on struct elements tags") 162 | } else { 163 | r.Type(inputType) 164 | 165 | return &validatorS{rule: r} 166 | } 167 | } 168 | 169 | func (o *generatorS) RuleSet(name ...string) ruleSet { 170 | var output = "" 171 | if len(name) != 0 { 172 | output = name[0] 173 | } 174 | ruleSet := &ruleSetS{name: output, validators: Validators{}, requires: requires{}, options: options{}, isOptional: true} 175 | return ruleSet.setGeneratorCustomValidators(&o.customValidators) 176 | } 177 | 178 | func (o *generatorS) R(name ...string) ruleSet { 179 | return o.RuleSet(name...) 180 | } 181 | 182 | func (o *generatorS) ComplexValidator(rules Rules, errorMessages ...Messages) Validator { 183 | var messages Messages = o.messages 184 | if len(errorMessages) != 0 { 185 | for key, value := range errorMessages[0] { 186 | messages[key] = value 187 | } 188 | } 189 | output := &validatorS{rule: nil, rules: rules, messages: nil} 190 | 191 | deepPassMessages(output, &messages) 192 | return output 193 | } 194 | 195 | // Returns a new Generator 196 | func NewGenerator() generator { 197 | return &generatorS{ 198 | messages: Messages{}, 199 | customValidators: Validators{}, 200 | } 201 | } 202 | 203 | // An alias for `NewGenerator` funcion 204 | func New() generator { 205 | return NewGenerator() 206 | } 207 | 208 | // An alias for `NewGenerator` funcion 209 | func G() generator { 210 | return NewGenerator() 211 | } 212 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/golodash/galidator/v2 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/dlclark/regexp2 v1.7.0 7 | github.com/gin-gonic/gin v1.8.1 8 | github.com/go-playground/validator/v10 v10.11.1 9 | github.com/golodash/godash v1.2.0 10 | github.com/nyaruka/phonenumbers v1.1.6 11 | ) 12 | 13 | require ( 14 | github.com/gin-contrib/sse v0.1.0 // indirect 15 | github.com/go-playground/assert/v2 v2.2.0 // indirect 16 | github.com/go-playground/locales v0.14.0 // indirect 17 | github.com/go-playground/universal-translator v0.18.0 // indirect 18 | github.com/goccy/go-json v0.9.7 // indirect 19 | github.com/golang/protobuf v1.5.0 // indirect 20 | github.com/jinzhu/copier v0.3.5 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/leodido/go-urn v1.2.1 // indirect 23 | github.com/mattn/go-isatty v0.0.14 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 25 | github.com/modern-go/reflect2 v1.0.2 // indirect 26 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 27 | github.com/ugorji/go/codec v1.2.7 // indirect 28 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect 29 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 30 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect 31 | golang.org/x/text v0.3.7 // indirect 32 | google.golang.org/protobuf v1.28.0 // indirect 33 | gopkg.in/yaml.v2 v2.4.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= 6 | github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 7 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 8 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 9 | github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= 10 | github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= 11 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 12 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 13 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 14 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 15 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 16 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 17 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 18 | github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= 19 | github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= 20 | github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= 21 | github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= 22 | github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 23 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 25 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 26 | github.com/golodash/godash v1.2.0 h1:2TlNmAGeYzZYb07oWGuqDzKhpUvIzzy5l7URlG3Vrls= 27 | github.com/golodash/godash v1.2.0/go.mod h1:oKwxn9UMkI6aa9OiR56sRw7Z5SokrfZSybLjSQ6OB5s= 28 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 29 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= 32 | github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 33 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 34 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 35 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 36 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 37 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 38 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 39 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 40 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 41 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 42 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 43 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 44 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 45 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 46 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 47 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 48 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 49 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 50 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 51 | github.com/nyaruka/phonenumbers v1.1.6 h1:DcueYq7QrOArAprAYNoQfDgp0KetO4LqtnBtQC6Wyes= 52 | github.com/nyaruka/phonenumbers v1.1.6/go.mod h1:yShPJHDSH3aTKzCbXyVxNpbl2kA+F+Ne5Pun/MvFRos= 53 | github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU= 54 | github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 55 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 59 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 60 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 61 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 62 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 63 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 64 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 65 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 66 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 67 | github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= 68 | github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= 69 | github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= 70 | github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= 71 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 72 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= 73 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 74 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 75 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 76 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 77 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= 82 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 84 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 85 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 86 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 87 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 88 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 89 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 90 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 91 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 92 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 93 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 95 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 97 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 98 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 99 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 100 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 101 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 103 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | -------------------------------------------------------------------------------- /requires_rules.go: -------------------------------------------------------------------------------- 1 | package galidator 2 | 3 | // Returns false if one of the fields values is not empty, nil or zero and input is empty, nil or zero 4 | // 5 | // False means it is required and all validators have to check 6 | func whenExistOneRequireRule(fields ...string) func(interface{}) func(interface{}) bool { 7 | return func(all interface{}) func(interface{}) bool { 8 | fieldsValues := getValues(all, fields...) 9 | return func(input interface{}) bool { 10 | inputIsNil := isEmptyNilZero(input) 11 | for _, element := range fieldsValues { 12 | if !isEmptyNilZero(element) && inputIsNil { 13 | return false 14 | } 15 | } 16 | return true 17 | } 18 | } 19 | } 20 | 21 | // Returns false if all of the fields values are not empty, nil or zero and input is empty, nil or zero 22 | // 23 | // False means it is required and all validators have to check 24 | func whenExistAllRequireRule(fields ...string) func(interface{}) func(interface{}) bool { 25 | return func(all interface{}) func(interface{}) bool { 26 | fieldsValues := getValues(all, fields...) 27 | return func(input interface{}) bool { 28 | for _, element := range fieldsValues { 29 | if isEmptyNilZero(element) { 30 | return true 31 | } 32 | } 33 | return !isEmptyNilZero(input) 34 | } 35 | } 36 | } 37 | 38 | // Returns false if one of the fields values is empty, nil or zero and input is empty, nil or zero 39 | // 40 | // False means it is required and all validators have to check 41 | func whenNotExistOneRequireRule(fields ...string) func(interface{}) func(interface{}) bool { 42 | return func(all interface{}) func(interface{}) bool { 43 | fieldsValues := getValues(all, fields...) 44 | return func(input interface{}) bool { 45 | inputIsNil := isEmptyNilZero(input) 46 | for _, element := range fieldsValues { 47 | if isEmptyNilZero(element) && inputIsNil { 48 | return false 49 | } 50 | } 51 | return true 52 | } 53 | } 54 | } 55 | 56 | // Returns false if all of the fields values are empty, nil or zero and input is empty, nil or zero 57 | // 58 | // False means it is required and all validators have to check 59 | func whenNotExistAllRequireRule(fields ...string) func(interface{}) func(interface{}) bool { 60 | return func(all interface{}) func(interface{}) bool { 61 | fieldsValues := getValues(all, fields...) 62 | return func(input interface{}) bool { 63 | for _, element := range fieldsValues { 64 | if !isEmptyNilZero(element) { 65 | return true 66 | } 67 | } 68 | return !isEmptyNilZero(input) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rule_set.go: -------------------------------------------------------------------------------- 1 | package galidator 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | type ( 11 | // A map with data recorded to be used in returning error messages 12 | option map[string]string 13 | 14 | // A map of different rules as key with their own option 15 | options map[string]option 16 | 17 | // A map full of validators which is assigned for a single key in a validator struct 18 | Validators map[string]func(ctx context.Context, input interface{}) bool 19 | 20 | // A map full of field require determining 21 | requires map[string]func(interface{}) func(interface{}) bool 22 | 23 | // A struct to implement ruleSet interface 24 | ruleSetS struct { 25 | // The name that will be shown in output of the errors 26 | name string 27 | // Used to validate user's data 28 | validators Validators 29 | // Used to determine what needs to be required 30 | requires requires 31 | // Used in returning error messages 32 | options options 33 | // Sets messages for specific rules in current ruleSet 34 | specificMessages Messages 35 | // If isOptional is true, if empty is sent, all errors will be ignored 36 | isOptional bool 37 | // Holds data for more complex structures, like: 38 | // 39 | // map or struct 40 | deepValidator Validator 41 | // Defines type of elements of a slice 42 | childrenValidator Validator 43 | // Custom validators which is defined in generator 44 | customValidators *Validators 45 | } 46 | 47 | // An interface with some functions to satisfy validation purpose 48 | ruleSet interface { 49 | // Validates all validators defined 50 | validate(ctx context.Context, input interface{}) []string 51 | 52 | // Checks if input is int 53 | Int() ruleSet 54 | // Checks if input is float 55 | Float() ruleSet 56 | // Checks if input acts like: input >= min or len(input) >= min 57 | Min(min float64) ruleSet 58 | // Checks if input acts like: input <= max or len(input) <= max 59 | Max(max float64) ruleSet 60 | // Checks if input acts like: len(input) >= from && len(input) <= to 61 | // 62 | // If from == -1, no check on from will happen 63 | // If to == -1, no check on to will happen 64 | LenRange(from int, to int) ruleSet 65 | // Checks if input acts like: len(input) == length 66 | Len(length int) ruleSet 67 | // Makes this field required, Checks if input is not zero(0, "", ''), nil or empty 68 | Required() ruleSet 69 | // Makes this field optional 70 | Optional() ruleSet 71 | // If data is zero(0, "", ''), nil or empty, all validation rules will be checked anyway 72 | // 73 | // Note: Validations will not work by default if the value is zero(0, "", ''), nil or empty 74 | // 75 | // # Note: `Required`, `NonZero`, `NonNil` and `NonEmpty` will call this method by default 76 | // 77 | // Note: If not used, when an int is 0 and min is 5, min function will not activate 78 | // 79 | // Note: Field will be validated anyway from now on but if `Optional` method is called 80 | // after calling this method in continue of the current ruleSet chain, it has no effect 81 | AlwaysCheckRules() ruleSet 82 | // Checks if input is not zero(0, "", '') 83 | // 84 | // Note: Field will be required 85 | NonZero() ruleSet 86 | // Checks if input is not nil 87 | // 88 | // Note: Field will be required 89 | NonNil() ruleSet 90 | // Checks if input has items inside it 91 | // 92 | // Note: Field will be required 93 | NonEmpty() ruleSet 94 | // Checks if input is a valid email address 95 | Email() ruleSet 96 | // Validates inputs with passed pattern 97 | Regex(pattern string) ruleSet 98 | // Checks if input is a valid phone number 99 | Phone() ruleSet 100 | // Adds custom validators 101 | Custom(validators Validators) ruleSet 102 | // Adds one custom validator which is registered before in generator 103 | RegisteredCustom(validatorKeys ...string) ruleSet 104 | // Checks if input is a map 105 | Map() ruleSet 106 | // Checks if input is a slice 107 | Slice() ruleSet 108 | // Checks if input is a struct 109 | Struct() ruleSet 110 | // Adds another deeper layer to validation structure 111 | // 112 | // Can check struct and map 113 | Complex(rules Rules) ruleSet 114 | // If children of a slice is not struct or map, use this function and otherwise use Complex function after Slice function 115 | Children(rule ruleSet) ruleSet 116 | // Checks if input is a Specific type 117 | Type(input interface{}) ruleSet 118 | // Checks if input is at least 8 characters long, has one lowercase, one uppercase and one number character 119 | Password() ruleSet 120 | // Checks if at least one of passed ruleSets pass its own validation check 121 | OR(ruleSets ...ruleSet) ruleSet 122 | // Checks if XOR of all ruleSets passes 123 | XOR(ruleSets ...ruleSet) ruleSet 124 | // Gets a list of values and checks if input is one of them 125 | Choices(choices ...interface{}) ruleSet 126 | // Makes field required if at least one of passed fields are not empty, nil or zero(0, "", '') 127 | WhenExistOne(choices ...string) ruleSet 128 | // Makes field required if all passed fields are not empty, nil or zero(0, "", '') 129 | WhenExistAll(choices ...string) ruleSet 130 | // Makes field required if at least one of passed fields are empty, nil or zero(0, "", '') 131 | WhenNotExistOne(choices ...string) ruleSet 132 | // Makes field required if all passed fields are empty, nil or zero(0, "", '') 133 | WhenNotExistAll(choices ...string) ruleSet 134 | // Checks if input is a string 135 | String() ruleSet 136 | // Returns Validator of current Element (For map and struct elements) 137 | GetValidator() Validator 138 | // Returns Validator of children Elements (For slices) 139 | GetChildrenValidator() Validator 140 | 141 | // Adds a new pair of `key: value` message into existing SpecificMessages variable 142 | appendSpecificMessages(key string, value string) 143 | // Sets messages for specific rules in current ruleSet 144 | SpecificMessages(specificMessages Messages) ruleSet 145 | // Return specificMessages 146 | getSpecificMessages() Messages 147 | 148 | // Returns option of the passed ruleKey 149 | getOption(ruleKey string) option 150 | // Adds a new subKey with a value associated with it to option of passed ruleKey 151 | addOption(ruleKey string, subKey string, value string) 152 | // Makes the field optional 153 | optional() 154 | // Makes the field required 155 | required() 156 | // Returns true if the ruleSet has to pass all validators 157 | // 158 | // Returns false if the ruleSet can be empty, nil or zero(0, "", '') and is allowed to not pass any validations 159 | isRequired() bool 160 | // Replaces passed validator with existing deepValidator 161 | setDeepValidator(input Validator) 162 | // Returns deepValidator 163 | getDeepValidator() Validator 164 | // Returns true if deepValidator is not nil 165 | hasDeepValidator() bool 166 | // Validates deepValidator 167 | validateDeepValidator(ctx context.Context, input interface{}, translator Translator) interface{} 168 | // Replaces passed validator with existing childrenValidator 169 | setChildrenValidator(input Validator) 170 | // Returns childrenValidator 171 | getChildrenValidator() Validator 172 | // Returns true if children is not nil 173 | hasChildrenValidator() bool 174 | // Validates childrenValidator 175 | validateChildrenValidator(ctx context.Context, input interface{}, translator Translator) interface{} 176 | // Returns requires 177 | getRequires() requires 178 | // Returns name 179 | getName() string 180 | // Returns current validators + r.Validators 181 | appendRuleSet(r ruleSet) ruleSet 182 | // Returns passed argument name from struct if exist 183 | get(name string) interface{} 184 | // Sets passed argument value instead of existing in name parameter if exists 185 | set(name string, value interface{}) 186 | // Sets custom validators which are defined in generator 187 | setGeneratorCustomValidators(validators *Validators) ruleSet 188 | } 189 | ) 190 | 191 | func (o *ruleSetS) Int() ruleSet { 192 | functionName := "int" 193 | o.validators[functionName] = intRule 194 | return o 195 | } 196 | 197 | func (o *ruleSetS) Float() ruleSet { 198 | functionName := "float" 199 | o.validators[functionName] = floatRule 200 | return o 201 | } 202 | 203 | func (o *ruleSetS) Min(min float64) ruleSet { 204 | functionName := "min" 205 | o.validators[functionName] = minRule(min) 206 | precision := determinePrecision(min) 207 | o.addOption(functionName, "min", fmt.Sprintf("%."+precision+"f", min)) 208 | return o 209 | } 210 | 211 | func (o *ruleSetS) Max(max float64) ruleSet { 212 | functionName := "max" 213 | o.validators[functionName] = maxRule(max) 214 | precision := determinePrecision(max) 215 | o.addOption(functionName, "max", fmt.Sprintf("%."+precision+"f", max)) 216 | return o 217 | } 218 | 219 | func (o *ruleSetS) LenRange(from, to int) ruleSet { 220 | functionName := "len_range" 221 | o.validators[functionName] = lenRangeRule(from, to) 222 | o.addOption(functionName, "from", fmt.Sprintf("%d", from)) 223 | o.addOption(functionName, "to", fmt.Sprintf("%d", to)) 224 | return o 225 | } 226 | 227 | func (o *ruleSetS) Len(length int) ruleSet { 228 | functionName := "len" 229 | o.validators[functionName] = lenRule(length) 230 | o.addOption(functionName, "length", fmt.Sprint(length)) 231 | return o 232 | } 233 | 234 | func (o *ruleSetS) AlwaysCheckRules() ruleSet { 235 | o.required() 236 | return o 237 | } 238 | 239 | func (o *ruleSetS) Required() ruleSet { 240 | functionName := "required" 241 | o.validators[functionName] = requiredRule 242 | return o.AlwaysCheckRules() 243 | } 244 | 245 | func (o *ruleSetS) Optional() ruleSet { 246 | o.optional() 247 | return o 248 | } 249 | 250 | func (o *ruleSetS) NonZero() ruleSet { 251 | functionName := "non_zero" 252 | o.validators[functionName] = nonZeroRule 253 | return o.AlwaysCheckRules() 254 | } 255 | 256 | func (o *ruleSetS) NonNil() ruleSet { 257 | functionName := "non_nil" 258 | o.validators[functionName] = nonNilRule 259 | return o.AlwaysCheckRules() 260 | } 261 | 262 | func (o *ruleSetS) NonEmpty() ruleSet { 263 | functionName := "non_empty" 264 | o.validators[functionName] = nonEmptyRule 265 | return o.AlwaysCheckRules() 266 | } 267 | 268 | func (o *ruleSetS) Email() ruleSet { 269 | functionName := "email" 270 | o.validators[functionName] = emailRule 271 | return o 272 | } 273 | 274 | func (o *ruleSetS) Regex(pattern string) ruleSet { 275 | functionName := "regex" 276 | o.validators[functionName] = regexRule(pattern) 277 | o.addOption(functionName, "pattern", pattern) 278 | return o 279 | } 280 | 281 | func (o *ruleSetS) Phone() ruleSet { 282 | functionName := "phone" 283 | o.validators[functionName] = phoneRule 284 | return o 285 | } 286 | 287 | func (o *ruleSetS) Custom(validators Validators) ruleSet { 288 | for key, function := range validators { 289 | if _, ok := o.validators[key]; ok { 290 | panic(fmt.Sprintf("%s is duplicate and has to be unique", key)) 291 | } 292 | o.validators[key] = function 293 | } 294 | return o 295 | } 296 | 297 | func (o *ruleSetS) RegisteredCustom(validatorKeys ...string) ruleSet { 298 | for _, key := range validatorKeys { 299 | vs := *o.customValidators 300 | if _, ok := o.validators[key]; ok { 301 | panic(fmt.Sprintf("%s is duplicate and has to be unique", key)) 302 | } 303 | if function, ok := vs[key]; ok { 304 | o.validators[key] = function 305 | } else { 306 | panic(fmt.Sprintf("%s custom validator doesn't exist, it is really defined in generator?", key)) 307 | } 308 | } 309 | return o 310 | } 311 | 312 | func (o *ruleSetS) Map() ruleSet { 313 | functionName := "map" 314 | o.validators[functionName] = mapRule 315 | return o 316 | } 317 | 318 | func (o *ruleSetS) Slice() ruleSet { 319 | functionName := "slice" 320 | o.validators[functionName] = sliceRule 321 | return o 322 | } 323 | 324 | func (o *ruleSetS) Struct() ruleSet { 325 | functionName := "struct" 326 | o.validators[functionName] = structRule 327 | return o 328 | } 329 | 330 | func (o *ruleSetS) Complex(rules Rules) ruleSet { 331 | v := &validatorS{rule: nil, rules: rules} 332 | o.setDeepValidator(v) 333 | return o 334 | } 335 | 336 | func (o *ruleSetS) Children(rule ruleSet) ruleSet { 337 | if o.childrenValidator == nil { 338 | v := &validatorS{rule: rule, rules: nil} 339 | o.childrenValidator = v 340 | } else { 341 | o.childrenValidator.getRule().appendRuleSet(rule) 342 | } 343 | return o 344 | } 345 | 346 | func (o *ruleSetS) Type(input interface{}) ruleSet { 347 | functionName := "type" 348 | switch v := input.(type) { 349 | case reflect.Type: 350 | o.addOption(functionName, "type", input.(reflect.Type).String()) 351 | o.validators[functionName] = typeRule(v.String()) 352 | default: 353 | o.addOption(functionName, "type", reflect.TypeOf(input).String()) 354 | o.validators[functionName] = typeRule(reflect.TypeOf(input).String()) 355 | } 356 | return o 357 | } 358 | 359 | func (o *ruleSetS) Password() ruleSet { 360 | functionName := "password" 361 | o.validators[functionName] = passwordRule 362 | return o 363 | } 364 | 365 | func (o *ruleSetS) OR(ruleSets ...ruleSet) ruleSet { 366 | functionName := "or" 367 | o.validators[functionName] = orRule(ruleSets...) 368 | return o 369 | } 370 | 371 | func (o *ruleSetS) XOR(ruleSets ...ruleSet) ruleSet { 372 | functionName := "xor" 373 | o.validators[functionName] = xorRule(ruleSets...) 374 | return o 375 | } 376 | 377 | func (o *ruleSetS) Choices(choices ...interface{}) ruleSet { 378 | functionName := "choices" 379 | o.validators[functionName] = choicesRule(choices...) 380 | choicesString := []string{} 381 | for i := 0; i < len(choices); i++ { 382 | choicesString = append(choicesString, fmt.Sprint(choices[i])) 383 | } 384 | o.addOption(functionName, "choices", strings.ReplaceAll(fmt.Sprint(choicesString), " ", ", ")) 385 | return o 386 | } 387 | 388 | func (o *ruleSetS) WhenExistOne(choices ...string) ruleSet { 389 | functionName := "when_exist_one" 390 | o.requires[functionName] = whenExistOneRequireRule(choices...) 391 | o.validators[functionName] = requiredRule 392 | o.addOption(functionName, "choices", strings.ReplaceAll(fmt.Sprint(choices), " ", ", ")) 393 | return o 394 | } 395 | 396 | func (o *ruleSetS) WhenExistAll(choices ...string) ruleSet { 397 | functionName := "when_exist_all" 398 | o.requires[functionName] = whenExistAllRequireRule(choices...) 399 | o.validators[functionName] = requiredRule 400 | o.addOption(functionName, "choices", strings.ReplaceAll(fmt.Sprint(choices), " ", ", ")) 401 | return o 402 | } 403 | 404 | func (o *ruleSetS) WhenNotExistOne(choices ...string) ruleSet { 405 | functionName := "when_not_exist_one" 406 | o.requires[functionName] = whenNotExistOneRequireRule(choices...) 407 | o.validators[functionName] = requiredRule 408 | o.addOption(functionName, "choices", strings.ReplaceAll(fmt.Sprint(choices), " ", ", ")) 409 | return o 410 | } 411 | 412 | func (o *ruleSetS) WhenNotExistAll(choices ...string) ruleSet { 413 | functionName := "when_not_exist_all" 414 | o.requires[functionName] = whenNotExistAllRequireRule(choices...) 415 | o.validators[functionName] = requiredRule 416 | o.addOption(functionName, "choices", strings.ReplaceAll(fmt.Sprint(choices), " ", ", ")) 417 | return o 418 | } 419 | 420 | func (o *ruleSetS) String() ruleSet { 421 | functionName := "string" 422 | o.validators[functionName] = stringRule 423 | return o 424 | } 425 | 426 | func (o *ruleSetS) GetValidator() Validator { 427 | return o.deepValidator 428 | } 429 | 430 | func (o *ruleSetS) GetChildrenValidator() Validator { 431 | return o.childrenValidator 432 | } 433 | 434 | func (o *ruleSetS) appendSpecificMessages(key string, value string) { 435 | var sm = o.getSpecificMessages() 436 | if sm == nil { 437 | sm = Messages{} 438 | } 439 | sm[key] = value 440 | 441 | o.SpecificMessages(sm) 442 | } 443 | 444 | func (o *ruleSetS) SpecificMessages(specificMessages Messages) ruleSet { 445 | o.specificMessages = specificMessages 446 | return o 447 | } 448 | 449 | func (o *ruleSetS) getSpecificMessages() Messages { 450 | return o.specificMessages 451 | } 452 | 453 | func (o *ruleSetS) setChildrenValidator(input Validator) { 454 | if o.childrenValidator != nil { 455 | o.childrenValidator.getRule().appendRuleSet(input.getRule()) 456 | } else { 457 | o.childrenValidator = input 458 | } 459 | } 460 | 461 | func (o *ruleSetS) getChildrenValidator() Validator { 462 | return o.childrenValidator 463 | } 464 | 465 | func (o *ruleSetS) hasChildrenValidator() bool { 466 | return o.childrenValidator != nil 467 | } 468 | 469 | func (o *ruleSetS) validateChildrenValidator(ctx context.Context, input interface{}, translator Translator) interface{} { 470 | return o.childrenValidator.Validate(ctx, input, translator) 471 | } 472 | 473 | func (o *ruleSetS) validate(ctx context.Context, input interface{}) []string { 474 | fails := []string{} 475 | for key, vFunction := range o.validators { 476 | if !vFunction(ctx, input) { 477 | fails = append(fails, key) 478 | } 479 | } 480 | 481 | return fails 482 | } 483 | 484 | func (o *ruleSetS) getOption(ruleKey string) option { 485 | if option, ok := o.options[ruleKey]; ok { 486 | return option 487 | } 488 | return option{} 489 | } 490 | 491 | func (o *ruleSetS) addOption(ruleKey string, subKey string, value string) { 492 | if option, ok := o.options[ruleKey]; ok { 493 | option[subKey] = value 494 | return 495 | } 496 | o.options[ruleKey] = option{subKey: value} 497 | } 498 | 499 | func (o *ruleSetS) optional() { 500 | o.isOptional = true 501 | } 502 | 503 | func (o *ruleSetS) required() { 504 | o.isOptional = false 505 | } 506 | 507 | func (o *ruleSetS) isRequired() bool { 508 | return !o.isOptional 509 | } 510 | 511 | func (o *ruleSetS) setDeepValidator(input Validator) { 512 | o.deepValidator = input 513 | } 514 | 515 | func (o *ruleSetS) getDeepValidator() Validator { 516 | return o.deepValidator 517 | } 518 | 519 | func (o *ruleSetS) hasDeepValidator() bool { 520 | return o.deepValidator != nil 521 | } 522 | 523 | func (o *ruleSetS) validateDeepValidator(ctx context.Context, input interface{}, translator Translator) interface{} { 524 | return o.deepValidator.Validate(ctx, input, translator) 525 | } 526 | 527 | func (o *ruleSetS) getRequires() requires { 528 | return o.requires 529 | } 530 | 531 | func (o *ruleSetS) getName() string { 532 | return o.name 533 | } 534 | 535 | func (o *ruleSetS) appendRuleSet(r ruleSet) ruleSet { 536 | rValidators := r.get("validators").(Validators) 537 | for key, value := range rValidators { 538 | o.validators[key] = value 539 | } 540 | rOptions := r.get("options").(options) 541 | for key, value := range rOptions { 542 | o.options[key] = value 543 | } 544 | rDeepValidator, ok := r.get("deepValidator").(Validator) 545 | if ok && rDeepValidator != nil && o.deepValidator == nil { 546 | o.deepValidator = rDeepValidator 547 | } 548 | rChildrenValidator, ok := r.get("childrenValidator").(Validator) 549 | if ok && rChildrenValidator != nil && o.childrenValidator == nil { 550 | o.childrenValidator = rChildrenValidator 551 | } else if ok && o.childrenValidator != nil && rChildrenValidator != nil { 552 | childrenValidatorRuleSet := rChildrenValidator.getRule() 553 | o.childrenValidator.getRule().appendRuleSet(childrenValidatorRuleSet) 554 | } 555 | rSpecificMessages := r.get("specificMessages").(Messages) 556 | for key, value := range rSpecificMessages { 557 | o.specificMessages[key] = value 558 | } 559 | if o.isOptional && !r.get("isOptional").(bool) { 560 | o.isOptional = false 561 | } 562 | name := r.get("name").(string) 563 | if name != "" && o.name == "" { 564 | o.name = name 565 | } 566 | rRequires := r.get("requires").(requires) 567 | for key, value := range rRequires { 568 | o.requires[key] = value 569 | } 570 | 571 | return o 572 | } 573 | 574 | func (o *ruleSetS) get(name string) interface{} { 575 | switch name { 576 | case "childrenValidator": 577 | return o.childrenValidator 578 | case "deepValidator": 579 | return o.deepValidator 580 | case "isOptional": 581 | return o.isOptional 582 | case "name": 583 | return o.name 584 | case "options": 585 | return o.options 586 | case "requires": 587 | return o.requires 588 | case "specificMessages": 589 | return o.specificMessages 590 | case "validators": 591 | return o.validators 592 | default: 593 | panic(fmt.Sprintf("there is no item as %s", name)) 594 | } 595 | } 596 | 597 | func (o *ruleSetS) set(name string, value interface{}) { 598 | switch name { 599 | case "childrenValidator": 600 | o.childrenValidator = value.(Validator) 601 | case "deepValidator": 602 | o.deepValidator = value.(Validator) 603 | case "isOptional": 604 | o.isOptional = value.(bool) 605 | case "name": 606 | o.name = value.(string) 607 | case "options": 608 | o.options = value.(options) 609 | case "requires": 610 | o.requires = value.(requires) 611 | case "specificMessages": 612 | o.specificMessages = value.(Messages) 613 | case "validators": 614 | o.validators = value.(Validators) 615 | default: 616 | panic(fmt.Sprintf("there is no item as %s", name)) 617 | } 618 | } 619 | 620 | func (o *ruleSetS) setGeneratorCustomValidators(validators *Validators) ruleSet { 621 | o.customValidators = validators 622 | return o 623 | } 624 | -------------------------------------------------------------------------------- /rules.go: -------------------------------------------------------------------------------- 1 | package galidator 2 | 3 | import ( 4 | "context" 5 | "net/mail" 6 | "reflect" 7 | 8 | "github.com/dlclark/regexp2" 9 | "github.com/golodash/godash/generals" 10 | "github.com/nyaruka/phonenumbers" 11 | ) 12 | 13 | // A map which with rule or require's key will provide the default error message of that rule or require's key 14 | var defaultValidatorErrorMessages = map[string]string{ 15 | // Rules 16 | "int": "not an integer value", 17 | "float": "not a float value", 18 | "min": "$field's length must be higher equal to $min", 19 | "max": "$field's length must be lower equal to $max", 20 | "len_range": "$field's length must be between $from to $to characters long", 21 | "len": "$field's length must be equal to $length", 22 | "required": "required", 23 | "non_zero": "can not be 0", 24 | "non_nil": "can not be nil", 25 | "non_empty": "can not be empty", 26 | "email": "not a valid email address", 27 | "regex": "$value does not pass /$pattern/ pattern", 28 | "phone": "$value is not a valid international phone number format", 29 | "map": "not a map", 30 | "struct": "not a struct", 31 | "slice": "not a slice", 32 | "password": "$field must be at least 8 characters long and contain one lowercase, one uppercase, one special and one number character", 33 | "or": "ruleSets in $field did not pass based on or logic", 34 | "xor": "ruleSets in $field did not pass based on xor logic", 35 | "choices": "$value does not include in allowed choices: $choices", 36 | "string": "not a string", 37 | "type": "not a $type", 38 | 39 | // Requires 40 | "when_exist_one": "$field is required because at least one of $choices fields are not nil, empty or zero(0, \"\", '')", 41 | "when_exist_all": "$field is required because all of $choices fields are not nil, empty or zero(0, \"\", '')", 42 | "when_not_exist_one": "$field is required because at least one of $choices fields are nil, empty or zero(0, \"\", '')", 43 | "when_not_exist_all": "$field is required because all of $choices fields are nil, empty or zero(0, \"\", '')", 44 | } 45 | 46 | func isValid(input interface{}) bool { 47 | return reflect.ValueOf(input).IsValid() 48 | } 49 | 50 | // Returns true if input is int 51 | func intRule(ctx context.Context, input interface{}) bool { 52 | if !isValid(input) { 53 | return false 54 | } 55 | inputValue := reflect.ValueOf(input) 56 | switch inputValue.Kind() { 57 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, 58 | reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, 59 | reflect.Uint32, reflect.Uint64: 60 | return true 61 | default: 62 | return false 63 | } 64 | } 65 | 66 | // Returns true if input is float 67 | func floatRule(ctx context.Context, input interface{}) bool { 68 | if !isValid(input) { 69 | return false 70 | } 71 | inputValue := reflect.ValueOf(input) 72 | switch inputValue.Kind() { 73 | case reflect.Float32, reflect.Float64: 74 | return true 75 | default: 76 | return false 77 | } 78 | } 79 | 80 | // Returns true if input type is equal to defined type 81 | func typeRule(t string) func(context.Context, interface{}) bool { 82 | return func(ctx context.Context, input interface{}) bool { 83 | return isValid(input) && reflect.TypeOf(input).String() == t 84 | } 85 | } 86 | 87 | // Returns true if: input >= min or len(input) >= min 88 | func minRule(min float64) func(context.Context, interface{}) bool { 89 | return func(ctx context.Context, input interface{}) bool { 90 | if !isValid(input) { 91 | return false 92 | } 93 | inputValue := reflect.ValueOf(input) 94 | switch inputValue.Kind() { 95 | case reflect.String, reflect.Map, reflect.Slice: 96 | return inputValue.Len() >= int(min) 97 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, 98 | reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, 99 | reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: 100 | return inputValue.Convert(reflect.TypeOf(1.0)).Float() >= min 101 | default: 102 | return false 103 | } 104 | } 105 | } 106 | 107 | // Returns true if: input <= max or len(input) <= max 108 | func maxRule(max float64) func(context.Context, interface{}) bool { 109 | return func(ctx context.Context, input interface{}) bool { 110 | if !isValid(input) { 111 | return false 112 | } 113 | inputValue := reflect.ValueOf(input) 114 | switch inputValue.Kind() { 115 | case reflect.String, reflect.Map, reflect.Slice: 116 | return inputValue.Len() <= int(max) 117 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, 118 | reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, 119 | reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: 120 | return inputValue.Convert(reflect.TypeOf(1.0)).Float() <= max 121 | default: 122 | return false 123 | } 124 | } 125 | } 126 | 127 | // Returns true if len(input) >= from && len(input) <= to 128 | // 129 | // If from == -1, no check on from config will happen 130 | // If to == -1, no check on to config will happen 131 | func lenRangeRule(from, to int) func(context.Context, interface{}) bool { 132 | return func(ctx context.Context, input interface{}) bool { 133 | if !isValid(input) { 134 | return false 135 | } 136 | inputValue := reflect.ValueOf(input) 137 | switch inputValue.Kind() { 138 | case reflect.String, reflect.Map, reflect.Slice: 139 | if from != -1 && inputValue.Len() < from { 140 | return false 141 | } else if to != -1 && inputValue.Len() > to { 142 | return false 143 | } 144 | return true 145 | default: 146 | return false 147 | } 148 | } 149 | } 150 | 151 | // Returns true if len(input) is equal to passed length 152 | func lenRule(length int) func(context.Context, interface{}) bool { 153 | return func(ctx context.Context, input interface{}) bool { 154 | if !isValid(input) { 155 | return false 156 | } 157 | inputValue := reflect.ValueOf(input) 158 | switch inputValue.Kind() { 159 | case reflect.String, reflect.Map, reflect.Slice: 160 | return inputValue.Len() == length 161 | default: 162 | return false 163 | } 164 | } 165 | } 166 | 167 | // Returns true if input is not 0, "", ”, nil and empty 168 | func requiredRule(ctx context.Context, input interface{}) bool { 169 | if intRule(ctx, input) || floatRule(ctx, input) { 170 | return true 171 | } 172 | return !isNil(input) && !hasZeroItems(input) && !reflect.ValueOf(input).IsZero() 173 | } 174 | 175 | // Returns true if input is not zero(0, "", ”) 176 | func nonZeroRule(ctx context.Context, input interface{}) bool { 177 | return !isValid(input) || !reflect.ValueOf(input).IsZero() 178 | } 179 | 180 | // Returns true if input is not nil 181 | func nonNilRule(ctx context.Context, input interface{}) bool { 182 | return !isNil(input) 183 | } 184 | 185 | // Returns true if input has items 186 | func nonEmptyRule(ctx context.Context, input interface{}) bool { 187 | return !hasZeroItems(input) 188 | } 189 | 190 | // Returns true if input is a valid email 191 | func emailRule(ctx context.Context, input interface{}) bool { 192 | if !isValid(input) { 193 | return false 194 | } 195 | switch reflect.ValueOf(input).Kind() { 196 | case reflect.String: 197 | _, err := mail.ParseAddress(input.(string)) 198 | if err != nil { 199 | return false 200 | } 201 | return true 202 | default: 203 | return false 204 | } 205 | } 206 | 207 | // Returns true if input is a valid phone number 208 | func phoneRule(ctx context.Context, input interface{}) bool { 209 | if !isValid(input) { 210 | return false 211 | } 212 | InternationalPhoneRegex := regexp2.MustCompile(`^\+\d+$`, regexp2.None) 213 | if ok, err := InternationalPhoneRegex.MatchString(input.(string)); !ok || err != nil { 214 | return false 215 | } 216 | parsedNumber, err := phonenumbers.Parse(input.(string), "") 217 | return err == nil && phonenumbers.IsValidNumber(parsedNumber) 218 | } 219 | 220 | // Returns true if input matches the passed pattern 221 | func regexRule(pattern string) func(context.Context, interface{}) bool { 222 | regex := regexp2.MustCompile(pattern, regexp2.None) 223 | return func(ctx context.Context, input interface{}) bool { 224 | if !isValid(input) { 225 | return false 226 | } 227 | inputValue := reflect.ValueOf(input) 228 | switch inputValue.Kind() { 229 | case reflect.String: 230 | output, _ := regex.MatchString(input.(string)) 231 | return output 232 | default: 233 | return false 234 | } 235 | } 236 | } 237 | 238 | // Returns true if input is a map 239 | func mapRule(ctx context.Context, input interface{}) bool { 240 | if !isValid(input) { 241 | return false 242 | } 243 | return reflect.TypeOf(input).Kind() == reflect.Map 244 | } 245 | 246 | // Returns true if input is a struct 247 | func structRule(ctx context.Context, input interface{}) bool { 248 | if !isValid(input) { 249 | return false 250 | } 251 | return reflect.TypeOf(input).Kind() == reflect.Struct 252 | } 253 | 254 | // Returns true if input is a slice 255 | func sliceRule(ctx context.Context, input interface{}) bool { 256 | if !isValid(input) { 257 | return false 258 | } 259 | return reflect.TypeOf(input).Kind() == reflect.Slice 260 | } 261 | 262 | // Returns true if input is at least 8 characters long, has one lowercase, one uppercase, one special and one number character 263 | func passwordRule(ctx context.Context, input interface{}) bool { 264 | if !isValid(input) { 265 | return false 266 | } 267 | return regexRule("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[ !\"#$%&'()*+,-.\\/:;<=>?@[\\]^_`{|}~])[A-Za-z\\d !\"#$%&'()*+,-.\\/:;<=>?@[\\]^_`{|}~]{8,}$")(ctx, input) 268 | } 269 | 270 | // If at least one of the passed ruleSets pass, this rule will pass 271 | func orRule(ruleSets ...ruleSet) func(context.Context, interface{}) bool { 272 | return func(ctx context.Context, input interface{}) bool { 273 | output := false 274 | 275 | if len(ruleSets) == 0 { 276 | return true 277 | } 278 | 279 | for _, ruleSet := range ruleSets { 280 | output = len(ruleSet.validate(ctx, input)) == 0 || output 281 | } 282 | 283 | return output 284 | } 285 | } 286 | 287 | // If Xor of the passed ruleSets pass, this rule will pass 288 | func xorRule(ruleSets ...ruleSet) func(context.Context, interface{}) bool { 289 | return func(ctx context.Context, input interface{}) bool { 290 | output := false 291 | 292 | if len(ruleSets) == 0 { 293 | return true 294 | } 295 | 296 | for _, ruleSet := range ruleSets { 297 | output = len(ruleSet.validate(ctx, input)) == 0 != output 298 | } 299 | 300 | return output 301 | } 302 | } 303 | 304 | // If passed data is not one of choices in choices variable, it fails 305 | func choicesRule(choices ...interface{}) func(context.Context, interface{}) bool { 306 | return func(ctx context.Context, input interface{}) bool { 307 | for i := 0; i < len(choices); i++ { 308 | element := choices[i] 309 | if generals.Same(element, input) { 310 | return true 311 | } 312 | } 313 | 314 | return false 315 | } 316 | } 317 | 318 | // Returns true if input is string 319 | func stringRule(ctx context.Context, input interface{}) bool { 320 | if !isValid(input) { 321 | return false 322 | } 323 | return reflect.TypeOf(input).Kind() == reflect.String 324 | } 325 | -------------------------------------------------------------------------------- /tests/choices_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestChoices(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "fail-1", 14 | validator: g.Validator(g.R().Choices("1", 2, 3.0), galidator.Messages{"choices": "choices failed"}), 15 | in: "2", 16 | panic: false, 17 | expected: []string{"choices failed"}, 18 | }, 19 | { 20 | name: "fail-2", 21 | validator: g.Validator(g.R().Choices("1", 2, 3.0), galidator.Messages{"choices": "choices failed"}), 22 | in: 1, 23 | panic: false, 24 | expected: []string{"choices failed"}, 25 | }, 26 | { 27 | name: "fail-3", 28 | validator: g.Validator(g.R().Choices("1", 2, 3.0), galidator.Messages{"choices": "choices failed"}), 29 | in: 3, 30 | panic: false, 31 | expected: []string{"choices failed"}, 32 | }, 33 | { 34 | name: "fail-4", 35 | validator: g.Validator(g.R().Choices("1", 2, 3.0), galidator.Messages{"choices": "choices failed"}), 36 | in: map[string]string{"s": "s"}, 37 | panic: false, 38 | expected: []string{"choices failed"}, 39 | }, 40 | { 41 | name: "pass-1", 42 | validator: g.Validator(g.R().Choices("1", 2, 3.0), galidator.Messages{"choices": "choices failed"}), 43 | in: "1", 44 | panic: false, 45 | expected: nil, 46 | }, 47 | { 48 | name: "pass-2", 49 | validator: g.Validator(g.R().Choices("1", 2, 3.0), galidator.Messages{"choices": "choices failed"}), 50 | in: 2, 51 | panic: false, 52 | expected: nil, 53 | }, 54 | { 55 | name: "pass-3", 56 | validator: g.Validator(g.R().Choices("1", 2, 3.0), galidator.Messages{"choices": "choices failed"}), 57 | in: 3.0, 58 | panic: false, 59 | expected: nil, 60 | }, 61 | } 62 | 63 | for _, s := range scenarios { 64 | t.Run(s.name, func(t *testing.T) { 65 | defer deferTestCases(t, s.panic, s.expected) 66 | 67 | output := s.validator.Validate(context.TODO(), s.in) 68 | check(t, s.expected, output) 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/complex_validator_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | type ComplexValidator struct { 11 | Name string 12 | Description string 13 | } 14 | 15 | func TestComplexValidator(t *testing.T) { 16 | v := g.ComplexValidator(galidator.Rules{ 17 | "Name": g.RuleSet("name").Required(), 18 | "Description": g.RuleSet("description").Required(), 19 | }) 20 | 21 | scenarios := []scenario{ 22 | { 23 | name: "pass-1-map", 24 | validator: v, 25 | in: map[string]interface{}{ 26 | "name": "name", 27 | "description": "description", 28 | }, 29 | panic: false, 30 | expected: nil, 31 | }, 32 | { 33 | name: "fail-1-map", 34 | validator: v, 35 | in: map[string]interface{}{ 36 | "name": "", 37 | "description": "description", 38 | }, 39 | panic: false, 40 | expected: map[string]interface{}{"name": []string{"required"}}, 41 | }, 42 | { 43 | name: "fail-2-map", 44 | validator: v, 45 | in: map[string]interface{}{ 46 | "name": "name", 47 | "description": "", 48 | }, 49 | panic: false, 50 | expected: map[string]interface{}{"description": []string{"required"}}, 51 | }, 52 | { 53 | name: "fail-3-map", 54 | validator: v, 55 | in: map[string]interface{}{ 56 | "name": "", 57 | "description": "", 58 | }, 59 | panic: false, 60 | expected: map[string]interface{}{"name": []string{"required"}, "description": []string{"required"}}, 61 | }, 62 | { 63 | name: "pass-1-struct", 64 | validator: v, 65 | in: ComplexValidator{ 66 | Name: "name", 67 | Description: "description", 68 | }, 69 | panic: false, 70 | expected: nil, 71 | }, 72 | { 73 | name: "fail-1-struct", 74 | validator: v, 75 | in: ComplexValidator{ 76 | Name: "", 77 | Description: "description", 78 | }, 79 | panic: false, 80 | expected: map[string]interface{}{"name": []string{"required"}}, 81 | }, 82 | { 83 | name: "fail-2-struct", 84 | validator: v, 85 | in: ComplexValidator{ 86 | Name: "name", 87 | Description: "", 88 | }, 89 | panic: false, 90 | expected: map[string]interface{}{"description": []string{"required"}}, 91 | }, 92 | { 93 | name: "fail-3-struct", 94 | validator: v, 95 | in: ComplexValidator{ 96 | Name: "", 97 | Description: "", 98 | }, 99 | panic: false, 100 | expected: map[string]interface{}{"name": []string{"required"}, "description": []string{"required"}}, 101 | }, 102 | } 103 | 104 | for _, s := range scenarios { 105 | t.Run(s.name, func(t *testing.T) { 106 | defer deferTestCases(t, s.panic, s.expected) 107 | 108 | output := s.validator.Validate(context.TODO(), s.in) 109 | check(t, s.expected, output) 110 | }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/custom_validators_from_generators_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestCustomValidatorsFromGenerators(t *testing.T) { 9 | scenarios := []scenario{ 10 | { 11 | name: "fail-1", 12 | validator: g.Validator(g.R().RegisteredCustom("custom_validator")), 13 | in: "some text", 14 | panic: false, 15 | expected: []string{"custom error"}, 16 | }, 17 | { 18 | name: "fail-2", 19 | validator: g.Validator(g.R().RegisteredCustom("second_custom_validator")), 20 | in: "some text", 21 | panic: false, 22 | expected: []string{"second custom error"}, 23 | }, 24 | } 25 | 26 | for _, s := range scenarios { 27 | t.Run(s.name, func(t *testing.T) { 28 | defer deferTestCases(t, s.panic, s.expected) 29 | 30 | output := s.validator.Validate(context.TODO(), s.in) 31 | check(t, s.expected, output) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/decrypt_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type login struct { 15 | Username string `json:"username" binding:"required" required:"$field is required"` 16 | Password string `json:"password"` 17 | } 18 | 19 | var ( 20 | validator = g.Validator(login{}) 21 | ) 22 | 23 | func test(c *gin.Context) { 24 | req := &login{} 25 | 26 | // Parse json 27 | if err := c.BindJSON(req); err != nil { 28 | c.JSON(400, gin.H{ 29 | "message": validator.DecryptErrors(err), 30 | }) 31 | return 32 | } 33 | 34 | c.JSON(200, gin.H{ 35 | "good": 200, 36 | }) 37 | } 38 | 39 | func TestDecryptErrors(t *testing.T) { 40 | gin.SetMode(gin.ReleaseMode) 41 | r := gin.Default() 42 | r.POST("/", test) 43 | t.Run("username_lost", func(t *testing.T) { 44 | jsonValue, _ := json.Marshal(login{}) 45 | req, _ := http.NewRequest("POST", "/", bytes.NewBuffer(jsonValue)) 46 | w := httptest.NewRecorder() 47 | r.ServeHTTP(w, req) 48 | responseData, _ := io.ReadAll(w.Body) 49 | check(t, "{\"message\":{\"username\":\"username is required\"}}", string(responseData)) 50 | }) 51 | t.Run("wrong_data", func(t *testing.T) { 52 | jsonValue, _ := json.Marshal([]login{{Username: "dasewae"}}) 53 | req, _ := http.NewRequest("POST", "/", bytes.NewBuffer(jsonValue)) 54 | w := httptest.NewRecorder() 55 | r.ServeHTTP(w, req) 56 | responseData, _ := io.ReadAll(w.Body) 57 | check(t, "{\"message\":\"unmarshal error\"}", string(responseData)) 58 | }) 59 | t.Run("fine", func(t *testing.T) { 60 | jsonValue, _ := json.Marshal(login{Username: "sd"}) 61 | req, _ := http.NewRequest("POST", "/", bytes.NewBuffer(jsonValue)) 62 | w := httptest.NewRecorder() 63 | r.ServeHTTP(w, req) 64 | responseData, _ := io.ReadAll(w.Body) 65 | check(t, "{\"good\":200}", string(responseData)) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /tests/email_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestEmail(t *testing.T) { 9 | scenarios := []scenario{ 10 | { 11 | name: "pass-1", 12 | validator: g.Validator(g.R().Email()), 13 | in: "m@g.c", 14 | panic: false, 15 | expected: nil, 16 | }, 17 | { 18 | name: "pass-2", 19 | validator: g.Validator(g.R().Email()), 20 | in: "m@g", 21 | panic: false, 22 | expected: nil, 23 | }, 24 | { 25 | name: "fail-1", 26 | validator: g.Validator(g.R().Email()), 27 | in: "m@", 28 | panic: false, 29 | expected: []string{"not a valid email address"}, 30 | }, 31 | { 32 | name: "fail-2", 33 | validator: g.Validator(g.R().Email()), 34 | in: "m", 35 | panic: false, 36 | expected: []string{"not a valid email address"}, 37 | }, 38 | { 39 | name: "fail-3", 40 | validator: g.Validator(g.R().Email()), 41 | in: "m@g.", 42 | panic: false, 43 | expected: []string{"not a valid email address"}, 44 | }, 45 | { 46 | name: "fail-4", 47 | validator: g.Validator(g.R().Email()), 48 | in: "m.", 49 | panic: false, 50 | expected: []string{"not a valid email address"}, 51 | }, 52 | } 53 | 54 | for _, s := range scenarios { 55 | t.Run(s.name, func(t *testing.T) { 56 | defer deferTestCases(t, s.panic, s.expected) 57 | 58 | output := s.validator.Validate(context.TODO(), s.in) 59 | check(t, s.expected, output) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/float_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestFloat(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "pass", 14 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 15 | in: 1.1, 16 | panic: false, 17 | expected: nil, 18 | }, 19 | { 20 | name: "fail", 21 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 22 | in: "1", 23 | panic: false, 24 | expected: []string{"1 is not float"}, 25 | }, 26 | { 27 | name: "float", 28 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 29 | in: int(1), 30 | panic: false, 31 | expected: []string{"1 is not float"}, 32 | }, 33 | { 34 | name: "int8", 35 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 36 | in: int8(1), 37 | panic: false, 38 | expected: []string{"1 is not float"}, 39 | }, 40 | { 41 | name: "int16", 42 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 43 | in: int16(1), 44 | panic: false, 45 | expected: []string{"1 is not float"}, 46 | }, 47 | { 48 | name: "int32", 49 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 50 | in: int32(1), 51 | panic: false, 52 | expected: []string{"1 is not float"}, 53 | }, 54 | { 55 | name: "int64", 56 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 57 | in: int64(1), 58 | panic: false, 59 | expected: []string{"1 is not float"}, 60 | }, 61 | { 62 | name: "uint", 63 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 64 | in: uint(1), 65 | panic: false, 66 | expected: []string{"1 is not float"}, 67 | }, 68 | { 69 | name: "uint8", 70 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 71 | in: uint8(1), 72 | panic: false, 73 | expected: []string{"1 is not float"}, 74 | }, 75 | { 76 | name: "uint16", 77 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 78 | in: uint16(1), 79 | panic: false, 80 | expected: []string{"1 is not float"}, 81 | }, 82 | { 83 | name: "uint32", 84 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 85 | in: uint32(1), 86 | panic: false, 87 | expected: []string{"1 is not float"}, 88 | }, 89 | { 90 | name: "uint64", 91 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 92 | in: uint64(1), 93 | panic: false, 94 | expected: []string{"1 is not float"}, 95 | }, 96 | { 97 | name: "float32", 98 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 99 | in: float32(1), 100 | panic: false, 101 | expected: nil, 102 | }, 103 | { 104 | name: "float64", 105 | validator: g.Validator(g.R().Float().SpecificMessages(galidator.Messages{"float": "$value is not float"})), 106 | in: float64(1), 107 | panic: false, 108 | expected: nil, 109 | }, 110 | } 111 | 112 | for _, s := range scenarios { 113 | t.Run(s.name, func(t *testing.T) { 114 | defer deferTestCases(t, s.panic, s.expected) 115 | 116 | output := s.validator.Validate(context.TODO(), s.in) 117 | check(t, s.expected, output) 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/from_slice_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestFromSlice(t *testing.T) { 11 | v := g.CustomValidators(galidator.Validators{ 12 | "custom_choices": custom_choices, 13 | }).Validator([]fromStructTest{}) 14 | 15 | scenarios := []scenario{ 16 | { 17 | name: "pass-1", 18 | validator: v, 19 | in: []fromStructTest{ 20 | { 21 | ID: 1, 22 | Rules: []string{"2", "1"}, 23 | Users: nil, 24 | }, 25 | }, 26 | panic: false, 27 | expected: nil, 28 | }, 29 | { 30 | name: "pass-2", 31 | validator: v, 32 | in: []fromStructTest{ 33 | { 34 | ID: 1, 35 | Rules: []string{"2", "1"}, 36 | Users: []fromStructUsers{ 37 | { 38 | Name: "1", 39 | }, 40 | { 41 | Name: "2", 42 | }, 43 | { 44 | Name: "3", 45 | }, 46 | }, 47 | }, 48 | }, 49 | panic: false, 50 | expected: nil, 51 | }, 52 | { 53 | name: "fail-1", 54 | validator: v, 55 | in: []fromStructTest{ 56 | { 57 | ID: 1, 58 | Rules: []string{"2", "1", "5"}, 59 | Users: []fromStructUsers{ 60 | { 61 | Name: "1", 62 | }, 63 | { 64 | Name: "2", 65 | }, 66 | { 67 | Name: "3", 68 | }, 69 | }, 70 | }, 71 | }, 72 | panic: false, 73 | expected: map[string]interface{}{"0": map[string]interface{}{"rules": []string{"not included in allowed choices"}}}, 74 | }, 75 | } 76 | 77 | for _, s := range scenarios { 78 | t.Run(s.name, func(t *testing.T) { 79 | defer deferTestCases(t, s.panic, s.expected) 80 | 81 | output := s.validator.Validate(context.TODO(), s.in) 82 | check(t, s.expected, output) 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/from_struct_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | "github.com/golodash/godash/slices" 9 | ) 10 | 11 | type fromStructUsers struct { 12 | Name string `json:"name"` 13 | } 14 | 15 | type fromStructTest struct { 16 | ID int `json:"id" g:"min=1"` 17 | Rules []string `json:"rules" g:"custom_choices" custom_choices:"not included in allowed choices"` 18 | Users []fromStructUsers `json:"users"` 19 | } 20 | 21 | var choices = []string{ 22 | "1", "2", "3", 23 | } 24 | 25 | func custom_choices(ctx context.Context, input interface{}) bool { 26 | for _, item := range input.([]string) { 27 | if slices.FindIndex(choices, item) == -1 { 28 | return false 29 | } 30 | } 31 | return true 32 | } 33 | 34 | func TestFromStruct(t *testing.T) { 35 | v := g.CustomValidators(galidator.Validators{ 36 | "custom_choices": custom_choices, 37 | }).Validator(fromStructTest{}) 38 | 39 | scenarios := []scenario{ 40 | { 41 | name: "pass-1", 42 | validator: v, 43 | in: fromStructTest{ 44 | ID: 1, 45 | Rules: []string{"2", "1"}, 46 | Users: nil, 47 | }, 48 | panic: false, 49 | expected: nil, 50 | }, 51 | { 52 | name: "pass-2", 53 | validator: v, 54 | in: fromStructTest{ 55 | ID: 1, 56 | Rules: []string{"2", "1"}, 57 | Users: []fromStructUsers{ 58 | { 59 | Name: "1", 60 | }, 61 | { 62 | Name: "2", 63 | }, 64 | { 65 | Name: "3", 66 | }, 67 | }, 68 | }, 69 | panic: false, 70 | expected: nil, 71 | }, 72 | { 73 | name: "fail-1", 74 | validator: v, 75 | in: fromStructTest{ 76 | ID: 1, 77 | Rules: []string{"2", "1", "5"}, 78 | Users: []fromStructUsers{ 79 | { 80 | Name: "1", 81 | }, 82 | { 83 | Name: "2", 84 | }, 85 | { 86 | Name: "3", 87 | }, 88 | }, 89 | }, 90 | panic: false, 91 | expected: map[string]interface{}{"rules": []string{"not included in allowed choices"}}, 92 | }, 93 | } 94 | 95 | for _, s := range scenarios { 96 | t.Run(s.name, func(t *testing.T) { 97 | defer deferTestCases(t, s.panic, s.expected) 98 | 99 | output := s.validator.Validate(context.TODO(), s.in) 100 | check(t, s.expected, output) 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/int_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestInt(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "pass", 14 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 15 | in: 1, 16 | panic: false, 17 | expected: nil, 18 | }, 19 | { 20 | name: "fail-1", 21 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 22 | in: "1", 23 | panic: false, 24 | expected: []string{"1 is not int"}, 25 | }, 26 | { 27 | name: "fail-2", 28 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 29 | in: map[string]string{"1": "1"}, 30 | panic: false, 31 | expected: []string{"map[1:1] is not int"}, 32 | }, 33 | { 34 | name: "int", 35 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 36 | in: int(1), 37 | panic: false, 38 | expected: nil, 39 | }, 40 | { 41 | name: "int8", 42 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 43 | in: int8(1), 44 | panic: false, 45 | expected: nil, 46 | }, 47 | { 48 | name: "int16", 49 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 50 | in: int16(1), 51 | panic: false, 52 | expected: nil, 53 | }, 54 | { 55 | name: "int32", 56 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 57 | in: int32(1), 58 | panic: false, 59 | expected: nil, 60 | }, 61 | { 62 | name: "int64", 63 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 64 | in: int64(1), 65 | panic: false, 66 | expected: nil, 67 | }, 68 | { 69 | name: "uint", 70 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 71 | in: uint(1), 72 | panic: false, 73 | expected: nil, 74 | }, 75 | { 76 | name: "uint8", 77 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 78 | in: uint8(1), 79 | panic: false, 80 | expected: nil, 81 | }, 82 | { 83 | name: "uint16", 84 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 85 | in: uint16(1), 86 | panic: false, 87 | expected: nil, 88 | }, 89 | { 90 | name: "uint32", 91 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 92 | in: uint32(1), 93 | panic: false, 94 | expected: nil, 95 | }, 96 | { 97 | name: "uint64", 98 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 99 | in: uint64(1), 100 | panic: false, 101 | expected: nil, 102 | }, 103 | { 104 | name: "float32", 105 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 106 | in: float32(1), 107 | panic: false, 108 | expected: []string{"1 is not int"}, 109 | }, 110 | { 111 | name: "float64", 112 | validator: g.Validator(g.R().Int().SpecificMessages(galidator.Messages{"int": "$value is not int"})), 113 | in: float64(1), 114 | panic: false, 115 | expected: []string{"1 is not int"}, 116 | }, 117 | } 118 | 119 | for _, s := range scenarios { 120 | t.Run(s.name, func(t *testing.T) { 121 | defer deferTestCases(t, s.panic, s.expected) 122 | 123 | output := s.validator.Validate(context.TODO(), s.in) 124 | check(t, s.expected, output) 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/len_range_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestLenRange(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "pass", 14 | validator: g.Validator(g.R().LenRange(3, 5).SpecificMessages(galidator.Messages{"len_range": "must be between $from to $to characters long"})), 15 | in: "1111", 16 | panic: false, 17 | expected: nil, 18 | }, 19 | { 20 | name: "fail", 21 | validator: g.Validator(g.R().LenRange(3, 5).SpecificMessages(galidator.Messages{"len_range": "must be between $from to $to characters long"})), 22 | in: 12, 23 | panic: false, 24 | expected: []string{"must be between 3 to 5 characters long"}, 25 | }, 26 | { 27 | name: "fail-string", 28 | validator: g.Validator(g.R().LenRange(3, 5).SpecificMessages(galidator.Messages{"len_range": "must be between $from to $to characters long"})), 29 | in: "abcdef", 30 | panic: false, 31 | expected: []string{"must be between 3 to 5 characters long"}, 32 | }, 33 | { 34 | name: "fail-slice", 35 | validator: g.Validator(g.R().LenRange(3, 5).SpecificMessages(galidator.Messages{"len_range": "must be between $from to $to characters long"})), 36 | in: []int{1, 2, 3, 4, 5, 6}, 37 | panic: false, 38 | expected: []string{"must be between 3 to 5 characters long"}, 39 | }, 40 | { 41 | name: "fail-map", 42 | validator: g.Validator(g.R().LenRange(3, 5).SpecificMessages(galidator.Messages{"len_range": "must be between $from to $to characters long"})), 43 | in: map[string]int{"1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6}, 44 | panic: false, 45 | expected: []string{"must be between 3 to 5 characters long"}, 46 | }, 47 | { 48 | name: "pass-string", 49 | validator: g.Validator(g.R().LenRange(3, 5).SpecificMessages(galidator.Messages{"len_range": "must be between $from to $to characters long"})), 50 | in: "111", 51 | panic: false, 52 | expected: nil, 53 | }, 54 | { 55 | name: "pass-slice", 56 | validator: g.Validator(g.R().LenRange(3, 5).SpecificMessages(galidator.Messages{"len_range": "must be between $from to $to characters long"})), 57 | in: []int{1, 2, 3}, 58 | panic: false, 59 | expected: nil, 60 | }, 61 | { 62 | name: "pass-map", 63 | validator: g.Validator(g.R().LenRange(3, 5).SpecificMessages(galidator.Messages{"len_range": "must be between $from to $to characters long"})), 64 | in: map[string]int{"1": 1, "2": 2, "3": 3}, 65 | panic: false, 66 | expected: nil, 67 | }, 68 | } 69 | 70 | for _, s := range scenarios { 71 | t.Run(s.name, func(t *testing.T) { 72 | defer deferTestCases(t, s.panic, s.expected) 73 | 74 | output := s.validator.Validate(context.TODO(), s.in) 75 | check(t, s.expected, output) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/len_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestLen(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "pass", 14 | validator: g.Validator(g.R().Len(3).SpecificMessages(galidator.Messages{"len": "must be exactly $length characters long"})), 15 | in: "111", 16 | panic: false, 17 | expected: nil, 18 | }, 19 | { 20 | name: "fail", 21 | validator: g.Validator(g.R().Len(3).SpecificMessages(galidator.Messages{"len": "must be exactly $length characters long"})), 22 | in: 12, 23 | panic: false, 24 | expected: []string{"must be exactly 3 characters long"}, 25 | }, 26 | { 27 | name: "fail-string", 28 | validator: g.Validator(g.R().Len(3).SpecificMessages(galidator.Messages{"len": "must be exactly $length characters long"})), 29 | in: "11", 30 | panic: false, 31 | expected: []string{"must be exactly 3 characters long"}, 32 | }, 33 | { 34 | name: "fail-slice", 35 | validator: g.Validator(g.R().Len(3).SpecificMessages(galidator.Messages{"len": "must be exactly $length characters long"})), 36 | in: []int{1, 2, 3, 4, 5, 6}, 37 | panic: false, 38 | expected: []string{"must be exactly 3 characters long"}, 39 | }, 40 | { 41 | name: "fail-map", 42 | validator: g.Validator(g.R().Len(3).SpecificMessages(galidator.Messages{"len": "must be exactly $length characters long"})), 43 | in: map[string]int{"1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6}, 44 | panic: false, 45 | expected: []string{"must be exactly 3 characters long"}, 46 | }, 47 | { 48 | name: "pass-string", 49 | validator: g.Validator(g.R().Len(3).SpecificMessages(galidator.Messages{"len": "must be exactly $length characters long"})), 50 | in: "111", 51 | panic: false, 52 | expected: nil, 53 | }, 54 | { 55 | name: "pass-slice", 56 | validator: g.Validator(g.R().Len(3).SpecificMessages(galidator.Messages{"len": "must be exactly $length characters long"})), 57 | in: []int{1, 2, 3}, 58 | panic: false, 59 | expected: nil, 60 | }, 61 | { 62 | name: "pass-map", 63 | validator: g.Validator(g.R().Len(3).SpecificMessages(galidator.Messages{"len": "must be exactly $length characters long"})), 64 | in: map[string]int{"1": 1, "2": 2, "3": 3}, 65 | panic: false, 66 | expected: nil, 67 | }, 68 | } 69 | 70 | for _, s := range scenarios { 71 | t.Run(s.name, func(t *testing.T) { 72 | defer deferTestCases(t, s.panic, s.expected) 73 | 74 | output := s.validator.Validate(context.TODO(), s.in) 75 | check(t, s.expected, output) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/map_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestMap(t *testing.T) { 9 | scenarios := []scenario{ 10 | { 11 | name: "fail-int", 12 | validator: g.Validator(g.R().Map()), 13 | in: 1, 14 | panic: false, 15 | expected: []string{"not a map"}, 16 | }, 17 | { 18 | name: "fail-float", 19 | validator: g.Validator(g.R().Map()), 20 | in: 1.3, 21 | panic: false, 22 | expected: []string{"not a map"}, 23 | }, 24 | { 25 | name: "fail-slice", 26 | validator: g.Validator(g.R().Map()), 27 | in: []int{1}, 28 | panic: false, 29 | expected: []string{"not a map"}, 30 | }, 31 | { 32 | name: "pass", 33 | validator: g.Validator(g.R().Map()), 34 | in: map[string]string{"1": "1"}, 35 | panic: false, 36 | expected: nil, 37 | }, 38 | } 39 | 40 | for _, s := range scenarios { 41 | t.Run(s.name, func(t *testing.T) { 42 | defer deferTestCases(t, s.panic, s.expected) 43 | 44 | output := s.validator.Validate(context.TODO(), s.in) 45 | check(t, s.expected, output) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/max_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestMax(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "fail-int", 14 | validator: g.Validator(g.R().Max(5).SpecificMessages(galidator.Messages{"max": "can be at most $max characters long"})), 15 | in: 6, 16 | panic: false, 17 | expected: []string{"can be at most 5 characters long"}, 18 | }, 19 | { 20 | name: "fail-string", 21 | validator: g.Validator(g.R().Max(5).SpecificMessages(galidator.Messages{"max": "can be at most $max characters long"})), 22 | in: "abcdef", 23 | panic: false, 24 | expected: []string{"can be at most 5 characters long"}, 25 | }, 26 | { 27 | name: "fail-slice", 28 | validator: g.Validator(g.R().Max(5).SpecificMessages(galidator.Messages{"max": "can be at most $max characters long"})), 29 | in: []int{1, 2, 3, 4, 5, 6}, 30 | panic: false, 31 | expected: []string{"can be at most 5 characters long"}, 32 | }, 33 | { 34 | name: "fail-map", 35 | validator: g.Validator(g.R().Max(5).SpecificMessages(galidator.Messages{"max": "can be at most $max characters long"})), 36 | in: map[string]int{"1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6}, 37 | panic: false, 38 | expected: []string{"can be at most 5 characters long"}, 39 | }, 40 | { 41 | name: "pass-int", 42 | validator: g.Validator(g.R().Max(5).SpecificMessages(galidator.Messages{"max": "can be at most $max characters long"})), 43 | in: 5, 44 | panic: false, 45 | expected: nil, 46 | }, 47 | { 48 | name: "pass-string", 49 | validator: g.Validator(g.R().Max(5).SpecificMessages(galidator.Messages{"max": "can be at most $max characters long"})), 50 | in: "111", 51 | panic: false, 52 | expected: nil, 53 | }, 54 | { 55 | name: "pass-slice", 56 | validator: g.Validator(g.R().Max(5).SpecificMessages(galidator.Messages{"max": "can be at most $max characters long"})), 57 | in: []int{1, 2, 3}, 58 | panic: false, 59 | expected: nil, 60 | }, 61 | { 62 | name: "pass-map", 63 | validator: g.Validator(g.R().Max(5).SpecificMessages(galidator.Messages{"max": "can be at most $max characters long"})), 64 | in: map[string]int{"1": 1, "2": 2, "3": 3}, 65 | panic: false, 66 | expected: nil, 67 | }, 68 | } 69 | 70 | for _, s := range scenarios { 71 | t.Run(s.name, func(t *testing.T) { 72 | defer deferTestCases(t, s.panic, s.expected) 73 | 74 | output := s.validator.Validate(context.TODO(), s.in) 75 | check(t, s.expected, output) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/min_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestMin(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "fail-int", 14 | validator: g.Validator(g.R().Min(3).SpecificMessages(galidator.Messages{"min": "has to be at least $min characters long"})), 15 | in: 2, 16 | panic: false, 17 | expected: []string{"has to be at least 3 characters long"}, 18 | }, 19 | { 20 | name: "fail-string", 21 | validator: g.Validator(g.R().Min(3).SpecificMessages(galidator.Messages{"min": "has to be at least $min characters long"})), 22 | in: "ab", 23 | panic: false, 24 | expected: []string{"has to be at least 3 characters long"}, 25 | }, 26 | { 27 | name: "fail-slice", 28 | validator: g.Validator(g.R().Min(3).SpecificMessages(galidator.Messages{"min": "has to be at least $min characters long"})), 29 | in: []int{1, 2}, 30 | panic: false, 31 | expected: []string{"has to be at least 3 characters long"}, 32 | }, 33 | { 34 | name: "fail-map", 35 | validator: g.Validator(g.R().Min(3).SpecificMessages(galidator.Messages{"min": "has to be at least $min characters long"})), 36 | in: map[string]int{"1": 1, "2": 2}, 37 | panic: false, 38 | expected: []string{"has to be at least 3 characters long"}, 39 | }, 40 | { 41 | name: "pass-int", 42 | validator: g.Validator(g.R().Min(3).SpecificMessages(galidator.Messages{"min": "has to be at least $min characters long"})), 43 | in: 3, 44 | panic: false, 45 | expected: nil, 46 | }, 47 | { 48 | name: "pass-string", 49 | validator: g.Validator(g.R().Min(3).SpecificMessages(galidator.Messages{"min": "has to be at least $min characters long"})), 50 | in: "111", 51 | panic: false, 52 | expected: nil, 53 | }, 54 | { 55 | name: "pass-slice", 56 | validator: g.Validator(g.R().Min(3).SpecificMessages(galidator.Messages{"min": "has to be at least $min characters long"})), 57 | in: []int{1, 2, 3}, 58 | panic: false, 59 | expected: nil, 60 | }, 61 | { 62 | name: "pass-map", 63 | validator: g.Validator(g.R().Min(3).SpecificMessages(galidator.Messages{"min": "has to be at least $min characters long"})), 64 | in: map[string]int{"1": 1, "2": 2, "3": 3}, 65 | panic: false, 66 | expected: nil, 67 | }, 68 | } 69 | 70 | for _, s := range scenarios { 71 | t.Run(s.name, func(t *testing.T) { 72 | defer deferTestCases(t, s.panic, s.expected) 73 | 74 | output := s.validator.Validate(context.TODO(), s.in) 75 | check(t, s.expected, output) 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/non_empty_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestNonEmpty(t *testing.T) { 9 | scenarios := []scenario{ 10 | { 11 | name: "pass-int", 12 | validator: g.Validator(g.R().NonEmpty()), 13 | in: 0, 14 | panic: false, 15 | expected: nil, 16 | }, 17 | { 18 | name: "pass-float", 19 | validator: g.Validator(g.R().NonEmpty()), 20 | in: 0, 21 | panic: false, 22 | expected: nil, 23 | }, 24 | { 25 | name: "pass-string", 26 | validator: g.Validator(g.R().NonEmpty()), 27 | in: "", 28 | panic: false, 29 | expected: nil, 30 | }, 31 | { 32 | name: "pass-slice", 33 | validator: g.Validator(g.R().NonEmpty()), 34 | in: []int{1}, 35 | panic: false, 36 | expected: nil, 37 | }, 38 | { 39 | name: "pass-map", 40 | validator: g.Validator(g.R().NonEmpty()), 41 | in: map[int]int{1: 1}, 42 | panic: false, 43 | expected: nil, 44 | }, 45 | { 46 | name: "pass-nil", 47 | validator: g.Validator(g.R().NonEmpty()), 48 | in: nil, 49 | panic: false, 50 | expected: nil, 51 | }, 52 | { 53 | name: "fail-slice", 54 | validator: g.Validator(g.R().NonEmpty()), 55 | in: []int{}, 56 | panic: false, 57 | expected: []string{"can not be empty"}, 58 | }, 59 | { 60 | name: "fail-map", 61 | validator: g.Validator(g.R().NonEmpty()), 62 | in: map[int]int{}, 63 | panic: false, 64 | expected: []string{"can not be empty"}, 65 | }, 66 | } 67 | 68 | for _, s := range scenarios { 69 | t.Run(s.name, func(t *testing.T) { 70 | defer deferTestCases(t, s.panic, s.expected) 71 | 72 | output := s.validator.Validate(context.TODO(), s.in) 73 | check(t, s.expected, output) 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/non_nil_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestNonNil(t *testing.T) { 9 | scenarios := []scenario{ 10 | { 11 | name: "pass-int", 12 | validator: g.Validator(g.R().NonNil()), 13 | in: 1, 14 | panic: false, 15 | expected: nil, 16 | }, 17 | { 18 | name: "pass-float", 19 | validator: g.Validator(g.R().NonNil()), 20 | in: 1.1, 21 | panic: false, 22 | expected: nil, 23 | }, 24 | { 25 | name: "pass-string", 26 | validator: g.Validator(g.R().NonNil()), 27 | in: "1", 28 | panic: false, 29 | expected: nil, 30 | }, 31 | { 32 | name: "pass-slice", 33 | validator: g.Validator(g.R().NonNil()), 34 | in: []int{}, 35 | panic: false, 36 | expected: nil, 37 | }, 38 | { 39 | name: "pass-map", 40 | validator: g.Validator(g.R().NonNil()), 41 | in: map[int]int{}, 42 | panic: false, 43 | expected: nil, 44 | }, 45 | { 46 | name: "fail-nil", 47 | validator: g.Validator(g.R().NonNil()), 48 | in: nil, 49 | panic: false, 50 | expected: []string{"can not be nil"}, 51 | }, 52 | } 53 | 54 | for _, s := range scenarios { 55 | t.Run(s.name, func(t *testing.T) { 56 | defer deferTestCases(t, s.panic, s.expected) 57 | 58 | output := s.validator.Validate(context.TODO(), s.in) 59 | check(t, s.expected, output) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/non_zero_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestNonZero(t *testing.T) { 9 | scenarios := []scenario{ 10 | { 11 | name: "pass-int", 12 | validator: g.Validator(g.R().NonZero()), 13 | in: 1, 14 | panic: false, 15 | expected: nil, 16 | }, 17 | { 18 | name: "pass-float", 19 | validator: g.Validator(g.R().NonZero()), 20 | in: 1.1, 21 | panic: false, 22 | expected: nil, 23 | }, 24 | { 25 | name: "pass-string", 26 | validator: g.Validator(g.R().NonZero()), 27 | in: "1", 28 | panic: false, 29 | expected: nil, 30 | }, 31 | { 32 | name: "pass-slice", 33 | validator: g.Validator(g.R().NonZero()), 34 | in: []int{}, 35 | panic: false, 36 | expected: nil, 37 | }, 38 | { 39 | name: "pass-map", 40 | validator: g.Validator(g.R().NonZero()), 41 | in: map[int]int{}, 42 | panic: false, 43 | expected: nil, 44 | }, 45 | { 46 | name: "fail-int", 47 | validator: g.Validator(g.R().NonZero()), 48 | in: 0, 49 | panic: false, 50 | expected: []string{"can not be 0"}, 51 | }, 52 | { 53 | name: "fail-float", 54 | validator: g.Validator(g.R().NonZero()), 55 | in: 0.0, 56 | panic: false, 57 | expected: []string{"can not be 0"}, 58 | }, 59 | { 60 | name: "fail-string", 61 | validator: g.Validator(g.R().NonZero()), 62 | in: "", 63 | panic: false, 64 | expected: []string{"can not be 0"}, 65 | }, 66 | { 67 | name: "fail-nil", 68 | validator: g.Validator(g.R().NonZero()), 69 | in: nil, 70 | panic: false, 71 | expected: nil, 72 | }, 73 | } 74 | 75 | for _, s := range scenarios { 76 | t.Run(s.name, func(t *testing.T) { 77 | defer deferTestCases(t, s.panic, s.expected) 78 | 79 | output := s.validator.Validate(context.TODO(), s.in) 80 | check(t, s.expected, output) 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/or_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestOR(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "fail-1", 14 | validator: g.Validator(g.R().OR(g.R().String().Min(2), g.R().Int().Max(3)), galidator.Messages{"or": "or failed"}), 15 | in: "2", 16 | panic: false, 17 | expected: []string{"or failed"}, 18 | }, 19 | { 20 | name: "fail-2", 21 | validator: g.Validator(g.R().OR(g.R().String().Min(2), g.R().Int().Max(3)), galidator.Messages{"or": "or failed"}), 22 | in: 4, 23 | panic: false, 24 | expected: []string{"or failed"}, 25 | }, 26 | { 27 | name: "fail-3", 28 | validator: g.Validator(g.R().OR(g.R().String().Min(2), g.R().Int().Max(3)), galidator.Messages{"or": "or failed"}), 29 | in: []string{"s"}, 30 | panic: false, 31 | expected: []string{"or failed"}, 32 | }, 33 | { 34 | name: "pass-1", 35 | validator: g.Validator(g.R().OR(g.R().String().Min(2), g.R().Int().Max(3)), galidator.Messages{"or": "or failed"}), 36 | in: "111", 37 | panic: false, 38 | expected: nil, 39 | }, 40 | { 41 | name: "pass-2", 42 | validator: g.Validator(g.R().OR(g.R().String().Min(2), g.R().Int().Max(3)), galidator.Messages{"or": "or failed"}), 43 | in: 2, 44 | panic: false, 45 | expected: nil, 46 | }, 47 | } 48 | 49 | for _, s := range scenarios { 50 | t.Run(s.name, func(t *testing.T) { 51 | defer deferTestCases(t, s.panic, s.expected) 52 | 53 | output := s.validator.Validate(context.TODO(), s.in) 54 | check(t, s.expected, output) 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/password_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestPassword(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "fail-1", 14 | validator: g.Validator(g.R().Password().SpecificMessages(galidator.Messages{"password": "password failed"})), 15 | in: "1234567891011121314151617181920", 16 | panic: false, 17 | expected: []string{"password failed"}, 18 | }, 19 | { 20 | name: "fail-2", 21 | validator: g.Validator(g.R().Password().SpecificMessages(galidator.Messages{"password": "password failed"})), 22 | in: "123456789M", 23 | panic: false, 24 | expected: []string{"password failed"}, 25 | }, 26 | { 27 | name: "fail-3", 28 | validator: g.Validator(g.R().Password().SpecificMessages(galidator.Messages{"password": "password failed"})), 29 | in: "123456789Mh", 30 | panic: false, 31 | expected: []string{"password failed"}, 32 | }, 33 | { 34 | name: "fail-4", 35 | validator: g.Validator(g.R().Password().SpecificMessages(galidator.Messages{"password": "password failed"})), 36 | in: "123Ms!", 37 | panic: false, 38 | expected: []string{"password failed"}, 39 | }, 40 | { 41 | name: "pass", 42 | validator: g.Validator(g.R().Password().SpecificMessages(galidator.Messages{"password": "password failed"})), 43 | in: "123456789Mh!", 44 | panic: false, 45 | expected: nil, 46 | }, 47 | { 48 | name: "pass-2", 49 | validator: g.Validator(g.R().Password().SpecificMessages(galidator.Messages{"password": "password failed"})), 50 | in: "5'N*'~M.fp7iY*F", 51 | panic: false, 52 | expected: nil, 53 | }, 54 | } 55 | 56 | for _, s := range scenarios { 57 | t.Run(s.name, func(t *testing.T) { 58 | defer deferTestCases(t, s.panic, s.expected) 59 | 60 | output := s.validator.Validate(context.TODO(), s.in) 61 | check(t, s.expected, output) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/phone_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestPhone(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "pass-1", 14 | validator: g.Validator(g.R().Phone()), 15 | in: "+989123456789", 16 | panic: false, 17 | expected: nil, 18 | }, 19 | { 20 | name: "pass-2", 21 | validator: g.Validator(g.R().Phone()), 22 | in: "+989127769381", 23 | panic: false, 24 | expected: nil, 25 | }, 26 | { 27 | name: "pass-3", 28 | validator: g.Validator(g.R().Phone()), 29 | in: "+989127769383", 30 | panic: false, 31 | expected: nil, 32 | }, 33 | { 34 | name: "fail-1", 35 | validator: g.Validator(g.R().Phone().SpecificMessages(galidator.Messages{"phone": "phone failed"})), 36 | in: "09101234567", 37 | panic: false, 38 | expected: []string{"phone failed"}, 39 | }, 40 | { 41 | name: "fail-2", 42 | validator: g.Validator(g.R().Phone().SpecificMessages(galidator.Messages{"phone": "phone failed"})), 43 | in: "+989123456", 44 | panic: false, 45 | expected: []string{"phone failed"}, 46 | }, 47 | { 48 | name: "fail-3", 49 | validator: g.Validator(g.R().Phone().SpecificMessages(galidator.Messages{"phone": "phone failed"})), 50 | in: "091012345", 51 | panic: false, 52 | expected: []string{"phone failed"}, 53 | }, 54 | { 55 | name: "fail-3", 56 | validator: g.Validator(g.R().Phone().SpecificMessages(galidator.Messages{"phone": "phone failed"})), 57 | in: "+989 10 015 4789", 58 | panic: false, 59 | expected: []string{"phone failed"}, 60 | }, 61 | } 62 | 63 | for _, s := range scenarios { 64 | t.Run(s.name, func(t *testing.T) { 65 | defer deferTestCases(t, s.panic, s.expected) 66 | 67 | output := s.validator.Validate(context.TODO(), s.in) 68 | check(t, s.expected, output) 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/regex_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestRegex(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "pass", 14 | validator: g.Validator(g.R().Regex("^123$")), 15 | in: "123", 16 | panic: false, 17 | expected: nil, 18 | }, 19 | { 20 | name: "fail", 21 | validator: g.Validator(g.R().Regex("&123$").SpecificMessages(galidator.Messages{"regex": "regex failed"})), 22 | in: "1233", 23 | panic: false, 24 | expected: []string{"regex failed"}, 25 | }, 26 | } 27 | 28 | for _, s := range scenarios { 29 | t.Run(s.name, func(t *testing.T) { 30 | defer deferTestCases(t, s.panic, s.expected) 31 | 32 | output := s.validator.Validate(context.TODO(), s.in) 33 | check(t, s.expected, output) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/required_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestRequired(t *testing.T) { 9 | scenarios := []scenario{ 10 | { 11 | name: "pass-int", 12 | validator: g.Validator(g.R().Required()), 13 | in: 1, 14 | panic: false, 15 | expected: nil, 16 | }, 17 | { 18 | name: "pass-float", 19 | validator: g.Validator(g.R().Required()), 20 | in: 1.1, 21 | panic: false, 22 | expected: nil, 23 | }, 24 | { 25 | name: "pass-string", 26 | validator: g.Validator(g.R().Required()), 27 | in: "1", 28 | panic: false, 29 | expected: nil, 30 | }, 31 | { 32 | name: "pass-slice", 33 | validator: g.Validator(g.R().Required()), 34 | in: []int{1}, 35 | panic: false, 36 | expected: nil, 37 | }, 38 | { 39 | name: "pass-map", 40 | validator: g.Validator(g.R().Required()), 41 | in: map[int]int{1: 1}, 42 | panic: false, 43 | expected: nil, 44 | }, 45 | { 46 | name: "pass-int", 47 | validator: g.Validator(g.R().Required()), 48 | in: 0, 49 | panic: false, 50 | expected: nil, 51 | }, 52 | { 53 | name: "pass-float", 54 | validator: g.Validator(g.R().Required()), 55 | in: 0.0, 56 | panic: false, 57 | expected: nil, 58 | }, 59 | { 60 | name: "fail-string", 61 | validator: g.Validator(g.R().Required()), 62 | in: "", 63 | panic: false, 64 | expected: []string{"required"}, 65 | }, 66 | { 67 | name: "fail-slice", 68 | validator: g.Validator(g.R().Required()), 69 | in: []int{}, 70 | panic: false, 71 | expected: []string{"required"}, 72 | }, 73 | { 74 | name: "fail-map", 75 | validator: g.Validator(g.R().Required()), 76 | in: map[string]string{}, 77 | panic: false, 78 | expected: []string{"required"}, 79 | }, 80 | } 81 | 82 | for _, s := range scenarios { 83 | t.Run(s.name, func(t *testing.T) { 84 | defer deferTestCases(t, s.panic, s.expected) 85 | 86 | output := s.validator.Validate(context.TODO(), s.in) 87 | check(t, s.expected, output) 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/slice_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestSlice(t *testing.T) { 9 | scenarios := []scenario{ 10 | { 11 | name: "fail-int", 12 | validator: g.Validator(g.R().Slice()), 13 | in: 1, 14 | panic: false, 15 | expected: []string{"not a slice"}, 16 | }, 17 | { 18 | name: "fail-float", 19 | validator: g.Validator(g.R().Slice()), 20 | in: 1.3, 21 | panic: false, 22 | expected: []string{"not a slice"}, 23 | }, 24 | { 25 | name: "fail-map", 26 | validator: g.Validator(g.R().Slice()), 27 | in: map[string]int{"1": 1}, 28 | panic: false, 29 | expected: []string{"not a slice"}, 30 | }, 31 | { 32 | name: "pass", 33 | validator: g.Validator(g.R().Slice()), 34 | in: []int{1}, 35 | panic: false, 36 | expected: nil, 37 | }, 38 | } 39 | 40 | for _, s := range scenarios { 41 | t.Run(s.name, func(t *testing.T) { 42 | defer deferTestCases(t, s.panic, s.expected) 43 | 44 | output := s.validator.Validate(context.TODO(), s.in) 45 | check(t, s.expected, output) 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/string_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | func TestString(t *testing.T) { 9 | scenarios := []scenario{ 10 | { 11 | name: "fail-int", 12 | validator: g.Validator(g.R().String()), 13 | in: 1, 14 | panic: false, 15 | expected: []string{"not a string"}, 16 | }, 17 | { 18 | name: "fail-float", 19 | validator: g.Validator(g.R().String()), 20 | in: 1.3, 21 | panic: false, 22 | expected: []string{"not a string"}, 23 | }, 24 | { 25 | name: "fail-slice", 26 | validator: g.Validator(g.R().String()), 27 | in: []int{1}, 28 | panic: false, 29 | expected: []string{"not a string"}, 30 | }, 31 | { 32 | name: "fail-map", 33 | validator: g.Validator(g.R().String()), 34 | in: map[int]int{1: 3}, 35 | panic: false, 36 | expected: []string{"not a string"}, 37 | }, 38 | { 39 | name: "pass", 40 | validator: g.Validator(g.R().String()), 41 | in: "1", 42 | panic: false, 43 | expected: nil, 44 | }, 45 | } 46 | 47 | for _, s := range scenarios { 48 | t.Run(s.name, func(t *testing.T) { 49 | defer deferTestCases(t, s.panic, s.expected) 50 | 51 | output := s.validator.Validate(context.TODO(), s.in) 52 | check(t, s.expected, output) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/struct_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | type structTest struct{} 9 | 10 | func TestStruct(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "fail-int", 14 | validator: g.Validator(g.R().Struct()), 15 | in: 1, 16 | panic: false, 17 | expected: []string{"not a struct"}, 18 | }, 19 | { 20 | name: "fail-float", 21 | validator: g.Validator(g.R().Struct()), 22 | in: 1.3, 23 | panic: false, 24 | expected: []string{"not a struct"}, 25 | }, 26 | { 27 | name: "fail-slice", 28 | validator: g.Validator(g.R().Struct()), 29 | in: []int{1}, 30 | panic: false, 31 | expected: []string{"not a struct"}, 32 | }, 33 | { 34 | name: "pass", 35 | validator: g.Validator(g.R().Struct()), 36 | in: structTest{}, 37 | panic: false, 38 | expected: nil, 39 | }, 40 | } 41 | 42 | for _, s := range scenarios { 43 | t.Run(s.name, func(t *testing.T) { 44 | defer deferTestCases(t, s.panic, s.expected) 45 | 46 | output := s.validator.Validate(context.TODO(), s.in) 47 | check(t, s.expected, output) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/super_complex_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestSuperComplex(t *testing.T) { 11 | v := g.Validator(g.R().Slice().Children( 12 | g.R().Map().Complex(galidator.Rules{ 13 | "id": g.R().Int().Min(1).AlwaysCheckRules(), 14 | "rules": g.R().Slice().Children(g.R().Choices("1", "2", "3")), 15 | "users": g.R().Slice().Children(g.R().Complex(galidator.Rules{ 16 | "name": g.R().String(), 17 | })), 18 | }), 19 | )) 20 | 21 | scenarios := []scenario{ 22 | { 23 | name: "pass-1", 24 | validator: v, 25 | in: []interface{}{map[string]interface{}{ 26 | "id": 1, 27 | "rules": []string{"2", "1"}, 28 | "users": nil, 29 | }}, 30 | panic: false, 31 | expected: nil, 32 | }, 33 | { 34 | name: "pass-2", 35 | validator: v, 36 | in: []interface{}{map[string]interface{}{ 37 | "id": 1, 38 | "rules": []string{"2", "1"}, 39 | "users": []interface{}{ 40 | map[string]string{ 41 | "name": "1", 42 | }, 43 | map[string]string{ 44 | "name": "2", 45 | }, 46 | map[string]string{ 47 | "name": "3", 48 | }, 49 | }, 50 | }}, 51 | panic: false, 52 | expected: nil, 53 | }, 54 | { 55 | name: "fail-1", 56 | validator: v, 57 | in: []interface{}{map[string]interface{}{ 58 | "id": 1, 59 | "rules": []string{"2", "1"}, 60 | "users": []interface{}{ 61 | map[string]string{ 62 | "name": "1", 63 | }, 64 | map[string]string{ 65 | "name": "2", 66 | }, 67 | map[string]int{ 68 | "name": 3, 69 | }, 70 | }, 71 | }}, 72 | panic: false, 73 | expected: map[string]interface{}{"0": map[string]interface{}{"users": map[string]interface{}{"2": map[string]interface{}{"name": []string{"not a string"}}}}}, 74 | }, 75 | { 76 | name: "fail-2", 77 | validator: v, 78 | in: []interface{}{map[string]interface{}{ 79 | "id": 0, 80 | "rules": []string{"2", "1"}, 81 | "users": []interface{}{ 82 | map[string]string{ 83 | "name": "1", 84 | }, 85 | map[string]string{ 86 | "name": "2", 87 | }, 88 | map[string]string{ 89 | "name": "3", 90 | }, 91 | }, 92 | }}, 93 | panic: false, 94 | expected: map[string]interface{}{"0": map[string]interface{}{"id": []string{"id's length must be higher equal to 1"}}}, 95 | }, 96 | } 97 | 98 | for _, s := range scenarios { 99 | t.Run(s.name, func(t *testing.T) { 100 | defer deferTestCases(t, s.panic, s.expected) 101 | 102 | output := s.validator.Validate(context.TODO(), s.in) 103 | check(t, s.expected, output) 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/tags/choices_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | type choices struct { 9 | Name []string `g:"c.choices=1&2&3" c.choices:"choices failed"` 10 | } 11 | 12 | type choices2 struct { 13 | Name []string `g:"child.choices=1&2&3" child.choices:"choices failed"` 14 | } 15 | 16 | func TestChoices(t *testing.T) { 17 | scenarios := []scenario{ 18 | { 19 | name: "success", 20 | validator: g.Validator(choices{}), 21 | in: choices{Name: []string{"1", "2", "3", "1"}}, 22 | panic: false, 23 | expected: nil, 24 | }, 25 | { 26 | name: "fail", 27 | validator: g.Validator(choices{}), 28 | in: choices{Name: []string{"a", "2", "3", "1"}}, 29 | panic: false, 30 | expected: map[string]map[string][]string{"Name": {"0": []string{"choices failed"}}}, 31 | }, 32 | { 33 | name: "success_2", 34 | validator: g.Validator(choices2{}), 35 | in: choices2{Name: []string{"a", "2", "3", "1"}}, 36 | panic: false, 37 | expected: map[string]map[string][]string{"Name": {"0": []string{"choices failed"}}}, 38 | }, 39 | { 40 | name: "fail_2", 41 | validator: g.Validator(choices2{}), 42 | in: choices2{Name: []string{"1", "2", "cd", "sw"}}, 43 | panic: false, 44 | expected: map[string]map[string][]string{"Name": {"2": []string{"choices failed"}, "3": []string{"choices failed"}}}, 45 | }, 46 | } 47 | 48 | for _, s := range scenarios { 49 | t.Run(s.name, func(t *testing.T) { 50 | defer deferTestCases(t, s.panic, s.expected) 51 | 52 | output := s.validator.Validate(context.TODO(), s.in) 53 | check(t, s.expected, output) 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/tags/tags_when_not_exist_all_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | type WhenNotExistAllRequest struct { 9 | IdNumber string `json:"id_number" g:"when_not_exist_all=PassportNumber" when_not_exist_all:"error"` 10 | PassportNumber string `json:"passport_number" g:"when_not_exist_all=IdNumber" when_not_exist_all:"error"` 11 | } 12 | 13 | func TestTagsWhenNotExistAll(t *testing.T) { 14 | scenarios := []scenario{ 15 | { 16 | name: "success_1", 17 | validator: g.Validator(WhenNotExistAllRequest{}), 18 | in: WhenNotExistAllRequest{IdNumber: "1", PassportNumber: "1"}, 19 | panic: false, 20 | expected: nil, 21 | }, 22 | { 23 | name: "success_2", 24 | validator: g.Validator(WhenNotExistAllRequest{}), 25 | in: WhenNotExistAllRequest{IdNumber: "1", PassportNumber: ""}, 26 | panic: false, 27 | expected: nil, 28 | }, 29 | { 30 | name: "success_3", 31 | validator: g.Validator(WhenNotExistAllRequest{}), 32 | in: WhenNotExistAllRequest{IdNumber: "", PassportNumber: "1"}, 33 | panic: false, 34 | expected: nil, 35 | }, 36 | { 37 | name: "fail", 38 | validator: g.Validator(WhenNotExistAllRequest{}), 39 | in: WhenNotExistAllRequest{IdNumber: "", PassportNumber: ""}, 40 | panic: false, 41 | expected: map[string][]string{"id_number": {"error"}, "passport_number": {"error"}}, 42 | }, 43 | } 44 | 45 | for _, s := range scenarios { 46 | t.Run(s.name, func(t *testing.T) { 47 | defer deferTestCases(t, s.panic, s.expected) 48 | 49 | output := s.validator.Validate(context.TODO(), s.in) 50 | check(t, s.expected, output) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/tags/tags_when_not_exist_one_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | type WhenNotExistOneRequest struct { 9 | IdNumber string `json:"id_number" g:"when_not_exist_one=PassportNumber" when_not_exist_one:"error"` 10 | PassportNumber string `json:"passport_number" g:"when_not_exist_one=IdNumber" when_not_exist_one:"error"` 11 | } 12 | 13 | func TestTagsWhenNotExistOne(t *testing.T) { 14 | scenarios := []scenario{ 15 | { 16 | name: "success_1", 17 | validator: g.Validator(WhenNotExistOneRequest{}), 18 | in: WhenNotExistOneRequest{IdNumber: "1", PassportNumber: "1"}, 19 | panic: false, 20 | expected: nil, 21 | }, 22 | { 23 | name: "success_2", 24 | validator: g.Validator(WhenNotExistOneRequest{}), 25 | in: WhenNotExistOneRequest{IdNumber: "1", PassportNumber: ""}, 26 | panic: false, 27 | expected: nil, 28 | }, 29 | { 30 | name: "success_3", 31 | validator: g.Validator(WhenNotExistOneRequest{}), 32 | in: WhenNotExistOneRequest{IdNumber: "", PassportNumber: "1"}, 33 | panic: false, 34 | expected: nil, 35 | }, 36 | { 37 | name: "fail", 38 | validator: g.Validator(WhenNotExistOneRequest{}), 39 | in: WhenNotExistOneRequest{IdNumber: "", PassportNumber: ""}, 40 | panic: false, 41 | expected: map[string][]string{"id_number": {"error"}, "passport_number": {"error"}}, 42 | }, 43 | } 44 | 45 | for _, s := range scenarios { 46 | t.Run(s.name, func(t *testing.T) { 47 | defer deferTestCases(t, s.panic, s.expected) 48 | 49 | output := s.validator.Validate(context.TODO(), s.in) 50 | check(t, s.expected, output) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/tags/utils.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golodash/galidator/v2" 7 | "github.com/golodash/godash/generals" 8 | ) 9 | 10 | type scenario struct { 11 | name string 12 | validator galidator.Validator 13 | in interface{} 14 | panic bool 15 | expected interface{} 16 | } 17 | 18 | var g = galidator.New() 19 | 20 | // Used in test cases to prevent code breaking 21 | func deferTestCases(t *testing.T, crash bool, expected interface{}) { 22 | err := recover() 23 | 24 | if err != nil && !crash { 25 | t.Errorf("expected = %v, err = %s", expected, err) 26 | } 27 | } 28 | 29 | // Checks if `expected` and `output` are the same. 30 | // 31 | // If they are the same, returns true. 32 | // 33 | // If not, raises an error in `t` and returns false. 34 | func check(t *testing.T, expected, output interface{}) bool { 35 | if !generals.Same(output, expected) { 36 | t.Errorf("expected = %v, output = %v", expected, output) 37 | return false 38 | } 39 | return true 40 | } 41 | -------------------------------------------------------------------------------- /tests/translator_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | var translates = map[string]string{ 9 | "required": "this is required which is translated", 10 | } 11 | 12 | func testTranslator(input string) string { 13 | if out, ok := translates[input]; ok { 14 | return out 15 | } 16 | return input 17 | } 18 | 19 | func TestTranslator(t *testing.T) { 20 | scenarios := []scenario{ 21 | { 22 | name: "pass-int", 23 | validator: g.Validator(g.R().Required()), 24 | in: 0, 25 | panic: false, 26 | expected: nil, 27 | }, 28 | { 29 | name: "pass-float", 30 | validator: g.Validator(g.R().Required()), 31 | in: 0.0, 32 | panic: false, 33 | expected: nil, 34 | }, 35 | { 36 | name: "fail-string", 37 | validator: g.Validator(g.R().Required()), 38 | in: "", 39 | panic: false, 40 | expected: []string{"this is required which is translated"}, 41 | }, 42 | { 43 | name: "fail-slice", 44 | validator: g.Validator(g.R().Required()), 45 | in: []int{}, 46 | panic: false, 47 | expected: []string{"this is required which is translated"}, 48 | }, 49 | { 50 | name: "fail-map", 51 | validator: g.Validator(g.R().Required()), 52 | in: map[string]string{}, 53 | panic: false, 54 | expected: []string{"this is required which is translated"}, 55 | }, 56 | } 57 | 58 | for _, s := range scenarios { 59 | t.Run(s.name, func(t *testing.T) { 60 | defer deferTestCases(t, s.panic, s.expected) 61 | 62 | output := s.validator.Validate(context.TODO(), s.in, testTranslator) 63 | check(t, s.expected, output) 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/type_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestType(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "pass", 14 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 15 | in: 1.1, 16 | panic: false, 17 | expected: nil, 18 | }, 19 | { 20 | name: "fail", 21 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 22 | in: "1", 23 | panic: false, 24 | expected: []string{"not float64"}, 25 | }, 26 | { 27 | name: "float", 28 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 29 | in: int(1), 30 | panic: false, 31 | expected: []string{"not float64"}, 32 | }, 33 | { 34 | name: "int8", 35 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 36 | in: int8(1), 37 | panic: false, 38 | expected: []string{"not float64"}, 39 | }, 40 | { 41 | name: "int16", 42 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 43 | in: int16(1), 44 | panic: false, 45 | expected: []string{"not float64"}, 46 | }, 47 | { 48 | name: "int32", 49 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 50 | in: int32(1), 51 | panic: false, 52 | expected: []string{"not float64"}, 53 | }, 54 | { 55 | name: "int64", 56 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 57 | in: int64(1), 58 | panic: false, 59 | expected: []string{"not float64"}, 60 | }, 61 | { 62 | name: "uint", 63 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 64 | in: uint(1), 65 | panic: false, 66 | expected: []string{"not float64"}, 67 | }, 68 | { 69 | name: "uint8", 70 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 71 | in: uint8(1), 72 | panic: false, 73 | expected: []string{"not float64"}, 74 | }, 75 | { 76 | name: "uint16", 77 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 78 | in: uint16(1), 79 | panic: false, 80 | expected: []string{"not float64"}, 81 | }, 82 | { 83 | name: "uint32", 84 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 85 | in: uint32(1), 86 | panic: false, 87 | expected: []string{"not float64"}, 88 | }, 89 | { 90 | name: "uint64", 91 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 92 | in: uint64(1), 93 | panic: false, 94 | expected: []string{"not float64"}, 95 | }, 96 | { 97 | name: "float32", 98 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 99 | in: float32(1), 100 | panic: false, 101 | expected: []string{"not float64"}, 102 | }, 103 | { 104 | name: "float64", 105 | validator: g.Validator(g.R().Type(1.0).SpecificMessages(galidator.Messages{"type": "not float64"})), 106 | in: float64(1), 107 | panic: false, 108 | expected: nil, 109 | }, 110 | } 111 | 112 | for _, s := range scenarios { 113 | t.Run(s.name, func(t *testing.T) { 114 | defer deferTestCases(t, s.panic, s.expected) 115 | 116 | output := s.validator.Validate(context.TODO(), s.in) 117 | check(t, s.expected, output) 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/utils.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | "github.com/golodash/godash/generals" 9 | ) 10 | 11 | type scenario struct { 12 | name string 13 | validator galidator.Validator 14 | in interface{} 15 | panic bool 16 | expected interface{} 17 | } 18 | 19 | var g = galidator.New().CustomValidators(galidator.Validators{ 20 | "custom_validator": func(ctx context.Context, i interface{}) bool { return false }, 21 | "second_custom_validator": func(ctx context.Context, i interface{}) bool { return false }, 22 | }).CustomMessages(galidator.Messages{"custom_validator": "custom error", "second_custom_validator": "second custom error"}) 23 | 24 | // Used in test cases to prevent code breaking 25 | func deferTestCases(t *testing.T, crash bool, expected interface{}) { 26 | err := recover() 27 | 28 | if err != nil && !crash { 29 | t.Errorf("expected = %v, err = %s", expected, err) 30 | } 31 | } 32 | 33 | // Checks if `expected` and `output` are the same. 34 | // 35 | // If they are the same, returns true. 36 | // 37 | // If not, raises an error in `t` and returns false. 38 | func check(t *testing.T, expected, output interface{}) bool { 39 | if !generals.Same(output, expected) { 40 | t.Errorf("expected = %v, output = %v", expected, output) 41 | return false 42 | } 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /tests/when_exist_all_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestWhenExistAll(t *testing.T) { 11 | v := g.Validator(g.R().Complex(galidator.Rules{ 12 | "id": g.R().Int(), 13 | "name": g.R().String(), 14 | "username": g.R().WhenExistAll("id", "name").String().SpecificMessages(galidator.Messages{"when_exist_all": "we are required now"}), 15 | })) 16 | 17 | scenarios := []scenario{ 18 | { 19 | name: "fail", 20 | validator: v, 21 | in: map[string]interface{}{ 22 | "id": 1, 23 | "name": "name", 24 | "username": "", 25 | }, 26 | panic: false, 27 | expected: map[string][]string{"username": {"we are required now"}}, 28 | }, 29 | { 30 | name: "pass-1", 31 | validator: v, 32 | in: map[string]interface{}{ 33 | "id": 0, 34 | "name": "name", 35 | "username": "", 36 | }, 37 | panic: false, 38 | expected: map[string][]string{"username": {"we are required now"}}, 39 | }, 40 | { 41 | name: "pass-2", 42 | validator: v, 43 | in: map[string]interface{}{ 44 | "id": 0, 45 | "name": "", 46 | "username": "", 47 | }, 48 | panic: false, 49 | expected: nil, 50 | }, 51 | { 52 | name: "pass-3", 53 | validator: v, 54 | in: map[string]interface{}{ 55 | "id": 1, 56 | "name": "", 57 | "username": "", 58 | }, 59 | panic: false, 60 | expected: nil, 61 | }, 62 | { 63 | name: "pass-4", 64 | validator: v, 65 | in: map[string]interface{}{ 66 | "id": 1, 67 | "name": "name", 68 | "username": "ss", 69 | }, 70 | panic: false, 71 | expected: nil, 72 | }, 73 | } 74 | 75 | for _, s := range scenarios { 76 | t.Run(s.name, func(t *testing.T) { 77 | defer deferTestCases(t, s.panic, s.expected) 78 | 79 | output := s.validator.Validate(context.TODO(), s.in) 80 | check(t, s.expected, output) 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/when_exist_one_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestWhenExistOne(t *testing.T) { 11 | v := g.Validator(g.R().Complex(galidator.Rules{ 12 | "id": g.R().Int(), 13 | "name": g.R().String(), 14 | "username": g.R().WhenExistOne("id", "name").String().SpecificMessages(galidator.Messages{"when_exist_one": "we are required now"}), 15 | })) 16 | 17 | scenarios := []scenario{ 18 | { 19 | name: "fail-1", 20 | validator: v, 21 | in: map[string]interface{}{ 22 | "id": 1, 23 | "name": "", 24 | "username": "", 25 | }, 26 | panic: false, 27 | expected: map[string][]string{"username": {"we are required now"}}, 28 | }, 29 | { 30 | name: "fail-2", 31 | validator: v, 32 | in: map[string]interface{}{ 33 | "id": 0, 34 | "name": "name", 35 | "username": "", 36 | }, 37 | panic: false, 38 | expected: map[string][]string{"username": {"we are required now"}}, 39 | }, 40 | { 41 | name: "fail-3", 42 | validator: v, 43 | in: map[string]interface{}{ 44 | "id": 5, 45 | "name": "name", 46 | "username": "", 47 | }, 48 | panic: false, 49 | expected: map[string][]string{"username": {"we are required now"}}, 50 | }, 51 | { 52 | name: "fail-4", 53 | validator: v, 54 | in: map[string]interface{}{ 55 | "id": 0, 56 | "name": "", 57 | "username": "", 58 | }, 59 | panic: false, 60 | expected: map[string][]string{"username": {"we are required now"}}, 61 | }, 62 | { 63 | name: "pass-1", 64 | validator: v, 65 | in: map[string]interface{}{ 66 | "id": 1, 67 | "name": "", 68 | "username": "ss", 69 | }, 70 | panic: false, 71 | expected: nil, 72 | }, 73 | { 74 | name: "pass-2", 75 | validator: v, 76 | in: map[string]interface{}{ 77 | "id": 0, 78 | "name": "name", 79 | "username": "ss", 80 | }, 81 | panic: false, 82 | expected: nil, 83 | }, 84 | { 85 | name: "pass-3", 86 | validator: v, 87 | in: map[string]interface{}{ 88 | "id": 1, 89 | "name": "name", 90 | "username": "ss", 91 | }, 92 | panic: false, 93 | expected: nil, 94 | }, 95 | } 96 | 97 | for _, s := range scenarios { 98 | t.Run(s.name, func(t *testing.T) { 99 | defer deferTestCases(t, s.panic, s.expected) 100 | 101 | output := s.validator.Validate(context.TODO(), s.in) 102 | check(t, s.expected, output) 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/when_not_exist_all_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestWhenNotExistAll(t *testing.T) { 11 | v := g.Validator(g.R().Complex(galidator.Rules{ 12 | "first_name": g.R().String(), 13 | "last_name": g.R().String(), 14 | "username": g.R().WhenNotExistAll("last_name", "first_name").String().SpecificMessages(galidator.Messages{"when_not_exist_all": "we are required now"}), 15 | })) 16 | 17 | scenarios := []scenario{ 18 | { 19 | name: "fail", 20 | validator: v, 21 | in: map[string]interface{}{ 22 | "first_name": "", 23 | "last_name": "", 24 | "username": "", 25 | }, 26 | panic: false, 27 | expected: map[string][]string{"username": {"we are required now"}}, 28 | }, 29 | { 30 | name: "pass-1", 31 | validator: v, 32 | in: map[string]interface{}{ 33 | "first_name": "name", 34 | "last_name": "", 35 | "username": "", 36 | }, 37 | panic: false, 38 | expected: nil, 39 | }, 40 | { 41 | name: "pass-2", 42 | validator: v, 43 | in: map[string]interface{}{ 44 | "first_name": "", 45 | "last_name": "name", 46 | "username": "", 47 | }, 48 | panic: false, 49 | expected: nil, 50 | }, 51 | { 52 | name: "pass-3", 53 | validator: v, 54 | in: map[string]interface{}{ 55 | "first_name": "name", 56 | "last_name": "name", 57 | "username": "", 58 | }, 59 | panic: false, 60 | expected: nil, 61 | }, 62 | { 63 | name: "pass-3", 64 | validator: v, 65 | in: map[string]interface{}{ 66 | "first_name": "name", 67 | "last_name": "name", 68 | "username": "ss", 69 | }, 70 | panic: false, 71 | expected: nil, 72 | }, 73 | { 74 | name: "pass-4", 75 | validator: v, 76 | in: map[string]interface{}{ 77 | "first_name": "", 78 | "last_name": "", 79 | "username": "ss", 80 | }, 81 | panic: false, 82 | expected: nil, 83 | }, 84 | } 85 | 86 | for _, s := range scenarios { 87 | t.Run(s.name, func(t *testing.T) { 88 | defer deferTestCases(t, s.panic, s.expected) 89 | 90 | output := s.validator.Validate(context.TODO(), s.in) 91 | check(t, s.expected, output) 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/when_not_exist_one_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestWhenNotExistOne(t *testing.T) { 11 | v := g.Validator(g.R().Complex(galidator.Rules{ 12 | "first_name": g.R().String(), 13 | "last_name": g.R().String(), 14 | "username": g.R().WhenNotExistOne("first_name", "last_name").String().SpecificMessages(galidator.Messages{"when_not_exist_one": "we are required now"}), 15 | })) 16 | 17 | scenarios := []scenario{ 18 | { 19 | name: "fail-1", 20 | validator: v, 21 | in: map[string]interface{}{ 22 | "first_name": "name", 23 | "last_name": "", 24 | "username": "", 25 | }, 26 | panic: false, 27 | expected: map[string][]string{"username": {"we are required now"}}, 28 | }, 29 | { 30 | name: "fail-2", 31 | validator: v, 32 | in: map[string]interface{}{ 33 | "first_name": "", 34 | "last_name": "name", 35 | "username": "", 36 | }, 37 | panic: false, 38 | expected: map[string][]string{"username": {"we are required now"}}, 39 | }, 40 | { 41 | name: "fail-3", 42 | validator: v, 43 | in: map[string]interface{}{ 44 | "first_name": "", 45 | "last_name": "", 46 | "username": "", 47 | }, 48 | panic: false, 49 | expected: map[string][]string{"username": {"we are required now"}}, 50 | }, 51 | { 52 | name: "pass-1", 53 | validator: v, 54 | in: map[string]interface{}{ 55 | "first_name": "name", 56 | "last_name": "name", 57 | "username": "", 58 | }, 59 | panic: false, 60 | expected: nil, 61 | }, 62 | { 63 | name: "pass-2", 64 | validator: v, 65 | in: map[string]interface{}{ 66 | "first_name": "name", 67 | "last_name": "", 68 | "username": "ss", 69 | }, 70 | panic: false, 71 | expected: nil, 72 | }, 73 | { 74 | name: "pass-3", 75 | validator: v, 76 | in: map[string]interface{}{ 77 | "first_name": "", 78 | "last_name": "name", 79 | "username": "ss", 80 | }, 81 | panic: false, 82 | expected: nil, 83 | }, 84 | { 85 | name: "pass-4", 86 | validator: v, 87 | in: map[string]interface{}{ 88 | "first_name": "name", 89 | "last_name": "name", 90 | "username": "ss", 91 | }, 92 | panic: false, 93 | expected: nil, 94 | }, 95 | } 96 | 97 | for _, s := range scenarios { 98 | t.Run(s.name, func(t *testing.T) { 99 | defer deferTestCases(t, s.panic, s.expected) 100 | 101 | output := s.validator.Validate(context.TODO(), s.in) 102 | check(t, s.expected, output) 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/xor_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golodash/galidator/v2" 8 | ) 9 | 10 | func TestXOR(t *testing.T) { 11 | scenarios := []scenario{ 12 | { 13 | name: "fail-both_fail", 14 | validator: g.Validator(g.R().XOR(g.R().Min(2), g.R().Max(1).String()), galidator.Messages{"xor": "xor failed"}), 15 | in: 1, 16 | panic: false, 17 | expected: []string{"xor failed"}, 18 | }, 19 | { 20 | name: "fail-both_succeed", 21 | validator: g.Validator(g.R().XOR(g.R().Min(2), g.R().Min(1).String()), galidator.Messages{"xor": "xor failed"}), 22 | in: "22", 23 | panic: false, 24 | expected: []string{"xor failed"}, 25 | }, 26 | { 27 | name: "pass-1", 28 | validator: g.Validator(g.R().XOR(g.R().Min(2), g.R().Min(1).String()), galidator.Messages{"xor": "xor failed"}), 29 | in: 35, 30 | panic: false, 31 | expected: nil, 32 | }, 33 | { 34 | name: "pass-2", 35 | validator: g.Validator(g.R().XOR(g.R().Min(2), g.R().Min(1).String()), galidator.Messages{"xor": "xor failed"}), 36 | in: "3", 37 | panic: false, 38 | expected: nil, 39 | }, 40 | } 41 | 42 | for _, s := range scenarios { 43 | t.Run(s.name, func(t *testing.T) { 44 | defer deferTestCases(t, s.panic, s.expected) 45 | 46 | output := s.validator.Validate(context.TODO(), s.in) 47 | check(t, s.expected, output) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package galidator 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | 11 | gStrings "github.com/golodash/godash/strings" 12 | ) 13 | 14 | // Determines the precision of a float number for print 15 | func determinePrecision(number float64) string { 16 | for i := 0; ; i++ { 17 | ten := math.Pow10(i) 18 | if math.Floor(ten*number) == ten*number { 19 | return fmt.Sprint(i) 20 | } 21 | } 22 | } 23 | 24 | // Returns true if input is nil 25 | func isNil(input interface{}) bool { 26 | return !reflect.ValueOf(input).IsValid() || input == nil || (reflect.TypeOf(input).Kind() == reflect.Ptr && reflect.ValueOf(input).IsNil()) 27 | } 28 | 29 | // Returns true if input is map or slice and has 0 elements 30 | // 31 | // Returns false if input is not map or slice 32 | func hasZeroItems(input interface{}) bool { 33 | inputValue := reflect.ValueOf(input) 34 | switch inputValue.Kind() { 35 | case reflect.Slice, reflect.Map: 36 | return inputValue.Len() == 0 37 | default: 38 | return false 39 | } 40 | } 41 | 42 | // A helper function to use inside code 43 | func isEmptyNilZero(input interface{}) bool { 44 | return !requiredRule(context.TODO(), input) 45 | } 46 | 47 | // Returns values of passed fields from passed struct or map 48 | func getValues(all interface{}, fields ...string) []interface{} { 49 | fieldsValues := []interface{}{} 50 | allValue := reflect.ValueOf(all) 51 | 52 | if allValue.Kind() == reflect.Map { 53 | for _, key := range fields { 54 | element := allValue.MapIndex(reflect.ValueOf(key)) 55 | if !element.IsValid() { 56 | element = allValue.MapIndex(reflect.ValueOf(key)) 57 | } 58 | 59 | if !element.IsValid() { 60 | panic(fmt.Sprintf("value on %s field is not valid", key)) 61 | } 62 | 63 | fieldsValues = append(fieldsValues, element.Interface()) 64 | } 65 | } else if allValue.Kind() == reflect.Struct { 66 | for _, key := range fields { 67 | element := allValue.FieldByName(key) 68 | if !element.IsValid() { 69 | panic(fmt.Sprintf("value on %s field is not valid", key)) 70 | } 71 | 72 | fieldsValues = append(fieldsValues, element.Interface()) 73 | } 74 | } 75 | 76 | return fieldsValues 77 | } 78 | 79 | // Returns a list of keys for requires which determine not required and a bool which determines if we need to validate or not 80 | func determineRequires(all interface{}, input interface{}, requires requires) (map[string]interface{}, bool) { 81 | output := map[string]interface{}{} 82 | if len(requires) == 0 { 83 | return output, false 84 | } 85 | for key, req := range requires { 86 | if !req(all)(input) { 87 | output[key] = 1 88 | } 89 | } 90 | 91 | return output, len(output) == len(requires) 92 | } 93 | 94 | // Passes messages to other validators 95 | func deepPassMessages(v Validator, messages *Messages) { 96 | v.setMessages(messages) 97 | r := v.getRule() 98 | if r != nil { 99 | if v1 := r.getChildrenValidator(); v1 != nil { 100 | deepPassMessages(v1, messages) 101 | } 102 | if v2 := r.getDeepValidator(); v2 != nil { 103 | deepPassMessages(v2, messages) 104 | } 105 | } 106 | rs := v.getRules() 107 | if rs == nil { 108 | return 109 | } 110 | for _, r := range rs { 111 | if r != nil { 112 | if v1 := r.getChildrenValidator(); v1 != nil { 113 | deepPassMessages(v1, messages) 114 | } 115 | if v2 := r.getDeepValidator(); v2 != nil { 116 | deepPassMessages(v2, messages) 117 | } 118 | } 119 | } 120 | } 121 | 122 | // Adds one specific message to passed ruleSet if message is not a empty string 123 | func addSpecificMessage(r ruleSet, funcName, message string) { 124 | splits := strings.SplitN(funcName, ".", 2) 125 | if firstElementSnake := gStrings.SnakeCase(splits[0]); len(splits) > 1 && (firstElementSnake == "c" || firstElementSnake == "child") { 126 | addSpecificMessage(r.getChildrenValidator().getRule(), splits[1], message) 127 | return 128 | } 129 | funcName = gStrings.SnakeCase(funcName) 130 | if message != "" { 131 | r.appendSpecificMessages(funcName, message) 132 | } 133 | } 134 | 135 | // Adds rules which are inside passed slice of strings called tag 136 | func applyRules(r ruleSet, tag []string, o *generatorS, orXor bool) (normalFuncName string) { 137 | normalFuncName = strings.TrimSpace(tag[0]) 138 | funcName := normalFuncName 139 | splits := strings.SplitN(normalFuncName, ".", 2) 140 | if firstElementSnake := gStrings.SnakeCase(splits[0]); len(splits) > 1 && (firstElementSnake == "c" || firstElementSnake == "child") { 141 | if r.getChildrenValidator() == nil { 142 | r.Children(o.R()) 143 | } 144 | if len(tag) > 1 { 145 | applyRules(r.getChildrenValidator().getRule(), []string{splits[1], tag[1]}, o, orXor) 146 | } else { 147 | applyRules(r.getChildrenValidator().getRule(), []string{splits[1]}, o, orXor) 148 | } 149 | return normalFuncName 150 | } else if len(splits) == 1 { 151 | funcName = gStrings.PascalCase(normalFuncName) 152 | } else { 153 | panic(fmt.Sprintf("can't understand %s rule", normalFuncName)) 154 | } 155 | 156 | parameters := []string{} 157 | if len(tag) == 2 { 158 | parameters = strings.Split(tag[1], "&") 159 | } 160 | 161 | switch funcName { 162 | case "Int": 163 | r.Int() 164 | case "Float": 165 | r.Float() 166 | case "Min": 167 | if len(parameters) == 1 { 168 | if p1, err := strconv.ParseFloat(parameters[0], 64); err == nil { 169 | r.Min(p1) 170 | } 171 | } 172 | case "Max": 173 | if len(parameters) == 1 { 174 | if p1, err := strconv.ParseFloat(parameters[0], 64); err == nil { 175 | r.Max(p1) 176 | } 177 | } 178 | case "LenRange": 179 | if len(parameters) == 2 { 180 | if p1, err := strconv.ParseInt(parameters[0], 10, 64); err == nil { 181 | if p2, err := strconv.ParseInt(parameters[1], 10, 64); err == nil { 182 | r.LenRange(int(p1), int(p2)) 183 | } 184 | } 185 | } 186 | case "Len": 187 | if len(parameters) == 1 { 188 | if p1, err := strconv.ParseInt(parameters[0], 10, 64); err == nil { 189 | r.Len(int(p1)) 190 | } 191 | } 192 | case "Required": 193 | r.Required() 194 | case "Optional": 195 | r.Optional() 196 | case "NonZero": 197 | r.NonZero() 198 | case "NonNil": 199 | r.NonNil() 200 | case "NonEmpty": 201 | r.NonEmpty() 202 | case "Email": 203 | r.Email() 204 | case "Regex": 205 | if len(parameters) > 1 { 206 | r.Regex(parameters[0]) 207 | } 208 | case "Phone": 209 | r.Phone() 210 | case "Map": 211 | r.Map() 212 | case "Slice": 213 | r.Slice() 214 | case "Struct": 215 | r.Struct() 216 | case "Password": 217 | r.Password() 218 | case "Or", "OR": 219 | if orXor { 220 | rules := []ruleSet{} 221 | parametersSeparated := strings.Split(tag[1], "|") 222 | for _, parameters := range parametersSeparated { 223 | rule := o.R() 224 | parameter := strings.Split(parameters, "+") 225 | for _, p := range parameter { 226 | applyRules(rule, strings.SplitN(p, "=", 2), o, false) 227 | } 228 | rules = append(rules, rule) 229 | } 230 | 231 | r.OR(rules...) 232 | } else { 233 | panic("OR or XOR inside another OR or XOR is not possible") 234 | } 235 | case "Xor", "XOr", "XOR": 236 | if orXor { 237 | rules := []ruleSet{} 238 | parametersSeparated := strings.Split(tag[1], "|") 239 | for _, parameters := range parametersSeparated { 240 | rule := o.R() 241 | parameter := strings.Split(parameters, "+") 242 | for _, p := range parameter { 243 | applyRules(rule, strings.SplitN(p, "=", 2), o, false) 244 | } 245 | rules = append(rules, rule) 246 | } 247 | 248 | r.XOR(rules...) 249 | } else { 250 | panic("OR or XOR inside another OR or XOR is not possible") 251 | } 252 | case "Choices": 253 | params := []interface{}{} 254 | for _, item := range parameters { 255 | params = append(params, item) 256 | } 257 | r.Choices(params...) 258 | case "WhenExistOne": 259 | r.WhenExistOne(parameters...) 260 | case "WhenExistAll": 261 | r.WhenExistAll(parameters...) 262 | case "WhenNotExistOne": 263 | r.WhenNotExistOne(parameters...) 264 | case "WhenNotExistAll": 265 | r.WhenNotExistAll(parameters...) 266 | case "String": 267 | r.String() 268 | case "Children", "Custom", "Complex", "Type": 269 | panic(fmt.Sprintf("take a look at documentations, %s rule does not work in tags like this", funcName)) 270 | default: 271 | if normalFuncName != "" { 272 | if function, ok := o.customValidators[normalFuncName]; ok { 273 | r.Custom(Validators{ 274 | normalFuncName: function, 275 | }) 276 | } else { 277 | panic(fmt.Sprintf("%s custom validator did not find, call CustomValidators function before calling Validator function", normalFuncName)) 278 | } 279 | } 280 | } 281 | 282 | return 283 | } 284 | -------------------------------------------------------------------------------- /validator.go: -------------------------------------------------------------------------------- 1 | package galidator 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | 12 | playgroundValidator "github.com/go-playground/validator/v10" 13 | gStrings "github.com/golodash/godash/strings" 14 | ) 15 | 16 | type ( 17 | // To create a map of rules 18 | Rules map[string]ruleSet 19 | 20 | // To specify errors for rules 21 | Messages map[string]string 22 | 23 | Translator func(string) string 24 | 25 | // A struct to implement Validator interface 26 | validatorS struct { 27 | // Slice validator has this item full 28 | rule ruleSet 29 | // Stores keys of fields with their rules 30 | rules Rules 31 | // Stores custom error messages sent by user 32 | messages *Messages 33 | } 34 | 35 | // Used just in decryptErrors function 36 | sliceValidationError []error 37 | 38 | // Validator interface 39 | Validator interface { 40 | // Validates passed data and returns a map of possible validation errors happened on every field with failed validation. 41 | // 42 | // If no errors found, output will be nil 43 | Validate(ctx context.Context, input interface{}, translator ...Translator) interface{} 44 | // Decrypts errors returned from gin's Bind process and returns proper error messages 45 | // 46 | // If returnUnmarshalErrorContext is true (default is true), if an error happened when 47 | // unmarshal process was happening, `UnmarshalError` message will return like: {"message":"unmarshal error"} 48 | // 49 | // If returnUnmarshalErrorContext is false, actual error message which tells you what went wrong 50 | // will return 51 | DecryptErrors(err error, returnUnmarshalErrorContext ...bool) interface{} 52 | // Sets passed default values if value field is nil 53 | SetDefaultOnNil(input interface{}, defaultValue interface{}) 54 | // Sets passed default values if value field is zero 55 | SetDefault(input interface{}, defaultValue interface{}) 56 | // Returns ruleSet of an input 57 | GetStructRule(input string) ruleSet 58 | // Returns the ruleSet of current validator 59 | GetRule() ruleSet 60 | // Returns Rules 61 | getRules() Rules 62 | // Returns rule 63 | getRule() ruleSet 64 | // Replaces passed messages with existing one 65 | setMessages(messages *Messages) 66 | // Returns messages 67 | getMessages() *Messages 68 | } 69 | ) 70 | 71 | const ( 72 | // Error message that will return when UnmarshalTypeError happens 73 | UnmarshalError = "unmarshal error" 74 | ) 75 | 76 | func (err sliceValidationError) Error() string { 77 | return "error" 78 | } 79 | 80 | func getFormattedErrorMessage(message string, fieldName string, value interface{}, options option, translator ...Translator) string { 81 | var t Translator = nil 82 | if len(translator) != 0 { 83 | t = translator[0] 84 | } 85 | for key, value := range options { 86 | message = strings.ReplaceAll(message, "$"+key, value) 87 | } 88 | newValue := fmt.Sprint(value) 89 | if t != nil { 90 | newValue = t(newValue) 91 | } 92 | return strings.ReplaceAll(strings.ReplaceAll(message, "$field", fieldName), "$value", newValue) 93 | } 94 | 95 | // Formats and returns error message associated with passed ruleKey 96 | func getRawErrorMessage(ruleKey string, messages Messages, specificMessage Messages, defaultErrorMessages map[string]string) string { 97 | // Search for error message in specific messages for the rule 98 | if out, ok := specificMessage[gStrings.SnakeCase(ruleKey)]; len(specificMessage) != 0 && ok { 99 | return out 100 | } 101 | 102 | // Search for error message in general messages for the validator 103 | if out, ok := messages[ruleKey]; ok { 104 | return out 105 | } else { 106 | // If no error message found, search if there is a default error message for rule error 107 | if defaultErrorMessage, ok := defaultErrorMessages[ruleKey]; ok { 108 | return defaultErrorMessage 109 | } else { 110 | // If no error message found, return that error message doesn't exist 111 | return fmt.Sprintf("error happened but no error message exists on '%s' rule key", ruleKey) 112 | } 113 | } 114 | } 115 | 116 | func (o *validatorS) GetRule() ruleSet { 117 | return o.rule 118 | } 119 | 120 | func (o *validatorS) GetStructRule(input string) ruleSet { 121 | if rule, ok := o.rules[input]; ok { 122 | return rule 123 | } 124 | return nil 125 | } 126 | 127 | func (o *validatorS) Validate(ctx context.Context, input interface{}, translator ...Translator) interface{} { 128 | for reflect.ValueOf(input).Kind() == reflect.Ptr { 129 | input = reflect.ValueOf(input).Elem().Interface() 130 | } 131 | var t Translator = nil 132 | if len(translator) != 0 { 133 | t = translator[0] 134 | } 135 | 136 | output := map[string]interface{}{} 137 | inputValue := reflect.ValueOf(input) 138 | 139 | validate := func(ruleSet ruleSet, onKeyInput interface{}, fieldName string) []string { 140 | for reflect.ValueOf(onKeyInput).IsValid() && reflect.TypeOf(onKeyInput).Kind() == reflect.Ptr { 141 | onKeyInputValue := reflect.ValueOf(onKeyInput).Elem() 142 | if onKeyInputValue.IsValid() { 143 | onKeyInput = onKeyInputValue.Interface() 144 | } else { 145 | onKeyInput = nil 146 | } 147 | } 148 | 149 | halfOutput := []string{} 150 | fails := ruleSet.validate(ctx, onKeyInput) 151 | if len(fails) != 0 { 152 | for _, failKey := range fails { 153 | var m Messages = nil 154 | var sm Messages = ruleSet.getSpecificMessages() 155 | if o.messages != nil { 156 | m = *o.messages 157 | } 158 | message := getRawErrorMessage(failKey, m, sm, defaultValidatorErrorMessages) 159 | if t != nil { 160 | message = t(message) 161 | } 162 | message = getFormattedErrorMessage(message, fieldName, onKeyInput, ruleSet.getOption(failKey), t) 163 | halfOutput = append(halfOutput, message) 164 | } 165 | } 166 | 167 | return halfOutput 168 | } 169 | 170 | if o.rules != nil { 171 | switch inputValue.Kind() { 172 | case reflect.Struct: 173 | for fieldName, ruleSet := range o.rules { 174 | valueOnKeyInput := inputValue.FieldByName(fieldName) 175 | typeOnKeyInput, found := inputValue.Type().FieldByName(fieldName) 176 | if ruleSet.getName() != "" { 177 | fieldName = ruleSet.getName() 178 | } 179 | 180 | if !valueOnKeyInput.IsValid() { 181 | valueOnKeyInput = inputValue.FieldByName(fieldName) 182 | } 183 | if !valueOnKeyInput.IsValid() { 184 | panic(fmt.Sprintf("value on %s is not valid", fieldName)) 185 | } 186 | 187 | // If not exported 188 | if !found || typeOnKeyInput.PkgPath != "" { 189 | break 190 | } 191 | value := valueOnKeyInput.Interface() 192 | // Just continue if no requires are set and field is empty, nil or zero 193 | requires, isRequired := determineRequires(input, value, ruleSet.getRequires()) 194 | if (!ruleSet.isRequired() && !isRequired) && isEmptyNilZero(value) { 195 | continue 196 | } 197 | 198 | errors := validate(ruleSet, value, fieldName) 199 | dels := []int{} 200 | for i, key := range errors { 201 | if _, ok := requires[key]; ok { 202 | dels = append(dels, i) 203 | } 204 | } 205 | j := 0 206 | for _, key := range dels { 207 | errors = append(errors[:key-j], errors[key+1-j:]...) 208 | j++ 209 | } 210 | if len(errors) != 0 { 211 | output[fieldName] = errors 212 | } 213 | 214 | if ruleSet.hasDeepValidator() && output[fieldName] == nil && (mapRule(ctx, value) || structRule(ctx, value) || sliceRule(ctx, value)) { 215 | data := ruleSet.validateDeepValidator(ctx, value, t) 216 | 217 | if reflect.ValueOf(data).IsValid() && reflect.ValueOf(data).Len() != 0 { 218 | output[fieldName] = data 219 | } 220 | } 221 | 222 | if ruleSet.hasChildrenValidator() && output[fieldName] == nil && sliceRule(ctx, value) { 223 | valueOnKeyInput = reflect.ValueOf(valueOnKeyInput.Interface()) 224 | for i := 0; i < valueOnKeyInput.Len(); i++ { 225 | element := valueOnKeyInput.Index(i) 226 | errors := ruleSet.validateChildrenValidator(ctx, element.Interface(), t) 227 | if reflect.ValueOf(errors).IsValid() && reflect.ValueOf(errors).Len() != 0 { 228 | if _, ok := output[fieldName]; !ok { 229 | output[fieldName] = map[string]interface{}{} 230 | } 231 | output[fieldName].(map[string]interface{})[strconv.Itoa(i)] = errors 232 | } 233 | } 234 | } 235 | } 236 | case reflect.Map: 237 | for fieldName, ruleSet := range o.rules { 238 | valueOnKeyInput := inputValue.MapIndex(reflect.ValueOf(fieldName)) 239 | if ruleSet.getName() != "" { 240 | fieldName = ruleSet.getName() 241 | } 242 | 243 | if !valueOnKeyInput.IsValid() { 244 | valueOnKeyInput = inputValue.MapIndex(reflect.ValueOf(fieldName)) 245 | } 246 | if !valueOnKeyInput.IsValid() { 247 | panic(fmt.Sprintf("value on %s is not valid", fieldName)) 248 | } 249 | 250 | value := valueOnKeyInput.Interface() 251 | // Just continue if no requires are set and field is empty, nil or zero 252 | requires, isRequired := determineRequires(input, value, ruleSet.getRequires()) 253 | if (!ruleSet.isRequired() && !isRequired) && isEmptyNilZero(value) { 254 | continue 255 | } 256 | 257 | errors := validate(ruleSet, value, fieldName) 258 | dels := []int{} 259 | for i, key := range errors { 260 | if _, ok := requires[key]; ok { 261 | dels = append(dels, i) 262 | } 263 | } 264 | j := 0 265 | for _, key := range dels { 266 | errors = append(errors[:key-j], errors[key+1-j:]...) 267 | j++ 268 | } 269 | if len(errors) != 0 { 270 | output[fieldName] = errors 271 | } 272 | 273 | if ruleSet.hasDeepValidator() && output[fieldName] == nil && (mapRule(ctx, value) || structRule(ctx, value) || sliceRule(ctx, value)) { 274 | data := ruleSet.validateDeepValidator(ctx, value, t) 275 | 276 | if reflect.ValueOf(data).IsValid() && reflect.ValueOf(data).Len() != 0 { 277 | output[fieldName] = data 278 | } 279 | } 280 | 281 | if ruleSet.hasChildrenValidator() && output[fieldName] == nil && sliceRule(ctx, value) { 282 | valueOnKeyInput = reflect.ValueOf(valueOnKeyInput.Interface()) 283 | for i := 0; i < valueOnKeyInput.Len(); i++ { 284 | element := valueOnKeyInput.Index(i) 285 | errors := ruleSet.validateChildrenValidator(ctx, element.Interface(), t) 286 | if reflect.ValueOf(errors).IsValid() && reflect.ValueOf(errors).Len() != 0 { 287 | if _, ok := output[fieldName]; !ok { 288 | output[fieldName] = map[string]interface{}{} 289 | } 290 | output[fieldName].(map[string]interface{})[strconv.Itoa(i)] = errors 291 | } 292 | } 293 | } 294 | } 295 | default: 296 | return []string{"invalid input"} 297 | } 298 | } else if o.rule != nil { 299 | if !o.rule.isRequired() && isEmptyNilZero(input) { 300 | return nil 301 | } 302 | 303 | errors := validate(o.rule, input, o.rule.getName()) 304 | if len(errors) != 0 { 305 | return errors 306 | } 307 | 308 | switch inputValue.Kind() { 309 | case reflect.Slice: 310 | if o.rule.hasChildrenValidator() { 311 | for i := 0; i < inputValue.Len(); i++ { 312 | element := inputValue.Index(i) 313 | errors := o.rule.validateChildrenValidator(ctx, element.Interface(), t) 314 | 315 | if reflect.ValueOf(errors).IsValid() && reflect.ValueOf(errors).Len() != 0 { 316 | if _, ok := output[strconv.Itoa(i)]; !ok { 317 | output[strconv.Itoa(i)] = errors 318 | } 319 | } 320 | } 321 | } 322 | default: 323 | if o.rule.hasDeepValidator() { 324 | errors := o.rule.validateDeepValidator(ctx, input, t) 325 | if reflect.ValueOf(errors).IsValid() && reflect.ValueOf(errors).Len() != 0 { 326 | return errors 327 | } 328 | } 329 | } 330 | } else { 331 | return []string{"invalid validator"} 332 | } 333 | 334 | if len(output) == 0 { 335 | return nil 336 | } 337 | 338 | return output 339 | } 340 | 341 | func (o *validatorS) getMessages() *Messages { 342 | return o.messages 343 | } 344 | 345 | func (o *validatorS) DecryptErrors(err error, returnUnmarshalErrorContext ...bool) interface{} { 346 | if err == nil { 347 | return nil 348 | } 349 | 350 | unmarshalError := false 351 | if len(returnUnmarshalErrorContext) > 0 { 352 | unmarshalError = returnUnmarshalErrorContext[0] 353 | } 354 | 355 | return decryptErrors(err, o, unmarshalError) 356 | } 357 | 358 | func (o *validatorS) SetDefaultOnNil(input interface{}, defaultValue interface{}) { 359 | if reflect.TypeOf(input).Kind() != reflect.Ptr { 360 | panic("Please send data as a pointer like: &input") 361 | } 362 | 363 | o.setDefaultOn(input, defaultValue, true, false) 364 | } 365 | 366 | func (o *validatorS) SetDefault(input interface{}, defaultValue interface{}) { 367 | if reflect.TypeOf(input).Kind() != reflect.Ptr { 368 | panic("Please send data as a pointer like: &input") 369 | } 370 | 371 | o.setDefaultOn(input, defaultValue, false, true) 372 | } 373 | 374 | func (o *validatorS) setDefaultOn(input interface{}, defaultInput interface{}, onNil, onZero bool) { 375 | inputValue := reflect.ValueOf(input) 376 | defaultValue := reflect.ValueOf(defaultInput) 377 | 378 | for inputValue.Kind() == reflect.Ptr { 379 | inputValue = inputValue.Elem() 380 | } 381 | 382 | for defaultValue.Kind() == reflect.Ptr { 383 | defaultValue = defaultValue.Elem() 384 | } 385 | 386 | if o.rules != nil { 387 | switch inputValue.Type().Kind() { 388 | case reflect.Struct: 389 | for fieldName, ruleSet := range o.rules { 390 | valueOnKeyInput := inputValue.FieldByName(fieldName) 391 | valueOnDefaultKeyInput := defaultValue.FieldByName(fieldName) 392 | if ruleSet.getName() != "" { 393 | fieldName = ruleSet.getName() 394 | } 395 | 396 | if !valueOnKeyInput.IsValid() { 397 | valueOnKeyInput = inputValue.FieldByName(fieldName) 398 | } 399 | if !valueOnDefaultKeyInput.IsValid() { 400 | valueOnDefaultKeyInput = defaultValue.FieldByName(fieldName) 401 | } 402 | if !valueOnKeyInput.IsValid() || !valueOnDefaultKeyInput.IsValid() { 403 | panic(fmt.Sprintf("value on %s is not valid", fieldName)) 404 | } 405 | 406 | value := valueOnKeyInput.Interface() 407 | if (onNil && isNil(value)) || (onZero && !nonZeroRule(nil, value)) { 408 | if valueOnKeyInput.Kind() != reflect.Ptr { 409 | panic(fmt.Sprintf("value on %s on the side you want to copy defaults on nil(first input) has to be pointer", fieldName)) 410 | } 411 | if valueOnDefaultKeyInput.Kind() != reflect.Ptr { 412 | valueOnDefaultKeyInput = valueOnDefaultKeyInput.Addr() 413 | } 414 | valueOnKeyInput.Set(valueOnDefaultKeyInput) 415 | } else { 416 | if ruleSet.hasDeepValidator() && (mapRule(nil, value) || structRule(nil, value) || sliceRule(nil, value)) { 417 | deepValidator := ruleSet.getDeepValidator() 418 | 419 | if onNil { 420 | deepValidator.SetDefaultOnNil(valueOnKeyInput.Interface(), valueOnDefaultKeyInput.Interface()) 421 | } else if onZero { 422 | deepValidator.SetDefault(valueOnKeyInput.Interface(), valueOnDefaultKeyInput.Interface()) 423 | } 424 | } 425 | } 426 | } 427 | case reflect.Map: 428 | for fieldName, ruleSet := range o.rules { 429 | valueOnKeyInput := inputValue.MapIndex(reflect.ValueOf(fieldName)) 430 | valueOnDefaultKeyInput := defaultValue.MapIndex(reflect.ValueOf(fieldName)) 431 | if ruleSet.getName() != "" { 432 | fieldName = ruleSet.getName() 433 | } 434 | 435 | if !valueOnKeyInput.IsValid() { 436 | valueOnKeyInput = inputValue.MapIndex(reflect.ValueOf(fieldName)) 437 | } 438 | if !valueOnDefaultKeyInput.IsValid() { 439 | valueOnDefaultKeyInput = defaultValue.MapIndex(reflect.ValueOf(fieldName)) 440 | } 441 | if !valueOnKeyInput.IsValid() || !valueOnDefaultKeyInput.IsValid() { 442 | panic(fmt.Sprintf("value on %s is not valid", fieldName)) 443 | } 444 | 445 | value := valueOnKeyInput.Interface() 446 | if (onNil && isNil(value)) || (onZero && !nonZeroRule(context.TODO(), value)) { 447 | valueOnKeyInput.Set(valueOnDefaultKeyInput) 448 | } else { 449 | if ruleSet.hasDeepValidator() && (mapRule(context.TODO(), value) || structRule(context.TODO(), value) || sliceRule(context.TODO(), value)) { 450 | deepValidator := ruleSet.getDeepValidator() 451 | 452 | if onNil { 453 | deepValidator.SetDefaultOnNil(valueOnKeyInput.Interface(), valueOnDefaultKeyInput.Interface()) 454 | } else if onZero { 455 | deepValidator.SetDefault(valueOnKeyInput.Interface(), valueOnDefaultKeyInput.Interface()) 456 | } 457 | } 458 | } 459 | } 460 | default: 461 | return 462 | } 463 | } else if o.rule != nil { 464 | value := inputValue.Interface() 465 | if o.rule.hasDeepValidator() && (mapRule(context.TODO(), value) || structRule(context.TODO(), value) || sliceRule(context.TODO(), value)) { 466 | deepValidator := o.rule.getDeepValidator() 467 | 468 | if onNil { 469 | deepValidator.SetDefaultOnNil(value, defaultInput) 470 | } else if onZero { 471 | deepValidator.SetDefault(value, defaultInput) 472 | } 473 | } 474 | } 475 | } 476 | 477 | func getFieldName(path string) string { 478 | return strings.SplitN(path, ".", 2)[0] 479 | } 480 | 481 | func decryptPath(path string, v Validator, errorField playgroundValidator.FieldError) interface{} { 482 | var ( 483 | splits = strings.SplitN(path, ".", 2) 484 | fieldName = splits[0] 485 | output = map[string]interface{}{} 486 | ) 487 | if len(splits) == 2 { 488 | path = splits[1] 489 | } else { 490 | path = "" 491 | } 492 | if path == "" { 493 | if rs := v.getRules(); len(rs) != 0 { 494 | if r, ok := rs[fieldName]; ok { 495 | if r.getName() != "" { 496 | fieldName = r.getName() 497 | } 498 | message := getRawErrorMessage(errorField.Tag(), *v.getMessages(), r.getSpecificMessages(), defaultValidatorErrorMessages) 499 | // Do not add translator, translator is for Validate process 500 | message = getFormattedErrorMessage(message, fieldName, errorField.Value(), r.getOption(errorField.Tag())) 501 | return message 502 | } else { 503 | panic("error structure does not match with validator structure") 504 | } 505 | } else { 506 | panic("error structure does not match with validator structure") 507 | } 508 | } else if rs := v.getRules(); len(rs) != 0 { 509 | re, _ := regexp.Compile(`(\w+)\[(\d+)\]`) 510 | slicePieces := re.FindStringSubmatch(fieldName) 511 | arrayItem := -1 512 | if len(slicePieces) > 0 { 513 | fieldName = slicePieces[1] 514 | arrayItem, _ = strconv.Atoi(slicePieces[2]) 515 | } 516 | if r, ok := rs[fieldName]; ok { 517 | if deep := r.getDeepValidator(); deep != nil { 518 | if r.getName() != "" { 519 | fieldName = r.getName() 520 | } 521 | output[fieldName] = decryptPath(path, deep, errorField) 522 | } else if children := r.getChildrenValidator(); arrayItem != -1 && children != nil { 523 | if splits := strings.Split(path, "."); len(splits) == 1 && len(re.FindStringSubmatch(splits[0])) == 0 { 524 | output[fieldName] = map[int]interface{}{arrayItem: map[string]interface{}{splits[0]: decryptPath(path, children, errorField)}} 525 | } else { 526 | output[fieldName] = map[int]interface{}{arrayItem: decryptPath(path, children, errorField)} 527 | } 528 | } else { 529 | panic("error structure does not match with validator structure") 530 | } 531 | } else { 532 | panic("error structure does not match with validator structure") 533 | } 534 | } else { 535 | panic("error structure does not match with validator structure") 536 | } 537 | 538 | return output 539 | } 540 | 541 | func decryptErrors(err error, v Validator, unmarshalError bool) interface{} { 542 | output := map[string]interface{}{} 543 | if e, ok := err.(playgroundValidator.ValidationErrors); ok { 544 | for i := 0; i < len(e); i++ { 545 | errorField := e[i] 546 | splits := strings.SplitN(errorField.StructNamespace(), ".", 2) 547 | path := splits[1] 548 | out := decryptPath(path, v, errorField) 549 | if outMap, ok := out.(map[string]interface{}); ok && len(outMap) != 0 { 550 | for key, value := range outMap { 551 | output[key] = value 552 | } 553 | } else if outString, ok := out.(string); ok { 554 | name := getFieldName(path) 555 | if r := v.getRules()[name]; r != nil && r.getName() != "" { 556 | name = r.getName() 557 | } 558 | output[name] = outString 559 | } 560 | } 561 | } else if e, ok := err.(sliceValidationError); ok { 562 | for i := 0; i < len(e); i++ { 563 | if r := v.getRule(); r != nil { 564 | if deep := r.getDeepValidator(); deep != nil { 565 | if out := decryptErrors(e[i], deep, unmarshalError); out != nil { 566 | output[strconv.Itoa(i)] = out 567 | } 568 | } else { 569 | panic("error structure does not match with validator structure") 570 | } 571 | } else { 572 | panic("error structure does not match with validator structure") 573 | } 574 | } 575 | } else if e, ok := err.(*json.UnmarshalTypeError); ok { 576 | if unmarshalError { 577 | return e.Error() 578 | } else { 579 | return UnmarshalError 580 | } 581 | } else { 582 | panic("passed error is not supported, errors returned from methods like BindJson(in gin framework) is supported") 583 | } 584 | 585 | if len(output) == 0 { 586 | return nil 587 | } 588 | 589 | return output 590 | } 591 | 592 | func (o *validatorS) getRules() Rules { 593 | return o.rules 594 | } 595 | 596 | func (o *validatorS) getRule() ruleSet { 597 | return o.rule 598 | } 599 | 600 | func (o *validatorS) setMessages(messages *Messages) { 601 | o.messages = messages 602 | } 603 | --------------------------------------------------------------------------------