├── .editorconfig ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── SUPPORT.md └── workflows │ ├── generate-readme.yml │ ├── tag-semver.yml │ └── test.yml ├── .golangci.yaml ├── .travis.yml ├── LICENSE ├── README.md ├── go.mod ├── parser.go ├── parser_test.go ├── query.go └── scanner.go /.editorconfig: -------------------------------------------------------------------------------- 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. 2 | # 3 | # editorconfig: https://editorconfig.org/ 4 | # actual source: https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.editorconfig 5 | # 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | indent_size = 4 13 | indent_style = space 14 | insert_final_newline = true 15 | trim_trailing_whitespace = true 16 | max_line_length = 100 17 | 18 | [*.tf] 19 | indent_size = 2 20 | 21 | [*.go] 22 | indent_style = tab 23 | indent_size = 4 24 | 25 | [*.md] 26 | trim_trailing_whitespace = false 27 | 28 | [*.{md,py,sh,yml,yaml,cjs,js,ts,vue,css}] 29 | max_line_length = 105 30 | 31 | [*.{yml,yaml,toml}] 32 | indent_size = 2 33 | 34 | [*.json] 35 | indent_size = 2 36 | 37 | [*.html] 38 | max_line_length = 140 39 | indent_size = 2 40 | 41 | [*.{cjs,js,ts,vue,css}] 42 | indent_size = 2 43 | 44 | [Makefile] 45 | indent_style = tab 46 | 47 | [**.min.js] 48 | indent_style = ignore 49 | 50 | [*.bat] 51 | indent_style = tab 52 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. 2 | .github/* @lrstanley 3 | LICENSE @lrstanley 4 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Code of Conduct 3 | 4 | ## Our Pledge :purple_heart: 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | disclosure@liam.sh. All complaints will be reviewed and investigated 65 | promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the Contributor Covenant, 119 | version 2.1, available [here](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). 120 | 121 | For answers to common questions about this code of conduct, see the [FAQ](https://www.contributor-covenant.org/faq). 122 | Translations are available at [translations](https://www.contributor-covenant.org/translations). 123 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # :handshake: Contributing 3 | 4 | This document outlines some of the guidelines that we try and adhere to while 5 | working on this project. 6 | 7 | > :point_right: **Note**: before participating in the community, please read our 8 | > [Code of Conduct][coc]. 9 | > By interacting with this repository, organization, or community you agree to 10 | > abide by our Code of Conduct. 11 | > 12 | > Additionally, if you contribute **any source code** to this repository, you 13 | > agree to the terms of the [Developer Certificate of Origin][dco]. This helps 14 | > ensure that contributions aren't in violation of 3rd party license terms. 15 | 16 | ## :lady_beetle: Issue submission 17 | 18 | When [submitting an issue][issues] or bug report, 19 | please follow these guidelines: 20 | 21 | * Provide as much information as possible (logs, metrics, screenshots, 22 | runtime environment, etc). 23 | * Ensure that you are running on the latest stable version (tagged), or 24 | when using `master`, provide the specific commit being used. 25 | * Provide the minimum needed viable source to replicate the problem. 26 | 27 | ## :bulb: Feature requests 28 | 29 | When [submitting a feature request][issues], please 30 | follow these guidelines: 31 | 32 | * Does this feature benefit others? or just your usecase? If the latter, 33 | it will likely be declined, unless it has a more broad benefit to others. 34 | * Please include the pros and cons of the feature. 35 | * If possible, describe how the feature would work, and any diagrams/mock 36 | examples of what the feature would look like. 37 | 38 | ## :rocket: Pull requests 39 | 40 | To review what is currently being worked on, or looked into, feel free to head 41 | over to the [open pull requests][pull-requests] or [issues list][issues]. 42 | 43 | ## :raised_back_of_hand: Assistance with discussions 44 | 45 | * Take a look at the [open discussions][discussions], and if you feel like 46 | you'd like to help out other members of the community, it would be much 47 | appreciated! 48 | 49 | ## :pushpin: Guidelines 50 | 51 | ### :test_tube: Language agnostic 52 | 53 | Below are a few guidelines if you would like to contribute: 54 | 55 | * If the feature is large or the bugfix has potential breaking changes, 56 | please open an issue first to ensure the changes go down the best path. 57 | * If possible, break the changes into smaller PRs. Pull requests should be 58 | focused on a specific feature/fix. 59 | * Pull requests will only be accepted with sufficient documentation 60 | describing the new functionality/fixes. 61 | * Keep the code simple where possible. Code that is smaller/more compact 62 | does not mean better. Don't do magic behind the scenes. 63 | * Use the same formatting/styling/structure as existing code. 64 | * Follow idioms and community-best-practices of the related language, 65 | unless the previous above guidelines override what the community 66 | recommends. 67 | * Always test your changes, both the features/fixes being implemented, but 68 | also in the standard way that a user would use the project (not just 69 | your configuration that fixes your issue). 70 | * Only use 3rd party libraries when necessary. If only a small portion of 71 | the library is needed, simply rewrite it within the library to prevent 72 | useless imports. 73 | 74 | ### :hamster: Golang 75 | 76 | * See [golang/go/wiki/CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) 77 | * This project uses [golangci-lint](https://golangci-lint.run/) for 78 | Go-related files. This should be available for any editor that supports 79 | `gopls`, however you can also run it locally with `golangci-lint run` 80 | after installing it. 81 | 82 | 83 | 84 | 85 | 86 | 87 | ## :clipboard: References 88 | 89 | * [Open Source: How to Contribute](https://opensource.guide/how-to-contribute/) 90 | * [About pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) 91 | * [GitHub Docs](https://docs.github.com/) 92 | 93 | ## :speech_balloon: What to do next? 94 | 95 | * :old_key: Find a vulnerability? Check out our [Security and Disclosure][security] policy. 96 | * :link: Repository [License][license]. 97 | * [Support][support] 98 | * [Code of Conduct][coc]. 99 | 100 | 101 | [coc]: https://github.com/lrstanley/go-queryparser/blob/master/.github/CODE_OF_CONDUCT.md 102 | [dco]: https://developercertificate.org/ 103 | [discussions]: https://github.com/lrstanley/go-queryparser/discussions 104 | [issues]: https://github.com/lrstanley/go-queryparser/issues/new/choose 105 | [license]: https://github.com/lrstanley/go-queryparser/blob/master/LICENSE 106 | [pull-requests]: https://github.com/lrstanley/go-queryparser/pulls?q=is%3Aopen+is%3Apr 107 | [security]: https://github.com/lrstanley/go-queryparser/security/policy 108 | [support]: https://github.com/lrstanley/go-queryparser/blob/master/.github/SUPPORT.md 109 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. 2 | name: "🐞 Submit a bug report" 3 | description: Create a report to help us improve! 4 | title: "bug: [REPLACE ME]" 5 | labels: 6 | - bug 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | ### Thanks for submitting a bug report to **go-queryparser**! 📋 12 | 13 | - 💬 Make sure to check out the [**discussions**](../discussions) section. If your issue isn't a bug (or you're not sure), and you're looking for help to solve it, please [start a discussion here](../discussions/new?category=q-a) first. 14 | - 🔎 Please [**search**](../labels/bug) to see if someone else has submitted a similar bug report, before making a new report. 15 | 16 | ---------------------------------------- 17 | - type: textarea 18 | id: description 19 | attributes: 20 | label: "🌧 Describe the problem" 21 | description: A clear and concise description of what the problem is. 22 | placeholder: 'Example: "When I attempted to do X, I got X error"' 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: expected 27 | attributes: 28 | label: "⛅ Expected behavior" 29 | description: A clear and concise description of what you expected to happen. 30 | placeholder: 'Example: "I expected X to let me add Y component, and be successful"' 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: reproduce 35 | attributes: 36 | label: "🔄 Minimal reproduction" 37 | description: >- 38 | Steps to reproduce the behavior (including code examples and/or 39 | configuration files if necessary) 40 | placeholder: >- 41 | Example: "1. Click on '....' | 2. Run command with flags --foo --bar, 42 | etc | 3. See error" 43 | - type: input 44 | id: version 45 | attributes: 46 | label: "💠 Version: go-queryparser" 47 | description: What version of go-queryparser is being used? 48 | placeholder: 'Examples: "v1.2.3, master branch, commit 1a2b3c"' 49 | validations: 50 | required: true 51 | - type: dropdown 52 | id: os 53 | attributes: 54 | label: "🖥 Version: Operating system" 55 | description: >- 56 | What operating system did this issue occur on (if other, specify in 57 | "Additional context" section)? 58 | options: 59 | - linux/ubuntu 60 | - linux/debian 61 | - linux/centos 62 | - linux/alpine 63 | - linux/other 64 | - windows/10 65 | - windows/11 66 | - windows/other 67 | - macos 68 | - other 69 | validations: 70 | required: true 71 | - type: textarea 72 | id: context 73 | attributes: 74 | label: "⚙ Additional context" 75 | description: >- 76 | Add any other context about the problem here. This includes things 77 | like logs, screenshots, code examples, what was the state when the 78 | bug occurred? 79 | placeholder: > 80 | Examples: "logs, code snippets, screenshots, os/browser version info, 81 | etc" 82 | - type: checkboxes 83 | id: requirements 84 | attributes: 85 | label: "🤝 Requirements" 86 | description: "Please confirm the following:" 87 | options: 88 | - label: >- 89 | I believe the problem I'm facing is a bug, and is not intended 90 | behavior. [Post here if you're not sure](../discussions/new?category=q-a). 91 | required: true 92 | - label: >- 93 | I have confirmed that someone else has not 94 | [submitted a similar bug report](../labels/bug). 95 | required: true 96 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: "🙋‍♂️ Ask the community a question!" 5 | about: Have a question, that might not be a bug? Wondering how to solve a problem? Ask away! 6 | url: "https://github.com/lrstanley/go-queryparser/discussions/new?category=q-a" 7 | - name: "🎉 Show us what you've made!" 8 | about: Have you built something using go-queryparser, and want to show others? Post here! 9 | url: "https://github.com/lrstanley/go-queryparser/discussions/new?category=show-and-tell" 10 | - name: "✋ Additional support information" 11 | about: Looking for something else? Check here. 12 | url: "https://github.com/lrstanley/go-queryparser/blob/master/.github/SUPPORT.md" 13 | - name: "💬 Discord chat" 14 | about: On-topic and off-topic discussions. 15 | url: "https://liam.sh/chat" 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. 2 | name: "💡 Submit a feature request" 3 | description: Suggest an awesome feature for this project! 4 | title: "feature: [REPLACE ME]" 5 | labels: 6 | - enhancement 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | ### Thanks for submitting a feature request! 📋 12 | 13 | - 💬 Make sure to check out the [**discussions**](../discussions) section of this repository. Do you have an idea for an improvement, but want to brainstorm it with others first? [Start a discussion here](../discussions/new?category=ideas) first. 14 | - 🔎 Please [**search**](../labels/enhancement) to see if someone else has submitted a similar feature request, before making a new request. 15 | 16 | --------------------------------------------- 17 | - type: textarea 18 | id: describe 19 | attributes: 20 | label: "✨ Describe the feature you'd like" 21 | description: >- 22 | A clear and concise description of what you want to happen, or what 23 | feature you'd like added. 24 | placeholder: 'Example: "It would be cool if X had support for Y"' 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: related 29 | attributes: 30 | label: "🌧 Is your feature request related to a problem?" 31 | description: >- 32 | A clear and concise description of what the problem is. 33 | placeholder: >- 34 | Example: "I'd like to see X feature added, as I frequently have to do Y, 35 | and I think Z would solve that problem" 36 | - type: textarea 37 | id: alternatives 38 | attributes: 39 | label: "🔎 Describe alternatives you've considered" 40 | description: >- 41 | A clear and concise description of any alternative solutions or features 42 | you've considered. 43 | placeholder: >- 44 | Example: "I've considered X and Y, however the potential problems with 45 | those solutions would be [...]" 46 | validations: 47 | required: true 48 | - type: dropdown 49 | id: breaking 50 | attributes: 51 | label: "⚠ If implemented, do you think this feature will be a breaking change to users?" 52 | description: >- 53 | To the best of your ability, do you think implementing this change 54 | would impact users in a way during an upgrade process? 55 | options: 56 | - "Yes" 57 | - "No" 58 | - "Not sure" 59 | validations: 60 | required: true 61 | - type: textarea 62 | id: context 63 | attributes: 64 | label: "⚙ Additional context" 65 | description: >- 66 | Add any other context or screenshots about the feature request here 67 | (attach if necessary). 68 | placeholder: "Examples: logs, screenshots, etc" 69 | - type: checkboxes 70 | id: requirements 71 | attributes: 72 | label: "🤝 Requirements" 73 | description: "Please confirm the following:" 74 | options: 75 | - label: >- 76 | I have confirmed that someone else has not 77 | [submitted a similar feature request](../labels/enhancement). 78 | required: true 79 | - label: >- 80 | If implemented, I believe this feature will help others, in 81 | addition to solving my problems. 82 | required: true 83 | - label: I have looked into alternative solutions to the best of my ability. 84 | required: true 85 | - label: >- 86 | (optional) I would be willing to contribute to testing this 87 | feature if implemented, or making a PR to implement this 88 | functionality. 89 | required: false 90 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## 🚀 Changes proposed by this PR 9 | 10 | 14 | 15 | 16 | ### 🔗 Related bug reports/feature requests 17 | 18 | 22 | - fixes #(issue) 23 | - closes #(issue) 24 | - relates to #(issue) 25 | - implements #(feature) 26 | 27 | ### 🧰 Type of change 28 | 29 | 30 | - [ ] Bug fix (non-breaking change which fixes an issue). 31 | - [ ] New feature (non-breaking change which adds functionality). 32 | - [ ] Breaking change (fix or feature that causes existing functionality to not work as expected). 33 | - [ ] This change requires (or is) a documentation update. 34 | 35 | ### 📝 Notes to reviewer 36 | 37 | 41 | 42 | ### 🤝 Requirements 43 | 44 | - [ ] ✍ I have read and agree to this projects [Code of Conduct](../../blob/master/.github/CODE_OF_CONDUCT.md). 45 | - [ ] ✍ I have read and agree to this projects [Contribution Guidelines](../../blob/master/.github/CONTRIBUTING.md). 46 | - [ ] ✍ I have read and agree to the [Developer Certificate of Origin](https://developercertificate.org/). 47 | - [ ] 🔎 I have performed a self-review of my own changes. 48 | - [ ] 🎨 My changes follow the style guidelines of this project. 49 | 50 | - [ ] 💬 My changes as properly commented, primarily for hard-to-understand areas. 51 | - [ ] 📝 I have made corresponding changes to the documentation. 52 | - [ ] 🧪 I have included tests (if necessary) for this change. 53 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | # :old_key: Security Policy 3 | 4 | ## :heavy_check_mark: Supported Versions 5 | 6 | The following restrictions apply for versions that are still supported in terms of security and bug fixes: 7 | 8 | * :grey_question: Must be using the latest major/minor version. 9 | * :grey_question: Must be using a supported platform for the repository (e.g. OS, browser, etc), and that platform must 10 | be within its supported versions (for example: don't use a legacy or unsupported version of Ubuntu or 11 | Google Chrome). 12 | * :grey_question: Repository must not be archived (unless the vulnerability is critical, and the repository moderately 13 | popular). 14 | * :heavy_check_mark: 15 | 16 | If one of the above doesn't apply to you, feel free to submit an issue and we can discuss the 17 | issue/vulnerability further. 18 | 19 | 20 | ## :lady_beetle: Reporting a Vulnerability 21 | 22 | Best method of contact: [GPG :key:](https://github.com/lrstanley.gpg) 23 | 24 | * :speech_balloon: [Discord][chat]: message `lrstanley` (`/home/liam#0000`). 25 | * :email: Email: `security@liam.sh` 26 | 27 | Backup contacts (if I am unresponsive after **48h**): [GPG :key:](https://github.com/FM1337.gpg) 28 | * :speech_balloon: [Discord][chat]: message `Allen#7440`. 29 | * :email: Email: `security@allenlydiard.ca` 30 | 31 | If you feel that this disclosure doesn't include a critical vulnerability and there is no sensitive 32 | information in the disclosure, you don't have to use the GPG key. For all other situations, please 33 | use it. 34 | 35 | ### :stopwatch: Vulnerability disclosure expectations 36 | 37 | * :no_bell: We expect you to not share this information with others, unless: 38 | * The maximum timeline for initial response has been exceeded (shown below). 39 | * The maximum resolution time has been exceeded (shown below). 40 | * :mag_right: We expect you to responsibly investigate this vulnerability -- please do not utilize the 41 | vulnerability beyond the initial findings. 42 | * :stopwatch: Initial response within 48h, however, if the primary contact shown above is unavailable, please 43 | use the backup contacts provided. The maximum timeline for an initial response should be within 44 | 7 days. 45 | * :stopwatch: Depending on the severity of the disclosure, resolution time may be anywhere from 24h to 2 46 | weeks after initial response, though in most cases it will likely be closer to the former. 47 | * If the vulnerability is very low/low in terms of risk, the above timelines **will not apply**. 48 | * :toolbox: Before the release of resolved versions, a [GitHub Security Advisory][advisory-docs]. 49 | will be released on the respective repository. [Browser all advisories here][advisory]. 50 | 51 | 52 | [chat]: https://liam.sh/chat 53 | [advisory]: https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago 54 | [advisory-docs]: https://docs.github.com/en/code-security/repository-security-advisories/creating-a-repository-security-advisory 55 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # :raising_hand_man: Support 2 | 3 | This document explains where and how to get help with most of my projects. 4 | Please ensure you read through it thoroughly. 5 | 6 | > :point_right: **Note**: before participating in the community, please read our 7 | > [Code of Conduct][coc]. 8 | > By interacting with this repository, organization, or community you agree to 9 | > abide by its terms. 10 | 11 | ## :grey_question: Asking quality questions 12 | 13 | Questions can go to [Github Discussions][discussions] or feel free to join 14 | the Discord [here][chat]. 15 | 16 | Help me help you! Spend time framing questions and add links and resources. 17 | Spending the extra time up front can help save everyone time in the long run. 18 | Here are some tips: 19 | 20 | * Don't fall for the [XY problem][xy]. 21 | * Search to find out if a similar question has been asked or if a similar 22 | issue/bug has been reported. 23 | * Try to define what you need help with: 24 | * Is there something in particular you want to do? 25 | * What problem are you encountering and what steps have you taken to try 26 | and fix it? 27 | * Is there a concept you don't understand? 28 | * Provide sample code, such as a [CodeSandbox][cs] or a simple snippet, if 29 | possible. 30 | * Screenshots can help, but if there's important text such as code or error 31 | messages in them, please also provide those. 32 | * The more time you put into asking your question, the better I and others 33 | can help you. 34 | 35 | ## :old_key: Security 36 | 37 | For any security or vulnerability related disclosure, please follow the 38 | guidelines outlined in our [security policy][security]. 39 | 40 | ## :handshake: Contributions 41 | 42 | See [`CONTRIBUTING.md`][contributing] on how to contribute. 43 | 44 | 45 | [coc]: https://github.com/lrstanley/go-queryparser/blob/master/.github/CODE_OF_CONDUCT.md 46 | [contributing]: https://github.com/lrstanley/go-queryparser/blob/master/.github/CONTRIBUTING.md 47 | [discussions]: https://github.com/lrstanley/go-queryparser/discussions/categories/q-a 48 | [issues]: https://github.com/lrstanley/go-queryparser/issues/new/choose 49 | [license]: https://github.com/lrstanley/go-queryparser/blob/master/LICENSE 50 | [pull-requests]: https://github.com/lrstanley/go-queryparser/issues/new/choose 51 | [security]: https://github.com/lrstanley/go-queryparser/security/policy 52 | [support]: https://github.com/lrstanley/go-queryparser/blob/master/.github/SUPPORT.md 53 | 54 | [xy]: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378 55 | [chat]: https://liam.sh/chat 56 | [cs]: https://codesandbox.io 57 | -------------------------------------------------------------------------------- /.github/workflows/generate-readme.yml: -------------------------------------------------------------------------------- 1 | name: generate-readme 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | tags: [v*] 7 | schedule: 8 | - cron: "0 13 * * *" 9 | 10 | jobs: 11 | generate: 12 | uses: lrstanley/.github/.github/workflows/generate-readme.yml@master 13 | secrets: 14 | token: ${{ secrets.USER_PAT }} 15 | -------------------------------------------------------------------------------- /.github/workflows/tag-semver.yml: -------------------------------------------------------------------------------- 1 | name: tag-semver 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | method: 7 | description: "Tagging method to use" 8 | required: true 9 | type: choice 10 | options: [major, minor, patch, alpha, rc, custom] 11 | custom: 12 | description: "Custom tag, if the default doesn't suffice. Must also use method 'custom'." 13 | required: false 14 | type: string 15 | ref: 16 | description: "Git ref to apply tag to (will use default branch if unspecified)." 17 | required: false 18 | type: string 19 | annotation: 20 | description: "Optional annotation to add to the commit." 21 | required: false 22 | type: string 23 | 24 | jobs: 25 | tag-semver: 26 | uses: lrstanley/.github/.github/workflows/tag-semver.yml@master 27 | with: 28 | method: ${{ github.event.inputs.method }} 29 | ref: ${{ github.event.inputs.ref }} 30 | custom: ${{ github.event.inputs.custom }} 31 | annotation: ${{ github.event.inputs.annotation }} 32 | secrets: 33 | token: ${{ secrets.USER_PAT }} 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | paths-ignore: [".gitignore", "**/*.md", ".github/ISSUE_TEMPLATE/**"] 7 | types: [opened, edited, reopened, synchronize, unlocked] 8 | push: 9 | branches: [master] 10 | paths-ignore: [".gitignore", "**/*.md", ".github/ISSUE_TEMPLATE/**"] 11 | 12 | jobs: 13 | go-test: 14 | uses: lrstanley/.github/.github/workflows/lang-go-test-matrix.yml@master 15 | with: { num-minor: 2, num-patch: 2 } 16 | go-lint: 17 | uses: lrstanley/.github/.github/workflows/lang-go-lint.yml@master 18 | secrets: 19 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 20 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. 2 | # 3 | # golangci-lint: https://golangci-lint.run/ 4 | # false-positives: https://golangci-lint.run/usage/false-positives/ 5 | # actual source: https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.golangci.yml 6 | # modified variant of: https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 7 | 8 | version: "2" 9 | 10 | formatters: 11 | enable: [gofumpt] 12 | 13 | issues: 14 | max-issues-per-linter: 0 15 | max-same-issues: 50 16 | 17 | severity: 18 | default: error 19 | rules: 20 | - linters: 21 | - errcheck 22 | - gocritic 23 | severity: warning 24 | 25 | linters: 26 | default: none 27 | enable: 28 | - asasalint # checks for pass []any as any in variadic func(...any) 29 | - asciicheck # checks that your code does not contain non-ASCII identifiers 30 | - bidichk # checks for dangerous unicode character sequences 31 | - bodyclose # checks whether HTTP response body is closed successfully 32 | - canonicalheader # checks whether net/http.Header uses canonical header 33 | - copyloopvar # detects places where loop variables are copied (Go 1.22+) 34 | - depguard # checks if package imports are in a list of acceptable packages 35 | - dupl # tool for code clone detection 36 | - durationcheck # checks for two durations multiplied together 37 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases 38 | - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error 39 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 40 | - exhaustive # checks exhaustiveness of enum switch statements 41 | - exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions 42 | - fatcontext # detects nested contexts in loops 43 | - forbidigo # forbids identifiers 44 | - funlen # tool for detection of long functions 45 | - gocheckcompilerdirectives # validates go compiler directive comments (//go:) 46 | - gochecknoinits # checks that no init functions are present in Go code 47 | - gochecksumtype # checks exhaustiveness on Go "sum types" 48 | - gocognit # computes and checks the cognitive complexity of functions 49 | - goconst # finds repeated strings that could be replaced by a constant 50 | - gocritic # provides diagnostics that check for bugs, performance and style issues 51 | # TODO: breaks multi-line comments when used with --fix: https://github.com/tetafro/godot/issues/39 52 | # - godot # checks if comments end in a period 53 | - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod 54 | - goprintffuncname # checks that printf-like functions are named with f at the end 55 | - gosec # inspects source code for security problems 56 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 57 | - iface # checks the incorrect use of interfaces, helping developers avoid interface pollution 58 | - ineffassign # detects when assignments to existing variables are not used 59 | - intrange # finds places where for loops could make use of an integer range 60 | - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) 61 | - makezero # finds slice declarations with non-zero initial length 62 | - mirror # reports wrong mirror patterns of bytes/strings usage 63 | - misspell # [useless] finds commonly misspelled English words in comments 64 | - musttag # enforces field tags in (un)marshaled structs 65 | - nakedret # finds naked returns in functions greater than a specified function length 66 | - nestif # reports deeply nested if statements 67 | - nilerr # finds the code that returns nil even if it checks that the error is not nil 68 | - nilnesserr # reports that it checks for err != nil, but it returns a different nil value error (powered by nilness and nilerr) 69 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value 70 | - noctx # finds sending http request without context.Context 71 | - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL 72 | - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative 73 | - predeclared # finds code that shadows one of Go's predeclared identifiers 74 | - promlinter # checks Prometheus metrics naming via promlint 75 | - reassign # checks that package variables are not reassigned 76 | - recvcheck # checks for receiver type consistency 77 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint 78 | - rowserrcheck # checks whether Err of rows is checked successfully 79 | - sloglint # ensure consistent code style when using log/slog 80 | - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed 81 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks 82 | - testableexamples # checks if examples are testable (have an expected output) 83 | - testifylint # checks usage of github.com/stretchr/testify 84 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes 85 | - unconvert # removes unnecessary type conversions 86 | - unparam # reports unused function parameters 87 | - unused # checks for unused constants, variables, functions and types 88 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library 89 | - usetesting # reports uses of functions with replacement inside the testing package 90 | - wastedassign # finds wasted assignment statements 91 | - whitespace # detects leading and trailing whitespace 92 | 93 | settings: 94 | gocognit: 95 | min-complexity: 40 96 | errcheck: 97 | check-type-assertions: true 98 | funlen: 99 | lines: 150 100 | statements: 75 101 | ignore-comments: true 102 | gocritic: 103 | disabled-checks: 104 | - whyNoLint 105 | - hugeParam 106 | - ifElseChain 107 | enabled-tags: 108 | - diagnostic 109 | - opinionated 110 | - performance 111 | - style 112 | settings: 113 | captLocal: 114 | paramsOnly: false 115 | underef: 116 | skipRecvDeref: false 117 | rangeValCopy: 118 | sizeThreshold: 512 119 | depguard: 120 | rules: 121 | "deprecated": 122 | files: ["$all"] 123 | deny: 124 | - pkg: github.com/golang/protobuf 125 | desc: Use google.golang.org/protobuf instead, see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules 126 | - pkg: github.com/satori/go.uuid 127 | desc: Use github.com/google/uuid instead, satori's package is not maintained 128 | - pkg: github.com/gofrs/uuid$ 129 | desc: Use github.com/gofrs/uuid/v5 or later, it was not a go module before v5 130 | "non-test files": 131 | files: ["!$test"] 132 | deny: 133 | - pkg: math/rand$ 134 | desc: Use math/rand/v2 instead, see https://go.dev/blog/randv2 135 | "non-main files": 136 | files: ["!**/main.go"] 137 | deny: 138 | - pkg: log$ 139 | desc: Use log/slog instead, see https://go.dev/blog/slog 140 | "incorrect import": 141 | files: ["$test"] 142 | deny: 143 | - pkg: github.com/tj/assert$ 144 | desc: Use github.com/stretchr/testify/assert instead, see 145 | gochecksumtype: 146 | default-signifies-exhaustive: false 147 | exhaustive: 148 | check: 149 | - switch 150 | - map 151 | govet: 152 | disable: 153 | - fieldalignment 154 | enable-all: true 155 | settings: 156 | shadow: 157 | strict: true 158 | perfsprint: 159 | strconcat: false 160 | nakedret: 161 | max-func-lines: 0 162 | nestif: 163 | min-complexity: 10 164 | rowserrcheck: 165 | packages: 166 | - github.com/jmoiron/sqlx 167 | sloglint: 168 | no-global: all 169 | context: scope 170 | staticcheck: 171 | checks: 172 | - all 173 | # Incorrect or missing package comment: https://staticcheck.dev/docs/checks/#ST1000 174 | - -ST1000 175 | # Use consistent method receiver names: https://staticcheck.dev/docs/checks/#ST1016 176 | - -ST1016 177 | # Omit embedded fields from selector expression: https://staticcheck.dev/docs/checks/#QF1008 178 | - -QF1008 179 | usetesting: 180 | os-temp-dir: true 181 | exclusions: 182 | warn-unused: true 183 | generated: lax 184 | presets: 185 | - common-false-positives 186 | - std-error-handling 187 | paths: 188 | - ".*\\.gen\\.go$" 189 | - ".*\\.gen_test\\.go$" 190 | rules: 191 | - source: "TODO" 192 | linters: [godot] 193 | - text: "should have a package comment" 194 | linters: [revive] 195 | - text: 'exported \S+ \S+ should have comment( \(or a comment on this block\))? or be unexported' 196 | linters: [revive] 197 | - text: 'package comment should be of the form ".+"' 198 | source: "// ?(nolint|TODO)" 199 | linters: [revive] 200 | - text: 'comment on exported \S+ \S+ should be of the form ".+"' 201 | source: "// ?(nolint|TODO)" 202 | linters: [revive, staticcheck] 203 | - text: 'unexported-return: exported func \S+ returns unexported type \S+ .*' 204 | linters: [revive] 205 | - text: "var-declaration: should drop .* from declaration of .*; it is the zero value" 206 | linters: [revive] 207 | - text: ".*use ALL_CAPS in Go names.*" 208 | linters: [revive, staticcheck] 209 | - text: '.* always receives \S+' 210 | linters: [unparam] 211 | - path: _test\.go 212 | linters: 213 | - bodyclose 214 | - dupl 215 | - funlen 216 | - gocognit 217 | - goconst 218 | - gosec 219 | - noctx 220 | - wrapcheck 221 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - tip 4 | script: 5 | - GORACE="exitcode=1 halt_on_error=1" go test -v -coverprofile=coverage.txt -race -timeout 3m -count 3 -cpu 1,4 6 | - go vet -v . 7 | after_success: 8 | - bash <(curl -s https://codecov.io/bash) 9 | branches: 10 | only: 11 | - master 12 | notifications: 13 | irc: 14 | channels: 15 | - irc.byteirc.org#/dev/null 16 | template: 17 | - "%{repository} #%{build_number} %{branch}/%{commit}: %{author} -- %{message} 18 | %{build_url}" 19 | on_success: change 20 | on_failure: change 21 | skip_join: false 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Liam Stanley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | ![logo](https://liam.sh/-/gh/svg/lrstanley/go-queryparser?icon=mdi%3Afilter-cog-outline&icon.height=110&layout=left&icon.color=rgba%280%2C+184%2C+126%2C+1%29) 7 | 8 | 9 | 10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |

