├── .errcheck_excludes.txt ├── .github └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── example ├── custom_matcher.go └── simple.go ├── go.mod ├── go.sum ├── integration ├── integration_custom_matcher_test.go └── integration_simple_test.go ├── parser ├── lexer.go ├── lexer_test.go ├── nodes.go ├── parser.go ├── parser_test.go ├── stack.go ├── stack_test.go └── token.go └── sql_adaptor ├── adaptor.go ├── sql.go ├── sql_test.go └── validators.go /.errcheck_excludes.txt: -------------------------------------------------------------------------------- 1 | fmt.Fprintln 2 | fmt.Fprint -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | pull_request: 4 | jobs: 5 | lint: 6 | name: lint 7 | if: github.repository == 'SeldonIO/goven' # Do not run this on forks. 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: '^1.18.0' 14 | - name: lint 15 | uses: golangci/golangci-lint-action@v2 16 | with: 17 | version: v1.45.0 18 | skip-go-installation: true 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | pull_request: 4 | jobs: 5 | lint: 6 | name: test 7 | if: github.repository == 'SeldonIO/goven' # Do not run this on forks. 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: '^1.18.0' 14 | - name: test 15 | run: make test-coverage 16 | - name: upload coverage to codecov 17 | uses: codecov/codecov-action@v2 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | .idea/ 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # options for analysis running 2 | run: 3 | # timeout for analysis, e.g. 30s, 5m, default is 1m 4 | deadline: 5m 5 | 6 | # exit code when at least one issue was found, default is 1 7 | issues-exit-code: 1 8 | 9 | # which dirs to skip: they won't be analyzed; 10 | # can use regexp here: generated.*, regexp is applied on full path; 11 | # default value is empty list, but next dirs are always skipped independently 12 | # from this option's value: 13 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 14 | skip-dirs: vendor 15 | 16 | # output configuration options 17 | output: 18 | # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" 19 | format: colored-line-number 20 | 21 | # print lines of code with issue, default is true 22 | print-issued-lines: true 23 | 24 | # print linter name in the end of issue text, default is true 25 | print-linter-name: true 26 | 27 | linters: 28 | disable-all: true 29 | enable: 30 | # Sorted alphabetically. 31 | - errcheck 32 | - exportloopref 33 | - goimports # Also includes gofmt style formatting 34 | - gosimple 35 | - govet 36 | - misspell 37 | - staticcheck 38 | - structcheck 39 | - typecheck 40 | - varcheck 41 | 42 | linters-settings: 43 | errcheck: 44 | exclude: ./.errcheck_excludes.txt 45 | goconst: 46 | min-occurrences: 5 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Seldon Technologies Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | lint: 2 | golangci-lint run --fix 3 | 4 | test: 5 | go test ./... 6 | 7 | test-coverage: 8 | go test ./... -race -covermode=atomic -coverprofile=coverage.out -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Reference](https://pkg.go.dev/badge/github.com/seldonio/goven.svg)](https://pkg.go.dev/github.com/seldonio/goven) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/seldonio/goven)](https://goreportcard.com/report/github.com/seldonio/goven) 3 | [![codecov](https://codecov.io/gh/seldonio/goven/branch/master/graph/badge.svg?token=ZBCTOI896Y)](https://codecov.io/gh/seldonio/goven) 4 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 5 | 6 | # Goven 🧑‍🍳 7 | 8 | Goven (go-oven) is a go library that allows you to have a drop-in query language for your database schema. 9 | 10 | * Take any gorm database object and with a few lines of code you can make it searchable. 11 | * [Safe query language not exposing SQL to your users](https://imgs.xkcd.com/comics/exploits_of_a_mom.png). 12 | * Easily extensible to support more advanced queries for your own schema. 13 | * Basic grammar that allows for powerful queries. 14 | 15 | Like a real life oven, it takes something raw (your database struct + a query input) and produces something specific (the schema specific parser + SQL output). We call the adaptors "recipes" that Goven can make. Currently Goven only supports a SQL adaptor, but the AST produced by the lexer/parser can easily be extended to other query languages. 16 | 17 | ## Recipes 18 | 19 | ### Basic Example 20 | 21 | You can make a basic query using gorm against your database, something like this: 22 | 23 | ```go 24 | reflection := reflect.ValueOf(&User{}) 25 | queryAdaptor, err := sql_adaptor.NewDefaultAdaptorFromStruct(reflection) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | dbQuery := db.WithContext(ctx) 31 | parsedQuery, err := queryAdaptor.Parse("(name=james AND age > 11) OR email=fred@gmail.com") 32 | if err != nil { 33 | return nil, err 34 | } 35 | dbQuery = query.Model(User{}).Where(parsedQuery.Raw, sql_adaptor.StringSliceToInterfaceSlice(parsedQuery.Values)...) 36 | err = dbQuery.Find(&users).Error 37 | ``` 38 | 39 | The values are interpolated to prevent injection attacks. 40 | 41 | ### Extension Example 42 | 43 | You can also extend the basic query language with regex matchers. An example would be having a Tag struct on your User schema. 44 | 45 | ```go 46 | type User struct { 47 | gorm.Model 48 | name string 49 | tags []Tag 50 | } 51 | 52 | type Tag struct { 53 | gorm.Model 54 | Key string 55 | Value string 56 | } 57 | ``` 58 | 59 | You can make this searchable my defining a regex and a custom matcher. 60 | 61 | e.g if we want `tags[key]=value` to work then we can add the following matcher when creating the adaptor. 62 | 63 | ```go 64 | KeyValueRegex = `(tags)\[(.+)\]` 65 | 66 | // keyValueMatcher is a custom matcher for and tags[y]. 67 | func keyValueMatcher(ex *goven_parser.Expression) (*goven_sql.SqlResponse, error) { 68 | reg, err := regexp.Compile(KeyValueRegex) 69 | if err != nil { 70 | return nil, err 71 | } 72 | slice := reg.FindStringSubmatch(ex.Field) 73 | if slice == nil { 74 | return nil, errors.New("didn't match regex expression") 75 | } 76 | if len(slice) < 3 { 77 | return nil, errors.New("regex match slice is too short") 78 | } 79 | sq := goven_sql.SqlResponse{ 80 | Raw: fmt.Sprintf("id IN (SELECT user_id FROM %s WHERE key=? AND value%s?)", slice[1], ex.Comparator), 81 | Values: []string{slice[2], ex.Value}, 82 | } 83 | return &sq, nil 84 | } 85 | ``` 86 | 87 | ### Protecting Fields 88 | 89 | Sometimes we may not want particular fields to be searchable by end users. You can protect them by removing them from the fields mapping when creating your adaptor. 90 | 91 | ```go 92 | defaultFields := goven_sql.FieldParseValidatorFromStruct(gorm) 93 | delete(defaultFields, "fieldname") 94 | ``` 95 | 96 | ## Grammar 97 | 98 | Goven has a simple syntax that allows for powerful queries. 99 | 100 | Fields can be compared using the following operators: 101 | 102 | `=`, `!=`, `>=`, `<=`, `<`, `>`, `%` 103 | 104 | The `%` operator allows you to do partial string matching using LIKE. 105 | 106 | Multiple queries can be combined using `AND`, `OR`. 107 | 108 | Together this means you can build up a query like this: 109 | 110 | `model_name=iris AND version>=2.0` 111 | 112 | More advanced queries can be built up using bracketed expressions: 113 | 114 | `(model_name=iris AND version>=2.0) OR artifact_type=TENSORFLOW` 115 | -------------------------------------------------------------------------------- /example/custom_matcher.go: -------------------------------------------------------------------------------- 1 | // Package example provides example use cases of goven with a data model. 2 | package example 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "reflect" 9 | "regexp" 10 | "strings" 11 | "time" 12 | 13 | "github.com/seldonio/goven/parser" 14 | 15 | "github.com/seldonio/goven/sql_adaptor" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | const ( 20 | keyValueRegex = `(.+)\[(.+)\]` 21 | ) 22 | 23 | // Model represents an example machine learning model schema. 24 | type Model struct { 25 | gorm.Model 26 | Name string 27 | Version string 28 | CreatedAt time.Time 29 | Tags []Tag `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"` 30 | } 31 | 32 | // Tag is a string key value tag. 33 | type Tag struct { 34 | gorm.Model 35 | Key string 36 | Value string 37 | ModelID uint 38 | } 39 | 40 | // ModelDAO is an example DAO for machine learning models. 41 | type ModelDAO struct { 42 | db *gorm.DB 43 | queryAdaptor *sql_adaptor.SqlAdaptor 44 | } 45 | 46 | // NewModelDAO returns a ModelDAO. 47 | func NewModelDAO(db *gorm.DB) (*ModelDAO, error) { 48 | adaptor, err := CreateModelAdaptor() 49 | if err != nil { 50 | return nil, err 51 | } 52 | return &ModelDAO{ 53 | db: db, 54 | queryAdaptor: adaptor, 55 | }, nil 56 | } 57 | 58 | // CreateModel commits the provided model to the database. 59 | func (u *ModelDAO) CreateModel(model *Model) error { 60 | ctx := context.Background() 61 | tx := u.db.Begin().WithContext(ctx) 62 | err := tx.Create(model).Error 63 | if err != nil { 64 | tx.Rollback() 65 | return err 66 | } 67 | return tx.Commit().Error 68 | } 69 | 70 | // MakeQuery takes a goven query and performs it against the model database. 71 | func (u *ModelDAO) MakeQuery(q string) ([]Model, error) { 72 | var models []Model 73 | ctx := context.Background() 74 | query := u.db.WithContext(ctx) 75 | queryResp, err := u.queryAdaptor.Parse(q) 76 | if err != nil { 77 | return nil, err 78 | } 79 | query = query.Preload("Tags").Model(Model{}).Where(queryResp.Raw, sql_adaptor.StringSliceToInterfaceSlice(queryResp.Values)...) 80 | err = query.Find(&models).Error 81 | if err != nil { 82 | return nil, err 83 | } 84 | return models, nil 85 | } 86 | 87 | // CreateModelAdaptor creates a new SqlAdaptor for the model schema. 88 | func CreateModelAdaptor() (*sql_adaptor.SqlAdaptor, error) { 89 | matchers := map[*regexp.Regexp]sql_adaptor.ParseValidateFunc{} 90 | fieldMappings := map[string]string{} 91 | 92 | // Custom matcher initialised here. 93 | reg, err := regexp.Compile(keyValueRegex) 94 | if err != nil { 95 | return nil, err 96 | } 97 | matchers[reg] = keyValueMatcher 98 | 99 | reflection := reflect.ValueOf(&Model{}) 100 | defaultFields := sql_adaptor.FieldParseValidatorFromStruct(reflection) 101 | return sql_adaptor.NewSqlAdaptor(fieldMappings, defaultFields, matchers), nil 102 | } 103 | 104 | // keyValueMatcher is a custom matcher for tags[x]. 105 | func keyValueMatcher(ex *parser.Expression) (*sql_adaptor.SqlResponse, error) { 106 | reg, err := regexp.Compile(keyValueRegex) 107 | if err != nil { 108 | return nil, err 109 | } 110 | slice := reg.FindStringSubmatch(ex.Field) 111 | if slice == nil { 112 | return nil, errors.New("didn't match regex expression") 113 | } 114 | if len(slice) < 3 { 115 | return nil, errors.New("regex match slice is too short") 116 | } 117 | if strings.ToLower(slice[1]) != "tags" { 118 | return nil, errors.New("expected tags as field name") 119 | } 120 | 121 | // We need to handle the % comparator differently since it isn't implicitly supported in SQL. 122 | defaultMatch := sql_adaptor.DefaultMatcher(&parser.Expression{ 123 | Field: "value", 124 | Comparator: ex.Comparator, 125 | Value: ex.Value, 126 | }) 127 | rawSnippet := defaultMatch.Raw 128 | if len(defaultMatch.Values) != 1 { 129 | return nil, errors.New("unexpected number of values from matcher") 130 | } 131 | value := defaultMatch.Values[0] 132 | sq := sql_adaptor.SqlResponse{ 133 | Raw: fmt.Sprintf("id IN (SELECT model_id FROM %s WHERE key=? AND %s AND deleted_at is NULL)", slice[1], rawSnippet), 134 | Values: []string{slice[2], value}, 135 | } 136 | return &sq, nil 137 | } 138 | -------------------------------------------------------------------------------- /example/simple.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "reflect" 7 | "time" 8 | 9 | "github.com/seldonio/goven/sql_adaptor" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | // User represents an simple example database schema. 14 | type User struct { 15 | ID uint 16 | Name string 17 | Email *string 18 | Age uint8 19 | Birthday *time.Time 20 | MemberNumber sql.NullString 21 | CreatedAt time.Time 22 | } 23 | 24 | // UserDAO is an example DAO for user data. 25 | type UserDAO struct { 26 | db *gorm.DB 27 | queryAdaptor *sql_adaptor.SqlAdaptor 28 | } 29 | 30 | // NewUserDAO returns a UserDAO. 31 | func NewUserDAO(db *gorm.DB) (*UserDAO, error) { 32 | reflection := reflect.ValueOf(&User{}) 33 | adaptor := sql_adaptor.NewDefaultAdaptorFromStruct(reflection) 34 | return &UserDAO{ 35 | db: db, 36 | queryAdaptor: adaptor, 37 | }, nil 38 | } 39 | 40 | // CreateUser commits the provided user to the database. 41 | func (u *UserDAO) CreateUser(user *User) error { 42 | ctx := context.Background() 43 | tx := u.db.Begin().WithContext(ctx) 44 | err := tx.Create(user).Error 45 | if err != nil { 46 | tx.Rollback() 47 | return err 48 | } 49 | return tx.Commit().Error 50 | } 51 | 52 | // MakeQuery takes a goven query and performs it against the user database. 53 | func (u *UserDAO) MakeQuery(q string) ([]User, error) { 54 | var users []User 55 | ctx := context.Background() 56 | query := u.db.WithContext(ctx) 57 | queryResp, err := u.queryAdaptor.Parse(q) 58 | if err != nil { 59 | return nil, err 60 | } 61 | query = query.Model(User{}).Where(queryResp.Raw, sql_adaptor.StringSliceToInterfaceSlice(queryResp.Values)...) 62 | err = query.Find(&users).Error 63 | if err != nil { 64 | return nil, err 65 | } 66 | return users, nil 67 | } 68 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/seldonio/goven 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/fergusstrange/embedded-postgres v1.12.0 7 | github.com/iancoleman/strcase v0.2.0 8 | github.com/onsi/gomega v1.16.0 9 | gorm.io/driver/postgres v1.2.3 10 | gorm.io/gorm v1.22.4 11 | ) 12 | 13 | require ( 14 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 15 | github.com/jackc/pgconn v1.10.1 // indirect 16 | github.com/jackc/pgio v1.0.0 // indirect 17 | github.com/jackc/pgpassfile v1.0.0 // indirect 18 | github.com/jackc/pgproto3/v2 v2.2.0 // indirect 19 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 20 | github.com/jackc/pgtype v1.9.1 // indirect 21 | github.com/jackc/pgx/v4 v4.14.1 // indirect 22 | github.com/jinzhu/inflection v1.0.0 // indirect 23 | github.com/jinzhu/now v1.1.4 // indirect 24 | github.com/lib/pq v1.10.4 // indirect 25 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 26 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect 27 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect 28 | golang.org/x/text v0.3.7 // indirect 29 | gopkg.in/yaml.v2 v2.4.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 3 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 4 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 5 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 6 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 7 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 8 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/fergusstrange/embedded-postgres v1.12.0 h1:+w/fObAUfr/6NFM0p/xqGnzFfM+c7R+G3W56pLlqv+k= 13 | github.com/fergusstrange/embedded-postgres v1.12.0/go.mod h1:tBq6ykQqQoYmhXhdwD9nioMI7L7r7NlVvQtM0ZRfUqs= 14 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 15 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 16 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 17 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 18 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 19 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 20 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 21 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 22 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 23 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 24 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 25 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 26 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 27 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 28 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 29 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 30 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 31 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 32 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 33 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 34 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 35 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 37 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 38 | github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= 39 | github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 40 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 41 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 42 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 43 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 44 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 45 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 46 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 47 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 48 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 49 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 50 | github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8= 51 | github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 52 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 53 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 54 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 55 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 56 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 57 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 58 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 59 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 60 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 61 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 62 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 63 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 64 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 65 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 66 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 67 | github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= 68 | github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 69 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 70 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 71 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 72 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 73 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 74 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 75 | github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 76 | github.com/jackc/pgtype v1.9.1 h1:MJc2s0MFS8C3ok1wQTdQxWuXQcB6+HwAm5x1CzW7mf0= 77 | github.com/jackc/pgtype v1.9.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 78 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 79 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 80 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 81 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 82 | github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8= 83 | github.com/jackc/pgx/v4 v4.14.1 h1:71oo1KAGI6mXhLiTMn6iDFcp3e7+zon/capWjl2OEFU= 84 | github.com/jackc/pgx/v4 v4.14.1/go.mod h1:RgDuE4Z34o7XE92RpLsvFiOEfrAUT0Xt2KxvX73W06M= 85 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 86 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 87 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 88 | github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 89 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 90 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 91 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 92 | github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 93 | github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= 94 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 95 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 96 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 97 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 98 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 99 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 100 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 101 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 102 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 103 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 104 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 105 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 106 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 107 | github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 108 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 109 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 110 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 111 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 112 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 113 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 114 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 115 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 116 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 117 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 118 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 119 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 120 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 121 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 122 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 123 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 124 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 125 | github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= 126 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 127 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 128 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 129 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 130 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 131 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 132 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 133 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 134 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 135 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 136 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 137 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 138 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 139 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 140 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 141 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 142 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 143 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 144 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 145 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 146 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 147 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 148 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 149 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 150 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 151 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 152 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 153 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 154 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 155 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 156 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 157 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 158 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 159 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 160 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 161 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 162 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 163 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 164 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 165 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 166 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 167 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 168 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 169 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 170 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 171 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 172 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 173 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 174 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 175 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 176 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= 177 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 178 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 179 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 180 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 181 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 182 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 183 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 184 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 185 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 186 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 187 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 188 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 189 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 190 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 191 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 192 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 193 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 194 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 195 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 196 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 197 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 198 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 199 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 200 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 201 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 202 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 203 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 204 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 205 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 212 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 216 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 217 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 218 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 219 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 220 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 221 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 222 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 223 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 224 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 225 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 226 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 227 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 228 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 229 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 230 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 231 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 232 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 233 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 234 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 235 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 236 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 237 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 238 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 239 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 240 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 241 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 242 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 243 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 244 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 245 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 246 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 247 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 248 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 249 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 250 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 251 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 252 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 253 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 254 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 255 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 256 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 257 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 258 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 259 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 260 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 261 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 262 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 263 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 264 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 265 | gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To= 266 | gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs= 267 | gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 268 | gorm.io/gorm v1.22.4 h1:8aPcyEJhY0MAt8aY6Dc524Pn+pO29K+ydu+e/cXSpQM= 269 | gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk= 270 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 271 | -------------------------------------------------------------------------------- /integration/integration_custom_matcher_test.go: -------------------------------------------------------------------------------- 1 | // Package integration contains integration tests using the example DAOs. 2 | package integration 3 | 4 | import ( 5 | "log" 6 | "testing" 7 | 8 | embeddedpostgres "github.com/fergusstrange/embedded-postgres" 9 | . "github.com/onsi/gomega" 10 | "github.com/seldonio/goven/example" 11 | ) 12 | 13 | var ( 14 | model1 = &example.Model{ 15 | Name: "model1", 16 | Tags: []example.Tag{ 17 | { 18 | Key: "auto_created", 19 | Value: "true", 20 | }, 21 | }, 22 | } 23 | model2 = &example.Model{ 24 | Name: "model2", 25 | Tags: []example.Tag{ 26 | { 27 | Key: "auto_created", 28 | Value: "false", 29 | }, 30 | }, 31 | } 32 | deployment1 = &example.Model{ 33 | Name: "deployment1", 34 | Tags: []example.Tag{ 35 | { 36 | Key: "tag", 37 | Value: "test_partial1", 38 | }, 39 | }, 40 | } 41 | deployment2 = &example.Model{ 42 | Name: "deployment2", 43 | Tags: []example.Tag{ 44 | { 45 | Key: "tag", 46 | Value: "test_partial2", 47 | }, 48 | }, 49 | } 50 | ) 51 | 52 | type testRigModel struct { 53 | pg *embeddedpostgres.EmbeddedPostgres 54 | modelDAO *example.ModelDAO 55 | } 56 | 57 | func newTestRigModel() (*testRigModel, error) { 58 | db, pg, err := setupDB() 59 | if err != nil { 60 | return nil, err 61 | } 62 | // Create Model table 63 | err = db.AutoMigrate(example.Model{}, example.Tag{}) 64 | if err != nil { 65 | return nil, err 66 | } 67 | dao, err := example.NewModelDAO(db) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return &testRigModel{ 72 | pg: pg, 73 | modelDAO: dao, 74 | }, nil 75 | } 76 | 77 | func (t *testRigModel) cleanup() { 78 | err := t.pg.Stop() 79 | if err != nil { 80 | log.Print(err) 81 | } 82 | } 83 | 84 | func TestSqlAdaptorModel(t *testing.T) { 85 | g := NewGomegaWithT(t) 86 | rig, err := newTestRigModel() 87 | defer rig.cleanup() 88 | g.Expect(err).To(BeNil()) 89 | // Setup entries 90 | for _, model := range []*example.Model{model1, model2, deployment1, deployment2} { 91 | err = rig.modelDAO.CreateModel(model) 92 | g.Expect(err).To(BeNil()) 93 | } 94 | t.Run("test simple successful query", func(t *testing.T) { 95 | result, err := rig.modelDAO.MakeQuery("name=model1") 96 | g.Expect(err).To(BeNil()) 97 | g.Expect(len(result)).To(Equal(1)) 98 | g.Expect(result[0].Name).To(Equal("model1")) 99 | g.Expect(len(result[0].Tags)).To(Equal(1)) 100 | g.Expect(result[0].Tags[0].Key).To(Equal("auto_created")) 101 | }) 102 | t.Run("test model tags true", func(t *testing.T) { 103 | result, err := rig.modelDAO.MakeQuery(`tags[auto_created]="true"`) 104 | g.Expect(err).To(BeNil()) 105 | g.Expect(len(result)).To(Equal(1)) 106 | g.Expect(result[0].Name).To(Equal("model1")) 107 | g.Expect(len(result[0].Tags)).To(Equal(1)) 108 | g.Expect(result[0].Tags[0].Value).To(Equal("true")) 109 | }) 110 | t.Run("test model tags false", func(t *testing.T) { 111 | result, err := rig.modelDAO.MakeQuery(`tags[auto_created]="false"`) 112 | g.Expect(err).To(BeNil()) 113 | g.Expect(len(result)).To(Equal(1)) 114 | g.Expect(result[0].Name).To(Equal("model2")) 115 | g.Expect(len(result[0].Tags)).To(Equal(1)) 116 | g.Expect(result[0].Tags[0].Value).To(Equal("false")) 117 | }) 118 | t.Run("test partial string match", func(t *testing.T) { 119 | result, err := rig.modelDAO.MakeQuery(`name%"model"`) 120 | g.Expect(err).To(BeNil()) 121 | g.Expect(len(result)).To(Equal(2)) 122 | }) 123 | t.Run("test partial string tags", func(t *testing.T) { 124 | result, err := rig.modelDAO.MakeQuery(`tags[tag] % partial`) 125 | g.Expect(err).To(BeNil()) 126 | g.Expect(len(result)).To(Equal(2)) 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /integration/integration_simple_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | . "github.com/onsi/gomega" 8 | "github.com/seldonio/goven/example" 9 | 10 | embeddedpostgres "github.com/fergusstrange/embedded-postgres" 11 | "gorm.io/driver/postgres" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | type testRigUser struct { 16 | pg *embeddedpostgres.EmbeddedPostgres 17 | userDAO *example.UserDAO 18 | } 19 | 20 | func setupDB() (*gorm.DB, *embeddedpostgres.EmbeddedPostgres, error) { 21 | // Create Postgres db 22 | config := embeddedpostgres.DefaultConfig().Port(9876) 23 | pg := embeddedpostgres.NewDatabase(config) 24 | err := pg.Start() 25 | if err != nil { 26 | return nil, nil, err 27 | } 28 | 29 | // Connect gorm to db 30 | dsn := "host=localhost port=9876 user=postgres password=postgres dbname=postgres sslmode=disable" 31 | db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) 32 | if err != nil { 33 | return nil, nil, err 34 | } 35 | return db, pg, err 36 | } 37 | 38 | func newTestRigUser() (*testRigUser, error) { 39 | db, pg, err := setupDB() 40 | if err != nil { 41 | return nil, err 42 | } 43 | // Create User table 44 | err = db.AutoMigrate(example.User{}) 45 | if err != nil { 46 | return nil, err 47 | } 48 | dao, err := example.NewUserDAO(db) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return &testRigUser{ 53 | pg: pg, 54 | userDAO: dao, 55 | }, nil 56 | } 57 | 58 | func (t *testRigUser) cleanup() { 59 | err := t.pg.Stop() 60 | if err != nil { 61 | log.Print(err) 62 | } 63 | } 64 | 65 | func TestSqlAdaptorUser(t *testing.T) { 66 | g := NewGomegaWithT(t) 67 | rig, err := newTestRigUser() 68 | defer rig.cleanup() 69 | g.Expect(err).To(BeNil()) 70 | 71 | // Setup entries 72 | err = rig.userDAO.CreateUser(&example.User{ 73 | Name: "", 74 | Age: 10, 75 | }) 76 | g.Expect(err).To(BeNil()) 77 | err = rig.userDAO.CreateUser(&example.User{ 78 | Name: "dom", 79 | Age: 12, 80 | }) 81 | g.Expect(err).To(BeNil()) 82 | err = rig.userDAO.CreateUser(&example.User{ 83 | Name: "dom", 84 | Age: 9, 85 | }) 86 | g.Expect(err).To(BeNil()) 87 | t.Run("test simple successful query", func(t *testing.T) { 88 | result, err := rig.userDAO.MakeQuery("name=dom AND age>11") 89 | g.Expect(err).To(BeNil()) 90 | g.Expect(len(result)).To(Equal(1)) 91 | g.Expect(result[0].Name).To(Equal("dom")) 92 | g.Expect(result[0].Age).To(Equal(uint8(12))) 93 | }) 94 | t.Run("test empty string query", func(t *testing.T) { 95 | result, err := rig.userDAO.MakeQuery("name=\"\"") 96 | g.Expect(err).To(BeNil()) 97 | g.Expect(len(result)).To(Equal(1)) 98 | g.Expect(result[0].Name).To(Equal("")) 99 | g.Expect(result[0].Age).To(Equal(uint8(10))) 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /parser/lexer.go: -------------------------------------------------------------------------------- 1 | // Package parser contains the definitions of the base tokens, the lexer that converts a query to a token stream, and the parser that converts a token stream into an AST. 2 | package parser 3 | 4 | import ( 5 | "bufio" 6 | "bytes" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | // Lexer represents a lexical scanner. 12 | type Lexer struct { 13 | r *bufio.Reader 14 | } 15 | 16 | // NewLexerFromString returns a Lexer for the provided string. 17 | func NewLexerFromString(s string) *Lexer { 18 | return NewLexer(strings.NewReader(s)) 19 | } 20 | 21 | // NewLexer returns a new instance of Lexer. 22 | func NewLexer(r io.Reader) *Lexer { 23 | return &Lexer{r: bufio.NewReader(r)} 24 | } 25 | 26 | // Scan returns the next token and literal Value. 27 | func (s *Lexer) Scan() TokenInfo { 28 | // Read the next rune. 29 | ch := s.read() 30 | if ch == eof { 31 | return TokenInfo{EOF, ""} 32 | } 33 | 34 | // Find all 1 or 2 length tokens 35 | if ch == '>' { 36 | next := s.read() 37 | if next == '=' { 38 | // Don't unread, found a 2 length token 39 | return TokenInfo{GREATHER_THAN_EQUAL, ">="} 40 | } 41 | s.unread() 42 | return TokenInfo{GREATER_THAN, string(ch)} 43 | } 44 | if ch == '<' { 45 | next := s.read() 46 | if next == '=' { 47 | // Don't unread, found a 2 length token 48 | return TokenInfo{LESS_THAN_EQUAL, "<="} 49 | } 50 | s.unread() 51 | return TokenInfo{LESS_THAN, string(ch)} 52 | } 53 | if ch == '!' { 54 | next := s.read() 55 | if next == '=' { 56 | // Don't unread, found a 2 length token 57 | return TokenInfo{NOT_EQUAL, "!="} 58 | } 59 | s.unread() 60 | return TokenInfo{EOF, ""} 61 | } 62 | 63 | switch { 64 | case ch == '=': 65 | return TokenInfo{EQUAL, string(ch)} 66 | case ch == '(': 67 | return TokenInfo{OPEN_BRACKET, string(ch)} 68 | case ch == ')': 69 | return TokenInfo{CLOSED_BRACKET, string(ch)} 70 | case ch == '%': 71 | return TokenInfo{PERCENT, string(ch)} 72 | case isWhitespace(ch): 73 | s.unread() 74 | return s.scanWhitespace() 75 | default: 76 | s.unread() 77 | return s.scanKeyword() 78 | } 79 | } 80 | 81 | // scanWhitespace consumes the current rune and all contiguous whitespace. 82 | func (s *Lexer) scanWhitespace() TokenInfo { 83 | // Create a buffer and read the current character into it. 84 | var ch rune 85 | _ = s.read() 86 | // Read every subsequent whitespace character into the buffer. 87 | // Non-whitespace characters and EOF will cause the loop to exit. 88 | for { 89 | ch = s.read() 90 | if ch == eof { 91 | break 92 | } else if !isWhitespace(ch) { 93 | s.unread() 94 | break 95 | } 96 | } 97 | 98 | return TokenInfo{WS, ""} 99 | } 100 | 101 | // scanKeyword consumes the current rune and all contiguous text runes. 102 | func (s *Lexer) scanKeyword() TokenInfo { 103 | // Create a buffer and read the current character into it. 104 | var buf bytes.Buffer 105 | 106 | // Read every subsequent text character into the buffer. 107 | // Non-text characters and EOF will cause the loop to exit. 108 | ch := s.read() 109 | quotedString := ch == '"' 110 | if !quotedString { 111 | s.unread() 112 | } 113 | for { 114 | ch = s.read() 115 | // Break if we hit EOF. 116 | if ch == eof { 117 | break 118 | } 119 | // Break is we hit the end of a quoted string. 120 | if ch == '"' && quotedString { 121 | break 122 | } 123 | // Break if we hit whitespace or a special char and we're not in a quoted string. 124 | if (isWhitespace(ch) || isSpecialChar(ch)) && !quotedString { 125 | s.unread() 126 | break 127 | } 128 | // Write the char into the buffer otherwise. 129 | buf.WriteRune(ch) 130 | } 131 | 132 | // If the string matches a keyword then return that keyword. 133 | switch strings.ToLower(buf.String()) { 134 | case "and": 135 | return TokenInfo{AND, "AND"} 136 | case "or": 137 | return TokenInfo{OR, "OR"} 138 | } 139 | 140 | return TokenInfo{STRING, buf.String()} 141 | } 142 | 143 | // read reads the next rune from the buffered reader. 144 | // Returns the rune(0) if an error occurs (or io.EOF is returned). 145 | func (s *Lexer) read() rune { 146 | ch, _, err := s.r.ReadRune() 147 | if err != nil { 148 | return eof 149 | } 150 | return ch 151 | } 152 | 153 | // unread places the previously read rune back on the reader, cannot unread twice sequentially. 154 | func (s *Lexer) unread() { 155 | // Unread can error if we have previously not called read, this is not dangerous (no data mutation) and returning 156 | // error here would unnecessarily complicate the code. 157 | _ = s.r.UnreadRune() 158 | } 159 | 160 | // isWhitespace returns true if the rune is a space, tab, or newline. 161 | func isWhitespace(ch rune) bool { return ch == ' ' || ch == '\t' || ch == '\n' } 162 | 163 | func isSpecialChar(ch rune) bool { 164 | specialChar := []rune{'=', '>', '!', '<', '(', ')', '%'} 165 | for _, char := range specialChar { 166 | if ch == char { 167 | return true 168 | } 169 | } 170 | return false 171 | } 172 | 173 | func isTokenGate(tok Token) bool { 174 | return tok == AND || tok == OR 175 | } 176 | 177 | func isTokenComparator(tok Token) bool { 178 | return tok == GREATER_THAN || tok == GREATHER_THAN_EQUAL || tok == LESS_THAN || tok == LESS_THAN_EQUAL || tok == EQUAL || tok == NOT_EQUAL || tok == PERCENT 179 | } 180 | 181 | // eof represents a marker rune for the end of the reader. 182 | var eof = rune(0) 183 | -------------------------------------------------------------------------------- /parser/lexer_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | func lexerHelper(lex *Lexer) ([]Token, []string) { 10 | var tokens []Token 11 | var literals []string 12 | for { 13 | tok := lex.Scan() 14 | tokens = append(tokens, tok.Token) 15 | literals = append(literals, tok.Literal) 16 | if tok.Token == EOF { 17 | return tokens, literals 18 | } 19 | } 20 | } 21 | 22 | func TestLexer(t *testing.T) { 23 | g := NewGomegaWithT(t) 24 | t.Run("scan into tokens succeeds", func(t *testing.T) { 25 | s := "name=model1" 26 | lexer := NewLexerFromString(s) 27 | tokens, literals := lexerHelper(lexer) 28 | g.Expect(tokens).To(Equal([]Token{STRING, EQUAL, STRING, EOF})) 29 | g.Expect(literals).To(Equal([]string{"name", "=", "model1", ""})) 30 | }) 31 | t.Run("scan into tokens succeeds percent", func(t *testing.T) { 32 | s := "name%model1" 33 | lexer := NewLexerFromString(s) 34 | tokens, literals := lexerHelper(lexer) 35 | g.Expect(tokens).To(Equal([]Token{STRING, PERCENT, STRING, EOF})) 36 | g.Expect(literals).To(Equal([]string{"name", "%", "model1", ""})) 37 | }) 38 | t.Run("scan into tokens succeeds for quoted string", func(t *testing.T) { 39 | s := "name=\"Iris Classifier\"" 40 | lexer := NewLexerFromString(s) 41 | tokens, literals := lexerHelper(lexer) 42 | g.Expect(tokens).To(Equal([]Token{STRING, EQUAL, STRING, EOF})) 43 | g.Expect(literals).To(Equal([]string{"name", "=", "Iris Classifier", ""})) 44 | }) 45 | t.Run("scan into tokens succeeds for empty quoted string", func(t *testing.T) { 46 | s := "name=\"\"" 47 | lexer := NewLexerFromString(s) 48 | tokens, literals := lexerHelper(lexer) 49 | g.Expect(tokens).To(Equal([]Token{STRING, EQUAL, STRING, EOF})) 50 | g.Expect(literals).To(Equal([]string{"name", "=", "", ""})) 51 | }) 52 | t.Run("scan into tokens with whitespace succeeds", func(t *testing.T) { 53 | s := "name = \n model1" 54 | lexer := NewLexerFromString(s) 55 | tokens, literals := lexerHelper(lexer) 56 | g.Expect(tokens).To(Equal([]Token{STRING, WS, EQUAL, WS, STRING, EOF})) 57 | g.Expect(literals).To(Equal([]string{"name", "", "=", "", "model1", ""})) 58 | }) 59 | t.Run("scan into tokens all token types", func(t *testing.T) { 60 | s := "string ( ) > >= < <= = != AND OR and or" 61 | lexer := NewLexerFromString(s) 62 | tokens, literals := lexerHelper(lexer) 63 | g.Expect(tokens).To(Equal([]Token{STRING, WS, OPEN_BRACKET, WS, CLOSED_BRACKET, WS, GREATER_THAN, WS, 64 | GREATHER_THAN_EQUAL, WS, LESS_THAN, WS, LESS_THAN_EQUAL, WS, EQUAL, WS, NOT_EQUAL, WS, AND, WS, OR, WS, AND, WS, OR, EOF})) 65 | var literalsNoWhitespace []string 66 | for _, val := range literals { 67 | if val != "" { 68 | literalsNoWhitespace = append(literalsNoWhitespace, val) 69 | } 70 | } 71 | g.Expect(literalsNoWhitespace).To(Equal([]string{"string", "(", ")", ">", ">=", "<", "<=", "=", "!=", "AND", "OR", "AND", "OR"})) 72 | }) 73 | t.Run("scan tokens is greedy", func(t *testing.T) { 74 | s := "<==" 75 | lexer := NewLexerFromString(s) 76 | tokens, _ := lexerHelper(lexer) 77 | // Here we expect the string to be consumed as less than equal, equal. Not less than, equal, equal. 78 | g.Expect(tokens).To(Equal([]Token{LESS_THAN_EQUAL, EQUAL, EOF})) 79 | s = ">==" 80 | lexer = NewLexerFromString(s) 81 | tokens, _ = lexerHelper(lexer) 82 | g.Expect(tokens).To(Equal([]Token{GREATHER_THAN_EQUAL, EQUAL, EOF})) 83 | s = "!==" 84 | lexer = NewLexerFromString(s) 85 | tokens, _ = lexerHelper(lexer) 86 | g.Expect(tokens).To(Equal([]Token{NOT_EQUAL, EQUAL, EOF})) 87 | }) 88 | } 89 | 90 | func FuzzLexer(f *testing.F) { 91 | testcases := []string{">=!=", "string ( ) > >=", "< <= = != AND OR and or", "1 != \"2\""} 92 | for _, tc := range testcases { 93 | f.Add(tc) 94 | } 95 | 96 | f.Fuzz(func(t *testing.T, s string) { 97 | lexer := NewLexerFromString(s) 98 | tokens, _ := lexerHelper(lexer) 99 | // This is really testing for panics only. 100 | for _, token := range tokens { 101 | if _, ok := TokenLookup[token]; !ok { 102 | t.Errorf("unexpected token %v", token) 103 | } 104 | } 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /parser/nodes.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "fmt" 4 | 5 | const ( 6 | OPERATION = "operation" 7 | EXPRESSION = "expression" 8 | ) 9 | 10 | // Node represents a node in the AST after the expression is parsed. 11 | type Node interface { 12 | Type() string 13 | String() string 14 | } 15 | 16 | // Expression represents something like x=y or x>=y 17 | type Expression struct { 18 | Field string 19 | Comparator string 20 | Value string 21 | } 22 | 23 | // Operation represents a Node (Operation or Expression) compared with another Node using either `AND` or `OR`. 24 | type Operation struct { 25 | LeftNode Node 26 | Gate string 27 | RightNode Node 28 | } 29 | 30 | // Type returns the type for expression. 31 | func (e Expression) Type() string { return EXPRESSION } 32 | 33 | // Type returns the type for operation. 34 | func (o Operation) Type() string { return OPERATION } 35 | 36 | // String returns the string representation of expression. 37 | func (e Expression) String() string { 38 | return fmt.Sprintf("%v %v %v", e.Field, e.Comparator, e.Value) 39 | } 40 | 41 | // String returns the string representation of operation. 42 | func (o Operation) String() string { 43 | if o.Gate == "" { 44 | return fmt.Sprintf("(%v)", o.LeftNode) 45 | } 46 | return fmt.Sprintf("(%v %v %v)", o.LeftNode, o.Gate, o.RightNode) 47 | } 48 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Parser represents a parser, including a scanner and the underlying raw input. 10 | // It also contains a small buffer to allow for two unscans. 11 | type Parser struct { 12 | s *Lexer 13 | raw string 14 | buf TokenStack 15 | } 16 | 17 | // NewParser returns a new instance of Parser. 18 | func NewParser(s string) *Parser { 19 | return &Parser{s: NewLexer(strings.NewReader(s)), raw: s} 20 | } 21 | 22 | // Parse takes the raw string and returns the root node of the AST. 23 | func (p *Parser) Parse() (Node, error) { 24 | operation, err := p.parseOperation() 25 | if err != nil { 26 | return nil, err 27 | } 28 | // Try to peel like an onion. 29 | gate := operation.(*Operation).Gate 30 | if gate != "" && operation.(*Operation).RightNode == nil { 31 | return nil, errors.New("found open gate") 32 | } 33 | for gate == "" { 34 | operation = operation.(*Operation).LeftNode 35 | if operation == nil { 36 | return nil, errors.New("got nil operation") 37 | } 38 | if operation.Type() == EXPRESSION { 39 | break 40 | } 41 | gate = operation.(*Operation).Gate 42 | } 43 | return operation, nil 44 | } 45 | 46 | func (p *Parser) parseOperation() (Node, error) { 47 | op := &Operation{ 48 | LeftNode: nil, 49 | Gate: "", 50 | RightNode: nil, 51 | } 52 | tok, lit := p.scanIgnoreWhitespace() 53 | for tok != EOF { 54 | switch { 55 | // If we hit an open bracket then we parse the operation contained in the brackets. 56 | case tok == OPEN_BRACKET: 57 | node, err := p.parseOperation() 58 | if err != nil { 59 | return nil, err 60 | } 61 | // Assign the operation to left node if we haven't already. 62 | if op.LeftNode == nil { 63 | op.LeftNode = node 64 | break 65 | } 66 | if op.Gate == "" { 67 | return nil, errors.New("shouldn't find operation before Gate if left node already exists") 68 | } 69 | // Assign to right otherwise. 70 | if op.RightNode == nil { 71 | op.RightNode = node 72 | tempOp := &Operation{ 73 | LeftNode: op, 74 | Gate: "", 75 | RightNode: nil, 76 | } 77 | op = tempOp 78 | break 79 | } 80 | case tok == STRING: 81 | if (op.LeftNode != nil && op.Gate == "") || (op.LeftNode != nil && op.RightNode != nil) { 82 | return nil, errors.New("didn't expect an expression here") 83 | } 84 | p.unscan(TokenInfo{ 85 | Token: tok, 86 | Literal: lit, 87 | }) 88 | expr, err := p.parseExpression() 89 | if err != nil { 90 | return nil, err 91 | } 92 | if op.LeftNode == nil { 93 | op.LeftNode = expr 94 | break 95 | } 96 | if op.RightNode == nil { 97 | op.RightNode = expr 98 | tempOp := &Operation{ 99 | LeftNode: op, 100 | Gate: "", 101 | RightNode: nil, 102 | } 103 | op = tempOp 104 | break 105 | } 106 | return nil, errors.New("parsed expression, left and right node shouldn't have both been populated") 107 | case tok == CLOSED_BRACKET: 108 | if op.LeftNode == nil { 109 | return nil, errors.New("can't close a bracket when we have parsed nothing for the left node") 110 | } 111 | return op, nil 112 | case isTokenGate(tok): 113 | if op.Gate != "" { 114 | return nil, errors.New("already found a Gate") 115 | } 116 | op.Gate = lit 117 | default: 118 | return nil, fmt.Errorf("unexpected token %v", tok) 119 | } 120 | tok, lit = p.scanIgnoreWhitespace() 121 | } 122 | return op, nil 123 | } 124 | 125 | func (p *Parser) parseExpression() (Node, error) { 126 | exp := &Expression{ 127 | Field: "", 128 | Comparator: "", 129 | Value: "", 130 | } 131 | 132 | isValueEmpty := false 133 | // This code relies on being Field -> Comparator -> Value in order. 134 | tok, lit := p.scan() 135 | for tok != EOF { 136 | switch { 137 | // Open bracket means we found an operation that needs parsing. 138 | case tok == OPEN_BRACKET: 139 | return p.parseOperation() 140 | // Ignore whitespace unless we have completed the expression. 141 | case tok == WS: 142 | if exp.Field != "" && exp.Comparator != "" && (exp.Value != "" || isValueEmpty) { 143 | // Got to the end of the expression so quit 144 | return exp, nil 145 | } 146 | // Looking for the Field name. 147 | case exp.Field == "": 148 | if tok != STRING { 149 | return nil, fmt.Errorf("expected Field, got %v", tok) 150 | } 151 | exp.Field = lit 152 | // Looking for the Comparator. 153 | case exp.Comparator == "": 154 | if !isTokenComparator(tok) { 155 | return nil, fmt.Errorf("expected Comparator, got %v", tok) 156 | } 157 | exp.Comparator = lit 158 | // Looking for the Value 159 | case exp.Value == "": 160 | if tok != STRING { 161 | // If we didn't have an empty string in the value field - return an error. 162 | if isValueEmpty { 163 | break 164 | } else { 165 | return nil, fmt.Errorf("expected Value, got %v", tok) 166 | } 167 | } 168 | if lit == "" { 169 | isValueEmpty = true 170 | } 171 | exp.Value = lit 172 | } 173 | tok, lit = p.scan() 174 | } 175 | if exp.Field != "" && exp.Comparator == "" { 176 | return nil, errors.New("found no comparator when expected") 177 | } 178 | return exp, nil 179 | } 180 | 181 | // scan returns the next token from the underlying scanner. 182 | // If a token has been unscanned then read that instead. 183 | func (p *Parser) scan() (tok Token, lit string) { 184 | // If we have a token on the buffer, then return it. 185 | if p.buf.Len() != 0 { 186 | // Can ignore the error since it's not empty. 187 | tokenInf, _ := p.buf.Pop() 188 | return tokenInf.Token, tokenInf.Literal 189 | } 190 | 191 | // Otherwise read the next token from the scanner. 192 | tokenInf := p.s.Scan() 193 | tok, lit = tokenInf.Token, tokenInf.Literal 194 | return tok, lit 195 | } 196 | 197 | // scanIgnoreWhitespace scans the next non-whitespace token. 198 | func (p *Parser) scanIgnoreWhitespace() (tok Token, lit string) { 199 | tok, lit = p.scan() 200 | if tok == WS { 201 | tok, lit = p.scan() 202 | } 203 | return tok, lit 204 | } 205 | 206 | // unscan pushes the previously read tokens back onto the buffer. 207 | func (p *Parser) unscan(tok TokenInfo) { 208 | p.buf.Push(tok) 209 | } 210 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestBasicParser(t *testing.T) { 11 | g := NewGomegaWithT(t) 12 | t.Run("parse expression succeeds", func(t *testing.T) { 13 | s := "name=max" 14 | parser := NewParser(s) 15 | expr, err := parser.parseExpression() 16 | g.Expect(err).To(BeNil()) 17 | g.Expect(expr).To(Equal(&Expression{ 18 | Field: "name", 19 | Comparator: "=", 20 | Value: "max", 21 | })) 22 | }) 23 | t.Run("parse expression succeeds with whitespace", func(t *testing.T) { 24 | s := "name= max " 25 | parser := NewParser(s) 26 | expr, err := parser.parseExpression() 27 | g.Expect(err).To(BeNil()) 28 | g.Expect(expr).To(Equal(&Expression{ 29 | Field: "name", 30 | Comparator: "=", 31 | Value: "max", 32 | })) 33 | }) 34 | t.Run("parse expression fails invalid", func(t *testing.T) { 35 | s := "name==dog" 36 | parser := NewParser(s) 37 | _, err := parser.parseExpression() 38 | g.Expect(err).ToNot(BeNil()) 39 | }) 40 | t.Run("parse operations badly formatted return errors", func(t *testing.T) { 41 | tests := []string{ 42 | "name=max AND AND artifact=wow", 43 | "name=max artifact=wow", 44 | ")(name = max)", 45 | } 46 | for _, test := range tests { 47 | parser := NewParser(test) 48 | _, err := parser.parseOperation() 49 | g.Expect(err).ToNot(BeNil(), fmt.Sprintf("failed case: `%s`", test)) 50 | } 51 | }) 52 | t.Run("parse operations correctly formatted succeeds", func(t *testing.T) { 53 | test := "name=max AND artifact%art1" 54 | parser := NewParser(test) 55 | expected := &Operation{ 56 | LeftNode: &Expression{ 57 | Field: "name", 58 | Comparator: "=", 59 | Value: "max", 60 | }, 61 | Gate: "AND", 62 | RightNode: &Expression{ 63 | Field: "artifact", 64 | Comparator: "%", 65 | Value: "art1", 66 | }, 67 | } 68 | node, err := parser.Parse() 69 | g.Expect(err).To(BeNil()) 70 | g.Expect(node).To(Equal(expected)) 71 | }) 72 | t.Run("parse operations correctly formatted succeeds", func(t *testing.T) { 73 | test := "(name=max AND artifact=art1) OR metric > 0.98" 74 | parser := NewParser(test) 75 | firstExpression := &Operation{ 76 | LeftNode: &Expression{ 77 | Field: "name", 78 | Comparator: "=", 79 | Value: "max", 80 | }, 81 | Gate: "AND", 82 | RightNode: &Expression{ 83 | Field: "artifact", 84 | Comparator: "=", 85 | Value: "art1", 86 | }, 87 | } 88 | secondExpression := &Operation{ 89 | LeftNode: firstExpression, 90 | Gate: "OR", 91 | RightNode: &Expression{ 92 | Field: "metric", 93 | Comparator: ">", 94 | Value: "0.98", 95 | }, 96 | } 97 | node, err := parser.Parse() 98 | g.Expect(err).To(BeNil()) 99 | g.Expect(node).To(Equal(secondExpression)) 100 | }) 101 | t.Run("parse operation when just expression succeeds", func(t *testing.T) { 102 | test := "name=max" 103 | parser := NewParser(test) 104 | node, err := parser.Parse() 105 | g.Expect(err).To(BeNil()) 106 | g.Expect(node).To(Equal(&Expression{ 107 | Field: "name", 108 | Comparator: "=", 109 | Value: "max", 110 | })) 111 | }) 112 | t.Run("parse operation when just bracketed expression succeeds", func(t *testing.T) { 113 | test := "(name=max)" 114 | parser := NewParser(test) 115 | node, err := parser.Parse() 116 | g.Expect(err).To(BeNil()) 117 | g.Expect(node).To(Equal(&Expression{ 118 | Field: "name", 119 | Comparator: "=", 120 | Value: "max", 121 | })) 122 | }) 123 | t.Run("parse operation with metrics/tags format", func(t *testing.T) { 124 | test := "(metrics[metric-name_1]>= 0.98)" 125 | parser := NewParser(test) 126 | node, err := parser.Parse() 127 | g.Expect(err).To(BeNil()) 128 | g.Expect(node).To(Equal(&Expression{ 129 | Field: "metrics[metric-name_1]", 130 | Comparator: ">=", 131 | Value: "0.98", 132 | })) 133 | }) 134 | t.Run("parse operation camelCase", func(t *testing.T) { 135 | test := "TaskType=classification" 136 | parser := NewParser(test) 137 | node, err := parser.Parse() 138 | g.Expect(err).To(BeNil()) 139 | g.Expect(node).To(Equal(&Expression{ 140 | Field: "TaskType", 141 | Comparator: "=", 142 | Value: "classification", 143 | })) 144 | }) 145 | t.Run("parse operation quoted string", func(t *testing.T) { 146 | test := "(Name=\"Iris Classifier\")" 147 | parser := NewParser(test) 148 | node, err := parser.Parse() 149 | g.Expect(err).To(BeNil()) 150 | g.Expect(node).To(Equal(&Expression{ 151 | Field: "Name", 152 | Comparator: "=", 153 | Value: "Iris Classifier", 154 | })) 155 | }) 156 | t.Run("parse empty quoted string", func(t *testing.T) { 157 | test := "(name=\"\" AND artifact=art1) OR metric > 0.98" 158 | parser := NewParser(test) 159 | firstExpression := &Operation{ 160 | LeftNode: &Expression{ 161 | Field: "name", 162 | Comparator: "=", 163 | Value: "", 164 | }, 165 | Gate: "AND", 166 | RightNode: &Expression{ 167 | Field: "artifact", 168 | Comparator: "=", 169 | Value: "art1", 170 | }, 171 | } 172 | secondExpression := &Operation{ 173 | LeftNode: firstExpression, 174 | Gate: "OR", 175 | RightNode: &Expression{ 176 | Field: "metric", 177 | Comparator: ">", 178 | Value: "0.98", 179 | }, 180 | } 181 | node, err := parser.Parse() 182 | g.Expect(err).To(BeNil()) 183 | g.Expect(node).To(Equal(secondExpression)) 184 | }) 185 | t.Run("parsing OR/AND is case insensitive", func(t *testing.T) { 186 | test := "name=model1 AND version=2.0" 187 | parser := NewParser(test) 188 | node, err := parser.Parse() 189 | g.Expect(err).To(BeNil()) 190 | g.Expect(node.(*Operation).Gate).To(Equal("AND")) 191 | 192 | test = "name=model1 and version=2.0" 193 | parser = NewParser(test) 194 | node, err = parser.Parse() 195 | g.Expect(err).To(BeNil()) 196 | g.Expect(node.(*Operation).Gate).To(Equal("AND")) 197 | 198 | test = "name=model1 OR version=2.0" 199 | parser = NewParser(test) 200 | node, err = parser.Parse() 201 | g.Expect(err).To(BeNil()) 202 | g.Expect(node.(*Operation).Gate).To(Equal("OR")) 203 | 204 | test = "name=model1 or version=2.0" 205 | parser = NewParser(test) 206 | node, err = parser.Parse() 207 | g.Expect(err).To(BeNil()) 208 | g.Expect(node.(*Operation).Gate).To(Equal("OR")) 209 | }) 210 | t.Run("parser fails for invalid queries with missing comparator", func(t *testing.T) { 211 | test := "name" 212 | parser := NewParser(test) 213 | _, err := parser.Parse() 214 | g.Expect(err).ToNot(BeNil()) 215 | 216 | test = "name=default OR age" 217 | parser = NewParser(test) 218 | _, err = parser.Parse() 219 | g.Expect(err).ToNot(BeNil()) 220 | 221 | test = "" 222 | parser = NewParser(test) 223 | _, err = parser.Parse() 224 | g.Expect(err).ToNot(BeNil()) 225 | }) 226 | t.Run("parser fails for invalid queries with open gate", func(t *testing.T) { 227 | test := "name=default AND" 228 | parser := NewParser(test) 229 | _, err := parser.Parse() 230 | g.Expect(err).ToNot(BeNil()) 231 | }) 232 | } 233 | 234 | func FuzzParser(f *testing.F) { 235 | testcases := []string{">=!=", "name=default OR age", "< <= = != AND OR and or", "1 != \"2\"", "(Name=\"Iris Classifier\")"} 236 | for _, tc := range testcases { 237 | f.Add(tc) 238 | } 239 | 240 | f.Fuzz(func(t *testing.T, s string) { 241 | parser := NewParser(s) 242 | node, err := parser.Parse() 243 | if err == nil { 244 | _, nodeIsOp := node.(*Operation) 245 | _, nodeIsExpr := node.(*Expression) 246 | if !nodeIsOp && !nodeIsExpr { 247 | t.Errorf("node must be either op or expression") 248 | } 249 | } 250 | }) 251 | } 252 | -------------------------------------------------------------------------------- /parser/stack.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // TokenStack is used as the buffer for the Parser. 8 | type TokenStack struct { 9 | stack []TokenInfo 10 | } 11 | 12 | // Push pushes a token to the TokenStack. 13 | func (s *TokenStack) Push(v TokenInfo) { 14 | s.stack = append(s.stack, v) 15 | } 16 | 17 | // Pop removes and returns a token from the TokenStack. 18 | func (s *TokenStack) Pop() (TokenInfo, error) { 19 | if len(s.stack) == 0 { 20 | return TokenInfo{}, errors.New("stack is empty") 21 | } 22 | l := len(s.stack) 23 | token := s.stack[l-1] 24 | s.stack = s.stack[:l-1] 25 | return token, nil 26 | } 27 | 28 | // Len returns the current length of the TokenStack. 29 | func (s *TokenStack) Len() int { 30 | return len(s.stack) 31 | } 32 | -------------------------------------------------------------------------------- /parser/stack_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | func TestTokenStack(t *testing.T) { 10 | g := NewGomegaWithT(t) 11 | t.Run("test token stack", func(t *testing.T) { 12 | stack := TokenStack{ 13 | stack: []TokenInfo{}, 14 | } 15 | stack.Push(TokenInfo{ 16 | Token: EQUAL, 17 | Literal: "=", 18 | }) 19 | length := stack.Len() 20 | g.Expect(length).To(Equal(1)) 21 | tok, err := stack.Pop() 22 | g.Expect(err).To(BeNil()) 23 | g.Expect(tok).To(Equal(TokenInfo{ 24 | Token: EQUAL, 25 | Literal: "=", 26 | })) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /parser/token.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // Token represents a lexical token. 4 | type Token int 5 | 6 | // TokenInfo stores relevant information about the token during scanning. 7 | type TokenInfo struct { 8 | Token Token 9 | Literal string 10 | } 11 | 12 | // TokenLookup is a map, useful for printing readable names of the tokens. 13 | var TokenLookup = map[Token]string{ 14 | OTHER: "OTHER", 15 | EOF: "EOF", 16 | WS: "WS", 17 | STRING: "STRING", 18 | EQUAL: "EQUAL", 19 | GREATER_THAN: "GREATER THAN", 20 | GREATHER_THAN_EQUAL: "GREATER THAN OR EQUAL", 21 | LESS_THAN: "LESS THAN", 22 | LESS_THAN_EQUAL: "LESS THAN OR EQUAL", 23 | NOT_EQUAL: "NOT EQUAL", 24 | AND: "AND", 25 | OR: "OR", 26 | OPEN_BRACKET: "(", 27 | CLOSED_BRACKET: ")", 28 | PERCENT: "%", 29 | } 30 | 31 | // String prints a human readable string name for a given token. 32 | func (t Token) String() (print string) { 33 | return TokenLookup[t] 34 | } 35 | 36 | // Declare the tokens here. 37 | const ( 38 | // Special tokens 39 | // Iota simply starts and integer count 40 | OTHER Token = iota 41 | EOF 42 | WS 43 | 44 | // Main literals 45 | STRING 46 | 47 | // Brackets 48 | OPEN_BRACKET 49 | CLOSED_BRACKET 50 | 51 | // Special characters 52 | GREATER_THAN 53 | GREATHER_THAN_EQUAL 54 | LESS_THAN 55 | LESS_THAN_EQUAL 56 | EQUAL 57 | NOT_EQUAL 58 | PERCENT 59 | 60 | // Keywords 61 | AND 62 | OR 63 | ) 64 | -------------------------------------------------------------------------------- /sql_adaptor/adaptor.go: -------------------------------------------------------------------------------- 1 | // Package sql_adaptor provides functions to convert a goven query to a valid and safe SQL query. 2 | package sql_adaptor 3 | 4 | import ( 5 | "reflect" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // NewDefaultAdaptorFromStruct returns a new basic SqlAdaptor from the reflection of your database object. 11 | func NewDefaultAdaptorFromStruct(gorm reflect.Value) *SqlAdaptor { 12 | matchers := map[*regexp.Regexp]ParseValidateFunc{} 13 | fieldMappings := map[string]string{} 14 | defaultFields := FieldParseValidatorFromStruct(gorm) 15 | return NewSqlAdaptor(fieldMappings, defaultFields, matchers) 16 | } 17 | 18 | // FieldParseValidatorFromStruct takes the reflection of your database object and returns a map of fieldnames to ParseValidateFuncs. 19 | // Don't panic - reflection is only used once on initialisation. 20 | func FieldParseValidatorFromStruct(gorm reflect.Value) map[string]ParseValidateFunc { 21 | defaultFields := map[string]ParseValidateFunc{} 22 | e := gorm.Elem() 23 | 24 | for i := 0; i < e.NumField(); i++ { 25 | varName := strings.ToLower(e.Type().Field(i).Name) 26 | varType := e.Type().Field(i).Type 27 | vType := strings.TrimPrefix(varType.String(), "*") 28 | 29 | switch vType { 30 | case "float32", "float64": 31 | defaultFields[varName] = DefaultMatcherWithValidator(NumericValidator) 32 | case "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64": 33 | defaultFields[varName] = DefaultMatcherWithValidator(IntegerValidator) 34 | default: 35 | defaultFields[varName] = DefaultMatcherWithValidator(NullValidator) 36 | } 37 | } 38 | return defaultFields 39 | } 40 | -------------------------------------------------------------------------------- /sql_adaptor/sql.go: -------------------------------------------------------------------------------- 1 | package sql_adaptor 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/seldonio/goven/parser" 10 | 11 | "github.com/iancoleman/strcase" 12 | ) 13 | 14 | type ( 15 | // ValidatorFunc takes a field name and validates that it is a legal/correct format. 16 | ValidatorFunc = func(s string) error 17 | // ParseValidateFunc takes an Expression from the AST and returns a templated SQL query. 18 | ParseValidateFunc = func(ex *parser.Expression) (*SqlResponse, error) 19 | ) 20 | 21 | // SqlResponse is an object that stores the raw query, and the values to interpolate. 22 | type SqlResponse struct { 23 | Raw string 24 | Values []string 25 | } 26 | 27 | // SqlAdaptor represents the adaptor tailored to your database schema. 28 | type SqlAdaptor struct { 29 | // fieldMappings (currently unimplemented) is used to provide ability to map different frontend to backend field names. 30 | fieldMappings map[string]string 31 | // defaultFields is the default field matcher, used when a regex isn't matched. 32 | defaultFields map[string]ParseValidateFunc 33 | // Non default matchers, these are custom matchers used to extend goven's functionality. 34 | matchers map[*regexp.Regexp]ParseValidateFunc 35 | } 36 | 37 | // NewSqlAdaptor returns a SqlAdaptor populated with the provided arguments. 38 | func NewSqlAdaptor(fieldMappings map[string]string, defaultFields map[string]ParseValidateFunc, matchers map[*regexp.Regexp]ParseValidateFunc) *SqlAdaptor { 39 | if fieldMappings == nil { 40 | fieldMappings = map[string]string{} 41 | } 42 | if defaultFields == nil { 43 | defaultFields = map[string]ParseValidateFunc{} 44 | } 45 | if matchers == nil { 46 | matchers = map[*regexp.Regexp]ParseValidateFunc{} 47 | } 48 | sa := SqlAdaptor{ 49 | fieldMappings: fieldMappings, 50 | defaultFields: defaultFields, 51 | matchers: matchers, 52 | } 53 | return &sa 54 | } 55 | 56 | // Parse takes a string goven query and returns a SqlResponse that can be executed against your database. 57 | func (s *SqlAdaptor) Parse(str string) (*SqlResponse, error) { 58 | newParser := parser.NewParser(str) 59 | node, err := newParser.Parse() 60 | if err != nil { 61 | return nil, errors.New("query could not be parsed") 62 | } 63 | return s.parseNodeToSQL(node) 64 | } 65 | 66 | func (s *SqlAdaptor) parseNodeToSQL(node parser.Node) (*SqlResponse, error) { 67 | sq := SqlResponse{} 68 | if node == nil { 69 | return &sq, nil 70 | } 71 | if node.Type() == parser.EXPRESSION { 72 | ex, ok := node.(*parser.Expression) 73 | if !ok { 74 | return nil, errors.New("failed to parse query correctly") 75 | } 76 | // Try and match any custom matchers. 77 | for k, v := range s.matchers { 78 | if k.MatchString(ex.Field) { 79 | return v(ex) 80 | } 81 | } 82 | // If that doesn't happen, then use the relevant default matcher. 83 | lowerCamelCase := strings.ToLower(strcase.ToCamel(ex.Field)) 84 | if val, ok := s.defaultFields[lowerCamelCase]; ok { 85 | return val(ex) 86 | } else { 87 | // Field is not valid because it must match either a custom regex, or have a validator. 88 | // If it does neither then we do not expect this field name. 89 | return nil, fmt.Errorf("field '%s' is not valid", lowerCamelCase) 90 | } 91 | } 92 | op, ok := node.(*parser.Operation) 93 | if !ok { 94 | return nil, errors.New("failed to parse query correctly") 95 | } 96 | left, err := s.parseNodeToSQL(op.LeftNode) 97 | if err != nil { 98 | return nil, err 99 | } 100 | // Don't want to have unwanted whitespace if no gate. 101 | if op.Gate == "" { 102 | sq = SqlResponse{ 103 | Raw: fmt.Sprintf("(%s)", left.Raw), 104 | Values: left.Values, 105 | } 106 | return &sq, nil 107 | } 108 | right, err := s.parseNodeToSQL(op.RightNode) 109 | if err != nil { 110 | return nil, err 111 | } 112 | sq = SqlResponse{ 113 | Raw: fmt.Sprintf("(%s %s %s)", left.Raw, op.Gate, right.Raw), 114 | Values: append(left.Values, right.Values...), 115 | } 116 | return &sq, nil 117 | } 118 | 119 | // StringSliceToInterfaceSlice is a helper function for making gorm queries. 120 | func StringSliceToInterfaceSlice(slice []string) []interface{} { 121 | interSlice := []interface{}{} 122 | for _, val := range slice { 123 | interSlice = append(interSlice, val) 124 | } 125 | return interSlice 126 | } 127 | -------------------------------------------------------------------------------- /sql_adaptor/sql_test.go: -------------------------------------------------------------------------------- 1 | package sql_adaptor_test 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | "time" 9 | 10 | "github.com/seldonio/goven/sql_adaptor" 11 | 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | type TestCase struct { 16 | test string 17 | expectedRaw string 18 | expectedValues []string 19 | } 20 | 21 | // Typical gorm database struct - a User 22 | type ExampleDBStruct struct { 23 | ID uint 24 | Name string 25 | Email *string 26 | Age uint8 27 | Birthday *time.Time 28 | MemberNumber sql.NullString 29 | ActivatedAt sql.NullTime 30 | CreatedAt time.Time 31 | UpdatedAt time.Time 32 | } 33 | 34 | func TestSqlAdaptor(t *testing.T) { 35 | g := NewGomegaWithT(t) 36 | t.Run("test sql adaptor success", func(t *testing.T) { 37 | testCases := []TestCase{ 38 | { 39 | test: "(name=max AND email=bob-dylan@aol.com) OR age > 1", 40 | expectedRaw: "((name=? AND email=?) OR age>?)", 41 | expectedValues: []string{"max", "bob-dylan@aol.com", "1"}, 42 | }, 43 | // Test for an empty quoted string. 44 | { 45 | test: "(name=\"\" AND email=bob-dylan@aol.com) OR age > 1", 46 | expectedRaw: "((name=? AND email=?) OR age>?)", 47 | expectedValues: []string{"", "bob-dylan@aol.com", "1"}, 48 | }, 49 | { 50 | test: "(name%max AND email=bob-dylan@aol.com) OR age > 1", 51 | expectedRaw: "((name LIKE ? AND email=?) OR age>?)", 52 | expectedValues: []string{"%max%", "bob-dylan@aol.com", "1"}, 53 | }, 54 | } 55 | for _, testCase := range testCases { 56 | sa := sql_adaptor.NewDefaultAdaptorFromStruct(reflect.ValueOf(&ExampleDBStruct{})) 57 | response, err := sa.Parse(testCase.test) 58 | g.Expect(err).To(BeNil(), fmt.Sprintf("failed case: %s", testCase.test)) 59 | g.Expect(response.Raw).To(Equal(testCase.expectedRaw), fmt.Sprintf("failed case raw: %s", testCase.test)) 60 | g.Expect(response.Values).To(Equal(testCase.expectedValues), fmt.Sprintf("failed case values: %s", testCase.test)) 61 | } 62 | }) 63 | t.Run("test sql adaptor failure", func(t *testing.T) { 64 | testCases := []TestCase{ 65 | { 66 | test: "(name=max AND invalidField=wow) OR age > 1", 67 | }, 68 | { 69 | test: "id = wow", 70 | }, 71 | { 72 | test: "age = wow", 73 | }, 74 | { 75 | test: "name = default AND", 76 | }, 77 | { 78 | test: "name", 79 | }, 80 | { 81 | test: "name = default AND age", 82 | }, 83 | } 84 | for _, testCase := range testCases { 85 | sa := sql_adaptor.NewDefaultAdaptorFromStruct(reflect.ValueOf(&ExampleDBStruct{})) 86 | _, err := sa.Parse(testCase.test) 87 | g.Expect(err).ToNot(BeNil(), fmt.Sprintf("failed case: %s", testCase.test)) 88 | } 89 | }) 90 | t.Run("test FieldParseValidatorFromStruct", func(t *testing.T) { 91 | defaultFields := sql_adaptor.FieldParseValidatorFromStruct(reflect.ValueOf(&ExampleDBStruct{})) 92 | _, ok := defaultFields["membernumber"] 93 | g.Expect(ok).To(Equal(true)) 94 | }) 95 | } 96 | 97 | func FuzzSqlAdaptor(f *testing.F) { 98 | testcases := []string{"(name=max AND invalidField=wow) OR age > 1", "(name=max AND email=bob-dylan@aol.com) OR age > 1", "id = wow", "(name%max AND email=\"bob-dylan@aol.com\") OR age > 1"} 99 | for _, tc := range testcases { 100 | f.Add(tc) 101 | } 102 | 103 | f.Fuzz(func(t *testing.T, s string) { 104 | sa := sql_adaptor.NewDefaultAdaptorFromStruct(reflect.ValueOf(&ExampleDBStruct{})) 105 | response, err := sa.Parse(s) 106 | if err != nil && response != nil { 107 | t.Errorf("expected nil response when err is not nil") 108 | } 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /sql_adaptor/validators.go: -------------------------------------------------------------------------------- 1 | package sql_adaptor 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/seldonio/goven/parser" 8 | ) 9 | 10 | // DefaultMatcherWithValidator wraps the default matcher with validation on the value. 11 | func DefaultMatcherWithValidator(validate ValidatorFunc) ParseValidateFunc { 12 | return func(ex *parser.Expression) (*SqlResponse, error) { 13 | err := validate(ex.Value) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return DefaultMatcher(ex), nil 18 | } 19 | } 20 | 21 | // DefaultMatcher takes an expression and spits out the default SqlResponse. 22 | func DefaultMatcher(ex *parser.Expression) *SqlResponse { 23 | if ex.Comparator == parser.TokenLookup[parser.PERCENT] { 24 | fmtValue := fmt.Sprintf("%%%s%%", ex.Value) 25 | sq := SqlResponse{ 26 | Raw: fmt.Sprintf("%s LIKE ?", ex.Field), 27 | Values: []string{fmtValue}, 28 | } 29 | return &sq 30 | } 31 | sq := SqlResponse{ 32 | Raw: fmt.Sprintf("%s%s?", ex.Field, ex.Comparator), 33 | Values: []string{ex.Value}, 34 | } 35 | return &sq 36 | } 37 | 38 | // NullValidator is a no-op validator on a string, always returns nil error. 39 | func NullValidator(_ string) error { 40 | return nil 41 | } 42 | 43 | // IntegerValidator validates that the input is an integer. 44 | func IntegerValidator(s string) error { 45 | _, err := strconv.Atoi(s) 46 | if err != nil { 47 | return fmt.Errorf("value '%s' is not an integer", s) 48 | } 49 | return nil 50 | } 51 | 52 | // NumericValidator validates that the input is a number. 53 | func NumericValidator(s string) error { 54 | _, err := strconv.ParseFloat(s, 64) 55 | if err != nil { 56 | return fmt.Errorf("value '%s' is not numeric", s) 57 | } 58 | return nil 59 | } 60 | --------------------------------------------------------------------------------