├── .github └── workflows │ └── go.yml ├── .gitignore ├── .golangci.yml ├── .vale.ini ├── AUTHORS.md ├── LICENSE ├── README.md ├── example_test.go ├── go.mod ├── go.sum ├── reflections.go └── reflections_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Set up Go 1.x 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: '^1.13' 19 | 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v4 22 | 23 | - name: Build 24 | run: go build -v . 25 | 26 | - name: Test 27 | run: go test -v . 28 | 29 | - name: Lint 30 | uses: golangci/golangci-lint-action@v6.1.0 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | styles/ -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # v1.47.2 2 | # Please don't remove the first line. It uses in CI to determine the golangci version 3 | run: 4 | deadline: 5m 5 | 6 | issues: 7 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 8 | max-issues-per-linter: 0 9 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 10 | max-same-issues: 0 11 | 12 | # We want to try and improve the comments in the k6 codebase, so individual 13 | # non-golint items from the default exclusion list will gradually be addded 14 | # to the exclude-rules below 15 | exclude-use-default: false 16 | 17 | exclude-rules: 18 | # Exclude duplicate code and function length and complexity checking in test 19 | # files (due to common repeats and long functions in test code) 20 | - path: _(test|gen)\.go 21 | linters: 22 | - cyclop 23 | - dupl 24 | - gocognit 25 | - funlen 26 | - lll 27 | - linters: 28 | - paralleltest # false positive: https://github.com/kunwardeep/paralleltest/issues/8. 29 | text: "does not use range value in test Run" 30 | 31 | linters-settings: 32 | exhaustive: 33 | default-signifies-exhaustive: true 34 | govet: 35 | enable-all: true 36 | disable: 37 | - fieldalignment 38 | cyclop: 39 | max-complexity: 25 40 | dupl: 41 | threshold: 150 42 | goconst: 43 | min-len: 10 44 | min-occurrences: 4 45 | funlen: 46 | lines: 80 47 | statements: 60 48 | 49 | linters: 50 | enable-all: true 51 | disable: 52 | - depguard 53 | - nlreturn 54 | - gci 55 | - gochecknoinits 56 | - godot 57 | - godox 58 | - gomodguard 59 | - testpackage 60 | - wsl 61 | - gomnd 62 | - err113 63 | - goheader 64 | - thelper 65 | - gocyclo # replaced by cyclop since it also calculates the package complexity 66 | - wrapcheck # a little bit too much for k6, maybe after https://github.com/tomarrell/wrapcheck/issues/2 is fixed 67 | - varnamelen 68 | - ireturn 69 | - tagliatelle 70 | - exhaustruct 71 | - execinquery 72 | - maintidx 73 | - grouper 74 | - decorder 75 | - nonamedreturns 76 | fast: false -------------------------------------------------------------------------------- /.vale.ini: -------------------------------------------------------------------------------- 1 | StylesPath = styles 2 | 3 | MinAlertLevel = suggestion 4 | Vocab = Lane 5 | 6 | Packages = Google, proselint, write-good 7 | 8 | [*] 9 | BasedOnStyles = Vale, Google, proselint, write-good 10 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | ## Creator 2 | 3 | * Oleiade 4 | 5 | ## Contributors 6 | 7 | * Cengle 8 | * Tomo Krajina 9 | * Seth Shelnutt 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Théo Crevon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reflections 2 | 3 | [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) 4 | [![Build Status](https://github.com/oleiade/reflections/actions/workflows/go.yml/badge.svg)](https://github.com/oleiade/reflections/actions/workflows/go.yml) 5 | [![Go Documentation](https://pkg.go.dev/badge/github.com/oleiade/reflections)](https://pkg.go.dev/github.com/oleiade/reflections) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/oleiade/reflections)](https://goreportcard.com/report/github.com/oleiade/reflections) 7 | ![Go Version](https://img.shields.io/github/go-mod/go-version/oleiade/reflections) 8 | 9 | The `reflections` library provides high-level abstractions on top of the go language standard `reflect` library. 10 | 11 | In practice, the `reflect` library's API proves somewhat low-level and un-intuitive. Using it can turn out pretty complex, daunting, and scary, especially when doing simple things like accessing a structure field value, a field tag, etc. 12 | 13 | The `reflections` package aims to make developers' life easier when it comes to introspect struct values at runtime. Its API takes inspiration in the python language's `getattr,` `setattr,` and `hasattr` set of methods and provides simplified access to structure fields and tags. 14 | 15 | - [Reflections](#reflections) 16 | - [Documentation](#documentation) 17 | - [Usage](#usage) 18 | - [`GetField`](#getfield) 19 | - [`GetFieldKind`](#getfieldkind) 20 | - [`GetFieldType`](#getfieldtype) 21 | - [`GetFieldTag`](#getfieldtag) 22 | - [`HasField`](#hasfield) 23 | - [`Fields`](#fields) 24 | - [`Items`](#items) 25 | - [`Tags`](#tags) 26 | - [`GetFieldNameByTagValue`](#getfieldnamebytagvalue) 27 | - [Important notes](#important-notes) 28 | - [Contribute](#contribute) 29 | 30 | ## Documentation 31 | 32 | Head to the [documentation](https://pkg.go.dev/github.com/oleiade/reflections) to get more details on the library's API. 33 | 34 | ## Usage 35 | 36 | ### `GetField` 37 | 38 | `GetField` returns the content of a structure field. For example, it proves beneficial when you want to iterate over struct-specific field values. You can provide `GetField` a structure or a pointer to a struct as the first argument. 39 | 40 | ```go 41 | s := MyStruct { 42 | FirstField: "first value", 43 | SecondField: 2, 44 | ThirdField: "third value", 45 | } 46 | 47 | fieldsToExtract := []string{"FirstField", "ThirdField"} 48 | 49 | for _, fieldName := range fieldsToExtract { 50 | value, err := reflections.GetField(s, fieldName) 51 | DoWhatEverWithThatValue(value) 52 | } 53 | ``` 54 | 55 | ### `GetFieldKind` 56 | 57 | `GetFieldKind` returns the [`reflect.Kind`](http://golang.org/src/pkg/reflect/type.go?s=6916:6930#L189) of a structure field. You can use it to operate type assertion over a structure field at runtime. You can provide `GetFieldKind` a structure or a pointer to structure as the first argument. 58 | 59 | ```go 60 | s := MyStruct{ 61 | FirstField: "first value", 62 | SecondField: 2, 63 | ThirdField: "third value", 64 | } 65 | 66 | var firstFieldKind reflect.String 67 | var secondFieldKind reflect.Int 68 | var err error 69 | 70 | firstFieldKind, err = GetFieldKind(s, "FirstField") 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | secondFieldKind, err = GetFieldKind(s, "SecondField") 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | ``` 80 | 81 | ### `GetFieldType` 82 | 83 | `GetFieldType` returns the string literal of a structure field type. You can use it to operate type assertion over a structure field at runtime. You can provide `GetFieldType` a structure or a pointer to structure as the first argument. 84 | 85 | ```go 86 | s := MyStruct{ 87 | FirstField: "first value", 88 | SecondField: 2, 89 | ThirdField: "third value", 90 | } 91 | 92 | var firstFieldKind string 93 | var secondFieldKind string 94 | var err error 95 | 96 | firstFieldKind, err = GetFieldType(s, "FirstField") 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | secondFieldKind, err = GetFieldType(s, "SecondField") 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | ``` 106 | 107 | ### `GetFieldTag` 108 | 109 | `GetFieldTag` extracts a specific structure field tag. You can provide `GetFieldTag` a structure or a pointer to structure as the first argument. 110 | 111 | ```go 112 | s := MyStruct{} 113 | 114 | tag, err := reflections.GetFieldTag(s, "FirstField", "matched") 115 | if err != nil { 116 | log.Fatal(err) 117 | } 118 | fmt.Println(tag) 119 | 120 | tag, err = reflections.GetFieldTag(s, "ThirdField", "unmatched") 121 | if err != nil { 122 | log.Fatal(err) 123 | } 124 | fmt.Println(tag) 125 | ``` 126 | 127 | ### `HasField` 128 | 129 | `HasField` asserts a field exists through the structure. You can provide `HasField` a struct or a pointer to a struct as the first argument. 130 | 131 | ```go 132 | s := MyStruct { 133 | FirstField: "first value", 134 | SecondField: 2, 135 | ThirdField: "third value", 136 | } 137 | 138 | // has == true 139 | has, _ := reflections.HasField(s, "FirstField") 140 | 141 | // has == false 142 | has, _ := reflections.HasField(s, "FourthField") 143 | ``` 144 | 145 | ### `Fields` 146 | 147 | `Fields` returns the list of structure field names so that you can access or update them later. You can provide `Fields` with a struct or a pointer to a struct as the first argument. 148 | 149 | ```go 150 | s := MyStruct { 151 | FirstField: "first value", 152 | SecondField: 2, 153 | ThirdField: "third value", 154 | } 155 | 156 | var fields []string 157 | 158 | // Fields will list every structure exportable fields. 159 | // Here, it's content would be equal to: 160 | // []string{"FirstField", "SecondField", "ThirdField"} 161 | fields, _ = reflections.Fields(s) 162 | ``` 163 | 164 | ### `Items` 165 | 166 | `Items` returns the structure's field name to the values map. You can provide `Items` with a struct or a pointer to structure as the first argument. 167 | 168 | ```go 169 | s := MyStruct { 170 | FirstField: "first value", 171 | SecondField: 2, 172 | ThirdField: "third value", 173 | } 174 | 175 | var structItems map[string]interface{} 176 | 177 | // Items will return a field name to 178 | // field value map 179 | structItems, _ = reflections.Items(s) 180 | ``` 181 | 182 | ### `Tags` 183 | 184 | `Tags` returns the structure's fields tag with the provided key. You can provide `Tags` with a struct or a pointer to a struct as the first argument. 185 | 186 | ```go 187 | s := MyStruct { 188 | FirstField: "first value", `matched:"first tag"` 189 | SecondField: 2, `matched:"second tag"` 190 | ThirdField: "third value", `unmatched:"third tag"` 191 | } 192 | 193 | var structTags map[string]string 194 | 195 | // Tags will return a field name to tag content 196 | // map. N.B that only field with the tag name 197 | // you've provided will be matched. 198 | // Here structTags will contain: 199 | // { 200 | // "FirstField": "first tag", 201 | // "SecondField": "second tag", 202 | // } 203 | structTags, _ = reflections.Tags(s, "matched") 204 | ``` 205 | 206 | ### `SetField` 207 | 208 | `SetField` updates a structure's field value with the one provided. Note that you can't set un-exported fields and that the field and value types must match. 209 | 210 | ```go 211 | s := MyStruct { 212 | FirstField: "first value", 213 | SecondField: 2, 214 | ThirdField: "third value", 215 | } 216 | 217 | //To be able to set the structure's values, 218 | // it must be passed as a pointer. 219 | _ := reflections.SetField(&s, "FirstField", "new value") 220 | 221 | // If you try to set a field's value using the wrong type, 222 | // an error will be returned 223 | err := reflection.SetField(&s, "FirstField", 123) // err != nil 224 | ``` 225 | 226 | ### `GetFieldNameByTagValue` 227 | 228 | `GetFieldNameByTagValue` looks up a field with a matching `{tagKey}:"{tagValue}"` tag in the provided `obj` item. 229 | If `obj` is not a `struct`, nor a `pointer`, or it does not have a field tagged with the `tagKey`, and the matching `tagValue`, this function returns an error. 230 | 231 | ```go 232 | s := MyStruct { 233 | FirstField: "first value", `matched:"first tag"` 234 | SecondField: 2, `matched:"second tag"` 235 | ThirdField: "third value", `unmatched:"third tag"` 236 | } 237 | 238 | // Getting field name from external source as json would be a headache to convert it manually, 239 | // so we get it directly from struct tag 240 | // returns fieldName = "FirstField" 241 | fieldName, _ = reflections.GetFieldNameByTagValue(s, "matched", "first tag"); 242 | 243 | // later we can do GetField(s, fieldName) 244 | ``` 245 | 246 | 247 | ## Important notes 248 | 249 | - **Un-exported fields** can't be accessed nor set using the `reflections` library. The Go lang standard `reflect` library intentionally prohibits un-exported fields values access or modifications. 250 | 251 | ## Contribute 252 | 253 | - Check for open issues or open a new issue to start a discussion around a feature idea or a bug. 254 | - Fork [the repository](http://github.com/oleiade/reflections) on GitHub to start making your changes to the **master** branch, or branch off of it. 255 | - Write tests showing that the bug was fixed or the feature works as expected. 256 | - Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to [`AUTHORS`](https://github.com/oleiade/reflections/blob/master/AUTHORS.md). 257 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package reflections_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "reflect" 8 | 9 | "github.com/oleiade/reflections" 10 | ) 11 | 12 | type MyStruct struct { 13 | MyEmbeddedStruct 14 | FirstField string `matched:"first tag"` 15 | SecondField int `matched:"second tag"` 16 | ThirdField string `unmatched:"third tag"` 17 | } 18 | 19 | type MyEmbeddedStruct struct { 20 | EmbeddedField string 21 | } 22 | 23 | func ExampleGetField() { 24 | s := MyStruct{ 25 | FirstField: "first value", 26 | SecondField: 2, 27 | ThirdField: "third value", 28 | } 29 | 30 | fieldsToExtract := []string{"FirstField", "ThirdField"} 31 | 32 | for _, fieldName := range fieldsToExtract { 33 | value, err := reflections.GetField(s, fieldName) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | fmt.Println(value) 39 | 40 | // output: 41 | // first value 42 | // third value 43 | } 44 | } 45 | 46 | func ExampleGetFieldKind() { 47 | s := MyStruct{ 48 | FirstField: "first value", 49 | SecondField: 2, 50 | ThirdField: "third value", 51 | } 52 | 53 | var firstFieldKind reflect.Kind 54 | var secondFieldKind reflect.Kind 55 | var err error 56 | 57 | // GetFieldKind will return reflect.String 58 | firstFieldKind, err = reflections.GetFieldKind(s, "FirstField") 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | fmt.Println(firstFieldKind) 63 | 64 | // GetFieldKind will return reflect.Int 65 | secondFieldKind, err = reflections.GetFieldKind(s, "SecondField") 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | fmt.Println(secondFieldKind) 70 | 71 | // output: 72 | // string 73 | // int 74 | } 75 | 76 | func ExampleGetFieldType() { 77 | s := MyStruct{ 78 | FirstField: "first value", 79 | SecondField: 2, 80 | ThirdField: "third value", 81 | } 82 | 83 | var firstFieldType string 84 | var secondFieldType string 85 | var err error 86 | 87 | // GetFieldType will return reflect.String 88 | firstFieldType, err = reflections.GetFieldType(s, "FirstField") 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | fmt.Println(firstFieldType) 93 | 94 | // GetFieldType will return reflect.Int 95 | secondFieldType, err = reflections.GetFieldType(s, "SecondField") 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | fmt.Println(secondFieldType) 100 | 101 | // output: 102 | // string 103 | // int 104 | } 105 | 106 | func ExampleGetFieldTag() { 107 | s := MyStruct{} 108 | 109 | tag, err := reflections.GetFieldTag(s, "FirstField", "matched") 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | fmt.Println(tag) 114 | 115 | tag, err = reflections.GetFieldTag(s, "ThirdField", "unmatched") 116 | if err != nil { 117 | log.Fatal(err) 118 | } 119 | fmt.Println(tag) 120 | 121 | // output: 122 | // first tag 123 | // third tag 124 | } 125 | 126 | func ExampleHasField() { 127 | s := MyStruct{ 128 | FirstField: "first value", 129 | SecondField: 2, 130 | ThirdField: "third value", 131 | } 132 | 133 | // has == true 134 | has, _ := reflections.HasField(s, "FirstField") 135 | fmt.Println(has) 136 | 137 | // has == false 138 | has, _ = reflections.HasField(s, "FourthField") 139 | fmt.Println(has) 140 | 141 | // output: 142 | // true 143 | // false 144 | } 145 | 146 | func ExampleFields() { 147 | s := MyStruct{ 148 | FirstField: "first value", 149 | SecondField: 2, 150 | ThirdField: "third value", 151 | } 152 | 153 | var fields []string 154 | 155 | // Fields will list every structure exportable fields. 156 | // Here, it's content would be equal to: 157 | // []string{"FirstField", "SecondField", "ThirdField"} 158 | fields, _ = reflections.Fields(s) 159 | fmt.Println(fields) 160 | 161 | // output: 162 | // [MyEmbeddedStruct FirstField SecondField ThirdField] 163 | } 164 | 165 | func ExampleItems() { 166 | s := MyStruct{ 167 | FirstField: "first value", 168 | SecondField: 2, 169 | ThirdField: "third value", 170 | } 171 | 172 | var structItems map[string]interface{} 173 | 174 | // Items will return a field name to 175 | // field value map 176 | structItems, _ = reflections.Items(s) 177 | fmt.Println(structItems) 178 | 179 | // output: 180 | // map[FirstField:first value MyEmbeddedStruct:{} SecondField:2 ThirdField:third value] 181 | } 182 | 183 | func ExampleItemsDeep() { 184 | s := MyStruct{ 185 | FirstField: "first value", 186 | SecondField: 2, 187 | ThirdField: "third value", 188 | MyEmbeddedStruct: MyEmbeddedStruct{ 189 | EmbeddedField: "embedded value", 190 | }, 191 | } 192 | 193 | var structItems map[string]interface{} 194 | 195 | // ItemsDeep will return a field name to 196 | // field value map, including fields from 197 | // anonymous embedded structs 198 | structItems, _ = reflections.ItemsDeep(s) 199 | fmt.Println(structItems) 200 | 201 | // output: 202 | // map[EmbeddedField:embedded value FirstField:first value SecondField:2 ThirdField:third value] 203 | } 204 | 205 | func ExampleTags() { 206 | s := MyStruct{ 207 | FirstField: "first value", 208 | SecondField: 2, 209 | ThirdField: "third value", 210 | } 211 | 212 | var structTags map[string]string 213 | 214 | // Tags will return a field name to tag content 215 | // map. Nota that only field with the tag name 216 | // you've provided which will be matched. 217 | // Here structTags will contain: 218 | // { 219 | // "FirstField": "first tag", 220 | // "SecondField": "second tag", 221 | // } 222 | structTags, _ = reflections.Tags(s, "matched") 223 | fmt.Println(structTags) 224 | 225 | // output: 226 | // map[FirstField:first tag MyEmbeddedStruct: SecondField:second tag ThirdField:] 227 | } 228 | 229 | func ExampleSetField() { 230 | s := MyStruct{ 231 | FirstField: "first value", 232 | SecondField: 2, 233 | ThirdField: "third value", 234 | } 235 | 236 | // In order to be able to set the structure's values, 237 | // a pointer to it has to be passed to it. 238 | err := reflections.SetField(&s, "FirstField", "new value") 239 | if err != nil { 240 | log.Fatal(err) 241 | } 242 | 243 | // Note that if you try to set a field's value using the wrong type, 244 | // an error will be returned 245 | _ = reflections.SetField(&s, "FirstField", 123) // err != nil 246 | 247 | // output: 248 | } 249 | 250 | func ExampleGetFieldNameByTagValue() { 251 | type Order struct { 252 | Step string `json:"order_step"` 253 | ID string `json:"id"` 254 | Category string `json:"category"` 255 | } 256 | type Condition struct { 257 | Field string `json:"field"` 258 | Value string `json:"value"` 259 | Next string `json:"next"` 260 | } 261 | 262 | // JSON data from external source 263 | orderJSON := `{ 264 | "order_step": "cooking", 265 | "id": "45457-fv54f54", 266 | "category": "Pizzas" 267 | }` 268 | 269 | conditionJSON := `{ 270 | "field": "order_step", 271 | "value": "cooking", 272 | "next": "serve" 273 | }` 274 | 275 | // Storing JSON in corresponding Variables 276 | var order Order 277 | err := json.Unmarshal([]byte(orderJSON), &order) 278 | if err != nil { 279 | log.Fatal(err) 280 | } 281 | 282 | var condition Condition 283 | err = json.Unmarshal([]byte(conditionJSON), &condition) 284 | if err != nil { 285 | log.Fatal(err) 286 | } 287 | 288 | fieldName, _ := reflections.GetFieldNameByTagValue(order, "json", condition.Field) 289 | fmt.Println(fieldName) 290 | fieldValue, _ := reflections.GetField(order, fieldName) 291 | fmt.Println(fieldValue) 292 | 293 | // Output: 294 | // Step 295 | // cooking 296 | } 297 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/oleiade/reflections 2 | 3 | go 1.22.6 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /reflections.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2013 Théo Crevon 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | // Package reflections provides high level abstractions over the Go standard [reflect] library. 6 | // 7 | // In practice, the `reflect` library's API proves somewhat low-level and un-intuitive. 8 | // Using it can turn out pretty complex, daunting, and scary, when doing simple 9 | // things like accessing a structure field value, a field tag, etc. 10 | // 11 | // The `reflections` package aims to make developers' life easier when it comes to introspect 12 | // struct values at runtime. Its API takes inspiration in the python language's `getattr,` `setattr,` and `hasattr` set 13 | // of methods and provides simplified access to structure fields and tags. 14 | // 15 | // [reflect]: http://golang.org/pkg/reflect/ 16 | package reflections 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "reflect" 22 | ) 23 | 24 | // ErrUnsupportedType indicates that the provided type doesn't support the requested reflection operation. 25 | var ErrUnsupportedType = errors.New("unsupported type") 26 | 27 | // ErrUnexportedField indicates that an operation failed as a result of 28 | // applying to a non-exported struct field. 29 | var ErrUnexportedField = errors.New("unexported field") 30 | 31 | // GetField returns the value of the provided obj field. 32 | // The `obj` can either be a structure or pointer to structure. 33 | func GetField(obj interface{}, name string) (interface{}, error) { 34 | if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { 35 | return nil, fmt.Errorf("cannot use GetField on a non-struct object: %w", ErrUnsupportedType) 36 | } 37 | 38 | objValue := reflectValue(obj) 39 | field := objValue.FieldByName(name) 40 | if !field.IsValid() { 41 | return nil, fmt.Errorf("no such field: %s in obj", name) 42 | } 43 | 44 | return field.Interface(), nil 45 | } 46 | 47 | // GetFieldKind returns the kind of the provided obj field. 48 | // The `obj` can either be a structure or pointer to structure. 49 | func GetFieldKind(obj interface{}, name string) (reflect.Kind, error) { 50 | if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { 51 | return reflect.Invalid, fmt.Errorf("cannot use GetFieldKind on a non-struct interface: %w", ErrUnsupportedType) 52 | } 53 | 54 | objValue := reflectValue(obj) 55 | field := objValue.FieldByName(name) 56 | 57 | if !field.IsValid() { 58 | return reflect.Invalid, fmt.Errorf("no such field: %s in obj", name) 59 | } 60 | 61 | return field.Type().Kind(), nil 62 | } 63 | 64 | // GetFieldType returns the kind of the provided obj field. 65 | // The `obj` can either be a structure or pointer to structure. 66 | func GetFieldType(obj interface{}, name string) (string, error) { 67 | if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { 68 | return "", fmt.Errorf("cannot use GetFieldType on a non-struct interface: %w", ErrUnsupportedType) 69 | } 70 | 71 | objValue := reflectValue(obj) 72 | field := objValue.FieldByName(name) 73 | 74 | if !field.IsValid() { 75 | return "", fmt.Errorf("no such field: %s in obj", name) 76 | } 77 | 78 | return field.Type().String(), nil 79 | } 80 | 81 | // GetFieldTag returns the provided obj field tag value. 82 | // The `obj` parameter can either be a structure or pointer to structure. 83 | func GetFieldTag(obj interface{}, fieldName, tagKey string) (string, error) { 84 | if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { 85 | return "", fmt.Errorf("cannot use GetFieldTag on a non-struct interface: %w", ErrUnsupportedType) 86 | } 87 | 88 | objValue := reflectValue(obj) 89 | objType := objValue.Type() 90 | 91 | field, ok := objType.FieldByName(fieldName) 92 | if !ok { 93 | return "", fmt.Errorf("no such field: %s in obj", fieldName) 94 | } 95 | 96 | if !isExportableField(field) { 97 | return "", fmt.Errorf("cannot GetFieldTag on a non-exported struct field: %w", ErrUnexportedField) 98 | } 99 | 100 | return field.Tag.Get(tagKey), nil 101 | } 102 | 103 | // GetFieldNameByTagValue looks up a field with a matching `{tagKey}:"{tagValue}"` tag in the provided `obj` item. 104 | // The `obj` parameter must be a `struct`, or a `pointer` to one. If the `obj` parameter doesn't have a field tagged 105 | // with the `tagKey`, and the matching `tagValue`, this function returns an error. 106 | func GetFieldNameByTagValue(obj interface{}, tagKey, tagValue string) (string, error) { 107 | if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { 108 | return "", fmt.Errorf("cannot use GetFieldByTag on a non-struct interface: %w", ErrUnsupportedType) 109 | } 110 | 111 | objValue := reflectValue(obj) 112 | objType := objValue.Type() 113 | fieldsCount := objType.NumField() 114 | 115 | for i := range fieldsCount { 116 | structField := objType.Field(i) 117 | if structField.Tag.Get(tagKey) == tagValue { 118 | return structField.Name, nil 119 | } 120 | } 121 | 122 | return "", errors.New("tag doesn't exist in the given struct") 123 | } 124 | 125 | // SetField sets the provided obj field with provided value. 126 | // 127 | // The `obj` parameter must be a pointer to a struct, otherwise it soundly fails. 128 | // The provided `value` type should match with the struct field being set. 129 | func SetField(obj interface{}, name string, value interface{}) error { 130 | // Fetch the field reflect.Value 131 | structValue := reflect.ValueOf(obj).Elem() 132 | structFieldValue := structValue.FieldByName(name) 133 | 134 | if !structFieldValue.IsValid() { 135 | return fmt.Errorf("no such field: %s in obj", name) 136 | } 137 | 138 | if !structFieldValue.CanSet() { 139 | return fmt.Errorf("cannot set %s field value", name) 140 | } 141 | 142 | structFieldType := structFieldValue.Type() 143 | val := reflect.ValueOf(value) 144 | if !val.Type().AssignableTo(structFieldType) { 145 | invalidTypeError := errors.New("provided value type not assignable to obj field type") 146 | return invalidTypeError 147 | } 148 | 149 | structFieldValue.Set(val) 150 | return nil 151 | } 152 | 153 | // HasField checks if the provided `obj` struct has field named `name`. 154 | // The `obj` can either be a structure or pointer to structure. 155 | func HasField(obj interface{}, name string) (bool, error) { 156 | if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { 157 | return false, fmt.Errorf("cannot use HasField on a non-struct interface: %w", ErrUnsupportedType) 158 | } 159 | 160 | objValue := reflectValue(obj) 161 | objType := objValue.Type() 162 | field, ok := objType.FieldByName(name) 163 | if !ok || !isExportableField(field) { 164 | return false, nil 165 | } 166 | 167 | return true, nil 168 | } 169 | 170 | // Fields returns the struct fields names list. 171 | // The `obj` parameter can either be a structure or pointer to structure. 172 | func Fields(obj interface{}) ([]string, error) { 173 | return fields(obj, false) 174 | } 175 | 176 | // FieldsDeep returns "flattened" fields. 177 | // 178 | // Note that FieldsDeep treats fields from anonymous inner structs as normal fields. 179 | func FieldsDeep(obj interface{}) ([]string, error) { 180 | return fields(obj, true) 181 | } 182 | 183 | func fields(obj interface{}, deep bool) ([]string, error) { 184 | if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { 185 | return nil, fmt.Errorf("cannot use fields on a non-struct interface: %w", ErrUnsupportedType) 186 | } 187 | 188 | objValue := reflectValue(obj) 189 | objType := objValue.Type() 190 | fieldsCount := objType.NumField() 191 | 192 | var allFields []string 193 | for i := range fieldsCount { 194 | field := objType.Field(i) 195 | if isExportableField(field) { 196 | if !deep || !field.Anonymous { 197 | allFields = append(allFields, field.Name) 198 | continue 199 | } 200 | 201 | fieldValue := objValue.Field(i) 202 | subFields, err := fields(fieldValue.Interface(), deep) 203 | if err != nil { 204 | return nil, fmt.Errorf("cannot get fields in %s: %w", field.Name, err) 205 | } 206 | allFields = append(allFields, subFields...) 207 | } 208 | } 209 | 210 | return allFields, nil 211 | } 212 | 213 | // Items returns the field:value struct pairs as a map. 214 | // The `obj` parameter can either be a structure or pointer to structure. 215 | func Items(obj interface{}) (map[string]interface{}, error) { 216 | return items(obj, false) 217 | } 218 | 219 | // ItemsDeep returns "flattened" items. 220 | // Note that ItemsDeep will treat fields from anonymous inner structs as normal fields. 221 | func ItemsDeep(obj interface{}) (map[string]interface{}, error) { 222 | return items(obj, true) 223 | } 224 | 225 | func items(obj interface{}, deep bool) (map[string]interface{}, error) { 226 | if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { 227 | return nil, fmt.Errorf("cannot use items on a non-struct interface: %w", ErrUnsupportedType) 228 | } 229 | 230 | objValue := reflectValue(obj) 231 | objType := objValue.Type() 232 | fieldsCount := objType.NumField() 233 | 234 | allItems := make(map[string]interface{}) 235 | 236 | for i := range fieldsCount { 237 | field := objType.Field(i) 238 | fieldValue := objValue.Field(i) 239 | 240 | if isExportableField(field) { 241 | if !deep || !field.Anonymous { 242 | allItems[field.Name] = fieldValue.Interface() 243 | continue 244 | } 245 | 246 | m, err := items(fieldValue.Interface(), deep) 247 | if err != nil { 248 | return nil, fmt.Errorf("cannot get items in %s: %w", field.Name, err) 249 | } 250 | 251 | for k, v := range m { 252 | allItems[k] = v 253 | } 254 | } 255 | } 256 | 257 | return allItems, nil 258 | } 259 | 260 | // Tags lists the struct tag fields. 261 | // The `obj` can whether be a structure or pointer to structure. 262 | func Tags(obj interface{}, key string) (map[string]string, error) { 263 | return tags(obj, key, false) 264 | } 265 | 266 | // TagsDeep returns "flattened" tags. 267 | // Note that TagsDeep treats fields from anonymous 268 | // inner structs as normal fields. 269 | func TagsDeep(obj interface{}, key string) (map[string]string, error) { 270 | return tags(obj, key, true) 271 | } 272 | 273 | func tags(obj interface{}, key string, deep bool) (map[string]string, error) { 274 | if !isSupportedType(obj, []reflect.Kind{reflect.Struct, reflect.Ptr}) { 275 | return nil, fmt.Errorf("cannot use tags on a non-struct interface: %w", ErrUnsupportedType) 276 | } 277 | 278 | objValue := reflectValue(obj) 279 | objType := objValue.Type() 280 | fieldsCount := objType.NumField() 281 | 282 | allTags := make(map[string]string) 283 | 284 | for i := range fieldsCount { 285 | structField := objType.Field(i) 286 | if isExportableField(structField) { 287 | if !deep || !structField.Anonymous { 288 | allTags[structField.Name] = structField.Tag.Get(key) 289 | continue 290 | } 291 | 292 | fieldValue := objValue.Field(i) 293 | m, err := tags(fieldValue.Interface(), key, deep) 294 | if err != nil { 295 | return nil, fmt.Errorf("cannot get items in %s: %w", structField.Name, err) 296 | } 297 | 298 | for k, v := range m { 299 | allTags[k] = v 300 | } 301 | } 302 | } 303 | 304 | return allTags, nil 305 | } 306 | 307 | func reflectValue(obj interface{}) reflect.Value { 308 | var val reflect.Value 309 | 310 | if reflect.TypeOf(obj).Kind() == reflect.Ptr { 311 | val = reflect.ValueOf(obj).Elem() 312 | } else { 313 | val = reflect.ValueOf(obj) 314 | } 315 | 316 | return val 317 | } 318 | 319 | func isExportableField(field reflect.StructField) bool { 320 | // PkgPath is empty for exported fields. 321 | return field.PkgPath == "" 322 | } 323 | 324 | func isSupportedType(obj interface{}, types []reflect.Kind) bool { 325 | for _, t := range types { 326 | if reflect.TypeOf(obj).Kind() == t { 327 | return true 328 | } 329 | } 330 | 331 | return false 332 | } 333 | -------------------------------------------------------------------------------- /reflections_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Théo Crevon 2 | // 3 | // See the file LICENSE for copying permission. 4 | 5 | package reflections 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | type TestStruct struct { 16 | Dummy string `test:"dummytag"` 17 | unexported uint64 18 | Yummy int `test:"yummytag"` 19 | } 20 | 21 | func TestGetField_on_struct(t *testing.T) { 22 | t.Parallel() 23 | 24 | dummyStruct := TestStruct{ 25 | Dummy: "test", 26 | } 27 | 28 | value, err := GetField(dummyStruct, "Dummy") 29 | require.NoError(t, err) 30 | assert.Equal(t, "test", value) 31 | } 32 | 33 | func TestGetField_on_struct_pointer(t *testing.T) { 34 | t.Parallel() 35 | 36 | dummyStruct := &TestStruct{ 37 | Dummy: "test", 38 | } 39 | 40 | value, err := GetField(dummyStruct, "Dummy") 41 | require.NoError(t, err) 42 | assert.Equal(t, "test", value) 43 | } 44 | 45 | func TestGetField_on_non_struct(t *testing.T) { 46 | t.Parallel() 47 | 48 | dummy := "abc 123" 49 | 50 | _, err := GetField(dummy, "Dummy") 51 | assert.Error(t, err) 52 | } 53 | 54 | func TestGetField_non_existing_field(t *testing.T) { 55 | t.Parallel() 56 | 57 | dummyStruct := TestStruct{ 58 | Dummy: "test", 59 | } 60 | 61 | _, err := GetField(dummyStruct, "obladioblada") 62 | assert.Error(t, err) 63 | } 64 | 65 | func TestGetField_unexported_field(t *testing.T) { 66 | t.Parallel() 67 | 68 | dummyStruct := TestStruct{ 69 | unexported: 12345, 70 | Dummy: "test", 71 | } 72 | 73 | assert.Panics(t, func() { 74 | GetField(dummyStruct, "unexported") //nolint:errcheck,gosec 75 | }) 76 | } 77 | 78 | func TestGetFieldKind_on_struct(t *testing.T) { 79 | t.Parallel() 80 | 81 | dummyStruct := TestStruct{ 82 | Dummy: "test", 83 | Yummy: 123, 84 | } 85 | 86 | kind, err := GetFieldKind(dummyStruct, "Dummy") 87 | require.NoError(t, err) 88 | assert.Equal(t, reflect.String, kind) 89 | 90 | kind, err = GetFieldKind(dummyStruct, "Yummy") 91 | require.NoError(t, err) 92 | assert.Equal(t, reflect.Int, kind) 93 | } 94 | 95 | func TestGetFieldKind_on_struct_pointer(t *testing.T) { 96 | t.Parallel() 97 | 98 | dummyStruct := &TestStruct{ 99 | Dummy: "test", 100 | Yummy: 123, 101 | } 102 | 103 | kind, err := GetFieldKind(dummyStruct, "Dummy") 104 | require.NoError(t, err) 105 | assert.Equal(t, reflect.String, kind) 106 | 107 | kind, err = GetFieldKind(dummyStruct, "Yummy") 108 | require.NoError(t, err) 109 | assert.Equal(t, reflect.Int, kind) 110 | } 111 | 112 | func TestGetFieldKind_on_non_struct(t *testing.T) { 113 | t.Parallel() 114 | 115 | dummy := "abc 123" 116 | 117 | _, err := GetFieldKind(dummy, "Dummy") 118 | assert.Error(t, err) 119 | } 120 | 121 | func TestGetFieldKind_non_existing_field(t *testing.T) { 122 | t.Parallel() 123 | 124 | dummyStruct := TestStruct{ 125 | Dummy: "test", 126 | Yummy: 123, 127 | } 128 | 129 | _, err := GetFieldKind(dummyStruct, "obladioblada") 130 | assert.Error(t, err) 131 | } 132 | 133 | func TestGetFieldType_on_struct(t *testing.T) { 134 | t.Parallel() 135 | 136 | dummyStruct := TestStruct{ 137 | Dummy: "test", 138 | Yummy: 123, 139 | } 140 | 141 | typeString, err := GetFieldType(dummyStruct, "Dummy") 142 | require.NoError(t, err) 143 | assert.Equal(t, "string", typeString) 144 | 145 | typeString, err = GetFieldType(dummyStruct, "Yummy") 146 | require.NoError(t, err) 147 | assert.Equal(t, "int", typeString) 148 | } 149 | 150 | func TestGetFieldType_on_struct_pointer(t *testing.T) { 151 | t.Parallel() 152 | 153 | dummyStruct := &TestStruct{ 154 | Dummy: "test", 155 | Yummy: 123, 156 | } 157 | 158 | typeString, err := GetFieldType(dummyStruct, "Dummy") 159 | require.NoError(t, err) 160 | assert.Equal(t, "string", typeString) 161 | 162 | typeString, err = GetFieldType(dummyStruct, "Yummy") 163 | require.NoError(t, err) 164 | assert.Equal(t, "int", typeString) 165 | } 166 | 167 | func TestGetFieldType_on_non_struct(t *testing.T) { 168 | t.Parallel() 169 | 170 | dummy := "abc 123" 171 | 172 | _, err := GetFieldType(dummy, "Dummy") 173 | assert.Error(t, err) 174 | } 175 | 176 | func TestGetFieldType_non_existing_field(t *testing.T) { 177 | t.Parallel() 178 | 179 | dummyStruct := TestStruct{ 180 | Dummy: "test", 181 | Yummy: 123, 182 | } 183 | 184 | _, err := GetFieldType(dummyStruct, "obladioblada") 185 | assert.Error(t, err) 186 | } 187 | 188 | func TestGetFieldTag_on_struct(t *testing.T) { 189 | t.Parallel() 190 | 191 | dummyStruct := TestStruct{} 192 | 193 | tag, err := GetFieldTag(dummyStruct, "Dummy", "test") 194 | require.NoError(t, err) 195 | assert.Equal(t, "dummytag", tag) 196 | 197 | tag, err = GetFieldTag(dummyStruct, "Yummy", "test") 198 | require.NoError(t, err) 199 | assert.Equal(t, "yummytag", tag) 200 | } 201 | 202 | func TestGetFieldTag_on_struct_pointer(t *testing.T) { 203 | t.Parallel() 204 | 205 | dummyStruct := &TestStruct{} 206 | 207 | tag, err := GetFieldTag(dummyStruct, "Dummy", "test") 208 | require.NoError(t, err) 209 | assert.Equal(t, "dummytag", tag) 210 | 211 | tag, err = GetFieldTag(dummyStruct, "Yummy", "test") 212 | require.NoError(t, err) 213 | assert.Equal(t, "yummytag", tag) 214 | } 215 | 216 | func TestGetFieldTag_on_non_struct(t *testing.T) { 217 | t.Parallel() 218 | 219 | dummy := "abc 123" 220 | 221 | _, err := GetFieldTag(dummy, "Dummy", "test") 222 | assert.Error(t, err) 223 | } 224 | 225 | func TestGetFieldTag_non_existing_field(t *testing.T) { 226 | t.Parallel() 227 | 228 | dummyStruct := TestStruct{} 229 | 230 | _, err := GetFieldTag(dummyStruct, "obladioblada", "test") 231 | assert.Error(t, err) 232 | } 233 | 234 | func TestGetFieldTag_unexported_field(t *testing.T) { 235 | t.Parallel() 236 | 237 | dummyStruct := TestStruct{ 238 | unexported: 12345, 239 | Dummy: "test", 240 | } 241 | 242 | _, err := GetFieldTag(dummyStruct, "unexported", "test") 243 | assert.Error(t, err) 244 | } 245 | 246 | func TestSetField_on_struct_with_valid_value_type(t *testing.T) { 247 | t.Parallel() 248 | 249 | dummyStruct := TestStruct{ 250 | Dummy: "test", 251 | } 252 | 253 | err := SetField(&dummyStruct, "Dummy", "abc") 254 | require.NoError(t, err) 255 | assert.Equal(t, "abc", dummyStruct.Dummy) 256 | } 257 | 258 | func TestSetField_non_existing_field(t *testing.T) { 259 | t.Parallel() 260 | 261 | dummyStruct := TestStruct{ 262 | Dummy: "test", 263 | } 264 | 265 | err := SetField(&dummyStruct, "obladioblada", "life goes on") 266 | assert.Error(t, err) 267 | } 268 | 269 | func TestSetField_invalid_value_type(t *testing.T) { 270 | t.Parallel() 271 | 272 | dummyStruct := TestStruct{ 273 | Dummy: "test", 274 | } 275 | 276 | err := SetField(&dummyStruct, "Yummy", "123") 277 | assert.Error(t, err) 278 | } 279 | 280 | func TestSetField_non_exported_field(t *testing.T) { 281 | t.Parallel() 282 | 283 | dummyStruct := TestStruct{ 284 | Dummy: "test", 285 | } 286 | 287 | assert.Error(t, SetField(&dummyStruct, "unexported", "fail, bitch")) 288 | } 289 | 290 | func TestFields_on_struct(t *testing.T) { 291 | t.Parallel() 292 | 293 | dummyStruct := TestStruct{ 294 | Dummy: "test", 295 | Yummy: 123, 296 | } 297 | 298 | fields, err := Fields(dummyStruct) 299 | require.NoError(t, err) 300 | assert.Equal(t, []string{"Dummy", "Yummy"}, fields) 301 | } 302 | 303 | func TestFields_on_struct_pointer(t *testing.T) { 304 | t.Parallel() 305 | 306 | dummyStruct := &TestStruct{ 307 | Dummy: "test", 308 | Yummy: 123, 309 | } 310 | 311 | fields, err := Fields(dummyStruct) 312 | require.NoError(t, err) 313 | assert.Equal(t, []string{"Dummy", "Yummy"}, fields) 314 | } 315 | 316 | func TestFields_on_non_struct(t *testing.T) { 317 | t.Parallel() 318 | 319 | dummy := "abc 123" 320 | 321 | _, err := Fields(dummy) 322 | assert.Error(t, err) 323 | } 324 | 325 | func TestFields_with_non_exported_fields(t *testing.T) { 326 | t.Parallel() 327 | 328 | dummyStruct := TestStruct{ 329 | unexported: 6789, 330 | Dummy: "test", 331 | Yummy: 123, 332 | } 333 | 334 | fields, err := Fields(dummyStruct) 335 | require.NoError(t, err) 336 | assert.Equal(t, []string{"Dummy", "Yummy"}, fields) 337 | } 338 | 339 | func TestHasField_on_struct_with_existing_field(t *testing.T) { 340 | t.Parallel() 341 | 342 | dummyStruct := TestStruct{ 343 | Dummy: "test", 344 | Yummy: 123, 345 | } 346 | 347 | has, err := HasField(dummyStruct, "Dummy") 348 | require.NoError(t, err) 349 | assert.True(t, has) 350 | } 351 | 352 | func TestHasField_on_struct_pointer_with_existing_field(t *testing.T) { 353 | t.Parallel() 354 | 355 | dummyStruct := &TestStruct{ 356 | Dummy: "test", 357 | Yummy: 123, 358 | } 359 | 360 | has, err := HasField(dummyStruct, "Dummy") 361 | require.NoError(t, err) 362 | assert.True(t, has) 363 | } 364 | 365 | func TestHasField_non_existing_field(t *testing.T) { 366 | t.Parallel() 367 | 368 | dummyStruct := TestStruct{ 369 | Dummy: "test", 370 | Yummy: 123, 371 | } 372 | 373 | has, err := HasField(dummyStruct, "Test") 374 | require.NoError(t, err) 375 | assert.False(t, has) 376 | } 377 | 378 | func TestHasField_on_non_struct(t *testing.T) { 379 | t.Parallel() 380 | 381 | dummy := "abc 123" 382 | 383 | _, err := HasField(dummy, "Test") 384 | assert.Error(t, err) 385 | } 386 | 387 | func TestHasField_unexported_field(t *testing.T) { 388 | t.Parallel() 389 | 390 | dummyStruct := TestStruct{ 391 | unexported: 7890, 392 | Dummy: "test", 393 | Yummy: 123, 394 | } 395 | 396 | has, err := HasField(dummyStruct, "unexported") 397 | require.NoError(t, err) 398 | assert.False(t, has) 399 | } 400 | 401 | func TestTags_on_struct(t *testing.T) { 402 | t.Parallel() 403 | 404 | dummyStruct := TestStruct{ 405 | Dummy: "test", 406 | Yummy: 123, 407 | } 408 | 409 | tags, err := Tags(dummyStruct, "test") 410 | require.NoError(t, err) 411 | assert.Equal(t, map[string]string{ 412 | "Dummy": "dummytag", 413 | "Yummy": "yummytag", 414 | }, tags) 415 | } 416 | 417 | func TestTags_on_struct_pointer(t *testing.T) { 418 | t.Parallel() 419 | 420 | dummyStruct := &TestStruct{ 421 | Dummy: "test", 422 | Yummy: 123, 423 | } 424 | 425 | tags, err := Tags(dummyStruct, "test") 426 | require.NoError(t, err) 427 | assert.Equal(t, map[string]string{ 428 | "Dummy": "dummytag", 429 | "Yummy": "yummytag", 430 | }, tags) 431 | } 432 | 433 | func TestTags_on_non_struct(t *testing.T) { 434 | t.Parallel() 435 | 436 | dummy := "abc 123" 437 | 438 | _, err := Tags(dummy, "test") 439 | assert.Error(t, err) 440 | } 441 | 442 | func TestItems_on_struct(t *testing.T) { 443 | t.Parallel() 444 | 445 | dummyStruct := TestStruct{ 446 | Dummy: "test", 447 | Yummy: 123, 448 | } 449 | 450 | tags, err := Items(dummyStruct) 451 | require.NoError(t, err) 452 | assert.Equal(t, map[string]interface{}{ 453 | "Dummy": "test", 454 | "Yummy": 123, 455 | }, tags) 456 | } 457 | 458 | func TestItems_on_struct_pointer(t *testing.T) { 459 | t.Parallel() 460 | 461 | dummyStruct := &TestStruct{ 462 | Dummy: "test", 463 | Yummy: 123, 464 | } 465 | 466 | tags, err := Items(dummyStruct) 467 | require.NoError(t, err) 468 | assert.Equal(t, map[string]interface{}{ 469 | "Dummy": "test", 470 | "Yummy": 123, 471 | }, tags) 472 | } 473 | 474 | func TestItems_on_non_struct(t *testing.T) { 475 | t.Parallel() 476 | 477 | dummy := "abc 123" 478 | 479 | _, err := Items(dummy) 480 | assert.Error(t, err) 481 | } 482 | 483 | //nolint:unused 484 | func TestItems_deep(t *testing.T) { 485 | t.Parallel() 486 | 487 | type Address struct { 488 | Street string `tag:"be"` 489 | Number int `tag:"bi"` 490 | } 491 | 492 | type unexportedStruct struct{} 493 | 494 | type Person struct { 495 | Name string `tag:"bu"` 496 | Address 497 | unexportedStruct 498 | } 499 | 500 | p := Person{} 501 | p.Name = "John" 502 | p.Street = "Decumanus maximus" 503 | p.Number = 17 504 | 505 | items, err := Items(p) 506 | require.NoError(t, err) 507 | itemsDeep, err := ItemsDeep(p) 508 | require.NoError(t, err) 509 | 510 | assert.Len(t, items, 2) 511 | assert.Len(t, itemsDeep, 3) 512 | assert.Equal(t, "John", itemsDeep["Name"]) 513 | assert.Equal(t, "Decumanus maximus", itemsDeep["Street"]) 514 | assert.Equal(t, 17, itemsDeep["Number"]) 515 | } 516 | 517 | func TestGetFieldNameByTagValue(t *testing.T) { 518 | t.Parallel() 519 | 520 | dummyStruct := TestStruct{ 521 | Dummy: "dummy", 522 | Yummy: 123, 523 | } 524 | 525 | tagJSON := "dummytag" 526 | field, err := GetFieldNameByTagValue(dummyStruct, "test", tagJSON) 527 | 528 | require.NoError(t, err) 529 | assert.Equal(t, "Dummy", field) 530 | } 531 | 532 | func TestGetFieldNameByTagValue_on_non_existing_tag(t *testing.T) { 533 | t.Parallel() 534 | 535 | dummyStruct := TestStruct{ 536 | Dummy: "dummy", 537 | Yummy: 123, 538 | } 539 | 540 | // non existing tag value with an existing tag key 541 | tagJSON := "tag" 542 | _, errTagValue := GetFieldNameByTagValue(dummyStruct, "test", tagJSON) 543 | require.Error(t, errTagValue) 544 | 545 | // non existing tag key with an existing tag value 546 | tagJSON = "dummytag" 547 | _, errTagKey := GetFieldNameByTagValue(dummyStruct, "json", tagJSON) 548 | require.Error(t, errTagKey) 549 | 550 | // non existing tag key and value 551 | tagJSON = "tag" 552 | _, errTagKeyValue := GetFieldNameByTagValue(dummyStruct, "json", tagJSON) 553 | require.Error(t, errTagKeyValue) 554 | } 555 | 556 | //nolint:unused 557 | func TestTags_deep(t *testing.T) { 558 | t.Parallel() 559 | 560 | type Address struct { 561 | Street string `tag:"be"` 562 | Number int `tag:"bi"` 563 | } 564 | 565 | type unexportedStruct struct{} 566 | 567 | type Person struct { 568 | Name string `tag:"bu"` 569 | Address 570 | unexportedStruct 571 | } 572 | 573 | p := Person{} 574 | p.Name = "John" 575 | p.Street = "Decumanus maximus" 576 | p.Number = 17 577 | 578 | tags, err := Tags(p, "tag") 579 | require.NoError(t, err) 580 | tagsDeep, err := TagsDeep(p, "tag") 581 | require.NoError(t, err) 582 | 583 | assert.Len(t, tags, 2) 584 | assert.Len(t, tagsDeep, 3) 585 | assert.Equal(t, "bu", tagsDeep["Name"]) 586 | assert.Equal(t, "be", tagsDeep["Street"]) 587 | assert.Equal(t, "bi", tagsDeep["Number"]) 588 | } 589 | 590 | //nolint:unused 591 | func TestFields_deep(t *testing.T) { 592 | t.Parallel() 593 | 594 | type Address struct { 595 | Street string `tag:"be"` 596 | Number int `tag:"bi"` 597 | } 598 | 599 | type unexportedStruct struct{} 600 | 601 | type Person struct { 602 | Name string `tag:"bu"` 603 | Address 604 | unexportedStruct 605 | } 606 | 607 | p := Person{} 608 | p.Name = "John" 609 | p.Street = "street?" 610 | p.Number = 17 611 | 612 | fields, err := Fields(p) 613 | require.NoError(t, err) 614 | fieldsDeep, err := FieldsDeep(p) 615 | require.NoError(t, err) 616 | 617 | assert.Len(t, fields, 2) 618 | assert.Len(t, fieldsDeep, 3) 619 | assert.Equal(t, "Name", fieldsDeep[0]) 620 | assert.Equal(t, "Street", fieldsDeep[1]) 621 | assert.Equal(t, "Number", fieldsDeep[2]) 622 | } 623 | 624 | type SingleString string 625 | 626 | type StringList []string 627 | 628 | type Bar struct { 629 | A StringList 630 | } 631 | 632 | func TestAssignable(t *testing.T) { 633 | t.Parallel() 634 | 635 | var b Bar 636 | expected := []string{"a", "b", "c"} 637 | require.NoError(t, SetField(&b, "A", expected)) 638 | assert.Equal(t, StringList(expected), b.A) 639 | 640 | err := SetField(&b, "A", []int{0, 1, 2}) 641 | require.Error(t, err) 642 | assert.Equal(t, "provided value type not assignable to obj field type", 643 | err.Error()) 644 | } 645 | --------------------------------------------------------------------------------