34 |

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |

49 | 50 | 51 | 52 | 53 | ## :link: Table of Contents 54 | 55 | - [What?](#what) 56 | - [Use:](#use) 57 | - [Example:](#example) 58 | - [Support & Assistance](#raising_hand_man-support--assistance) 59 | - [Contributing](#handshake-contributing) 60 | - [License](#balance_scale-license) 61 | 62 | 63 | ## What? 64 | 65 | go-queryparser parses a common "q" http GET variable to strip out filters, 66 | which can be used for advanced searching, like: 67 | 68 | ``` 69 | Hello World tags:example,world foo:"something quoted" author:lrstanley 70 | ``` 71 | 72 | ## Use: 73 | 74 | 75 | 76 | ```console 77 | go get -u github.com/lrstanley/go-queryparser/v3@latest 78 | ``` 79 | 80 | 81 | ## Example: 82 | 83 | ```go 84 | package main 85 | 86 | import ( 87 | "fmt" 88 | "net/http" 89 | 90 | "github.com/lrstanley/go-queryparser/v3" 91 | ) 92 | 93 | func main() { 94 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 95 | q := queryparser.Parse(r.FormValue("q")) 96 | if q.Has("author") { 97 | fmt.Fprintf(w, "filtering by author %q!", q.GetOne("author")) 98 | return 99 | } 100 | 101 | fmt.Fprint(w, "no filtering requested!") 102 | }) 103 | 104 | http.ListenAndServe(":8080", nil) 105 | } 106 | ``` 107 | 108 | ```console 109 | $ curl -s localhost:8080 110 | no filtering requested! 111 | $ curl -s 'localhost:8080?q=author:"liam"' 112 | filtering by author "liam"! 113 | ``` 114 | 115 | The main benefit is for user input boxes where you want additional filtering, 116 | like the Github issues search box, or similar. 117 | 118 | 119 | 120 | ## :raising_hand_man: Support & Assistance 121 | 122 | * :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for 123 | guidelines on ensuring everyone has the best experience interacting with 124 | the community. 125 | * :raising_hand_man: Take a look at the [support](.github/SUPPORT.md) document on 126 | guidelines for tips on how to ask the right questions. 127 | * :lady_beetle: For all features/bugs/issues/questions/etc, [head over here](https://github.com/lrstanley/go-queryparser/issues/new/choose). 128 | 129 | 130 | 131 | 132 | ## :handshake: Contributing 133 | 134 | * :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for guidelines 135 | on ensuring everyone has the best experience interacting with the 136 | community. 137 | * :clipboard: Please review the [contributing](.github/CONTRIBUTING.md) doc for submitting 138 | issues/a guide on submitting pull requests and helping out. 139 | * :old_key: For anything security related, please review this repositories [security policy](https://github.com/lrstanley/go-queryparser/security/policy). 140 | 141 | 142 | 143 | 144 | ## :balance_scale: License 145 | 146 | ``` 147 | MIT License 148 | 149 | Copyright (c) 2017 Liam Stanley 150 | 151 | Permission is hereby granted, free of charge, to any person obtaining a copy 152 | of this software and associated documentation files (the "Software"), to deal 153 | in the Software without restriction, including without limitation the rights 154 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 155 | copies of the Software, and to permit persons to whom the Software is 156 | furnished to do so, subject to the following conditions: 157 | 158 | The above copyright notice and this permission notice shall be included in all 159 | copies or substantial portions of the Software. 160 | 161 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 162 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 163 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 164 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 165 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 166 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 167 | SOFTWARE. 168 | ``` 169 | 170 | _Also located [here](LICENSE)_ 171 | 172 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lrstanley/go-queryparser/v3 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Liam Stanley . All rights reserved. Use 2 | // of this source code is governed by the MIT license that can be found in 3 | // the LICENSE file. 4 | 5 | package queryparser 6 | 7 | import ( 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | // Options allow the adjustment of allowed filters and characters. 13 | type Options struct { 14 | // CutFn allows excluding specific characters from being allowed within 15 | // the filter fields. When the function returns true on a rune, it will 16 | // be excluded from the filter field. 17 | CutFn func(rune) bool 18 | // Allowed is a slice of allowed filter names. If no allowed filter names 19 | // are provided, all are considered allowed. 20 | Allowed []string 21 | } 22 | 23 | // Parser represents a parser. 24 | type Parser struct { 25 | s *scanner 26 | opt *Options 27 | 28 | buf []tokenRef 29 | } 30 | 31 | // New returns a new instance of Parser. Make sure Parser.Parser() is called 32 | // or this will leak goroutines. 33 | func New(query string, opt Options) *Parser { 34 | return &Parser{s: newScanner(query), opt: &opt} 35 | } 36 | 37 | // Parse is a higher level helper method to return a query from a query string. 38 | func Parse(query string) *Query { 39 | return New(query, Options{CutFn: DefaultCut}).Parse() 40 | } 41 | 42 | // scan returns the next token from the underlying scanner. 43 | // If a token has been unscanned then read that instead. 44 | func (p *Parser) scan() (tr tokenRef) { 45 | // If we have a token on the buffer, then return it. 46 | if len(p.buf) > 0 { 47 | // Pop first item off the buffer. 48 | tr = p.buf[0] 49 | copy(p.buf, p.buf[1:]) 50 | p.buf = p.buf[:len(p.buf)-1] 51 | return tr 52 | } 53 | 54 | // Otherwise read the next token from the scanner. 55 | tr = p.s.nextToken() 56 | return tr 57 | } 58 | 59 | // unscan pushes provided token/literal back onto the buffer. 60 | func (p *Parser) unscan(tr tokenRef) { 61 | p.buf = append(p.buf, tr) 62 | } 63 | 64 | // accept scans if the provided token matches, otherwise unscans. 65 | func (p *Parser) accept(tok token) bool { 66 | tr := p.scan() 67 | if tr.tok == tok { 68 | return true 69 | } 70 | 71 | p.unscan(tr) 72 | return false 73 | } 74 | 75 | // Parse parses the input query and returns a new instance of Query if there 76 | // were no errors. 77 | func (p *Parser) Parse() *Query { 78 | defer p.s.drain() 79 | 80 | qp := &Query{Filters: make(map[string][]string)} 81 | 82 | for { 83 | tr := p.scan() 84 | 85 | switch tr.tok { 86 | case tokenIDENT: 87 | p.scanField(tr, qp) 88 | case tokenEOF: 89 | if p.opt.CutFn != nil { 90 | qp.Raw = cutsetFunc(qp.Raw, p.opt.CutFn) 91 | qp.Raw = stripDuplicateWS(qp.Raw) 92 | } 93 | return qp 94 | default: 95 | qp.Raw += tr.lit 96 | } 97 | } 98 | } 99 | 100 | func (p *Parser) scanField(ident tokenRef, qp *Query) { 101 | if !isIdent(ident.lit) { 102 | qp.Raw += ident.lit 103 | return 104 | } 105 | 106 | // Return early if it's not allowed. 107 | if p.opt.Allowed != nil && len(p.opt.Allowed) > 0 { 108 | var in bool 109 | for i := 0; i < len(p.opt.Allowed); i++ { 110 | if strings.EqualFold(p.opt.Allowed[i], ident.lit) { 111 | in = true 112 | break 113 | } 114 | } 115 | if !in { 116 | qp.Raw += ident.lit 117 | return 118 | } 119 | } 120 | 121 | delim := p.scan() 122 | if delim.tok != tokenDELIM { 123 | qp.Raw += ident.lit 124 | p.unscan(delim) 125 | return 126 | } 127 | 128 | // Chomp all trailing fields. 129 | var fields []tokenRef 130 | var count int 131 | for { 132 | field := p.scan() 133 | count++ 134 | 135 | if field.tok == tokenFIELD || field.tok == tokenIDENT { 136 | fields = append(fields, field) 137 | continue 138 | } 139 | 140 | if count == 1 { 141 | qp.Raw += ident.lit 142 | p.unscan(delim) 143 | p.unscan(field) 144 | return 145 | } 146 | p.unscan(field) 147 | break 148 | } 149 | 150 | // Chomp trailing whitespaces if there are any. 151 | _ = p.accept(tokenWS) 152 | 153 | var fieldText string 154 | for i := 0; i < len(fields); i++ { 155 | fieldText += fields[i].lit 156 | } 157 | 158 | if p.opt.CutFn != nil { 159 | qp.Add(ident.lit, cutsetFunc(fieldText, p.opt.CutFn)) 160 | return 161 | } 162 | 163 | qp.Add(ident.lit, fieldText) 164 | } 165 | 166 | func cutsetFunc(input string, cutFn func(rune) bool) (out string) { 167 | for _, c := range input { 168 | if !cutFn(c) { 169 | out += string(c) 170 | } 171 | } 172 | return out 173 | } 174 | 175 | // DefaultCut is the default cut function, which allowed stripping out potentially 176 | // unwanted characters from filter fields and raw text. Only allows 177 | // " _,-.:A-Za-z0-9" (or unicode equivalents). 178 | func DefaultCut(r rune) (strip bool) { 179 | return !unicode.IsLetter(r) && !unicode.IsNumber(r) && r != ' ' && r != '\t' && 180 | r != '_' && r != ',' && r != '-' && r != '.' && r != ':' 181 | } 182 | 183 | func stripDuplicateWS(val string) string { 184 | for strings.Contains(val, " ") { 185 | val = strings.ReplaceAll(val, " ", " ") 186 | } 187 | 188 | return val 189 | } 190 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Liam Stanley . All rights reserved. Use 2 | // of this source code is governed by the MIT license that can be found in 3 | // the LICENSE file. 4 | 5 | package queryparser 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | type caseArgs struct { 13 | name string 14 | input string 15 | tokens []tokenRef 16 | allowed []string 17 | query *Query 18 | } 19 | 20 | var cases = []caseArgs{ 21 | { 22 | name: "quoted fields", 23 | input: `foo:"bar" bar:"baz baz1"`, 24 | tokens: []tokenRef{ 25 | {tok: tokenIDENT, lit: "foo"}, 26 | {tok: tokenDELIM, lit: ":"}, 27 | {tok: tokenFIELD, lit: `"bar"`}, 28 | {tok: tokenWS, lit: " "}, 29 | {tok: tokenIDENT, lit: "bar"}, 30 | {tok: tokenDELIM, lit: ":"}, 31 | {tok: tokenFIELD, lit: `"baz baz1"`}, 32 | }, 33 | allowed: []string{"foo", "bar"}, 34 | query: &Query{ 35 | Raw: "", 36 | Filters: map[string][]string{ 37 | "foo": {"bar"}, 38 | "bar": {"baz baz1"}, 39 | }, 40 | }, 41 | }, 42 | { 43 | name: "unquoted fields", 44 | input: `foo:1,2 bar:3`, 45 | tokens: []tokenRef{ 46 | {tok: tokenIDENT, lit: "foo"}, 47 | {tok: tokenDELIM, lit: ":"}, 48 | {tok: tokenIDENT, lit: "1,2"}, 49 | {tok: tokenWS, lit: " "}, 50 | {tok: tokenIDENT, lit: "bar"}, 51 | {tok: tokenDELIM, lit: ":"}, 52 | {tok: tokenIDENT, lit: "3"}, 53 | }, 54 | query: &Query{ 55 | Raw: "", 56 | Filters: map[string][]string{ 57 | "foo": {"1", "2"}, 58 | "bar": {"3"}, 59 | }, 60 | }, 61 | }, 62 | { 63 | name: "trailing", 64 | input: `foo:"bar" test`, 65 | tokens: []tokenRef{ 66 | {tok: tokenIDENT, lit: "foo"}, 67 | {tok: tokenDELIM, lit: ":"}, 68 | {tok: tokenFIELD, lit: `"bar"`}, 69 | {tok: tokenWS, lit: " "}, 70 | {tok: tokenIDENT, lit: "test"}, 71 | }, 72 | query: &Query{ 73 | Raw: "test", 74 | Filters: map[string][]string{ 75 | "foo": {"bar"}, 76 | }, 77 | }, 78 | }, 79 | { 80 | name: "trailing with single quote", 81 | input: `foo:'bar' test`, 82 | tokens: []tokenRef{ 83 | {tok: tokenIDENT, lit: "foo"}, 84 | {tok: tokenDELIM, lit: ":"}, 85 | {tok: tokenFIELD, lit: `'bar'`}, 86 | {tok: tokenWS, lit: " "}, 87 | {tok: tokenIDENT, lit: "test"}, 88 | }, 89 | query: &Query{ 90 | Raw: "test", 91 | Filters: map[string][]string{ 92 | "foo": {"bar"}, 93 | }, 94 | }, 95 | }, 96 | { 97 | name: "trailing with single and inner double quote", 98 | input: `foo:'bar' test`, 99 | tokens: []tokenRef{ 100 | {tok: tokenIDENT, lit: "foo"}, 101 | {tok: tokenDELIM, lit: ":"}, 102 | {tok: tokenFIELD, lit: `'ba"r'`}, 103 | {tok: tokenWS, lit: " "}, 104 | {tok: tokenIDENT, lit: "test"}, 105 | }, 106 | query: &Query{ 107 | Raw: "test", 108 | Filters: map[string][]string{ 109 | "foo": {"bar"}, 110 | }, 111 | }, 112 | }, 113 | { 114 | name: "trailing with random double quotes", 115 | input: `foo:"bar" test " :" a:"`, 116 | tokens: []tokenRef{ 117 | {tok: tokenIDENT, lit: "foo"}, 118 | {tok: tokenDELIM, lit: ":"}, 119 | {tok: tokenFIELD, lit: `"bar"`}, 120 | {tok: tokenWS, lit: " "}, 121 | {tok: tokenIDENT, lit: "test"}, 122 | {tok: tokenWS, lit: " "}, 123 | {tok: tokenFIELD, lit: `" :"`}, 124 | {tok: tokenWS, lit: " "}, 125 | {tok: tokenIDENT, lit: "a"}, 126 | {tok: tokenDELIM, lit: ":"}, 127 | {tok: tokenFIELD, lit: `"`}, 128 | }, 129 | query: &Query{ 130 | Raw: "test : ", 131 | Filters: map[string][]string{ 132 | "foo": {"bar"}, 133 | "a": {}, 134 | }, 135 | }, 136 | }, 137 | { 138 | name: "strip DefaultCut", 139 | input: `foo:"bar" te$st#!`, 140 | tokens: []tokenRef{ 141 | {tok: tokenIDENT, lit: "foo"}, 142 | {tok: tokenDELIM, lit: ":"}, 143 | {tok: tokenFIELD, lit: `"bar"`}, 144 | {tok: tokenWS, lit: " "}, 145 | {tok: tokenIDENT, lit: "test"}, 146 | }, 147 | query: &Query{ 148 | Raw: "test", 149 | Filters: map[string][]string{ 150 | "foo": {"bar"}, 151 | }, 152 | }, 153 | }, 154 | { 155 | name: "text only", 156 | input: `test test1`, 157 | tokens: []tokenRef{ 158 | {tok: tokenIDENT, lit: "test"}, 159 | {tok: tokenWS, lit: " "}, 160 | {tok: tokenIDENT, lit: "test1"}, 161 | }, 162 | query: &Query{ 163 | Raw: "test test1", 164 | Filters: map[string][]string{}, 165 | }, 166 | }, 167 | { 168 | name: "empty", 169 | input: ``, 170 | tokens: []tokenRef{}, 171 | query: &Query{Filters: map[string][]string{}}, 172 | }, 173 | } 174 | 175 | func TestScanner(t *testing.T) { 176 | for _, tt := range cases { 177 | t.Run("scanner_"+tt.name, func(t *testing.T) { 178 | s := newScanner(tt.input) 179 | 180 | for _, valid := range tt.tokens { 181 | tr := s.nextToken() 182 | 183 | // Make sure both have had their fair share of cutsets. 184 | tr.lit = cutsetFunc(tr.lit, DefaultCut) 185 | valid.lit = cutsetFunc(valid.lit, DefaultCut) 186 | 187 | if tr.lit != valid.lit || tr.tok != valid.tok { 188 | t.Fatalf("expected %#v but got %#v", valid, tr) 189 | } 190 | } 191 | 192 | if tr := s.nextToken(); tr.tok != tokenEOF { 193 | t.Fatalf("expected EOF, got %#v", tr) 194 | } 195 | 196 | s.drain() 197 | }) 198 | } 199 | } 200 | 201 | func TestParser(t *testing.T) { 202 | for _, tt := range cases { 203 | t.Run("parser_"+tt.name, func(t *testing.T) { 204 | p := New(tt.input, Options{Allowed: tt.allowed, CutFn: DefaultCut}) 205 | qp := p.Parse() 206 | 207 | if !reflect.DeepEqual(tt.query, qp) { 208 | t.Fatalf("expected query %#v, but got %#v", tt.query, qp) 209 | } 210 | }) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Liam Stanley . All rights reserved. Use 2 | // of this source code is governed by the MIT license that can be found in 3 | // the LICENSE file. 4 | 5 | // Package queryparser parses a common "q" http GET variable to look for text 6 | // filters, which can be used for advanced searching. For example: 7 | // Hello World tags:example,world foo:"something quoted" author:lrstanley 8 | // 9 | // go-queryparser also strips out not very safe/useful characters by default. 10 | // See the DefaultCut function for details (and New() for how you can do this 11 | // yourself.) 12 | package queryparser 13 | 14 | import ( 15 | "sort" 16 | "strings" 17 | ) 18 | 19 | // Query represents filtered input. 20 | type Query struct { 21 | // Raw is the raw (trailing) text of items that weren't filters. 22 | Raw string 23 | // Contains the parsed filters 24 | Filters map[string][]string 25 | } 26 | 27 | // add a filter manually 28 | func (q *Query) Add(key, val string) { 29 | val = stripDuplicateWS(val) 30 | var vals []string 31 | 32 | if strings.HasPrefix(val, `"`) { 33 | vals = []string{strings.Trim(val, `"`)} 34 | } else if strings.HasPrefix(val, `'`) { 35 | vals = []string{strings.Trim(val, `'`)} 36 | } else { 37 | vals = strings.FieldsFunc(val, func(r rune) bool { 38 | return r == ',' 39 | }) 40 | } 41 | 42 | key = strings.ToLower(key) 43 | if _, ok := q.Filters[key]; !ok { 44 | q.Filters[key] = make([]string, 0, 1) 45 | } 46 | 47 | q.Filters[key] = append(q.Filters[key], vals...) 48 | } 49 | 50 | // Has returns true if there is a filter matching the given name. 51 | func (q *Query) Has(key string) (exists bool) { 52 | _, exists = q.Filters[strings.ToLower(key)] 53 | return exists 54 | } 55 | 56 | // Get returns the results of the filter if it exists, and if it successfully 57 | // found a result. 58 | func (q *Query) Get(key string) (results []string, ok bool) { 59 | results, ok = q.Filters[strings.ToLower(key)] 60 | return results, ok 61 | } 62 | 63 | // GetOne returns the last known result for the filter, if it exists. Useful 64 | // if you only want a user to define a filter once. The resulting string 65 | // is empty if no filter of that key was found. 66 | func (q *Query) GetOne(key string) string { 67 | results, ok := q.Get(key) 68 | 69 | if !ok { 70 | return "" 71 | } 72 | 73 | out := results[len(results)-1] 74 | 75 | return out 76 | } 77 | 78 | // String will return a string representation of the query, however all filters 79 | // will be sorted alphabetically, and the raw text will be placed at the end 80 | // of the string. 81 | func (q *Query) String() (out string) { 82 | skeys := make([]string, 0, len(q.Filters)) 83 | for key := range q.Filters { 84 | skeys = append(skeys, key) 85 | } 86 | sort.Strings(skeys) 87 | 88 | for _, key := range skeys { 89 | vals := q.Filters[key] 90 | sort.Strings(vals) 91 | 92 | for _, val := range vals { 93 | out += key + `:"` + val + `" ` 94 | } 95 | } 96 | 97 | if q.Raw != "" { 98 | out += q.Raw 99 | } 100 | 101 | return strings.TrimSpace(out) 102 | } 103 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Liam Stanley . All rights reserved. Use 2 | // of this source code is governed by the MIT license that can be found in 3 | // the LICENSE file. 4 | 5 | package queryparser 6 | 7 | import ( 8 | "fmt" 9 | "unicode" 10 | "unicode/utf8" 11 | ) 12 | 13 | // stateFn represents the state of the scanner as a function that returns the 14 | // next state. 15 | type stateFn func(*scanner) stateFn 16 | 17 | type token int 18 | 19 | type tokenRef struct { 20 | tok token // Token that represents the literal. 21 | pos int // Position in the input. 22 | lit string // Literal/value of item. 23 | } 24 | 25 | const ( 26 | tokenEOF token = iota // EOF. 27 | 28 | tokenDELIM // : 29 | tokenFIELD // Quoted (with spaces/words) or unquoted (single word). 30 | tokenIDENT // Raw text, an IDENT can also be a WORD. 31 | tokenWS // Whitespaces. 32 | 33 | // eof isn't a token, but rather the literal reference to the EOF token. 34 | eof = 1 35 | ) 36 | 37 | func (i tokenRef) String() string { 38 | if i.tok == tokenEOF { 39 | return "EOF" 40 | } 41 | 42 | if len(i.lit) > 10 { 43 | return fmt.Sprintf("%.10q...", i.lit) 44 | } 45 | 46 | return fmt.Sprintf("%q", i.lit) 47 | } 48 | 49 | // scanner represents a lexical scanner. 50 | type scanner struct { 51 | items chan tokenRef // The channel of scanned items. 52 | input string // The string being scanned. 53 | pos int // Current position in the input. 54 | start int // Start position of the acive item. 55 | width int // Width of last rune read from input. 56 | lastPos int // Position of most recent item returned by nextItem. 57 | } 58 | 59 | // newScanner returns a new instance of Scanner. This starts a goroutine. Make sure 60 | // to call drain() on it to ensure it doesn't leak goroutines. 61 | func newScanner(input string) *scanner { 62 | s := &scanner{ 63 | input: input, 64 | items: make(chan tokenRef), 65 | } 66 | 67 | go s.run() 68 | return s 69 | } 70 | 71 | // emit passes a tokenRef back to the client. 72 | func (s *scanner) emit(t token) { 73 | s.items <- tokenRef{t, s.start, s.input[s.start:s.pos]} 74 | s.start = s.pos 75 | } 76 | 77 | // nextToken returns the next tokenRef from the input. Called by the parser, not 78 | // in the lexing goroutine. 79 | func (s *scanner) nextToken() tokenRef { 80 | item := <-s.items 81 | s.lastPos = item.pos 82 | return item 83 | } 84 | 85 | // run runs the state machine for the lexer. 86 | func (s *scanner) run() { 87 | for state := scanMain; state != nil; { 88 | state = state(s) 89 | } 90 | close(s.items) 91 | } 92 | 93 | // drain drains the output so the lexing goroutine will exit. Called by the 94 | // parser, not in the lexing goroutine. 95 | func (s *scanner) drain() { 96 | for range s.items { 97 | } 98 | } 99 | 100 | // read reads the next rune from the buffered reader. 101 | // Returns eof if an error occurs (or io.EOF is returned). 102 | func (s *scanner) next() rune { 103 | if s.pos >= len(s.input) { 104 | s.width = 0 105 | return eof 106 | } 107 | 108 | r, w := utf8.DecodeRuneInString(s.input[s.pos:]) 109 | s.width = w 110 | s.pos += s.width 111 | 112 | return r 113 | } 114 | 115 | // backup steps back one rune. Can only be called once per call of next. 116 | func (s *scanner) backup() { 117 | s.pos -= s.width 118 | } 119 | 120 | // peek steps forward one rune, reads, and backs up again. 121 | func (s *scanner) peek() rune { 122 | r := s.next() 123 | s.backup() 124 | return r 125 | } 126 | 127 | func scanMain(s *scanner) stateFn { 128 | switch r := s.next(); { 129 | case r == eof: 130 | s.emit(tokenEOF) 131 | return nil 132 | case isWhitespace(r): 133 | return scanWhitespace 134 | case r == ':': 135 | s.emit(tokenDELIM) 136 | return scanMain 137 | case r == '"': 138 | return scanDoubleQuote 139 | case r == '\'': 140 | return scanSingleQuote 141 | case isWord(r): 142 | return scanWord 143 | } 144 | return nil 145 | } 146 | 147 | // scanWhitespace scans a run of space characters. One space has already been 148 | // seen. 149 | func scanWhitespace(s *scanner) stateFn { 150 | for isWhitespace(s.peek()) { 151 | s.next() 152 | } 153 | s.emit(tokenWS) 154 | return scanMain 155 | } 156 | 157 | // scanWord scans a run of word characters. One word character has already been 158 | // seen. 159 | func scanWord(s *scanner) stateFn { 160 | for isWord(s.peek()) { 161 | s.next() 162 | } 163 | s.emit(tokenIDENT) 164 | return scanMain 165 | } 166 | 167 | // scanSingleQuote scans a quoted string. 168 | func scanSingleQuote(s *scanner) stateFn { 169 | Loop: 170 | for { 171 | switch s.next() { 172 | case '\\': 173 | if r := s.next(); r != eof { 174 | break 175 | } 176 | fallthrough 177 | case eof: 178 | // Should this be a req? Unterminated quoted string. 179 | break Loop 180 | case '\'': 181 | break Loop 182 | } 183 | } 184 | s.emit(tokenFIELD) 185 | return scanMain 186 | } 187 | 188 | // scanDoubleQuote scans a quoted string. 189 | func scanDoubleQuote(s *scanner) stateFn { 190 | Loop: 191 | for { 192 | switch s.next() { 193 | case '\\': 194 | if r := s.next(); r != eof { 195 | break 196 | } 197 | fallthrough 198 | case eof: 199 | // Should this be a req? Unterminated quoted string. 200 | break Loop 201 | case '"': 202 | break Loop 203 | } 204 | } 205 | s.emit(tokenFIELD) 206 | return scanMain 207 | } 208 | 209 | // isWhitespace returns true if ch is a space or a tab. 210 | func isWhitespace(ch rune) bool { 211 | return ch == ' ' || ch == '\t' 212 | } 213 | 214 | // isWord returns true if ch is character allowed in raw text. 215 | func isWord(ch rune) bool { 216 | return ch >= '!' && ch <= '~' && ch != ':' 217 | } 218 | 219 | func isIdent(input string) bool { 220 | for _, r := range input { 221 | if r != '_' && r != '-' && !unicode.IsLetter(r) && !unicode.IsDigit(r) { 222 | return false 223 | } 224 | } 225 | 226 | return true 227 | } 228 | --------------------------------------------------------------------------------