pax_global_header00006660000000000000000000000064136731637060014526gustar00rootroot0000000000000052 comment=44d13a198576d06f96cbda193b772f3b7be12d4f assertly-0.5.4/000077500000000000000000000000001367316370600134025ustar00rootroot00000000000000assertly-0.5.4/CHANGELOG.md000077500000000000000000000031221367316370600152140ustar00rootroot00000000000000## Jan 18 2020 - v0.5.2 * modify @length directive to support arbitrary path ## April 23 2019 - v0.4.8 * patched time validation with custom time format * patched field level time layout ## April 23 2019 - v0.4.6 * Updated keysValue for selector returing a map ## April 2 2019 - v0.4.5 * Updated multi line JSON handling ## Feb 23 2019 - v0.4.4 * Added limit to available keys ## Feb 12 2019 - v0.4.3 * Patched @numericPrecisionPoint@ with zero value ## Feb 10 2019 - v0.4.2 * Added empty value provider: ## Jan 30 2019 - v0.4.1 * Patch assertPath directive * Remove cast error login, in case of error original value is used ## Nov 30 2018 - v0.3.0 * Added @length@ directive * Patched map level directives ## Nov 30 2018 - v0.3.0 * Added assertPath directive ## Nov 30 2018 - v0.2.2 * Update assertSlice logic to account for key/value pairs as map validation * Added keyCaseSensitive directive * Change behaviour to caseSensitive directive to only apply to values * Added coalesceWithZero directive, patched 0 | 0.0 with nil validation ## Nov 26 2018 - v0.2.1 * Added numericPrecisionPoint directive ## Nov 26 2018 - v0.2.0 * Added numericPrecisionPoint directive ## Nov 25 2018 - v0.1.1 * Patch nil pointer issue on assertTime * Added int check reporting if applicable in assertFloat ## Nov 5 2018 - v0.1.0 * Enhanced apply time directory to only non function based values * Introduced AssertValuesWithContext helper validation method with context ## Jan 17 2018 (Alpha) * Initial Release. assertly-0.5.4/LICENSE000077500000000000000000000264301367316370600144170ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2012-2016 Viant. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. assertly-0.5.4/NOTICE000077500000000000000000000002351367316370600143110ustar00rootroot00000000000000Viant, inc. Assertly (Data structure testing library) Copyright 2012-2018 Viant This product includes software developed at Viant (http://viantinc.com/)assertly-0.5.4/README.md000066400000000000000000000410241367316370600146620ustar00rootroot00000000000000# Data structure testing library (assertly) [![Data structure testing library for Go.](https://goreportcard.com/badge/github.com/viant/assertly)](https://goreportcard.com/report/github.com/viant/assertly) [![GoDoc](https://godoc.org/github.com/viant/assertly?status.svg)](https://godoc.org/github.com/viant/assertly) This library is compatible with Go 1.10+ Please refer to [`CHANGELOG.md`](CHANGELOG.md) if you encounter breaking changes. - [Introduction](#Introduction) - [Motivation](#Motivation) - [Usage](#Usage) - [Validation](#Validation) - [Directive](#Directive) - [Macros](#Macros) - [External Resource](#external) - [License](#License) - [Credits and Acknowledgements](#Credits-and-Acknowledgements) ## Introduction This library enables complex data structure testing, specifically: 1. Realtime transformation or casting of incompatible data types with directives system. 2. Consistent way of testing of unordered structures. 3. Contains, Range, RegExp support on any data structure deeph level. 4. Switch case directive to provide expected value alternatives based on actual switch/case input match. 5. Macro system enabling complex predicate and expression evaluation, and customization. ## Motivation This library has been created as a way to unify original testing approaches introduced to [dsunit](https://github.com/viant/dsunit) and [endly](https://github.com/viant/endly) ## Usage **Complete data validation with concrete types** ```go import( "github.com/stretchr/testify/assert" "github.com/viant/assertly" ) func Test_XX(t *testing.T) { var actualRecords []*User = //get actual var expectedRecords []*User = //get expected assertly.AssertValues(t, expectedRecords, actualRecords) //or with custom path and testing.T integration validation, err := assertly.Assert(expected, actual, assertly.NewDataPath("/")) assert.EqualValues(t, 0, validation.FailedCount, validation.Report()) } ``` **Partial data validation with directive and reg expression** ```go func Test_XX(t *testing.T) { var actualConfig = &Config{ Endpoint: &Endpoint{ Port: 8080, TimeoutMs: 2000, }, LogTypes: map[string]*LogType{ "type1": &LogType{ Locations:[]*Location{ { URL:"file:///data/log/type1", }, }, MaxQueueSize: 2048, QueueFlashCount: 1024, FlushFrequencyInMs: 500, }, "type2": &LogType{ Locations:[]*Location{ { URL:"file:///data/log/type2", }, }, MaxQueueSize: 4096, QueueFlashCount: 2048, FlushFrequencyInMs: 1000, }, }, } var expectedConfig = expected: `{ "Endpoint": { "Port": 8080, "TimeoutMs": 2000 }, "LogTypes": { "type1": { "Locations":[ { "URL":"~/type1/" } ], "MaxQueueSize": 2048, "QueueFlashCount": 1024, "FlushFrequencyInMs": 500 }, "@exists@type2": true } }`, assertly.AssertValues(t, expectedConfig, actualConfig) } ``` - reg expression: "URL":"~/type1/" - directive: @exists@ **Validation with custom macro value provider** ```go type fooProvider struct{} func (*fooProvider) Get(context toolbox.Context, arguments ...interface{}) (interface{}, error) { var args = []string{} for _, arg := range arguments { args = append(args, toolbox.AsString(arg)) } return fmt.Sprintf("foo{%v}", strings.Join(args, ",")), nil } func Test_XX(t *testing.T) { ctx := NewDefaultContext() var provider toolbox.ValueProvider = &fooProvider{} ctx.Evaluator.ValueProviderRegistry.Register("foo", provider) var actual = map[string]string{ "k1":"v1", "k2":"Macro test: foo{1,abc} !", } var expected = map[string]string{ "k1":"v1", "k2":"Macro test: !", } AssertValuesWithContext(ctx, t, expected, actual) } ``` **Validation with custom predicate** ```go type rangePredicate struct { min int max int actual int err error } func (p *rangePredicate) String() string { return fmt.Sprintf("min: %v, max: %v, actual: %v, err: %v", p.min, p.max, p.actual, p.err) } func (p *rangePredicate) Apply(value interface{}) bool { p.actual, p.err = toolbox.ToInt(value) return p.actual >= p.min && p.actual <= p.max } type inRangePredicateProvider struct{} func (*inRangePredicateProvider) Get(context toolbox.Context, arguments ...interface{}) (interface{}, error) { if len(arguments) != 2 { return nil, fmt.Errorf("expected 2 arguments (min, max) but had: %v", len(arguments)) } min, err := toolbox.ToInt(arguments[0]) if err != nil { return nil, fmt.Errorf("invalid min %v", err) } max, err := toolbox.ToInt(arguments[1]) if err != nil { return nil, fmt.Errorf("invalid min %v", err) } var predicate toolbox.Predicate = &rangePredicate{min:min, max: max} return &predicate, nil } func Test_XX(t *testing.T) { ctx := NewDefaultContext() var provider toolbox.ValueProvider = &inRangePredicateProvider{} ctx.Evaluator.ValueProviderRegistry.Register("inRange", provider) var actual = map[string]int{ "k1":1, "k2":3, } var expected = map[string]string{ "k1":"1", "k2":"", } AssertValuesWithContext(ctx, t, expected, actual) } ``` ## Validation Validation rules: 1) JSON textual data is converted into data structure 2) New Line Delimited JSON is converted into data structure collection. 3) Object/Struct is converted into data structure 4) Only existing keys/fields in expected data structure are validated 5) Only existing items in the array/slice are validated 6) Directive and macros/predicate provide validation extension 7) The following expression can be used on any data structure level: | Assertion Type | input | expected expression | example | | --- | --- | --- | --- | | equal | actual | expected | a:a | | not equal | actual | !expected | a:!b | | contains | actual | /expected/| abcd:/bc/| | not contains | actual | !/expected/| abcd:!/xc/ | | regExpr | actual | ~/expected/ | 1234a:/\d+/ | | not regExpr | actual | !~/expected/ | 1234:!/\w/ | | between | actual | /[minExpected..maxExpected]/ | 12:/[1..13]/ | | exists | n/a | { "key": "@exists@" } | | | not exists | n/a | { "key": "@!exists@" } | | **example**: ```go func Test_XX(t *testing.T) { var expected = ` { "Meta": "abc", "Table": "/table_/", "Rows": [ { "id": 1, "name": "~/name (\\d+)/", "@exists@":"dob" }, { "id": 2, "name": "name 2", "settings": { "k1": "v2" } }, { "id": 2, "name": "name 2" } ] }`, var actual = ` { "Table": "table_xx", "Rows": [ { "id": 1, "name": "name 12", "dob":"2018-01-01" }, { "id": 2, "name": "name 2", "settings": { "k1": "v20" } }, { "id": 4, "name": "name 2" } ] }`, validation, err := assertly.Assert(expected, actual, assertly.NewDataPath("/")) assert.EqualValues(t, 0, validation.FailedCount, validation.Report()) } ``` ## Directive Directive is piece of information instructing validator to either convert data just before validation takes place or to validate a date according to provided rules. - KeyExistsDirective = "@exists@" - KeyDoesNotExistsDirective = "@!exists@" - TimeFormatDirective = "@timeFormat@" - TimeLayoutDirective = "@timeLayout@" - SwitchByDirective = "@switchCaseBy@" - CastDataTypeDirective = "@cast@" - IndexByDirective = "@indexBy@" - CaseSensitiveDirective = "@caseSensitive@" - KeyCaseSensitiveDirective = "@CaseSensitive@" - NumericPrecisionPointDirective = "@numericPrecisionPoint@" - CoalesceWithZeroDirective = "@coalesceWithZero@" - AssertPathDirective = "@assertPath@" - LengthDirective = "@length@" - StrictMapCheckDirective = "@strictMapCheck@" ## Assert Path **@assertPath@** directive allows validation only specified path within given node, the following construct can be used: - _directive prefixed_ ```json { "@assertPath@Responses[0].Code":200, "@assertPath@Responses[1].Code":200 } ``` - _directive with subpath and values map_ ```json { "@assertPath@":{ "Responses[0].Code":200, "Responses[1].Code":200 } } ``` - _directive with the same data point validation_ ```json { "@assertPath@":[ { "Responses[0].Code":200, "Responses[0].Body":"/some fragment/" }, { "Responses[0].Body":"~/.+\\d{3}.+/" } ] } ``` ### Index by **@indexBy@** - index by directive indexes a slice for validation process, specifically. 1) Two unordered array/slice/collection that can be index by a unique fields 2) A map with a actual array/slice/collection that can be ordered by unique fields **Example 1** \#expected ```json { "@indexBy@":"id", "1" :{"id":1, "name":"name1"}, "2" :{"id":2, "name":"name2"} } ``` \#actual ```json [ {"id":1, "name":"name1"}, {"id":2, "name":"name2"} ] ``` **Example 2** \#expected ```json {"@indexBy@":"id"} {"id":1, "name":"name1"} {"id":2, "name":"name2"} ``` \#actual ```json {"id":1, "name":"name1"} {"id":2, "name":"name2"} ``` **Example 3** \#expected ```json {"@indexBy@":"request.id"} {"request":{"id":1111, "name":"name1"}, "ts":189321233} {"request":{"id":2222, "name":"name2"}, "ts":189321235} ``` \#actual ```json {"request":{"id":2222, "name":"name2"}, "ts":189321235} {"request":{"id":1111, "name":"name1"}, "ts":189321233} ``` ## Switch/case **@switchCaseBy@** - switch directive instructs a validator to select matching expected subset based on some actual value. . For non deterministic system there could be various alternative output for the same input. **Example 1** \#expected ```json [ { "@switchCaseBy@":["experimentID"] }, { "1":{"experimentID":1, "seq":1, "outcome":[1.53,7.42,6.34]}, "2":{"experimentID":2, "seq":1, "outcome":[3.53,6.32,3.34]} }, { "1":{"experimentID":1, "seq":2, "outcome":[5.63,4.3]}, "2":{"experimentID":1, "seq":2, "outcome":[3.65,3.2]} } ] ``` \#actual ```json {"experimentID":1, "seq":1, "outcome":[1.53,7.42,6.34]} {"experimentID":1, "seq":2, "outcome":[5.63,4.3]} ``` **Example 2** \#expected ```json [ { "@switchCaseBy@":["experimentID"] }, { "1":{"experimentID":1, "seq":1, "outcome":[1.53,7.42,6.34]}, "2":{"experimentID":2, "seq":1, "outcome":[3.53,6.32,3.34]}, "shared": {"k1":"v1", "k2":"v2"} }, { "1":{"experimentID":1, "seq":2, "outcome":[5.63,4.3]}, "2":{"experimentID":1, "seq":2, "outcome":[3.65,3.2]}, "shared": {"k1":"v10", "k2":"v20"} } ] ``` \#actual ```json {"experimentID":1, "seq":1, "outcome":[1.53,7.42,6.34], "k1":"v1", "k2":"v2"} {"experimentID":1, "seq":2, "outcome":[5.63,4.3], "k1":"v10", "k2":"v20"} ``` ## Time format @timeFormat@ - time format directive instructs a validator to convert data into time with specified time format before actual validation takes place. Time format is expressed in java style date format. **Example** \#expected ```go expected := map[string]interface{}{ "@timeFormat@date": "yyyy-MM-dd", "@timeFormat@ts": "yyyy-MM-dd hh:mm:ss" "@timeFormat@" "yyyy-MM-dd hh:mm:ss" //default time format "id":123, "date": "2019-01-01", "ts": "2019-01-01 12:00:01", } ``` \#actual ```go expected := map[string]interface{}{ "id":123, "date": "2019-01-01 12:00:01",, "ts": "2019-01-01 12:00:01", } ``` ## Time layout @timeLayout@ - time format directive instructs a validator to convert data into time with specified time format before actual validation takes place. Time layout uses golang time layout. **Example** \#expected ```go expected := map[string]interface{}{ "@timeFormat@date": "yyyy-MM-dd", "@timeFormat@ts": "yyyy-MM-dd hh:mm:ss" "@timeFormat@" "yyyy-MM-dd hh:mm:ss" //default time format "id":123, "date": "2019-01-01", "ts": "2019-01-01 12:00:01", } ``` \#actual ```go expected := map[string]interface{}{ "id":123, "date": "2019-01-01 12:00:01",, "ts": "2019-01-01 12:00:01", } ``` ## Cast data type @cast@ - instruct a validator to convert data to the specified data type before actual validation takes place. Supported data type casting: * int * float * boolean **Example** \#expected ```json [ { "@cast@field1":"float","@cast@field2":"int" }, { "field1":2.3, "field2":123 }, { "field1":6.3, "field2":551 } ] ``` \#actual ```json {"field1":"2.3","field2":"123"} {"field1":"6.3","field2":"551"} ``` ## KeyCaseSensitiveDirective By default map key match is case sensitive, directive allows to disable that behaviours. ## CaseSensitiveDirective By default text value match is case sensitive, directive allows to disable that behaviours. ## NumericPrecisionPoint NumericPrecisionPoint controls numeric precision validation comparision **Example** \#expected ```json [ { "@numericPrecisionPoint@":"7" }, { "field1":0.006521405, "field2":123 }, { "field1":0.006521408, "field2":551 } ] ``` \#actual ```json [ { "field1":0.0065214, "field2":123 }, { "field1":0.0065214, "field2":551 } ] ``` ## CoalesceWithZero Coalesce with zero directive sets all nil numeric values to zero ## Length Directive Checks length or map or slice **Example** \#expected ```json { "@length@k1":3 } ``` \#actual ```json { "k1":[1,2,3] } ``` ## Source directive Source directive is helper directive providing additional information about data point source, i.e. file.json#L113 ## Macro and predicates The macro is an expression with parameters that expands original text value. The general format of macro: <ds:MACRO_NAME [json formated array of parameters]> The following macro are build-in: | Name | Parameters | Description | Example | | --- | --- | --- | --- | | env | name env variable| Returns value env variable| <ds:env["user"]> | | nil |n/a| Returns nil value| <ds:nil> | | cast | type name| Returns value env variable| <ds:cast["int", "123"]> | | current_timestamp | n/a | Returns time.Now() | <ds:current_timestamp> | | dob | user age, month, day, format(yyyy-MM-dd as default) | Returns Date Of Birth| <ds:dob> | ## Predicates Predicate allows expected value to be evaluated with actual data using custom predicate logic. | Name | Parameters | Description | Example | | --- | --- | --- | --- | | between | from, to values | Evaluate actual value with between predicate | <ds:between[1.888889, 1.88889]> | | within_sec | base time, delta, optional date format | Evaluate if actual time is within delta of the base time | <ds:within_sec["now", 6, "yyyyMMdd HH:mm:ss"]> | **Example** ```go expected := `` actual := 3 ``` ```go expected := `13`, actual := fmt.Sprintf("1%v3", os.Getenv("USER")) ``` ```go expected := `` actual := 2015-06-03 ``` ```go expected := `` actual := 2015-06-03 ``` ```go expected := `` actual := 2015 ``` ```go expected := `` actual := 2015-09 ``` ```go expected := `` actual := 12-25 ``` ## External resource - [Advanced Data Testing with Go](https://www.youtube.com/watch?v=ESUJkSMS47k) - [Advanced Data Testing with Go Presentation](https://github.com/adrianwit/golang-data-testing/raw/master/advanced_data_testing.pptx) ## GoCover [![GoCover](https://gocover.io/github.com/viant/assertly)](https://gocover.io/github.com/viant/assertly) ## License The source code is made available under the terms of the Apache License, Version 2, as stated in the file `LICENSE`. Individual files may be made available under their own specific license, all compatible with Apache License, Version 2. Please see individual files for details. ## Credits and Acknowledgements **Library Author:** Adrian Witas assertly-0.5.4/assert.go000066400000000000000000000026501367316370600152350ustar00rootroot00000000000000package assertly import ( "fmt" "testing" ) //AssertValues validates expected against actual data structure func AssertValues(t *testing.T, expected, actual interface{}, arguments ...interface{}) bool { validation, err := Assert(expected, actual, NewDataPath("/")) if err != nil { return handlerValidationError(t, err, arguments...) } if validation.FailedCount != 0 { return handlerValidationFailures(t, validation, arguments...) } return true } //AssertValuesWithContext validates expected against actual data structure with context func AssertValuesWithContext(context *Context, t *testing.T, expected, actual interface{}, arguments ...interface{}) bool { validation, err := AssertWithContext(expected, actual, NewDataPath("/"), context) if err != nil { return handlerValidationError(t, err, arguments...) } if validation.FailedCount != 0 { return handlerValidationFailures(t, validation, arguments...) } return true } func handlerValidationError(t *testing.T, err error, arguments ...interface{}) bool { if len(arguments) > 0 { handleFailure(t, fmt.Sprint(arguments...)) } handleFailure(t, err) return false } func handlerValidationFailures(t *testing.T, validation *Validation, arguments ...interface{}) bool { if len(arguments) > 0 { handleFailure(t, arguments...) } for _, failure := range validation.Failures { handleFailure(t, fmt.Sprintf("%v: %v", failure.Path, failure.Message)) } return false } assertly-0.5.4/assert_test.go000066400000000000000000000106211367316370600162710ustar00rootroot00000000000000package assertly import ( "fmt" "github.com/viant/toolbox" "strings" "testing" ) func Test_AssertValues(t *testing.T) { var actual = `[ { "id": 1, "name": "user 1", "perf_rank": 100, "perf_score": "6.50", "quiz": "{\n\t\"1\": {\n\t\t\"id\": 1,\n\t\t\"score\": 10,\n\t\t\"taken\": \"2018-01-10 16:02:01 UTC\"\n\t},\n\t\"2\": {\n\t\t\"id\": 2,\n\t\t\"score\": 3,\n\t\t\"taken\": \"2018-01-15 08:02:23 UTC\"\n\t}\n}", "visited": "2018-01-15 08:02:23Z" }, { "id": 2, "name": "user 2", "perf_rank": 101, "perf_score": "7.00", "quiz": "{\n\t\"1\": {\n\t\t\"id\": 1,\n\t\t\"score\": 10,\n\t\t\"taken\": \"2018-01-11 13:01:48 UTC\"\n\t},\n\t\"2\": {\n\t\t\"id\": 2,\n\t\t\"score\": 4,\n\t\t\"taken\": \"2018-01-12 09:00:26 UTC\"\n\t}\n}", "visited": "2018-01-12 09:00:26Z" }, { "id": 3, "name": "user 3", "perf_rank": 99, "perf_score": "5.00", "quiz": "{\n\t\"1\": {\n\t\t\"id\": 1,\n\t\t\"score\": 5,\n\t\t\"taken\": \"2018-01-10 05:01:33 UTC\"\n\t},\n\t\"2\": {\n\t\t\"id\": 2,\n\t\t\"score\": 5,\n\t\t\"taken\": \"2018-01-12 07:30:52 UTC\"\n\t}\n}", "visited": "2018-01-12 07:30:52Z" } ]` var expected = `[ { "@indexBy@": [ "id" ], "@timeFormat@": "yyyy-MM-dd HH:mm:ss" }, { "id": 1, "name": "user 1", "perf_rank": 100, "perf_score": 6.5, "quiz": { "1": { "id": 1, "score": 10, "taken": "2018-01-10 16:02:01 UTC" }, "2": { "id": 2, "score": 3, "taken": "2018-01-15 08:02:23 UTC" } }, "visited": "2018-01-15 08:02:23 UTC" }, { "id": 2, "name": "user 2", "perf_rank": 101, "perf_score": 7, "quiz": { "1": { "id": 1, "score": 10, "taken": "2018-01-11 13:01:48 UTC" }, "2": { "id": 2, "score": 4, "taken": "2018-01-12 09:00:26 UTC" } }, "visited": "2018-01-12 09:00:26 UTC" }, { "id": 3, "name": "user 3", "perf_rank": 99, "perf_score": 5, "quiz": { "1": { "id": 1, "score": 5, "taken": "2018-01-10 05:01:33 UTC" }, "2": { "id": 2, "score": 5, "taken": "2018-01-12 07:30:52 UTC" } }, "visited": "2018-01-12 07:30:52 UTC" } ] ` AssertValues(t, expected, actual) } type fooProvider struct{} func (*fooProvider) Get(context toolbox.Context, arguments ...interface{}) (interface{}, error) { var args = []string{} for _, arg := range arguments { args = append(args, toolbox.AsString(arg)) } return fmt.Sprintf("foo{%v}", strings.Join(args, ",")), nil } func Test_AssertValuesWithContext(t *testing.T) { ctx := NewDefaultContext() var provider toolbox.ValueProvider = &fooProvider{} ctx.Evaluator.ValueProviderRegistry.Register("foo", provider) var expected = map[string]string{ "k1": "v1", "k2": "Macro test: ", } var actual = map[string]string{ "k1": "v1", "k2": "Macro test: foo{1,abc}", } AssertValuesWithContext(ctx, t, expected, actual) } type rangePredicate struct { min int max int actual int err error } func (p *rangePredicate) String() string { return fmt.Sprintf("min: %v, max: %v, actual: %v, err: %v", p.min, p.max, p.actual, p.err) } func (p *rangePredicate) Apply(value interface{}) bool { p.actual, p.err = toolbox.ToInt(value) return p.actual >= p.min && p.actual <= p.max } type inRangePredicateProvider struct{} func (*inRangePredicateProvider) Get(context toolbox.Context, arguments ...interface{}) (interface{}, error) { if len(arguments) != 2 { return nil, fmt.Errorf("expected 2 arguments (min, max) but had: %v", len(arguments)) } min, err := toolbox.ToInt(arguments[0]) if err != nil { return nil, fmt.Errorf("invalid min %v", err) } max, err := toolbox.ToInt(arguments[1]) if err != nil { return nil, fmt.Errorf("invalid min %v", err) } var predicate toolbox.Predicate = &rangePredicate{min: min, max: max} return &predicate, nil } func Test_AssertValuesWithContextPredicate(t *testing.T) { ctx := NewDefaultContext() var provider toolbox.ValueProvider = &inRangePredicateProvider{} ctx.Evaluator.ValueProviderRegistry.Register("inRange", provider) var actual = map[string]int{ "k1": 1, "k2": 3, } var expected = map[string]string{ "k1": "1", "k2": "", } AssertValuesWithContext(ctx, t, expected, actual) } func Test_AssertValues_ConcreteValues(t *testing.T) { { var i = 0 AssertValues(t, 0, &i, "int with *int") } { var i = 0 AssertValues(t, 0.0, &i, "float with *int") } { var i = 0.0 AssertValues(t, 0.0, &i, "float with *float") } } assertly-0.5.4/context.go000066400000000000000000000015561367316370600154240ustar00rootroot00000000000000package assertly import ( "github.com/viant/toolbox" ) //Context represent validation context type Context struct { toolbox.Context Directives *Directives Evaluator *toolbox.MacroEvaluator StrictDatTypeCheck bool } //NewContext returns a context func NewContext(ctx toolbox.Context, directives *Directives, evaluator *toolbox.MacroEvaluator) *Context { if ctx == nil { ctx = toolbox.NewContext() } if directives == nil { directives = NewDirectives() } if evaluator == nil { evaluator = NewDefaultMacroEvaluator() } return &Context{ Context: ctx, Directives: directives, Evaluator: evaluator, } } //NewDefaultContext returns default context func NewDefaultContext() *Context { return NewContext(nil, nil, nil) } func NewDefaultMacroEvaluator() *toolbox.MacroEvaluator { return toolbox.NewMacroEvaluator("", ValueProviderRegistry) } assertly-0.5.4/directive.go000066400000000000000000000320241367316370600157100ustar00rootroot00000000000000package assertly import ( "fmt" "github.com/viant/toolbox" "strings" ) const ( KeyExistsDirective = "@exists@" KeyDoesNotExistsDirective = "@!exists@" TimeFormatDirective = "@timeFormat@" TimeLayoutDirective = "@timeLayout@" SwitchByDirective = "@switchCaseBy@" CastDataTypeDirective = "@cast@" IndexByDirective = "@indexBy@" KeyCaseSensitiveDirective = "@keyCaseSensitive@" CaseSensitiveDirective = "@caseSensitive@" SourceDirective = "@source@" SortTextDirective = "@sortText@" NumericPrecisionPointDirective = "@numericPrecisionPoint@" CoalesceWithZeroDirective = "@coalesceWithZero@" AssertPathDirective = "@assertPath@" LengthDirective = "@length@" StrictMapCheckDirective = "@strictMapCheck@" ) type AssertPath struct { SubPath string Expected interface{} } //Match represents a validation TestDirective type Directive struct { DataPath KeyExists map[string]bool KeyDoesNotExist map[string]bool TimeLayout string KeyCaseSensitive bool CaseSensitive bool StrictMapCheck bool TimeLayouts map[string]string DataType map[string]string Lengths map[string]int SwitchBy []string CoalesceWithZero bool NumericPrecisionPoint int IndexBy []string Source string SortText bool AssertPaths []*AssertPath } func (d *Directive) mergeFrom(source *Directive) { if source == nil { return } mergeTextMap(source.DataType, &d.DataType) mergeTextMap(source.TimeLayouts, &d.TimeLayouts) mergeBoolMap(source.KeyExists, &d.KeyExists) mergeBoolMap(source.KeyDoesNotExist, &d.KeyDoesNotExist) d.CoalesceWithZero = source.CoalesceWithZero d.CaseSensitive = source.CaseSensitive d.KeyCaseSensitive = source.KeyCaseSensitive d.StrictMapCheck = source.StrictMapCheck d.TimeLayouts = source.TimeLayouts if d.MatchingPath() == "" && len(d.IndexBy) == 0 { d.IndexBy = source.IndexBy } if d.NumericPrecisionPoint == 0 { d.NumericPrecisionPoint = source.NumericPrecisionPoint } if d.TimeLayout == "" { d.TimeLayout = source.TimeLayout } } //AddKeyExists adds key exists TestDirective func (d *Directive) AddSort(key string) { if key == SortTextDirective { d.SortText = true } } //AddKeyExists adds key exists TestDirective func (d *Directive) AddKeyExists(key string) { if len(d.KeyExists) == 0 { d.KeyExists = make(map[string]bool) } d.KeyExists[key] = true } //AddKeyDoesNotExist adds key does exist TestDirective func (d *Directive) AddKeyDoesNotExist(key string) { if len(d.KeyDoesNotExist) == 0 { d.KeyDoesNotExist = make(map[string]bool) } d.KeyDoesNotExist[key] = true } //AddTimeLayout adds time layout TestDirective func (d *Directive) AddTimeLayout(key, value string) { if len(d.TimeLayouts) == 0 { d.TimeLayouts = make(map[string]string) } d.TimeLayouts[key] = value } //AddDataType adds data type TestDirective func (d *Directive) AddDataType(key, value string) { if len(d.DataType) == 0 { d.DataType = make(map[string]string) } d.DataType[key] = value } //ExtractDataTypes extracts data from from supplied map func (d *Directive) ExtractDataTypes(aMap map[string]interface{}) { for k, v := range aMap { if toolbox.IsInt(v) { d.AddDataType(k, "int") } else if toolbox.IsFloat(v) { d.AddDataType(k, "float") } else if toolbox.IsBool(v) { d.AddDataType(k, "bool") } else if toolbox.IsTime(v) { if _, has := d.TimeLayouts[k]; !has { var dateFormat = "yyyy-MM-dd HH:mm:ss.SSSZ" layout := toolbox.DateFormatToLayout(dateFormat) d.AddTimeLayout(k, layout) } } } } func (d *Directive) asCaseInsensitveMap(aMap map[string]string) map[string]string { if len(aMap) == 0 { return aMap } var result = make(map[string]string) for k, v := range aMap { result[strings.ToUpper(k)] = v } return result } func (d *Directive) ApplyKeyCaseInsensitive() { if len(d.IndexBy) > 0 { d.IndexBy = strings.Split(strings.ToUpper(strings.Join(d.IndexBy, ",")), ",") } d.TimeLayouts = d.asCaseInsensitveMap(d.TimeLayouts) d.DataType = d.asCaseInsensitveMap(d.DataType) } //Add adds by to supplied target func (d *Directive) Add(target map[string]interface{}) { if len(d.SwitchBy) > 0 { target[SwitchByDirective] = d.SwitchBy } if len(d.IndexBy) > 0 { target[IndexByDirective] = d.IndexBy } if d.NumericPrecisionPoint > 0 { target[NumericPrecisionPointDirective] = d.NumericPrecisionPoint } if d.CoalesceWithZero { target[CoalesceWithZeroDirective] = d.CoalesceWithZero } if len(d.DataType) > 0 { for k, v := range d.DataType { target[CastDataTypeDirective+k] = v } } if len(d.TimeLayouts) > 0 { for k, v := range d.TimeLayouts { target[TimeLayoutDirective+k] = v } } if d.TimeLayout != "" { target[TimeLayoutDirective] = d.TimeLayout } } func (d *Directive) addAssertPath(subpath string, expected interface{}) { d.AssertPaths = append(d.AssertPaths, &AssertPath{ SubPath: subpath, Expected: expected, }) } //ExtractDirective extract TestDirective from supplied map func (d *Directive) ExtractDirectives(aMap map[string]interface{}) bool { var keyCount = len(aMap) var directiveCount = 0 if len(d.Lengths) == 0 { d.Lengths = make(map[string]int) } for k, v := range aMap { if d.IsDirectiveKey(k) { directiveCount++ } if k == SwitchByDirective { d.SwitchBy = toStringSlice(v) continue } if k == SortTextDirective { d.SortText = toolbox.AsBoolean(v) continue } if k == IndexByDirective { d.IndexBy = toStringSlice(v) continue } if k == IndexByDirective { d.IndexBy = toStringSlice(v) continue } if k == KeyCaseSensitiveDirective { d.KeyCaseSensitive = toolbox.AsBoolean(v) continue } if k == StrictMapCheckDirective { d.StrictMapCheck = toolbox.AsBoolean(v) continue } if k == CaseSensitiveDirective { d.CaseSensitive = toolbox.AsBoolean(v) continue } if k == NumericPrecisionPointDirective { d.NumericPrecisionPoint = toolbox.AsInt(v) continue } if k == CoalesceWithZeroDirective { d.CoalesceWithZero = toolbox.AsBoolean(v) continue } if k == SourceDirective { d.Source = toolbox.AsString(v) continue } if strings.HasPrefix(k, AssertPathDirective) { var subPath = strings.Replace(k, AssertPathDirective, "", 1) if subPath != "" { d.addAssertPath(subPath, v) } else if toolbox.IsSlice(v) { for _, item := range toolbox.AsSlice(v) { if toolbox.IsMap(item) { for subPath, expcted := range toolbox.AsMap(item) { d.addAssertPath(subPath, expcted) } } } } else if toolbox.IsMap(v) { for subPath, expcted := range toolbox.AsMap(v) { d.addAssertPath(subPath, expcted) } } continue } if strings.HasPrefix(k, LengthDirective) { var key = strings.Replace(k, LengthDirective, "", 1) d.Lengths[key] = toolbox.AsInt(v) continue } else if strings.HasPrefix(k, KeyExistsDirective) { var key = strings.Replace(k, KeyExistsDirective, "", 1) if toolbox.AsBoolean(v) { d.AddKeyExists(key) } else { d.AddKeyDoesNotExist(key) } continue } else if strings.HasPrefix(k, KeyDoesNotExistsDirective) { var key = strings.Replace(k, KeyDoesNotExistsDirective, "", 1) if toolbox.AsBoolean(v) { d.AddKeyDoesNotExist(key) } else { d.AddKeyExists(key) } continue } if text, ok := v.(string); ok { if text == KeyExistsDirective { d.AddKeyExists(k) continue } if text == KeyDoesNotExistsDirective { d.AddKeyDoesNotExist(k) continue } if strings.HasPrefix(k, TimeFormatDirective) { var key = strings.Replace(k, TimeFormatDirective, "", 1) if key == "" { d.TimeLayout = toolbox.DateFormatToLayout(text) } else { d.AddTimeLayout(key, toolbox.DateFormatToLayout(text)) } continue } if strings.HasPrefix(k, TimeLayoutDirective) { var key = strings.Replace(k, TimeLayoutDirective, "", 1) if key == "" { d.TimeLayout = text } else { d.AddTimeLayout(key, text) } continue } if strings.HasPrefix(k, CastDataTypeDirective) { var key = strings.Replace(k, CastDataTypeDirective, "", 1) d.AddDataType(key, text) } } } return keyCount > 0 && keyCount == directiveCount } //Apply applies TestDirective to supplied map func (d *Directive) Apply(aMap map[string]interface{}) error { if err := d.applyTimeFormat(aMap); err != nil { return err } if d.NumericPrecisionPoint != 0 { aMap[NumericPrecisionPointDirective] = d.NumericPrecisionPoint } if d.CoalesceWithZero { aMap[CoalesceWithZeroDirective] = d.CoalesceWithZero } if err := d.castData(aMap); err != nil { return err } return nil } //DefaultTimeLayout returns default time layout func (d *Directive) DefaultTimeLayout() string { if d.TimeLayout == "" { d.TimeLayout = toolbox.DefaultDateLayout } return d.TimeLayout } func (d *Directive) applyTimeFormat(aMap map[string]interface{}) error { if len(d.TimeLayouts) == 0 { return nil } for key, layout := range d.TimeLayouts { val, ok := aMap[key] if !ok || val == nil || getPredicate(val) != nil || toolbox.IsFunc(val) { continue } timeValue, err := toolbox.ToTime(val, layout) if err != nil { return err } aMap[key] = timeValue } return nil } func (d *Directive) castData(aMap map[string]interface{}) error { if len(d.DataType) == 0 { return nil } for key, dataType := range d.DataType { var err error var casted interface{} val, ok := aMap[key] if !ok || val == nil || getPredicate(val) != nil || toolbox.IsFunc(val) { continue } textVal := toolbox.AsString(val) if strings.HasPrefix(textVal, "<") || strings.HasSuffix(textVal, ">") { continue } if d.IsDirectiveValue(toolbox.AsString(val)) { continue } if text, ok := val.(string); ok { if strings.HasPrefix(text, "!") || strings.HasPrefix(text, "/") || strings.HasPrefix(text, "~") { continue } val = strings.TrimSpace(text) } switch dataType { case "float": casted, err = toolbox.ToFloat(val) case "int": casted, err = toolbox.ToInt(val) case "bool": casted = toolbox.AsBoolean(val) default: err = fmt.Errorf("unsupported cast type: %v", dataType) } if toolbox.IsNilPointerError(err) { casted = nil } else if err != nil { casted = val } aMap[key] = casted } return nil } //IsDirectiveKey returns true if key is TestDirective func (d *Directive) IsDirectiveKey(key string) bool { return strings.HasPrefix(key, "@") && strings.Count(key, "@") > 1 } //IsDirectiveKey returns true if value is TestDirective func (d *Directive) IsDirectiveValue(value string) bool { return value == KeyExistsDirective || value == KeyDoesNotExistsDirective } //NewDirective creates a new TestDirective for supplied path func NewDirective(path DataPath) *Directive { dataPath, ok := path.(*dataPath) if ok { if dataPath.directive != nil { return dataPath.directive } } var result = &Directive{ DataPath: path, KeyCaseSensitive: true, CaseSensitive: true, AssertPaths: make([]*AssertPath, 0), } if dataPath != nil { dataPath.directive = result } //inherit default time from first ancestor path.Each(func(path DataPath) bool { directive := path.Directive() if directive != nil { if directive.TimeLayout != "" { result.TimeLayout = directive.TimeLayout return false } } return true }) //inherit default numeric precision point path.Each(func(path DataPath) bool { directive := path.Directive() if directive != nil { if directive.NumericPrecisionPoint != 0 { result.NumericPrecisionPoint = directive.NumericPrecisionPoint return false } } return true }) //inherit default numeric precision point path.Each(func(path DataPath) bool { directive := path.Directive() if directive != nil { if directive.CoalesceWithZero { result.CoalesceWithZero = directive.CoalesceWithZero return false } } return true }) return result } //TestDirective represents TestDirective record type TestDirective map[string]interface{} func (r TestDirective) IndexBy(key string) TestDirective { r[IndexByDirective] = key return r } func IndexBy(key string) TestDirective { var result = TestDirective{} return result.IndexBy(key) } func (r TestDirective) TimeFormat(key, format string) TestDirective { r[TimeFormatDirective+key] = format return r } func TimeFormat(key, format string) TestDirective { var result = TestDirective{} return result.TimeFormat(key, format) } func (r TestDirective) TimeLayout(key, format string) TestDirective { r[TimeLayoutDirective+key] = format return r } func TimeLayout(key, format string) TestDirective { var result = TestDirective{} return result.TimeLayout(key, format) } func (r TestDirective) KeyCaseSensitive() TestDirective { r[KeyCaseSensitiveDirective] = true return r } func (r TestDirective) Cast(field, dataType string) TestDirective { r[CastDataTypeDirective+field] = dataType return r } func (r TestDirective) SortText() TestDirective { r[SortTextDirective] = true return r } assertly-0.5.4/directive_test.go000066400000000000000000000114541367316370600167530ustar00rootroot00000000000000package assertly import ( "github.com/stretchr/testify/assert" "github.com/viant/toolbox" "testing" "time" ) func TestDirective_ExtractExpected(t *testing.T) { directive := NewDirectives() { //test switch by directive var source = map[string]interface{}{ "k1": 1, SwitchByDirective: "k1", } directive.ExtractDirectives(source) assert.Equal(t, []string{"k1"}, directive.SwitchBy) } { //test index by directive var source = map[string]interface{}{ "k1": 1, "k2": 3, IndexByDirective: []string{"k1", "k2"}, } directive.ExtractDirectives(source) assert.Equal(t, []string{"k1", "k2"}, directive.IndexBy) } { //test index by directive var source = map[string]interface{}{ "k1": 1, "k2": 3, IndexByDirective: []string{"k1", "k2"}, } directive.ExtractDirectives(source) assert.Equal(t, []string{"k1", "k2"}, directive.IndexBy) } { //test key exists, does not exist directive var source = map[string]interface{}{ "k1": KeyDoesNotExistsDirective, "k2": KeyExistsDirective, } directive.ExtractDirectives(source) assert.True(t, directive.KeyExists["k2"]) assert.True(t, directive.KeyDoesNotExist["k1"]) } { //test key exists, does not exist directive var source = map[string]interface{}{ KeyDoesNotExistsDirective + "k001": true, KeyExistsDirective + "k002": true, } directive.ExtractDirectives(source) assert.True(t, directive.KeyExists["k2"]) assert.True(t, directive.KeyDoesNotExist["k1"]) } { //test key time format var source = map[string]interface{}{ TimeFormatDirective + "k2": "yyyy-MM-dd", TimeFormatDirective: "yyyy-MM-dd", "k2": "2011-02-11", } directive.ExtractDirectives(source) assert.EqualValues(t, "2006-01-02", directive.TimeLayouts["k2"]) assert.EqualValues(t, "2006-01-02", directive.DefaultTimeLayout()) } { //test key time format var source = map[string]interface{}{ CastDataTypeDirective + "k2": "float", "k2": "3.7", } directive.ExtractDirectives(source) assert.EqualValues(t, "float", directive.DataType["k2"]) } } func truncateDate(dateFormat string) *time.Time { var layout = toolbox.DateFormatToLayout(dateFormat) date := time.Now().Format(layout) result, _ := time.Parse(layout, date) return &result } func TestDirective_ExtractDataTypes(t *testing.T) { date := truncateDate("yyyy-MM-dd") dateHour := truncateDate("yyyy-MM-dd hh") dateHourMiniute := truncateDate("yyyy-MM-dd hh:mm") dateHourMiniuteSec := truncateDate("yyyy-MM-dd hh:mm:ss") { //test index by directive var source = map[string]interface{}{ "d1": date, "d2": dateHour, "d3": dateHourMiniute, "d4": dateHourMiniuteSec, "f": 3.2, "i": 213, "b": true, } directive := NewDirectives() directive.ExtractDataTypes(source) assert.EqualValues(t, "float", directive.DataType["f"]) assert.EqualValues(t, "int", directive.DataType["i"]) assert.EqualValues(t, "bool", directive.DataType["b"]) assert.EqualValues(t, "2006-01-02 15:04:05.000-07", directive.TimeLayouts["d4"]) } } func TestDirective_Add(t *testing.T) { directive := NewDirective(NewDataPath("/")) directive.DataType = map[string]string{"f1": "float"} directive.IndexBy = []string{"id"} directive.SwitchBy = []string{"case"} directive.DataType = map[string]string{"f1": "float"} directive.TimeLayouts = map[string]string{"t1": "2016-01"} directive.TimeLayout = "2016-01-02" var target = make(map[string]interface{}) directive.Add(target) assert.EqualValues(t, "float", target[CastDataTypeDirective+"f1"]) assert.EqualValues(t, "2016-01-02", target[TimeLayoutDirective]) assert.EqualValues(t, "2016-01", target[TimeLayoutDirective+"t1"]) assert.EqualValues(t, []string{"id"}, target[IndexByDirective]) assert.EqualValues(t, []string{"case"}, target[SwitchByDirective]) } func TestAssertPath_Directive(t *testing.T) { expected := `[ { "@indexBy@":"SubPath" }, { "SubPath": "group1.field1", "Expected": 1 }, { "SubPath": "group1.field2", "Expected": 2 }, { "SubPath": "group1.field3", "Expected": 3 } ] ` { directive := NewDirective(NewDataPath("/")) var target = map[string]interface{}{ AssertPathDirective + "group1.field1": 1, AssertPathDirective: []interface{}{ map[string]interface{}{ "group1.field2": 2, }, map[string]interface{}{ "group1.field3": 3, }, }, } directive.ExtractDirectives(target) AssertValues(t, expected, directive.AssertPaths) } { directive := NewDirective(NewDataPath("/")) var target = map[string]interface{}{ AssertPathDirective + "group1.field1": 1, AssertPathDirective: map[string]interface{}{ "group1.field2": 2, "group1.field3": 3, }, } directive.ExtractDirectives(target) AssertValues(t, expected, directive.AssertPaths) } } assertly-0.5.4/directives.go000066400000000000000000000014361367316370600160760ustar00rootroot00000000000000package assertly //Directives represent a directive type Directives struct { *Directive PathDirectives map[string]*Directive } func (d *Directives) Match(path DataPath) *Directive { var result = NewDirective(path) result.mergeFrom(d.Directive) if matched, ok := d.PathDirectives[path.MatchingPath()]; ok { result.mergeFrom(matched) } return result } //NewDirectives returns new directives func NewDirectives(directives ...*Directive) *Directives { var result = &Directives{ Directive: NewDirective(NewDataPath("")), PathDirectives: make(map[string]*Directive), } for i, directive := range directives { if directive.MatchingPath() == "" { result.Directive = directive continue } result.PathDirectives[directive.MatchingPath()] = directives[i] } return result } assertly-0.5.4/failure.go000066400000000000000000000101451367316370600153610ustar00rootroot00000000000000package assertly import ( "fmt" "github.com/viant/toolbox" "strings" "unicode" ) //Failure represents a validation failre type Failure struct { Source string Path string Expected interface{} Actual interface{} Args []interface{} Reason string Message string } func (f *Failure) Index() int { pair := strings.SplitN(f.Path, ":", 2) if len(pair) != 2 { return -1 } var index = "" var expectIndex = false outer: for _, r := range pair[1] { switch r { case '[': expectIndex = true case ']': expectIndex = false if len(index) > 0 { break outer } default: if expectIndex && unicode.IsNumber(r) { index += string(r) } } } if len(index) == 0 { return -1 } return toolbox.AsInt(index) } func (f *Failure) LeafKey() string { leafIndex := strings.LastIndex(f.Path, ".") if leafIndex == -1 { return "" } return string(f.Path[leafIndex+1:]) } func removeDirectives(aMap map[string]interface{}) map[string]interface{} { var result = make(map[string]interface{}) for k, v := range aMap { if strings.HasPrefix(k, "@") { continue } result[k] = v } return result } //NewFailure creates a new failure func NewFailure(source, path string, reason string, expected, actual interface{}, args ...interface{}) *Failure { if expected != nil && toolbox.IsMap(expected) { expected = removeDirectives(toolbox.AsMap(expected)) } if actual != nil && toolbox.IsMap(actual) { actual = removeDirectives(toolbox.AsMap(actual)) } var result = &Failure{ Source: source, Path: path, Reason: reason, Expected: expected, Actual: actual, Args: args, } result.Message = FormatMessage(result) return result } func FormatMessage(failure *Failure) string { switch failure.Reason { case MissingEntryViolation: return fmt.Sprintf("entry for %v was missing, expected: %v, actual keys: %v", failure.Args[0], failure.Expected, failure.Actual) case MissingItemViolation: return fmt.Sprintf("item was missing, expected: %v, actual: %v", failure.Expected, failure.Actual) case ItemMismatchViolation: return fmt.Sprintf("item was mismatched, key1: %v, key2: %v", failure.Expected, failure.Actual) case IncompatibleDataTypeViolation: return fmt.Sprintf("actual was %T, but expected %T(%v)", failure.Actual, failure.Expected, failure.Expected) case KeyExistsViolation: return fmt.Sprintf("key '%v' should exists", failure.Expected) case KeyDoesNotExistViolation: return fmt.Sprintf("'%v' should not exists", failure.Expected) case EqualViolation: return fmt.Sprintf("actual(%T): '%v' was not equal (%T) '%v'", failure.Actual, failure.Actual, failure.Expected, failure.Expected) case NotEqualViolation: return fmt.Sprintf("actual(%T): '%v' was equal (%T) '%v'", failure.Actual, failure.Actual, failure.Expected, failure.Expected) case LengthViolation: return fmt.Sprintf("actual length %v was not equal: %v", failure.Actual, failure.Expected) case MissingCaseViolation: switchBy := failure.Args[0].([]string) caseValue := toolbox.AsString(failure.Args[1]) var availableKeys = toolbox.MapKeysToStringSlice(failure.Expected) return fmt.Sprintf("actual case %v => %v, was missing in expected set: available keys: [%v]", strings.Join(switchBy, ","), caseValue, strings.Join(availableKeys, ",")) case RegExprMatchesViolation: return fmt.Sprintf("actual: '%v' should matched %v", failure.Actual, failure.Expected) case RegExprDoesNotMatchViolation: return fmt.Sprintf("actual: '%v' should not be matched %v", failure.Actual, failure.Expected) case RangeViolation: return fmt.Sprintf("actual '%v' is not in: '%v'", failure.Actual, failure.Expected) case RangeNotViolation: return fmt.Sprintf("actual '%v' should not be in: '%v'", failure.Actual, failure.Expected) case ContainsViolation: return fmt.Sprintf("actual '%v' does not contain: '%v'", failure.Actual, failure.Expected) case DoesNotContainViolation: return fmt.Sprintf("actual '%v' should not not contain: '%v'", failure.Actual, failure.Expected) case PredicateViolation: return fmt.Sprintf("actual '%v' should pass predicate: '%v'", failure.Actual, failure.Expected) } return failure.Reason } assertly-0.5.4/failure_test.go000066400000000000000000000025421367316370600164220ustar00rootroot00000000000000package assertly import ( "github.com/stretchr/testify/assert" "testing" ) func TestFailure_Index(t *testing.T) { { var failure = NewFailure("", "[/]:ad[12].we", "", nil, nil) assert.EqualValues(t, 12, failure.Index()) } { var failure = NewFailure("", "ad[12].we", "", nil, nil) assert.EqualValues(t, -1, failure.Index()) } { var failure = NewFailure("", ":ad[a].we", "", nil, nil) assert.EqualValues(t, -1, failure.Index()) } { var failure = NewFailure("", ":ad[].we", "", nil, nil) assert.EqualValues(t, -1, failure.Index()) } } func TestFailure_LeafKey(t *testing.T) { { var failure = NewFailure("", "[/]:ad[12].we", "", nil, nil) assert.EqualValues(t, "we", failure.LeafKey()) } { var failure = NewFailure("", "[/]:ad[12].", "", nil, nil) assert.EqualValues(t, "", failure.LeafKey()) } { var failure = NewFailure("", "[/]:ad[12]", "", nil, nil) assert.EqualValues(t, "", failure.LeafKey()) } } func TestFailure_MergeFrom(t *testing.T) { var failure = NewFailure("", "[/]:ad[12].we", "", nil, nil) source := NewValidation() source.PassedCount = 2 source.AddFailure(failure) target := NewValidation() target.AddFailure(failure) target.PassedCount = 2 target.MergeFrom(source) assert.EqualValues(t, 4, target.PassedCount) assert.EqualValues(t, 2, target.FailedCount) assert.EqualValues(t, 2, len(target.Failures)) } assertly-0.5.4/helper.go000066400000000000000000000057521367316370600152210ustar00rootroot00000000000000package assertly import ( "github.com/viant/toolbox" "github.com/viant/toolbox/data" "strings" ) func asDataStructure(candidate string) interface{} { if isMultiline(candidate) { lines := strings.Split(strings.TrimSpace(candidate), "\n") if toolbox.IsCompleteJSON(lines[0]) && toolbox.IsCompleteJSON(lines[len(lines)-1]) { var result = make([]interface{}, 0) for _, line := range lines { if strings.TrimSpace(line) == "" { continue } item, err := toolbox.JSONToInterface(line) if err != nil { result = []interface{}{} break } result = append(result, item) } if len(result) > 0 { return result } } else if toolbox.IsCompleteJSON(candidate) { if result, err := toolbox.JSONToInterface(candidate); err == nil { return result } } } else if result, err := toolbox.JSONToInterface(candidate); err == nil { return result } return candidate } func isMultiline(candidate string) bool { return strings.Count(candidate, "\n") > 0 } func reverseSlice(stringSlice []string) { last := len(stringSlice) - 1 for i := 0; i < len(stringSlice)/2; i++ { stringSlice[i], stringSlice[last-i] = stringSlice[last-i], stringSlice[i] } } func mergeTextMap(source map[string]string, target *map[string]string) { if len(source) == 0 { return } if target == nil || len(*target) == 0 { *target = make(map[string]string) } for k := range source { (*target)[k] = source[k] } } func mergeBoolMap(source map[string]bool, target *map[string]bool) { if len(source) == 0 { return } if target == nil || len(*target) == 0 { *target = make(map[string]bool) } for k := range source { (*target)[k] = source[k] } } func keysValue(aMap data.Map, keys ...string) string { var result = "" for _, key := range keys { value, ok := aMap.GetValue(key) if !ok { value = "" } if value != nil && toolbox.IsMap(value) { valueMap := toolbox.AsMap(value) for k := range valueMap { result += k } } else { result += toolbox.AsString(value) } } return result } func keysPairValue(aMap map[string]interface{}, keys ...string) string { var result = "" for _, key := range keys { value := aMap[key] if len(result) > 0 { result += "" } result += key + "(" + toolbox.AsString(value) + ")" } return result } func indexSliceBy(aSlice []interface{}, indexFields ...string) map[string]interface{} { var result = make(map[string]interface{}) for _, item := range aSlice { var value = keysValue(toolbox.AsMap(item), indexFields...) result[value] = item } return result } func toStringSlice(source interface{}) []string { if !toolbox.IsSlice(source) { return strings.Split(toolbox.AsString(source), ",") } var result = make([]string, 0) for _, item := range toolbox.AsSlice(source) { result = append(result, toolbox.AsString(item)) } return result } func isIndexable(source map[string]interface{}) bool { for _, v := range source { if v == nil { continue } if toolbox.IsMap(v) { return true } } return false } assertly-0.5.4/helper_test.go000066400000000000000000000101151367316370600162450ustar00rootroot00000000000000package assertly import ( "github.com/stretchr/testify/assert" "github.com/viant/toolbox" "reflect" "testing" ) func TestAsDataStructure(t *testing.T) { var useCases = []struct { Description string Input string Kind reflect.Kind Length int }{ { Description: "multiline textual value", Input: "abc\nxyz", Kind: reflect.String, Length: -1, }, { Description: "multiline textual with incomplete JSON like struct at the end", Input: "abc\nxyz\n{123}", Kind: reflect.String, Length: -1, }, { Description: "multiline textual with incomplete JSON like struct at the begining and at the end", Input: "{123}\nabc\nxyz\n{123}", Kind: reflect.String, Length: -1, }, { Description: "multiline textual with incomplete JSON like struct at the begining", Input: "{123}\nabc\nxyz\n{123}", Kind: reflect.String, Length: -1, }, { Description: "valida JSON only at the first and last line", Input: "[1,2,3]\nabc\nxyz\n[1,2,3]", Kind: reflect.String, Length: -1, }, { Description: "valida JSON only at the first and last line", Input: "[1,2,3]\n[2,3,4]\n[1,2,3]", Kind: reflect.Slice, Length: 3, }, { Description: "multiline valid JSON object", Input: `{ "k1":"v1", "k2": "v2" }`, Kind: reflect.Map, Length: 2, }, { Description: "multiline valid JSON array", Input: `[1, 2, 3 ]`, Kind: reflect.Slice, Length: 3, }, { Description: "new line delimited valid JSON array with empty line", Input: `[1,2,3] [2,3,4]`, Kind: reflect.Slice, Length: 2, }, { Description: "multiline invalid JSON object", Input: `{ "k1":"v1", z "k2", "v2" }`, Kind: reflect.String, Length: -1, }, { Description: "empty string", Input: ` `, Kind: reflect.String, Length: -1, }, { Description: "single line valid JSON object", Input: `{"a":1}`, Kind: reflect.Map, Length: 1, }, } for _, useCase := range useCases { output := asDataStructure(useCase.Input) actualKind := reflect.TypeOf(output).Kind() if assert.EqualValues(t, useCase.Kind, actualKind, useCase.Description) { if actualKind == reflect.String { //check for no string modification assert.EqualValues(t, useCase.Input, output, useCase.Description) } if useCase.Length >= 0 { var actualLength = 0 if actualKind == reflect.Map { actualLength = len(toolbox.AsMap(output)) } else { actualLength = len(toolbox.AsSlice(output)) } assert.EqualValues(t, useCase.Length, actualLength, useCase.Description) } } } } func TestReverseSlice(t *testing.T) { var aSlice = []string{"1", "10", "3"} reverseSlice(aSlice) assert.EqualValues(t, []string{"3", "10", "1"}, aSlice) } func TestMergeTextMap(t *testing.T) { { var source = map[string]string{} var target map[string]string mergeTextMap(source, &target) assert.EqualValues(t, 0, len(target)) } { var source = map[string]string{ "k1": "v1", } var target map[string]string mergeTextMap(source, &target) assert.EqualValues(t, 1, len(target)) } { var source = map[string]string{ "k1": "v1", } var target = make(map[string]string) mergeTextMap(source, &target) assert.EqualValues(t, 1, len(target)) } } func TestMergeBoolMap(t *testing.T) { { var source = map[string]bool{} var target map[string]bool mergeBoolMap(source, &target) assert.EqualValues(t, 0, len(target)) } { var source = map[string]bool{ "k1": true, } var target map[string]bool mergeBoolMap(source, &target) assert.EqualValues(t, 1, len(target)) } { var source = map[string]bool{ "k1": true, } var target map[string]bool mergeBoolMap(source, &target) assert.EqualValues(t, 1, len(target)) } } func Test_ToStringSlice(t *testing.T) { { aSlice := toStringSlice([]interface{}{"1", 2}) assert.EqualValues(t, []string{"1", "2"}, aSlice) } { aSlice := toStringSlice(1) assert.EqualValues(t, []string{"1"}, aSlice) } } assertly-0.5.4/path.go000066400000000000000000000065411367316370600146730ustar00rootroot00000000000000package assertly import ( "fmt" "strings" ) //DataPath represents a dat path type DataPath interface { //MatchingPath returns matching path MatchingPath() string //Path data path Path() string //Index creates subpath for supplied index Index(index int) DataPath //Index creates subpath for supplied key Key(key string) DataPath //Set source for this path, source may represent detail location of data point SetSource(string) //Get source from this path Source() string //Match returns a matched directive for this path Match(context *Context) *Directive //Match returns a directive for this path Directive() *Directive //Each traverse each data path node upto parent Each(callback func(path DataPath) bool) } type dataPath struct { root string source string index int key string parent *dataPath directive *Directive } func (p *dataPath) Index(index int) DataPath { return &dataPath{ index: index, parent: p, directive: p.directive, } } func (p *dataPath) Key(field string) DataPath { keyPath := &dataPath{ key: field, parent: p, } keyDirective := NewDirective(keyPath) keyPath.directive = keyDirective if p.directive != nil { if len(p.directive.TimeLayouts) > 0 { if timeLayout, ok := p.directive.TimeLayouts[field]; ok { keyPath.directive.TimeLayout = timeLayout } } keyDirective.mergeFrom(p.directive) } return keyPath } func (p *dataPath) SetSource(source string) { p.source = source } func (p *dataPath) Source() string { if p.source != "" { return p.source } var result = "" p.each(func(node *dataPath) bool { if node.source != "" { result = node.source return false } return true }) return result } func (p *dataPath) Directive() *Directive { return p.directive } func (p *dataPath) Match(context *Context) *Directive { if p.directive != nil { return p.directive } directive := context.Directives.Match(p) p.each(func(node *dataPath) bool { if node.directive != nil { directive.mergeFrom(node.directive) return false } return true }) p.directive = directive return directive } func (p *dataPath) Each(callback func(path DataPath) bool) { var node = p for node != nil { if !callback(node) { break } node = node.parent } } func (p *dataPath) each(callback func(path *dataPath) bool) { var node = p for node != nil { if !callback(node) { break } node = node.parent } } func (p *dataPath) Path() string { var result = make([]string, 0) p.each(func(node *dataPath) bool { if node.root != "" { result = append(result, "["+node.root+"]:") } else if node.key != "" { var dot = "." if node.parent != nil && node.parent.root != "" { dot = "" } result = append(result, dot+node.key) } else { result = append(result, fmt.Sprintf("[%d]", node.index)) } return true }) reverseSlice(result) return strings.Join(result, "") } func (p *dataPath) MatchingPath() string { var result = make([]string, 0) p.each(func(node *dataPath) bool { if node.root != "" { return false } if node.key != "" { result = append(result, node.key) } else { result = append(result, "*") } return true }) reverseSlice(result) return strings.Join(result, "/") } //NewDataPath returns a new data path. func NewDataPath(root string) DataPath { if root == "" { root = " " } return &dataPath{ root: root, } } assertly-0.5.4/path_test.go000066400000000000000000000006451367316370600157310ustar00rootroot00000000000000package assertly import ( "github.com/stretchr/testify/assert" "testing" ) func TestDataPath_Key(t *testing.T) { var path = NewDataPath("root") subPath := path.Key("f1").Index(1).Key("s1") assert.Equal(t, "f1/*/s1", subPath.MatchingPath()) } func TestDataPath_Path(t *testing.T) { var path = NewDataPath("root") subPath := path.Key("f1").Index(1).Key("s1") assert.Equal(t, "[root]:f1[1].s1", subPath.Path()) } assertly-0.5.4/validation.go000066400000000000000000000023711367316370600160660ustar00rootroot00000000000000package assertly import ( "fmt" "strings" ) //Validation validation type Validation struct { TagID string Description string PassedCount int FailedCount int Failures []*Failure } //AddFailure add failure to current violation func (v *Validation) AddFailure(failure *Failure) { if len(v.Failures) == 0 { v.Failures = make([]*Failure, 0) } v.Failures = append(v.Failures, failure) v.FailedCount++ } //HasFailure returns true if validation has failures func (v *Validation) HasFailure() bool { return v.FailedCount > 0 } //MergeFrom merges failures and passes from source func (v *Validation) MergeFrom(source *Validation) { v.PassedCount += source.PassedCount for _, failure := range source.Failures { v.AddFailure(failure) } } //Report returns validation report func (v *Validation) Report() string { var result = make([]string, 0) for _, failure := range v.Failures { result = append(result, failure.Path+": "+failure.Message) } result = append(result, fmt.Sprintf("Passed: %v", v.PassedCount)) result = append(result, fmt.Sprintf("Failed: %v", v.FailedCount)) return strings.Join(result, "\n") } //NewValidation returns new validation func NewValidation() *Validation { return &Validation{ Failures: make([]*Failure, 0), } } assertly-0.5.4/validation_test.go000066400000000000000000000005001367316370600171150ustar00rootroot00000000000000package assertly import ( "github.com/stretchr/testify/assert" "testing" ) func TestValidation_Report(t *testing.T) { source := NewValidation() source.AddFailure(NewFailure("", ":ad[].we", "test", nil, nil)) source.PassedCount++ assert.EqualValues(t, ":ad[].we: test\nPassed: 1\nFailed: 1", source.Report()) } assertly-0.5.4/validator.go000066400000000000000000000566211367316370600157300ustar00rootroot00000000000000package assertly import ( "fmt" "github.com/viant/toolbox" "github.com/viant/toolbox/data" "log" "math" "path" "reflect" "regexp" "sort" "strings" "testing" "time" ) const ( MissingEntryViolation = "entry was missing" MissingItemViolation = "item was missing" ItemMismatchViolation = "item was mismatched" IncompatibleDataTypeViolation = "data type was incompatible" KeyExistsViolation = "key should exist" KeyDoesNotExistViolation = "key should not exist" EqualViolation = "value should be equal" NotEqualViolation = "value should not be equal" LengthViolation = "should have the same length" MissingCaseViolation = "missing switch/case value" RegExprMatchesViolation = "should match regexpr" RegExprDoesNotMatchViolation = "should not match regexpr" RangeViolation = "should be in range" RangeNotViolation = "should not be in range" ContainsViolation = "should contain fragment" DoesNotContainViolation = "should not contain fragment" PredicateViolation = "should pass predicate" ValueWasNil = "should have not nil" SharedSwitchCaseKey = "shared" ) //Assert validates expected against actual data structure for supplied path func Assert(expected, actual interface{}, path DataPath) (*Validation, error) { context := NewDefaultContext() return AssertWithContext(expected, actual, path, context) } func handleFailure(t *testing.T, args ...interface{}) { file, method, line := toolbox.DiscoverCaller(2, 10, "assert.go", "stack_helper.go", "validator.go") _, file = path.Split(file) var argsLiteral = fmt.Sprint(args...) fmt.Printf("%v:%v (%v)\n%v\n", file, line, method, argsLiteral) t.Fail() } //AssertWithContext validates expected against actual data structure for supplied path and context func AssertWithContext(expected, actual interface{}, path DataPath, context *Context) (*Validation, error) { validation := NewValidation() err := assertValue(expected, actual, path, context, validation) return validation, err } func getPredicate(input interface{}) toolbox.Predicate { predicate, ok := input.(toolbox.Predicate) if !ok { if predicatePointer, ok := input.(*toolbox.Predicate); ok { predicate = *predicatePointer } } return predicate } func expandExpectedText(text string, path DataPath, context *Context) (interface{}, error) { if toolbox.IsNewLineDelimitedJSON(text) || toolbox.IsCompleteJSON(text) { return asDataStructure(text), nil } if context.Evaluator.HasMacro(text) { evaluated, err := context.Evaluator.Expand(context.Context, text) if err != nil { return nil, fmt.Errorf("failed to expand macro %v, path:%v, %v", text, path.Path(), err) } if !toolbox.IsString(evaluated) { return evaluated, nil } text = toolbox.AsString(evaluated) } return text, nil } func assertTime(expected *time.Time, actual interface{}, path DataPath, context *Context, validation *Validation) (err error) { dateLayout := path.Match(context).DefaultTimeLayout() actualTime, err := toolbox.ToTime(actual, dateLayout) if err == nil { actual = actualTime if expected == nil { if actualTime == nil { validation.PassedCount++ return nil } validation.AddFailure(NewFailure(path.Source(), path.Path(), EqualViolation, expected, actual)) } if actualTime == nil { validation.AddFailure(NewFailure(path.Source(), path.Path(), EqualViolation, expected, actual)) return nil } if expected.Location() != actualTime.Location() { actualTimeInLoc := actualTime.In(expected.Location()) actualTime = &actualTimeInLoc actual = actualTime } if expected.Equal(*actualTime) { validation.PassedCount++ return nil } expectedText := expected.Format(dateLayout) actualText := actualTime.Format(dateLayout) if expectedText == actualText { validation.PassedCount++ return nil } } validation.AddFailure(NewFailure(path.Source(), path.Path(), EqualViolation, expected, actual)) return nil } func assertValue(expected, actual interface{}, path DataPath, context *Context, validation *Validation) (err error) { directive := NewDirective(path) if expected == nil { if actual == nil { validation.PassedCount++ return nil } if !directive.StrictMapCheck { validation.AddFailure(NewFailure(path.Source(), path.Path(), NotEqualViolation, expected, actual)) return } } switch val := expected.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: assertInt(expected, actual, path, context, validation) return case float32, float64: assertFloat(expected, actual, path, context, validation) return case string: if expected, err = expandExpectedText(val, path, context); err != nil { return err } } predicate := getPredicate(expected) if predicate == nil { switch val := actual.(type) { case string: if toolbox.IsNewLineDelimitedJSON(val) || toolbox.IsCompleteJSON(val) { actual = asDataStructure(val) } case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: assertInt(expected, actual, path, context, validation) return case float32, float64: assertFloat(expected, actual, path, context, validation) return } } else { if !predicate.Apply(actual) { validation.AddFailure(NewFailure(path.Source(), path.Path(), PredicateViolation, fmt.Sprintf("%T%v", predicate, predicate), actual)) } else { validation.PassedCount++ } return nil } dateLayout := path.Match(context).DefaultTimeLayout() if toolbox.IsTime(expected) || toolbox.IsTime(actual) { expectedTime, _ := toolbox.ToTime(expected, dateLayout) return assertTime(expectedTime, actual, path, context, validation) } else if toolbox.IsStruct(expected) || (actual != nil && toolbox.IsStruct(actual)) { var converter = toolbox.NewColumnConverter(dateLayout) if toolbox.IsStruct(expected) { var expectedMap = make(map[string]interface{}) _ = converter.AssignConverted(&expectedMap, expected) expected = expectedMap } if toolbox.IsStruct(actual) { var actualMap = make(map[string]interface{}) _ = converter.AssignConverted(&actualMap, actual) actual = actualMap } } if expected != nil && toolbox.IsMap(expected) { return assertMap(toolbox.AsMap(expected), actual, path, context, validation) } else if expected != nil && toolbox.IsSlice(expected) { return assertSlice(toolbox.AsSlice(expected), actual, path, context, validation) } else if expected == actual || reflect.DeepEqual(expected, actual) { validation.PassedCount++ return nil } expectedText := toolbox.AsString(expected) if !context.StrictDatTypeCheck { expectedTime, err := toolbox.ToTime(expectedText, directive.DefaultTimeLayout()) actualTime, e := toolbox.ToTime(actual, directive.DefaultTimeLayout()) if e == nil || err == nil { if expectedTime == nil { if strings.HasPrefix(actualTime.String(), expectedText) { validation.PassedCount++ return nil } } else if actualTime != nil { if actualTime.Equal(*expectedTime) { validation.PassedCount++ return nil } } } } return assertText(toolbox.AsString(expected), toolbox.AsString(actual), path, context, validation) } func isNegated(candidate string) (string, bool) { isNot := strings.HasPrefix(candidate, "!") if isNot { candidate = string(candidate[1:]) } return candidate, isNot } func assertRegExpr(isNegated bool, expected, actual string, path DataPath, context *Context, validation *Validation) error { expected = string(expected[2 : len(expected)-1]) useMultiLine := strings.Count(actual, "\n") > 0 pattern := "" if useMultiLine { pattern = "(?m)" } pattern += expected compiled, err := regexp.Compile(pattern) if err != nil { return fmt.Errorf("failed to compile %v, path: %v, %v", expected, path, err) } var matches = compiled.Match(([]byte)(actual)) if !matches && !isNegated { validation.AddFailure(NewFailure(path.Source(), path.Path(), RegExprMatchesViolation, expected, actual)) } else if matches && isNegated { validation.AddFailure(NewFailure(path.Source(), path.Path(), RegExprDoesNotMatchViolation, expected, actual)) } else { validation.PassedCount++ } return nil } func assertRange(isNegated bool, expected, actual string, path DataPath, context *Context, validation *Validation) error { if strings.Count(expected, "..")+strings.Count(expected, ",") == 0 { return fmt.Errorf("invalid range format, expected /[min..max]/ or /[val1,val2,valN]/, but had:%v, path: %v", expected, path.Path()) } actual = strings.TrimSpace(actual) expected = string(expected[2 : len(expected)-2]) var rangeValues = strings.Split(expected, "..") var withinRange bool if len(rangeValues) > 1 { var minExpected = toolbox.AsFloat(strings.TrimSpace(rangeValues[0])) var maxExpected = toolbox.AsFloat(strings.TrimSpace(rangeValues[1])) var actualNumber = toolbox.AsFloat(actual) withinRange = actualNumber >= minExpected && actualNumber <= maxExpected } else { rangeValues = strings.Split(expected, ",") for _, candidate := range rangeValues { if strings.TrimSpace(candidate) == actual { withinRange = true break } } } if !withinRange && !isNegated { validation.AddFailure(NewFailure(path.Source(), path.Path(), RangeViolation, expected, actual)) } else if withinRange && isNegated { validation.AddFailure(NewFailure(path.Source(), path.Path(), RangeNotViolation, expected, actual)) } else { validation.PassedCount++ } return nil } func assertContains(isNegated bool, expected, actual string, path DataPath, context *Context, validation *Validation) { expected = string(expected[1 : len(expected)-1]) contains := strings.Contains(actual, expected) if !contains && !isNegated { validation.AddFailure(NewFailure(path.Source(), path.Path(), ContainsViolation, expected, actual)) } else if contains && isNegated { validation.AddFailure(NewFailure(path.Source(), path.Path(), DoesNotContainViolation, expected, actual)) } else { validation.PassedCount++ } } func assertText(expected, actual string, path DataPath, context *Context, validation *Validation) error { directive := path.Directive() if directive != nil && !directive.CaseSensitive { expected = strings.ToLower(expected) actual = strings.ToLower(actual) } expected = strings.TrimSpace(expected) if strings.HasSuffix(expected, "/") { expected, isNegated := isNegated(expected) isRegExpr := strings.HasPrefix(expected, "~/") if isRegExpr { return assertRegExpr(isNegated, expected, actual, path, context, validation) } isRangeExpr := (strings.HasPrefix(expected, "/[") || strings.HasPrefix(expected, "!/[")) && strings.HasSuffix(expected, "]/") if isRangeExpr { return assertRange(isNegated, expected, actual, path, context, validation) } isContains := strings.HasPrefix(expected, "/") if isContains { assertContains(isNegated, expected, actual, path, context, validation) return nil } } expected, isNegated := isNegated(expected) isEqual := expected == actual if !isEqual && !isNegated { validation.AddFailure(NewFailure(path.Source(), path.Path(), EqualViolation, expected, actual)) } else if isEqual && isNegated { validation.AddFailure(NewFailure(path.Source(), path.Path(), NotEqualViolation, expected, actual)) } else { validation.PassedCount++ } return nil } func actualMap(expected, actualValue interface{}, path DataPath, directive *Directive, validation *Validation) map[string]interface{} { var actual map[string]interface{} if toolbox.IsMap(actualValue) { actual = toolbox.AsMap(actualValue) } else if toolbox.IsSlice(actualValue) { if len(directive.IndexBy) == 0 { validation.AddFailure(NewFailure(path.Source(), path.Path(), IncompatibleDataTypeViolation, expected, actualValue)) return nil } aSlice := toolbox.AsSlice(actualValue) actual = indexSliceBy(aSlice, directive.IndexBy...) } else { validation.AddFailure(NewFailure(path.Source(), path.Path(), IncompatibleDataTypeViolation, expected, actualValue)) return nil } return actual } func assertInt(expected, actual interface{}, path DataPath, context *Context, validation *Validation) { directive := path.Directive() expectedInt, expectedErr := toolbox.ToInt(expected) if expectedErr != nil && !toolbox.IsNilPointerError(expectedErr) { _ = assertText(toolbox.AsString(expected), toolbox.AsString(actual), path, context, validation) return } if toolbox.IsNilPointerError(expectedErr) && directive.CoalesceWithZero && directive.StrictMapCheck { expectedErr = nil expectedInt = 0 expected = 0 } actualInt, actualErr := toolbox.ToInt(actual) if toolbox.IsNilPointerError(actualErr) { if directive != nil && directive.CoalesceWithZero { actualErr = nil actualInt = 0 actual = 0 } } isEqual := actualErr == nil && expectedInt == actualInt if !isEqual { if text, ok := expected.(string); ok { if strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!") { assertText(toolbox.AsString(expected), toolbox.AsString(actual), path, context, validation) return } } validation.AddFailure(NewFailure(path.Source(), path.Path(), EqualViolation, expected, actual)) } else { validation.PassedCount++ } } func assertFloat(expected, actual interface{}, path DataPath, context *Context, validation *Validation) { directive := path.Directive() expectedFloat, expectedErr := toolbox.ToFloat(expected) if toolbox.IsNilPointerError(expectedErr) && directive.CoalesceWithZero && directive.StrictMapCheck { expectedErr = nil expectedFloat = 0 expected = 0 } actualFloat, actualErr := toolbox.ToFloat(actual) if toolbox.IsNilPointerError(actualErr) { if directive != nil && directive.CoalesceWithZero { actualErr = nil actualFloat = 0 actual = 0 } } if directive != nil { precisionPoint := float64(directive.NumericPrecisionPoint) if expectedErr == nil && actualErr == nil && precisionPoint >= 0 { unit := 1 / math.Pow(10, precisionPoint) expectedFloat = math.Round(expectedFloat/unit) * unit actualFloat = math.Round(actualFloat/unit) * unit } } isEqual := expectedErr == nil && actualErr == nil && expectedFloat == actualFloat if !isEqual { if text, ok := expected.(string); ok { if strings.HasPrefix(text, "/") || strings.HasPrefix(text, "!") { assertText(toolbox.AsString(expected), toolbox.AsString(actual), path, context, validation) return } } if expectedErr == nil && float64(int(expectedFloat)) == expectedFloat { expected = int(expectedFloat) } if actualErr == nil && float64(int(actualFloat)) == actualFloat { actual = int(actualFloat) } validation.AddFailure(NewFailure(path.Source(), path.Path(), EqualViolation, expected, actual)) } else { validation.PassedCount++ } } func assertPathIfNeeded(directive *Directive, path DataPath, context *Context, validation *Validation, actual map[string]interface{}) error { if len(directive.AssertPaths) > 0 { actualMap := data.Map(actual) for _, assertPath := range directive.AssertPaths { keyPath := path.Key(assertPath.SubPath) subPathActual, ok := actualMap.GetValue(assertPath.SubPath) if !ok { if assertPath.Expected == KeyDoesNotExistsDirective { validation.PassedCount++ } else { validation.AddFailure(NewFailure(path.Source(), keyPath.Path(), KeyExistsViolation, assertPath.Expected, actual)) } continue } if err := assertValue(assertPath.Expected, subPathActual, keyPath, context, validation); err != nil { return err } } } return nil } func assertMap(expected map[string]interface{}, actualValue interface{}, path DataPath, context *Context, validation *Validation) error { if actualValue == nil { if expected == nil { validation.PassedCount++ return nil } validation.AddFailure(NewFailure(path.Source(), path.Path(), ValueWasNil, nil, expected)) return nil } directive := NewDirective(path) directive.mergeFrom(path.Match(context)) directive.ExtractDirectives(expected) path.SetSource(directive.Source) var actual = actualMap(expected, actualValue, path, directive, validation) if actual == nil { return nil } if err := assertPathIfNeeded(directive, path, context, validation, actual); err != nil { return err } directive.ExtractDataTypes(actual) if err := directive.Apply(actual); err != nil { log.Print("failed to apply directive to actual actual value: " + err.Error()) } if len(directive.SwitchBy) > 0 { switchValue := keysValue(actual, directive.SwitchBy...) caseValue, ok := expected[switchValue] if !ok { validation.AddFailure(NewFailure(path.Source(), path.Path(), MissingCaseViolation, expected, actual, directive.SwitchBy, switchValue)) return nil } if !toolbox.IsMap(caseValue) { return fmt.Errorf("case value should be map but was %T, path: %v", caseValue, path.Path()) } caseValueMap := toolbox.AsMap(caseValue) if shared, ok := expected[SharedSwitchCaseKey]; ok && toolbox.IsMap(shared) { for k, v := range toolbox.AsMap(shared) { caseValueMap[k] = v } } expected = caseValueMap } if err := directive.Apply(expected); err != nil { log.Print("failed to apply directive to expected value:" + err.Error()) } indexable := isIndexable(expected) if len(directive.IndexBy) == 0 { indexable = false } if len(directive.Lengths) > 0 { for key, expectedLength := range directive.Lengths { aMap := data.Map(actual) value, ok := aMap.GetValue(key) keyPath := path.Key(key) if !ok { validation.AddFailure(NewFailure(keyPath.Source(), keyPath.Path(), LengthViolation, expectedLength, value)) continue } actualLength := 0 if value != nil { if toolbox.IsSlice(value) { actualLength = len(toolbox.AsSlice(value)) } else if toolbox.IsMap(value) { actualLength = len(toolbox.AsMap(value)) } } if actualLength == expectedLength { validation.PassedCount++ continue } validation.AddFailure(NewFailure(keyPath.Source(), keyPath.Path(), LengthViolation, expectedLength, actualLength)) } } var checkedKeys map[string]bool if directive.StrictMapCheck { checkedKeys = getKeys(expected, actual) } else { checkedKeys = getKeys(expected) } for expectedKey := range checkedKeys { expectedValue := expected[expectedKey] if directive.IsDirectiveKey(expectedKey) { continue } var keyPath DataPath if indexable && toolbox.IsMap(expectedValue) { keyPath = path.Key(keysPairValue(toolbox.AsMap(expectedValue), directive.IndexBy...)) } else { keyPath = path.Key(expectedKey) } actualValue, ok := actual[expectedKey] if directive.KeyDoesNotExist[expectedKey] { if ok { validation.AddFailure(NewFailure(keyPath.Source(), keyPath.Path(), KeyDoesNotExistViolation, expectedKey, expectedKey)) } else { validation.PassedCount++ } continue } if directive.KeyExists[expectedKey] { if !ok { availableKeys := toolbox.MapKeysToStringSlice(expected) validation.AddFailure(NewFailure(keyPath.Source(), keyPath.Path(), KeyExistsViolation, expectedKey, strings.Join(availableKeys, ","))) } else { validation.PassedCount++ } continue } if !ok { key := "key:" + expectedKey available := toolbox.MapKeysToStringSlice(actual) if len(available) > 32 { available = append(available[0:16], "...") } validation.AddFailure(NewFailure(keyPath.Source(), keyPath.Path(), MissingEntryViolation, expectedValue, available, key)) continue } if err := assertValue(expectedValue, actualValue, keyPath, context, validation); err != nil { return err } } return nil } func getKeys(mapList ...map[string]interface{}) map[string]bool { result := make(map[string]bool, 0) for _, mapElement := range mapList { for key := range mapElement { result[key] = true } } return result } func asKeyCaseInsensitiveSlice(aSlice []interface{}) []interface{} { var result = make([]interface{}, 0) for _, item := range aSlice { result = append(result, asKeyCaseInsensitiveMap(toolbox.AsMap(item))) } return result } func asKeyCaseInsensitiveMap(aMap map[string]interface{}) map[string]interface{} { var result = make(map[string]interface{}) for k, v := range aMap { result[strings.ToUpper(k)] = v } return result } func asValueCaseInsensitiveSlice(aSlice []interface{}) []interface{} { var result = make([]interface{}, 0) for _, item := range aSlice { aMap := toolbox.AsMap(item) aMap[CaseSensitiveDirective] = false result = append(result, aMap) } return result } func assertSlice(expected []interface{}, actualValue interface{}, path DataPath, context *Context, validation *Validation) error { if actualValue == nil { validation.AddFailure(NewFailure(path.Source(), path.Path(), IncompatibleDataTypeViolation, expected, actualValue)) return nil } if toolbox.IsMap(actualValue) { //given that pairs of key/value makes a map expectedMap, err := toolbox.ToMap(expected) if err == nil { return assertMap(expectedMap, actualValue, path, context, validation) } } if !toolbox.IsSlice(actualValue) { validation.AddFailure(NewFailure(path.Source(), path.Path(), IncompatibleDataTypeViolation, expected, actualValue)) return nil } var actual = toolbox.AsSlice(actualValue) if len(expected) == 0 { if len(expected) == len(actual) { validation.PassedCount++ return nil } validation.AddFailure(NewFailure(path.Source(), path.Path(), LengthViolation, len(expected), len(actual))) return nil } directive := path.Match(context) if toolbox.IsMap(expected[0]) || toolbox.IsStruct(expected[0]) { first := toolbox.AsMap(expected[0]) if directive.ExtractDirectives(first) { expected = expected[1:] } if directive.SortText { var expectedSlice = []string{} toolbox.ProcessSlice(expected, func(item interface{}) bool { expectedSlice = append(expectedSlice, toolbox.AsString(item)) return true }) var actualSlice = []string{} toolbox.ProcessSlice(expected, func(item interface{}) bool { actualSlice = append(actualSlice, toolbox.AsString(item)) return true }) sort.Strings(expectedSlice) expected = []interface{}{} for _, item := range expectedSlice { expected = append(expected, item) } sort.Strings(actualSlice) actual = []interface{}{} for _, item := range actualSlice { actual = append(actual, item) } } else { if !directive.KeyCaseSensitive { expected = asKeyCaseInsensitiveSlice(expected) actual = asKeyCaseInsensitiveSlice(actual) directive.ApplyKeyCaseInsensitive() } if !directive.CaseSensitive { expected = asValueCaseInsensitiveSlice(expected) actual = asValueCaseInsensitiveSlice(actual) } for i := 0; i < len(actual); i++ { var actualMap = toolbox.AsMap(actual[i]) directive.ExtractDataTypes(actualMap) } //add directive to expected for i := 0; i < len(expected); i++ { var expectedMap = toolbox.AsMap(expected[i]) directive.Add(expectedMap) directive.Apply(expectedMap) expected[i] = expectedMap if i < len(actual) { actualMap := toolbox.AsMap(actual[i]) directive.Apply(actualMap) actual[i] = actualMap } } shouldIndex := len(directive.IndexBy) > 0 if shouldIndex { expectedMap := indexSliceBy(expected, directive.IndexBy...) actualMap := indexSliceBy(actual, directive.IndexBy...) return assertMap(expectedMap, actualMap, path, context, validation) } } } for i := 0; i < len(expected); i++ { if i >= len(actual) { validation.AddFailure(NewFailure(path.Source(), path.Path(), LengthViolation, len(expected), len(actual))) return nil } indexPath := path.Index(i) if err := assertValue(expected[i], actual[i], indexPath, context, validation); err != nil { return err } } return nil } assertly-0.5.4/validator_test.go000066400000000000000000000740641367316370600167700ustar00rootroot00000000000000package assertly_test import ( "fmt" "github.com/stretchr/testify/assert" "github.com/viant/assertly" "github.com/viant/toolbox" "os" "testing" ) type assertUseCase struct { Description string Expected interface{} Actual interface{} PassedCount int FailedCount int HasError bool } func TestAssertMap(t *testing.T) { var useCases = []*assertUseCase{ { Description: "missing key test", Expected: map[string]interface{}{ "k1": 1, "k2": 2.0, "k3": 11, }, Actual: map[string]interface{}{ "k1": 1, "k3": 11, }, PassedCount: 2, FailedCount: 1, }, { Description: "a map test", Expected: map[string]interface{}{ "k1": 1, "k2": 2.0, "k3": 11, }, Actual: map[string]interface{}{ "k1": 1, "k2": 2.0, "k3": 11, }, PassedCount: 3, FailedCount: 0, }, { Description: "key does not exist violation test", Expected: map[string]interface{}{ "k1": assertly.KeyDoesNotExistsDirective, "k2": 2.0, }, Actual: map[string]interface{}{ "k1": 1, "k2": 2.0, }, PassedCount: 1, FailedCount: 1, }, { Description: "key does not exist test", Expected: map[string]interface{}{ "k1": assertly.KeyDoesNotExistsDirective, "k2": 2.0, }, Actual: map[string]interface{}{ "k2": 2.0, }, PassedCount: 2, FailedCount: 0, }, { Description: "key exists violation test", Expected: map[string]interface{}{ "k1": assertly.KeyExistsDirective, "k2": 2.0, }, Actual: map[string]interface{}{ "k2": 2.0, }, PassedCount: 1, FailedCount: 1, }, { Description: "key exists test", Expected: map[string]interface{}{ "k1": assertly.KeyExistsDirective, "k2": 2.0, }, Actual: map[string]interface{}{ "k1": 2.0, "k2": 2.0, }, PassedCount: 2, FailedCount: 0, }, { Description: "slice incompatible data type test", Expected: map[string]interface{}{ "1": map[string]interface{}{ "id": 1, "name": "name 1", }, "2": map[string]interface{}{ "id": 2, "name": "name 2", }, }, Actual: []interface{}{ map[string]interface{}{ "id": 1, "name": "name 1", }, map[string]interface{}{ "id": 2, "name": "name 2", }, }, PassedCount: 0, FailedCount: 1, }, { Description: "incompatible data type test", Expected: map[string]interface{}{ assertly.IndexByDirective: "id", "1": map[string]interface{}{ "id": 1, "name": "name 1", }, "2": map[string]interface{}{ "id": 2, "name": "name 2", }, }, Actual: "123", PassedCount: 0, FailedCount: 1, }, { Description: "slice index test", Expected: map[string]interface{}{ assertly.IndexByDirective: "id", "1": map[string]interface{}{ "id": 1, "name": "name 1", }, "2": map[string]interface{}{ "id": 2, "name": "name 2", }, }, Actual: []interface{}{ map[string]interface{}{ "id": 1, "name": "name 1", }, map[string]interface{}{ "id": 2, "name": "name 2", }, }, PassedCount: 4, FailedCount: 0, }, { Description: "two slice with index test", Expected: []interface{}{ map[string]interface{}{ assertly.IndexByDirective: "id", }, map[string]interface{}{ "id": 1, "name": "name 1", }, map[string]interface{}{ "id": 2, "name": "name 2", }, }, Actual: []interface{}{ map[string]interface{}{ "id": 2, "name": "name 2", }, map[string]interface{}{ "id": 1, "name": "name 1", }, }, PassedCount: 4, FailedCount: 0, }, { Description: "expected apply error", Expected: map[string]interface{}{ assertly.CastDataTypeDirective + "k2": "abc", "k2": 2.0, }, Actual: map[string]interface{}{}, FailedCount: 1, }, { Description: "time format directive test", Expected: map[string]interface{}{ assertly.TimeFormatDirective + "k2": "yyyy-MM-dd", assertly.TimeFormatDirective + "k4": "yyyy-MM-dd", assertly.TimeFormatDirective: "yyyy-MM-dd hh:mm:ss", "k2": "2019-01-01", }, Actual: map[string]interface{}{ "k2": "2019-01-01", }, PassedCount: 1, }, { Description: "actual error", Expected: map[string]interface{}{ assertly.TimeFormatDirective + "k2": "yyyy-MM-dd", "k2": "2019-01-01", }, Actual: map[string]interface{}{ "k2": "99-99-99", }, FailedCount: 1, }, { Description: "sortText use case", Expected: []interface{}{ map[string]interface{}{ "@sortText@": true, }, "z523", "abc", "ax5", }, Actual: []interface{}{ "ax5", "z523", "abc", }, HasError: false, PassedCount: 3, FailedCount: 0, }, { Description: "length directive use case", Expected: map[string]interface{}{ "@length@key1": 3, "f2": 2, }, Actual: map[string]interface{}{ "key1": []interface{}{1, 2, 3}, "f2": 2, }, HasError: false, PassedCount: 2, FailedCount: 0, }, { Description: "length directive failure use case", Expected: map[string]interface{}{ "@length@key1": 3, "f2": 2, }, Actual: map[string]interface{}{ "key1": []interface{}{1, 2, 3, 4}, "f2": 2, }, HasError: false, PassedCount: 1, FailedCount: 1, }, { Description: "length directive missing failure use case", Expected: map[string]interface{}{ "@length@key1": 3, "f2": 2, }, Actual: map[string]interface{}{ "f2": 2, }, HasError: false, PassedCount: 1, FailedCount: 1, }, } runUseCases(t, useCases) } func TestAssert_StictMapCheckAbsent(t *testing.T) { var useCases = []*assertUseCase{ { Description: "strict keys", Expected: `[ { "@keyCaseSensitive@": true, "@coalesceWithZero@":true }, { "k1": "value1", "k2": "value2" }, { "k1": "valueA", "k2": "valueB" } ]`, Actual: `[ { "k1": "value1", "k2": "value2", "k8": "value8" }, { "k1": "valueA", "k2": "valueB", "k9": "valueZ" } ]`, PassedCount: 4, FailedCount: 0, }, } runUseCases(t, useCases) } func TestAssert_StictMapCheckFalse(t *testing.T) { var useCases = []*assertUseCase{ { Description: "strict keys", Expected: `[ { "@keyCaseSensitive@": true, "@coalesceWithZero@": true, "@strictMapCheck@": false }, { "k1": "value1", "k2": "value2" }, { "k1": "valueA", "k2": "valueB" } ]`, Actual: `[ { "k1": "value1", "k2": "value2", "k8": "value8" }, { "k1": "valueA", "k2": "valueB", "k9": "valueZ" } ]`, PassedCount: 4, FailedCount: 0, }, } runUseCases(t, useCases) } func TestAssert_StictMapCheckTrue(t *testing.T) { var useCases = []*assertUseCase{ { Description: "strict keys", Expected: `[ { "@keyCaseSensitive@": false, "@coalesceWithZero@": true, "@strictMapCheck@": true }, { "k1": "value1", "k2": "value2", "k3": null, "k5": null }, { "k1": "valueA", "k2": "valueB", "k4": null, "k6": 0.0 } ]`, Actual: `[ { "k1": "value1", "k2": "value2", "k3": "zero", "k5": 0, "k8": 98 }, { "k1": "valueA", "k2": "valueB", "k4": "one", "k6": null, "k9": 99 } ]`, PassedCount: 6, FailedCount: 4, }, } runUseCases(t, useCases) } func TestAssert_CaseInsensitive(t *testing.T) { var useCases = []*assertUseCase{ { Description: "case sensitive", Expected: `[ { "@keyCaseSensitive@": false, "@indexBy@": [ "ID" ], "@timeFormat@modified": "yyyy-MM-dd HH:mm:ss" }, { "active": true, "comments": "dsunit test", "id": 1, "modified": "2016-03-01 03:10:00", "salary": 12400, "username": "Dudi", "@source@":"s1" }, { "active": true, "comments": "def", "id": 2, "modified": "2016-03-01 05:10:00", "salary": 12600, "username": "Rudi" } ]`, Actual: `[ { "ACTIVE": 1, "COMMENTS": "dsunit test", "ID": 1, "MODIFIED": "2016-03-01 03:10:00", "SALARY": 12400, "USERNAME": "Dudi" }, { "ACTIVE": 1, "COMMENTS": "def", "ID": 2, "MODIFIED": "2016-03-01 05:10:00", "SALARY": 12600, "USERNAME": "Rudi" } ]`, PassedCount: 12, }, } runUseCases(t, useCases) } func TestAssertSlice(t *testing.T) { var useCases = []*assertUseCase{ { Description: "slice test", Expected: []int{1, 2, 3}, Actual: []int{1, 2, 3}, PassedCount: 3, }, { Description: "slice nil", Expected: []int{1, 2, 3}, Actual: nil, FailedCount: 1, }, { Description: "slice not equal test", Expected: []int{1, 2, 3}, Actual: []int{1, 2, 4}, PassedCount: 2, FailedCount: 1, }, { Description: "slice len not equal test", Expected: []int{1, 2, 3}, Actual: []int{1, 2}, PassedCount: 2, FailedCount: 1, }, { Description: "expected slice shorter - no violation since only supplied element are expected to be validated", Expected: []int{1, 2}, Actual: []int{1, 2, 3}, PassedCount: 2, FailedCount: 0, }, { Description: "indexed slice test", Expected: []map[string]interface{}{ { assertly.IndexByDirective: "key", }, { "key": 1, "x": 100, "y": 200, }, { "key": 2, "x": 200, "y": 300, }, }, Actual: []map[string]interface{}{ { "key": 2, "x": 200, "y": 300, }, { "key": 1, "x": 100, "y": 200, }, }, PassedCount: 6, }, { Description: "incompatible slice type test", Expected: []int{1, 2}, Actual: "1,2,3", PassedCount: 0, FailedCount: 1, }, { Description: "different len slice test", Expected: []int{}, Actual: []int{1}, PassedCount: 0, FailedCount: 1, }, { Description: "zero len slice test", Expected: []int{}, Actual: []int{}, PassedCount: 1, }, { Description: "indexed slice cast error test", Expected: []map[string]interface{}{ { assertly.IndexByDirective: "key", assertly.CastDataTypeDirective + "x": "float", }, { "key": 1, "x": 100, "y": 200, }, { "key": 2, "x": 200, "y": 300, }, }, Actual: []map[string]interface{}{ { "key": 2, "x": "abc", "y": 300, }, { "key": 1, "x": "100", "y": 200, }, }, PassedCount: 5, FailedCount: 1, }, { Description: "indexed slice cast test", Expected: []map[string]interface{}{ { "key": 1, "x": 100, "y": 200, assertly.CastDataTypeDirective + "x": "float", }, }, Actual: []map[string]interface{}{ { "key": 1, "x": "xyz", "y": 200, }, }, PassedCount: 2, FailedCount: 1, }, } runUseCases(t, useCases) } func TestAssertJSONSlice(t *testing.T) { var useCases = []*assertUseCase{ { Description: "JSON slice test", Expected: `[1,2,3] [3,4]`, Actual: `[1,2,3] [3,5]`, PassedCount: 4, FailedCount: 1, }, { Description: "broken JSON slice test", Expected: `[1,2,3] [2,] [3,4]`, Actual: `[1,2,3] [3,5]`, PassedCount: 0, FailedCount: 1, }, } runUseCases(t, useCases) } func TestAssertText(t *testing.T) { var useCases = []*assertUseCase{ { Description: "text qual test", Expected: "123", Actual: "123", PassedCount: 1, }, { Description: "text qual test", Expected: "123", Actual: "1234", FailedCount: 1, }, { Description: "text qual test", Expected: "!123", Actual: "1234", PassedCount: 1, }, { Description: "text equal test", Expected: "!123", Actual: "123", FailedCount: 1, }, { Description: "text qual test", Expected: "!0", Actual: "0", FailedCount: 1, }, } runUseCases(t, useCases) } type TestStructA struct { K1 string K2 int } func TestAssertStruct(t *testing.T) { var useCases = []*assertUseCase{ { Description: "struct test", Expected: &TestStructA{K1: "123", K2: 123}, Actual: &TestStructA{K1: "123", K2: 123}, PassedCount: 2, FailedCount: 0, }, { Description: "struct with JSON test", Expected: &TestStructA{K1: "123", K2: 123}, Actual: `{"K1":"123", "K2":124}`, PassedCount: 1, FailedCount: 1, }, } runUseCases(t, useCases) } func TestAssertSwitchCase(t *testing.T) { var useCases = []*assertUseCase{ { Description: "switch/case test", Expected: `{ "@switchCaseBy@":"alg", "1": { "alg":1, "value":100 }, "2":{ "alg":2, "value":200 } }`, Actual: `{ "alg":2, "value":200 } `, PassedCount: 2, FailedCount: 0, }, { Description: "missing switch/case test", Expected: `{ "@switchCaseBy@":"alg", "1": { "alg":1, "value":100 }, "2":{ "alg":2, "value":200 } }`, Actual: `{ "alg":3, "value":200 } `, PassedCount: 0, FailedCount: 1, }, { Description: "missing switch/case setup error test", Expected: `{ "@switchCaseBy@":"alg", "1": 1, "2": 2 }`, Actual: `{ "alg":1, "value":200 } `, HasError: true, }, { Description: "switch/case with shared values test", PassedCount: 2, FailedCount: 1, Expected: `[ { "@switchCaseBy@": "algid", "1": { "algid": 1, "t": "640,650,750,753" }, "2": { "algid": 2, "t": "640,650,750,753" }, "shared": { "d": 2 } } ]`, Actual: `[ { "algid": 1, "t": "640,650,750,753", "d": 1 } ]`, }, { Description: "switch/case with shared values test", PassedCount: 3, Expected: `[ { "@switchCaseBy@": "algid", "1": { "algid": 1, "t": "640,650,750,753" }, "2": { "algid": 2, "t": "640,650,750,753" }, "shared": { "d": 2 } } ]`, Actual: `[ { "algid": 1, "t": "640,650,750,753", "d": 2 } ]`, }, } runUseCases(t, useCases) } func TestAssertWithGlobalDirective(t *testing.T) { context := assertly.NewDefaultContext() directivePath := assertly.NewDataPath("") { directive1 := assertly.NewDirective(directivePath) directive1.AddDataType("id", "int") directive1.AddDataType("isEnabled", "bool") directive2 := assertly.NewDirective(directivePath.Key("k1")) directive2.AddTimeLayout("date", toolbox.DateFormatToLayout("yyyy-MM-dd")) testPath := assertly.NewDataPath("root") context.Directives = assertly.NewDirectives(directive1, directive2) { validation, err := assertly.AssertWithContext(`{ "id":"213", "isEnabled":false, "done":"true" } `, `{ "id":213, "isEnabled":"false", "done":"true1" } `, testPath.Key("field"), context) assert.Nil(t, err) assert.Equal(t, 2, validation.PassedCount) assert.Equal(t, 1, validation.FailedCount) } { validation, err := assertly.AssertWithContext(`{ "date":"2017-01-01", "id":1 } `, `{ "date":"2017-01-01", "id":1 } `, testPath.Key("k1"), context) assert.Nil(t, err) assert.Equal(t, 2, validation.PassedCount) assert.Equal(t, 0, validation.FailedCount) } } } func TestAssertRegExpr(t *testing.T) { var useCases = []*assertUseCase{ { Description: "reg expr test", Expected: "~/.+(\\d+).+/", Actual: "avc1erwer", PassedCount: 1, FailedCount: 0, }, { Description: "reg expr test", Expected: "~/.+(\\d+).+/", Actual: "avcerwer", PassedCount: 0, FailedCount: 1, }, { Description: "reg expr not test", Expected: "!~/.+(\\d+).+/", Actual: "avc1erwer", PassedCount: 0, FailedCount: 1, }, { Description: "multiline reg expr not test", Expected: "!~/.+(\\d+).+/", Actual: "avc\ner\nwer", PassedCount: 1, FailedCount: 0, }, { Description: "multiline reg expr test", Expected: "~/^1.+3$/", Actual: "1avc\n1ass3\nwer4", PassedCount: 1, FailedCount: 0, }, { Description: "reg expr compilation error test", Expected: "~/m???:1/", Actual: "123", HasError: true, }, } runUseCases(t, useCases) } func TestAssertMacro(t *testing.T) { var useCases = []*assertUseCase{ { Description: "macro-predicate test", Expected: "", Actual: "3", PassedCount: 1, FailedCount: 0, }, { Description: "macro-predicate violation test", Expected: "", Actual: "13", PassedCount: 0, FailedCount: 1, }, { Description: "macro-predicate error test", Expected: "", Actual: "13", HasError: true, }, { Description: "macro expansion", Expected: `13`, Actual: fmt.Sprintf("1%v3", os.Getenv("USER")), PassedCount: 1, }, } runUseCases(t, useCases) } func TestAssertRange(t *testing.T) { var useCases = []*assertUseCase{ { Description: "range min max test", Expected: "/[1..10]/", Actual: "3", PassedCount: 1, FailedCount: 0, }, { Description: "range min max test", Expected: "/[1..10]/", Actual: "30", PassedCount: 0, FailedCount: 1, }, { Description: "not in min max range test", Expected: "!/[1..10]/", Actual: "30", PassedCount: 1, FailedCount: 0, }, { Description: "range min max test", Expected: "!/[1..10]/", Actual: "3", PassedCount: 0, FailedCount: 1, }, { Description: "range test", Expected: "/[1,3,10]/", Actual: "3", PassedCount: 1, FailedCount: 0, }, { Description: "range test", Expected: "/[1,3,10]/", Actual: "4", PassedCount: 0, FailedCount: 1, }, { Description: "range error test", Expected: "/[3]/", Actual: "4", HasError: true, }, } runUseCases(t, useCases) } func TestAssertContains(t *testing.T) { var useCases = []*assertUseCase{ { Description: "contain test", Expected: "/123/", Actual: "123456", PassedCount: 1, FailedCount: 0, }, { Description: "contain violation test", Expected: "/123/", Actual: "3456", PassedCount: 0, FailedCount: 1, }, { Description: "does not contain test", Expected: "!/123/", Actual: "30", PassedCount: 1, FailedCount: 0, }, { Description: "range min max test", Expected: "!/123/", Actual: "01234", PassedCount: 0, FailedCount: 1, }, } runUseCases(t, useCases) } func runUseCases(t *testing.T, useCases []*assertUseCase) { for _, useCase := range useCases { path := assertly.NewDataPath("/") validation, err := assertly.Assert(useCase.Expected, useCase.Actual, path) if err != nil { if useCase.HasError { continue } assert.Nil(t, err, useCase.Description) continue } else if useCase.HasError { assert.NotNil(t, err, useCase.Description) continue } assert.EqualValues(t, useCase.PassedCount, validation.PassedCount, "Passed count "+useCase.Description) if !assert.EqualValues(t, useCase.FailedCount, validation.FailedCount, "Failed count "+useCase.Description) { // fmt.Printf(validation.Report()) } } } func runUseCasesWithContext(t *testing.T, useCases []*assertUseCase, context *assertly.Context) { for _, useCase := range useCases { path := assertly.NewDataPath("/") validation, err := assertly.AssertWithContext(useCase.Expected, useCase.Actual, path, context) if err != nil { if useCase.HasError { continue } assert.Nil(t, err, useCase.Description) continue } else if useCase.HasError { assert.NotNil(t, err, useCase.Description) continue } assert.EqualValues(t, useCase.PassedCount, validation.PassedCount, "PassedCount "+useCase.Description) assert.EqualValues(t, useCase.FailedCount, validation.FailedCount, "FailedCount "+useCase.Description) assert.EqualValues(t, useCase.FailedCount > 0, validation.HasFailure()) if validation.HasFailure() { fmt.Printf("%v\n", validation.Report()) } } } func TestAssertStructure(t *testing.T) { var useCases = []*assertUseCase{ { Description: "data structure test", Expected: `{ "1": { "id":1, "name":"name 1" }, "2": { "id":2, "name":"name 2" } }`, Actual: `{ "1": { "id":1, "name":"name 1" }, "2": { "id":2, "name":"name 22" } }`, PassedCount: 3, FailedCount: 1, }, { Description: "data structure test", Expected: `{ "Meta": "abc", "Table": "abc", "Rows": [ { "id": 1, "name": "name 1" }, { "id": 2, "name": "name 2", "settings": { "k1": "v2" } }, { "id": 2, "name": "name 2" } ] }`, Actual: `{ "Table":"abc", "Rows":[ { "id":1, "name":"name 12" }, { "id":2, "name":"name 2", "settings": { "k1":"v20" } }, { "id":4, "name":"name 2" } ] }`, PassedCount: 5, FailedCount: 4, }, } runUseCases(t, useCases) } func TestAssertStructureWithIndexDirective(t *testing.T) { var useCases = []*assertUseCase{ { Description: "data structure with index directive", Expected: `{ "1": { "id":1, "seq":0, "name":"name 1" }, "2": { "id":2, "seq":0, "name":"name 2" } }`, Actual: `{ "1": { "id":1, "seq":0, "name":"name 1" }, "2": { "id":2, "seq":0, "name":"name 22" } }`, PassedCount: 5, FailedCount: 1, }, } defaultDirective := assertly.NewDirective(assertly.NewDataPath("")) defaultDirective.IndexBy = []string{"id", "seq"} context := assertly.NewContext(nil, assertly.NewDirectives(defaultDirective), nil) runUseCasesWithContext(t, useCases, context) } func TestAssertNumericPrecission(t *testing.T) { var useCases = []*assertUseCase{ { Description: "data structure with numericPrecisionPoint", Expected: `[ { "@numericPrecisionPoint@":"7" }, { "tac":0.006521405 } ] `, Actual: `[ { "tac": 0.0065214 } ]`, PassedCount: 1, FailedCount: 0, }, { Description: "data structure with 0 numericPrecisionPoint", Expected: `{ "@numericPrecisionPoint@":"0", "value":425147 }`, Actual: `{ "value": 425147.00000000006 }`, PassedCount: 1, FailedCount: 0, }, { Description: "data text expected text float and with 0 numericPrecisionPoint", Expected: `{ "@numericPrecisionPoint@":"0", "value": "425147" }`, Actual: `{ "value": 425147.00000000006 }`, PassedCount: 1, FailedCount: 0, }, { Description: "data text expected and actual text float and with 0 numericPrecisionPoint", Expected: `{ "@numericPrecisionPoint@":"0", "value": "425147" }`, Actual: `{ "value": "425147.00000000006" }`, PassedCount: 1, FailedCount: 0, }, } context := assertly.NewDefaultContext() runUseCasesWithContext(t, useCases, context) } func TestAssertCoalesceWithZero(t *testing.T) { var useCases = []*assertUseCase{ { Description: "data structure with coalesceWithZero", Expected: `[ { "@coalesceWithZero@": true }, { "tac":0 } ] `, Actual: `[ { "tac": null } ]`, PassedCount: 1, FailedCount: 0, }, { Description: "data structure without coalesceWithZero", Expected: `[ { "tac":0 } ] `, Actual: `[ { "tac": null } ]`, PassedCount: 0, FailedCount: 1, }, } context := assertly.NewDefaultContext() runUseCasesWithContext(t, useCases, context) } func TestAssertCaseSensitive(t *testing.T) { var useCases = []*assertUseCase{ { Description: "case insensitive", Expected: `[ { "@caseSensitive@": false }, { "tac":"ABC" } ] `, Actual: `[ { "tac": "abc" } ]`, PassedCount: 1, FailedCount: 0, }, { Description: "case sensitive", Expected: `[ { "@caseSensitive@": true }, { "tac":"ABC" } ] `, Actual: `[ { "tac": "abc" } ]`, PassedCount: 0, FailedCount: 1, }, } context := assertly.NewDefaultContext() runUseCasesWithContext(t, useCases, context) } func TestAssertMultiIndexBy(t *testing.T) { var useCases = []*assertUseCase{ { Description: "data structure with multi index directive", Expected: `{ "rr": { "id": "602b3d53-44f6-11e8-aa2a-5d0983199cde", "timestamp": "2018-04-20 23:56:00.109+00", "pp": { "Id": "602b3d51-44f6-11e8-aa2a-5d0983199cde", "seg": [ { "@indexBy@":"pId,id" }, { "pId": 501, "ids": [ 49 ] }, { "pId": -501, "ids": [ -49 ] }, { "id": -502, "ids": [ 50 ] } ] }, "ff": { "p": { "rp": 0.045, "op": 0.045 }, "alg": { "max": 0.06 } }, "ml": [ { "@indexBy@":"key" }, { "key": "NU_b", "value": "-1" }, { "key": "XR_b", "value": "1" } ] } }`, Actual: `{ "rr": { "id": "602b3d53-44f6-11e8-aa2a-5d0983199cde", "timestamp": "2018-04-20 23:56:00.109+00", "pp": { "Id": "602b3d51-44f6-11e8-aa2a-5d0983199cde", "seg": [ { "pId": 501, "ids": [ 49 ] }, { "id": -502, "ids": [ 50 ] }, { "pId": -501, "ids": [ -49 ] } ] }, "ff": { "p": { "rp": 0.045, "op": 0.045 }, "alg": { "max": 0.06 } }, "ml": [ { "key": "XR_b", "value": "1" }, { "key": "NU_b", "value": "-1" } ] } }`, PassedCount: 16, FailedCount: 0, }, } context := assertly.NewDefaultContext() runUseCasesWithContext(t, useCases, context) } func TestAssertStructureWithSource(t *testing.T) { var useCases = []*assertUseCase{ { Description: "data structure with index directive", Expected: `{ "1": { "@source@":"pk:1", "id":1, "seq":0, "name":"name 1" }, "2": { "@source@":"pk:2", "id":2, "seq":0, "name":"name 2" } }`, Actual: `{ "1": { "id":1, "seq":0, "name":"name 1" }, "2": { "id":2, "seq":0, "name":"name 22" } }`, PassedCount: 5, FailedCount: 1, }, } defaultDirective := assertly.NewDirective(assertly.NewDataPath("")) defaultDirective.IndexBy = []string{"id", "seq"} context := assertly.NewContext(nil, assertly.NewDirectives(defaultDirective), nil) runUseCasesWithContext(t, useCases, context) } func TestAssertWithAssertPath(t *testing.T) { var useCases = []*assertUseCase{ { Description: "data structure with assertPath directive", Expected: `{ "@assertPath@key1.id":1, "@assertPath@key2.id":2, "@assertPath@key2.name":"name 22" }`, Actual: `{ "key1": { "id":1, "seq":0, "name":"name 1" }, "key2": { "id":2, "seq":0, "name":"name 22" } }`, PassedCount: 3, FailedCount: 0, }, { Description: "data structure with assertPath directive and regular data", Expected: `{ "@assertPath@":{ "key1.id":1, "key2.id":2 }, "key3": { "id":3, "seq":3, "name":"name 3" } }`, Actual: `{ "key1": { "id":1, "seq":0, "name":"name 1" }, "key2": { "id":2, "seq":0, "name":"name 22" }, "key3": { "id":3, "seq":3, "name":"name 3" } }`, PassedCount: 5, FailedCount: 0, }, { Description: "complex check", Actual: `{ "Data": { "events": [ { "event_type": 2, "id": 1, "type":{ "id":2, "name":"type 2" }, "modified": "2020-01-15T02:11:00Z", "quantity": 33.23432374000549, "timestamp": "2019-03-11T02:20:33Z" }, { "event_type": 2, "id": 2, "modified": "2020-01-15T02:11:00Z", "quantity": 21.957962334156036, "timestamp": "2019-03-15T12:07:33Z" }, { "event_type": 2, "id": 3, "modified": "2020-01-15T02:11:00Z", "quantity": 5.084940046072006, "timestamp": "2019-04-10T05:15:33Z" }, { "event_type": 3, "id": 4, "modified": "2020-01-15T02:11:00Z", "quantity": 6.557567559182644, "timestamp": "2019-04-06T06:30:33Z" }, { "event_type": 3, "id": 5, "modified": "2020-01-15T02:11:00Z", "quantity": 26.21499478816986, "timestamp": "2019-03-21T04:08:33Z" }, { "event_type": 3, "id": 6, "modified": "2020-01-15T02:11:00Z", "quantity": 12.070401951670647, "timestamp": "2019-03-13T09:47:33Z" }, { "event_type": 4, "id": 7, "modified": "2020-01-15T02:11:00Z", "quantity": 16.948733657598495, "timestamp": "2019-03-18T09:10:33Z" }, { "event_type": 4, "id": 8, "modified": "2020-01-15T02:11:00Z", "quantity": 15.302604854106903, "timestamp": "2019-03-28T11:16:33Z" }, { "event_type": 5, "id": 9, "modified": "2020-01-15T02:11:00Z", "quantity": 33.91697895526886, "timestamp": "2019-04-23T12:12:33Z" }, { "event_type": 5, "id": 10, "modified": "2020-01-15T02:11:00Z", "quantity": 11.549782872200012, "timestamp": "2019-04-24T06:51:33Z" }, { "event_type": 5, "id": 11, "modified": "2020-01-15T02:11:00Z", "quantity": 28.821644484996796, "timestamp": "2019-04-01T02:02:33Z" } ] }, "Rule": { "Info": { "URL": "file://localhost/Projects/go/workspace/src/github.com/viant/datly/reader/test/read/case001/rule/event_uri_1.yaml" }, "Outputs": [ { "DataView": "events", "Key": "events" } ], "URI": "/case001/", "URIPrefix": "/case001/", "UseCache": false, "Views": [ { "Connector": "db", "Name": "events", "Table": "events" } ] }, "Status": "ok" }`, Expected: `{ "@assertPath@Data.events[0].id": 1, "@assertPath@Data.events[0].type.name": "type 2", "@assertPath@Data.events[0].type.id": 2, "@length@Data.events":11 }`, PassedCount: 4, }, } defaultDirective := assertly.NewDirective(assertly.NewDataPath("")) context := assertly.NewContext(nil, assertly.NewDirectives(defaultDirective), nil) runUseCasesWithContext(t, useCases, context) } assertly-0.5.4/value_provider.go000066400000000000000000000021701367316370600167570ustar00rootroot00000000000000package assertly import ( "github.com/viant/toolbox" ) //ValueProviderRegistry represents value provider ValueProviderRegistry var ValueProviderRegistry = toolbox.NewValueProviderRegistry() func init() { ValueProviderRegistry.Register("nil", toolbox.NewNilValueProvider()) ValueProviderRegistry.Register("empty", toolbox.NewConstValueProvider("")) ValueProviderRegistry.Register("env", toolbox.NewEnvValueProvider()) ValueProviderRegistry.Register("cast", toolbox.NewCastedValueProvider()) ValueProviderRegistry.Register("timediff", toolbox.NewTimeDiffProvider()) ValueProviderRegistry.Register("current_timestamp", toolbox.NewCurrentTimeProvider()) ValueProviderRegistry.Register("current_date", toolbox.NewCurrentDateProvider()) ValueProviderRegistry.Register("between", toolbox.NewBetweenPredicateValueProvider()) ValueProviderRegistry.Register("within_sec", toolbox.NewWithinSecPredicateValueProvider()) ValueProviderRegistry.Register("weekday", toolbox.NewWeekdayProvider()) ValueProviderRegistry.Register("dob", toolbox.NewDateOfBirthrovider()) ValueProviderRegistry.Register("cat", toolbox.NewFileValueProvider(true)) }